|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
169
src/app/app.scss
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)});
|
||||
}
|
||||
|
|
After Width: | Height: | Size: 3.8 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 4.0 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.3 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 4.8 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.2 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 5.7 KiB |
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 3.4 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 3.5 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 4.4 KiB |
After Width: | Height: | Size: 4.5 KiB |
After Width: | Height: | Size: 3.9 KiB |
After Width: | Height: | Size: 4.9 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 4.2 KiB |
After Width: | Height: | Size: 5.2 KiB |
After Width: | Height: | Size: 3.6 KiB |
After Width: | Height: | Size: 4.6 KiB |
After Width: | Height: | Size: 2.1 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 5.3 KiB |
After Width: | Height: | Size: 4.1 KiB |
After Width: | Height: | Size: 2.8 KiB |
|
@ -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 |
|
@ -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 |
After Width: | Height: | Size: 7.0 KiB |
|
@ -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));
|
||||
|
|
|
@ -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} = ?`);
|
||||
}
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() {}
|
||||
}
|
|
@ -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>
|
|
@ -0,0 +1,2 @@
|
|||
core-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();
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 + '%');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -16,6 +16,11 @@ core-show-password {
|
|||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.core-ioninput-password {
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.md {
|
||||
|
|
|
@ -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 {}
|
|
@ -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>
|
|
@ -0,0 +1,6 @@
|
|||
core-courses-course-list-item {
|
||||
.core-course-enrollment-img {
|
||||
max-width: 16px;
|
||||
max-height: 16px;
|
||||
}
|
||||
}
|
|
@ -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});
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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});
|
||||
}
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"allowguests": "يسمح للمستخدمين الضيوف بالدخول إلى هذا المقرر الدراسي",
|
||||
"availablecourses": "المقررات الدراسية المتاحة",
|
||||
"categories": "تصنيفات المقررات الدراسية",
|
||||
"courses": "المقررات الدراسية",
|
||||
"enrolme": "سجلني",
|
||||
"frontpage": "الصفحة الرئيسية",
|
||||
"mycourses": "مقرراتي الدراسية",
|
||||
"nocourses": "لا يوجد معلومات لمقرر دراسي ليتم اظهرها",
|
||||
"nocoursesyet": "لا توجد مقررات دراسية لهذه الفئة",
|
||||
"nosearchresults": "لا توجد نتائج لهذا البحث",
|
||||
"notenroled": "أنت لست مسجلاً كطالب في هذا المقرر",
|
||||
"password": "كلمة المرور",
|
||||
"paymentrequired": "هذا المقرر الدراسي غير مجانين لذا يجب دفع القيمة للدخول.",
|
||||
"paypalaccepted": "تم قبول التبرع المدفوع",
|
||||
"search": "بحث",
|
||||
"searchcourses": "بحث مقررات دراسية",
|
||||
"sendpaymentbutton": "ارسل القيمة المدفوعة عن طريق التبرع"
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"allowguests": "В този курс могат да влизат гости",
|
||||
"availablecourses": "Налични курсове",
|
||||
"categories": "Категории курсове",
|
||||
"courses": "Курсове",
|
||||
"enrolme": "Запишете ме",
|
||||
"errorloadcourses": "Грешка при зареждането на курсовете.",
|
||||
"frontpage": "Заглавна страница",
|
||||
"mycourses": "Моите курсове",
|
||||
"nocourses": "Няма информация за курса, която да бъде показана.",
|
||||
"nocoursesyet": "Няма курсове в тази категория",
|
||||
"nosearchresults": "Няма открити резултати за Вашето търсене",
|
||||
"password": "Ключ за записване",
|
||||
"search": "Търсене",
|
||||
"searchcourses": "Търсене на курсове"
|
||||
}
|
|
@ -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}}"
|
||||
}
|
|
@ -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}}"
|
||||
}
|
|
@ -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}}"
|
||||
}
|
|
@ -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}}"
|
||||
}
|
|
@ -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}}"
|
||||
}
|
|
@ -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}}"
|
||||
}
|
|
@ -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}}"
|
||||
}
|
|
@ -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}}"
|
||||
}
|
|
@ -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}}"
|
||||
}
|
|
@ -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}}"
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"allowguests": "ورود کاربران مهمان به این درس مجاز است",
|
||||
"availablecourses": "درسهای موجود",
|
||||
"categories": "طبقههای درسی",
|
||||
"confirmselfenrol": "آیا مطمئنید که میخواهید خود را در این درس ثبتنام کنید؟",
|
||||
"courses": "درسها",
|
||||
"enrolme": "ثبتنام من",
|
||||
"errorloadcourses": "در هنگام بارگیری درسها خطایی رخ داد.",
|
||||
"filtermycourses": "پالایش درسهای من",
|
||||
"frontpage": "صفحهٔ اول",
|
||||
"mycourses": "درسهای من",
|
||||
"nocourses": "هیچ درسی برای نمایش وجود ندارد",
|
||||
"nocoursesyet": "درسی در این طبقه وجود ندارد",
|
||||
"nosearchresults": "نتایجی برای جستجوی شما وجود نداشت.",
|
||||
"notenroled": "شما در این درس ثبتنام نیستید",
|
||||
"password": "رمز ورود",
|
||||
"paymentrequired": "ثبتنام در این درس مستلزم پرداخت شهریه است.",
|
||||
"search": "جستجو...",
|
||||
"searchcourses": "جستجو بین درسها"
|
||||
}
|
|
@ -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}}"
|
||||
}
|