Merge pull request #1218 from dpalou/MOBILE-2302

Mobile 2302
main
Juan Leyva 2018-01-11 09:31:32 +01:00 committed by GitHub
commit 2fe496aaa9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
243 changed files with 7921 additions and 293 deletions

View File

@ -12,10 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { Component, ViewChild, AfterViewInit } from '@angular/core';
import { Platform, Nav } from 'ionic-angular';
import { Component, OnInit } from '@angular/core';
import { Platform } from 'ionic-angular';
import { StatusBar } from '@ionic-native/status-bar';
import { SplashScreen } from '@ionic-native/splash-screen';
import { CoreAppProvider } from '../providers/app';
import { CoreEventsProvider } from '../providers/events';
import { CoreLoggerProvider } from '../providers/logger';
import { CoreLoginHelperProvider } from '../core/login/providers/helper';
@ -23,8 +24,7 @@ import { CoreLoginHelperProvider } from '../core/login/providers/helper';
@Component({
templateUrl: 'app.html'
})
export class MyApp implements AfterViewInit {
@ViewChild(Nav) navCtrl;
export class MoodleMobileApp implements OnInit {
// Use the page name (string) because the page is lazy loaded (Ionic feature). That way we can load pages without
// having to import them. The downside is that each page needs to implement a ngModule.
rootPage:any = 'CoreLoginInitPage';
@ -32,7 +32,8 @@ export class MyApp implements AfterViewInit {
protected lastUrls = {};
constructor(private platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen, logger: CoreLoggerProvider,
private eventsProvider: CoreEventsProvider, private loginHelper: CoreLoginHelperProvider) {
private eventsProvider: CoreEventsProvider, private loginHelper: CoreLoginHelperProvider,
private appProvider: CoreAppProvider) {
this.logger = logger.getInstance('AppComponent');
platform.ready().then(() => {
@ -45,14 +46,12 @@ export class MyApp implements AfterViewInit {
}
/**
* View has been initialized.
* Component being initialized.
*/
ngAfterViewInit() {
this.loginHelper.setNavCtrl(this.navCtrl);
ngOnInit() {
// Go to sites page when user is logged out.
this.eventsProvider.on(CoreEventsProvider.LOGOUT, () => {
this.navCtrl.setRoot('CoreLoginSitesPage');
this.appProvider.getRootNavController().setRoot('CoreLoginSitesPage');
});
// Listen for session expired events.

View File

@ -8,9 +8,25 @@
color: color($colors, primary, base);
}
.col[align-self-stretch] .card-ios {
height: calc(100% - #{($card-ios-margin-end + $card-ios-margin-start)});
}
// Top tabs
// -------------------------
.ios .core-top-tabbar {
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
> a {
font-size: 1.6rem;
}
}
// Highlights inside the input element.
@if ($mm-text-input-ios-show-highlight) {
@if ($core-text-input-ios-show-highlight) {
.card-ios, .list-ios {
// In order to get a 2px border we need to add an inset
// box-shadow 1px (this is to avoid the div resizing)

View File

@ -8,8 +8,12 @@
color: color($colors, primary, base);
}
.col[align-self-stretch] .card-md {
height: calc(100% - #{($card-md-margin-end + $card-md-margin-start)});
}
// Highlights inside the input element.
@if ($mm-text-input-md-show-highlight) {
@if ($core-text-input-md-show-highlight) {
.card-md, .list-md {
// In order to get a 2px border we need to add an inset
// box-shadow 1px (this is to avoid the div resizing)

View File

@ -18,20 +18,15 @@ import { IonicApp, IonicModule, Platform } from 'ionic-angular';
import { HttpModule } from '@angular/http';
import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { SplashScreen } from '@ionic-native/splash-screen';
import { StatusBar } from '@ionic-native/status-bar';
import { SQLite } from '@ionic-native/sqlite';
import { Keyboard } from '@ionic-native/keyboard';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { MyApp } from './app.component';
import { MoodleMobileApp } from './app.component';
import { CoreInterceptor } from '../classes/interceptor';
import { CoreLoggerProvider } from '../providers/logger';
import { CoreDbProvider } from '../providers/db';
import { CoreAppProvider } from '../providers/app';
import { CoreConfigProvider } from '../providers/config';
import { CoreEmulatorModule } from '../core/emulator/emulator.module';
import { CoreLangProvider } from '../providers/lang';
import { CoreTextUtilsProvider } from '../providers/utils/text';
import { CoreDomUtilsProvider } from '../providers/utils/dom';
@ -53,7 +48,12 @@ import { CoreFilepoolProvider } from '../providers/filepool';
import { CoreUpdateManagerProvider } from '../providers/update-manager';
import { CorePluginFileDelegate } from '../providers/plugin-file-delegate';
import { CoreComponentsModule } from '../components/components.module';
import { CoreEmulatorModule } from '../core/emulator/emulator.module';
import { CoreLoginModule } from '../core/login/login.module';
import { CoreMainMenuModule } from '../core/mainmenu/mainmenu.module';
import { CoreCoursesModule } from '../core/courses/courses.module';
// For translate loader. AoT requires an exported function for factories.
export function createTranslateLoader(http: HttpClient) {
@ -62,13 +62,13 @@ export function createTranslateLoader(http: HttpClient) {
@NgModule({
declarations: [
MyApp
MoodleMobileApp
],
imports: [
BrowserModule,
HttpClientModule, // HttpClient is used to make JSON requests. It fails for HEAD requests because there is no content.
HttpModule,
IonicModule.forRoot(MyApp, {
IonicModule.forRoot(MoodleMobileApp, {
pageTransition: 'ios-transition'
}),
TranslateModule.forRoot({
@ -79,11 +79,14 @@ export function createTranslateLoader(http: HttpClient) {
}
}),
CoreEmulatorModule,
CoreLoginModule
CoreLoginModule,
CoreMainMenuModule,
CoreCoursesModule,
CoreComponentsModule
],
bootstrap: [IonicApp],
entryComponents: [
MyApp
MoodleMobileApp
],
providers: [
{
@ -91,10 +94,6 @@ export function createTranslateLoader(http: HttpClient) {
useClass: CoreInterceptor,
multi: true,
},
StatusBar,
SplashScreen,
SQLite,
Keyboard,
CoreLoggerProvider,
CoreDbProvider,
CoreAppProvider,

View File

@ -23,10 +23,16 @@
.text-right { text-align: right; }
.text-center { text-align: center; }
.text-justify { text-align: justify; }
.clearfix {
&:after {
content: "";
display: table;
clear: both;
}
}
@media only screen and (min-width: 430px) {
.mm-center-view .scroll-content {
.core-center-view .scroll-content {
display: flex!important;
align-content: center !important;
align-items: center !important;
@ -37,14 +43,27 @@
}
}
@media only screen and (max-width: 768px) {
.hidden-phone {
display: none !important;
}
}
@media only screen and (min-width: 769px) {
.hidden-tablet {
display: none !important;
}
}
// Define an alternative way to set a heading in an item without using a heading tag.
// This is done for accessibility reasons when a heading is semantically incorrect.
.item .item-heading {
@extend h6;
margin: 0;
}
.mm-oauth-icon, .item.mm-oauth-icon, .list .item.mm-oauth-icon {
.core-oauth-icon, .item.core-oauth-icon, .list .item.core-oauth-icon {
min-height: 32px;
img, .label {
max-height: 32px;
@ -59,7 +78,7 @@
}
}
.mm-bold, .mm-bold .label {
.core-bold, .core-bold .label {
font-weight: bold;
}
@ -113,3 +132,145 @@ ion-avatar ion-img, ion-avatar img {
font-style: italic;
}
/** Format Text */
core-format-text[maxHeight], *[core-format-text][maxHeight] {
display: block;
position: relative;
width: 100%;
overflow: hidden;
/* Force display inline */
&.inline {
display: inline-block;
width: auto;
}
// This is to allow clicks in radio/checkbox content.
&.core-text-formatted {
cursor: pointer;
.core-show-more {
display: none;
}
&:not(.core-shortened) {
max-height: none !important;
}
&.core-shortened {
color: $gray-darker;
overflow: hidden;
min-height: 50px;
.core-show-more {
color: color($colors, dark);
text-align: right;
font-size: 14px;
display: block;
position: absolute;
bottom: 0;
right: 0;
z-index: 1001;
background-color: $white;
padding-left: 10px;
/* @todo
&:after {
@extend .ion;
content: $ionicon-var-chevron-down;
margin-left: 10px;
color: $item-icon-accessory-color;
}
*/
}
&.core-expand-in-fullview .core-show-more:after {
// content: $ionicon-var-chevron-right; @todo
}
&:before {
content: '';
height: 100%;
position: absolute;
left: 0;
right: 0;
bottom: 0;
background: -moz-linear-gradient(top, rgba(255, 255, 255, 0) calc(100% - 50px), white calc(100% - 15px));
background: -webkit-gradient(left top, left bottom, color-stop(calc(100% - 50px), rgba(255, 255, 255, 0)), color-stop(calc(100% - 15px), white));
background: -webkit-linear-gradient(top, rgba(255, 255, 255, 0) calc(100% - 50px), white calc(100% - 15px));
background: -o-linear-gradient(top, rgba(255, 255, 255, 0) calc(100% - 50px), white calc(100% - 15px));
background: -ms-linear-gradient(top, rgba(255, 255, 255, 0) calc(100% - 50px), white calc(100% - 15px));
background: linear-gradient(to bottom, rgba(255, 255, 255, 0) calc(100% - 50px), white calc(100% - 15px));
z-index: 1000;
}
}
}
}
core-format-text, *[core-format-text] {
audio, video, a, iframe {
pointer-events: auto;
}
// Fix lists styles in core-format-text.
ul, ol {
-webkit-padding-start: 40px;
}
ul {
list-style: disc;
}
ol {
list-style: decimal;
}
.badge {
position: initial !important;
}
}
// Message item.
.item-message {
core-format-text > p:only-child {
display: inline;
}
}
// Media item, ideal for icons.
.item-media {
min-height: $item-media-height + ($content-padding * 2);
> img:first-child {
max-width: $item-media-width;
max-height: $item-media-height;
}
}
// Ionic fix. Button can occupy all page if not.
ion-select {
position: relative
}
// Top tabs
// -------------------------
.core-top-tabbar {
@include position(null, null, 0, 0);
z-index: $z-index-toolbar;
display: flex;
width: 100%;
background: $core-top-tabs-background;
> a {
@extend .tab-button;
background: $core-top-tabs-background;
color: $core-top-tabs-color !important;
border-bottom: 1px solid $core-top-tabs-border;
font-size: 1.6rem;
&[aria-selected=true] {
color: $core-top-tabs-color-active !important;
border-bottom: 2px solid $core-top-tabs-color-active;
}
}
}

View File

@ -7,3 +7,7 @@
.button-wp-light {
color: color($colors, primary, base);
}
.col[align-self-stretch] .card-wp {
height: calc(100% - #{($card-wp-margin-end + $card-wp-margin-start)});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,178 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="157 -1509 148 125"
preserveAspectRatio="xMinYMid meet"
version="1.1"
id="svg23"
sodipodi:docname="activities.svg"
inkscape:version="0.92.1 r15371">
<metadata
id="metadata27">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1016"
id="namedview25"
showgrid="false"
inkscape:zoom="5.981125"
inkscape:cx="38.889548"
inkscape:cy="62.5"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="1"
inkscape:current-layer="Group_42" />
<defs
id="defs7">
<style
id="style2">
.cls-1 {
clip-path: url(#clip-Activities);
}
.cls-2 {
fill: #eee;
}
.cls-3 {
fill: #c4c8cc;
}
.cls-4 {
fill: #fff;
}
</style>
<clipPath
id="clip-Activities">
<rect
x="157"
y="-1509"
width="148"
height="125"
id="rect4" />
</clipPath>
</defs>
<g
id="Activities"
class="cls-1"
clip-path="url(#clip-Activities)">
<g
id="Group_42"
data-name="Group 42"
transform="translate(-268 -1985)">
<ellipse
id="Ellipse_37"
data-name="Ellipse 37"
class="cls-2"
cx="74"
cy="14.785"
rx="74"
ry="14.785"
transform="translate(425 571.43)"
style="fill:#000000;fill-opacity:0.06666667" />
<rect
id="Rectangle_80"
data-name="Rectangle 80"
class="cls-3"
width="94.182"
height="110.215"
transform="translate(451.909 476)" />
<g
id="Group_41"
data-name="Group 41"
transform="translate(467.043 493)">
<rect
id="Rectangle_81"
data-name="Rectangle 81"
class="cls-4"
width="44.456"
height="5.625"
transform="translate(21.16 0.549)" />
<rect
id="Rectangle_82"
data-name="Rectangle 82"
class="cls-4"
width="33.342"
height="5.625"
transform="translate(21.16 11.652)" />
<rect
id="Rectangle_83"
data-name="Rectangle 83"
class="cls-4"
width="44.456"
height="5.625"
transform="translate(21.16 30.772)" />
<rect
id="Rectangle_84"
data-name="Rectangle 84"
class="cls-4"
width="33.342"
height="5.625"
transform="translate(21.16 41.875)" />
<rect
id="Rectangle_85"
data-name="Rectangle 85"
class="cls-4"
width="44.456"
height="5.625"
transform="translate(21.16 61.291)" />
<rect
id="Rectangle_86"
data-name="Rectangle 86"
class="cls-4"
width="33.342"
height="5.625"
transform="translate(21.16 72.393)" />
<ellipse
id="Ellipse_38"
data-name="Ellipse 38"
class="cls-4"
cx="7.007"
cy="7"
rx="7.007"
ry="7"
transform="translate(0 0)" />
<ellipse
id="Ellipse_39"
data-name="Ellipse 39"
class="cls-4"
cx="7.007"
cy="7"
rx="7.007"
ry="7"
transform="translate(0 31)" />
<ellipse
id="Ellipse_40"
data-name="Ellipse 40"
class="cls-4"
cx="7.007"
cy="7"
rx="7.007"
ry="7"
transform="translate(0 61)" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,257 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="157 -1305 148 125"
preserveAspectRatio="xMinYMid meet"
version="1.1"
id="svg34"
sodipodi:docname="courses.svg"
inkscape:version="0.92.1 r15371">
<metadata
id="metadata38">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="744"
inkscape:window-height="480"
id="namedview36"
showgrid="false"
inkscape:zoom="1.888"
inkscape:cx="74"
inkscape:cy="62.5"
inkscape:window-x="0"
inkscape:window-y="27"
inkscape:window-maximized="0"
inkscape:current-layer="Group_44" />
<defs
id="defs7">
<style
id="style2">
.cls-1 {
clip-path: url(#clip-Courses);
}
.cls-2 {
fill: #eee;
}
.cls-3 {
fill: #c4c8cc;
}
.cls-4 {
fill: #fff;
}
</style>
<clipPath
id="clip-Courses">
<rect
x="157"
y="-1305"
width="148"
height="125"
id="rect4" />
</clipPath>
</defs>
<g
id="Courses"
class="cls-1"
clip-path="url(#clip-Courses)">
<g
id="Group_44"
data-name="Group 44"
transform="translate(-268 -1781)">
<ellipse
id="Ellipse_41"
data-name="Ellipse 41"
class="cls-2"
cx="74"
cy="14.785"
rx="74"
ry="14.785"
transform="translate(425 571.43)"
style="fill:#000000;fill-opacity:0.06666667" />
<rect
id="Rectangle_87"
data-name="Rectangle 87"
class="cls-3"
width="95.097"
height="110.215"
transform="translate(451.909 476)" />
<g
id="Group_43"
data-name="Group 43"
transform="translate(464.04 494)">
<rect
id="Rectangle_88"
data-name="Rectangle 88"
class="cls-4"
width="31.043"
height="34"
transform="translate(0)" />
<rect
id="Rectangle_89"
data-name="Rectangle 89"
class="cls-4"
width="31.043"
height="34"
transform="translate(0 42)" />
<rect
id="Rectangle_90"
data-name="Rectangle 90"
class="cls-4"
width="31.067"
height="34"
transform="translate(39.005)" />
<rect
id="Rectangle_91"
data-name="Rectangle 91"
class="cls-4"
width="31.067"
height="34"
transform="translate(39.005 42)" />
<rect
id="Rectangle_92"
data-name="Rectangle 92"
class="cls-3"
width="23.023"
height="3.18"
transform="translate(3.081 16.549)" />
<rect
id="Rectangle_93"
data-name="Rectangle 93"
class="cls-3"
width="23.023"
height="3.18"
transform="translate(3.081 58.549)" />
<rect
id="Rectangle_94"
data-name="Rectangle 94"
class="cls-3"
width="23.023"
height="3.18"
transform="translate(43.122 16.549)" />
<rect
id="Rectangle_95"
data-name="Rectangle 95"
class="cls-3"
width="23.023"
height="3.18"
transform="translate(43.122 58.549)" />
<rect
id="Rectangle_96"
data-name="Rectangle 96"
class="cls-3"
width="14.014"
height="3.18"
transform="translate(3.081 21.825)" />
<rect
id="Rectangle_97"
data-name="Rectangle 97"
class="cls-3"
width="18.845"
height="3.18"
transform="translate(3.081 26.825)" />
<rect
id="Rectangle_98"
data-name="Rectangle 98"
class="cls-3"
width="14.014"
height="3.18"
transform="translate(3.081 63.825)" />
<rect
id="Rectangle_99"
data-name="Rectangle 99"
class="cls-3"
width="18.845"
height="3.18"
transform="translate(3.081 68.825)" />
<rect
id="Rectangle_100"
data-name="Rectangle 100"
class="cls-3"
width="14.014"
height="3.18"
transform="translate(43.122 21.825)" />
<rect
id="Rectangle_101"
data-name="Rectangle 101"
class="cls-3"
width="18.845"
height="3.18"
transform="translate(43.122 26.825)" />
<rect
id="Rectangle_102"
data-name="Rectangle 102"
class="cls-3"
width="14.014"
height="3.18"
transform="translate(43.122 63.825)" />
<rect
id="Rectangle_103"
data-name="Rectangle 103"
class="cls-3"
width="18.845"
height="3.18"
transform="translate(43.122 68.825)" />
<ellipse
id="Ellipse_42"
data-name="Ellipse 42"
class="cls-3"
cx="5.658"
cy="5.652"
rx="5.658"
ry="5.652"
transform="translate(3.003 3.55)" />
<ellipse
id="Ellipse_43"
data-name="Ellipse 43"
class="cls-3"
cx="5.658"
cy="5.652"
rx="5.658"
ry="5.652"
transform="translate(3.003 45.55)" />
<ellipse
id="Ellipse_44"
data-name="Ellipse 44"
class="cls-3"
cx="5.658"
cy="5.652"
rx="5.658"
ry="5.652"
transform="translate(43.044 3.55)" />
<ellipse
id="Ellipse_45"
data-name="Ellipse 45"
class="cls-3"
cx="5.658"
cy="5.652"
rx="5.658"
ry="5.652"
transform="translate(43.044 45.55)" />
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -482,7 +482,14 @@ export class CoreSite {
// We pass back a clone of the original object, this may
// prevent errors if in the callback the object is modified.
return Object.assign({}, response);
if (typeof response == 'object') {
if (Array.isArray(response)) {
return Array.from(response);
} else {
return Object.assign({}, response);
}
}
return response;
}).catch((error) => {
if (error.errorcode == 'invalidtoken' ||
(error.errorcode == 'accessexception' && error.message.indexOf('Invalid token - token expired') > -1)) {
@ -853,10 +860,10 @@ export class CoreSite {
/**
* Returns the URL to the documentation of the app, based on Moodle version and current language.
*
* @param {string} [page] Docs page to go to.
* @return {Promise} Promise resolved with the Moodle docs URL.
* @param {string} [page] Docs page to go to.
* @return {Promise<string>} Promise resolved with the Moodle docs URL.
*/
getDocsUrl(page: string) : Promise<string> {
getDocsUrl(page?: string) : Promise<string> {
const release = this.infos.release ? this.infos.release : undefined;
return this.urlUtils.getDocsUrl(release, page);
}
@ -1054,7 +1061,7 @@ export class CoreSite {
}
if (alertMessage) {
let alert = this.domUtils.showAlert('core.notice', alertMessage, null, 3000);
let alert = this.domUtils.showAlert('core.notice', alertMessage, undefined, 3000);
alert.onDidDismiss(() => {
if (inApp) {
resolve(this.utils.openInApp(url, options));

View File

@ -262,7 +262,7 @@ export class SQLiteDB {
* @return {Promise<any>} Promise resolved when done.
*/
deleteRecords(table: string, conditions?: object) : Promise<any> {
if (conditions === null) {
if (conditions === null || typeof conditions == 'undefined') {
// No conditions, delete the whole table.
return this.execute(`DELETE FROM TABLE ${table}`);
}
@ -329,6 +329,21 @@ export class SQLiteDB {
});
}
/**
* Format the data to insert in the database. Removes undefined entries so they are stored as null instead of 'undefined'.
*
* @param {object} data Data to insert.
*/
protected formatDataToInsert(data: object) : void {
// Remove undefined entries and convert null to "NULL".
for (let name in data) {
let value = data[name];
if (typeof value == 'undefined') {
delete data[name];
}
}
}
/**
* Get all the records from a table.
*
@ -585,6 +600,8 @@ export class SQLiteDB {
* @return {any[]} Array with the SQL query and the params.
*/
protected getSqlInsertQuery(table: string, data: object) : any[] {
this.formatDataToInsert(data);
let keys = Object.keys(data),
fields = keys.join(','),
questionMarks = ',?'.repeat(keys.length).substr(1);
@ -674,10 +691,10 @@ export class SQLiteDB {
*/
normaliseLimitFromNum(limitFrom: any, limitNum: any) : number[] {
// We explicilty treat these cases as 0.
if (limitFrom === null || limitFrom === '' || limitFrom === -1) {
if (typeof limitFrom == 'undefined' || limitFrom === null || limitFrom === '' || limitFrom === -1) {
limitFrom = 0;
}
if (limitNum === null || limitNum === '' || limitNum === -1) {
if (typeof limitNum == 'undefined' || limitNum === null || limitNum === '' || limitNum === -1) {
limitNum = 0;
}
@ -773,6 +790,8 @@ export class SQLiteDB {
sql,
params;
this.formatDataToInsert(data);
for (let key in data) {
sets.push(`${key} = ?`);
}

View File

@ -21,6 +21,13 @@ import { CoreMarkRequiredComponent } from './mark-required/mark-required';
import { CoreInputErrorsComponent } from './input-errors/input-errors';
import { CoreShowPasswordComponent } from './show-password/show-password';
import { CoreIframeComponent } from './iframe/iframe';
import { CoreProgressBarComponent } from './progress-bar/progress-bar';
import { CoreEmptyBoxComponent } from './empty-box/empty-box';
import { CoreSearchBoxComponent } from './search-box/search-box';
import { CoreFileComponent } from './file/file';
import { CoreContextMenuComponent } from './context-menu/context-menu';
import { CoreContextMenuItemComponent } from './context-menu/context-menu-item';
import { CoreContextMenuPopoverComponent } from './context-menu/context-menu-popover';
@NgModule({
declarations: [
@ -28,7 +35,17 @@ import { CoreIframeComponent } from './iframe/iframe';
CoreMarkRequiredComponent,
CoreInputErrorsComponent,
CoreShowPasswordComponent,
CoreIframeComponent
CoreIframeComponent,
CoreProgressBarComponent,
CoreEmptyBoxComponent,
CoreSearchBoxComponent,
CoreFileComponent,
CoreContextMenuComponent,
CoreContextMenuItemComponent,
CoreContextMenuPopoverComponent
],
entryComponents: [
CoreContextMenuPopoverComponent
],
imports: [
IonicModule,
@ -40,7 +57,13 @@ import { CoreIframeComponent } from './iframe/iframe';
CoreMarkRequiredComponent,
CoreInputErrorsComponent,
CoreShowPasswordComponent,
CoreIframeComponent
CoreIframeComponent,
CoreProgressBarComponent,
CoreEmptyBoxComponent,
CoreSearchBoxComponent,
CoreFileComponent,
CoreContextMenuComponent,
CoreContextMenuItemComponent
]
})
export class CoreComponentsModule {}

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, Input, Output, OnInit, OnDestroy, EventEmitter, OnChanges, SimpleChange } from '@angular/core';
import { CoreContextMenuComponent } from './context-menu';
/**
* This directive adds a item to the Context Menu popover.
*
* @description
* This directive defines and item to be added to the popover generated in CoreContextMenu.
*
* It is required to place this tag inside a core-context-menu tag.
*
* <core-context-menu>
* <core-context-menu-item [hidden]="showGrid" [priority]="601" [content]="'core.layoutgrid' | translate"
* (action)="switchGrid()" [iconAction]="'apps'"></core-context-menu-item>
* </core-context-menu>
*/
@Component({
selector: 'core-context-menu-item',
template: ''
})
export class CoreContextMenuItemComponent implements OnInit, OnDestroy, OnChanges {
@Input() content: string; // Content of the item.
@Input() iconDescription?: string; // Name of the icon to be shown on the left side of the item.
@Input() iconAction?: string; // Name of the icon to be shown on the right side of the item. It represents the action to do on
// click. If is "spinner" an spinner will be shown. If no icon or spinner is selected, no action
// or link will work. If href but no iconAction is provided ion-arrow-right-c will be used.
@Input() ariaDescription?: string; // Aria label to add to iconDescription.
@Input() ariaAction?: string; // Aria label to add to iconAction. If not set, it will be equal to content.
@Input() href?: string; // Link to go if no action provided.
@Input() captureLink?: boolean|string; // Whether the link needs to be captured by the app.
@Input() autoLogin?: string; // Whether the link needs to be opened using auto-login.
@Input() closeOnClick?: boolean|string = true; // Whether to close the popover when the item is clicked.
@Input() priority?: number; // Used to sort items. The highest priority, the highest position.
@Input() badge?: string; // A badge to show in the item.
@Input() badgeClass?: number; // A class to set in the badge.
@Input() hidden?: boolean; // Whether the item should be hidden.
@Output() action?: EventEmitter<string>; // Will emit an event when the item clicked.
protected hasAction = false;
protected destroyed = false;
constructor(private ctxtMenu: CoreContextMenuComponent) {
this.action = new EventEmitter();
}
/**
* Component being initialized.
*/
ngOnInit() {
// Initialize values.
this.priority = this.priority || 1;
this.closeOnClick = this.getBooleanValue(this.closeOnClick, true);
this.hasAction = this.action.observers.length > 0;
this.ariaAction = this.ariaAction || this.content;
if (this.hasAction) {
this.href = '';
}
// Navigation help if href provided.
this.captureLink = this.href && this.captureLink ? this.captureLink : false;
this.autoLogin = this.autoLogin || 'check';
if (!this.destroyed) {
this.ctxtMenu.addItem(this);
}
}
/**
* Get a boolean value from item.
*
* @param {any} value Value to check.
* @param {boolean} defaultValue Value to use if undefined.
* @return {boolean} Boolean value.
*/
protected getBooleanValue(value: any, defaultValue: boolean) : boolean {
if (typeof value == 'undefined') {
return defaultValue;
}
return value && value !== 'false';
}
/**
* Component destroyed.
*/
ngOnDestroy() {
this.destroyed = true;
this.ctxtMenu.removeItem(this);
}
/**
* Detect changes on input properties.
*/
ngOnChanges(changes: {[name: string]: SimpleChange}) {
if (changes.hidden && !changes.hidden.firstChange) {
this.ctxtMenu.itemsChanged();
}
}
}

View File

@ -0,0 +1,10 @@
<ion-list>
<ion-list-header *ngIf="title">{{title}}</ion-list-header>
<a ion-item text-wrap *ngFor="let item of items" core-link [capture]="item.captureLink" [autoLogin]="item.autoLogin" [href]="item.href" (click)="itemClicked($event, item)" [attr.aria-label]="item.ariaAction" [hidden]="item.hidden" [attr.detail-none]="!item.href || item.iconAction">
<ion-icon *ngIf="item.iconDescription" [name]="item.iconDescription" [attr.aria-label]="item.ariaDescription" item-start></ion-icon>
<core-format-text [clean]="true" [text]="item.content"></core-format-text>
<ion-icon *ngIf="(item.href || item.action) && item.iconAction && item.iconAction != 'spinner'" [name]="item.iconAction" item-end></ion-icon>
<ion-spinner *ngIf="(item.href || item.action) && item.iconAction == 'spinner'" item-end></ion-spinner>
<ion-badge class="{{item.badgeClass}}" item-end *ngIf="item.badge">{{item.badge}}</ion-badge>
</a>
</ion-list>

View File

@ -0,0 +1,69 @@
// (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 { NavParams, ViewController } from 'ionic-angular';
import { CoreContextMenuItemComponent } from './context-menu-item';
/**
* Component to display a list of items received by param in a popover.
*/
@Component({
selector: 'core-context-menu-popover',
templateUrl: 'context-menu-popover.html'
})
export class CoreContextMenuPopoverComponent {
title: string;
items: CoreContextMenuItemComponent[];
constructor(navParams: NavParams, private viewCtrl: ViewController) {
this.title = navParams.get('title');
this.items = navParams.get('items') || [];
}
/**
* Close the popover.
*/
closeMenu() : void {
this.viewCtrl.dismiss();
}
/**
* Function called when an item is clicked.
*
* @param {Event} event Click event.
* @param {CoreContextMenuItemComponent} item Item clicked.
* @return {boolean} Return true if success, false if error.
*/
itemClicked(event: Event, item: CoreContextMenuItemComponent) : boolean {
if (item.action.observers.length > 0) {
event.preventDefault();
event.stopPropagation();
if (!item.iconAction || item.iconAction == 'spinner') {
return false;
}
if (item.closeOnClick) {
this.closeMenu();
}
item.action.emit(this.closeMenu.bind(this));
} else if (item.href && item.closeOnClick) {
this.closeMenu();
}
return true;
}
}

View File

@ -0,0 +1,4 @@
<button [hidden]="hideMenu" ion-button clear icon-only [attr.aria-label]="ariaLabel" (click)="showContextMenu($event)">
<ion-icon [name]="icon"></ion-icon>
</button>
<ng-content></ng-content>

View File

@ -0,0 +1,98 @@
// (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, OnInit } from '@angular/core';
import { PopoverController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreContextMenuItemComponent } from './context-menu-item';
import { CoreContextMenuPopoverComponent } from './context-menu-popover';
import { Subject } from 'rxjs';
/**
* This component adds a button (usually in the navigation bar) that displays a context menu popover.
*/
@Component({
selector: 'core-context-menu',
templateUrl: 'context-menu.html'
})
export class CoreContextMenuComponent implements OnInit {
@Input() icon?: string; // Icon to be shown on the navigation bar. Default: Kebab menu icon.
@Input() title?: string; // Aria label and text to be shown on the top of the popover.
hideMenu: boolean;
ariaLabel: string;
protected items: CoreContextMenuItemComponent[] = [];
protected itemsChangedStream: Subject<void>; // Stream to update the hideMenu boolean when items change.
constructor(private translate: TranslateService, private popoverCtrl: PopoverController) {
// Create the stream and subscribe to it. We ignore successive changes during 250ms.
this.itemsChangedStream = new Subject<void>();
this.itemsChangedStream.auditTime(250).subscribe(() => {
// Hide the menu if all items are hidden.
this.hideMenu = !this.items.some((item) => {
return !item.hidden;
});
})
}
/**
* Component being initialized.
*/
ngOnInit() {
this.icon = this.icon || 'more';
this.ariaLabel = this.title || this.translate.instant('core.info');
}
/**
* Add a context menu item.
*
* @param {CoreContextMenuItemComponent} item The item to add.
*/
addItem(item: CoreContextMenuItemComponent) : void {
this.items.push(item);
this.itemsChanged();
}
/**
* Function called when the items change.
*/
itemsChanged() {
this.itemsChangedStream.next();
}
/**
* Remove an item from the context menu.
*
* @param {CoreContextMenuItemComponent} item The item to remove.
*/
removeItem(item: CoreContextMenuItemComponent) : void {
let index = this.items.indexOf(item);
if (index >= 0) {
this.items.splice(index, 1);
}
this.itemsChanged();
}
/**
* Show the context menu.
*
* @param {MouseEvent} event Event.
*/
showContextMenu(event: MouseEvent) : void {
let popover = this.popoverCtrl.create(CoreContextMenuPopoverComponent, {title: this.title, items: this.items});
popover.present({
ev: event
});
}
}

View File

@ -0,0 +1,8 @@
<div class="core-empty-box" [class.core-empty-box-inline]="!image && !icon">
<div class="core-empty-box-content" padding>
<img *ngIf="image && !icon" [src]="image" role="presentation">
<ion-icon *ngIf="icon" [name]="icon"></ion-icon>
<p *ngIf="message" [class.padding-top]="image || icon">{{ message }}</p>
<ng-content></ng-content>
</div>
</div>

View File

@ -0,0 +1,57 @@
core-empty-box {
.core-empty-box {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: table;
height: 100%;
width: 100%;
z-index: -1;
margin: 0;
padding: 0;
clear: both;
.core-empty-box-content {
color: $black;
margin: 0;
display: table-cell;
text-align: center;
vertical-align: middle;
}
&.core-empty-box-inline {
position: relative;
top: initial;
left: initial;
right: initial;
z-index: initial;
}
.icon {
font-size: 120px;
}
img {
height: 125px;
width: 145px;
}
p {
font-size: 120%;
}
}
@media only screen and (max-height: 420px) {
.core-empty-box {
position: relative;
.icon {
font-size: 100px;
}
img {
height: 104px;
width: 121px;
}
}
}
}

View File

@ -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 { Component, Input } from '@angular/core';
/**
* Component to show an empty box message. It will show an optional icon or image and a text centered on page.
*
* Usage:
* <core-empty-box *ngIf="empty" icon="bell" [message]="'core.emptymessage' | translate"></core-empty-box>
*/
@Component({
selector: 'core-empty-box',
templateUrl: 'empty-box.html'
})
export class CoreEmptyBoxComponent {
@Input() message: string; // Message to display.
@Input() icon?: string; // Name of the icon to use.
@Input() image?: string; // Image source. If an icon is provided, image won't be used.
constructor() {}
}

View File

@ -0,0 +1,13 @@
<a ion-item text-wrap class="item-media" (click)="download($event, true)" [class.item-2-button-right]="canDelete">
<img [src]="fileIcon" alt="" role="presentation" item-start />
<p>{{fileName}}</p>
<div class="buttons" item-end>
<button ion-button clear icon-only (click)="download($event)" *ngIf="!isDownloading && showDownload" [attr.aria-label]="'core.download' | translate">
<ion-icon [name]="isDownloaded ? 'refresh' : 'cloud-download'"></ion-icon>
</button>
<button ion-button clear icon-only (click)="delete($event)" *ngIf="!isDownloading && canDelete" [attr.aria-label]="'core.delete' | translate" color="danger">
<ion-icon name="trash"></ion-icon>
</button>
</div>
<ion-spinner *ngIf="isDownloading" item-end></ion-spinner>
</a>

View File

@ -0,0 +1,2 @@
core-file {
}

View File

@ -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<string>; // 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<void>} Promise resolved when state has been calculated.
*/
protected calculateState() : Promise<void> {
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<string>} Promise resolved when file is downloaded.
*/
protected downloadFile() : Promise<string> {
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<string>} Promise resolved when file is opened.
*/
protected openFile() : Promise<any> {
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();
}
}

View File

@ -1,4 +1,4 @@
<div [class.mm-loading-container]="loading">
<iframe #iframe [hidden]="loading" class="mm-iframe" [ngStyle]="{'width': iframeWidth, 'height': iframeHeight}" [src]="safeUrl"></iframe>
<div [class.core-loading-container]="loading">
<iframe #iframe [hidden]="loading" class="core-iframe" [ngStyle]="{'width': iframeWidth, 'height': iframeHeight}" [src]="safeUrl"></iframe>
<ion-spinner *ngIf="loading"></ion-spinner>
</div>

View File

@ -1,8 +1,9 @@
<div class="mm-loading-container mm-animate-show-hide" *ngIf="!hideUntil">
<span class="mm-loading-spinner">
<!-- @TODO: Add show hide animation -->
<div class="core-loading-container" *ngIf="!hideUntil">
<span class="core-loading-spinner">
<ion-spinner></ion-spinner>
<p class="mm-loading-message" *ngIf="message">{{message}}</p>
<p class="core-loading-message" *ngIf="message">{{message}}</p>
</span>
</div>
<ng-content class="mm-loading-content mm-animate-show-hide" *ngIf="hideUntil">
<ng-content class="core-loading-content" *ngIf="hideUntil">
</ng-content>

View File

@ -1,26 +1,37 @@
core-loading {
.mm-loading-container {
.core-loading-container {
width: 100%;
text-align: center;
padding-top: 10px;
clear: both;
}
.mm-loading-content {
.core-loading-content {
padding-bottom: 1px; /* This makes height be real */
}
&.mm-loading-noheight .mm-loading-content {
&.core-loading-noheight .core-loading-content {
height: auto;
}
}
.scroll-content > .padding > core-loading > .mm-loading-container,
ion-content[padding] > .scroll-content > core-loading > .mm-loading-container,
.mm-loading-center .mm-loading-container {
.scroll-content > .padding > core-loading > .core-loading-container,
ion-content[padding] > .scroll-content > core-loading > .core-loading-container,
.core-loading-center .core-loading-container {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: table;
height: 100%;
width: 100%;
z-index: 1;
margin: 0;
padding: 0;
clear: both;
.mm-loading-spinner {
.core-loading-spinner {
display: table-cell;
text-align: center;
vertical-align: middle;

View File

@ -0,0 +1,8 @@
<div *ngIf="progress >= 0">
<progress max="100" [value]="progress">
<div class="progress-bar-fallback" role="progressbar" aria-valuemin="0" aria-valuemax="100" [attr.aria-valuenow]="progress">
<span [style.width]="width"></span>
</div>
</progress>
<span class="core-progress-text">{{ 'core.percentagenumber' | translate: {$a: text} }}</span>
</div>

View File

@ -0,0 +1,53 @@
$core-progress-bar-height: 5px !default;
core-progress-bar {
padding-right: 55px;
position: relative;
display: block;
@extend .clearfix;
.core-progress-text {
margin-left: 10px;
line-height: normal;
font-size: 1.4rem;
color: $gray-darker;
right: 0;
top: -6px;
position: absolute;
}
progress {
-webkit-appearance: none;
appearance: none;
height: $core-progress-bar-height;
margin: 15px 0;
padding: 0;
display: block;
width: 100%;
.progress-bar-fallback,
&[value]::-webkit-progress-bar {
background-color: $gray-light;
border-radius: 2px;
box-shadow: 0 1px 1px rgba(0, 0, 0, 0.1) inset;
}
.progress-bar-fallback span,
&[value]::-webkit-progress-value {
background-color: $core-color-light;
border-radius: 2px;
}
.progress-bar-fallback {
width: 100%;
height: $core-progress-bar-height;
display: block;
position: relative;
span {
height: $core-progress-bar-height;
display: block;
}
}
}
}

View File

@ -0,0 +1,68 @@
// (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, OnChanges, SimpleChange, ChangeDetectionStrategy } from '@angular/core';
import { DomSanitizer, SafeStyle } from '@angular/platform-browser';
/**
* Component to show a progress bar and its value.
*
* Example usage:
* <core-progress-bar [progress]="percentage"></core-progress-bar>
*/
@Component({
selector: 'core-progress-bar',
templateUrl: 'progress-bar.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CoreProgressBarComponent implements OnChanges {
@Input() progress: number|string; // Percentage from 0 to 100.
@Input() text?: string; // Percentage in text to be shown at the right. If not defined, progress will be used.
width: SafeStyle;
protected textSupplied = false;
constructor(private sanitizer: DomSanitizer) {}
/**
* Detect changes on input properties.
*/
ngOnChanges(changes: {[name: string]: SimpleChange}) {
if (changes.text && typeof changes.text.currentValue != 'undefined') {
// User provided a custom text, don't use default.
this.textSupplied = true;
}
if (changes.progress) {
// Progress has changed.
if (typeof this.progress == 'string') {
this.progress = parseInt(this.progress, 10);
}
if (this.progress < 0 || isNaN(this.progress)) {
this.progress = -1;
}
if (this.progress != -1) {
// Remove decimals.
this.progress = Math.floor(this.progress);
if (!this.textSupplied) {
this.text = String(this.progress);
}
this.width = this.sanitizer.bypassSecurityTrustStyle(this.progress + '%');
}
}
}
}

View File

@ -0,0 +1,10 @@
<ion-card>
<form #f="ngForm" (ngSubmit)="submitForm(f.value.search)">
<ion-item>
<ion-input type="text" name="search" ngModel [placeholder]="placeholder" [autocorrect]="autocorrect" [spellcheck]="spellcheck" [core-auto-focus]="autoFocus"></ion-input>
<button item-end ion-button clear icon-only type="submit" class="button-small" [attr.aria-label]="searchLabel" [disabled]="!f.value.search || (f.value.search.length < lengthCheck)">
<ion-icon name="search"></ion-icon>
</button>
</ion-item>
</form>
</ion-card>

View File

@ -0,0 +1,9 @@
core-search-box {
.button.item-button[icon-only] {
margin: 0;
padding: ($content-padding / 2) $content-padding;
}
.item.item-input.item-block .item-inner ion-input {
border-bottom: 0;
}
}

View File

@ -0,0 +1,67 @@
// (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, EventEmitter, OnInit } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { CoreUtilsProvider } from '../../providers/utils/utils';
/**
* Component to display a "search box".
*
* @description
* This component will display a standalone search box with its search button in order to have a better UX.
*
* Example usage:
* <core-search-box (onSubmit)="search($event)" [placeholder]="'core.courses.search' | translate"
* [searchLabel]="'core.courses.search' | translate" autoFocus="true"></core-search-box>
*/
@Component({
selector: 'core-search-box',
templateUrl: 'search-box.html'
})
export class CoreSearchBoxComponent implements OnInit {
@Input() initialValue?: string = ''; // Initial value for search text.
@Input() searchLabel?: string ; // Label to be used on action button.
@Input() placeholder?: string; // Placeholder text for search text input.
@Input() autocorrect?: string = 'on'; // Enables/disable Autocorrection on search text input.
@Input() spellcheck?: string|boolean = true; // Enables/disable Spellchecker on search text input.
@Input() autoFocus?: string|boolean; // Enables/disable Autofocus when entering view.
@Input() lengthCheck?: number = 3; // Check value length before submit. If 0, any string will be submitted.
@Output() onSubmit: EventEmitter<string>; // Send data when submitting the search form.
constructor(private translate: TranslateService, private utils: CoreUtilsProvider) {
this.onSubmit = new EventEmitter();
}
ngOnInit() {
this.searchLabel = this.searchLabel || this.translate.instant('core.search');
this.placeholder = this.placeholder || this.translate.instant('core.search');
this.spellcheck = this.utils.isTrueOrOne(this.spellcheck);
}
/**
* Form submitted.
*
* @param {string} value Entered value.
*/
submitForm(value: string) {
if (value.length < this.lengthCheck) {
// The view should handle this case, but we check it here too just in case.
return;
}
this.onSubmit.emit(value);
}
}

View File

@ -16,6 +16,11 @@ core-show-password {
margin-top: 0;
margin-bottom: 0;
}
.core-ioninput-password {
padding-top: 0;
padding-bottom: 0;
}
}
.md {

View File

@ -0,0 +1,48 @@
// (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 { CoreCoursesCourseProgressComponent } from '../components/course-progress/course-progress';
import { CoreCoursesCourseListItemComponent } from '../components/course-list-item/course-list-item';
import { CoreCoursesOverviewEventsComponent } from '../components/overview-events/overview-events';
@NgModule({
declarations: [
CoreCoursesCourseProgressComponent,
CoreCoursesCourseListItemComponent,
CoreCoursesOverviewEventsComponent
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule
],
providers: [
],
exports: [
CoreCoursesCourseProgressComponent,
CoreCoursesCourseListItemComponent,
CoreCoursesOverviewEventsComponent
]
})
export class CoreCoursesComponentsModule {}

View File

@ -0,0 +1,12 @@
<a ion-item text-wrap (click)="openCourse(course)" [attr.disabled]="course.visible == 0 ? true : null" [attr.detail-none]="course.visible == 0 ? true : null" [title]="course.fullname">
<ion-icon name="ionic" item-start></ion-icon>
<h2><core-format-text [text]="course.fullname"></core-format-text></h2>
<div item-end>
<span *ngIf="!course.isEnrolled">
<span ion-button icon-only clear color="gray" *ngFor="let instance of course.enrollment" [attr.aria-label]=" instance.name | translate">
<ion-icon *ngIf="instance.icon" [name]="instance.icon"></ion-icon>
<img *ngIf="instance.img && !instance.icon" [src]="instance.img" class="core-course-enrollment-img">
</span>
</span>
</div>
</a>

View File

@ -0,0 +1,6 @@
core-courses-course-list-item {
.core-course-enrollment-img {
max-width: 16px;
max-height: 16px;
}
}

View File

@ -0,0 +1,82 @@
// (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, OnInit } from '@angular/core';
import { NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreCoursesProvider } from '../../providers/courses';
/**
* This directive is meant to display an item for a list of courses.
*
* Example usage:
*
* <core-courses-course-list-item [course]="course"></core-courses-course-list-item>
*/
@Component({
selector: 'core-courses-course-list-item',
templateUrl: 'course-list-item.html'
})
export class CoreCoursesCourseListItemComponent implements OnInit {
@Input() course: any; // The course to render.
constructor(private navCtrl: NavController, private translate: TranslateService, private coursesProvider: CoreCoursesProvider) {}
/**
* Component being initialized.
*/
ngOnInit() {
// Check if the user is enrolled in the course.
return this.coursesProvider.getUserCourse(this.course.id).then(() => {
this.course.isEnrolled = true;
}).catch(() => {
this.course.isEnrolled = false;
this.course.enrollment = [];
this.course.enrollmentmethods.forEach((instance) => {
if (instance === 'self') {
this.course.enrollment.push({
name: this.translate.instant('core.courses.selfenrolment'),
icon: 'unlock'
});
} else if (instance === 'guest') {
this.course.enrollment.push({
name: this.translate.instant('core.courses.allowguests'),
icon: 'person'
});
} else if (instance === 'paypal') {
this.course.enrollment.push({
name: this.translate.instant('core.courses.paypalaccepted'),
img: 'assets/img/icons/paypal.png'
});
}
});
if (this.course.enrollment.length == 0) {
this.course.enrollment.push({
name: this.translate.instant('core.courses.notenrollable'),
icon: 'lock'
});
}
});
}
/**
* Open a course.
*/
openCourse(course) {
this.navCtrl.push('CoreCoursesCoursePreviewPage', {course: course});
}
}

View File

@ -0,0 +1,22 @@
<ion-card>
<a ion-item text-wrap detail-none (click)="openCourse(course)" [title]="course.fullname">
<h2 float-start><core-format-text [text]="course.fullname"></core-format-text></h2>
<!-- Download course. -->
<!--<button [hidden]="!downloadButton.isDownload" ion-button icon-only clear color="dark" float-end>
<ion-icon name="cloud-download"></ion-icon>
</button>-->
<!-- Download course spinner. -->
<!-- <ion-spinner *ngIf="prefetchCourseIcon == 'spinner'" class="core-course-download-spinner"></ion-spinner> -->
</a>
<ion-item text-wrap *ngIf="course.summary && course.summary.length">
<p>
<summary>
<core-format-text [text]="course.summary" maxHeight="20"></core-format-text>
</summary>
</p>
</ion-item>
<ion-item *ngIf="course.progress != null && course.progress >= 0">
<core-progress-bar [progress]="course.progress"></core-progress-bar>
</ion-item>
<ng-content></ng-content>
</ion-card>

View File

@ -0,0 +1,12 @@
core-courses-course-progress.core-courseoverview {
@media (max-width: 576px) {
ion-card.card {
margin: 0;
border-radius: 0;
box-shadow: none;
border-bottom: 1px solid $list-border-color;
width: 100%;
height: 100% !important;
}
}
}

View File

@ -0,0 +1,65 @@
// (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, OnInit } from '@angular/core';
import { NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
/**
* This component is meant to display a course for a list of courses with progress.
*
* Example usage:
*
* <core-courses-course-progress [course]="course">
* </core-courses-course-progress>
*/
@Component({
selector: 'core-courses-course-progress',
templateUrl: 'course-progress.html'
})
export class CoreCoursesCourseProgressComponent implements OnInit {
@Input() course: any; // The course to render.
isDownloading: boolean;
protected obsStatus;
protected downloadText;
protected downloadingText;
protected downloadButton = {
isDownload: true,
className: 'core-download-course',
priority: 1000
};
protected buttons;
constructor(private navCtrl: NavController, private translate: TranslateService) {
this.downloadText = this.translate.instant('core.course.downloadcourse');
this.downloadingText = this.translate.instant('core.downloading');
}
/**
* Component being initialized.
*/
ngOnInit() {
// @todo: Handle course prefetch.
}
/**
* Open a course.
*/
openCourse(course) {
this.navCtrl.push('CoreCourseSectionPage', {course: course});
}
}

View File

@ -0,0 +1,49 @@
<ng-template #eventTemplate let-event="event">
<a ion-item core-link text-wrap detail-none captureLink="true" class="core-course-module-handler item-media" [href]="event.url" [title]="event.name" [class.item-badge-right-phone]="event.action && event.action.showitemcount">
<img [src]="event.iconUrl" core-external-content alt="" role="presentation" *ngIf="event.iconUrl">
<p class="item-heading"><core-format-text [text]="event.name"></core-format-text></p>
<p>{{event.timesort * 1000 | coreFormatDate:"dfmediumdate" }} <core-format-text *ngIf="showCourse" [text]="event.course.fullnamedisplay"></core-format-text></p>
<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-left *ngIf="event.action.showitemcount">{{event.action.itemcount}}</ion-badge>
</button>
<ion-badge class="hidden-tablet" item-end *ngIf="event.action.showitemcount">{{event.action.itemcount}}</ion-badge>
</a>
</ng-template>
<ion-item-group *ngIf="recentlyOverdue.length > 0">
<ion-item-divider color="danger">{{ 'core.courses.recentlyoverdue' | translate }}</ion-item-divider>
<ng-container *ngFor="let event of recentlyOverdue">
<ng-container *ngTemplateOutlet="eventTemplate; context: {event: event}"></ng-container>
</ng-container>
</ion-item-group>
<ion-item-group *ngIf="next7Days.length > 0">
<ion-item-divider color="light">{{ 'core.courses.next7days' | translate }}</ion-item-divider>
<ng-container *ngFor="let event of next7Days">
<ng-container *ngTemplateOutlet="eventTemplate; context: {event: event}"></ng-container>
</ng-container>
</ion-item-group>
<ion-item-group *ngIf="next30Days.length > 0">
<ion-item-divider color="light">{{ 'core.courses.next30days' | translate }}</ion-item-divider>
<ng-container *ngFor="let event of next30Days">
<ng-container *ngTemplateOutlet="eventTemplate; context: {event: event}"></ng-container>
</ng-container>
</ion-item-group>
<ion-item-group *ngIf="future.length > 0">
<ion-item-divider color="light">{{ 'core.courses.future' | translate }}</ion-item-divider>
<ng-container *ngFor="let event of future">
<ng-container *ngTemplateOutlet="eventTemplate; context: {event: event}"></ng-container>
</ng-container>
</ion-item-group>
<div padding text-center *ngIf="canLoadMore && !empty">
<!-- Button and spinner to show more attempts. -->
<button *ngIf="!loadingMore" ion-button block (click)="loadMoreEvents()">{{ 'core.loadmore' | translate }}</button>
<ion-spinner *ngIf="loadingMore"></ion-spinner>
</div>
<core-empty-box *ngIf="empty && showCourse" image="assets/img/icons/activities.svg" [message]="'core.courses.noevents' | translate"></core-empty-box>
<core-empty-box *ngIf="empty && !showCourse" [message]="'core.courses.noevents' | translate"></core-empty-box>

View File

@ -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;
}
}

View File

@ -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<void>; // 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;
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,19 @@
{
"allowguests": "يسمح للمستخدمين الضيوف بالدخول إلى هذا المقرر الدراسي",
"availablecourses": "المقررات الدراسية المتاحة",
"categories": "تصنيفات المقررات الدراسية",
"courses": "المقررات الدراسية",
"enrolme": "سجلني",
"frontpage": "الصفحة الرئيسية",
"mycourses": "مقرراتي الدراسية",
"nocourses": "لا يوجد معلومات لمقرر دراسي ليتم اظهرها",
"nocoursesyet": "لا توجد مقررات دراسية لهذه الفئة",
"nosearchresults": "لا توجد نتائج لهذا البحث",
"notenroled": "أنت لست مسجلاً كطالب في هذا المقرر",
"password": "كلمة المرور",
"paymentrequired": "هذا المقرر الدراسي غير مجانين لذا يجب دفع القيمة للدخول.",
"paypalaccepted": "تم قبول التبرع المدفوع",
"search": "بحث",
"searchcourses": "بحث مقررات دراسية",
"sendpaymentbutton": "ارسل القيمة المدفوعة عن طريق التبرع"
}

View File

@ -0,0 +1,16 @@
{
"allowguests": "В този курс могат да влизат гости",
"availablecourses": "Налични курсове",
"categories": "Категории курсове",
"courses": "Курсове",
"enrolme": "Запишете ме",
"errorloadcourses": "Грешка при зареждането на курсовете.",
"frontpage": "Заглавна страница",
"mycourses": "Моите курсове",
"nocourses": "Няма информация за курса, която да бъде показана.",
"nocoursesyet": "Няма курсове в тази категория",
"nosearchresults": "Няма открити резултати за Вашето търсене",
"password": "Ключ за записване",
"search": "Търсене",
"searchcourses": "Търсене на курсове"
}

View File

@ -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}}"
}

View File

@ -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}}"
}

View File

@ -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}}"
}

View File

@ -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}}"
}

View File

@ -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}}"
}

View File

@ -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}}"
}

View File

@ -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}}"
}

View File

@ -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": "<<<filtrar mis cursos",
"frontpage": "Portada",
"mycourses": "Mis cursos",
"nocourses": "Sin cursos",
"nocoursesyet": "No hay cursos en esta categoría",
"nosearchresults": "Sin resultados",
"notenroled": "Usted no está inscrito en este curso",
"notenrollable": "Usted no puede inscribirse a Usted mismo en este curso.",
"password": "Secreto Compartido",
"paymentrequired": "Para entrar a este curso es necesario pagar.",
"paypalaccepted": "Pagos PayPal aceptados",
"search": "Buscar",
"searchcourses": "Buscar cursos",
"searchcoursesadvice": "Usted puede usar el botón de 'buscar cursos' para encontrar cursos a los cuales acceder como un invitado o para inscribirse Usted mismo en los cursos que lo permitan.",
"selfenrolment": "Auto-inscripción",
"sendpaymentbutton": "Enviar pago por Paypal",
"totalcoursesearchresults": "Total de cursos: {{$a}}"
}

View File

@ -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á seguro que desea auto-matricularse en este curso?",
"courses": "Cursos",
"enrolme": "Matricularme",
"errorloadcategories": "Ocurrió un error al cargar categorías.",
"errorloadcourses": "Se ha producido un error cargando los cursos.",
"errorsearching": "Se ha producido un error durante la búsqueda.",
"errorselfenrol": "Se ha producido un error durante la auto-matriculación.",
"filtermycourses": "Filtrar mis cursos",
"frontpage": "Página Principal",
"mycourses": "Mis cursos",
"nocourses": "Sin cursos",
"nocoursesyet": "No hay cursos en esta categoría",
"nosearchresults": "La búsqueda no produjo resultados",
"notenroled": "Usted no está matriculado en este curso",
"notenrollable": "No puede auto-matricularse en este curso.",
"password": "Contraseña",
"paymentrequired": "Para entrar a este curso es necesario pagar.",
"paypalaccepted": "Pagos PayPal aceptados",
"search": "Buscar",
"searchcourses": "Buscar cursos",
"searchcoursesadvice": "Puede utilizar el botón de buscar cursos para acceder como invitado o auto-matricularse en los cursos que lo permitan.",
"selfenrolment": "Auto-matriculación",
"sendpaymentbutton": "Enviar pago por Paypal",
"totalcoursesearchresults": "Total de cursos: {{$a}}"
}

View File

@ -0,0 +1,30 @@
{
"allowguests": "Ikastaro honetan bisitariak sar daitezke",
"availablecourses": "Eskura dauden ikastaroak",
"cannotretrievemorecategories": "{{$a}}. maila baino sakonagoko kategoriak ezin dira eskuratu.",
"categories": "Ikastaro-kategoriak",
"confirmselfenrol": "Ziur zaude ikastaro honetan izena eman nahi duzula?",
"courses": "Ikastaroak",
"enrolme": "Matrikula nazazu",
"errorloadcategories": "Errorea gertatu da kategoriak kargatzean.",
"errorloadcourses": "Errore bat gertatu da ikastaroak kargatzean.",
"errorsearching": "Errorea gertatu da bilatzean.",
"errorselfenrol": "Errorea gertatu da matrikulazio automatikoa egitean",
"filtermycourses": "Nire ikastaroak iragazi",
"frontpage": "Hasiera-orria",
"mycourses": "Nire ikastaroak",
"nocourses": "Ez dago ikastaroei buruzko informaziorik",
"nocoursesyet": "Ez dago ikastarorik kategoria honetan",
"nosearchresults": "Zure bilaketak du ezer topatu",
"notenroled": "Ez zaude matrikulatuta ikastaro honetan",
"notenrollable": "Ezin duzu zeure burua matrikulatu ikastaro honetan.",
"password": "Pasahitza",
"paymentrequired": "Ikastaro hau ordainpekoa da",
"paypalaccepted": "Paypal ordainketak onartu dira",
"search": "Bilatu...",
"searchcourses": "Bilatu Ikastaroak",
"searchcoursesadvice": "Bilatu botoia erabil dezakezu ikastaroak topatu eta bisitari gisa sartu edo bertan matrikulatzeko ikastaroak baimentzen badu.",
"selfenrolment": "Matrikulazio automatikoa",
"sendpaymentbutton": "Paypal bidezko ordainketa bidali",
"totalcoursesearchresults": "Ikastaroak guztira: {{$a}}"
}

View File

@ -0,0 +1,20 @@
{
"allowguests": "ورود کاربران مهمان به این درس مجاز است",
"availablecourses": "درس‌های موجود",
"categories": "طبقه‌های درسی",
"confirmselfenrol": "آیا مطمئنید که می‌خواهید خود را در این درس ثبت‌نام کنید؟",
"courses": "درس‌ها",
"enrolme": "ثبت‌نام من",
"errorloadcourses": "در هنگام بارگیری درس‌ها خطایی رخ داد.",
"filtermycourses": "پالایش درس‌های من",
"frontpage": "صفحهٔ اول",
"mycourses": "درس‌های من",
"nocourses": "هیچ درسی برای نمایش وجود ندارد",
"nocoursesyet": "درسی در این طبقه وجود ندارد",
"nosearchresults": "نتایجی برای جستجوی شما وجود نداشت.",
"notenroled": "شما در این درس ثبت‌نام نیستید",
"password": "رمز ورود",
"paymentrequired": "ثبت‌نام در این درس مستلزم پرداخت شهریه است.",
"search": "جستجو...",
"searchcourses": "جستجو بین درس‌ها"
}

View File

@ -0,0 +1,30 @@
{
"allowguests": "Tämä kurssi päästää vierailijatunnuksella sisään.",
"availablecourses": "Saatavilla olevat kurssit",
"cannotretrievemorecategories": "Kategorioita, jotka ovat \"syvemmällä\" kuin tasolla {{$a}} ei voida noutaa.",
"categories": "Kategoriat",
"confirmselfenrol": "Oletko varma, että haluat lisätä itsesi kurssin osallistujaksi?",
"courses": "Kurssit",
"enrolme": "Lisää minut kurssialueelle",
"errorloadcategories": "Kategorioita ladattaessa tapahtui virhe.",
"errorloadcourses": "Kursseja ladattaessa tapahtui virhe.",
"errorsearching": "Hauan aikana tapahtu virhe.",
"errorselfenrol": "Itserekisteröitymisessä tapahtui virhe.",
"filtermycourses": "Suodata kursseja",
"frontpage": "Etusivu",
"mycourses": "Omat kurssini",
"nocourses": "Ei kursseja",
"nocoursesyet": "Ei kursseja tässä kategoriassa",
"nosearchresults": "Ei tuloksia",
"notenroled": "Et ole ilmoittautuneena kurssille",
"notenrollable": "Et voi itserekisteröityä tälle kursille.",
"password": "Jaettu salaisuus",
"paymentrequired": "Tämä kurssi vaatii osallistumismaksun.",
"paypalaccepted": "PayPal maksu hyväksytty",
"search": "Etsi",
"searchcourses": "Etsi kursseilta",
"searchcoursesadvice": "Voit käyttää kurssinhakupainiketta liittääksesi itsesi kurssille vierailijaksi tai osallistujaksi, mikäli kurssin asetukset sallivat sen.",
"selfenrolment": "Itserekisteröityminen",
"sendpaymentbutton": "Lähetä maksu PayPalin kautta",
"totalcoursesearchresults": "Kursseja yhteensä: {{$a}}"
}

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