diff --git a/.travis.yml b/.travis.yml
index c75db89dc54..2d7f96a8b8a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -15,8 +15,7 @@ cache:
- $HOME/.cache/electron-builder
before_script:
- - npm install -g @angular/cli
- - npm i npm@latest -g
+ - npm install npm@latest -g
- gulp
script:
diff --git a/Dockerfile b/Dockerfile
index d7a27002cfd..991bd3f7f19 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -11,17 +11,17 @@ EXPOSE 35729
# Port 53703 is the Chrome dev logger port.
EXPOSE 53703
-# MoodleMobile uses Ionic and Gulp.
-RUN npm i -g ionic gulp && rm -rf /root/.npm
-
WORKDIR /app
COPY . /app
-# Install npm libraries and run gulp to initialize the project.
-RUN npm install && gulp && rm -rf /root/.npm
+# Install npm libraries.
+RUN npm install && rm -rf /root/.npm
+
+# Run gulp before starting.
+RUN npx gulp
# Provide a Healthcheck command for easier use in CI.
HEALTHCHECK --interval=10s --timeout=3s --start-period=30s CMD curl -f http://localhost:8100 || exit 1
-CMD ["ionic", "serve", "-b"]
+CMD ["npm", "run", "ionic:serve"]
diff --git a/config.xml b/config.xml
index 186608e98c8..1ab1ad371ee 100644
--- a/config.xml
+++ b/config.xml
@@ -1,5 +1,5 @@
-
+
Moodle
Moodle official app
Moodle Mobile team
@@ -23,7 +23,7 @@
-
+
@@ -113,30 +113,30 @@
-
-
+
+
-
+
-
+
-
-
+
+
-
+
-
-
-
-
-
-
+
+
+
+
+
+
@@ -147,6 +147,9 @@
+
+
+
diff --git a/desktop/assets/windows/AppXManifest.xml b/desktop/assets/windows/AppXManifest.xml
index 4d53ac2d375..be1e56919f4 100644
--- a/desktop/assets/windows/AppXManifest.xml
+++ b/desktop/assets/windows/AppXManifest.xml
@@ -6,7 +6,7 @@
+ Version="3.7.1.0" />
Moodle Desktop
Moodle Pty Ltd.
diff --git a/package-lock.json b/package-lock.json
index 24f38ee3fcf..c82b49deb62 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
{
"name": "moodlemobile",
- "version": "3.7.0",
+ "version": "3.7.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@@ -1589,23 +1589,15 @@
}
},
"async-done": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.1.tgz",
- "integrity": "sha512-R1BaUeJ4PMoLNJuk+0tLJgjmEqVsdN118+Z8O+alhnQDQgy0kmD5Mqi0DNEmMx2LM0Ed5yekKu+ZXYvIHceicg==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/async-done/-/async-done-1.3.2.tgz",
+ "integrity": "sha512-uYkTP8dw2og1tu1nmza1n1CMW0qb8gWWlwqMmLb7MhBVs4BXrFziT6HXUd+/RlRA/i4H9AkofYloUbs1fwMqlw==",
"dev": true,
"requires": {
"end-of-stream": "^1.1.0",
"once": "^1.3.2",
- "process-nextick-args": "^1.0.7",
+ "process-nextick-args": "^2.0.0",
"stream-exhaust": "^1.0.1"
- },
- "dependencies": {
- "process-nextick-args": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
- "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=",
- "dev": true
- }
}
},
"async-each": {
@@ -2606,1228 +2598,5890 @@
"is-plain-object": "^2.0.1"
}
},
- "cordova-android": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/cordova-android/-/cordova-android-7.1.2.tgz",
- "integrity": "sha512-w28HJGtfAZCT96hVH9BMppWMnmDTZplKu2NRQZN2dCr5e9r7aHpay41MYy9IBkh8+7E7lMo/jZkRwBDNr4VnEg==",
+ "cordova": {
+ "version": "8.1.2",
+ "resolved": "https://registry.npmjs.org/cordova/-/cordova-8.1.2.tgz",
+ "integrity": "sha512-IfslM3MP42CA/ebVJVlit6FhQ2P6Fercwx9NNQjkVs0wahEwqamL4bcqh1gKiTti7+/ZsDtBRSVmRv+y7LcTbg==",
"requires": {
- "abbrev": "*",
- "android-versions": "1.3.0",
- "ansi": "*",
- "balanced-match": "*",
- "base64-js": "1.2.0",
- "big-integer": "*",
- "bplist-parser": "*",
- "brace-expansion": "*",
- "concat-map": "*",
- "cordova-common": "2.2.5",
- "cordova-registry-mapper": "*",
- "elementtree": "0.1.6",
- "glob": "5.0.15",
- "inflight": "*",
- "inherits": "*",
- "minimatch": "*",
- "nopt": "3.0.1",
- "once": "*",
- "path-is-absolute": "*",
- "plist": "2.1.0",
- "properties-parser": "0.2.3",
- "q": "1.4.1",
- "sax": "0.3.5",
- "semver": "*",
- "shelljs": "0.5.3",
- "underscore": "*",
- "unorm": "*",
- "wrappy": "*",
- "xmlbuilder": "8.2.2",
- "xmldom": "*"
+ "configstore": "^3.1.2",
+ "cordova-common": "^2.2.0",
+ "cordova-lib": "8.1.1",
+ "editor": "1.0.0",
+ "insight": "^0.8.4",
+ "loud-rejection": "^1.6.0",
+ "nopt": "^4.0.1",
+ "update-notifier": "^2.5.0"
},
"dependencies": {
+ "JSONStream": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.4.tgz",
+ "integrity": "sha512-Y7vfi3I5oMOYIr+WxV8NZxDSwcbNgzdKYsTNInmycOq9bUYwGg9ryu57Wg5NLmCjqdFPNUmpMBo3kSJN9tCbXg==",
+ "requires": {
+ "jsonparse": "^1.2.0",
+ "through": ">=2.2.7 <3"
+ }
+ },
"abbrev": {
"version": "1.1.1",
- "bundled": true
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
+ "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="
},
- "android-versions": {
- "version": "1.3.0",
- "bundled": true,
+ "accepts": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.5.tgz",
+ "integrity": "sha1-63d99gEXI6OxTopywIBcjoZ0a9I=",
"requires": {
- "semver": "^5.4.1"
+ "mime-types": "~2.1.18",
+ "negotiator": "0.6.1"
}
},
- "ansi": {
- "version": "0.3.1",
- "bundled": true
- },
- "balanced-match": {
- "version": "1.0.0",
- "bundled": true
- },
- "base64-js": {
- "version": "1.2.0",
- "bundled": true
- },
- "big-integer": {
- "version": "1.6.32",
- "bundled": true
+ "acorn": {
+ "version": "5.7.3",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.7.3.tgz",
+ "integrity": "sha512-T/zvzYRfbVojPWahDsE5evJdHb3oJoQfFbsrKM7w5Zcs++Tr257tia3BmMP8XYVjp1S9RZXQMh7gao96BlqZOw=="
},
- "bplist-parser": {
- "version": "0.1.1",
- "bundled": true,
+ "acorn-jsx": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz",
+ "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=",
"requires": {
- "big-integer": "^1.6.7"
+ "acorn": "^3.0.4"
+ },
+ "dependencies": {
+ "acorn": {
+ "version": "3.3.0",
+ "resolved": "http://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
+ "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo="
+ }
}
},
- "brace-expansion": {
- "version": "1.1.11",
- "bundled": true,
+ "acorn-node": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.6.0.tgz",
+ "integrity": "sha512-ZsysjEh+Y3i14f7YXCAKJy99RXbd56wHKYBzN4FlFtICIZyFpYwK6OwNJhcz8A/FMtxoUZkJofH1v9KIfNgWmw==",
"requires": {
- "balanced-match": "^1.0.0",
- "concat-map": "0.0.1"
+ "acorn": "^6.0.1",
+ "acorn-walk": "^6.0.1",
+ "xtend": "^4.0.1"
+ },
+ "dependencies": {
+ "acorn": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.0.2.tgz",
+ "integrity": "sha512-GXmKIvbrN3TV7aVqAzVFaMW8F8wzVX7voEBRO3bDA64+EX37YSayggRJP5Xig6HYHBkWKpFg9W5gg6orklubhg=="
+ }
}
},
- "concat-map": {
- "version": "0.0.1",
- "bundled": true
+ "acorn-walk": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.1.0.tgz",
+ "integrity": "sha512-ugTb7Lq7u4GfWSqqpwE0bGyoBZNMTok/zDBXxfEG0QM50jNlGhIWjRC1pPN7bvV1anhF+bs+/gNcRw+o55Evbg=="
},
- "cordova-common": {
- "version": "2.2.5",
- "bundled": true,
+ "ajv": {
+ "version": "5.5.2",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
+ "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
"requires": {
- "ansi": "^0.3.1",
- "bplist-parser": "^0.1.0",
- "cordova-registry-mapper": "^1.1.8",
- "elementtree": "0.1.6",
- "glob": "^5.0.13",
- "minimatch": "^3.0.0",
- "plist": "^2.1.0",
- "q": "^1.4.1",
- "shelljs": "^0.5.3",
- "underscore": "^1.8.3",
- "unorm": "^1.3.3"
+ "co": "^4.6.0",
+ "fast-deep-equal": "^1.0.0",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.3.0"
}
},
- "cordova-registry-mapper": {
- "version": "1.1.15",
- "bundled": true
+ "ajv-keywords": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-2.1.1.tgz",
+ "integrity": "sha1-YXmX/F9gV2iUxDX5QNgZ4TW4B2I="
},
- "elementtree": {
- "version": "0.1.6",
- "bundled": true,
+ "aliasify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/aliasify/-/aliasify-2.1.0.tgz",
+ "integrity": "sha1-fDCCW5RQueYYW6J1M+r24gZ9S0I=",
"requires": {
- "sax": "0.3.5"
+ "browserify-transform-tools": "~1.7.0"
}
},
- "glob": {
- "version": "5.0.15",
- "bundled": true,
+ "amdefine": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
+ "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=",
+ "optional": true
+ },
+ "ansi": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/ansi/-/ansi-0.3.1.tgz",
+ "integrity": "sha1-DELU+xcWDVqa8eSEus4cZpIsGyE="
+ },
+ "ansi-align": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-2.0.0.tgz",
+ "integrity": "sha1-w2rsy6VjuJzrVW82kPCx2eNUf38=",
"requires": {
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "2 || 3",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
+ "string-width": "^2.0.0"
}
},
- "inflight": {
- "version": "1.0.6",
- "bundled": true,
+ "ansi-escapes": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.1.0.tgz",
+ "integrity": "sha512-UgAb8H9D41AQnu/PbWlCofQVcnV4Gs2bBJi9eZPxfU/hgglFh3SMDMENRIqdr7H6XFnXdoknctFByVsCOotTVw=="
+ },
+ "ansi-regex": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
+ "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
+ },
+ "ansi-styles": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz",
+ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4="
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"requires": {
- "once": "^1.3.0",
- "wrappy": "1"
+ "sprintf-js": "~1.0.2"
}
},
- "inherits": {
- "version": "2.0.3",
- "bundled": true
+ "array-filter": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/array-filter/-/array-filter-0.0.1.tgz",
+ "integrity": "sha1-fajPLiZijtcygDWB/SH2fKzS7uw="
},
- "minimatch": {
- "version": "3.0.4",
- "bundled": true,
+ "array-find-index": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz",
+ "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E="
+ },
+ "array-flatten": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
+ "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI="
+ },
+ "array-map": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/array-map/-/array-map-0.0.0.tgz",
+ "integrity": "sha1-iKK6tz0c97zVwbEYoAP2b2ZfpmI="
+ },
+ "array-reduce": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/array-reduce/-/array-reduce-0.0.0.tgz",
+ "integrity": "sha1-FziZ0//Rx9k4PkR5Ul2+J4yrXys="
+ },
+ "array-union": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz",
+ "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=",
"requires": {
- "brace-expansion": "^1.1.7"
+ "array-uniq": "^1.0.1"
}
},
- "nopt": {
- "version": "3.0.1",
- "bundled": true,
+ "array-uniq": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz",
+ "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY="
+ },
+ "arrify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz",
+ "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0="
+ },
+ "asn1": {
+ "version": "0.2.4",
+ "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz",
+ "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==",
"requires": {
- "abbrev": "1"
+ "safer-buffer": "~2.1.0"
}
},
- "once": {
- "version": "1.4.0",
- "bundled": true,
+ "asn1.js": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
+ "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
"requires": {
- "wrappy": "1"
+ "bn.js": "^4.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
}
},
- "path-is-absolute": {
- "version": "1.0.1",
- "bundled": true
- },
- "plist": {
- "version": "2.1.0",
- "bundled": true,
+ "assert": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/assert/-/assert-1.4.1.tgz",
+ "integrity": "sha1-mZEtWRg2tab1s0XA8H7vwI/GXZE=",
"requires": {
- "base64-js": "1.2.0",
- "xmlbuilder": "8.2.2",
- "xmldom": "0.1.x"
+ "util": "0.10.3"
+ },
+ "dependencies": {
+ "inherits": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz",
+ "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE="
+ },
+ "util": {
+ "version": "0.10.3",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz",
+ "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=",
+ "requires": {
+ "inherits": "2.0.1"
+ }
+ }
}
},
- "properties-parser": {
- "version": "0.2.3",
- "bundled": true
+ "assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU="
},
- "q": {
- "version": "1.4.1",
- "bundled": true
+ "async": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+ "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo="
},
- "sax": {
- "version": "0.3.5",
- "bundled": true
+ "asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
- "semver": {
- "version": "5.5.0",
- "bundled": true
+ "aws-sign2": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
+ "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg="
},
- "shelljs": {
- "version": "0.5.3",
- "bundled": true
+ "aws4": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.8.0.tgz",
+ "integrity": "sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ=="
},
- "underscore": {
- "version": "1.9.1",
- "bundled": true
+ "babel-code-frame": {
+ "version": "6.26.0",
+ "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
+ "integrity": "sha1-Y/1D99weO7fONZR9uP42mj9Yx0s=",
+ "requires": {
+ "chalk": "^1.1.3",
+ "esutils": "^2.0.2",
+ "js-tokens": "^3.0.2"
+ }
},
- "unorm": {
- "version": "1.4.1",
- "bundled": true
+ "balanced-match": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
+ "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
- "wrappy": {
+ "base64-js": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.0.tgz",
+ "integrity": "sha1-o5mS1yNYSBGYK+XikLtqU9hnAPE="
+ },
+ "bcrypt-pbkdf": {
"version": "1.0.2",
- "bundled": true
+ "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
+ "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
+ "optional": true,
+ "requires": {
+ "tweetnacl": "^0.14.3"
+ }
},
- "xmlbuilder": {
- "version": "8.2.2",
- "bundled": true
+ "big-integer": {
+ "version": "1.6.36",
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.36.tgz",
+ "integrity": "sha512-t70bfa7HYEA1D9idDbmuv7YbsbVkQ+Hp+8KFSul4aE5e/i1bjCNIRYJZlA8Q8p0r9T8cF/RVvwUgRA//FydEyg=="
+ },
+ "block-stream": {
+ "version": "0.0.9",
+ "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
+ "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=",
+ "requires": {
+ "inherits": "~2.0.0"
+ }
},
- "xmldom": {
- "version": "0.1.27",
- "bundled": true
- }
- }
- },
- "cordova-android-support-gradle-release": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/cordova-android-support-gradle-release/-/cordova-android-support-gradle-release-3.0.0.tgz",
- "integrity": "sha512-vyiqQ6N9Qb+4xRizWSpUX/LyJ1HaDN0piWc8xoS9Hx9YodIS3vyi1UpQyfLQmCixoeLVcRieKXjuSMXnUrv1dw==",
- "requires": {
- "q": "^1.4.1",
- "semver": "5.6.0"
- },
- "dependencies": {
- "semver": {
- "version": "5.6.0",
- "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
- "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg=="
- }
- }
- },
- "cordova-clipboard": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/cordova-clipboard/-/cordova-clipboard-1.2.1.tgz",
- "integrity": "sha512-WTGxyQJYsgmll8wDEo0u4XevZDUH1ZH1VPoOwwNkQ8YOtCNQS8gRIIVtZ70Kan+Vo+CiUMV0oJXdNAdARb8JwQ=="
- },
- "cordova-ios": {
- "version": "4.5.5",
- "resolved": "https://registry.npmjs.org/cordova-ios/-/cordova-ios-4.5.5.tgz",
- "integrity": "sha512-3+30m2dZ2yii7kg+H7cgpdpkXpMj54zoX5imjGGG4Z7dPXKmalTLc/9rLq+Iaa+Q1BqyOrUFaHopWOODRU6vCg==",
- "requires": {
- "abbrev": "*",
- "ansi": "*",
- "balanced-match": "*",
- "base64-js": "1.2.0",
- "big-integer": "*",
- "bplist-creator": "*",
- "bplist-parser": "*",
- "brace-expansion": "*",
- "concat-map": "*",
- "cordova-common": "2.2.5",
- "cordova-registry-mapper": "*",
- "elementtree": "0.1.6",
- "glob": "5.0.15",
- "inflight": "*",
- "inherits": "*",
- "ios-sim": "6.1.3",
- "minimatch": "*",
- "nopt": "3.0.6",
- "once": "*",
- "path-is-absolute": "*",
- "plist": "2.1.0",
- "q": "1.5.1",
- "sax": "0.3.5",
- "shelljs": "0.5.3",
- "simctl": "*",
- "simple-plist": "0.2.1",
- "stream-buffers": "2.2.0",
- "tail": "0.4.0",
- "underscore": "*",
- "unorm": "*",
- "uuid": "3.0.1",
- "wrappy": "*",
- "xcode": "0.9.3",
- "xml-escape": "1.1.0",
- "xmlbuilder": "8.2.2",
- "xmldom": "*"
- },
- "dependencies": {
- "abbrev": {
- "version": "1.1.1",
- "bundled": true
- },
- "ansi": {
- "version": "0.3.1",
- "bundled": true
- },
- "balanced-match": {
- "version": "1.0.0",
- "bundled": true
+ "bn.js": {
+ "version": "4.11.8",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.8.tgz",
+ "integrity": "sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA=="
},
- "base64-js": {
- "version": "1.2.0",
- "bundled": true
+ "body-parser": {
+ "version": "1.18.2",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.2.tgz",
+ "integrity": "sha1-h2eKGdhLR9hZuDGZvVm84iKxBFQ=",
+ "requires": {
+ "bytes": "3.0.0",
+ "content-type": "~1.0.4",
+ "debug": "2.6.9",
+ "depd": "~1.1.1",
+ "http-errors": "~1.6.2",
+ "iconv-lite": "0.4.19",
+ "on-finished": "~2.3.0",
+ "qs": "6.5.1",
+ "raw-body": "2.3.2",
+ "type-is": "~1.6.15"
+ }
},
- "big-integer": {
- "version": "1.6.32",
- "bundled": true
+ "boxen": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/boxen/-/boxen-1.3.0.tgz",
+ "integrity": "sha512-TNPjfTr432qx7yOjQyaXm3dSR0MH9vXp7eT1BFSl/C51g+EFnOR9hTg1IreahGBmDNCehscshe45f+C1TBZbLw==",
+ "requires": {
+ "ansi-align": "^2.0.0",
+ "camelcase": "^4.0.0",
+ "chalk": "^2.0.1",
+ "cli-boxes": "^1.0.0",
+ "string-width": "^2.0.0",
+ "term-size": "^1.2.0",
+ "widest-line": "^2.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "chalk": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+ "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
},
"bplist-creator": {
"version": "0.0.7",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.0.7.tgz",
+ "integrity": "sha1-N98VNgkoJLh8QvlXsBNEEXNyrkU=",
"requires": {
"stream-buffers": "~2.2.0"
}
},
"bplist-parser": {
"version": "0.1.1",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.1.1.tgz",
+ "integrity": "sha1-1g1dzCDLptx+HymbNdPh+V2vuuY=",
"requires": {
"big-integer": "^1.6.7"
}
},
"brace-expansion": {
"version": "1.1.11",
- "bundled": true,
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
- "concat-map": {
- "version": "0.0.1",
- "bundled": true
- },
- "cordova-common": {
- "version": "2.2.5",
- "bundled": true,
- "requires": {
- "ansi": "^0.3.1",
- "bplist-parser": "^0.1.0",
- "cordova-registry-mapper": "^1.1.8",
- "elementtree": "0.1.6",
- "glob": "^5.0.13",
- "minimatch": "^3.0.0",
- "plist": "^2.1.0",
- "q": "^1.4.1",
- "shelljs": "^0.5.3",
- "underscore": "^1.8.3",
- "unorm": "^1.3.3"
- }
- },
- "cordova-registry-mapper": {
- "version": "1.1.15",
- "bundled": true
- },
- "elementtree": {
- "version": "0.1.6",
- "bundled": true,
- "requires": {
- "sax": "0.3.5"
- }
+ "brorand": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+ "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8="
},
- "glob": {
- "version": "5.0.15",
- "bundled": true,
+ "browser-pack": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/browser-pack/-/browser-pack-6.1.0.tgz",
+ "integrity": "sha512-erYug8XoqzU3IfcU8fUgyHqyOXqIE4tUTTQ+7mqUjQlvnXkOO6OlT9c/ZoJVHYoAaqGxr09CN53G7XIsO4KtWA==",
"requires": {
- "inflight": "^1.0.4",
- "inherits": "2",
- "minimatch": "2 || 3",
- "once": "^1.3.0",
- "path-is-absolute": "^1.0.0"
+ "JSONStream": "^1.0.3",
+ "combine-source-map": "~0.8.0",
+ "defined": "^1.0.0",
+ "safe-buffer": "^5.1.1",
+ "through2": "^2.0.0",
+ "umd": "^3.0.0"
}
},
- "inflight": {
- "version": "1.0.6",
- "bundled": true,
+ "browser-resolve": {
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz",
+ "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==",
"requires": {
- "once": "^1.3.0",
- "wrappy": "1"
+ "resolve": "1.1.7"
+ },
+ "dependencies": {
+ "resolve": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
+ "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs="
+ }
}
},
- "inherits": {
- "version": "2.0.3",
- "bundled": true
- },
- "ios-sim": {
- "version": "6.1.3",
- "bundled": true,
+ "browserify": {
+ "version": "14.4.0",
+ "resolved": "https://registry.npmjs.org/browserify/-/browserify-14.4.0.tgz",
+ "integrity": "sha1-CJo0Y69Y0OSNjNQHCz90ZU1avKk=",
"requires": {
- "bplist-parser": "^0.0.6",
- "nopt": "1.0.9",
- "plist": "^2.1.0",
- "simctl": "^1.1.1"
+ "JSONStream": "^1.0.3",
+ "assert": "^1.4.0",
+ "browser-pack": "^6.0.1",
+ "browser-resolve": "^1.11.0",
+ "browserify-zlib": "~0.1.2",
+ "buffer": "^5.0.2",
+ "cached-path-relative": "^1.0.0",
+ "concat-stream": "~1.5.1",
+ "console-browserify": "^1.1.0",
+ "constants-browserify": "~1.0.0",
+ "crypto-browserify": "^3.0.0",
+ "defined": "^1.0.0",
+ "deps-sort": "^2.0.0",
+ "domain-browser": "~1.1.0",
+ "duplexer2": "~0.1.2",
+ "events": "~1.1.0",
+ "glob": "^7.1.0",
+ "has": "^1.0.0",
+ "htmlescape": "^1.1.0",
+ "https-browserify": "^1.0.0",
+ "inherits": "~2.0.1",
+ "insert-module-globals": "^7.0.0",
+ "labeled-stream-splicer": "^2.0.0",
+ "module-deps": "^4.0.8",
+ "os-browserify": "~0.1.1",
+ "parents": "^1.0.1",
+ "path-browserify": "~0.0.0",
+ "process": "~0.11.0",
+ "punycode": "^1.3.2",
+ "querystring-es3": "~0.2.0",
+ "read-only-stream": "^2.0.0",
+ "readable-stream": "^2.0.2",
+ "resolve": "^1.1.4",
+ "shasum": "^1.0.0",
+ "shell-quote": "^1.6.1",
+ "stream-browserify": "^2.0.0",
+ "stream-http": "^2.0.0",
+ "string_decoder": "~1.0.0",
+ "subarg": "^1.0.0",
+ "syntax-error": "^1.1.1",
+ "through2": "^2.0.0",
+ "timers-browserify": "^1.0.1",
+ "tty-browserify": "~0.0.0",
+ "url": "~0.11.0",
+ "util": "~0.10.1",
+ "vm-browserify": "~0.0.1",
+ "xtend": "^4.0.0"
},
"dependencies": {
- "bplist-parser": {
- "version": "0.0.6",
- "bundled": true
- },
- "nopt": {
- "version": "1.0.9",
- "bundled": true,
+ "glob": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"requires": {
- "abbrev": "1"
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
}
}
}
},
- "minimatch": {
- "version": "3.0.4",
- "bundled": true,
+ "browserify-aes": {
+ "version": "1.2.0",
+ "resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+ "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
"requires": {
- "brace-expansion": "^1.1.7"
+ "buffer-xor": "^1.0.3",
+ "cipher-base": "^1.0.0",
+ "create-hash": "^1.1.0",
+ "evp_bytestokey": "^1.0.3",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
}
},
- "nopt": {
- "version": "3.0.6",
- "bundled": true,
+ "browserify-cipher": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
+ "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
"requires": {
- "abbrev": "1"
+ "browserify-aes": "^1.0.4",
+ "browserify-des": "^1.0.0",
+ "evp_bytestokey": "^1.0.0"
}
},
- "once": {
- "version": "1.4.0",
- "bundled": true,
+ "browserify-des": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
+ "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
"requires": {
- "wrappy": "1"
+ "cipher-base": "^1.0.1",
+ "des.js": "^1.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
}
},
- "path-is-absolute": {
- "version": "1.0.1",
- "bundled": true
- },
- "plist": {
- "version": "2.1.0",
- "bundled": true,
+ "browserify-rsa": {
+ "version": "4.0.1",
+ "resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
+ "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
"requires": {
- "base64-js": "1.2.0",
- "xmlbuilder": "8.2.2",
- "xmldom": "0.1.x"
+ "bn.js": "^4.1.0",
+ "randombytes": "^2.0.1"
}
},
- "q": {
- "version": "1.5.1",
- "bundled": true
- },
- "sax": {
- "version": "0.3.5",
- "bundled": true
+ "browserify-sign": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.0.4.tgz",
+ "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=",
+ "requires": {
+ "bn.js": "^4.1.1",
+ "browserify-rsa": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "create-hmac": "^1.1.2",
+ "elliptic": "^6.0.0",
+ "inherits": "^2.0.1",
+ "parse-asn1": "^5.0.0"
+ }
},
- "shelljs": {
- "version": "0.5.3",
- "bundled": true
+ "browserify-transform-tools": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/browserify-transform-tools/-/browserify-transform-tools-1.7.0.tgz",
+ "integrity": "sha1-g+J3Ih9jJZvtLn6yooOpcKUB9MQ=",
+ "requires": {
+ "falafel": "^2.0.0",
+ "through": "^2.3.7"
+ }
},
- "simctl": {
- "version": "1.1.1",
- "bundled": true,
+ "browserify-zlib": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.1.4.tgz",
+ "integrity": "sha1-uzX4pRn2AOD6a4SFJByXnQFB+y0=",
"requires": {
- "shelljs": "^0.2.6",
- "tail": "^0.4.0"
- },
- "dependencies": {
- "shelljs": {
- "version": "0.2.6",
- "bundled": true
- }
+ "pako": "~0.2.0"
}
},
- "simple-plist": {
- "version": "0.2.1",
- "bundled": true,
+ "buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.2.1.tgz",
+ "integrity": "sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==",
"requires": {
- "bplist-creator": "0.0.7",
- "bplist-parser": "0.1.1",
- "plist": "2.0.1"
- },
- "dependencies": {
- "base64-js": {
- "version": "1.1.2",
- "bundled": true
- },
- "plist": {
- "version": "2.0.1",
- "bundled": true,
- "requires": {
- "base64-js": "1.1.2",
- "xmlbuilder": "8.2.2",
- "xmldom": "0.1.x"
- }
- }
+ "base64-js": "^1.0.2",
+ "ieee754": "^1.1.4"
}
},
- "stream-buffers": {
- "version": "2.2.0",
- "bundled": true
+ "buffer-from": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz",
+ "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A=="
},
- "tail": {
- "version": "0.4.0",
- "bundled": true
+ "buffer-xor": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
+ "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk="
},
- "underscore": {
- "version": "1.9.1",
- "bundled": true
+ "builtin-modules": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
+ "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8="
},
- "unorm": {
- "version": "1.4.1",
- "bundled": true
+ "builtin-status-codes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
+ "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug="
},
- "uuid": {
- "version": "3.0.1",
- "bundled": true
+ "builtins": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/builtins/-/builtins-1.0.3.tgz",
+ "integrity": "sha1-y5T662HIaWRR2zZTThQi+U8K7og="
},
- "wrappy": {
- "version": "1.0.2",
- "bundled": true
+ "bytes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+ "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg="
},
- "xcode": {
- "version": "0.9.3",
- "bundled": true,
+ "cached-path-relative": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/cached-path-relative/-/cached-path-relative-1.0.1.tgz",
+ "integrity": "sha1-0JxLUoAKpMB44t2BqGmqyQ0uVOc="
+ },
+ "caller-path": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz",
+ "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=",
"requires": {
- "pegjs": "^0.10.0",
- "simple-plist": "^0.2.1",
- "uuid": "3.0.1"
+ "callsites": "^0.2.0"
}
},
- "xml-escape": {
- "version": "1.1.0",
- "bundled": true
+ "callsites": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz",
+ "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo="
},
- "xmlbuilder": {
- "version": "8.2.2",
- "bundled": true
+ "camelcase": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz",
+ "integrity": "sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0="
},
- "xmldom": {
- "version": "0.1.27",
- "bundled": true
- }
- }
- },
- "cordova-plugin-badge": {
- "version": "0.8.8",
- "resolved": "https://registry.npmjs.org/cordova-plugin-badge/-/cordova-plugin-badge-0.8.8.tgz",
- "integrity": "sha512-RhIBtd5xhD/iLnxjt35jvOae28oNW/wtMZBOmQR3Rf0y4wirvA1bpAZEhBoFqL+rZGhsd6ddOdQXdex1T0DRyQ=="
- },
- "cordova-plugin-camera": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/cordova-plugin-camera/-/cordova-plugin-camera-4.0.3.tgz",
- "integrity": "sha1-c3Olk4MYyGzP2E43E+I4LRD+B2s="
- },
- "cordova-plugin-customurlscheme": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/cordova-plugin-customurlscheme/-/cordova-plugin-customurlscheme-4.3.0.tgz",
- "integrity": "sha1-Avlod4tAk5kOsEB/P6GxRY1wX5Q="
- },
- "cordova-plugin-device": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/cordova-plugin-device/-/cordova-plugin-device-2.0.2.tgz",
- "integrity": "sha1-/Ajzci5n7ve2xnv8mag99q3Quro="
- },
- "cordova-plugin-file": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/cordova-plugin-file/-/cordova-plugin-file-6.0.1.tgz",
- "integrity": "sha1-SWBrjBWlaI1HKPkuSnMloGHeB/U="
- },
- "cordova-plugin-file-opener2": {
- "version": "2.0.19",
- "resolved": "https://registry.npmjs.org/cordova-plugin-file-opener2/-/cordova-plugin-file-opener2-2.0.19.tgz",
- "integrity": "sha1-yjrhIlOVt3qx/lsgrMv+zGiOJJM="
- },
- "cordova-plugin-file-transfer": {
- "version": "1.7.1",
- "resolved": "https://registry.npmjs.org/cordova-plugin-file-transfer/-/cordova-plugin-file-transfer-1.7.1.tgz",
- "integrity": "sha1-p12L4uvDu5sjxbG70ZkhTsJnWGs="
- },
- "cordova-plugin-globalization": {
- "version": "1.11.0",
- "resolved": "https://registry.npmjs.org/cordova-plugin-globalization/-/cordova-plugin-globalization-1.11.0.tgz",
- "integrity": "sha1-6sMVgQAphJOvowvolA5pj2HvvP4="
- },
- "cordova-plugin-inappbrowser": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/cordova-plugin-inappbrowser/-/cordova-plugin-inappbrowser-3.0.0.tgz",
- "integrity": "sha1-1K4A02Z2IQdRBXrSWK5K1KkWGto="
- },
- "cordova-plugin-ionic-keyboard": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/cordova-plugin-ionic-keyboard/-/cordova-plugin-ionic-keyboard-2.1.3.tgz",
- "integrity": "sha512-6ucQ6FdlLdBm8kJfFnzozmBTjru/0xekHP/dAhjoCZggkGRlgs8TsUJFkxa/bV+qi7Dlo50JjmpE4UMWAO+aOQ=="
- },
- "cordova-plugin-local-notification": {
- "version": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#5b2f3073a1c1fb39cad3566be792445c343db2c6",
- "from": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle"
- },
- "cordova-plugin-media-capture": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/cordova-plugin-media-capture/-/cordova-plugin-media-capture-3.0.2.tgz",
- "integrity": "sha1-2mV8L6rc/H/cKGjlnSFe2D5wDDo="
- },
- "cordova-plugin-network-information": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/cordova-plugin-network-information/-/cordova-plugin-network-information-2.0.1.tgz",
- "integrity": "sha1-6QQh9DDGq3bUCSI/Jfzvu7zhdpA="
- },
- "cordova-plugin-screen-orientation": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/cordova-plugin-screen-orientation/-/cordova-plugin-screen-orientation-3.0.1.tgz",
- "integrity": "sha1-daNXzik4CB6PYdRgU5S213Rjwfg="
- },
- "cordova-plugin-splashscreen": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/cordova-plugin-splashscreen/-/cordova-plugin-splashscreen-5.0.2.tgz",
- "integrity": "sha1-dH509W4gHNWFvGLRS8oZ9oZ/8e0="
- },
- "cordova-plugin-statusbar": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/cordova-plugin-statusbar/-/cordova-plugin-statusbar-2.4.2.tgz",
- "integrity": "sha1-/B+9wNjXAzp+jh8ff/FnrJvU+vY="
- },
- "cordova-plugin-whitelist": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/cordova-plugin-whitelist/-/cordova-plugin-whitelist-1.3.3.tgz",
- "integrity": "sha1-tehezbv+Wu3tQKG/TuI3LmfZb7Q="
- },
- "cordova-plugin-zip": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/cordova-plugin-zip/-/cordova-plugin-zip-3.1.0.tgz",
- "integrity": "sha1-F2yCSOog058c+VnvXmFWrMqWshc="
- },
- "cordova-sqlite-storage": {
- "version": "2.6.0",
- "resolved": "https://registry.npmjs.org/cordova-sqlite-storage/-/cordova-sqlite-storage-2.6.0.tgz",
- "integrity": "sha512-m+KylFwNRsQloJ6TZihupfma2muXkmNglh8cFSiJQqriTTm6eq0gyPaAuKxgoPswe679G+0aUai07NFC/f0GGQ==",
- "requires": {
- "cordova-sqlite-storage-dependencies": "1.2.1"
- }
- },
- "cordova-sqlite-storage-dependencies": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/cordova-sqlite-storage-dependencies/-/cordova-sqlite-storage-dependencies-1.2.1.tgz",
- "integrity": "sha512-4ihQApBGVKR1QZ4oOSGctKFfthtCfiWMTcIIfxe97vKxlvGr9NyXOvYG9vXU9S7yVR7Ua+Rj47hkE7pQIKvQTg=="
- },
- "cordova-support-google-services": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/cordova-support-google-services/-/cordova-support-google-services-1.2.1.tgz",
- "integrity": "sha512-EnFjKAE9oI2uzyUvEfWpLgTM200nuJVvShaA4yyz9wMKBUN+H/BRG1byd1ibZz3sSihNKi3FxjQPxmmEn6/IfA=="
- },
- "core-js": {
- "version": "2.3.0",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz",
- "integrity": "sha1-+rg/uwstjchfpjbEudNMdUIMbWU="
- },
- "core-util-is": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
- "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
- },
- "create-ecdh": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
- "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==",
- "dev": true,
- "requires": {
- "bn.js": "^4.1.0",
- "elliptic": "^6.0.0"
- }
- },
- "create-hash": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
- "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
- "dev": true,
- "requires": {
- "cipher-base": "^1.0.1",
- "inherits": "^2.0.1",
- "md5.js": "^1.3.4",
- "ripemd160": "^2.0.1",
- "sha.js": "^2.4.0"
- }
- },
- "create-hmac": {
- "version": "1.1.7",
- "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
- "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
- "dev": true,
- "requires": {
- "cipher-base": "^1.0.3",
- "create-hash": "^1.1.0",
- "inherits": "^2.0.1",
- "ripemd160": "^2.0.0",
- "safe-buffer": "^5.0.1",
- "sha.js": "^2.4.8"
- }
- },
- "cross-spawn": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
- "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
- "dev": true,
- "requires": {
- "lru-cache": "^4.0.1",
- "shebang-command": "^1.2.0",
- "which": "^1.2.9"
- }
- },
- "crypto-browserify": {
- "version": "3.12.0",
- "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
- "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
- "dev": true,
- "requires": {
- "browserify-cipher": "^1.0.0",
- "browserify-sign": "^4.0.0",
- "create-ecdh": "^4.0.0",
- "create-hash": "^1.1.0",
- "create-hmac": "^1.1.0",
- "diffie-hellman": "^5.0.0",
- "inherits": "^2.0.1",
- "pbkdf2": "^3.0.3",
- "public-encrypt": "^4.0.0",
- "randombytes": "^2.0.0",
- "randomfill": "^1.0.3"
- }
- },
- "currently-unhandled": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
- "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
- "dev": true,
- "requires": {
- "array-find-index": "^1.0.1"
- }
- },
- "d": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz",
- "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=",
- "dev": true,
- "requires": {
- "es5-ext": "^0.10.9"
- }
- },
- "dashdash": {
- "version": "1.14.1",
- "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
- "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
- "dev": true,
- "requires": {
- "assert-plus": "^1.0.0"
- },
- "dependencies": {
- "assert-plus": {
+ "capture-stack-trace": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz",
+ "integrity": "sha512-mYQLZnx5Qt1JgB1WEiMCf2647plpGeQ2NMR/5L0HNZzGQo4fuSPnK+wjfPnKZV0aiJDgzmWqqkV/g7JD+DW0qw=="
+ },
+ "caseless": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
+ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw="
+ },
+ "chalk": {
+ "version": "1.1.3",
+ "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
+ "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
+ "requires": {
+ "ansi-styles": "^2.2.1",
+ "escape-string-regexp": "^1.0.2",
+ "has-ansi": "^2.0.0",
+ "strip-ansi": "^3.0.0",
+ "supports-color": "^2.0.0"
+ }
+ },
+ "chardet": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz",
+ "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I="
+ },
+ "ci-info": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-1.6.0.tgz",
+ "integrity": "sha512-vsGdkwSCDpWmP80ncATX7iea5DWQemg1UgCW5J8tqjU3lYw4FBYuj89J0CTVomA7BEfvSZd84GmHko+MxFQU2A=="
+ },
+ "cipher-base": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
+ "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==",
+ "requires": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "circular-json": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz",
+ "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A=="
+ },
+ "cli-boxes": {
"version": "1.0.0",
- "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
- "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
- "dev": true
- }
- }
- },
- "date-now": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
- "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=",
- "dev": true
- },
- "dateformat": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz",
- "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=",
- "dev": true
- },
- "debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "dev": true,
- "requires": {
- "ms": "2.0.0"
- }
- },
- "decamelize": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
- "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
- "dev": true
- },
- "decode-uri-component": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
- "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
- "dev": true
- },
- "default-compare": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz",
- "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==",
- "dev": true,
- "requires": {
- "kind-of": "^5.0.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
- "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
- "dev": true
- }
- }
- },
- "default-resolution": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz",
- "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=",
- "dev": true
- },
- "define-properties": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz",
- "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=",
- "requires": {
- "foreach": "^2.0.5",
- "object-keys": "^1.0.8"
- }
- },
- "define-property": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
- "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
- "dev": true,
- "requires": {
- "is-descriptor": "^1.0.2",
- "isobject": "^3.0.1"
- },
- "dependencies": {
- "is-accessor-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
- "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
- "dev": true,
+ "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-1.0.0.tgz",
+ "integrity": "sha1-T6kXw+WclKAEzWH47lCdplFocUM="
+ },
+ "cli-cursor": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz",
+ "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=",
"requires": {
- "kind-of": "^6.0.0"
+ "restore-cursor": "^2.0.0"
}
},
- "is-data-descriptor": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
- "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
- "dev": true,
+ "cli-width": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.0.tgz",
+ "integrity": "sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk="
+ },
+ "co": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
+ "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ="
+ },
+ "code-point-at": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
+ "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"requires": {
- "kind-of": "^6.0.0"
+ "color-name": "1.1.3"
}
},
- "is-descriptor": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
- "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
- "dev": true,
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+ },
+ "combine-source-map": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmjs.org/combine-source-map/-/combine-source-map-0.8.0.tgz",
+ "integrity": "sha1-pY0N8ELBhvz4IqjoAV9UUNLXmos=",
"requires": {
- "is-accessor-descriptor": "^1.0.0",
- "is-data-descriptor": "^1.0.0",
- "kind-of": "^6.0.2"
+ "convert-source-map": "~1.1.0",
+ "inline-source-map": "~0.6.0",
+ "lodash.memoize": "~3.0.3",
+ "source-map": "~0.5.3"
}
},
- "isobject": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
- "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
- "dev": true
+ "combined-stream": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.7.tgz",
+ "integrity": "sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==",
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
},
- "kind-of": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
- "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
- "dev": true
- }
- }
- },
- "delayed-stream": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
- "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
- "dev": true
- },
- "delegates": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
- "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
- "dev": true
- },
- "depd": {
- "version": "1.1.2",
- "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
- "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
- "dev": true
- },
- "des.js": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz",
- "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=",
- "dev": true,
- "requires": {
- "inherits": "^2.0.1",
- "minimalistic-assert": "^1.0.0"
- }
- },
- "destroy": {
- "version": "1.0.4",
- "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
- "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=",
- "dev": true
- },
- "detect-file": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
- "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=",
- "dev": true
- },
- "detect-libc": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
- "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
- "dev": true
- },
- "diff": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
- "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
- "dev": true
- },
- "diffie-hellman": {
- "version": "5.0.3",
- "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
- "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
- "dev": true,
- "requires": {
- "bn.js": "^4.1.0",
- "miller-rabin": "^4.0.0",
- "randombytes": "^2.0.0"
- }
- },
- "doctrine": {
- "version": "0.7.2",
- "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-0.7.2.tgz",
- "integrity": "sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM=",
- "dev": true,
- "requires": {
- "esutils": "^1.1.6",
- "isarray": "0.0.1"
- },
- "dependencies": {
- "esutils": {
- "version": "1.1.6",
- "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz",
- "integrity": "sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U=",
- "dev": true
+ "commander": {
+ "version": "2.17.1",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz",
+ "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==",
+ "optional": true
},
- "isarray": {
+ "compressible": {
+ "version": "2.0.15",
+ "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.15.tgz",
+ "integrity": "sha512-4aE67DL33dSW9gw4CI2H/yTxqHLNcxp0yS6jB+4h+wr3e43+1z7vm0HU9qXOH8j+qjKuL8+UtkOxYQSMq60Ylw==",
+ "requires": {
+ "mime-db": ">= 1.36.0 < 2"
+ }
+ },
+ "compression": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.3.tgz",
+ "integrity": "sha512-HSjyBG5N1Nnz7tF2+O7A9XUhyjru71/fwgNb7oIsEVHR0WShfs2tIS/EySLgiTe98aOK18YDlMXpzjCXY/n9mg==",
+ "requires": {
+ "accepts": "~1.3.5",
+ "bytes": "3.0.0",
+ "compressible": "~2.0.14",
+ "debug": "2.6.9",
+ "on-headers": "~1.0.1",
+ "safe-buffer": "5.1.2",
+ "vary": "~1.1.2"
+ }
+ },
+ "concat-map": {
"version": "0.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
- "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
- "dev": true
- }
- }
- },
- "domain-browser": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
- "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==",
- "dev": true
- },
- "dotenv": {
- "version": "6.2.0",
- "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz",
- "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==",
- "dev": true
- },
- "dotenv-defaults": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/dotenv-defaults/-/dotenv-defaults-1.0.2.tgz",
- "integrity": "sha512-iXFvHtXl/hZPiFj++1hBg4lbKwGM+t/GlvELDnRtOFdjXyWP7mubkVr+eZGWG62kdsbulXAef6v/j6kiWc/xGA==",
- "dev": true,
- "requires": {
- "dotenv": "^6.2.0"
- }
- },
- "dotenv-expand": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-4.2.0.tgz",
- "integrity": "sha1-3vHxyl1gWdJKdm5YeULCEQbOEnU=",
- "dev": true
- },
- "dotenv-webpack": {
- "version": "1.7.0",
- "resolved": "https://registry.npmjs.org/dotenv-webpack/-/dotenv-webpack-1.7.0.tgz",
- "integrity": "sha512-wwNtOBW/6gLQSkb8p43y0Wts970A3xtNiG/mpwj9MLUhtPCQG6i+/DSXXoNN7fbPCU/vQ7JjwGmgOeGZSSZnsw==",
- "dev": true,
- "requires": {
- "dotenv-defaults": "^1.0.2"
- }
- },
- "duplexer2": {
- "version": "0.0.2",
- "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz",
- "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=",
- "dev": true,
- "requires": {
- "readable-stream": "~1.1.9"
- },
- "dependencies": {
- "isarray": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
- "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
- "dev": true
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
- "readable-stream": {
- "version": "1.1.14",
- "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
- "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
- "dev": true,
+ "concat-stream": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.5.2.tgz",
+ "integrity": "sha1-cIl4Yk2FavQaWnQd790mHadSwmY=",
"requires": {
- "core-util-is": "~1.0.0",
"inherits": "~2.0.1",
- "isarray": "0.0.1",
- "string_decoder": "~0.10.x"
+ "readable-stream": "~2.0.0",
+ "typedarray": "~0.0.5"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+ },
+ "process-nextick-args": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz",
+ "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M="
+ },
+ "readable-stream": {
+ "version": "2.0.6",
+ "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.0.6.tgz",
+ "integrity": "sha1-j5A0HmilPMySh4jaz80Rs265t44=",
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~1.0.6",
+ "string_decoder": "~0.10.x",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ="
+ }
}
},
- "string_decoder": {
- "version": "0.10.31",
- "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
- "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
- "dev": true
- }
- }
- },
- "duplexify": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.6.0.tgz",
- "integrity": "sha512-fO3Di4tBKJpYTFHAxTU00BcfWMY9w24r/x21a6rZRbsD/ToUgGxsMbiGRmB7uVAXeGKXD9MwiLZa5E97EVgIRQ==",
- "dev": true,
- "requires": {
- "end-of-stream": "^1.0.0",
- "inherits": "^2.0.1",
- "readable-stream": "^2.0.0",
- "stream-shift": "^1.0.0"
- }
- },
- "each-props": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz",
- "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==",
- "dev": true,
- "requires": {
- "is-plain-object": "^2.0.1",
- "object.defaults": "^1.1.0"
- }
- },
- "ecc-jsbn": {
- "version": "0.1.1",
- "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
- "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=",
- "dev": true,
- "optional": true,
- "requires": {
- "jsbn": "~0.1.0"
- }
- },
- "ee-first": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
- "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
- "dev": true
- },
- "ejs": {
- "version": "2.6.1",
- "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz",
- "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==",
- "dev": true
- },
- "electron-builder-lib": {
- "version": "20.23.1",
- "resolved": "https://registry.npmjs.org/electron-builder-lib/-/electron-builder-lib-20.23.1.tgz",
- "integrity": "sha512-9bYeANVqFPpSmswPwXv8efu9voPE1Q8hw/jNwiWGICjPeYjHyKwa4ao+Vd1beY6ZhUDVhxxXIdlJWnmvH7Mcxw==",
- "dev": true,
- "requires": {
- "7zip-bin": "~4.0.2",
- "app-builder-bin": "1.11.4",
- "async-exit-hook": "^2.0.1",
- "bluebird-lst": "^1.0.5",
- "builder-util": "5.17.0",
- "builder-util-runtime": "4.4.1",
- "chromium-pickle-js": "^0.2.0",
- "debug": "^3.1.0",
- "ejs": "^2.6.1",
- "electron-osx-sign": "0.4.10",
- "electron-publish": "20.23.1",
- "env-paths": "^1.0.0",
- "fs-extra-p": "^4.6.1",
- "hosted-git-info": "^2.7.1",
- "is-ci": "^1.1.0",
- "isbinaryfile": "^3.0.2",
- "js-yaml": "^3.12.0",
- "lazy-val": "^1.0.3",
- "minimatch": "^3.0.4",
- "normalize-package-data": "^2.4.0",
- "plist": "^3.0.1",
- "read-config-file": "3.1.0",
- "sanitize-filename": "^1.6.1",
- "semver": "^5.5.0",
- "sumchecker": "^2.0.2",
- "temp-file": "^3.1.3"
- },
- "dependencies": {
- "app-builder-bin": {
- "version": "1.11.4",
- "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-1.11.4.tgz",
- "integrity": "sha512-04sgoFSz6q5pbAxAXcxfUFPl16gJsay5b8dudFXUwQbFfS7ox2uGgbOO5LGHF0t7sM7q/N82ztGePuuCSkKZHQ==",
- "dev": true
- },
- "builder-util-runtime": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-4.4.1.tgz",
- "integrity": "sha512-8L2pbL6D3VdI1f8OMknlZJpw0c7KK15BRz3cY77AOUElc4XlCv2UhVV01jJM7+6Lx7henaQh80ALULp64eFYAQ==",
- "dev": true,
+ "configstore": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/configstore/-/configstore-3.1.2.tgz",
+ "integrity": "sha512-vtv5HtGjcYUgFrXc6Kx747B83MRRVS5R1VTEQoXvuP+kMI+if6uywV0nDGoiydJRy4yk7h9od5Og0kxx4zUXmw==",
"requires": {
- "bluebird-lst": "^1.0.5",
- "debug": "^3.1.0",
- "fs-extra-p": "^4.6.1",
- "sax": "^1.2.4"
+ "dot-prop": "^4.1.0",
+ "graceful-fs": "^4.1.2",
+ "make-dir": "^1.0.0",
+ "unique-string": "^1.0.0",
+ "write-file-atomic": "^2.0.0",
+ "xdg-basedir": "^3.0.0"
}
},
- "debug": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
- "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
- "dev": true,
+ "console-browserify": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz",
+ "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=",
"requires": {
- "ms": "^2.1.1"
+ "date-now": "^0.1.4"
}
},
- "hosted-git-info": {
- "version": "2.7.1",
- "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
- "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==",
- "dev": true
+ "constants-browserify": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
+ "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U="
},
- "ms": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
- "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
- "dev": true
- }
- }
- },
- "electron-osx-sign": {
- "version": "0.4.10",
- "resolved": "http://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.4.10.tgz",
- "integrity": "sha1-vk87ibKnWh3F8eckkIGrKSnKOiY=",
- "dev": true,
- "requires": {
- "bluebird": "^3.5.0",
- "compare-version": "^0.1.2",
- "debug": "^2.6.8",
- "isbinaryfile": "^3.0.2",
- "minimist": "^1.2.0",
- "plist": "^2.1.0"
- },
- "dependencies": {
- "base64-js": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.0.tgz",
- "integrity": "sha1-o5mS1yNYSBGYK+XikLtqU9hnAPE=",
- "dev": true
+ "contains-path": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz",
+ "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo="
},
- "plist": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/plist/-/plist-2.1.0.tgz",
- "integrity": "sha1-V8zbeggh3yGDEhejytVOPhRqECU=",
- "dev": true,
- "requires": {
- "base64-js": "1.2.0",
- "xmlbuilder": "8.2.2",
- "xmldom": "0.1.x"
- }
+ "content-disposition": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
+ "integrity": "sha1-DPaLud318r55YcOoUXjLhdunjLQ="
},
- "xmlbuilder": {
- "version": "8.2.2",
- "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz",
- "integrity": "sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=",
- "dev": true
- }
- }
- },
- "electron-publish": {
- "version": "20.23.1",
- "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-20.23.1.tgz",
- "integrity": "sha512-FsNL5bY4/Uab5YGYWE+/7wL8znkghEPwJvDyD0985Obw0Eg9JZP1MpUbt4jUxrK8z5wTLVMFdSv7ymV8yq6ypA==",
- "dev": true,
- "requires": {
- "bluebird-lst": "^1.0.5",
- "builder-util": "~5.17.0",
- "builder-util-runtime": "^4.4.1",
- "chalk": "^2.4.1",
- "fs-extra-p": "^4.6.1",
- "lazy-val": "^1.0.3",
- "mime": "^2.3.1"
- },
- "dependencies": {
- "builder-util-runtime": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-4.4.1.tgz",
- "integrity": "sha512-8L2pbL6D3VdI1f8OMknlZJpw0c7KK15BRz3cY77AOUElc4XlCv2UhVV01jJM7+6Lx7henaQh80ALULp64eFYAQ==",
- "dev": true,
+ "content-type": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz",
+ "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA=="
+ },
+ "convert-source-map": {
+ "version": "1.1.3",
+ "resolved": "http://registry.npmjs.org/convert-source-map/-/convert-source-map-1.1.3.tgz",
+ "integrity": "sha1-SCnId+n+SbMWHzvzZziI4gRpmGA="
+ },
+ "cookie": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.3.1.tgz",
+ "integrity": "sha1-5+Ch+e9DtMi6klxcWpboBtFoc7s="
+ },
+ "cookie-signature": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
+ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw="
+ },
+ "cordova-app-hello-world": {
+ "version": "3.12.0",
+ "resolved": "https://registry.npmjs.org/cordova-app-hello-world/-/cordova-app-hello-world-3.12.0.tgz",
+ "integrity": "sha1-Jw4Gtnsq6UvP7mWS7TnrQjA9GG8="
+ },
+ "cordova-common": {
+ "version": "2.2.5",
+ "resolved": "https://registry.npmjs.org/cordova-common/-/cordova-common-2.2.5.tgz",
+ "integrity": "sha1-+TzvKtSUz8v1bEbj1hKqqctfzDI=",
"requires": {
- "bluebird-lst": "^1.0.5",
- "debug": "^3.1.0",
- "fs-extra-p": "^4.6.1",
- "sax": "^1.2.4"
+ "ansi": "^0.3.1",
+ "bplist-parser": "^0.1.0",
+ "cordova-registry-mapper": "^1.1.8",
+ "elementtree": "0.1.6",
+ "glob": "^5.0.13",
+ "minimatch": "^3.0.0",
+ "plist": "^2.1.0",
+ "q": "^1.4.1",
+ "shelljs": "^0.5.3",
+ "underscore": "^1.8.3",
+ "unorm": "^1.3.3"
}
},
- "debug": {
- "version": "3.2.6",
- "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
- "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
- "dev": true,
+ "cordova-create": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/cordova-create/-/cordova-create-1.1.2.tgz",
+ "integrity": "sha1-g7CScbN40cA7x9mnhv7dYEhcPM8=",
"requires": {
- "ms": "^2.1.1"
+ "cordova-app-hello-world": "^3.11.0",
+ "cordova-common": "^2.2.0",
+ "cordova-fetch": "^1.3.0",
+ "q": "1.0.1",
+ "shelljs": "0.3.0",
+ "valid-identifier": "0.0.1"
+ },
+ "dependencies": {
+ "q": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/q/-/q-1.0.1.tgz",
+ "integrity": "sha1-EYcq7t7okmgRCxCnGESP+xARKhQ="
+ },
+ "shelljs": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz",
+ "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E="
+ }
}
},
- "mime": {
- "version": "2.4.0",
- "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz",
- "integrity": "sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==",
- "dev": true
- },
- "ms": {
- "version": "2.1.1",
+ "cordova-fetch": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/cordova-fetch/-/cordova-fetch-1.3.1.tgz",
+ "integrity": "sha512-/0PNQUPxHvVcjlzVQcydD5BQtfx1XdCfzQ2KigdtZma5oVVUtR4IxfnYB15RuT/GVb/SGRLvR5AIi2Gd5Gb+mg==",
+ "requires": {
+ "cordova-common": "^2.2.5",
+ "dependency-ls": "^1.1.0",
+ "hosted-git-info": "^2.5.0",
+ "is-git-url": "^1.0.0",
+ "is-url": "^1.2.1",
+ "q": "^1.4.1",
+ "shelljs": "^0.7.0"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "shelljs": {
+ "version": "0.7.8",
+ "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz",
+ "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=",
+ "requires": {
+ "glob": "^7.0.0",
+ "interpret": "^1.0.0",
+ "rechoir": "^0.6.2"
+ }
+ }
+ }
+ },
+ "cordova-js": {
+ "version": "4.2.4",
+ "resolved": "https://registry.npmjs.org/cordova-js/-/cordova-js-4.2.4.tgz",
+ "integrity": "sha512-Qy0O3w/gwbIqIJzlyCy60nPwJlF1c74ELpsfDIGXB92/uST5nQSSUDVDP4UOfb/c6OU7yPqxhCWOGROyTYKPDw==",
+ "requires": {
+ "browserify": "14.4.0"
+ }
+ },
+ "cordova-lib": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/cordova-lib/-/cordova-lib-8.1.1.tgz",
+ "integrity": "sha512-PcrlEGRGubV2c9ThcSwoVtN/1hKQ0qtwRopl4188rD10gjtt8K+NSKrnRqh6Ia5PouVUUOZBrlhBxDd5BRbfeg==",
+ "requires": {
+ "aliasify": "^2.1.0",
+ "cordova-common": "^2.2.0",
+ "cordova-create": "^1.1.0",
+ "cordova-fetch": "^1.3.0",
+ "cordova-js": "^4.2.2",
+ "cordova-serve": "^2.0.0",
+ "dep-graph": "1.1.0",
+ "dependency-ls": "^1.1.1",
+ "detect-indent": "^5.0.0",
+ "elementtree": "^0.1.7",
+ "glob": "^7.1.2",
+ "init-package-json": "^1.2.0",
+ "nopt": "4.0.1",
+ "opener": "^1.4.3",
+ "plist": "2.0.1",
+ "properties-parser": "0.3.1",
+ "q": "^1.5.1",
+ "read-chunk": "^2.1.0",
+ "request": "^2.88.0",
+ "semver": "^5.3.0",
+ "shebang-command": "^1.2.0",
+ "shelljs": "0.3.0",
+ "tar": "^2.2.1",
+ "underscore": "^1.9.0",
+ "unorm": "^1.4.1",
+ "valid-identifier": "0.0.1",
+ "which": "^1.3.1",
+ "xcode": "^1.0.0"
+ },
+ "dependencies": {
+ "base64-js": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.1.2.tgz",
+ "integrity": "sha1-1kAMrBxMZgl22Q0HoENR2JOV9eg="
+ },
+ "elementtree": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.7.tgz",
+ "integrity": "sha1-mskb5uUvtuYkTE5UpKw+2K6OKcA=",
+ "requires": {
+ "sax": "1.1.4"
+ }
+ },
+ "glob": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "plist": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/plist/-/plist-2.0.1.tgz",
+ "integrity": "sha1-CjLKlIGxw2TpLhjcVch23p0B2os=",
+ "requires": {
+ "base64-js": "1.1.2",
+ "xmlbuilder": "8.2.2",
+ "xmldom": "0.1.x"
+ }
+ },
+ "sax": {
+ "version": "1.1.4",
+ "resolved": "http://registry.npmjs.org/sax/-/sax-1.1.4.tgz",
+ "integrity": "sha1-dLbTPJrh4AFRDxeakRaFiPGu2qk="
+ },
+ "shelljs": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz",
+ "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E="
+ }
+ }
+ },
+ "cordova-registry-mapper": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/cordova-registry-mapper/-/cordova-registry-mapper-1.1.15.tgz",
+ "integrity": "sha1-4kS5GFuBdUc7/2B5MkkFEV+D3Hw="
+ },
+ "cordova-serve": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/cordova-serve/-/cordova-serve-2.0.1.tgz",
+ "integrity": "sha512-3Xl1D5eyiQlY5ow6Kn/say0us2TqSw/zgQmyTLxbewTngQZ1CIqxmqD7EFGoCNBrB4HsdPmpiSpFCitybKQN9g==",
+ "requires": {
+ "chalk": "^1.1.1",
+ "compression": "^1.6.0",
+ "express": "^4.13.3",
+ "opn": "^5.3.0",
+ "shelljs": "^0.5.3"
+ }
+ },
+ "core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+ },
+ "create-ecdh": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
+ "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==",
+ "requires": {
+ "bn.js": "^4.1.0",
+ "elliptic": "^6.0.0"
+ }
+ },
+ "create-error-class": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/create-error-class/-/create-error-class-3.0.2.tgz",
+ "integrity": "sha1-Br56vvlHo/FKMP1hBnHUAbyot7Y=",
+ "requires": {
+ "capture-stack-trace": "^1.0.0"
+ }
+ },
+ "create-hash": {
+ "version": "1.2.0",
+ "resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+ "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+ "requires": {
+ "cipher-base": "^1.0.1",
+ "inherits": "^2.0.1",
+ "md5.js": "^1.3.4",
+ "ripemd160": "^2.0.1",
+ "sha.js": "^2.4.0"
+ }
+ },
+ "create-hmac": {
+ "version": "1.1.7",
+ "resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+ "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+ "requires": {
+ "cipher-base": "^1.0.3",
+ "create-hash": "^1.1.0",
+ "inherits": "^2.0.1",
+ "ripemd160": "^2.0.0",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
+ "cross-spawn": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
+ "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
+ "requires": {
+ "lru-cache": "^4.0.1",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ }
+ },
+ "crypto-browserify": {
+ "version": "3.12.0",
+ "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
+ "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
+ "requires": {
+ "browserify-cipher": "^1.0.0",
+ "browserify-sign": "^4.0.0",
+ "create-ecdh": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "create-hmac": "^1.1.0",
+ "diffie-hellman": "^5.0.0",
+ "inherits": "^2.0.1",
+ "pbkdf2": "^3.0.3",
+ "public-encrypt": "^4.0.0",
+ "randombytes": "^2.0.0",
+ "randomfill": "^1.0.3"
+ }
+ },
+ "crypto-random-string": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-1.0.0.tgz",
+ "integrity": "sha1-ojD2T1aDEOFJgAmUB5DsmVRbyn4="
+ },
+ "currently-unhandled": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
+ "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
+ "requires": {
+ "array-find-index": "^1.0.1"
+ }
+ },
+ "dashdash": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+ "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+ "requires": {
+ "assert-plus": "^1.0.0"
+ }
+ },
+ "date-now": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
+ "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs="
+ },
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "deep-extend": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+ "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="
+ },
+ "deep-is": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
+ "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ="
+ },
+ "defined": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz",
+ "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM="
+ },
+ "del": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz",
+ "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=",
+ "requires": {
+ "globby": "^5.0.0",
+ "is-path-cwd": "^1.0.0",
+ "is-path-in-cwd": "^1.0.0",
+ "object-assign": "^4.0.1",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0",
+ "rimraf": "^2.2.8"
+ },
+ "dependencies": {
+ "pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
+ }
+ }
+ },
+ "delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
+ },
+ "dep-graph": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/dep-graph/-/dep-graph-1.1.0.tgz",
+ "integrity": "sha1-+t6GqSeZqBPptCURzfPfpsyNvv4=",
+ "requires": {
+ "underscore": "1.2.1"
+ },
+ "dependencies": {
+ "underscore": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.2.1.tgz",
+ "integrity": "sha1-/FxrB2VnPZKi1KyLTcCqiHAuK9Q="
+ }
+ }
+ },
+ "depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak="
+ },
+ "dependency-ls": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/dependency-ls/-/dependency-ls-1.1.1.tgz",
+ "integrity": "sha1-BIGwfwI9dM4xEZLlxpDRPhhgAFQ=",
+ "requires": {
+ "q": "1.4.1"
+ },
+ "dependencies": {
+ "q": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz",
+ "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4="
+ }
+ }
+ },
+ "deps-sort": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/deps-sort/-/deps-sort-2.0.0.tgz",
+ "integrity": "sha1-CRckkC6EZYJg65EHSMzNGvbiH7U=",
+ "requires": {
+ "JSONStream": "^1.0.3",
+ "shasum": "^1.0.0",
+ "subarg": "^1.0.0",
+ "through2": "^2.0.0"
+ }
+ },
+ "des.js": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz",
+ "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=",
+ "requires": {
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "destroy": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA="
+ },
+ "detect-indent": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-5.0.0.tgz",
+ "integrity": "sha1-OHHMCmoALow+Wzz38zYmRnXwa50="
+ },
+ "detective": {
+ "version": "4.7.1",
+ "resolved": "https://registry.npmjs.org/detective/-/detective-4.7.1.tgz",
+ "integrity": "sha512-H6PmeeUcZloWtdt4DAkFyzFL94arpHr3NOwwmVILFiy+9Qd4JTxxXrzfyGk/lmct2qVGBwTSwSXagqu2BxmWig==",
+ "requires": {
+ "acorn": "^5.2.1",
+ "defined": "^1.0.0"
+ }
+ },
+ "diffie-hellman": {
+ "version": "5.0.3",
+ "resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+ "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
+ "requires": {
+ "bn.js": "^4.1.0",
+ "miller-rabin": "^4.0.0",
+ "randombytes": "^2.0.0"
+ }
+ },
+ "doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "requires": {
+ "esutils": "^2.0.2"
+ }
+ },
+ "domain-browser": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.1.7.tgz",
+ "integrity": "sha1-hnqksJP6oF8d4IwG9NeyH9+GmLw="
+ },
+ "dot-prop": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz",
+ "integrity": "sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ==",
+ "requires": {
+ "is-obj": "^1.0.0"
+ }
+ },
+ "duplexer2": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
+ "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=",
+ "requires": {
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "duplexer3": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
+ "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI="
+ },
+ "ecc-jsbn": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
+ "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=",
+ "optional": true,
+ "requires": {
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.1.0"
+ }
+ },
+ "editor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/editor/-/editor-1.0.0.tgz",
+ "integrity": "sha1-YMf4e9YrzGqJT6jM1q+3gjok90I="
+ },
+ "ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0="
+ },
+ "elementtree": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/elementtree/-/elementtree-0.1.6.tgz",
+ "integrity": "sha1-KsTEbqMFFsjEy9teOsdBjlkt4gw=",
+ "requires": {
+ "sax": "0.3.5"
+ }
+ },
+ "elliptic": {
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.4.1.tgz",
+ "integrity": "sha512-BsXLz5sqX8OHcsh7CqBMztyXARmGQ3LWPtGjJi6DiJHq5C/qvi9P3OqgswKSDftbu8+IoI/QDTAm2fFnQ9SZSQ==",
+ "requires": {
+ "bn.js": "^4.4.0",
+ "brorand": "^1.0.1",
+ "hash.js": "^1.0.0",
+ "hmac-drbg": "^1.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0",
+ "minimalistic-crypto-utils": "^1.0.0"
+ }
+ },
+ "encodeurl": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
+ "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k="
+ },
+ "error-ex": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz",
+ "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==",
+ "requires": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg="
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+ },
+ "escodegen": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.8.1.tgz",
+ "integrity": "sha1-WltTr0aTEQvrsIZ6o0MN07cKEBg=",
+ "requires": {
+ "esprima": "^2.7.1",
+ "estraverse": "^1.9.1",
+ "esutils": "^2.0.2",
+ "optionator": "^0.8.1",
+ "source-map": "~0.2.0"
+ },
+ "dependencies": {
+ "esprima": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz",
+ "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE="
+ },
+ "estraverse": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.9.3.tgz",
+ "integrity": "sha1-r2fy3JIlgkFZUJJgkaQAXSnJu0Q="
+ },
+ "source-map": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.2.0.tgz",
+ "integrity": "sha1-2rc/vPwrqBm03gO9b26qSBZLP50=",
+ "optional": true,
+ "requires": {
+ "amdefine": ">=0.0.4"
+ }
+ }
+ }
+ },
+ "eslint": {
+ "version": "4.19.1",
+ "resolved": "http://registry.npmjs.org/eslint/-/eslint-4.19.1.tgz",
+ "integrity": "sha512-bT3/1x1EbZB7phzYu7vCr1v3ONuzDtX8WjuM9c0iYxe+cq+pwcKEoQjl7zd3RpC6YOLgnSy3cTN58M2jcoPDIQ==",
+ "requires": {
+ "ajv": "^5.3.0",
+ "babel-code-frame": "^6.22.0",
+ "chalk": "^2.1.0",
+ "concat-stream": "^1.6.0",
+ "cross-spawn": "^5.1.0",
+ "debug": "^3.1.0",
+ "doctrine": "^2.1.0",
+ "eslint-scope": "^3.7.1",
+ "eslint-visitor-keys": "^1.0.0",
+ "espree": "^3.5.4",
+ "esquery": "^1.0.0",
+ "esutils": "^2.0.2",
+ "file-entry-cache": "^2.0.0",
+ "functional-red-black-tree": "^1.0.1",
+ "glob": "^7.1.2",
+ "globals": "^11.0.1",
+ "ignore": "^3.3.3",
+ "imurmurhash": "^0.1.4",
+ "inquirer": "^3.0.6",
+ "is-resolvable": "^1.0.0",
+ "js-yaml": "^3.9.1",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.3.0",
+ "lodash": "^4.17.4",
+ "minimatch": "^3.0.2",
+ "mkdirp": "^0.5.1",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.8.2",
+ "path-is-inside": "^1.0.2",
+ "pluralize": "^7.0.0",
+ "progress": "^2.0.0",
+ "regexpp": "^1.0.1",
+ "require-uncached": "^1.0.3",
+ "semver": "^5.3.0",
+ "strip-ansi": "^4.0.0",
+ "strip-json-comments": "~2.0.1",
+ "table": "4.0.2",
+ "text-table": "~0.2.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "chalk": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+ "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "concat-stream": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+ "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ }
+ },
+ "debug": {
+ "version": "3.2.5",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz",
+ "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==",
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "glob": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "ms": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "eslint-config-semistandard": {
+ "version": "12.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-config-semistandard/-/eslint-config-semistandard-12.0.1.tgz",
+ "integrity": "sha512-4zaPW5uRFasf2uRZkE19Y+W84KBV3q+oyWYOsgUN+5DQXE5HCsh7ZxeWDXxozk7NPycGm0kXcsJzLe5GZ1jCeg=="
+ },
+ "eslint-config-standard": {
+ "version": "11.0.0",
+ "resolved": "http://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-11.0.0.tgz",
+ "integrity": "sha512-oDdENzpViEe5fwuRCWla7AXQd++/oyIp8zP+iP9jiUPG6NBj3SHgdgtl/kTn00AjeN+1HNvavTKmYbMo+xMOlw=="
+ },
+ "eslint-import-resolver-node": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz",
+ "integrity": "sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q==",
+ "requires": {
+ "debug": "^2.6.9",
+ "resolve": "^1.5.0"
+ }
+ },
+ "eslint-module-utils": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.2.0.tgz",
+ "integrity": "sha1-snA2LNiLGkitMIl2zn+lTphBF0Y=",
+ "requires": {
+ "debug": "^2.6.8",
+ "pkg-dir": "^1.0.0"
+ }
+ },
+ "eslint-plugin-import": {
+ "version": "2.14.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.14.0.tgz",
+ "integrity": "sha512-FpuRtniD/AY6sXByma2Wr0TXvXJ4nA/2/04VPlfpmUDPOpOY264x+ILiwnrk/k4RINgDAyFZByxqPUbSQ5YE7g==",
+ "requires": {
+ "contains-path": "^0.1.0",
+ "debug": "^2.6.8",
+ "doctrine": "1.5.0",
+ "eslint-import-resolver-node": "^0.3.1",
+ "eslint-module-utils": "^2.2.0",
+ "has": "^1.0.1",
+ "lodash": "^4.17.4",
+ "minimatch": "^3.0.3",
+ "read-pkg-up": "^2.0.0",
+ "resolve": "^1.6.0"
+ },
+ "dependencies": {
+ "doctrine": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz",
+ "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=",
+ "requires": {
+ "esutils": "^2.0.2",
+ "isarray": "^1.0.0"
+ }
+ },
+ "isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+ }
+ }
+ },
+ "eslint-plugin-node": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-node/-/eslint-plugin-node-5.2.1.tgz",
+ "integrity": "sha512-xhPXrh0Vl/b7870uEbaumb2Q+LxaEcOQ3kS1jtIXanBAwpMre1l5q/l2l/hESYJGEFKuI78bp6Uw50hlpr7B+g==",
+ "requires": {
+ "ignore": "^3.3.6",
+ "minimatch": "^3.0.4",
+ "resolve": "^1.3.3",
+ "semver": "5.3.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz",
+ "integrity": "sha1-myzl094C0XxgEq0yaqa00M9U+U8="
+ }
+ }
+ },
+ "eslint-plugin-promise": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-3.8.0.tgz",
+ "integrity": "sha512-JiFL9UFR15NKpHyGii1ZcvmtIqa3UTwiDAGb8atSffe43qJ3+1czVGN6UtkklpcJ2DVnqvTMzEKRaJdBkAL2aQ=="
+ },
+ "eslint-plugin-standard": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-standard/-/eslint-plugin-standard-3.1.0.tgz",
+ "integrity": "sha512-fVcdyuKRr0EZ4fjWl3c+gp1BANFJD1+RaWa2UPYfMZ6jCtp5RG00kSaXnK/dE5sYzt4kaWJ9qdxqUfc0d9kX0w=="
+ },
+ "eslint-scope": {
+ "version": "3.7.3",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-3.7.3.tgz",
+ "integrity": "sha512-W+B0SvF4gamyCTmUc+uITPY0989iXVfKvhwtmJocTaYoc/3khEHmEmvfY/Gn9HA9VV75jrQECsHizkNw1b68FA==",
+ "requires": {
+ "esrecurse": "^4.1.0",
+ "estraverse": "^4.1.1"
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz",
+ "integrity": "sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ=="
+ },
+ "espree": {
+ "version": "3.5.4",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.4.tgz",
+ "integrity": "sha512-yAcIQxtmMiB/jL32dzEp2enBeidsB7xWPLNiw3IIkpVds1P+h7qF9YwJq1yUNzp2OKXgAprs4F61ih66UsoD1A==",
+ "requires": {
+ "acorn": "^5.5.0",
+ "acorn-jsx": "^3.0.0"
+ }
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
+ },
+ "esquery": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz",
+ "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==",
+ "requires": {
+ "estraverse": "^4.0.0"
+ }
+ },
+ "esrecurse": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz",
+ "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==",
+ "requires": {
+ "estraverse": "^4.1.0"
+ }
+ },
+ "estraverse": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz",
+ "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM="
+ },
+ "esutils": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz",
+ "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs="
+ },
+ "etag": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
+ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc="
+ },
+ "events": {
+ "version": "1.1.1",
+ "resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz",
+ "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ="
+ },
+ "evp_bytestokey": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
+ "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+ "requires": {
+ "md5.js": "^1.3.4",
+ "safe-buffer": "^5.1.1"
+ }
+ },
+ "execa": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz",
+ "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=",
+ "requires": {
+ "cross-spawn": "^5.0.1",
+ "get-stream": "^3.0.0",
+ "is-stream": "^1.1.0",
+ "npm-run-path": "^2.0.0",
+ "p-finally": "^1.0.0",
+ "signal-exit": "^3.0.0",
+ "strip-eof": "^1.0.0"
+ }
+ },
+ "exit-hook": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz",
+ "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g="
+ },
+ "express": {
+ "version": "4.16.3",
+ "resolved": "http://registry.npmjs.org/express/-/express-4.16.3.tgz",
+ "integrity": "sha1-avilAjUNsyRuzEvs9rWjTSL37VM=",
+ "requires": {
+ "accepts": "~1.3.5",
+ "array-flatten": "1.1.1",
+ "body-parser": "1.18.2",
+ "content-disposition": "0.5.2",
+ "content-type": "~1.0.4",
+ "cookie": "0.3.1",
+ "cookie-signature": "1.0.6",
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "1.1.1",
+ "fresh": "0.5.2",
+ "merge-descriptors": "1.0.1",
+ "methods": "~1.1.2",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.2",
+ "path-to-regexp": "0.1.7",
+ "proxy-addr": "~2.0.3",
+ "qs": "6.5.1",
+ "range-parser": "~1.2.0",
+ "safe-buffer": "5.1.1",
+ "send": "0.16.2",
+ "serve-static": "1.13.2",
+ "setprototypeof": "1.1.0",
+ "statuses": "~1.4.0",
+ "type-is": "~1.6.16",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
+ },
+ "dependencies": {
+ "safe-buffer": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
+ "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
+ }
+ }
+ },
+ "extend": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
+ },
+ "external-editor": {
+ "version": "2.2.0",
+ "resolved": "http://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz",
+ "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==",
+ "requires": {
+ "chardet": "^0.4.0",
+ "iconv-lite": "^0.4.17",
+ "tmp": "^0.0.33"
+ }
+ },
+ "extsprintf": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
+ "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU="
+ },
+ "falafel": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/falafel/-/falafel-2.1.0.tgz",
+ "integrity": "sha1-lrsXdh2rqU9G0AFzizzt86Z/4Gw=",
+ "requires": {
+ "acorn": "^5.0.0",
+ "foreach": "^2.0.5",
+ "isarray": "0.0.1",
+ "object-keys": "^1.0.6"
+ }
+ },
+ "fast-deep-equal": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz",
+ "integrity": "sha1-wFNHeBfIa1HaqFPIHgWbcz0CNhQ="
+ },
+ "fast-json-stable-stringify": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz",
+ "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I="
+ },
+ "fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc="
+ },
+ "figures": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz",
+ "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=",
+ "requires": {
+ "escape-string-regexp": "^1.0.5"
+ }
+ },
+ "file-entry-cache": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz",
+ "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=",
+ "requires": {
+ "flat-cache": "^1.2.1",
+ "object-assign": "^4.0.1"
+ }
+ },
+ "finalhandler": {
+ "version": "1.1.1",
+ "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz",
+ "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==",
+ "requires": {
+ "debug": "2.6.9",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.3.0",
+ "parseurl": "~1.3.2",
+ "statuses": "~1.4.0",
+ "unpipe": "~1.0.0"
+ }
+ },
+ "find-up": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz",
+ "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=",
+ "requires": {
+ "path-exists": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ }
+ },
+ "flat-cache": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.3.0.tgz",
+ "integrity": "sha1-0wMLMrOBVPTjt+nHCfSQ9++XxIE=",
+ "requires": {
+ "circular-json": "^0.3.1",
+ "del": "^2.0.2",
+ "graceful-fs": "^4.1.2",
+ "write": "^0.2.1"
+ }
+ },
+ "foreach": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz",
+ "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k="
+ },
+ "forever-agent": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
+ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE="
+ },
+ "form-data": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.2.tgz",
+ "integrity": "sha1-SXBJi+YEwgwAXU9cI67NIda0kJk=",
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "1.0.6",
+ "mime-types": "^2.1.12"
+ },
+ "dependencies": {
+ "combined-stream": {
+ "version": "1.0.6",
+ "resolved": "http://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz",
+ "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=",
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ }
+ }
+ },
+ "forwarded": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz",
+ "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ="
+ },
+ "fresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac="
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
+ },
+ "fstream": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.11.tgz",
+ "integrity": "sha1-XB+x8RdHcRTwYyoOtLcbPLD9MXE=",
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "inherits": "~2.0.0",
+ "mkdirp": ">=0.5 0",
+ "rimraf": "2"
+ }
+ },
+ "function-bind": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
+ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
+ },
+ "functional-red-black-tree": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz",
+ "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc="
+ },
+ "get-assigned-identifiers": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz",
+ "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ=="
+ },
+ "get-stream": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz",
+ "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ="
+ },
+ "getpass": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
+ "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=",
+ "requires": {
+ "assert-plus": "^1.0.0"
+ }
+ },
+ "glob": {
+ "version": "5.0.15",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz",
+ "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=",
+ "requires": {
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "2 || 3",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "global-dirs": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz",
+ "integrity": "sha1-sxnA3UYH81PzvpzKTHL8FIxJ9EU=",
+ "requires": {
+ "ini": "^1.3.4"
+ }
+ },
+ "globals": {
+ "version": "11.7.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-11.7.0.tgz",
+ "integrity": "sha512-K8BNSPySfeShBQXsahYB/AbbWruVOTyVpgoIDnl8odPpeSfP2J5QO2oLFFdl2j7GfDCtZj2bMKar2T49itTPCg=="
+ },
+ "globby": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz",
+ "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=",
+ "requires": {
+ "array-union": "^1.0.1",
+ "arrify": "^1.0.0",
+ "glob": "^7.0.3",
+ "object-assign": "^4.0.1",
+ "pify": "^2.0.0",
+ "pinkie-promise": "^2.0.0"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
+ }
+ }
+ },
+ "got": {
+ "version": "6.7.1",
+ "resolved": "http://registry.npmjs.org/got/-/got-6.7.1.tgz",
+ "integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=",
+ "requires": {
+ "create-error-class": "^3.0.0",
+ "duplexer3": "^0.1.4",
+ "get-stream": "^3.0.0",
+ "is-redirect": "^1.0.0",
+ "is-retry-allowed": "^1.0.0",
+ "is-stream": "^1.0.0",
+ "lowercase-keys": "^1.0.0",
+ "safe-buffer": "^5.0.1",
+ "timed-out": "^4.0.0",
+ "unzip-response": "^2.0.1",
+ "url-parse-lax": "^1.0.0"
+ }
+ },
+ "graceful-fs": {
+ "version": "4.1.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz",
+ "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
+ },
+ "handlebars": {
+ "version": "4.0.12",
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.0.12.tgz",
+ "integrity": "sha512-RhmTekP+FZL+XNhwS1Wf+bTTZpdLougwt5pcgA1tuz6Jcx0fpH/7z0qd71RKnZHBCxIRBHfBOnio4gViPemNzA==",
+ "requires": {
+ "async": "^2.5.0",
+ "optimist": "^0.6.1",
+ "source-map": "^0.6.1",
+ "uglify-js": "^3.1.4"
+ },
+ "dependencies": {
+ "async": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz",
+ "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==",
+ "requires": {
+ "lodash": "^4.17.10"
+ }
+ },
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ }
+ }
+ },
+ "har-schema": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
+ "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI="
+ },
+ "har-validator": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.0.tgz",
+ "integrity": "sha512-+qnmNjI4OfH2ipQ9VQOw23bBd/ibtfbVdK2fYbY4acTDqKTW/YDp9McimZdDbG8iV9fZizUqQMD5xvriB146TA==",
+ "requires": {
+ "ajv": "^5.3.0",
+ "har-schema": "^2.0.0"
+ }
+ },
+ "has": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
+ "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==",
+ "requires": {
+ "function-bind": "^1.1.1"
+ }
+ },
+ "has-ansi": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz",
+ "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=",
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+ },
+ "hash-base": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.4.tgz",
+ "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=",
+ "requires": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "hash.js": {
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.5.tgz",
+ "integrity": "sha512-eWI5HG9Np+eHV1KQhisXWwM+4EPPYe5dFX1UZZH7k/E3JzDEazVH+VGlZi6R94ZqImq+A3D1mCEtrFIfg/E7sA==",
+ "requires": {
+ "inherits": "^2.0.3",
+ "minimalistic-assert": "^1.0.1"
+ }
+ },
+ "hmac-drbg": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+ "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=",
+ "requires": {
+ "hash.js": "^1.0.3",
+ "minimalistic-assert": "^1.0.0",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
+ "hosted-git-info": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
+ "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w=="
+ },
+ "htmlescape": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/htmlescape/-/htmlescape-1.1.1.tgz",
+ "integrity": "sha1-OgPtwiFLyjtmQko+eVk0lQnLA1E="
+ },
+ "http-errors": {
+ "version": "1.6.3",
+ "resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
+ "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
+ "requires": {
+ "depd": "~1.1.2",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.1.0",
+ "statuses": ">= 1.4.0 < 2"
+ }
+ },
+ "http-signature": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
+ "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=",
+ "requires": {
+ "assert-plus": "^1.0.0",
+ "jsprim": "^1.2.2",
+ "sshpk": "^1.7.0"
+ }
+ },
+ "https-browserify": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
+ "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM="
+ },
+ "iconv-lite": {
+ "version": "0.4.19",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.19.tgz",
+ "integrity": "sha512-oTZqweIP51xaGPI4uPa56/Pri/480R+mo7SeU+YETByQNhDG55ycFyNLIgta9vXhILrxXDmF7ZGhqZIcuN0gJQ=="
+ },
+ "ieee754": {
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.12.tgz",
+ "integrity": "sha512-GguP+DRY+pJ3soyIiGPTvdiVXjZ+DbXOxGpXn3eMvNW4x4irjqXm4wHKscC+TfxSJ0yw/S1F24tqdMNsMZTiLA=="
+ },
+ "ignore": {
+ "version": "3.3.10",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz",
+ "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug=="
+ },
+ "import-lazy": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz",
+ "integrity": "sha1-BWmOPUXIjo1+nZLLBYTnfwlvPkM="
+ },
+ "imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o="
+ },
+ "indexof": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz",
+ "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10="
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
+ "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
+ },
+ "ini": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz",
+ "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw=="
+ },
+ "init-package-json": {
+ "version": "1.10.3",
+ "resolved": "https://registry.npmjs.org/init-package-json/-/init-package-json-1.10.3.tgz",
+ "integrity": "sha512-zKSiXKhQveNteyhcj1CoOP8tqp1QuxPIPBl8Bid99DGLFqA1p87M6lNgfjJHSBoWJJlidGOv5rWjyYKEB3g2Jw==",
+ "requires": {
+ "glob": "^7.1.1",
+ "npm-package-arg": "^4.0.0 || ^5.0.0 || ^6.0.0",
+ "promzard": "^0.3.0",
+ "read": "~1.0.1",
+ "read-package-json": "1 || 2",
+ "semver": "2.x || 3.x || 4 || 5",
+ "validate-npm-package-license": "^3.0.1",
+ "validate-npm-package-name": "^3.0.0"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ }
+ }
+ },
+ "inline-source-map": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/inline-source-map/-/inline-source-map-0.6.2.tgz",
+ "integrity": "sha1-+Tk0ccGKedFyT4Y/o4tYY3Ct4qU=",
+ "requires": {
+ "source-map": "~0.5.3"
+ }
+ },
+ "inquirer": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-3.3.0.tgz",
+ "integrity": "sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ==",
+ "requires": {
+ "ansi-escapes": "^3.0.0",
+ "chalk": "^2.0.0",
+ "cli-cursor": "^2.1.0",
+ "cli-width": "^2.0.0",
+ "external-editor": "^2.0.4",
+ "figures": "^2.0.0",
+ "lodash": "^4.3.0",
+ "mute-stream": "0.0.7",
+ "run-async": "^2.2.0",
+ "rx-lite": "^4.0.8",
+ "rx-lite-aggregates": "^4.0.8",
+ "string-width": "^2.1.0",
+ "strip-ansi": "^4.0.0",
+ "through": "^2.3.6"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "chalk": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+ "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "insert-module-globals": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/insert-module-globals/-/insert-module-globals-7.2.0.tgz",
+ "integrity": "sha512-VE6NlW+WGn2/AeOMd496AHFYmE7eLKkUY6Ty31k4og5vmA3Fjuwe9v6ifH6Xx/Hz27QvdoMoviw1/pqWRB09Sw==",
+ "requires": {
+ "JSONStream": "^1.0.3",
+ "acorn-node": "^1.5.2",
+ "combine-source-map": "^0.8.0",
+ "concat-stream": "^1.6.1",
+ "is-buffer": "^1.1.0",
+ "path-is-absolute": "^1.0.1",
+ "process": "~0.11.0",
+ "through2": "^2.0.0",
+ "undeclared-identifiers": "^1.1.2",
+ "xtend": "^4.0.0"
+ },
+ "dependencies": {
+ "concat-stream": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz",
+ "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==",
+ "requires": {
+ "buffer-from": "^1.0.0",
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.2.2",
+ "typedarray": "^0.0.6"
+ }
+ }
+ }
+ },
+ "insight": {
+ "version": "0.8.4",
+ "resolved": "https://registry.npmjs.org/insight/-/insight-0.8.4.tgz",
+ "integrity": "sha1-ZxyvZbR8n+jD0bMgbPRbshG3WIQ=",
+ "requires": {
+ "async": "^1.4.2",
+ "chalk": "^1.0.0",
+ "configstore": "^1.0.0",
+ "inquirer": "^0.10.0",
+ "lodash.debounce": "^3.0.1",
+ "object-assign": "^4.0.1",
+ "os-name": "^1.0.0",
+ "request": "^2.74.0",
+ "tough-cookie": "^2.0.0",
+ "uuid": "^3.0.0"
+ },
+ "dependencies": {
+ "ansi-escapes": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz",
+ "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4="
+ },
+ "cli-cursor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz",
+ "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=",
+ "requires": {
+ "restore-cursor": "^1.0.1"
+ }
+ },
+ "cli-width": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-1.1.1.tgz",
+ "integrity": "sha1-pNKT72frt7iNSk1CwMzwDE0eNm0="
+ },
+ "configstore": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/configstore/-/configstore-1.4.0.tgz",
+ "integrity": "sha1-w1eB0FAdJowlxUuLF/YkDopPsCE=",
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "mkdirp": "^0.5.0",
+ "object-assign": "^4.0.1",
+ "os-tmpdir": "^1.0.0",
+ "osenv": "^0.1.0",
+ "uuid": "^2.0.1",
+ "write-file-atomic": "^1.1.2",
+ "xdg-basedir": "^2.0.0"
+ },
+ "dependencies": {
+ "uuid": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-2.0.3.tgz",
+ "integrity": "sha1-Z+LoY3lyFVMN/zGOW/nc6/1Hsho="
+ }
+ }
+ },
+ "figures": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz",
+ "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=",
+ "requires": {
+ "escape-string-regexp": "^1.0.5",
+ "object-assign": "^4.1.0"
+ }
+ },
+ "inquirer": {
+ "version": "0.10.1",
+ "resolved": "http://registry.npmjs.org/inquirer/-/inquirer-0.10.1.tgz",
+ "integrity": "sha1-6iXkzmnKFF4FyZ5G3P7AXkASWUo=",
+ "requires": {
+ "ansi-escapes": "^1.1.0",
+ "ansi-regex": "^2.0.0",
+ "chalk": "^1.0.0",
+ "cli-cursor": "^1.0.1",
+ "cli-width": "^1.0.1",
+ "figures": "^1.3.5",
+ "lodash": "^3.3.1",
+ "readline2": "^1.0.1",
+ "run-async": "^0.1.0",
+ "rx-lite": "^3.1.2",
+ "strip-ansi": "^3.0.0",
+ "through": "^2.3.6"
+ }
+ },
+ "lodash": {
+ "version": "3.10.1",
+ "resolved": "http://registry.npmjs.org/lodash/-/lodash-3.10.1.tgz",
+ "integrity": "sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y="
+ },
+ "onetime": {
+ "version": "1.1.0",
+ "resolved": "http://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz",
+ "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k="
+ },
+ "restore-cursor": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz",
+ "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=",
+ "requires": {
+ "exit-hook": "^1.0.0",
+ "onetime": "^1.0.0"
+ }
+ },
+ "run-async": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz",
+ "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=",
+ "requires": {
+ "once": "^1.3.0"
+ }
+ },
+ "rx-lite": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz",
+ "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI="
+ },
+ "write-file-atomic": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-1.3.4.tgz",
+ "integrity": "sha1-+Aek8LHZ6ROuekgRLmzDrxmRtF8=",
+ "requires": {
+ "graceful-fs": "^4.1.11",
+ "imurmurhash": "^0.1.4",
+ "slide": "^1.1.5"
+ }
+ },
+ "xdg-basedir": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-2.0.0.tgz",
+ "integrity": "sha1-7byQPMOF/ARSPZZqM1UEtVBNG9I=",
+ "requires": {
+ "os-homedir": "^1.0.0"
+ }
+ }
+ }
+ },
+ "interpret": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz",
+ "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ="
+ },
+ "ipaddr.js": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
+ "integrity": "sha1-6qM9bd16zo9/b+DJygRA5wZzix4="
+ },
+ "is-arrayish": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
+ "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0="
+ },
+ "is-buffer": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
+ },
+ "is-builtin-module": {
+ "version": "1.0.0",
+ "resolved": "http://registry.npmjs.org/is-builtin-module/-/is-builtin-module-1.0.0.tgz",
+ "integrity": "sha1-VAVy0096wxGfj3bDDLwbHgN6/74=",
+ "requires": {
+ "builtin-modules": "^1.0.0"
+ }
+ },
+ "is-ci": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-1.2.1.tgz",
+ "integrity": "sha512-s6tfsaQaQi3JNciBH6shVqEDvhGut0SUXr31ag8Pd8BBbVVlcGfWhpPmEOoM6RJ5TFhbypvf5yyRw/VXW1IiWg==",
+ "requires": {
+ "ci-info": "^1.5.0"
+ }
+ },
+ "is-fullwidth-code-point": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz",
+ "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8="
+ },
+ "is-git-url": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-git-url/-/is-git-url-1.0.0.tgz",
+ "integrity": "sha1-U/aEzRQyhbUsMkS05vKCU1J69ms="
+ },
+ "is-installed-globally": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.1.0.tgz",
+ "integrity": "sha1-Df2Y9akRFxbdU13aZJL2e/PSWoA=",
+ "requires": {
+ "global-dirs": "^0.1.0",
+ "is-path-inside": "^1.0.0"
+ }
+ },
+ "is-npm": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-1.0.0.tgz",
+ "integrity": "sha1-8vtjpl5JBbQGyGBydloaTceTufQ="
+ },
+ "is-obj": {
+ "version": "1.0.1",
+ "resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
+ "integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8="
+ },
+ "is-path-cwd": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz",
+ "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0="
+ },
+ "is-path-in-cwd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.1.tgz",
+ "integrity": "sha512-FjV1RTW48E7CWM7eE/J2NJvAEEVektecDBVBE5Hh3nM1Jd0kvhHtX68Pr3xsDf857xt3Y4AkwVULK1Vku62aaQ==",
+ "requires": {
+ "is-path-inside": "^1.0.0"
+ }
+ },
+ "is-path-inside": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.1.tgz",
+ "integrity": "sha1-jvW33lBDej/cprToZe96pVy0gDY=",
+ "requires": {
+ "path-is-inside": "^1.0.1"
+ }
+ },
+ "is-promise": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.1.0.tgz",
+ "integrity": "sha1-eaKp7OfwlugPNtKy87wWwf9L8/o="
+ },
+ "is-redirect": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-redirect/-/is-redirect-1.0.0.tgz",
+ "integrity": "sha1-HQPd7VO9jbDzDCbk+V02/HyH3CQ="
+ },
+ "is-resolvable": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz",
+ "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg=="
+ },
+ "is-retry-allowed": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz",
+ "integrity": "sha1-EaBgVotnM5REAz0BJaYaINVk+zQ="
+ },
+ "is-stream": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
+ "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
+ },
+ "is-typedarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
+ "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo="
+ },
+ "is-url": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
+ "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww=="
+ },
+ "is-wsl": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz",
+ "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0="
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8="
+ },
+ "isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA="
+ },
+ "isstream": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
+ "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo="
+ },
+ "istanbul": {
+ "version": "0.4.5",
+ "resolved": "https://registry.npmjs.org/istanbul/-/istanbul-0.4.5.tgz",
+ "integrity": "sha1-ZcfXPUxNqE1POsMQuRj7C4Azczs=",
+ "requires": {
+ "abbrev": "1.0.x",
+ "async": "1.x",
+ "escodegen": "1.8.x",
+ "esprima": "2.7.x",
+ "glob": "^5.0.15",
+ "handlebars": "^4.0.1",
+ "js-yaml": "3.x",
+ "mkdirp": "0.5.x",
+ "nopt": "3.x",
+ "once": "1.x",
+ "resolve": "1.1.x",
+ "supports-color": "^3.1.0",
+ "which": "^1.1.1",
+ "wordwrap": "^1.0.0"
+ },
+ "dependencies": {
+ "abbrev": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.0.9.tgz",
+ "integrity": "sha1-kbR5JYinc4wl813W9jdSovh3YTU="
+ },
+ "esprima": {
+ "version": "2.7.3",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-2.7.3.tgz",
+ "integrity": "sha1-luO3DVd59q1JzQMmc9HDEnZ7pYE="
+ },
+ "has-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
+ "integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo="
+ },
+ "nopt": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz",
+ "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=",
+ "requires": {
+ "abbrev": "1"
+ }
+ },
+ "resolve": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz",
+ "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs="
+ },
+ "supports-color": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
+ "integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY=",
+ "requires": {
+ "has-flag": "^1.0.0"
+ }
+ }
+ }
+ },
+ "jasmine": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-3.2.0.tgz",
+ "integrity": "sha512-qv6TZ32r+slrQz8fbx2EhGbD9zlJo3NwPrpLK1nE8inILtZO9Fap52pyHk7mNTh4tG50a+1+tOiWVT3jO5I0Sg==",
+ "requires": {
+ "glob": "^7.0.6",
+ "jasmine-core": "~3.2.0"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ }
+ }
+ },
+ "jasmine-core": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.2.1.tgz",
+ "integrity": "sha512-pa9tbBWgU0EE4SWgc85T4sa886ufuQdsgruQANhECYjwqgV4z7Vw/499aCaP8ZH79JDS4vhm8doDG9HO4+e4sA=="
+ },
+ "js-tokens": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz",
+ "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls="
+ },
+ "js-yaml": {
+ "version": "3.12.0",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.12.0.tgz",
+ "integrity": "sha512-PIt2cnwmPfL4hKNwqeiuz4bKfnzHTBv6HyVgjahA6mPLwPDzjDWrplJBMjHUFxku/N3FlmrbyPclad+I+4mJ3A==",
+ "requires": {
+ "argparse": "^1.0.7",
+ "esprima": "^4.0.0"
+ }
+ },
+ "jsbn": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
+ "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=",
+ "optional": true
+ },
+ "json-parse-better-errors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz",
+ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="
+ },
+ "json-schema": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz",
+ "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM="
+ },
+ "json-schema-traverse": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.3.1.tgz",
+ "integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A="
+ },
+ "json-stable-stringify": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-0.0.1.tgz",
+ "integrity": "sha1-YRwj6BTbN1Un34URk9tZ3Sryf0U=",
+ "requires": {
+ "jsonify": "~0.0.0"
+ }
+ },
+ "json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE="
+ },
+ "json-stringify-safe": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
+ "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus="
+ },
+ "jsonify": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
+ "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM="
+ },
+ "jsonparse": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
+ "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA="
+ },
+ "jsprim": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
+ "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=",
+ "requires": {
+ "assert-plus": "1.0.0",
+ "extsprintf": "1.3.0",
+ "json-schema": "0.2.3",
+ "verror": "1.10.0"
+ }
+ },
+ "labeled-stream-splicer": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/labeled-stream-splicer/-/labeled-stream-splicer-2.0.1.tgz",
+ "integrity": "sha512-MC94mHZRvJ3LfykJlTUipBqenZz1pacOZEMhhQ8dMGcDHs0SBE5GbsavUXV7YtP3icBW17W0Zy1I0lfASmo9Pg==",
+ "requires": {
+ "inherits": "^2.0.1",
+ "isarray": "^2.0.4",
+ "stream-splicer": "^2.0.0"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.4.tgz",
+ "integrity": "sha512-GMxXOiUirWg1xTKRipM0Ek07rX+ubx4nNVElTJdNLYmNO/2YrDkgJGw9CljXn+r4EWiDQg/8lsRdHyg2PJuUaA=="
+ }
+ }
+ },
+ "latest-version": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-3.1.0.tgz",
+ "integrity": "sha1-ogU4P+oyKzO1rjsYq+4NwvNW7hU=",
+ "requires": {
+ "package-json": "^4.0.0"
+ }
+ },
+ "levn": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
+ "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=",
+ "requires": {
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2"
+ }
+ },
+ "load-json-file": {
+ "version": "2.0.0",
+ "resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
+ "integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=",
+ "requires": {
+ "graceful-fs": "^4.1.2",
+ "parse-json": "^2.2.0",
+ "pify": "^2.0.0",
+ "strip-bom": "^3.0.0"
+ },
+ "dependencies": {
+ "pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
+ }
+ }
+ },
+ "locate-path": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz",
+ "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=",
+ "requires": {
+ "p-locate": "^2.0.0",
+ "path-exists": "^3.0.0"
+ },
+ "dependencies": {
+ "path-exists": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
+ "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU="
+ }
+ }
+ },
+ "lodash": {
+ "version": "4.17.11",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
+ "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg=="
+ },
+ "lodash._getnative": {
+ "version": "3.9.1",
+ "resolved": "https://registry.npmjs.org/lodash._getnative/-/lodash._getnative-3.9.1.tgz",
+ "integrity": "sha1-VwvH3t5G1hzc3mh9ZdPuy6o6r/U="
+ },
+ "lodash.debounce": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-3.1.1.tgz",
+ "integrity": "sha1-gSIRw3ipTMKdWqTjNGzwv846ffU=",
+ "requires": {
+ "lodash._getnative": "^3.0.0"
+ }
+ },
+ "lodash.memoize": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-3.0.4.tgz",
+ "integrity": "sha1-LcvSwofLwKVcxCMovQxzYVDVPj8="
+ },
+ "loud-rejection": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz",
+ "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=",
+ "requires": {
+ "currently-unhandled": "^0.4.1",
+ "signal-exit": "^3.0.0"
+ }
+ },
+ "lowercase-keys": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz",
+ "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA=="
+ },
+ "lru-cache": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.3.tgz",
+ "integrity": "sha512-fFEhvcgzuIoJVUF8fYr5KR0YqxD238zgObTps31YdADwPPAp82a4M8TrckkWyx7ekNlf9aBcVn81cFwwXngrJA==",
+ "requires": {
+ "pseudomap": "^1.0.2",
+ "yallist": "^2.1.2"
+ },
+ "dependencies": {
+ "yallist": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",
+ "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI="
+ }
+ }
+ },
+ "make-dir": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz",
+ "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==",
+ "requires": {
+ "pify": "^3.0.0"
+ }
+ },
+ "md5.js": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
+ "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
+ "requires": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "media-typer": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g="
+ },
+ "merge-descriptors": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
+ "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
+ },
+ "methods": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
+ "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4="
+ },
+ "miller-rabin": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
+ "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
+ "requires": {
+ "bn.js": "^4.0.0",
+ "brorand": "^1.0.1"
+ }
+ },
+ "mime": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.4.1.tgz",
+ "integrity": "sha512-KI1+qOZu5DcW6wayYHSzR/tXKCDC5Om4s1z2QJjDULzLcmf3DvzS7oluY4HCTrc+9FiKmWUgeNLg7W3uIQvxtQ=="
+ },
+ "mime-db": {
+ "version": "1.36.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.36.0.tgz",
+ "integrity": "sha512-L+xvyD9MkoYMXb1jAmzI/lWYAxAMCPvIBSWur0PZ5nOf5euahRLVqH//FKW9mWp2lkqUgYiXPgkzfMUFi4zVDw=="
+ },
+ "mime-types": {
+ "version": "2.1.20",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.20.tgz",
+ "integrity": "sha512-HrkrPaP9vGuWbLK1B1FfgAkbqNjIuy4eHlIYnFi7kamZyLLrGlo2mpcx0bBmNpKqBtYtAfGbodDddIgddSJC2A==",
+ "requires": {
+ "mime-db": "~1.36.0"
+ }
+ },
+ "mimic-fn": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz",
+ "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ=="
+ },
+ "minimalistic-assert": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
+ },
+ "minimalistic-crypto-utils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+ "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo="
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
+ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+ },
+ "mkdirp": {
+ "version": "0.5.1",
+ "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+ "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+ "requires": {
+ "minimist": "0.0.8"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "0.0.8",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
+ }
+ }
+ },
+ "module-deps": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/module-deps/-/module-deps-4.1.1.tgz",
+ "integrity": "sha1-IyFYM/HaE/1gbMuAh7RIUty4If0=",
+ "requires": {
+ "JSONStream": "^1.0.3",
+ "browser-resolve": "^1.7.0",
+ "cached-path-relative": "^1.0.0",
+ "concat-stream": "~1.5.0",
+ "defined": "^1.0.0",
+ "detective": "^4.0.0",
+ "duplexer2": "^0.1.2",
+ "inherits": "^2.0.1",
+ "parents": "^1.0.0",
+ "readable-stream": "^2.0.2",
+ "resolve": "^1.1.3",
+ "stream-combiner2": "^1.1.1",
+ "subarg": "^1.0.0",
+ "through2": "^2.0.0",
+ "xtend": "^4.0.0"
+ }
+ },
+ "ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
+ },
+ "mute-stream": {
+ "version": "0.0.7",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz",
+ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s="
+ },
+ "natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc="
+ },
+ "negotiator": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.1.tgz",
+ "integrity": "sha1-KzJxhOiZIQEXeyhWP7XnECrNDKk="
+ },
+ "nopt": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz",
+ "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=",
+ "requires": {
+ "abbrev": "1",
+ "osenv": "^0.1.4"
+ }
+ },
+ "normalize-package-data": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.4.0.tgz",
+ "integrity": "sha512-9jjUFbTPfEy3R/ad/2oNbKtW9Hgovl5O1FvFWKkKblNXoN/Oou6+9+KKohPK13Yc3/TyunyWhJp6gvRNR/PPAw==",
+ "requires": {
+ "hosted-git-info": "^2.1.4",
+ "is-builtin-module": "^1.0.0",
+ "semver": "2 || 3 || 4 || 5",
+ "validate-npm-package-license": "^3.0.1"
+ }
+ },
+ "npm-package-arg": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.0.tgz",
+ "integrity": "sha512-zYbhP2k9DbJhA0Z3HKUePUgdB1x7MfIfKssC+WLPFMKTBZKpZh5m13PgexJjCq6KW7j17r0jHWcCpxEqnnncSA==",
+ "requires": {
+ "hosted-git-info": "^2.6.0",
+ "osenv": "^0.1.5",
+ "semver": "^5.5.0",
+ "validate-npm-package-name": "^3.0.0"
+ }
+ },
+ "npm-run-path": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
+ "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=",
+ "requires": {
+ "path-key": "^2.0.0"
+ }
+ },
+ "number-is-nan": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
+ "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
+ },
+ "oauth-sign": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
+ "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
+ "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM="
+ },
+ "object-keys": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.12.tgz",
+ "integrity": "sha512-FTMyFUm2wBcGHnH2eXmz7tC6IwlqQZ6mVZ+6dm6vZ4IQIHjs6FdNsQBuKGPuUUUY6NfJw2PshC08Tn6LzLDOag=="
+ },
+ "on-finished": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
+ "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=",
+ "requires": {
+ "ee-first": "1.1.1"
+ }
+ },
+ "on-headers": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.1.tgz",
+ "integrity": "sha1-ko9dD0cNSTQmUepnlLCFfBAGk/c="
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "onetime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz",
+ "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=",
+ "requires": {
+ "mimic-fn": "^1.0.0"
+ }
+ },
+ "opener": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.1.tgz",
+ "integrity": "sha512-goYSy5c2UXE4Ra1xixabeVh1guIX/ZV/YokJksb6q2lubWu6UbvPQ20p542/sFIll1nl8JnCyK9oBaOcCWXwvA=="
+ },
+ "opn": {
+ "version": "5.4.0",
+ "resolved": "https://registry.npmjs.org/opn/-/opn-5.4.0.tgz",
+ "integrity": "sha512-YF9MNdVy/0qvJvDtunAOzFw9iasOQHpVthTCvGzxt61Il64AYSGdK+rYwld7NAfk9qJ7dt+hymBNSc9LNYS+Sw==",
+ "requires": {
+ "is-wsl": "^1.1.0"
+ }
+ },
+ "optimist": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
+ "integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
+ "requires": {
+ "minimist": "~0.0.1",
+ "wordwrap": "~0.0.2"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "0.0.10",
+ "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
+ "integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8="
+ },
+ "wordwrap": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
+ "integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc="
+ }
+ }
+ },
+ "optionator": {
+ "version": "0.8.2",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz",
+ "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=",
+ "requires": {
+ "deep-is": "~0.1.3",
+ "fast-levenshtein": "~2.0.4",
+ "levn": "~0.3.0",
+ "prelude-ls": "~1.1.2",
+ "type-check": "~0.3.2",
+ "wordwrap": "~1.0.0"
+ }
+ },
+ "os-browserify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.1.2.tgz",
+ "integrity": "sha1-ScoCk+CxlZCl9d4Qx/JlphfY/lQ="
+ },
+ "os-homedir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz",
+ "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M="
+ },
+ "os-name": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/os-name/-/os-name-1.0.3.tgz",
+ "integrity": "sha1-GzefZINa98Wn9JizV8uVIVwVnt8=",
+ "requires": {
+ "osx-release": "^1.0.0",
+ "win-release": "^1.0.0"
+ }
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
+ "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ="
+ },
+ "osenv": {
+ "version": "0.1.5",
+ "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz",
+ "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==",
+ "requires": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.0"
+ }
+ },
+ "osx-release": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/osx-release/-/osx-release-1.1.0.tgz",
+ "integrity": "sha1-8heRGigTaUmvG/kwiyQeJzfTzWw=",
+ "requires": {
+ "minimist": "^1.1.0"
+ }
+ },
+ "p-finally": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
+ "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
+ },
+ "p-limit": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz",
+ "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==",
+ "requires": {
+ "p-try": "^1.0.0"
+ }
+ },
+ "p-locate": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz",
+ "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=",
+ "requires": {
+ "p-limit": "^1.1.0"
+ }
+ },
+ "p-try": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz",
+ "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M="
+ },
+ "package-json": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/package-json/-/package-json-4.0.1.tgz",
+ "integrity": "sha1-iGmgQBJTZhxMTKPabCEh7VVfXu0=",
+ "requires": {
+ "got": "^6.7.1",
+ "registry-auth-token": "^3.0.1",
+ "registry-url": "^3.0.3",
+ "semver": "^5.1.0"
+ }
+ },
+ "pako": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz",
+ "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU="
+ },
+ "parents": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/parents/-/parents-1.0.1.tgz",
+ "integrity": "sha1-/t1NK/GTp3dF/nHjcdc8MwfZx1E=",
+ "requires": {
+ "path-platform": "~0.11.15"
+ }
+ },
+ "parse-asn1": {
+ "version": "5.1.1",
+ "resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
+ "integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==",
+ "requires": {
+ "asn1.js": "^4.0.0",
+ "browserify-aes": "^1.0.0",
+ "create-hash": "^1.1.0",
+ "evp_bytestokey": "^1.0.0",
+ "pbkdf2": "^3.0.3"
+ }
+ },
+ "parse-json": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz",
+ "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=",
+ "requires": {
+ "error-ex": "^1.2.0"
+ }
+ },
+ "parseurl": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.2.tgz",
+ "integrity": "sha1-/CidTtiZMRlGDBViUyYs3I3mW/M="
+ },
+ "path-browserify": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz",
+ "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ=="
+ },
+ "path-exists": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz",
+ "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=",
+ "requires": {
+ "pinkie-promise": "^2.0.0"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
+ },
+ "path-is-inside": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+ "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM="
+ },
+ "path-key": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
+ "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
+ },
+ "path-parse": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
+ },
+ "path-platform": {
+ "version": "0.11.15",
+ "resolved": "https://registry.npmjs.org/path-platform/-/path-platform-0.11.15.tgz",
+ "integrity": "sha1-6GQhf3TDaFDwhSt43Hv31KVyG/I="
+ },
+ "path-to-regexp": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
+ "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w="
+ },
+ "path-type": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz",
+ "integrity": "sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM=",
+ "requires": {
+ "pify": "^2.0.0"
+ },
+ "dependencies": {
+ "pify": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz",
+ "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw="
+ }
+ }
+ },
+ "pbkdf2": {
+ "version": "3.0.17",
+ "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.0.17.tgz",
+ "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==",
+ "requires": {
+ "create-hash": "^1.1.2",
+ "create-hmac": "^1.1.4",
+ "ripemd160": "^2.0.1",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
+ "pegjs": {
+ "version": "0.10.0",
+ "resolved": "http://registry.npmjs.org/pegjs/-/pegjs-0.10.0.tgz",
+ "integrity": "sha1-z4uvrm7d/0tafvsYUmnqr0YQ3b0="
+ },
+ "performance-now": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
+ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns="
+ },
+ "pify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz",
+ "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY="
+ },
+ "pinkie": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz",
+ "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA="
+ },
+ "pinkie-promise": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz",
+ "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=",
+ "requires": {
+ "pinkie": "^2.0.0"
+ }
+ },
+ "pkg-dir": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz",
+ "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=",
+ "requires": {
+ "find-up": "^1.0.0"
+ }
+ },
+ "plist": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/plist/-/plist-2.1.0.tgz",
+ "integrity": "sha1-V8zbeggh3yGDEhejytVOPhRqECU=",
+ "requires": {
+ "base64-js": "1.2.0",
+ "xmlbuilder": "8.2.2",
+ "xmldom": "0.1.x"
+ }
+ },
+ "pluralize": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-7.0.0.tgz",
+ "integrity": "sha512-ARhBOdzS3e41FbkW/XWrTEtukqqLoK5+Z/4UeDaLuSW+39JPeFgs4gCGqsrJHVZX0fUrx//4OF0K1CUGwlIFow=="
+ },
+ "prelude-ls": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
+ "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ="
+ },
+ "prepend-http": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz",
+ "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw="
+ },
+ "process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI="
+ },
+ "process-nextick-args": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz",
+ "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw=="
+ },
+ "progress": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.0.tgz",
+ "integrity": "sha1-ihvjZr+Pwj2yvSPxDG/pILQ4nR8="
+ },
+ "promzard": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/promzard/-/promzard-0.3.0.tgz",
+ "integrity": "sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=",
+ "requires": {
+ "read": "1"
+ }
+ },
+ "properties-parser": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/properties-parser/-/properties-parser-0.3.1.tgz",
+ "integrity": "sha1-ExbpU5/7/ZOEXjabIRAiq9R4dxo=",
+ "requires": {
+ "string.prototype.codepointat": "^0.2.0"
+ }
+ },
+ "proxy-addr": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.4.tgz",
+ "integrity": "sha512-5erio2h9jp5CHGwcybmxmVqHmnCBZeewlfJ0pex+UW7Qny7OOZXTtH56TGNyBizkgiOwhJtMKrVzDTeKcySZwA==",
+ "requires": {
+ "forwarded": "~0.1.2",
+ "ipaddr.js": "1.8.0"
+ }
+ },
+ "pseudomap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz",
+ "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM="
+ },
+ "psl": {
+ "version": "1.1.29",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.1.29.tgz",
+ "integrity": "sha512-AeUmQ0oLN02flVHXWh9sSJF7mcdFq0ppid/JkErufc3hGIV/AMa8Fo9VgDo/cT2jFdOWoFvHp90qqBH54W+gjQ=="
+ },
+ "public-encrypt": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
+ "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==",
+ "requires": {
+ "bn.js": "^4.1.0",
+ "browserify-rsa": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "parse-asn1": "^5.0.0",
+ "randombytes": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "punycode": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+ "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4="
+ },
+ "q": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz",
+ "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc="
+ },
+ "qs": {
+ "version": "6.5.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.1.tgz",
+ "integrity": "sha512-eRzhrN1WSINYCDCbrz796z37LOe3m5tmW7RQf6oBntukAG1nmovJvhnwHHRMAfeoItc1m2Hk02WER2aQ/iqs+A=="
+ },
+ "querystring": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz",
+ "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA="
+ },
+ "querystring-es3": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
+ "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM="
+ },
+ "randombytes": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.0.6.tgz",
+ "integrity": "sha512-CIQ5OFxf4Jou6uOKe9t1AOgqpeU5fd70A8NPdHSGeYXqXsPe6peOwI0cUl88RWZ6sP1vPMV3avd/R6cZ5/sP1A==",
+ "requires": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "randomfill": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz",
+ "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==",
+ "requires": {
+ "randombytes": "^2.0.5",
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "range-parser": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
+ "integrity": "sha1-9JvmtIeJTdxA3MlKMi9hEJLgDV4="
+ },
+ "raw-body": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.3.2.tgz",
+ "integrity": "sha1-vNYMd9Prk83gBQKVw/N5OJvIj4k=",
+ "requires": {
+ "bytes": "3.0.0",
+ "http-errors": "1.6.2",
+ "iconv-lite": "0.4.19",
+ "unpipe": "1.0.0"
+ },
+ "dependencies": {
+ "depd": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.1.tgz",
+ "integrity": "sha1-V4O04cRZ8G+lyif5kfPQbnoxA1k="
+ },
+ "http-errors": {
+ "version": "1.6.2",
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.2.tgz",
+ "integrity": "sha1-CgAsyFcHGSp+eUbO7cERVfYOxzY=",
+ "requires": {
+ "depd": "1.1.1",
+ "inherits": "2.0.3",
+ "setprototypeof": "1.0.3",
+ "statuses": ">= 1.3.1 < 2"
+ }
+ },
+ "setprototypeof": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.0.3.tgz",
+ "integrity": "sha1-ZlZ+NwQ+608E2RvWWMDL77VbjgQ="
+ }
+ }
+ },
+ "rc": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+ "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+ "requires": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ }
+ },
+ "read": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz",
+ "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=",
+ "requires": {
+ "mute-stream": "~0.0.4"
+ }
+ },
+ "read-chunk": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/read-chunk/-/read-chunk-2.1.0.tgz",
+ "integrity": "sha1-agTAkoAF7Z1C4aasVgDhnLx/9lU=",
+ "requires": {
+ "pify": "^3.0.0",
+ "safe-buffer": "^5.1.1"
+ }
+ },
+ "read-only-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/read-only-stream/-/read-only-stream-2.0.0.tgz",
+ "integrity": "sha1-JyT9aoET1zdkrCiNQ4YnDB2/F/A=",
+ "requires": {
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "read-package-json": {
+ "version": "2.0.13",
+ "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.0.13.tgz",
+ "integrity": "sha512-/1dZ7TRZvGrYqE0UAfN6qQb5GYBsNcqS1C0tNK601CFOJmtHI7NIGXwetEPU/OtoFHZL3hDxm4rolFFVE9Bnmg==",
+ "requires": {
+ "glob": "^7.1.1",
+ "graceful-fs": "^4.1.2",
+ "json-parse-better-errors": "^1.0.1",
+ "normalize-package-data": "^2.0.0",
+ "slash": "^1.0.0"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ }
+ }
+ },
+ "read-pkg": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz",
+ "integrity": "sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg=",
+ "requires": {
+ "load-json-file": "^2.0.0",
+ "normalize-package-data": "^2.3.2",
+ "path-type": "^2.0.0"
+ }
+ },
+ "read-pkg-up": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz",
+ "integrity": "sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4=",
+ "requires": {
+ "find-up": "^2.0.0",
+ "read-pkg": "^2.0.0"
+ },
+ "dependencies": {
+ "find-up": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz",
+ "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=",
+ "requires": {
+ "locate-path": "^2.0.0"
+ }
+ }
+ }
+ },
+ "readable-stream": {
+ "version": "2.3.6",
+ "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
+ "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
+ },
+ "string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ }
+ }
+ },
+ "readline2": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz",
+ "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=",
+ "requires": {
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "mute-stream": "0.0.5"
+ },
+ "dependencies": {
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
+ "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "mute-stream": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz",
+ "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA="
+ }
+ }
+ },
+ "rechoir": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz",
+ "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=",
+ "requires": {
+ "resolve": "^1.1.6"
+ }
+ },
+ "regexpp": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-1.1.0.tgz",
+ "integrity": "sha512-LOPw8FpgdQF9etWMaAfG/WRthIdXJGYp4mJ2Jgn/2lpkbod9jPn0t9UqN7AxBOKNfzRbYyVfgc7Vk4t/MpnXgw=="
+ },
+ "registry-auth-token": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-3.3.2.tgz",
+ "integrity": "sha512-JL39c60XlzCVgNrO+qq68FoNb56w/m7JYvGR2jT5iR1xBrUA3Mfx5Twk5rqTThPmQKMWydGmq8oFtDlxfrmxnQ==",
+ "requires": {
+ "rc": "^1.1.6",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "registry-url": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-3.1.0.tgz",
+ "integrity": "sha1-PU74cPc93h138M+aOBQyRE4XSUI=",
+ "requires": {
+ "rc": "^1.0.1"
+ }
+ },
+ "request": {
+ "version": "2.88.0",
+ "resolved": "https://registry.npmjs.org/request/-/request-2.88.0.tgz",
+ "integrity": "sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==",
+ "requires": {
+ "aws-sign2": "~0.7.0",
+ "aws4": "^1.8.0",
+ "caseless": "~0.12.0",
+ "combined-stream": "~1.0.6",
+ "extend": "~3.0.2",
+ "forever-agent": "~0.6.1",
+ "form-data": "~2.3.2",
+ "har-validator": "~5.1.0",
+ "http-signature": "~1.2.0",
+ "is-typedarray": "~1.0.0",
+ "isstream": "~0.1.2",
+ "json-stringify-safe": "~5.0.1",
+ "mime-types": "~2.1.19",
+ "oauth-sign": "~0.9.0",
+ "performance-now": "^2.1.0",
+ "qs": "~6.5.2",
+ "safe-buffer": "^5.1.2",
+ "tough-cookie": "~2.4.3",
+ "tunnel-agent": "^0.6.0",
+ "uuid": "^3.3.2"
+ },
+ "dependencies": {
+ "qs": {
+ "version": "6.5.2",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz",
+ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA=="
+ }
+ }
+ },
+ "require-uncached": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz",
+ "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=",
+ "requires": {
+ "caller-path": "^0.1.0",
+ "resolve-from": "^1.0.0"
+ }
+ },
+ "resolve": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.8.1.tgz",
+ "integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==",
+ "requires": {
+ "path-parse": "^1.0.5"
+ }
+ },
+ "resolve-from": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz",
+ "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY="
+ },
+ "restore-cursor": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz",
+ "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=",
+ "requires": {
+ "onetime": "^2.0.0",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "rewire": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/rewire/-/rewire-4.0.1.tgz",
+ "integrity": "sha512-+7RQ/BYwTieHVXetpKhT11UbfF6v1kGhKFrtZN7UDL2PybMsSt/rpLWeEUGF5Ndsl1D5BxiCB14VDJyoX+noYw==",
+ "requires": {
+ "eslint": "^4.19.1"
+ }
+ },
+ "rimraf": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz",
+ "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==",
+ "requires": {
+ "glob": "^7.0.5"
+ },
+ "dependencies": {
+ "glob": {
+ "version": "7.1.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
+ "integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ }
+ }
+ },
+ "ripemd160": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz",
+ "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==",
+ "requires": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1"
+ }
+ },
+ "run-async": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.3.0.tgz",
+ "integrity": "sha1-A3GrSuC91yDUFm19/aZP96RFpsA=",
+ "requires": {
+ "is-promise": "^2.1.0"
+ }
+ },
+ "rx-lite": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-4.0.8.tgz",
+ "integrity": "sha1-Cx4Rr4vESDbwSmQH6S2kJGe3lEQ="
+ },
+ "rx-lite-aggregates": {
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz",
+ "integrity": "sha1-dTuHqJoRyVRnxKwWJsTvxOBcZ74=",
+ "requires": {
+ "rx-lite": "*"
+ }
+ },
+ "safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
+ },
+ "sax": {
+ "version": "0.3.5",
+ "resolved": "http://registry.npmjs.org/sax/-/sax-0.3.5.tgz",
+ "integrity": "sha1-iPz8H3PAyLvVt8d2ttPzUB7tBz0="
+ },
+ "semver": {
+ "version": "5.5.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz",
+ "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw=="
+ },
+ "semver-diff": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-2.1.0.tgz",
+ "integrity": "sha1-S7uEN8jTfksM8aaP1ybsbWRdbTY=",
+ "requires": {
+ "semver": "^5.0.3"
+ }
+ },
+ "send": {
+ "version": "0.16.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.16.2.tgz",
+ "integrity": "sha512-E64YFPUssFHEFBvpbbjr44NCLtI1AohxQ8ZSiJjQLskAdKuriYEP6VyGEsRDH8ScozGpkaX1BGvhanqCwkcEZw==",
+ "requires": {
+ "debug": "2.6.9",
+ "depd": "~1.1.2",
+ "destroy": "~1.0.4",
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "0.5.2",
+ "http-errors": "~1.6.2",
+ "mime": "1.4.1",
+ "ms": "2.0.0",
+ "on-finished": "~2.3.0",
+ "range-parser": "~1.2.0",
+ "statuses": "~1.4.0"
+ }
+ },
+ "serve-static": {
+ "version": "1.13.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.13.2.tgz",
+ "integrity": "sha512-p/tdJrO4U387R9oMjb1oj7qSMaMfmOyd4j9hOFoxZe2baQszgHcSWjuya/CiT5kgZZKRudHNOA0pYXOl8rQ5nw==",
+ "requires": {
+ "encodeurl": "~1.0.2",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.2",
+ "send": "0.16.2"
+ }
+ },
+ "setprototypeof": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz",
+ "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ=="
+ },
+ "sha.js": {
+ "version": "2.4.11",
+ "resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
+ "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+ "requires": {
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "shasum": {
+ "version": "1.0.2",
+ "resolved": "http://registry.npmjs.org/shasum/-/shasum-1.0.2.tgz",
+ "integrity": "sha1-5wEjENj0F/TetXEhUOVni4euVl8=",
+ "requires": {
+ "json-stable-stringify": "~0.0.0",
+ "sha.js": "~2.4.4"
+ }
+ },
+ "shebang-command": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz",
+ "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=",
+ "requires": {
+ "shebang-regex": "^1.0.0"
+ }
+ },
+ "shebang-regex": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz",
+ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM="
+ },
+ "shell-quote": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.6.1.tgz",
+ "integrity": "sha1-9HgZSczkAmlxJ0MOo7PFR29IF2c=",
+ "requires": {
+ "array-filter": "~0.0.0",
+ "array-map": "~0.0.0",
+ "array-reduce": "~0.0.0",
+ "jsonify": "~0.0.0"
+ }
+ },
+ "shelljs": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.5.3.tgz",
+ "integrity": "sha1-xUmCuZbHbvDB5rWfvcWCX1txMRM="
+ },
+ "signal-exit": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz",
+ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
+ },
+ "simple-concat": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.0.tgz",
+ "integrity": "sha1-c0TLuLbib7J9ZrL8hvn21Zl1IcY="
+ },
+ "simple-plist": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-0.2.1.tgz",
+ "integrity": "sha1-cXZts1IyaSjPOoByQrp2IyJjZyM=",
+ "requires": {
+ "bplist-creator": "0.0.7",
+ "bplist-parser": "0.1.1",
+ "plist": "2.0.1"
+ },
+ "dependencies": {
+ "base64-js": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.1.2.tgz",
+ "integrity": "sha1-1kAMrBxMZgl22Q0HoENR2JOV9eg="
+ },
+ "plist": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/plist/-/plist-2.0.1.tgz",
+ "integrity": "sha1-CjLKlIGxw2TpLhjcVch23p0B2os=",
+ "requires": {
+ "base64-js": "1.1.2",
+ "xmlbuilder": "8.2.2",
+ "xmldom": "0.1.x"
+ }
+ }
+ }
+ },
+ "slash": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz",
+ "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU="
+ },
+ "slice-ansi": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-1.0.0.tgz",
+ "integrity": "sha512-POqxBK6Lb3q6s047D/XsDVNPnF9Dl8JSaqe9h9lURl0OdNqy/ujDrOiIHtsqXMGbWWTIomRzAMaTyawAU//Reg==",
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0"
+ }
+ },
+ "slide": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/slide/-/slide-1.1.6.tgz",
+ "integrity": "sha1-VusCfWW00tzmyy4tMsTUr8nh1wc="
+ },
+ "source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
+ },
+ "spdx-correct": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.0.0.tgz",
+ "integrity": "sha512-N19o9z5cEyc8yQQPukRCZ9EUmb4HUpnrmaL/fxS2pBo2jbfcFRVuFZ/oFC+vZz0MNNk0h80iMn5/S6qGZOL5+g==",
+ "requires": {
+ "spdx-expression-parse": "^3.0.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-exceptions": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz",
+ "integrity": "sha512-4K1NsmrlCU1JJgUrtgEeTVyfx8VaYea9J9LvARxhbHtVtohPs/gFGG5yy49beySjlIMhhXZ4QqujIZEfS4l6Cg=="
+ },
+ "spdx-expression-parse": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz",
+ "integrity": "sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg==",
+ "requires": {
+ "spdx-exceptions": "^2.1.0",
+ "spdx-license-ids": "^3.0.0"
+ }
+ },
+ "spdx-license-ids": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.1.tgz",
+ "integrity": "sha512-TfOfPcYGBB5sDuPn3deByxPhmfegAhpDYKSOXZQN81Oyrrif8ZCodOLzK3AesELnCx03kikhyDwh0pfvvQvF8w=="
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
+ },
+ "sshpk": {
+ "version": "1.14.2",
+ "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.14.2.tgz",
+ "integrity": "sha1-xvxhZIo9nE52T9P8306hBeSSupg=",
+ "requires": {
+ "asn1": "~0.2.3",
+ "assert-plus": "^1.0.0",
+ "bcrypt-pbkdf": "^1.0.0",
+ "dashdash": "^1.12.0",
+ "ecc-jsbn": "~0.1.1",
+ "getpass": "^0.1.1",
+ "jsbn": "~0.1.0",
+ "safer-buffer": "^2.0.2",
+ "tweetnacl": "~0.14.0"
+ }
+ },
+ "statuses": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.4.0.tgz",
+ "integrity": "sha512-zhSCtt8v2NDrRlPQpCNtw/heZLtfUDqxBM1udqikb/Hbk52LK4nQSwr10u77iopCW5LsyHpuXS0GnEc48mLeew=="
+ },
+ "stream-browserify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.1.tgz",
+ "integrity": "sha1-ZiZu5fm9uZQKTkUUyvtDu3Hlyds=",
+ "requires": {
+ "inherits": "~2.0.1",
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "stream-buffers": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz",
+ "integrity": "sha1-kdX1Ew0c75bc+n9yaUUYh0HQnuQ="
+ },
+ "stream-combiner2": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz",
+ "integrity": "sha1-+02KFCDqNidk4hrUeAOXvry0HL4=",
+ "requires": {
+ "duplexer2": "~0.1.0",
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "stream-http": {
+ "version": "2.8.3",
+ "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz",
+ "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==",
+ "requires": {
+ "builtin-status-codes": "^3.0.0",
+ "inherits": "^2.0.1",
+ "readable-stream": "^2.3.6",
+ "to-arraybuffer": "^1.0.0",
+ "xtend": "^4.0.0"
+ }
+ },
+ "stream-splicer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/stream-splicer/-/stream-splicer-2.0.0.tgz",
+ "integrity": "sha1-G2O+Q4oTPktnHMGTUZdgAXWRDYM=",
+ "requires": {
+ "inherits": "^2.0.1",
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "string-width": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz",
+ "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==",
+ "requires": {
+ "is-fullwidth-code-point": "^2.0.0",
+ "strip-ansi": "^4.0.0"
+ },
+ "dependencies": {
+ "ansi-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz",
+ "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg="
+ },
+ "strip-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz",
+ "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=",
+ "requires": {
+ "ansi-regex": "^3.0.0"
+ }
+ }
+ }
+ },
+ "string.prototype.codepointat": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz",
+ "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg=="
+ },
+ "string_decoder": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz",
+ "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==",
+ "requires": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "strip-ansi": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
+ "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
+ "requires": {
+ "ansi-regex": "^2.0.0"
+ }
+ },
+ "strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM="
+ },
+ "strip-eof": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz",
+ "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8="
+ },
+ "strip-json-comments": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+ "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo="
+ },
+ "subarg": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/subarg/-/subarg-1.0.0.tgz",
+ "integrity": "sha1-9izxdYHplrSPyWVpn1TAauJouNI=",
+ "requires": {
+ "minimist": "^1.1.0"
+ }
+ },
+ "supports-color": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz",
+ "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc="
+ },
+ "syntax-error": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/syntax-error/-/syntax-error-1.4.0.tgz",
+ "integrity": "sha512-YPPlu67mdnHGTup2A8ff7BC2Pjq0e0Yp/IyTFN03zWO0RcK07uLcbi7C2KpGR2FvWbaB0+bfE27a+sBKebSo7w==",
+ "requires": {
+ "acorn-node": "^1.2.0"
+ }
+ },
+ "table": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/table/-/table-4.0.2.tgz",
+ "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==",
+ "requires": {
+ "ajv": "^5.2.3",
+ "ajv-keywords": "^2.1.0",
+ "chalk": "^2.1.0",
+ "lodash": "^4.17.4",
+ "slice-ansi": "1.0.0",
+ "string-width": "^2.1.1"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "chalk": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+ "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "tar": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/tar/-/tar-2.2.1.tgz",
+ "integrity": "sha1-jk0qJWwOIYXGsYrWlK7JaLg8sdE=",
+ "requires": {
+ "block-stream": "*",
+ "fstream": "^1.0.2",
+ "inherits": "2"
+ }
+ },
+ "term-size": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/term-size/-/term-size-1.2.0.tgz",
+ "integrity": "sha1-RYuDiH8oj8Vtb/+/rSYuJmOO+mk=",
+ "requires": {
+ "execa": "^0.7.0"
+ }
+ },
+ "text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ="
+ },
+ "through": {
+ "version": "2.3.8",
+ "resolved": "http://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU="
+ },
+ "through2": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.3.tgz",
+ "integrity": "sha1-AARWmzfHx0ujnEPzzteNGtlBQL4=",
+ "requires": {
+ "readable-stream": "^2.1.5",
+ "xtend": "~4.0.1"
+ }
+ },
+ "timed-out": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz",
+ "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8="
+ },
+ "timers-browserify": {
+ "version": "1.4.2",
+ "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-1.4.2.tgz",
+ "integrity": "sha1-ycWLV1voQHN1y14kYtrO50NZ9B0=",
+ "requires": {
+ "process": "~0.11.0"
+ }
+ },
+ "tmp": {
+ "version": "0.0.33",
+ "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
+ "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==",
+ "requires": {
+ "os-tmpdir": "~1.0.2"
+ }
+ },
+ "to-arraybuffer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz",
+ "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M="
+ },
+ "tough-cookie": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.4.3.tgz",
+ "integrity": "sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==",
+ "requires": {
+ "psl": "^1.1.24",
+ "punycode": "^1.4.1"
+ }
+ },
+ "tty-browserify": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz",
+ "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw=="
+ },
+ "tunnel-agent": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+ "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=",
+ "requires": {
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "tweetnacl": {
+ "version": "0.14.5",
+ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
+ "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
+ "optional": true
+ },
+ "type-check": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
+ "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=",
+ "requires": {
+ "prelude-ls": "~1.1.2"
+ }
+ },
+ "type-is": {
+ "version": "1.6.16",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.16.tgz",
+ "integrity": "sha512-HRkVv/5qY2G6I8iab9cI7v1bOIdhm94dVjQCPFElW9W+3GeDOSHmy2EBYe4VTApuzolPcmgFTN3ftVJRKR2J9Q==",
+ "requires": {
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.18"
+ }
+ },
+ "typedarray": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
+ "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
+ },
+ "uglify-js": {
+ "version": "3.4.9",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.9.tgz",
+ "integrity": "sha512-8CJsbKOtEbnJsTyv6LE6m6ZKniqMiFWmm9sRbopbkGs3gMPPfd3Fh8iIA4Ykv5MgaTbqHr4BaoGLJLZNhsrW1Q==",
+ "optional": true,
+ "requires": {
+ "commander": "~2.17.1",
+ "source-map": "~0.6.1"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "optional": true
+ }
+ }
+ },
+ "umd": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/umd/-/umd-3.0.3.tgz",
+ "integrity": "sha512-4IcGSufhFshvLNcMCV80UnQVlZ5pMOC8mvNPForqwA4+lzYQuetTESLDQkeLmihq8bRcnpbQa48Wb8Lh16/xow=="
+ },
+ "undeclared-identifiers": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/undeclared-identifiers/-/undeclared-identifiers-1.1.2.tgz",
+ "integrity": "sha512-13EaeocO4edF/3JKime9rD7oB6QI8llAGhgn5fKOPyfkJbRb6NFv9pYV6dFEmpa4uRjKeBqLZP8GpuzqHlKDMQ==",
+ "requires": {
+ "acorn-node": "^1.3.0",
+ "get-assigned-identifiers": "^1.2.0",
+ "simple-concat": "^1.0.0",
+ "xtend": "^4.0.1"
+ }
+ },
+ "underscore": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.9.1.tgz",
+ "integrity": "sha512-5/4etnCkd9c8gwgowi5/om/mYO5ajCaOgdzj/oW+0eQV9WxKBDZw5+ycmKmeaTXjInS/W0BzpGLo2xR2aBwZdg=="
+ },
+ "unique-string": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-1.0.0.tgz",
+ "integrity": "sha1-nhBXzKhRq7kzmPizOuGHuZyuwRo=",
+ "requires": {
+ "crypto-random-string": "^1.0.0"
+ }
+ },
+ "unorm": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.4.1.tgz",
+ "integrity": "sha1-NkIA1fE2RsqLzURJAnEzVhR5IwA="
+ },
+ "unpipe": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
+ "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw="
+ },
+ "unzip-response": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/unzip-response/-/unzip-response-2.0.1.tgz",
+ "integrity": "sha1-0vD3N9FrBhXnKmk17QQhRXLVb5c="
+ },
+ "update-notifier": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-2.5.0.tgz",
+ "integrity": "sha512-gwMdhgJHGuj/+wHJJs9e6PcCszpxR1b236igrOkUofGhqJuG+amlIKwApH1IW1WWl7ovZxsX49lMBWLxSdm5Dw==",
+ "requires": {
+ "boxen": "^1.2.1",
+ "chalk": "^2.0.1",
+ "configstore": "^3.0.0",
+ "import-lazy": "^2.1.0",
+ "is-ci": "^1.0.10",
+ "is-installed-globally": "^0.1.0",
+ "is-npm": "^1.0.0",
+ "latest-version": "^3.0.0",
+ "semver-diff": "^2.0.0",
+ "xdg-basedir": "^3.0.0"
+ },
+ "dependencies": {
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "requires": {
+ "color-convert": "^1.9.0"
+ }
+ },
+ "chalk": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.1.tgz",
+ "integrity": "sha512-ObN6h1v2fTJSmUXoS3nMQ92LbDK9be4TV+6G+omQlGJFdcUX5heKi1LZ1YnRMIgwTLEj3E24bT6tYni50rlCfQ==",
+ "requires": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ }
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "^3.0.0"
+ }
+ }
+ }
+ },
+ "url": {
+ "version": "0.11.0",
+ "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz",
+ "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=",
+ "requires": {
+ "punycode": "1.3.2",
+ "querystring": "0.2.0"
+ },
+ "dependencies": {
+ "punycode": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
+ "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0="
+ }
+ }
+ },
+ "url-parse-lax": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz",
+ "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=",
+ "requires": {
+ "prepend-http": "^1.0.1"
+ }
+ },
+ "util": {
+ "version": "0.10.4",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz",
+ "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==",
+ "requires": {
+ "inherits": "2.0.3"
+ }
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
+ },
+ "utils-merge": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
+ "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM="
+ },
+ "uuid": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
+ "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
+ },
+ "valid-identifier": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/valid-identifier/-/valid-identifier-0.0.1.tgz",
+ "integrity": "sha1-7x1wk6nTKH4/zpLfkW+GFrI/kLQ="
+ },
+ "validate-npm-package-license": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
+ "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
+ "requires": {
+ "spdx-correct": "^3.0.0",
+ "spdx-expression-parse": "^3.0.0"
+ }
+ },
+ "validate-npm-package-name": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-3.0.0.tgz",
+ "integrity": "sha1-X6kS2B630MdK/BQN5zF/DKffQ34=",
+ "requires": {
+ "builtins": "^1.0.3"
+ }
+ },
+ "vary": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
+ "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw="
+ },
+ "verror": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
+ "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=",
+ "requires": {
+ "assert-plus": "^1.0.0",
+ "core-util-is": "1.0.2",
+ "extsprintf": "^1.2.0"
+ }
+ },
+ "vm-browserify": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-0.0.4.tgz",
+ "integrity": "sha1-XX6kW7755Kb/ZflUOOCofDV9WnM=",
+ "requires": {
+ "indexof": "0.0.1"
+ }
+ },
+ "which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "requires": {
+ "isexe": "^2.0.0"
+ }
+ },
+ "widest-line": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-2.0.0.tgz",
+ "integrity": "sha1-AUKk6KJD+IgsAjOqDgKBqnYVInM=",
+ "requires": {
+ "string-width": "^2.1.1"
+ }
+ },
+ "win-release": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/win-release/-/win-release-1.1.1.tgz",
+ "integrity": "sha1-X6VeAr58qTTt/BJmVjLoSbcuUgk=",
+ "requires": {
+ "semver": "^5.0.1"
+ }
+ },
+ "wordwrap": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
+ "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus="
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
+ },
+ "write": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz",
+ "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=",
+ "requires": {
+ "mkdirp": "^0.5.1"
+ }
+ },
+ "write-file-atomic": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-2.3.0.tgz",
+ "integrity": "sha512-xuPeK4OdjWqtfi59ylvVL0Yn35SF3zgcAcv7rBPFHVaEapaDr4GdGgm3j7ckTwH9wHL7fGmgfAnb0+THrHb8tA==",
+ "requires": {
+ "graceful-fs": "^4.1.11",
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^3.0.2"
+ }
+ },
+ "xcode": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/xcode/-/xcode-1.0.0.tgz",
+ "integrity": "sha1-4fWxRDJF3tOMGAeW3xoQ/e2ghOw=",
+ "requires": {
+ "pegjs": "^0.10.0",
+ "simple-plist": "^0.2.1",
+ "uuid": "3.0.1"
+ },
+ "dependencies": {
+ "uuid": {
+ "version": "3.0.1",
+ "resolved": "http://registry.npmjs.org/uuid/-/uuid-3.0.1.tgz",
+ "integrity": "sha1-ZUS7ot/ajBzxfmKaOjBeK7H+5sE="
+ }
+ }
+ },
+ "xdg-basedir": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-3.0.0.tgz",
+ "integrity": "sha1-SWsswQnsqNus/i3HK2A8F8WHCtQ="
+ },
+ "xmlbuilder": {
+ "version": "8.2.2",
+ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz",
+ "integrity": "sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M="
+ },
+ "xmldom": {
+ "version": "0.1.27",
+ "resolved": "https://registry.npmjs.org/xmldom/-/xmldom-0.1.27.tgz",
+ "integrity": "sha1-1QH5ezvbQDr4757MIFcxh6rawOk="
+ },
+ "xtend": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz",
+ "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68="
+ }
+ }
+ },
+ "cordova-android": {
+ "version": "7.1.2",
+ "resolved": "https://registry.npmjs.org/cordova-android/-/cordova-android-7.1.2.tgz",
+ "integrity": "sha512-w28HJGtfAZCT96hVH9BMppWMnmDTZplKu2NRQZN2dCr5e9r7aHpay41MYy9IBkh8+7E7lMo/jZkRwBDNr4VnEg==",
+ "requires": {
+ "abbrev": "*",
+ "android-versions": "1.3.0",
+ "ansi": "*",
+ "balanced-match": "*",
+ "base64-js": "1.2.0",
+ "big-integer": "*",
+ "bplist-parser": "*",
+ "brace-expansion": "*",
+ "concat-map": "*",
+ "cordova-common": "2.2.5",
+ "cordova-registry-mapper": "*",
+ "elementtree": "0.1.6",
+ "glob": "5.0.15",
+ "inflight": "*",
+ "inherits": "*",
+ "minimatch": "*",
+ "nopt": "3.0.1",
+ "once": "*",
+ "path-is-absolute": "*",
+ "plist": "2.1.0",
+ "properties-parser": "0.2.3",
+ "q": "1.4.1",
+ "sax": "0.3.5",
+ "semver": "*",
+ "shelljs": "0.5.3",
+ "underscore": "*",
+ "unorm": "*",
+ "wrappy": "*",
+ "xmlbuilder": "8.2.2",
+ "xmldom": "*"
+ },
+ "dependencies": {
+ "abbrev": {
+ "version": "1.1.1",
+ "bundled": true
+ },
+ "android-versions": {
+ "version": "1.3.0",
+ "bundled": true,
+ "requires": {
+ "semver": "^5.4.1"
+ }
+ },
+ "ansi": {
+ "version": "0.3.1",
+ "bundled": true
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "base64-js": {
+ "version": "1.2.0",
+ "bundled": true
+ },
+ "big-integer": {
+ "version": "1.6.32",
+ "bundled": true
+ },
+ "bplist-parser": {
+ "version": "0.1.1",
+ "bundled": true,
+ "requires": {
+ "big-integer": "^1.6.7"
+ }
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "bundled": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "bundled": true
+ },
+ "cordova-common": {
+ "version": "2.2.5",
+ "bundled": true,
+ "requires": {
+ "ansi": "^0.3.1",
+ "bplist-parser": "^0.1.0",
+ "cordova-registry-mapper": "^1.1.8",
+ "elementtree": "0.1.6",
+ "glob": "^5.0.13",
+ "minimatch": "^3.0.0",
+ "plist": "^2.1.0",
+ "q": "^1.4.1",
+ "shelljs": "^0.5.3",
+ "underscore": "^1.8.3",
+ "unorm": "^1.3.3"
+ }
+ },
+ "cordova-registry-mapper": {
+ "version": "1.1.15",
+ "bundled": true
+ },
+ "elementtree": {
+ "version": "0.1.6",
+ "bundled": true,
+ "requires": {
+ "sax": "0.3.5"
+ }
+ },
+ "glob": {
+ "version": "5.0.15",
+ "bundled": true,
+ "requires": {
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "2 || 3",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "bundled": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "bundled": true
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "bundled": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "nopt": {
+ "version": "3.0.1",
+ "bundled": true,
+ "requires": {
+ "abbrev": "1"
+ }
+ },
+ "once": {
+ "version": "1.4.0",
+ "bundled": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "bundled": true
+ },
+ "plist": {
+ "version": "2.1.0",
+ "bundled": true,
+ "requires": {
+ "base64-js": "1.2.0",
+ "xmlbuilder": "8.2.2",
+ "xmldom": "0.1.x"
+ }
+ },
+ "properties-parser": {
+ "version": "0.2.3",
+ "bundled": true
+ },
+ "q": {
+ "version": "1.4.1",
+ "bundled": true
+ },
+ "sax": {
+ "version": "0.3.5",
+ "bundled": true
+ },
+ "semver": {
+ "version": "5.5.0",
+ "bundled": true
+ },
+ "shelljs": {
+ "version": "0.5.3",
+ "bundled": true
+ },
+ "underscore": {
+ "version": "1.9.1",
+ "bundled": true
+ },
+ "unorm": {
+ "version": "1.4.1",
+ "bundled": true
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "bundled": true
+ },
+ "xmlbuilder": {
+ "version": "8.2.2",
+ "bundled": true
+ },
+ "xmldom": {
+ "version": "0.1.27",
+ "bundled": true
+ }
+ }
+ },
+ "cordova-android-support-gradle-release": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/cordova-android-support-gradle-release/-/cordova-android-support-gradle-release-3.0.1.tgz",
+ "integrity": "sha512-RSW55DkSckmqhX/kjj+a1YeVdy7s/AtlZn6Qa5XMQmmA4Iogq+IF2jvInZqzCF19DbI5YE95AP7VDbRk+DdDRw==",
+ "requires": {
+ "q": "^1.4.1",
+ "semver": "5.6.0"
+ },
+ "dependencies": {
+ "semver": {
+ "version": "5.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
+ "integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg=="
+ }
+ }
+ },
+ "cordova-clipboard": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/cordova-clipboard/-/cordova-clipboard-1.3.0.tgz",
+ "integrity": "sha512-IGk4LZm/DJ0Xk/jgakHm4wa+A/lrRP3QfzMAHDG7oWLJS4ISOpfI32Wez4ndnENItRslGyBVyJyKD83CxELCAw=="
+ },
+ "cordova-ios": {
+ "version": "4.5.5",
+ "resolved": "https://registry.npmjs.org/cordova-ios/-/cordova-ios-4.5.5.tgz",
+ "integrity": "sha512-3+30m2dZ2yii7kg+H7cgpdpkXpMj54zoX5imjGGG4Z7dPXKmalTLc/9rLq+Iaa+Q1BqyOrUFaHopWOODRU6vCg==",
+ "requires": {
+ "abbrev": "*",
+ "ansi": "*",
+ "balanced-match": "*",
+ "base64-js": "1.2.0",
+ "big-integer": "*",
+ "bplist-creator": "*",
+ "bplist-parser": "*",
+ "brace-expansion": "*",
+ "concat-map": "*",
+ "cordova-common": "2.2.5",
+ "cordova-registry-mapper": "*",
+ "elementtree": "0.1.6",
+ "glob": "5.0.15",
+ "inflight": "*",
+ "inherits": "*",
+ "ios-sim": "6.1.3",
+ "minimatch": "*",
+ "nopt": "3.0.6",
+ "once": "*",
+ "path-is-absolute": "*",
+ "plist": "2.1.0",
+ "q": "1.5.1",
+ "sax": "0.3.5",
+ "shelljs": "0.5.3",
+ "simctl": "*",
+ "simple-plist": "0.2.1",
+ "stream-buffers": "2.2.0",
+ "tail": "0.4.0",
+ "underscore": "*",
+ "unorm": "*",
+ "uuid": "3.0.1",
+ "wrappy": "*",
+ "xcode": "0.9.3",
+ "xml-escape": "1.1.0",
+ "xmlbuilder": "8.2.2",
+ "xmldom": "*"
+ },
+ "dependencies": {
+ "abbrev": {
+ "version": "1.1.1",
+ "bundled": true
+ },
+ "ansi": {
+ "version": "0.3.1",
+ "bundled": true
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "bundled": true
+ },
+ "base64-js": {
+ "version": "1.2.0",
+ "bundled": true
+ },
+ "big-integer": {
+ "version": "1.6.32",
+ "bundled": true
+ },
+ "bplist-creator": {
+ "version": "0.0.7",
+ "bundled": true,
+ "requires": {
+ "stream-buffers": "~2.2.0"
+ }
+ },
+ "bplist-parser": {
+ "version": "0.1.1",
+ "bundled": true,
+ "requires": {
+ "big-integer": "^1.6.7"
+ }
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "bundled": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "bundled": true
+ },
+ "cordova-common": {
+ "version": "2.2.5",
+ "bundled": true,
+ "requires": {
+ "ansi": "^0.3.1",
+ "bplist-parser": "^0.1.0",
+ "cordova-registry-mapper": "^1.1.8",
+ "elementtree": "0.1.6",
+ "glob": "^5.0.13",
+ "minimatch": "^3.0.0",
+ "plist": "^2.1.0",
+ "q": "^1.4.1",
+ "shelljs": "^0.5.3",
+ "underscore": "^1.8.3",
+ "unorm": "^1.3.3"
+ }
+ },
+ "cordova-registry-mapper": {
+ "version": "1.1.15",
+ "bundled": true
+ },
+ "elementtree": {
+ "version": "0.1.6",
+ "bundled": true,
+ "requires": {
+ "sax": "0.3.5"
+ }
+ },
+ "glob": {
+ "version": "5.0.15",
+ "bundled": true,
+ "requires": {
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "2 || 3",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "bundled": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "bundled": true
+ },
+ "ios-sim": {
+ "version": "6.1.3",
+ "bundled": true,
+ "requires": {
+ "bplist-parser": "^0.0.6",
+ "nopt": "1.0.9",
+ "plist": "^2.1.0",
+ "simctl": "^1.1.1"
+ },
+ "dependencies": {
+ "bplist-parser": {
+ "version": "0.0.6",
+ "bundled": true
+ },
+ "nopt": {
+ "version": "1.0.9",
+ "bundled": true,
+ "requires": {
+ "abbrev": "1"
+ }
+ }
+ }
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "bundled": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "nopt": {
+ "version": "3.0.6",
+ "bundled": true,
+ "requires": {
+ "abbrev": "1"
+ }
+ },
+ "once": {
+ "version": "1.4.0",
+ "bundled": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "bundled": true
+ },
+ "plist": {
+ "version": "2.1.0",
+ "bundled": true,
+ "requires": {
+ "base64-js": "1.2.0",
+ "xmlbuilder": "8.2.2",
+ "xmldom": "0.1.x"
+ }
+ },
+ "q": {
+ "version": "1.5.1",
+ "bundled": true
+ },
+ "sax": {
+ "version": "0.3.5",
+ "bundled": true
+ },
+ "shelljs": {
+ "version": "0.5.3",
+ "bundled": true
+ },
+ "simctl": {
+ "version": "1.1.1",
+ "bundled": true,
+ "requires": {
+ "shelljs": "^0.2.6",
+ "tail": "^0.4.0"
+ },
+ "dependencies": {
+ "shelljs": {
+ "version": "0.2.6",
+ "bundled": true
+ }
+ }
+ },
+ "simple-plist": {
+ "version": "0.2.1",
+ "bundled": true,
+ "requires": {
+ "bplist-creator": "0.0.7",
+ "bplist-parser": "0.1.1",
+ "plist": "2.0.1"
+ },
+ "dependencies": {
+ "base64-js": {
+ "version": "1.1.2",
+ "bundled": true
+ },
+ "plist": {
+ "version": "2.0.1",
+ "bundled": true,
+ "requires": {
+ "base64-js": "1.1.2",
+ "xmlbuilder": "8.2.2",
+ "xmldom": "0.1.x"
+ }
+ }
+ }
+ },
+ "stream-buffers": {
+ "version": "2.2.0",
+ "bundled": true
+ },
+ "tail": {
+ "version": "0.4.0",
+ "bundled": true
+ },
+ "underscore": {
+ "version": "1.9.1",
+ "bundled": true
+ },
+ "unorm": {
+ "version": "1.4.1",
+ "bundled": true
+ },
+ "uuid": {
+ "version": "3.0.1",
+ "bundled": true
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "bundled": true
+ },
+ "xcode": {
+ "version": "0.9.3",
+ "bundled": true,
+ "requires": {
+ "pegjs": "^0.10.0",
+ "simple-plist": "^0.2.1",
+ "uuid": "3.0.1"
+ }
+ },
+ "xml-escape": {
+ "version": "1.1.0",
+ "bundled": true
+ },
+ "xmlbuilder": {
+ "version": "8.2.2",
+ "bundled": true
+ },
+ "xmldom": {
+ "version": "0.1.27",
+ "bundled": true
+ }
+ }
+ },
+ "cordova-plugin-badge": {
+ "version": "0.8.8",
+ "resolved": "https://registry.npmjs.org/cordova-plugin-badge/-/cordova-plugin-badge-0.8.8.tgz",
+ "integrity": "sha512-RhIBtd5xhD/iLnxjt35jvOae28oNW/wtMZBOmQR3Rf0y4wirvA1bpAZEhBoFqL+rZGhsd6ddOdQXdex1T0DRyQ=="
+ },
+ "cordova-plugin-camera": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cordova-plugin-camera/-/cordova-plugin-camera-4.1.0.tgz",
+ "integrity": "sha512-fCLhWjWYn49q3X5xaypAPgTz6MAWSKFFQvD2Gpi5SuVlrRPRphtX2jIqR2zCBuDTBR082QVnlc+yUDXt65Mjgw=="
+ },
+ "cordova-plugin-customurlscheme": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/cordova-plugin-customurlscheme/-/cordova-plugin-customurlscheme-4.4.0.tgz",
+ "integrity": "sha512-7VPJnNfvfZQSU1IdhJX7BpDgvC7bEe+Kfg9Cj8guSoZDcTi378qQFb6VOwthT8hwGXx2bZzWf0qnTZdRlLQy+Q=="
+ },
+ "cordova-plugin-device": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/cordova-plugin-device/-/cordova-plugin-device-2.0.3.tgz",
+ "integrity": "sha512-Jb3V72btxf3XHpkPQsGdyc8N6tVBYn1vsxSFj43fIz9vonJDUThYPCJJHqk6PX6N4dJw6I4FjxkpfCR4LDYMlw=="
+ },
+ "cordova-plugin-file": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/cordova-plugin-file/-/cordova-plugin-file-6.0.2.tgz",
+ "integrity": "sha512-m7cughw327CjONN/qjzsTpSesLaeybksQh420/gRuSXJX5Zt9NfgsSbqqKDon6jnQ9Mm7h7imgyO2uJ34XMBtA=="
+ },
+ "cordova-plugin-file-opener2": {
+ "version": "2.0.19",
+ "resolved": "https://registry.npmjs.org/cordova-plugin-file-opener2/-/cordova-plugin-file-opener2-2.0.19.tgz",
+ "integrity": "sha1-yjrhIlOVt3qx/lsgrMv+zGiOJJM="
+ },
+ "cordova-plugin-file-transfer": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/cordova-plugin-file-transfer/-/cordova-plugin-file-transfer-1.7.1.tgz",
+ "integrity": "sha1-p12L4uvDu5sjxbG70ZkhTsJnWGs="
+ },
+ "cordova-plugin-globalization": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/cordova-plugin-globalization/-/cordova-plugin-globalization-1.11.0.tgz",
+ "integrity": "sha1-6sMVgQAphJOvowvolA5pj2HvvP4="
+ },
+ "cordova-plugin-inappbrowser": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cordova-plugin-inappbrowser/-/cordova-plugin-inappbrowser-3.1.0.tgz",
+ "integrity": "sha512-YqrZfYgbGTS20SFID0mrRxud312VH072QVlFonCAkPgtGg1Svy7lELOCNFd+KU7t4mVtZeTEjZPEeefvjaetwQ=="
+ },
+ "cordova-plugin-ionic-keyboard": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/cordova-plugin-ionic-keyboard/-/cordova-plugin-ionic-keyboard-2.1.3.tgz",
+ "integrity": "sha512-6ucQ6FdlLdBm8kJfFnzozmBTjru/0xekHP/dAhjoCZggkGRlgs8TsUJFkxa/bV+qi7Dlo50JjmpE4UMWAO+aOQ=="
+ },
+ "cordova-plugin-local-notification": {
+ "version": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#5b2f3073a1c1fb39cad3566be792445c343db2c6",
+ "from": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle"
+ },
+ "cordova-plugin-media-capture": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/cordova-plugin-media-capture/-/cordova-plugin-media-capture-3.0.3.tgz",
+ "integrity": "sha512-pVQOrNM7VAuVUMXibAlMGIArrftHPrRs4dUCoE+e2HEFUp3LmN3Yj539LjdUxcWmz/A/cHC65m9E3DS56YJhcg=="
+ },
+ "cordova-plugin-network-information": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/cordova-plugin-network-information/-/cordova-plugin-network-information-2.0.2.tgz",
+ "integrity": "sha512-NwO3qDBNL/vJxUxBTPNOA1HvkDf9eTeGH8JSZiwy1jq2W2mJKQEDBwqWkaEQS19Yd/MQTiw0cykxg5D7u4J6cQ=="
+ },
+ "cordova-plugin-screen-orientation": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/cordova-plugin-screen-orientation/-/cordova-plugin-screen-orientation-3.0.2.tgz",
+ "integrity": "sha512-2w6CMC+HGvbhogJetalwGurL2Fx8DQCCPy3wlSZHN1/W7WoQ5n9ujVozcoKrY4VaagK6bxrPFih+ElkO8Uqfzg=="
+ },
+ "cordova-plugin-splashscreen": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/cordova-plugin-splashscreen/-/cordova-plugin-splashscreen-5.0.3.tgz",
+ "integrity": "sha512-rnoDXMDfzoeHDBvsnu6JmzDE/pV5YJCAfc5hYX/Mb2BIXGgSjFJheByt0tU6kp3Wl40tSyFX4pYfBwFblBGyRg=="
+ },
+ "cordova-plugin-statusbar": {
+ "version": "2.4.3",
+ "resolved": "https://registry.npmjs.org/cordova-plugin-statusbar/-/cordova-plugin-statusbar-2.4.3.tgz",
+ "integrity": "sha512-ThmXzl6QIKWFXf4wWw7Q/zpB+VKkz3VM958+5A0sXD4jmR++u7KnGttLksXshVwWr6lvGwUebLYtIyXwS4Ovcg=="
+ },
+ "cordova-plugin-whitelist": {
+ "version": "1.3.4",
+ "resolved": "https://registry.npmjs.org/cordova-plugin-whitelist/-/cordova-plugin-whitelist-1.3.4.tgz",
+ "integrity": "sha512-EYC5eQFVkoYXq39l7tYKE6lEjHJ04mvTmKXxGL7quHLdFPfJMNzru/UYpn92AOfpl3PQaZmou78C7EgmFOwFQQ=="
+ },
+ "cordova-plugin-zip": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/cordova-plugin-zip/-/cordova-plugin-zip-3.1.0.tgz",
+ "integrity": "sha1-F2yCSOog058c+VnvXmFWrMqWshc="
+ },
+ "cordova-sqlite-storage": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/cordova-sqlite-storage/-/cordova-sqlite-storage-2.6.0.tgz",
+ "integrity": "sha512-m+KylFwNRsQloJ6TZihupfma2muXkmNglh8cFSiJQqriTTm6eq0gyPaAuKxgoPswe679G+0aUai07NFC/f0GGQ==",
+ "requires": {
+ "cordova-sqlite-storage-dependencies": "1.2.1"
+ }
+ },
+ "cordova-sqlite-storage-dependencies": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/cordova-sqlite-storage-dependencies/-/cordova-sqlite-storage-dependencies-1.2.1.tgz",
+ "integrity": "sha512-4ihQApBGVKR1QZ4oOSGctKFfthtCfiWMTcIIfxe97vKxlvGr9NyXOvYG9vXU9S7yVR7Ua+Rj47hkE7pQIKvQTg=="
+ },
+ "cordova-support-google-services": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/cordova-support-google-services/-/cordova-support-google-services-1.2.1.tgz",
+ "integrity": "sha512-EnFjKAE9oI2uzyUvEfWpLgTM200nuJVvShaA4yyz9wMKBUN+H/BRG1byd1ibZz3sSihNKi3FxjQPxmmEn6/IfA=="
+ },
+ "core-js": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.3.0.tgz",
+ "integrity": "sha1-+rg/uwstjchfpjbEudNMdUIMbWU="
+ },
+ "core-util-is": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
+ "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac="
+ },
+ "create-ecdh": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.3.tgz",
+ "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.1.0",
+ "elliptic": "^6.0.0"
+ }
+ },
+ "create-hash": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+ "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+ "dev": true,
+ "requires": {
+ "cipher-base": "^1.0.1",
+ "inherits": "^2.0.1",
+ "md5.js": "^1.3.4",
+ "ripemd160": "^2.0.1",
+ "sha.js": "^2.4.0"
+ }
+ },
+ "create-hmac": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+ "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+ "dev": true,
+ "requires": {
+ "cipher-base": "^1.0.3",
+ "create-hash": "^1.1.0",
+ "inherits": "^2.0.1",
+ "ripemd160": "^2.0.0",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
+ "cross-spawn": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz",
+ "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=",
+ "dev": true,
+ "requires": {
+ "lru-cache": "^4.0.1",
+ "shebang-command": "^1.2.0",
+ "which": "^1.2.9"
+ }
+ },
+ "crypto-browserify": {
+ "version": "3.12.0",
+ "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz",
+ "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==",
+ "dev": true,
+ "requires": {
+ "browserify-cipher": "^1.0.0",
+ "browserify-sign": "^4.0.0",
+ "create-ecdh": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "create-hmac": "^1.1.0",
+ "diffie-hellman": "^5.0.0",
+ "inherits": "^2.0.1",
+ "pbkdf2": "^3.0.3",
+ "public-encrypt": "^4.0.0",
+ "randombytes": "^2.0.0",
+ "randomfill": "^1.0.3"
+ }
+ },
+ "currently-unhandled": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz",
+ "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=",
+ "dev": true,
+ "requires": {
+ "array-find-index": "^1.0.1"
+ }
+ },
+ "d": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz",
+ "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=",
+ "dev": true,
+ "requires": {
+ "es5-ext": "^0.10.9"
+ }
+ },
+ "dashdash": {
+ "version": "1.14.1",
+ "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
+ "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=",
+ "dev": true,
+ "requires": {
+ "assert-plus": "^1.0.0"
+ },
+ "dependencies": {
+ "assert-plus": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
+ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=",
+ "dev": true
+ }
+ }
+ },
+ "date-now": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz",
+ "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=",
+ "dev": true
+ },
+ "dateformat": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-2.2.0.tgz",
+ "integrity": "sha1-QGXiATz5+5Ft39gu+1Bq1MZ2kGI=",
+ "dev": true
+ },
+ "debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "dev": true,
+ "requires": {
+ "ms": "2.0.0"
+ }
+ },
+ "decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
+ "dev": true
+ },
+ "decode-uri-component": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
+ "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=",
+ "dev": true
+ },
+ "default-compare": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/default-compare/-/default-compare-1.0.0.tgz",
+ "integrity": "sha512-QWfXlM0EkAbqOCbD/6HjdwT19j7WCkMyiRhWilc4H9/5h/RzTF9gv5LYh1+CmDV5d1rki6KAWLtQale0xt20eQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^5.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+ "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+ "dev": true
+ }
+ }
+ },
+ "default-resolution": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/default-resolution/-/default-resolution-2.0.0.tgz",
+ "integrity": "sha1-vLgrqnKtebQmp2cy8aga1t8m1oQ=",
+ "dev": true
+ },
+ "define-properties": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz",
+ "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=",
+ "requires": {
+ "foreach": "^2.0.5",
+ "object-keys": "^1.0.8"
+ }
+ },
+ "define-property": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz",
+ "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^1.0.2",
+ "isobject": "^3.0.1"
+ },
+ "dependencies": {
+ "is-accessor-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
+ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-data-descriptor": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz",
+ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==",
+ "dev": true,
+ "requires": {
+ "kind-of": "^6.0.0"
+ }
+ },
+ "is-descriptor": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz",
+ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "^1.0.0",
+ "is-data-descriptor": "^1.0.0",
+ "kind-of": "^6.0.2"
+ }
+ },
+ "isobject": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
+ "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
+ "dev": true
+ },
+ "kind-of": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.2.tgz",
+ "integrity": "sha512-s5kLOcnH0XqDO+FvuaLX8DDjZ18CGFk7VygH40QoKPUQhW4e2rvM0rwUq0t8IQDOwYSeLK01U90OjzBTme2QqA==",
+ "dev": true
+ }
+ }
+ },
+ "delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
+ "dev": true
+ },
+ "delegates": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
+ "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=",
+ "dev": true
+ },
+ "depd": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz",
+ "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=",
+ "dev": true
+ },
+ "des.js": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.0.tgz",
+ "integrity": "sha1-wHTS4qpqipoH29YfmhXCzYPsjsw=",
+ "dev": true,
+ "requires": {
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "destroy": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz",
+ "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=",
+ "dev": true
+ },
+ "detect-file": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz",
+ "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=",
+ "dev": true
+ },
+ "detect-libc": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
+ "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=",
+ "dev": true
+ },
+ "diff": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz",
+ "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==",
+ "dev": true
+ },
+ "diffie-hellman": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+ "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
+ "dev": true,
+ "requires": {
+ "bn.js": "^4.1.0",
+ "miller-rabin": "^4.0.0",
+ "randombytes": "^2.0.0"
+ }
+ },
+ "doctrine": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-0.7.2.tgz",
+ "integrity": "sha1-fLhgNZujvpDgQLJrcpzkv6ZUxSM=",
+ "dev": true,
+ "requires": {
+ "esutils": "^1.1.6",
+ "isarray": "0.0.1"
+ },
+ "dependencies": {
+ "esutils": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.1.6.tgz",
+ "integrity": "sha1-wBzKqa5LiXxtDD4hCuUvPHqEQ3U=",
+ "dev": true
+ },
+ "isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+ "dev": true
+ }
+ }
+ },
+ "domain-browser": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz",
+ "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==",
+ "dev": true
+ },
+ "dotenv": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz",
+ "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==",
+ "dev": true
+ },
+ "dotenv-defaults": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/dotenv-defaults/-/dotenv-defaults-1.0.2.tgz",
+ "integrity": "sha512-iXFvHtXl/hZPiFj++1hBg4lbKwGM+t/GlvELDnRtOFdjXyWP7mubkVr+eZGWG62kdsbulXAef6v/j6kiWc/xGA==",
+ "dev": true,
+ "requires": {
+ "dotenv": "^6.2.0"
+ }
+ },
+ "dotenv-expand": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-4.2.0.tgz",
+ "integrity": "sha1-3vHxyl1gWdJKdm5YeULCEQbOEnU=",
+ "dev": true
+ },
+ "dotenv-webpack": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/dotenv-webpack/-/dotenv-webpack-1.7.0.tgz",
+ "integrity": "sha512-wwNtOBW/6gLQSkb8p43y0Wts970A3xtNiG/mpwj9MLUhtPCQG6i+/DSXXoNN7fbPCU/vQ7JjwGmgOeGZSSZnsw==",
+ "dev": true,
+ "requires": {
+ "dotenv-defaults": "^1.0.2"
+ }
+ },
+ "duplexer2": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz",
+ "integrity": "sha1-xhTc9n4vsUmVqRcR5aYX6KYKMds=",
+ "dev": true,
+ "requires": {
+ "readable-stream": "~1.1.9"
+ },
+ "dependencies": {
+ "isarray": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz",
+ "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=",
+ "dev": true
+ },
+ "readable-stream": {
+ "version": "1.1.14",
+ "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz",
+ "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=",
+ "dev": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.1",
+ "isarray": "0.0.1",
+ "string_decoder": "~0.10.x"
+ }
+ },
+ "string_decoder": {
+ "version": "0.10.31",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz",
+ "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=",
+ "dev": true
+ }
+ }
+ },
+ "duplexify": {
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz",
+ "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==",
+ "dev": true,
+ "requires": {
+ "end-of-stream": "^1.0.0",
+ "inherits": "^2.0.1",
+ "readable-stream": "^2.0.0",
+ "stream-shift": "^1.0.0"
+ }
+ },
+ "each-props": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/each-props/-/each-props-1.3.2.tgz",
+ "integrity": "sha512-vV0Hem3zAGkJAyU7JSjixeU66rwdynTAa1vofCrSA5fEln+m67Az9CcnkVD776/fsN/UjIWmBDoNRS6t6G9RfA==",
+ "dev": true,
+ "requires": {
+ "is-plain-object": "^2.0.1",
+ "object.defaults": "^1.1.0"
+ }
+ },
+ "ecc-jsbn": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz",
+ "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "jsbn": "~0.1.0"
+ }
+ },
+ "ee-first": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
+ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=",
+ "dev": true
+ },
+ "ejs": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/ejs/-/ejs-2.6.1.tgz",
+ "integrity": "sha512-0xy4A/twfrRCnkhfk8ErDi5DqdAsAqeGxht4xkCUrsvhhbQNs7E+4jV0CN7+NKIY0aHE72+XvqtBIXzD31ZbXQ==",
+ "dev": true
+ },
+ "electron-builder-lib": {
+ "version": "20.23.1",
+ "resolved": "https://registry.npmjs.org/electron-builder-lib/-/electron-builder-lib-20.23.1.tgz",
+ "integrity": "sha512-9bYeANVqFPpSmswPwXv8efu9voPE1Q8hw/jNwiWGICjPeYjHyKwa4ao+Vd1beY6ZhUDVhxxXIdlJWnmvH7Mcxw==",
+ "dev": true,
+ "requires": {
+ "7zip-bin": "~4.0.2",
+ "app-builder-bin": "1.11.4",
+ "async-exit-hook": "^2.0.1",
+ "bluebird-lst": "^1.0.5",
+ "builder-util": "5.17.0",
+ "builder-util-runtime": "4.4.1",
+ "chromium-pickle-js": "^0.2.0",
+ "debug": "^3.1.0",
+ "ejs": "^2.6.1",
+ "electron-osx-sign": "0.4.10",
+ "electron-publish": "20.23.1",
+ "env-paths": "^1.0.0",
+ "fs-extra-p": "^4.6.1",
+ "hosted-git-info": "^2.7.1",
+ "is-ci": "^1.1.0",
+ "isbinaryfile": "^3.0.2",
+ "js-yaml": "^3.12.0",
+ "lazy-val": "^1.0.3",
+ "minimatch": "^3.0.4",
+ "normalize-package-data": "^2.4.0",
+ "plist": "^3.0.1",
+ "read-config-file": "3.1.0",
+ "sanitize-filename": "^1.6.1",
+ "semver": "^5.5.0",
+ "sumchecker": "^2.0.2",
+ "temp-file": "^3.1.3"
+ },
+ "dependencies": {
+ "app-builder-bin": {
+ "version": "1.11.4",
+ "resolved": "https://registry.npmjs.org/app-builder-bin/-/app-builder-bin-1.11.4.tgz",
+ "integrity": "sha512-04sgoFSz6q5pbAxAXcxfUFPl16gJsay5b8dudFXUwQbFfS7ox2uGgbOO5LGHF0t7sM7q/N82ztGePuuCSkKZHQ==",
+ "dev": true
+ },
+ "builder-util-runtime": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-4.4.1.tgz",
+ "integrity": "sha512-8L2pbL6D3VdI1f8OMknlZJpw0c7KK15BRz3cY77AOUElc4XlCv2UhVV01jJM7+6Lx7henaQh80ALULp64eFYAQ==",
+ "dev": true,
+ "requires": {
+ "bluebird-lst": "^1.0.5",
+ "debug": "^3.1.0",
+ "fs-extra-p": "^4.6.1",
+ "sax": "^1.2.4"
+ }
+ },
+ "debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "hosted-git-info": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.7.1.tgz",
+ "integrity": "sha512-7T/BxH19zbcCTa8XkMlbK5lTo1WtgkFi3GvdWEyNuc4Vex7/9Dqbnpsf4JMydcfj9HCg4zUWFTL3Za6lapg5/w==",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
+ "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
+ "dev": true
+ }
+ }
+ },
+ "electron-osx-sign": {
+ "version": "0.4.10",
+ "resolved": "http://registry.npmjs.org/electron-osx-sign/-/electron-osx-sign-0.4.10.tgz",
+ "integrity": "sha1-vk87ibKnWh3F8eckkIGrKSnKOiY=",
+ "dev": true,
+ "requires": {
+ "bluebird": "^3.5.0",
+ "compare-version": "^0.1.2",
+ "debug": "^2.6.8",
+ "isbinaryfile": "^3.0.2",
+ "minimist": "^1.2.0",
+ "plist": "^2.1.0"
+ },
+ "dependencies": {
+ "base64-js": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.2.0.tgz",
+ "integrity": "sha1-o5mS1yNYSBGYK+XikLtqU9hnAPE=",
+ "dev": true
+ },
+ "plist": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/plist/-/plist-2.1.0.tgz",
+ "integrity": "sha1-V8zbeggh3yGDEhejytVOPhRqECU=",
+ "dev": true,
+ "requires": {
+ "base64-js": "1.2.0",
+ "xmlbuilder": "8.2.2",
+ "xmldom": "0.1.x"
+ }
+ },
+ "xmlbuilder": {
+ "version": "8.2.2",
+ "resolved": "http://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz",
+ "integrity": "sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=",
+ "dev": true
+ }
+ }
+ },
+ "electron-publish": {
+ "version": "20.23.1",
+ "resolved": "https://registry.npmjs.org/electron-publish/-/electron-publish-20.23.1.tgz",
+ "integrity": "sha512-FsNL5bY4/Uab5YGYWE+/7wL8znkghEPwJvDyD0985Obw0Eg9JZP1MpUbt4jUxrK8z5wTLVMFdSv7ymV8yq6ypA==",
+ "dev": true,
+ "requires": {
+ "bluebird-lst": "^1.0.5",
+ "builder-util": "~5.17.0",
+ "builder-util-runtime": "^4.4.1",
+ "chalk": "^2.4.1",
+ "fs-extra-p": "^4.6.1",
+ "lazy-val": "^1.0.3",
+ "mime": "^2.3.1"
+ },
+ "dependencies": {
+ "builder-util-runtime": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-4.4.1.tgz",
+ "integrity": "sha512-8L2pbL6D3VdI1f8OMknlZJpw0c7KK15BRz3cY77AOUElc4XlCv2UhVV01jJM7+6Lx7henaQh80ALULp64eFYAQ==",
+ "dev": true,
+ "requires": {
+ "bluebird-lst": "^1.0.5",
+ "debug": "^3.1.0",
+ "fs-extra-p": "^4.6.1",
+ "sax": "^1.2.4"
+ }
+ },
+ "debug": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
+ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
+ "dev": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "mime": {
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.0.tgz",
+ "integrity": "sha512-ikBcWwyqXQSHKtciCcctu9YfPbFYZ4+gbHEmE0Q8jzcTYQg5dHCr3g2wwAZjPoJfQVXZq6KXAjpXOTf5/cjT7w==",
+ "dev": true
+ },
+ "ms": {
+ "version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==",
"dev": true
@@ -4241,9 +8895,9 @@
}
},
"extend": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz",
- "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=",
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
+ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"dev": true
},
"extend-shallow": {
@@ -4356,13 +9010,13 @@
}
},
"findup-sync": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz",
- "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz",
+ "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==",
"dev": true,
"requires": {
"detect-file": "^1.0.0",
- "is-glob": "^3.1.0",
+ "is-glob": "^4.0.0",
"micromatch": "^3.0.4",
"resolve-dir": "^1.0.1"
},
@@ -4595,12 +9249,12 @@
"dev": true
},
"is-glob": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
- "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+ "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
"dev": true,
"requires": {
- "is-extglob": "^2.1.0"
+ "is-extglob": "^2.1.1"
}
},
"is-number": {
@@ -4659,9 +9313,9 @@
}
},
"fined": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/fined/-/fined-1.1.0.tgz",
- "integrity": "sha1-s33IRLdqL15wgeiE98CuNE8VNHY=",
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz",
+ "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==",
"dev": true,
"requires": {
"expand-tilde": "^2.0.2",
@@ -4672,19 +9326,19 @@
}
},
"flagged-respawn": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.0.tgz",
- "integrity": "sha1-Tnmumy6zi/hrO7Vr8+ClaqX8q9c=",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz",
+ "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==",
"dev": true
},
"flush-write-stream": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.0.3.tgz",
- "integrity": "sha512-calZMC10u0FMUqoiunI2AiGIIUtUIvifNwkHhNupZH4cbNnW1Itkoh/Nf5HFYmDrwWPjrUxpkZT0KhuCq0jmGw==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz",
+ "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==",
"dev": true,
"requires": {
- "inherits": "^2.0.1",
- "readable-stream": "^2.0.4"
+ "inherits": "^2.0.3",
+ "readable-stream": "^2.3.6"
}
},
"font-awesome": {
@@ -4799,13 +9453,13 @@
"dev": true
},
"fsevents": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz",
- "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==",
+ "version": "1.2.9",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz",
+ "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==",
"optional": true,
"requires": {
- "nan": "^2.9.2",
- "node-pre-gyp": "^0.10.0"
+ "nan": "^2.12.1",
+ "node-pre-gyp": "^0.12.0"
},
"dependencies": {
"abbrev": {
@@ -4823,7 +9477,7 @@
"optional": true
},
"are-we-there-yet": {
- "version": "1.1.4",
+ "version": "1.1.5",
"bundled": true,
"optional": true,
"requires": {
@@ -4844,7 +9498,7 @@
}
},
"chownr": {
- "version": "1.0.1",
+ "version": "1.1.1",
"bundled": true,
"optional": true
},
@@ -4866,15 +9520,15 @@
"optional": true
},
"debug": {
- "version": "2.6.9",
+ "version": "4.1.1",
"bundled": true,
"optional": true,
"requires": {
- "ms": "2.0.0"
+ "ms": "^2.1.1"
}
},
"deep-extend": {
- "version": "0.5.1",
+ "version": "0.6.0",
"bundled": true,
"optional": true
},
@@ -4917,7 +9571,7 @@
}
},
"glob": {
- "version": "7.1.2",
+ "version": "7.1.3",
"bundled": true,
"optional": true,
"requires": {
@@ -4935,11 +9589,11 @@
"optional": true
},
"iconv-lite": {
- "version": "0.4.21",
+ "version": "0.4.24",
"bundled": true,
"optional": true,
"requires": {
- "safer-buffer": "^2.1.0"
+ "safer-buffer": ">= 2.1.2 < 3"
}
},
"ignore-walk": {
@@ -4992,15 +9646,15 @@
"bundled": true
},
"minipass": {
- "version": "2.2.4",
+ "version": "2.3.5",
"bundled": true,
"requires": {
- "safe-buffer": "^5.1.1",
+ "safe-buffer": "^5.1.2",
"yallist": "^3.0.0"
}
},
"minizlib": {
- "version": "1.1.0",
+ "version": "1.2.1",
"bundled": true,
"optional": true,
"requires": {
@@ -5015,32 +9669,38 @@
}
},
"ms": {
- "version": "2.0.0",
+ "version": "2.1.1",
"bundled": true,
"optional": true
},
+ "nan": {
+ "version": "2.14.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
+ "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==",
+ "optional": true
+ },
"needle": {
- "version": "2.2.0",
+ "version": "2.3.0",
"bundled": true,
"optional": true,
"requires": {
- "debug": "^2.1.2",
+ "debug": "^4.1.0",
"iconv-lite": "^0.4.4",
"sax": "^1.2.4"
}
},
"node-pre-gyp": {
- "version": "0.10.0",
+ "version": "0.12.0",
"bundled": true,
"optional": true,
"requires": {
"detect-libc": "^1.0.2",
"mkdirp": "^0.5.1",
- "needle": "^2.2.0",
+ "needle": "^2.2.1",
"nopt": "^4.0.1",
"npm-packlist": "^1.1.6",
"npmlog": "^4.0.2",
- "rc": "^1.1.7",
+ "rc": "^1.2.7",
"rimraf": "^2.6.1",
"semver": "^5.3.0",
"tar": "^4"
@@ -5056,12 +9716,12 @@
}
},
"npm-bundled": {
- "version": "1.0.3",
+ "version": "1.0.6",
"bundled": true,
"optional": true
},
"npm-packlist": {
- "version": "1.1.10",
+ "version": "1.4.1",
"bundled": true,
"optional": true,
"requires": {
@@ -5126,11 +9786,11 @@
"optional": true
},
"rc": {
- "version": "1.2.7",
+ "version": "1.2.8",
"bundled": true,
"optional": true,
"requires": {
- "deep-extend": "^0.5.1",
+ "deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
@@ -5158,15 +9818,15 @@
}
},
"rimraf": {
- "version": "2.6.2",
+ "version": "2.6.3",
"bundled": true,
"optional": true,
"requires": {
- "glob": "^7.0.5"
+ "glob": "^7.1.3"
}
},
"safe-buffer": {
- "version": "5.1.1",
+ "version": "5.1.2",
"bundled": true
},
"safer-buffer": {
@@ -5180,7 +9840,7 @@
"optional": true
},
"semver": {
- "version": "5.5.0",
+ "version": "5.7.0",
"bundled": true,
"optional": true
},
@@ -5224,16 +9884,16 @@
"optional": true
},
"tar": {
- "version": "4.4.1",
+ "version": "4.4.8",
"bundled": true,
"optional": true,
"requires": {
- "chownr": "^1.0.1",
+ "chownr": "^1.1.1",
"fs-minipass": "^1.2.5",
- "minipass": "^2.2.4",
- "minizlib": "^1.1.0",
+ "minipass": "^2.3.4",
+ "minizlib": "^1.1.1",
"mkdirp": "^0.5.0",
- "safe-buffer": "^5.1.1",
+ "safe-buffer": "^5.1.2",
"yallist": "^3.0.2"
}
},
@@ -5243,11 +9903,11 @@
"optional": true
},
"wide-align": {
- "version": "1.1.2",
+ "version": "1.1.3",
"bundled": true,
"optional": true,
"requires": {
- "string-width": "^1.0.2"
+ "string-width": "^1.0.2 || 2"
}
},
"wrappy": {
@@ -5255,7 +9915,7 @@
"bundled": true
},
"yallist": {
- "version": "3.0.2",
+ "version": "3.0.3",
"bundled": true
}
}
@@ -5420,13 +10080,15 @@
}
},
"glob-watcher": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.1.tgz",
- "integrity": "sha512-fK92r2COMC199WCyGUblrZKhjra3cyVMDiypDdqg1vsSDmexnbYivK1kNR4QItiNXLKmGlqan469ks67RtNa2g==",
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/glob-watcher/-/glob-watcher-5.0.3.tgz",
+ "integrity": "sha512-8tWsULNEPHKQ2MR4zXuzSmqbdyV5PtwwCaWSGQ1WwHsJ07ilNeN1JB8ntxhckbnpSHaf9dXFUHzIWvm1I13dsg==",
"dev": true,
"requires": {
+ "anymatch": "^2.0.0",
"async-done": "^1.2.0",
"chokidar": "^2.0.0",
+ "is-negated-glob": "^1.0.0",
"just-debounce": "^1.0.0",
"object.defaults": "^1.1.0"
},
@@ -5459,17 +10121,196 @@
"integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==",
"dev": true,
"requires": {
- "arr-flatten": "^1.1.0",
+ "arr-flatten": "^1.1.0",
+ "array-unique": "^0.3.2",
+ "extend-shallow": "^2.0.1",
+ "fill-range": "^4.0.0",
+ "isobject": "^3.0.1",
+ "repeat-element": "^1.1.2",
+ "snapdragon": "^0.8.1",
+ "snapdragon-node": "^2.0.1",
+ "split-string": "^3.0.2",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "chokidar": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.6.tgz",
+ "integrity": "sha512-V2jUo67OKkc6ySiRpJrjlpJKl9kDuG+Xb8VgsGzb+aEouhgS1D0weyPU4lEzdAcsCAvrih2J2BqyXqHWvVLw5g==",
+ "dev": true,
+ "requires": {
+ "anymatch": "^2.0.0",
+ "async-each": "^1.0.1",
+ "braces": "^2.3.2",
+ "fsevents": "^1.2.7",
+ "glob-parent": "^3.1.0",
+ "inherits": "^2.0.3",
+ "is-binary-path": "^1.0.0",
+ "is-glob": "^4.0.0",
+ "normalize-path": "^3.0.0",
+ "path-is-absolute": "^1.0.0",
+ "readdirp": "^2.2.1",
+ "upath": "^1.1.1"
+ },
+ "dependencies": {
+ "normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true
+ }
+ }
+ },
+ "expand-brackets": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
+ "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
+ "dev": true,
+ "requires": {
+ "debug": "^2.3.3",
+ "define-property": "^0.2.5",
+ "extend-shallow": "^2.0.1",
+ "posix-character-classes": "^0.1.0",
+ "regex-not": "^1.0.0",
+ "snapdragon": "^0.8.1",
+ "to-regex": "^3.0.1"
+ },
+ "dependencies": {
+ "define-property": {
+ "version": "0.2.5",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
+ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^0.1.0"
+ }
+ },
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ },
+ "is-accessor-descriptor": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
+ "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "is-data-descriptor": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
+ "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
+ "dev": true,
+ "requires": {
+ "kind-of": "^3.0.2"
+ },
+ "dependencies": {
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "dev": true,
+ "requires": {
+ "is-buffer": "^1.1.5"
+ }
+ }
+ }
+ },
+ "is-descriptor": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
+ "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+ "dev": true,
+ "requires": {
+ "is-accessor-descriptor": "^0.1.6",
+ "is-data-descriptor": "^0.1.4",
+ "kind-of": "^5.0.0"
+ }
+ },
+ "kind-of": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
+ "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
+ "dev": true
+ }
+ }
+ },
+ "extglob": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
+ "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
+ "dev": true,
+ "requires": {
"array-unique": "^0.3.2",
+ "define-property": "^1.0.0",
+ "expand-brackets": "^2.1.4",
"extend-shallow": "^2.0.1",
- "fill-range": "^4.0.0",
- "isobject": "^3.0.1",
- "repeat-element": "^1.1.2",
+ "fragment-cache": "^0.2.1",
+ "regex-not": "^1.0.0",
"snapdragon": "^0.8.1",
- "snapdragon-node": "^2.0.1",
- "split-string": "^3.0.2",
"to-regex": "^3.0.1"
},
+ "dependencies": {
+ "define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
+ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "dev": true,
+ "requires": {
+ "is-descriptor": "^1.0.0"
+ }
+ },
+ "extend-shallow": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
+ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "dev": true,
+ "requires": {
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "fill-range": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
+ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
+ "dev": true,
+ "requires": {
+ "extend-shallow": "^2.0.1",
+ "is-number": "^3.0.0",
+ "repeat-string": "^1.6.1",
+ "to-regex-range": "^2.1.0"
+ },
"dependencies": {
"extend-shallow": {
"version": "2.0.1",
@@ -5477,180 +10318,537 @@
"integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
"dev": true,
"requires": {
- "is-extendable": "^0.1.0"
+ "is-extendable": "^0.1.0"
+ }
+ }
+ }
+ },
+ "fsevents": {
+ "version": "1.2.9",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.9.tgz",
+ "integrity": "sha512-oeyj2H3EjjonWcFjD5NvZNE9Rqe4UW+nQBU2HNeKw0koVLEFIhtyETyAakeAM3de7Z/SW5kcA+fZUait9EApnw==",
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "nan": "^2.12.1",
+ "node-pre-gyp": "^0.12.0"
+ },
+ "dependencies": {
+ "abbrev": {
+ "version": "1.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "ansi-regex": {
+ "version": "2.1.1",
+ "bundled": true,
+ "dev": true
+ },
+ "aproba": {
+ "version": "1.2.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "are-we-there-yet": {
+ "version": "1.1.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "delegates": "^1.0.0",
+ "readable-stream": "^2.0.6"
+ }
+ },
+ "balanced-match": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "bundled": true,
+ "dev": true,
+ "requires": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "chownr": {
+ "version": "1.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "code-point-at": {
+ "version": "1.1.0",
+ "bundled": true,
+ "dev": true
+ },
+ "concat-map": {
+ "version": "0.0.1",
+ "bundled": true,
+ "dev": true
+ },
+ "console-control-strings": {
+ "version": "1.1.0",
+ "bundled": true,
+ "dev": true
+ },
+ "core-util-is": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "debug": {
+ "version": "4.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "ms": "^2.1.1"
+ }
+ },
+ "deep-extend": {
+ "version": "0.6.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "delegates": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "detect-libc": {
+ "version": "1.0.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "fs-minipass": {
+ "version": "1.2.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "minipass": "^2.2.1"
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "gauge": {
+ "version": "2.7.4",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "aproba": "^1.0.3",
+ "console-control-strings": "^1.0.0",
+ "has-unicode": "^2.0.0",
+ "object-assign": "^4.1.0",
+ "signal-exit": "^3.0.0",
+ "string-width": "^1.0.1",
+ "strip-ansi": "^3.0.1",
+ "wide-align": "^1.1.0"
+ }
+ },
+ "glob": {
+ "version": "7.1.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "fs.realpath": "^1.0.0",
+ "inflight": "^1.0.4",
+ "inherits": "2",
+ "minimatch": "^3.0.4",
+ "once": "^1.3.0",
+ "path-is-absolute": "^1.0.0"
+ }
+ },
+ "has-unicode": {
+ "version": "2.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "iconv-lite": {
+ "version": "0.4.24",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "safer-buffer": ">= 2.1.2 < 3"
+ }
+ },
+ "ignore-walk": {
+ "version": "3.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "minimatch": "^3.0.4"
+ }
+ },
+ "inflight": {
+ "version": "1.0.6",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.3",
+ "bundled": true,
+ "dev": true
+ },
+ "ini": {
+ "version": "1.3.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "is-fullwidth-code-point": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "requires": {
+ "number-is-nan": "^1.0.0"
+ }
+ },
+ "isarray": {
+ "version": "1.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "minimatch": {
+ "version": "3.0.4",
+ "bundled": true,
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
}
- }
- }
- },
- "chokidar": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.0.4.tgz",
- "integrity": "sha512-z9n7yt9rOvIJrMhvDtDictKrkFHeihkNl6uWMmZlmL6tJtX9Cs+87oK+teBx+JIgzvbX3yZHT3eF8vpbDxHJXQ==",
- "dev": true,
- "requires": {
- "anymatch": "^2.0.0",
- "async-each": "^1.0.0",
- "braces": "^2.3.0",
- "fsevents": "^1.2.2",
- "glob-parent": "^3.1.0",
- "inherits": "^2.0.1",
- "is-binary-path": "^1.0.0",
- "is-glob": "^4.0.0",
- "lodash.debounce": "^4.0.8",
- "normalize-path": "^2.1.1",
- "path-is-absolute": "^1.0.0",
- "readdirp": "^2.0.0",
- "upath": "^1.0.5"
- }
- },
- "expand-brackets": {
- "version": "2.1.4",
- "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz",
- "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=",
- "dev": true,
- "requires": {
- "debug": "^2.3.3",
- "define-property": "^0.2.5",
- "extend-shallow": "^2.0.1",
- "posix-character-classes": "^0.1.0",
- "regex-not": "^1.0.0",
- "snapdragon": "^0.8.1",
- "to-regex": "^3.0.1"
- },
- "dependencies": {
- "define-property": {
- "version": "0.2.5",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz",
- "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=",
+ },
+ "minimist": {
+ "version": "0.0.8",
+ "bundled": true,
+ "dev": true
+ },
+ "minipass": {
+ "version": "2.3.5",
+ "bundled": true,
"dev": true,
"requires": {
- "is-descriptor": "^0.1.0"
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.0"
+ }
+ },
+ "minizlib": {
+ "version": "1.2.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "minipass": "^2.2.1"
+ }
+ },
+ "mkdirp": {
+ "version": "0.5.1",
+ "bundled": true,
+ "dev": true,
+ "requires": {
+ "minimist": "0.0.8"
+ }
+ },
+ "ms": {
+ "version": "2.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "needle": {
+ "version": "2.3.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "debug": "^4.1.0",
+ "iconv-lite": "^0.4.4",
+ "sax": "^1.2.4"
+ }
+ },
+ "node-pre-gyp": {
+ "version": "0.12.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "detect-libc": "^1.0.2",
+ "mkdirp": "^0.5.1",
+ "needle": "^2.2.1",
+ "nopt": "^4.0.1",
+ "npm-packlist": "^1.1.6",
+ "npmlog": "^4.0.2",
+ "rc": "^1.2.7",
+ "rimraf": "^2.6.1",
+ "semver": "^5.3.0",
+ "tar": "^4"
+ }
+ },
+ "nopt": {
+ "version": "4.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "abbrev": "1",
+ "osenv": "^0.1.4"
+ }
+ },
+ "npm-bundled": {
+ "version": "1.0.6",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "npm-packlist": {
+ "version": "1.4.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "ignore-walk": "^3.0.1",
+ "npm-bundled": "^1.0.1"
+ }
+ },
+ "npmlog": {
+ "version": "4.1.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "are-we-there-yet": "~1.1.2",
+ "console-control-strings": "~1.1.0",
+ "gauge": "~2.7.3",
+ "set-blocking": "~2.0.0"
+ }
+ },
+ "number-is-nan": {
+ "version": "1.0.1",
+ "bundled": true,
+ "dev": true
+ },
+ "object-assign": {
+ "version": "4.1.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "bundled": true,
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "os-homedir": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "os-tmpdir": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "osenv": {
+ "version": "0.1.5",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "os-homedir": "^1.0.0",
+ "os-tmpdir": "^1.0.0"
+ }
+ },
+ "path-is-absolute": {
+ "version": "1.0.1",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "process-nextick-args": {
+ "version": "2.0.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "rc": {
+ "version": "1.2.8",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "deep-extend": "^0.6.0",
+ "ini": "~1.3.0",
+ "minimist": "^1.2.0",
+ "strip-json-comments": "~2.0.1"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "1.2.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ }
+ }
+ },
+ "readable-stream": {
+ "version": "2.3.6",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "rimraf": {
+ "version": "2.6.3",
+ "bundled": true,
+ "dev": true,
+ "optional": true,
+ "requires": {
+ "glob": "^7.1.3"
}
},
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "safe-buffer": {
+ "version": "5.1.2",
+ "bundled": true,
+ "dev": true
+ },
+ "safer-buffer": {
+ "version": "2.1.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "sax": {
+ "version": "1.2.4",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "semver": {
+ "version": "5.7.0",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "set-blocking": {
+ "version": "2.0.0",
+ "bundled": true,
"dev": true,
- "requires": {
- "is-extendable": "^0.1.0"
- }
+ "optional": true
},
- "is-accessor-descriptor": {
- "version": "0.1.6",
- "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz",
- "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=",
+ "signal-exit": {
+ "version": "3.0.2",
+ "bundled": true,
"dev": true,
- "requires": {
- "kind-of": "^3.0.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "^1.1.5"
- }
- }
- }
+ "optional": true
},
- "is-data-descriptor": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz",
- "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=",
+ "string-width": {
+ "version": "1.0.2",
+ "bundled": true,
"dev": true,
"requires": {
- "kind-of": "^3.0.2"
- },
- "dependencies": {
- "kind-of": {
- "version": "3.2.2",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
- "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
- "dev": true,
- "requires": {
- "is-buffer": "^1.1.5"
- }
- }
+ "code-point-at": "^1.0.0",
+ "is-fullwidth-code-point": "^1.0.0",
+ "strip-ansi": "^3.0.0"
}
},
- "is-descriptor": {
- "version": "0.1.6",
- "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz",
- "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==",
+ "string_decoder": {
+ "version": "1.1.1",
+ "bundled": true,
"dev": true,
+ "optional": true,
"requires": {
- "is-accessor-descriptor": "^0.1.6",
- "is-data-descriptor": "^0.1.4",
- "kind-of": "^5.0.0"
+ "safe-buffer": "~5.1.0"
}
},
- "kind-of": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz",
- "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==",
- "dev": true
- }
- }
- },
- "extglob": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz",
- "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==",
- "dev": true,
- "requires": {
- "array-unique": "^0.3.2",
- "define-property": "^1.0.0",
- "expand-brackets": "^2.1.4",
- "extend-shallow": "^2.0.1",
- "fragment-cache": "^0.2.1",
- "regex-not": "^1.0.0",
- "snapdragon": "^0.8.1",
- "to-regex": "^3.0.1"
- },
- "dependencies": {
- "define-property": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz",
- "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=",
+ "strip-ansi": {
+ "version": "3.0.1",
+ "bundled": true,
"dev": true,
"requires": {
- "is-descriptor": "^1.0.0"
+ "ansi-regex": "^2.0.0"
}
},
- "extend-shallow": {
+ "strip-json-comments": {
"version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "tar": {
+ "version": "4.4.8",
+ "bundled": true,
"dev": true,
+ "optional": true,
"requires": {
- "is-extendable": "^0.1.0"
+ "chownr": "^1.1.1",
+ "fs-minipass": "^1.2.5",
+ "minipass": "^2.3.4",
+ "minizlib": "^1.1.1",
+ "mkdirp": "^0.5.0",
+ "safe-buffer": "^5.1.2",
+ "yallist": "^3.0.2"
}
- }
- }
- },
- "fill-range": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz",
- "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=",
- "dev": true,
- "requires": {
- "extend-shallow": "^2.0.1",
- "is-number": "^3.0.0",
- "repeat-string": "^1.6.1",
- "to-regex-range": "^2.1.0"
- },
- "dependencies": {
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
+ },
+ "util-deprecate": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true,
+ "optional": true
+ },
+ "wide-align": {
+ "version": "1.1.3",
+ "bundled": true,
"dev": true,
+ "optional": true,
"requires": {
- "is-extendable": "^0.1.0"
+ "string-width": "^1.0.2 || 2"
}
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "bundled": true,
+ "dev": true
+ },
+ "yallist": {
+ "version": "3.0.3",
+ "bundled": true,
+ "dev": true
}
}
},
@@ -5711,9 +10909,9 @@
"dev": true
},
"is-glob": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.0.tgz",
- "integrity": "sha1-lSHHaEXMJhCoUgPd8ICpWML/q8A=",
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz",
+ "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==",
"dev": true,
"requires": {
"is-extglob": "^2.1.1"
@@ -5771,6 +10969,30 @@
"snapdragon": "^0.8.1",
"to-regex": "^3.0.2"
}
+ },
+ "nan": {
+ "version": "2.14.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
+ "integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==",
+ "dev": true,
+ "optional": true
+ },
+ "readdirp": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz",
+ "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==",
+ "dev": true,
+ "requires": {
+ "graceful-fs": "^4.1.11",
+ "micromatch": "^3.1.10",
+ "readable-stream": "^2.0.2"
+ }
+ },
+ "upath": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/upath/-/upath-1.1.2.tgz",
+ "integrity": "sha512-kXpym8nmDmlCBr7nKdIx8P2jNBa+pBpIUFRnKJ4dr8htyYGJFokkr2ZvERRtUN+9SY+JqXouNgUPtv6JQva/2Q==",
+ "dev": true
}
}
},
@@ -5824,21 +11046,21 @@
"integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg="
},
"gulp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.0.tgz",
- "integrity": "sha1-lXZsYB2t5Kd+0+eyttwDiBtZY2Y=",
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz",
+ "integrity": "sha512-dvEs27SCZt2ibF29xYgmnwwCYZxdxhQ/+LFWlbAW8y7jt68L/65402Lz3+CKy0Ov4rOs+NERmDq7YlZaDqUIfA==",
"dev": true,
"requires": {
- "glob-watcher": "^5.0.0",
- "gulp-cli": "^2.0.0",
- "undertaker": "^1.0.0",
+ "glob-watcher": "^5.0.3",
+ "gulp-cli": "^2.2.0",
+ "undertaker": "^1.2.1",
"vinyl-fs": "^3.0.0"
},
"dependencies": {
"gulp-cli": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.0.1.tgz",
- "integrity": "sha512-RxujJJdN8/O6IW2nPugl7YazhmrIEjmiVfPKrWt68r71UCaLKS71Hp0gpKT+F6qOUFtr7KqtifDKaAJPRVvMYQ==",
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.2.0.tgz",
+ "integrity": "sha512-rGs3bVYHdyJpLqR0TUBnlcZ1O5O++Zs4bA0ajm+zr3WFCfiSLjGwoCBqFs18wzN+ZxahT9DkOK5nDf26iDsWjA==",
"dev": true,
"requires": {
"ansi-colors": "^1.0.1",
@@ -5851,7 +11073,7 @@
"gulplog": "^1.0.0",
"interpret": "^1.1.0",
"isobject": "^3.0.1",
- "liftoff": "^2.5.0",
+ "liftoff": "^3.1.0",
"matchdep": "^2.0.0",
"mute-stdout": "^1.0.0",
"pretty-hrtime": "^1.0.0",
@@ -6232,9 +11454,9 @@
}
},
"homedir-polyfill": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz",
- "integrity": "sha1-TCu8inWJmP7r9e1oWA921GdotLw=",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz",
+ "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==",
"dev": true,
"requires": {
"parse-passwd": "^1.0.0"
@@ -6725,14 +11947,11 @@
"integrity": "sha1-NJptRMU6Ud6JtAgFxdXlm0F9M0A=",
"dev": true
},
- "json-stable-stringify": {
+ "json-stable-stringify-without-jsonify": {
"version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz",
- "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=",
- "dev": true,
- "requires": {
- "jsonify": "~0.0.0"
- }
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=",
+ "dev": true
},
"json-stringify-safe": {
"version": "5.0.1",
@@ -6758,12 +11977,6 @@
"graceful-fs": "^4.1.6"
}
},
- "jsonify": {
- "version": "0.0.0",
- "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz",
- "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=",
- "dev": true
- },
"jsprim": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz",
@@ -6893,13 +12106,13 @@
}
},
"liftoff": {
- "version": "2.5.0",
- "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-2.5.0.tgz",
- "integrity": "sha1-IAkpG7Mc6oYbvxCnwVooyvdcMew=",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz",
+ "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==",
"dev": true,
"requires": {
"extend": "^3.0.0",
- "findup-sync": "^2.0.0",
+ "findup-sync": "^3.0.0",
"fined": "^1.0.1",
"flagged-respawn": "^1.0.0",
"is-plain-object": "^2.0.4",
@@ -6963,9 +12176,9 @@
}
},
"lodash": {
- "version": "4.17.11",
- "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz",
- "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==",
+ "version": "4.17.15",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
+ "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==",
"dev": true
},
"lodash._basecopy": {
@@ -7034,12 +12247,6 @@
"integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=",
"dev": true
},
- "lodash.debounce": {
- "version": "4.0.8",
- "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
- "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=",
- "dev": true
- },
"lodash.escape": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-3.2.0.tgz",
@@ -7073,9 +12280,9 @@
}
},
"lodash.mergewith": {
- "version": "4.6.1",
- "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.1.tgz",
- "integrity": "sha512-eWw5r+PYICtEBgrBE5hhlT6aAa75f411bgDz/ZL2KZqYV03USvucsxcHUIlGTDTECs1eunpI7HOV7U+WLDvNdQ==",
+ "version": "4.6.2",
+ "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz",
+ "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==",
"dev": true
},
"lodash.restparam": {
@@ -7403,6 +12610,18 @@
}
}
},
+ "findup-sync": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz",
+ "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=",
+ "dev": true,
+ "requires": {
+ "detect-file": "^1.0.0",
+ "is-glob": "^3.1.0",
+ "micromatch": "^3.0.4",
+ "resolve-dir": "^1.0.1"
+ }
+ },
"is-accessor-descriptor": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz",
@@ -7432,6 +12651,21 @@
"kind-of": "^6.0.2"
}
},
+ "is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=",
+ "dev": true
+ },
+ "is-glob": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz",
+ "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=",
+ "dev": true,
+ "requires": {
+ "is-extglob": "^2.1.0"
+ }
+ },
"is-number": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
@@ -7641,9 +12875,9 @@
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
},
"mixin-deep": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.1.tgz",
- "integrity": "sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz",
+ "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==",
"dev": true,
"requires": {
"for-in": "^1.0.2",
@@ -7705,7 +12939,8 @@
"nan": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.10.0.tgz",
- "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA=="
+ "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==",
+ "dev": true
},
"nanomatch": {
"version": "1.2.9",
@@ -8086,9 +13321,9 @@
"dev": true
},
"now-and-later": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.0.tgz",
- "integrity": "sha1-vGHLtFbXnLMiB85HygUTb/Ln1u4=",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/now-and-later/-/now-and-later-2.0.1.tgz",
+ "integrity": "sha512-KGvQ0cB70AQfg107Xvs/Fbu+dGmZoTRJp2TaPwcwQm3/7PteUyN2BCgk8KBMPGBUXZdVwyWS8fDCGFygBm19UQ==",
"dev": true,
"requires": {
"once": "^1.3.2"
@@ -9701,9 +14936,9 @@
"integrity": "sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E="
},
"set-value": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.0.tgz",
- "integrity": "sha512-hw0yxk9GT/Hr5yJEYnHNKYXkIA8mVJgd9ditYZCe16ZczcaELYYcfvaXesNACk2O8O0nTiPQcQhGUQj8JLzeeg==",
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz",
+ "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==",
"dev": true,
"requires": {
"extend-shallow": "^2.0.1",
@@ -10257,9 +15492,9 @@
}
},
"through2-filter": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-2.0.0.tgz",
- "integrity": "sha1-YLxVoNrLdghdsfna6Zq0P4PWIuw=",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/through2-filter/-/through2-filter-3.0.0.tgz",
+ "integrity": "sha512-jaRjI2WxN3W1V8/FMZ9HKIBXixtiqs3SQSX4/YGIiP3gL6djW48VoZq9tDqeCWs3MT8YY5wb/zli8VW8snY1CA==",
"dev": true,
"requires": {
"through2": "~2.0.0",
@@ -10623,9 +15858,9 @@
"dev": true
},
"undertaker": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.2.0.tgz",
- "integrity": "sha1-M52kZGJS0ILcN45wgGcpl1DhG0k=",
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/undertaker/-/undertaker-1.2.1.tgz",
+ "integrity": "sha512-71WxIzDkgYk9ZS+spIB8iZXchFhAdEo2YU8xYqBYJ39DIUIqziK78ftm26eecoIY49X0J2MLhG4hr18Yp6/CMA==",
"dev": true,
"requires": {
"arr-flatten": "^1.0.1",
@@ -10646,48 +15881,25 @@
"dev": true
},
"union-value": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.0.tgz",
- "integrity": "sha1-XHHDTLW61dzr4+oM0IIHulqhrqQ=",
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz",
+ "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==",
"dev": true,
"requires": {
"arr-union": "^3.1.0",
"get-value": "^2.0.6",
"is-extendable": "^0.1.1",
- "set-value": "^0.4.3"
- },
- "dependencies": {
- "extend-shallow": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
- "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=",
- "dev": true,
- "requires": {
- "is-extendable": "^0.1.0"
- }
- },
- "set-value": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/set-value/-/set-value-0.4.3.tgz",
- "integrity": "sha1-fbCPnT0i3H945Trzw79GZuzfzPE=",
- "dev": true,
- "requires": {
- "extend-shallow": "^2.0.1",
- "is-extendable": "^0.1.1",
- "is-plain-object": "^2.0.1",
- "to-object-path": "^0.3.0"
- }
- }
+ "set-value": "^2.0.1"
}
},
"unique-stream": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.2.1.tgz",
- "integrity": "sha1-WqADz76Uxf+GbE59ZouxxNuts2k=",
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/unique-stream/-/unique-stream-2.3.1.tgz",
+ "integrity": "sha512-2nY4TnBE70yoxHkDli7DMazpWiP7xMdCYqU2nBRO0UB+ZpEkGsSija7MvmvnZFUeC+mrgiUfcHSr3LmRFIg4+A==",
"dev": true,
"requires": {
- "json-stable-stringify": "^1.0.0",
- "through2-filter": "^2.0.0"
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "through2-filter": "^3.0.0"
}
},
"universalify": {
@@ -10845,9 +16057,9 @@
"dev": true
},
"v8flags": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.1.tgz",
- "integrity": "sha512-iw/1ViSEaff8NJ3HLyEjawk/8hjJib3E7pvG4pddVXfUg1983s3VGsiClDjhK64MQVDGqc1Q8r18S4VKQZS9EQ==",
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz",
+ "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==",
"dev": true,
"requires": {
"homedir-polyfill": "^1.0.1"
diff --git a/package.json b/package.json
index 91b804947a0..8d5fda2e1ac 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "moodlemobile",
- "version": "3.7.0",
+ "version": "3.7.1",
"description": "The official app for Moodle.",
"author": {
"name": "Moodle Pty Ltd.",
@@ -23,21 +23,21 @@
}
],
"scripts": {
- "setup": "npm install && cordova prepare && gulp",
- "clean": "ionic-app-scripts clean",
- "build": "ionic-app-scripts build",
- "lint": "ionic-app-scripts lint",
+ "setup": "npm install && npx cordova prepare && npx gulp",
+ "clean": "npx ionic-app-scripts clean",
+ "build": "npx ionic-app-scripts build",
+ "lint": "npx ionic-app-scripts lint",
"ionic:build": "node --max-old-space-size=16384 ./node_modules/@ionic/app-scripts/bin/ionic-app-scripts.js build",
- "ionic:serve:before": "gulp",
- "ionic:serve": "gulp watch | ionic-app-scripts serve",
- "ionic:build:before": "gulp",
- "ionic:watch:before": "gulp",
- "ionic:build:after": "gulp copy-component-templates",
- "preionic:build": "gulp",
- "postionic:build": "gulp copy-component-templates",
- "desktop.pack": "electron-builder --dir",
- "desktop.dist": "electron-builder -p never",
- "windows.store": "electron-windows-store --input-directory .\\desktop\\dist\\win-unpacked --output-directory .\\desktop\\store --flatten true -a .\\resources\\desktop -m .\\desktop\\assets\\windows\\AppXManifest.xml --package-version 0.0.0.0 --package-name MoodleDesktop"
+ "ionic:serve:before": "npx gulp",
+ "ionic:serve": "npx gulp watch & npx ionic-app-scripts serve -b --devapp --address=0.0.0.0",
+ "ionic:build:before": "npx gulp",
+ "ionic:watch:before": "npx gulp",
+ "ionic:build:after": "npx gulp copy-component-templates",
+ "preionic:build": "npx gulp",
+ "postionic:build": "npx gulp copy-component-templates",
+ "desktop.pack": "npx electron-builder --dir",
+ "desktop.dist": "npx electron-builder -p never",
+ "windows.store": "npx electron-windows-store --input-directory .\\desktop\\dist\\win-unpacked --output-directory .\\desktop\\store -a .\\resources\\desktop -m .\\desktop\\assets\\windows\\AppXManifest.xml --package-version 0.0.0.0 --package-name MoodleDesktop"
},
"dependencies": {
"@angular/animations": "5.2.10",
@@ -80,27 +80,28 @@
"@types/promise.prototype.finally": "2.0.2",
"chart.js": "2.7.2",
"com-darryncampbell-cordova-plugin-intent": "1.1.7",
+ "cordova": "8.1.2",
"cordova-android": "7.1.2",
- "cordova-android-support-gradle-release": "3.0.0",
- "cordova-clipboard": "1.2.1",
+ "cordova-android-support-gradle-release": "3.0.1",
+ "cordova-clipboard": "1.3.0",
"cordova-ios": "4.5.5",
"cordova-plugin-badge": "0.8.8",
- "cordova-plugin-camera": "4.0.3",
- "cordova-plugin-customurlscheme": "4.3.0",
- "cordova-plugin-device": "2.0.2",
- "cordova-plugin-file": "6.0.1",
+ "cordova-plugin-camera": "4.1.0",
+ "cordova-plugin-customurlscheme": "4.4.0",
+ "cordova-plugin-device": "2.0.3",
+ "cordova-plugin-file": "6.0.2",
"cordova-plugin-file-opener2": "2.0.19",
"cordova-plugin-file-transfer": "1.7.1",
"cordova-plugin-globalization": "1.11.0",
- "cordova-plugin-inappbrowser": "3.0.0",
+ "cordova-plugin-inappbrowser": "3.1.0",
"cordova-plugin-ionic-keyboard": "2.1.3",
"cordova-plugin-local-notification": "git+https://github.com/moodlemobile/cordova-plugin-local-notification.git#moodle",
- "cordova-plugin-media-capture": "3.0.2",
- "cordova-plugin-network-information": "2.0.1",
- "cordova-plugin-screen-orientation": "3.0.1",
- "cordova-plugin-splashscreen": "5.0.2",
- "cordova-plugin-statusbar": "2.4.2",
- "cordova-plugin-whitelist": "1.3.3",
+ "cordova-plugin-media-capture": "3.0.3",
+ "cordova-plugin-network-information": "2.0.2",
+ "cordova-plugin-screen-orientation": "3.0.2",
+ "cordova-plugin-splashscreen": "5.0.3",
+ "cordova-plugin-statusbar": "2.4.3",
+ "cordova-plugin-whitelist": "1.3.4",
"cordova-plugin-zip": "3.1.0",
"cordova-sqlite-storage": "2.6.0",
"cordova-support-google-services": "1.2.1",
@@ -124,7 +125,7 @@
"@ionic/app-scripts": "3.2.2",
"electron-builder-lib": "20.23.1",
"electron-rebuild": "1.8.1",
- "gulp": "4.0.0",
+ "gulp": "4.0.2",
"gulp-clip-empty-files": "0.1.2",
"gulp-concat": "2.6.1",
"gulp-flatten": "0.4.0",
@@ -210,11 +211,12 @@
}
],
"compression": "maximum",
- "electronVersion": "4.0.1",
+ "electronVersion": "4.2.5",
"mac": {
"category": "public.app-category.education",
"icon": "resources/desktop/icon.icns",
"target": "mas",
+ "bundleVersion": "3.7.0",
"extendInfo": {
"ElectronTeamID": "2NU57U5PAW"
}
diff --git a/scripts/aot.sh b/scripts/aot.sh
index 5fa706e6472..931d8e4b56b 100755
--- a/scripts/aot.sh
+++ b/scripts/aot.sh
@@ -1,5 +1,8 @@
#!/bin/bash
+# List first level of installed libraries so we can check the installed versions.
+npm list --depth=0
+
# Compile AOT.
if [ $TRAVIS_BRANCH == 'integration' ] || [ $TRAVIS_BRANCH == 'master' ] || [ $TRAVIS_BRANCH == 'desktop' ] || [ -z $TRAVIS_BRANCH ] ; then
cd scripts
@@ -40,17 +43,34 @@ if [ ! -z $GIT_ORG ] && [ ! -z $GIT_TOKEN ] ; then
gitfolder=${PWD##*/}
git clone --depth 1 --no-single-branch https://github.com/$GIT_ORG/moodlemobile-phonegapbuild.git ../pgb
pushd ../pgb
+
+ mkdir /tmp/travistemp
+ cp .travis.yml /tmp/travistemp
+ mkdir /tmp/travistemp/scripts
+ cp scripts/* /tmp/travistemp/scripts
+
git checkout $TRAVIS_BRANCH
- rm -Rf assets build index.html templates
- cp -Rf ../$gitfolder/www/* ./
- rm -Rf assets/countries assets/mimetypes
+
+ rm -Rf assets build index.html templates www destkop
+
+ if [ $TRAVIS_BRANCH == 'desktop' ] ; then
+ cp -Rf ../$gitfolder/desktop ./
+ cp -Rf ../$gitfolder/package.json ./
+ cp -Rf ../$gitfolder/www ./
+ rm -Rf www/assets/countries www/assets/mimetypes
+ else
+ cp -Rf ../$gitfolder/www/* ./
+ rm -Rf assets/countries assets/mimetypes
+ fi
+
+ cp /tmp/travistemp/.travis.yml .travis.yml
+ mkdir scripts
+ cp /tmp/travistemp/scripts/* scripts
+
+
git add .
git commit -m "Travis build: $TRAVIS_BUILD_NUMBER"
git push https://$GIT_TOKEN@github.com/$GIT_ORG/moodlemobile-phonegapbuild.git
popd
fi
-if [ ! -z $GIT_ORG_PRIVATE ] && [ ! -z $GIT_TOKEN ] && [ $TRAVIS_BRANCH == 'desktop' ] && [ $TRAVIS_OS_NAME == 'linux' ]; then
- ./scripts/linux.sh
-fi
-
diff --git a/scripts/lang_functions.php b/scripts/lang_functions.php
new file mode 100644
index 00000000000..af1e2ab3b31
--- /dev/null
+++ b/scripts/lang_functions.php
@@ -0,0 +1,426 @@
+.
+
+/**
+ * Helper functions converting moodle strings to json.
+ */
+
+function detect_languages($languages, $keys) {
+ echo "\n\n\n";
+
+ $all_languages = glob(LANGPACKSFOLDER.'/*' , GLOB_ONLYDIR);
+ function get_lang_from_dir($dir) {
+ return str_replace('_', '-', explode('/', $dir)[3]);
+ }
+ function get_lang_not_wp($langname) {
+ return (substr($langname, -3) !== '-wp');
+ }
+ $all_languages = array_map('get_lang_from_dir', $all_languages);
+ $all_languages = array_filter($all_languages, 'get_lang_not_wp');
+
+ $detect_lang = array_diff($all_languages, $languages);
+ $new_langs = array();
+ foreach ($detect_lang as $lang) {
+ reset_translations_strings();
+ $new = detect_lang($lang, $keys);
+ if ($new) {
+ $new_langs[$lang] = $lang;
+ }
+ }
+
+ return $new_langs;
+}
+
+function build_languages($languages, $keys, $added_langs = []) {
+ // Process the languages.
+ foreach ($languages as $lang) {
+ reset_translations_strings();
+ $ok = build_lang($lang, $keys);
+ if ($ok) {
+ $added_langs[$lang] = $lang;
+ }
+ }
+
+ return $added_langs;
+}
+
+function get_langindex_keys() {
+ // Process the index file, just once.
+ $keys = file_get_contents('langindex.json');
+ $keys = (array) json_decode($keys);
+
+ foreach ($keys as $key => $value) {
+ $map = new StdClass();
+ if ($value == 'local_moodlemobileapp') {
+ $map->file = $value;
+ $map->string = $key;
+ } else {
+ $exp = explode('/', $value, 2);
+ $map->file = $exp[0];
+ if (count($exp) == 2) {
+ $map->string = $exp[1];
+ } else {
+ $exp = explode('.', $key, 3);
+
+ if (count($exp) == 3) {
+ $map->string = $exp[2];
+ } else {
+ $map->string = $exp[1];
+ }
+ }
+ }
+
+ $keys[$key] = $map;
+ }
+
+ $total = count($keys);
+ echo "Total strings to translate $total\n";
+
+ return $keys;
+}
+
+function add_langs_to_config($langs, $config) {
+ $changed = false;
+ $config_langs = get_object_vars($config['languages']);
+ foreach ($langs as $lang) {
+ if (!isset($config_langs[$lang])) {
+ $langfoldername = str_replace('-', '_', $lang);
+
+ $string = [];
+ include(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php');
+ $config['languages']->$lang = $string['thislanguage'];
+ $changed = true;
+ }
+ }
+
+ if ($changed) {
+ // Sort languages by key.
+ $config['languages'] = json_decode( json_encode( $config['languages'] ), true );
+ ksort($config['languages']);
+ $config['languages'] = json_decode( json_encode( $config['languages'] ), false );
+ file_put_contents(CONFIG, json_encode($config, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
+ }
+}
+
+function get_langfolder($lang) {
+ $folder = LANGPACKSFOLDER.'/'.str_replace('-', '_', $lang);
+ if (!is_dir($folder) || !is_file($folder.'/langconfig.php')) {
+ echo "Cannot translate $folder, folder not found";
+
+ return false;
+ }
+
+ return $folder;
+}
+
+function get_translation_strings($langfoldername, $file, $override_folder = false) {
+ global $strings;
+
+ if (isset($strings[$file])) {
+ return $strings[$file];
+ }
+
+ $string = import_translation_strings($langfoldername, $file);
+ $string_override = $override_folder ? import_translation_strings($override_folder, $file) : false;
+
+ if ($string) {
+ $strings[$file] = $string;
+ if ($string_override) {
+ $strings[$file] = array_merge($strings[$file], $string_override);
+ }
+ } else if ($string_override) {
+ $strings[$file] = $string_override;
+ } else {
+ $strings[$file] = false;
+ }
+
+ return $strings[$file];
+}
+
+function import_translation_strings($langfoldername, $file) {
+ $path = $langfoldername.'/'.$file.'.php';
+ // Apply translations.
+ if (!file_exists($path)) {
+ return false;
+ }
+
+ $string = [];
+ include($path);
+
+ return $string;
+}
+
+function reset_translations_strings() {
+ global $strings;
+ $strings = [];
+}
+
+function build_lang($lang, $keys) {
+ $langfoldername = get_langfolder($lang);
+ if (!$langfoldername) {
+ return false;
+ }
+
+ if (OVERRIDE_LANG_SUFIX) {
+ $override_langfolder = get_langfolder($lang.OVERRIDE_LANG_SUFIX);
+ } else {
+ $override_langfolder = false;
+ }
+
+ $total = count ($keys);
+ $local = 0;
+
+ $string = get_translation_strings($langfoldername, 'langconfig');
+ $parent = isset($string['parentlanguage']) ? $string['parentlanguage'] : "";
+
+ echo "Processing $lang";
+ if ($parent != "" && $parent != $lang) {
+ echo " ($parent)";
+ }
+
+ $langFile = false;
+ // Not yet translated. Do not override.
+ if (file_exists(ASSETSPATH.$lang.'.json')) {
+ // Load lang files just once.
+ $langFile = file_get_contents(ASSETSPATH.$lang.'.json');
+ $langFile = (array) json_decode($langFile);
+ }
+
+ $translations = [];
+ // Add the translation to the array.
+ foreach ($keys as $key => $value) {
+ $string = get_translation_strings($langfoldername, $value->file, $override_langfolder);
+ // Apply translations.
+ if (!$string) {
+ if (TOTRANSLATE) {
+ echo "\n\t\To translate $value->string on $value->file";
+ }
+ continue;
+ }
+
+ if (!isset($string[$value->string]) || ($lang == 'en' && $value->file == 'local_moodlemobileapp')) {
+ // Not yet translated. Do not override.
+ if ($langFile && is_array($langFile) && isset($langFile[$key])) {
+ $translations[$key] = $langFile[$key];
+ $local++;
+ }
+ if (TOTRANSLATE) {
+ echo "\n\t\tTo translate $value->string on $value->file";
+ }
+ continue;
+ } else {
+ $text = $string[$value->string];
+ }
+
+ if ($value->file != 'local_moodlemobileapp') {
+ $text = str_replace('$a->', '$a.', $text);
+ $text = str_replace('{$a', '{{$a', $text);
+ $text = str_replace('}', '}}', $text);
+ // Prevent double.
+ $text = str_replace(array('{{{', '}}}'), array('{{', '}}'), $text);
+ } else {
+ $local++;
+ }
+
+ $translations[$key] = html_entity_decode($text);
+ }
+
+ // Sort and save.
+ ksort($translations);
+ file_put_contents(ASSETSPATH.$lang.'.json', str_replace('\/', '/', json_encode($translations, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)));
+
+ $success = count($translations);
+ $percentage = floor($success/$total * 100);
+ echo "\t\t$success of $total -> $percentage% ($local local)\n";
+
+ if ($lang == 'en') {
+ generate_local_moodlemobileapp($keys, $translations);
+ override_component_lang_files($keys, $translations);
+ }
+
+ return true;
+}
+
+function detect_lang($lang, $keys) {
+ $langfoldername = get_langfolder($lang);
+ if (!$langfoldername) {
+ return false;
+ }
+
+ $total = count ($keys);
+ $success = 0;
+ $local = 0;
+
+ $string = get_translation_strings($langfoldername, 'langconfig');
+ $parent = isset($string['parentlanguage']) ? $string['parentlanguage'] : "";
+ if (!isset($string['thislanguage'])) {
+ echo "Cannot translate $lang, translated name not found";
+ return false;
+ }
+
+ echo "Checking $lang";
+ if ($parent != "" && $parent != $lang) {
+ echo " ($parent)";
+ }
+ $langname = $string['thislanguage'];
+ echo " ".$langname." -D";
+
+ // Add the translation to the array.
+ foreach ($keys as $key => $value) {
+ $string = get_translation_strings($langfoldername, $value->file);
+ // Apply translations.
+ if (!$string) {
+ continue;
+ }
+
+ if (!isset($string[$value->string])) {
+ continue;
+ } else {
+ $text = $string[$value->string];
+ }
+
+ if ($value->file == 'local_moodlemobileapp') {
+ $local++;
+ }
+
+ $success++;
+ }
+
+ $percentage = floor($success/$total * 100);
+ echo "\t\t$success of $total -> $percentage% ($local local)";
+ if (($percentage > 75 && $local > 50) || ($percentage > 50 && $local > 75)) {
+ echo " \t DETECTED\n";
+ return true;
+ }
+ echo "\n";
+
+ return false;
+}
+
+function save_key($key, $value, $path) {
+ $filePath = $path . '/en.json';
+
+ $file = file_get_contents($filePath);
+ $file = (array) json_decode($file);
+ $value = html_entity_decode($value);
+ if (!isset($file[$key]) || $file[$key] != $value) {
+ $file[$key] = $value;
+ ksort($file);
+ file_put_contents($filePath, str_replace('\/', '/', json_encode($file, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)));
+ }
+}
+
+function override_component_lang_files($keys, $translations) {
+ echo "Override component lang files.\n";
+ foreach ($translations as $key => $value) {
+ $path = '../src/';
+ $exp = explode('.', $key, 3);
+
+ $type = $exp[0];
+ if (count($exp) == 3) {
+ $component = $exp[1];
+ $plainid = $exp[2];
+ } else {
+ $component = 'moodle';
+ $plainid = $exp[1];
+ }
+ switch($type) {
+ case 'core':
+ case 'addon':
+ switch($component) {
+ case 'moodle':
+ $path .= 'lang';
+ break;
+ default:
+ $path .= $type.'/'.str_replace('_', '/', $component).'/lang';
+ break;
+ }
+ break;
+ case 'assets':
+ $path .= $type.'/'.$component;
+ break;
+
+ }
+
+ if (is_file($path.'/en.json')) {
+ save_key($plainid, $value, $path);
+ }
+ }
+}
+
+/**
+ * Generates local moodle mobile app file to update languages in AMOS.
+ *
+ * @param [array] $keys Translation keys.
+ * @param [array] $translations English translations.
+ */
+function generate_local_moodlemobileapp($keys, $translations) {
+ echo "Generate local_moodlemobileapp.\n";
+ $string = '.
+
+/**
+ * Version details.
+ *
+ * @package local
+ * @subpackage moodlemobileapp
+ * @copyright 2014 Juan Leyva
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+$string[\'appstoredescription\'] = \'NOTE: This official Moodle Mobile app will ONLY work with Moodle sites that have been set up to allow it. Please talk to your Moodle administrator if you have any problems connecting.
+
+If your Moodle site has been configured correctly, you can use this app to:
+
+- browse the content of your courses, even when offline
+- receive instant notifications of messages and other events
+- quickly find and contact other people in your courses
+- upload images, audio, videos and other files from your mobile device
+- view your course grades
+- and more!
+
+Please see http://docs.moodle.org/en/Mobile_app for all the latest information.
+
+We’d really appreciate any good reviews about the functionality so far, and your suggestions on what else you want this app to do!
+
+The app requires the following permissions:
+Record audio - For recording audio to upload to Moodle
+Read and modify the contents of your SD card - Contents are downloaded to the SD Card so you can see them offline
+Network access - To be able to connect with your Moodle site and check if you are connected or not to switch to offline mode
+Run at startup - So you receive local notifications even when the app is running in the background
+Prevent phone from sleeping - So you can receive push notifications anytime\';'."\n";
+ foreach ($keys as $key => $value) {
+ if (isset($translations[$key]) && $value->file == 'local_moodlemobileapp') {
+ $string .= '$string[\''.$key.'\'] = \''.str_replace("'", "\'", $translations[$key]).'\';'."\n";
+ }
+ }
+ $string .= '$string[\'pluginname\'] = \'Moodle Mobile language strings\';'."\n";
+
+ file_put_contents('../../moodle-local_moodlemobileapp/lang/en/local_moodlemobileapp.php', $string."\n");
+}
diff --git a/scripts/langindex.json b/scripts/langindex.json
index 7381c6fc8b7..fe54142bdcf 100644
--- a/scripts/langindex.json
+++ b/scripts/langindex.json
@@ -27,6 +27,17 @@
"addon.badges.version": "badges",
"addon.badges.warnexpired": "badges",
"addon.block_activitymodules.pluginname": "block_activity_modules",
+ "addon.block_badges.pluginname": "block_badges",
+ "addon.block_blogmenu.pluginname": "block_blog_menu",
+ "addon.block_blogrecent.nocourses": "block_blog_recent",
+ "addon.block_blogrecent.pluginname": "block_blog_recent",
+ "addon.block_blogtags.pluginname": "block_blog_tags",
+ "addon.block_calendarmonth.pluginname": "block_calendar_month",
+ "addon.block_calendarupcoming.pluginname": "block_calendar_upcoming",
+ "addon.block_comments.pluginname": "block_comments",
+ "addon.block_completionstatus.pluginname": "block_completionstatus",
+ "addon.block_glossaryrandom.pluginname": "block_glossary_random",
+ "addon.block_learningplans.pluginname": "block_lp",
"addon.block_myoverview.all": "block_myoverview",
"addon.block_myoverview.favourites": "block_myoverview",
"addon.block_myoverview.future": "block_myoverview",
@@ -38,13 +49,20 @@
"addon.block_myoverview.past": "block_myoverview",
"addon.block_myoverview.pluginname": "block_myoverview",
"addon.block_myoverview.title": "block_myoverview",
+ "addon.block_newsitems.pluginname": "block_news_items",
+ "addon.block_onlineusers.pluginname": "block_online_users",
+ "addon.block_privatefiles.pluginname": "block_private_files",
+ "addon.block_recentactivity.pluginname": "block_recent_activity",
"addon.block_recentlyaccessedcourses.nocourses": "block_recentlyaccessedcourses",
"addon.block_recentlyaccessedcourses.pluginname": "block_recentlyaccessedcourses",
"addon.block_recentlyaccesseditems.noitems": "block_recentlyaccesseditems",
"addon.block_recentlyaccesseditems.pluginname": "block_recentlyaccesseditems",
+ "addon.block_rssclient.pluginname": "block_rss_client",
+ "addon.block_selfcompletion.pluginname": "block_selfcompletion",
"addon.block_sitemainmenu.pluginname": "block_site_main_menu",
"addon.block_starredcourses.nocourses": "block_starredcourses",
"addon.block_starredcourses.pluginname": "block_starredcourses",
+ "addon.block_tags.pluginname": "block_tags",
"addon.block_timeline.duedate": "block_timeline",
"addon.block_timeline.next30days": "block_timeline",
"addon.block_timeline.next3months": "block_timeline",
@@ -66,18 +84,61 @@
"addon.blog.publishtoworld": "blog",
"addon.blog.showonlyyourentries": "local_moodlemobileapp",
"addon.blog.siteblogheading": "blog",
+ "addon.calendar.allday": "calendar",
"addon.calendar.calendar": "calendar",
+ "addon.calendar.calendarevent": "local_moodlemobileapp",
"addon.calendar.calendarevents": "local_moodlemobileapp",
"addon.calendar.calendarreminders": "local_moodlemobileapp",
+ "addon.calendar.confirmeventdelete": "calendar",
+ "addon.calendar.confirmeventseriesdelete": "calendar",
+ "addon.calendar.currentmonth": "local_moodlemobileapp",
+ "addon.calendar.daynext": "calendar",
+ "addon.calendar.dayprev": "calendar",
"addon.calendar.defaultnotificationtime": "local_moodlemobileapp",
+ "addon.calendar.deleteallevents": "calendar",
+ "addon.calendar.deleteevent": "calendar",
+ "addon.calendar.deleteoneevent": "calendar",
+ "addon.calendar.durationminutes": "calendar",
+ "addon.calendar.durationnone": "calendar",
+ "addon.calendar.durationuntil": "calendar",
+ "addon.calendar.editevent": "calendar",
"addon.calendar.errorloadevent": "local_moodlemobileapp",
"addon.calendar.errorloadevents": "local_moodlemobileapp",
+ "addon.calendar.eventcalendareventdeleted": "calendar",
+ "addon.calendar.eventduration": "calendar",
"addon.calendar.eventendtime": "calendar",
+ "addon.calendar.eventkind": "calendar",
+ "addon.calendar.eventname": "calendar",
"addon.calendar.eventstarttime": "calendar",
+ "addon.calendar.eventtype": "calendar",
+ "addon.calendar.fri": "calendar",
+ "addon.calendar.friday": "calendar",
"addon.calendar.gotoactivity": "calendar",
+ "addon.calendar.invalidtimedurationminutes": "calendar",
+ "addon.calendar.invalidtimedurationuntil": "calendar",
+ "addon.calendar.mon": "calendar",
+ "addon.calendar.monday": "calendar",
+ "addon.calendar.monthlyview": "calendar",
+ "addon.calendar.newevent": "calendar",
"addon.calendar.noevents": "local_moodlemobileapp",
+ "addon.calendar.nopermissiontoupdatecalendar": "error",
"addon.calendar.reminders": "local_moodlemobileapp",
+ "addon.calendar.repeatedevents": "calendar",
+ "addon.calendar.repeateditall": "calendar",
+ "addon.calendar.repeateditthis": "calendar",
+ "addon.calendar.repeatevent": "calendar",
+ "addon.calendar.repeatweeksl": "calendar",
+ "addon.calendar.sat": "calendar",
+ "addon.calendar.saturday": "calendar",
"addon.calendar.setnewreminder": "local_moodlemobileapp",
+ "addon.calendar.sun": "calendar",
+ "addon.calendar.sunday": "calendar",
+ "addon.calendar.thu": "calendar",
+ "addon.calendar.thursday": "calendar",
+ "addon.calendar.today": "calendar",
+ "addon.calendar.tomorrow": "calendar",
+ "addon.calendar.tue": "calendar",
+ "addon.calendar.tuesday": "calendar",
"addon.calendar.typecategory": "calendar",
"addon.calendar.typeclose": "calendar",
"addon.calendar.typecourse": "calendar",
@@ -87,6 +148,11 @@
"addon.calendar.typeopen": "calendar",
"addon.calendar.typesite": "calendar",
"addon.calendar.typeuser": "calendar",
+ "addon.calendar.upcomingevents": "calendar",
+ "addon.calendar.wed": "calendar",
+ "addon.calendar.wednesday": "calendar",
+ "addon.calendar.when": "calendar",
+ "addon.calendar.yesterday": "calendar",
"addon.competency.activities": "tool_lp",
"addon.competency.competencies": "competency",
"addon.competency.competenciesmostoftennotproficientincourse": "tool_lp",
@@ -355,6 +421,7 @@
"addon.mod_assign_submission_onlinetext.wordlimitexceeded": "assignsubmission_onlinetext",
"addon.mod_book.errorchapter": "book",
"addon.mod_book.modulenameplural": "book",
+ "addon.mod_book.tagarea_book_chapters": "book",
"addon.mod_book.toc": "book",
"addon.mod_chat.beep": "chat",
"addon.mod_chat.chatreport": "chat",
@@ -414,6 +481,7 @@
"addon.mod_data.confirmdeleterecord": "data",
"addon.mod_data.descending": "data",
"addon.mod_data.disapprove": "data",
+ "addon.mod_data.edittagsnotsupported": "local_moodlemobileapp",
"addon.mod_data.emptyaddform": "data",
"addon.mod_data.entrieslefttoadd": "data",
"addon.mod_data.entrieslefttoaddtoview": "data",
@@ -438,8 +506,10 @@
"addon.mod_data.recorddisapproved": "data",
"addon.mod_data.resetsettings": "data",
"addon.mod_data.search": "data",
+ "addon.mod_data.searchbytagsnotsupported": "local_moodlemobileapp",
"addon.mod_data.selectedrequired": "data",
"addon.mod_data.single": "data",
+ "addon.mod_data.tagarea_data_records": "data",
"addon.mod_data.timeadded": "data",
"addon.mod_data.timemodified": "data",
"addon.mod_data.usedate": "data",
@@ -532,6 +602,7 @@
"addon.mod_forum.reply": "forum",
"addon.mod_forum.replyplaceholder": "forum",
"addon.mod_forum.subject": "forum",
+ "addon.mod_forum.tagarea_forum_posts": "forum",
"addon.mod_forum.thisforumhasduedate": "forum",
"addon.mod_forum.thisforumisdue": "forum",
"addon.mod_forum.unlockdiscussion": "forum",
@@ -566,9 +637,11 @@
"addon.mod_glossary.modulenameplural": "glossary",
"addon.mod_glossary.noentriesfound": "local_moodlemobileapp",
"addon.mod_glossary.searchquery": "local_moodlemobileapp",
+ "addon.mod_glossary.tagarea_glossary_entries": "glossary",
"addon.mod_imscp.deploymenterror": "imscp",
"addon.mod_imscp.modulenameplural": "imscp",
"addon.mod_imscp.showmoduledescription": "local_moodlemobileapp",
+ "addon.mod_imscp.toc": "imscp",
"addon.mod_lesson.answer": "lesson",
"addon.mod_lesson.attempt": "lesson",
"addon.mod_lesson.attemptheader": "lesson",
@@ -665,6 +738,7 @@
"addon.mod_quiz.attemptnumber": "quiz",
"addon.mod_quiz.attemptquiznow": "quiz",
"addon.mod_quiz.attemptstate": "quiz",
+ "addon.mod_quiz.canattemptbutnotsubmit": "local_moodlemobileapp",
"addon.mod_quiz.cannotsubmitquizdueto": "local_moodlemobileapp",
"addon.mod_quiz.clearchoice": "qtype_multichoice",
"addon.mod_quiz.comment": "quiz",
@@ -737,6 +811,7 @@
"addon.mod_quiz.warningattemptfinished": "local_moodlemobileapp",
"addon.mod_quiz.warningdatadiscarded": "local_moodlemobileapp",
"addon.mod_quiz.warningdatadiscardedfromfinished": "local_moodlemobileapp",
+ "addon.mod_quiz.warningquestionsnotsupported": "local_moodlemobileapp",
"addon.mod_quiz.yourfinalgradeis": "quiz",
"addon.mod_resource.errorwhileloadingthecontent": "local_moodlemobileapp",
"addon.mod_resource.modifieddate": "resource",
@@ -820,6 +895,7 @@
"addon.mod_wiki.pageexists": "wiki",
"addon.mod_wiki.pagename": "wiki",
"addon.mod_wiki.subwiki": "local_moodlemobileapp",
+ "addon.mod_wiki.tagarea_wiki_pages": "wiki",
"addon.mod_wiki.titleshouldnotbeempty": "local_moodlemobileapp",
"addon.mod_wiki.viewpage": "local_moodlemobileapp",
"addon.mod_wiki.wikipage": "local_moodlemobileapp",
@@ -898,7 +974,9 @@
"addon.mod_workshop_assessment_rubric.mustchooseone": "workshopform_rubric",
"addon.notes.addnewnote": "notes",
"addon.notes.coursenotes": "notes",
+ "addon.notes.deleteconfirm": "notes",
"addon.notes.eventnotecreated": "notes",
+ "addon.notes.eventnotedeleted": "notes",
"addon.notes.nonotes": "notes",
"addon.notes.note": "notes",
"addon.notes.notes": "notes",
@@ -1231,6 +1309,7 @@
"core.answered": "quiz",
"core.areyousure": "moodle",
"core.back": "moodle",
+ "core.block.blocks": "moodle",
"core.cancel": "moodle",
"core.cannotconnect": "local_moodlemobileapp",
"core.cannotdownloadfiles": "local_moodlemobileapp",
@@ -1246,6 +1325,16 @@
"core.clicktoseefull": "local_moodlemobileapp",
"core.close": "repository",
"core.comments": "moodle",
+ "core.comments.addcomment": "moodle",
+ "core.comments.comments": "moodle",
+ "core.comments.commentscount": "moodle",
+ "core.comments.commentsnotworking": "local_moodlemobileapp",
+ "core.comments.deletecommentbyon": "moodle",
+ "core.comments.eventcommentcreated": "moodle",
+ "core.comments.eventcommentdeleted": "moodle",
+ "core.comments.nocomments": "moodle",
+ "core.comments.savecomment": "moodle",
+ "core.comments.warningcommentsnotsent": "local_moodlemobileapp",
"core.commentscount": "moodle",
"core.commentsnotworking": "local_moodlemobileapp",
"core.completion-alt-auto-fail": "completion",
@@ -1260,6 +1349,8 @@
"core.completion-alt-manual-y-override": "completion",
"core.confirmcanceledit": "local_moodlemobileapp",
"core.confirmdeletefile": "repository",
+ "core.confirmgotabroot": "local_moodlemobileapp",
+ "core.confirmgotabrootdefault": "local_moodlemobileapp",
"core.confirmloss": "local_moodlemobileapp",
"core.confirmopeninbrowser": "local_moodlemobileapp",
"core.considereddigitalminor": "moodle",
@@ -1306,6 +1397,7 @@
"core.course.warningmanualcompletionmodified": "local_moodlemobileapp",
"core.course.warningofflinemanualcompletiondeleted": "local_moodlemobileapp",
"core.coursedetails": "moodle",
+ "core.coursenogroups": "local_moodlemobileapp",
"core.courses.addtofavourites": "block_myoverview",
"core.courses.allowguests": "enrol_guest",
"core.courses.availablecourses": "moodle",
@@ -1323,6 +1415,7 @@
"core.courses.filtermycourses": "local_moodlemobileapp",
"core.courses.frontpage": "admin",
"core.courses.hidecourse": "block_myoverview",
+ "core.courses.ignore": "admin",
"core.courses.mycourses": "moodle",
"core.courses.mymoodle": "admin",
"core.courses.nocourses": "my",
@@ -1333,6 +1426,7 @@
"core.courses.password": "local_moodlemobileapp",
"core.courses.paymentrequired": "moodle",
"core.courses.paypalaccepted": "enrol_paypal",
+ "core.courses.reload": "moodle",
"core.courses.removefromfavourites": "block_myoverview",
"core.courses.search": "moodle",
"core.courses.searchcourses": "moodle",
@@ -1383,6 +1477,7 @@
"core.erroropenfilenoextension": "local_moodlemobileapp",
"core.erroropenpopup": "local_moodlemobileapp",
"core.errorrenamefile": "local_moodlemobileapp",
+ "core.errorsomedatanotdownloaded": "local_moodlemobileapp",
"core.errorsync": "local_moodlemobileapp",
"core.errorsyncblocked": "local_moodlemobileapp",
"core.explanationdigitalminor": "moodle",
@@ -1435,6 +1530,7 @@
"core.grades.range": "grades",
"core.grades.rank": "grades",
"core.grades.weight": "grades",
+ "core.group": "moodle",
"core.groupsseparate": "moodle",
"core.groupsvisible": "moodle",
"core.hasdatatosync": "local_moodlemobileapp",
@@ -1446,6 +1542,7 @@
"core.image": "local_moodlemobileapp",
"core.imageviewer": "local_moodlemobileapp",
"core.info": "moodle",
+ "core.invalidformdata": "error",
"core.ios": "local_moodlemobileapp",
"core.labelsep": "langconfig",
"core.lastaccess": "moodle",
@@ -1465,6 +1562,8 @@
"core.login.confirmdeletesite": "local_moodlemobileapp",
"core.login.connect": "local_moodlemobileapp",
"core.login.connecttomoodle": "local_moodlemobileapp",
+ "core.login.connecttomoodleapp": "local_moodlemobileapp",
+ "core.login.connecttoworkplaceapp": "local_moodlemobileapp",
"core.login.contactyouradministrator": "local_moodlemobileapp",
"core.login.contactyouradministratorissue": "local_moodlemobileapp",
"core.login.createaccount": "moodle",
@@ -1602,12 +1701,14 @@
"core.nopermissionerror": "local_moodlemobileapp",
"core.nopermissions": "error",
"core.noresults": "moodle",
+ "core.noselection": "form",
"core.notapplicable": "local_moodlemobileapp",
"core.notenrolledprofile": "moodle",
"core.notice": "moodle",
"core.notingroup": "moodle",
"core.notsent": "local_moodlemobileapp",
"core.now": "moodle",
+ "core.nummore": "local_moodlemobileapp",
"core.numwords": "moodle",
"core.offline": "message",
"core.ok": "moodle",
@@ -1670,6 +1771,9 @@
"core.sec": "moodle",
"core.secs": "moodle",
"core.seemoredetail": "survey",
+ "core.selectacategory": "moodle",
+ "core.selectacourse": "moodle",
+ "core.selectagroup": "moodle",
"core.send": "message",
"core.sending": "chat",
"core.serverconnection": "error",
@@ -1695,6 +1799,8 @@
"core.settings.disabled": "lesson",
"core.settings.displayformat": "local_moodlemobileapp",
"core.settings.enabledownloadsection": "local_moodlemobileapp",
+ "core.settings.enablefirebaseanalytics": "local_moodlemobileapp",
+ "core.settings.enablefirebaseanalyticsdescription": "local_moodlemobileapp",
"core.settings.enablerichtexteditor": "local_moodlemobileapp",
"core.settings.enablerichtexteditordescription": "local_moodlemobileapp",
"core.settings.enablesyncwifi": "local_moodlemobileapp",
@@ -1703,6 +1809,8 @@
"core.settings.errorsyncsite": "local_moodlemobileapp",
"core.settings.estimatedfreespace": "local_moodlemobileapp",
"core.settings.filesystemroot": "local_moodlemobileapp",
+ "core.settings.fontsize": "local_moodlemobileapp",
+ "core.settings.fontsizecharacter": "block_accessibility/char",
"core.settings.general": "moodle",
"core.settings.language": "moodle",
"core.settings.license": "moodle",
@@ -1738,6 +1846,7 @@
"core.sharedfiles.sharedfiles": "local_moodlemobileapp",
"core.sharedfiles.successstorefile": "local_moodlemobileapp",
"core.show": "moodle",
+ "core.showless": "form",
"core.showmore": "form",
"core.site": "moodle",
"core.sitehome.sitehome": "moodle",
@@ -1770,6 +1879,20 @@
"core.submit": "moodle",
"core.success": "moodle",
"core.tablet": "local_moodlemobileapp",
+ "core.tag.defautltagcoll": "moodle",
+ "core.tag.errorareanotsupported": "local_moodlemobileapp",
+ "core.tag.inalltagcoll": "moodle",
+ "core.tag.itemstaggedwith": "moodle",
+ "core.tag.notagsfound": "moodle",
+ "core.tag.searchtags": "moodle",
+ "core.tag.showingfirsttags": "moodle",
+ "core.tag.tag": "moodle",
+ "core.tag.tagarea_course": "moodle",
+ "core.tag.tagarea_course_modules": "moodle",
+ "core.tag.tagarea_post": "moodle",
+ "core.tag.tagarea_user": "moodle",
+ "core.tag.tags": "moodle",
+ "core.tag.warningareasnotsupported": "local_moodlemobileapp",
"core.teachers": "moodle",
"core.thereisdatatosync": "local_moodlemobileapp",
"core.thisdirection": "langconfig",
@@ -1786,6 +1909,7 @@
"core.unlimited": "moodle",
"core.unzipping": "local_moodlemobileapp",
"core.upgraderunning": "error",
+ "core.user": "moodle",
"core.user.address": "moodle",
"core.user.city": "moodle",
"core.user.contact": "local_moodlemobileapp",
diff --git a/scripts/linux.sh b/scripts/linux.sh
deleted file mode 100755
index 571102f5b59..00000000000
--- a/scripts/linux.sh
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/bin/bash
-#
-# Script for generating the Desktop builds
-#
-
-sudo apt-get install -y libnss3-dev
-
-npm install -g electron-builder electron
-
-electron-builder install-app-deps
-
-jq -s '.[0] + {"name": "moodledesktop"}' package.json > package_new.json
-mv package_new.json package.json
-
-rm -Rf desktop/dist
-
-npm run desktop.dist -- -l --x64 --ia32
-
-if [ ! -z $GIT_ORG_PRIVATE ] && [ ! -z $GIT_TOKEN ] ; then
- git clone -q https://$GIT_TOKEN@github.com/moodlemobile/bma-apps-builds.git ../apps
-
- mv desktop/dist/*.AppImage ../apps
-
- cd ../apps
-
- chmod +x *.AppImage
- mv *i386.AppImage linux-ia32.AppImage
- mv Moodle*.AppImage linux-x64.AppImage
- ls
-
- tar -czvf MoodleDesktop32.tar.gz linux-ia32.AppImage
- tar -czvf MoodleDesktop64.tar.gz linux-x64.AppImage
- rm *.AppImage
-
- git add .
- git commit -m "Linux desktop versions from Travis build $TRAVIS_BUILD_NUMBER"
- git push
-fi
diff --git a/scripts/moodle_to_json.php b/scripts/moodle_to_json.php
index bc6d128e544..e82eb95236b 100644
--- a/scripts/moodle_to_json.php
+++ b/scripts/moodle_to_json.php
@@ -26,6 +26,10 @@
define('LANGPACKSFOLDER', '../../moodle-langpacks');
define('ASSETSPATH', '../src/assets/lang/');
define('CONFIG', '../src/config.json');
+define('OVERRIDE_LANG_SUFIX', false);
+
+global $strings;
+require_once('lang_functions.php');
$config = file_get_contents(CONFIG);
$config = (array) json_decode($config);
@@ -42,355 +46,17 @@
$languages = $config_langs;
}
-// Process the index file, just once.
-$keys = file_get_contents('langindex.json');
-$keys = (array) json_decode($keys);
-
-foreach ($keys as $key => $value) {
- $map = new StdClass();
- if ($value == 'local_moodlemobileapp') {
- $map->file = $value;
- $map->string = $key;
- } else {
- $exp = explode('/', $value, 2);
- $map->file = $exp[0];
- if (count($exp) == 2) {
- $map->string = $exp[1];
- } else {
- $exp = explode('.', $key, 3);
-
- if (count($exp) == 3) {
- $map->string = $exp[2];
- } else {
- $map->string = $exp[1];
- }
- }
- }
-
- $keys[$key] = $map;
-}
-$total = count ($keys);
-
-echo "Total strings to translate $total\n";
+$keys = get_langindex_keys();
-$add_langs = array();
-// Process the languages.
-foreach ($languages as $lang) {
- $ok = build_lang($lang, $keys, $total);
- if ($ok) {
- $add_langs[$lang] = $lang;
- }
-}
+$added_langs = build_languages($languages, $keys);
if ($forcedetect) {
- echo "\n\n\n";
-
- $all_languages = glob(LANGPACKSFOLDER.'/*' , GLOB_ONLYDIR);
- function get_lang_from_dir($dir) {
- return str_replace('_', '-', explode('/', $dir)[3]);
- }
- $all_languages = array_map('get_lang_from_dir', $all_languages);
- $detect_lang = array_diff($all_languages, $languages);
- $new_langs = array();
- foreach ($detect_lang as $lang) {
- $new = detect_lang($lang, $keys, $total);
- if ($new) {
- $new_langs[$lang] = $lang;
- }
- }
+ $new_langs = detect_languages($languages, $keys);
if (!empty($new_langs)) {
echo "\n\n\nThe following languages are going to be added\n\n\n";
- foreach ($new_langs as $lang) {
- $ok = build_lang($lang, $keys, $total);
- if ($ok) {
- $add_langs[$lang] = $lang;
- }
- }
- add_langs_to_config($add_langs, $config);
- }
-} else {
- add_langs_to_config($add_langs, $config);
-}
-
-function add_langs_to_config($langs, $config) {
- $changed = false;
- $config_langs = get_object_vars($config['languages']);
- foreach ($langs as $lang) {
- if (!isset($config_langs[$lang])) {
- $langfoldername = str_replace('-', '_', $lang);
-
- $string = [];
- include(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php');
- $config['languages']->$lang = $string['thislanguage'];
- $changed = true;
- }
- }
-
- if ($changed) {
- // Sort languages by key.
- $config['languages'] = json_decode( json_encode( $config['languages'] ), true );
- ksort($config['languages']);
- $config['languages'] = json_decode( json_encode( $config['languages'] ), false );
- file_put_contents(CONFIG, json_encode($config, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
- }
-}
-
-function build_lang($lang, $keys, $total) {
- $local = 0;
- $langFile = false;
- $translations = [];
- $langfoldername = str_replace('-', '_', $lang);
-
- if (!is_dir(LANGPACKSFOLDER.'/'.$langfoldername) || !is_file(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php')) {
- echo "Cannot translate $langfoldername, folder not found";
- return false;
- }
-
- $string = [];
- include(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php');
- $parent = isset($string['parentlanguage']) ? $string['parentlanguage'] : "";
-
- echo "Processing $lang";
- if ($parent != "" && $parent != $lang) {
- echo "($parent)";
- }
-
-
- // Add the translation to the array.
- foreach ($keys as $key => $value) {
- $file = LANGPACKSFOLDER.'/'.$langfoldername.'/'.$value->file.'.php';
- // Apply translations.
- if (!file_exists($file)) {
- if (TOTRANSLATE) {
- echo "\n\t\To translate $value->string on $value->file";
- }
- continue;
- }
-
- $string = [];
- include($file);
-
- if (!isset($string[$value->string]) || ($lang == 'en' && $value->file == 'local_moodlemobileapp')) {
- // Not yet translated. Do not override.
- if (!$langFile) {
- // Load lang files just once.
- $langFile = file_get_contents(ASSETSPATH.$lang.'.json');
- $langFile = (array) json_decode($langFile);
- }
- if (is_array($langFile) && isset($langFile[$key])) {
- $translations[$key] = $langFile[$key];
- $local++;
- }
- if (TOTRANSLATE) {
- echo "\n\t\tTo translate $value->string on $value->file";
- }
- continue;
- } else {
- $text = $string[$value->string];
- }
-
- if ($value->file != 'local_moodlemobileapp') {
- $text = str_replace('$a->', '$a.', $text);
- $text = str_replace('{$a', '{{$a', $text);
- $text = str_replace('}', '}}', $text);
- // Prevent double.
- $text = str_replace(array('{{{', '}}}'), array('{{', '}}'), $text);
- } else {
- $local++;
- }
-
- $translations[$key] = html_entity_decode($text);
- }
-
- // Sort and save.
- ksort($translations);
- file_put_contents(ASSETSPATH.$lang.'.json', str_replace('\/', '/', json_encode($translations, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)));
-
- $success = count($translations);
- $percentage = floor($success/$total *100);
- echo "\t\t$success of $total -> $percentage% ($local local)\n";
-
- if ($lang == 'en') {
- generate_local_moodlemobileapp($keys, $translations);
- override_component_lang_files($keys, $translations);
- }
-
- return true;
-}
-
-function detect_lang($lang, $keys, $total) {
- $success = 0;
- $local = 0;
- $langfoldername = str_replace('-', '_', $lang);
-
- if (!is_dir(LANGPACKSFOLDER.'/'.$langfoldername) || !is_file(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php')) {
- echo "Cannot translate $langfoldername, folder not found";
- return false;
- }
-
- $string = [];
- include(LANGPACKSFOLDER.'/'.$langfoldername.'/langconfig.php');
- $parent = isset($string['parentlanguage']) ? $string['parentlanguage'] : "";
- if (!isset($string['thislanguage'])) {
- echo "Cannot translate $langfoldername, name not found";
- return false;
- }
-
- echo "Checking $lang";
- if ($parent != "" && $parent != $lang) {
- echo "($parent)";
- }
- $langname = $string['thislanguage'];
- echo " ".$langname." -D";
-
- // Add the translation to the array.
- foreach ($keys as $key => $value) {
- $file = LANGPACKSFOLDER.'/'.$langfoldername.'/'.$value->file.'.php';
- // Apply translations.
- if (!file_exists($file)) {
- continue;
- }
-
- $string = [];
- include($file);
-
- if (!isset($string[$value->string])) {
- continue;
- } else {
- $text = $string[$value->string];
- }
-
- if ($value->file == 'local_moodlemobileapp') {
- $local++;
- }
-
- $success++;
- }
-
- $percentage = floor($success/$total *100);
- echo "\t\t$success of $total -> $percentage% ($local local)";
- if (($percentage > 75 && $local > 50) || ($percentage > 50 && $local > 75)) {
- echo " \t DETECTED\n";
- return true;
- }
- echo "\n";
-
- return false;
-}
-
-function save_key($key, $value, $path) {
- $filePath = $path . '/en.json';
-
- $file = file_get_contents($filePath);
- $file = (array) json_decode($file);
- $value = html_entity_decode($value);
- if ($file[$key] != $value) {
- $file[$key] = $value;
- ksort($file);
- file_put_contents($filePath, str_replace('\/', '/', json_encode($file, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT)));
+ $added_langs = build_languages($new_langs, $keys, $added_langs);
}
}
-function override_component_lang_files($keys, $translations) {
- echo "Override component lang files.\n";
- foreach ($translations as $key => $value) {
- $path = '../src/';
- $exp = explode('.', $key, 3);
-
- $type = $exp[0];
- if (count($exp) == 3) {
- $component = $exp[1];
- $plainid = $exp[2];
- } else {
- $component = 'moodle';
- $plainid = $exp[1];
- }
- switch($type) {
- case 'core':
- case 'addon':
- switch($component) {
- case 'moodle':
- $path .= 'lang';
- break;
- default:
- $path .= $type.'/'.str_replace('_', '/', $component).'/lang';
- break;
- }
- break;
- case 'assets':
- $path .= $type.'/'.$component;
- break;
-
- }
-
- if (is_file($path.'/en.json')) {
- save_key($plainid, $value, $path);
- }
- }
-}
-
-/**
- * Generates local moodle mobile app file to update languages in AMOS.
- *
- * @param [array] $keys Translation keys.
- * @param [array] $translations English translations.
- */
-function generate_local_moodlemobileapp($keys, $translations) {
- echo "Generate local_moodlemobileapp.\n";
- $string = '.
-
-/**
- * Version details.
- *
- * @package local
- * @subpackage moodlemobileapp
- * @copyright 2014 Juan Leyva
- * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
- */
-
-$string[\'appstoredescription\'] = \'NOTE: This official Moodle Mobile app will ONLY work with Moodle sites that have been set up to allow it. Please talk to your Moodle administrator if you have any problems connecting.
-
-If your Moodle site has been configured correctly, you can use this app to:
-
-- browse the content of your courses, even when offline
-- receive instant notifications of messages and other events
-- quickly find and contact other people in your courses
-- upload images, audio, videos and other files from your mobile device
-- view your course grades
-- and more!
-
-Please see http://docs.moodle.org/en/Mobile_app for all the latest information.
-
-We’d really appreciate any good reviews about the functionality so far, and your suggestions on what else you want this app to do!
-
-The app requires the following permissions:
-Record audio - For recording audio to upload to Moodle
-Read and modify the contents of your SD card - Contents are downloaded to the SD Card so you can see them offline
-Network access - To be able to connect with your Moodle site and check if you are connected or not to switch to offline mode
-Run at startup - So you receive local notifications even when the app is running in the background
-Prevent phone from sleeping - So you can receive push notifications anytime\';'."\n";
- foreach ($keys as $key => $value) {
- if (isset($translations[$key]) && $value->file == 'local_moodlemobileapp') {
- $string .= '$string[\''.$key.'\'] = \''.str_replace("'", "\'", $translations[$key]).'\';'."\n";
- }
- }
- $string .= '$string[\'pluginname\'] = \'Moodle Mobile language strings\';'."\n";
-
- file_put_contents('../../moodle-local_moodlemobileapp/lang/en/local_moodlemobileapp.php', $string."\n");
-}
-
+add_langs_to_config($added_langs, $config);
diff --git a/src/addon/badges/providers/badge-link-handler.ts b/src/addon/badges/providers/badge-link-handler.ts
index bcaf759e0c1..144acad5cd1 100644
--- a/src/addon/badges/providers/badge-link-handler.ts
+++ b/src/addon/badges/providers/badge-link-handler.ts
@@ -15,7 +15,7 @@
import { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
-import { CoreLoginHelperProvider } from '@core/login/providers/helper';
+import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { AddonBadgesProvider } from './badges';
/**
@@ -26,7 +26,7 @@ export class AddonBadgesBadgeLinkHandler extends CoreContentLinksHandlerBase {
name = 'AddonBadgesBadgeLinkHandler';
pattern = /\/badges\/badge\.php.*([\?\&]hash=)/;
- constructor(private badgesProvider: AddonBadgesProvider, private loginHelper: CoreLoginHelperProvider) {
+ constructor(private badgesProvider: AddonBadgesProvider, private linkHelper: CoreContentLinksHelperProvider) {
super();
}
@@ -44,8 +44,7 @@ export class AddonBadgesBadgeLinkHandler extends CoreContentLinksHandlerBase {
return [{
action: (siteId, navCtrl?): void => {
- // Always use redirect to make it the new history root (to avoid "loops" in history).
- this.loginHelper.redirect('AddonBadgesIssuedBadgePage', {courseId: 0, badgeHash: params.hash}, siteId);
+ this.linkHelper.goInSite(navCtrl, 'AddonBadgesIssuedBadgePage', {courseId: 0, badgeHash: params.hash}, siteId);
}
}];
}
diff --git a/src/addon/badges/providers/mybadges-link-handler.ts b/src/addon/badges/providers/mybadges-link-handler.ts
index 8a36cbbc0df..190e575acbb 100644
--- a/src/addon/badges/providers/mybadges-link-handler.ts
+++ b/src/addon/badges/providers/mybadges-link-handler.ts
@@ -15,7 +15,7 @@
import { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
-import { CoreLoginHelperProvider } from '@core/login/providers/helper';
+import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { AddonBadgesProvider } from './badges';
/**
@@ -27,7 +27,7 @@ export class AddonBadgesMyBadgesLinkHandler extends CoreContentLinksHandlerBase
featureName = 'CoreUserDelegate_AddonBadges';
pattern = /\/badges\/mybadges\.php/;
- constructor(private badgesProvider: AddonBadgesProvider, private loginHelper: CoreLoginHelperProvider) {
+ constructor(private badgesProvider: AddonBadgesProvider, private linkHelper: CoreContentLinksHelperProvider) {
super();
}
@@ -45,8 +45,7 @@ export class AddonBadgesMyBadgesLinkHandler extends CoreContentLinksHandlerBase
return [{
action: (siteId, navCtrl?): void => {
- // Always use redirect to make it the new history root (to avoid "loops" in history).
- this.loginHelper.redirect('AddonBadgesUserBadgesPage', {}, siteId);
+ this.linkHelper.goInSite(navCtrl, 'AddonBadgesUserBadgesPage', {}, siteId);
}
}];
}
diff --git a/src/addon/block/activitymodules/providers/block-handler.ts b/src/addon/block/activitymodules/providers/block-handler.ts
index 0024324c991..752d23a47aa 100644
--- a/src/addon/block/activitymodules/providers/block-handler.ts
+++ b/src/addon/block/activitymodules/providers/block-handler.ts
@@ -38,7 +38,7 @@ export class AddonBlockActivityModulesHandler extends CoreBlockBaseHandler {
* @param {number} instanceId The instance ID associated with the context level.
* @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
*/
- getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
: CoreBlockHandlerData | Promise {
return {
diff --git a/src/addon/block/badges/badges.module.ts b/src/addon/block/badges/badges.module.ts
new file mode 100644
index 00000000000..ff9c450d293
--- /dev/null
+++ b/src/addon/block/badges/badges.module.ts
@@ -0,0 +1,38 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreBlockDelegate } from '@core/block/providers/delegate';
+import { AddonBlockBadgesHandler } from './providers/block-handler';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ IonicModule,
+ TranslateModule.forChild()
+ ],
+ exports: [
+ ],
+ providers: [
+ AddonBlockBadgesHandler
+ ]
+})
+export class AddonBlockBadgesModule {
+ constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockBadgesHandler) {
+ blockDelegate.registerHandler(blockHandler);
+ }
+}
diff --git a/src/addon/block/badges/badges.scss b/src/addon/block/badges/badges.scss
new file mode 100644
index 00000000000..3f9d26d8bac
--- /dev/null
+++ b/src/addon/block/badges/badges.scss
@@ -0,0 +1,23 @@
+.addon-block-badges core-block-pre-rendered {
+ .core-block-content {
+ ul.badges {
+ list-style: none;
+ @include margin-horizontal(0);
+ -webkit-padding-start: 0;
+
+ li {
+ position: relative;
+ display: inline-block;
+ padding-top: 1em;
+ text-align: center;
+ vertical-align: top;
+ width: 150px;
+
+ .badge-name {
+ display: block;
+ padding: 5px;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/addon/block/badges/lang/en.json b/src/addon/block/badges/lang/en.json
new file mode 100644
index 00000000000..dd957321f57
--- /dev/null
+++ b/src/addon/block/badges/lang/en.json
@@ -0,0 +1,3 @@
+{
+ "pluginname": "Latest badges"
+}
\ No newline at end of file
diff --git a/src/addon/block/badges/providers/block-handler.ts b/src/addon/block/badges/providers/block-handler.ts
new file mode 100644
index 00000000000..d6cfb677a39
--- /dev/null
+++ b/src/addon/block/badges/providers/block-handler.ts
@@ -0,0 +1,51 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+
+import { CoreBlockHandlerData } from '@core/block/providers/delegate';
+import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block';
+import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
+
+/**
+ * Block handler.
+ */
+@Injectable()
+export class AddonBlockBadgesHandler extends CoreBlockBaseHandler {
+ name = 'AddonBlockBadges';
+ blockName = 'badges';
+
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the data needed to render the block.
+ *
+ * @param {Injector} injector Injector.
+ * @param {any} block The block to render.
+ * @param {string} contextLevel The context where the block will be used.
+ * @param {number} instanceId The instance ID associated with the context level.
+ * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
+ */
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ : CoreBlockHandlerData | Promise {
+
+ return {
+ title: 'addon.block_badges.pluginname',
+ class: 'addon-block-badges',
+ component: CoreBlockPreRenderedComponent
+ };
+ }
+}
diff --git a/src/addon/block/blogmenu/blogmenu.module.ts b/src/addon/block/blogmenu/blogmenu.module.ts
new file mode 100644
index 00000000000..cbca102001d
--- /dev/null
+++ b/src/addon/block/blogmenu/blogmenu.module.ts
@@ -0,0 +1,38 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreBlockDelegate } from '@core/block/providers/delegate';
+import { AddonBlockBlogMenuHandler } from './providers/block-handler';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ IonicModule,
+ TranslateModule.forChild()
+ ],
+ exports: [
+ ],
+ providers: [
+ AddonBlockBlogMenuHandler
+ ]
+})
+export class AddonBlockBlogMenuModule {
+ constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockBlogMenuHandler) {
+ blockDelegate.registerHandler(blockHandler);
+ }
+}
diff --git a/src/addon/block/blogmenu/blogmenu.scss b/src/addon/block/blogmenu/blogmenu.scss
new file mode 100644
index 00000000000..c649986ed0e
--- /dev/null
+++ b/src/addon/block/blogmenu/blogmenu.scss
@@ -0,0 +1,16 @@
+.addon-block-blog-menu core-block-pre-rendered {
+ .core-block-content {
+ ul.list {
+ list-style: none;
+ @include margin-horizontal(0);
+ -webkit-padding-start: 0;
+
+ li {
+ padding-bottom: 8px;
+ }
+ }
+ }
+ .core-block-footer {
+ display: none;
+ }
+}
\ No newline at end of file
diff --git a/src/addon/block/blogmenu/lang/en.json b/src/addon/block/blogmenu/lang/en.json
new file mode 100644
index 00000000000..23541f7a0b0
--- /dev/null
+++ b/src/addon/block/blogmenu/lang/en.json
@@ -0,0 +1,3 @@
+{
+ "pluginname": "Blog menu"
+}
\ No newline at end of file
diff --git a/src/addon/block/blogmenu/providers/block-handler.ts b/src/addon/block/blogmenu/providers/block-handler.ts
new file mode 100644
index 00000000000..231137b8e0f
--- /dev/null
+++ b/src/addon/block/blogmenu/providers/block-handler.ts
@@ -0,0 +1,51 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+
+import { CoreBlockHandlerData } from '@core/block/providers/delegate';
+import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block';
+import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
+
+/**
+ * Block handler.
+ */
+@Injectable()
+export class AddonBlockBlogMenuHandler extends CoreBlockBaseHandler {
+ name = 'AddonBlockBlogMenu';
+ blockName = 'blog_menu';
+
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the data needed to render the block.
+ *
+ * @param {Injector} injector Injector.
+ * @param {any} block The block to render.
+ * @param {string} contextLevel The context where the block will be used.
+ * @param {number} instanceId The instance ID associated with the context level.
+ * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
+ */
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ : CoreBlockHandlerData | Promise {
+
+ return {
+ title: 'addon.block_blogmenu.pluginname',
+ class: 'addon-block-blog-menu',
+ component: CoreBlockPreRenderedComponent
+ };
+ }
+}
diff --git a/src/addon/block/blogrecent/blogrecent.module.ts b/src/addon/block/blogrecent/blogrecent.module.ts
new file mode 100644
index 00000000000..1ecdf6f7b36
--- /dev/null
+++ b/src/addon/block/blogrecent/blogrecent.module.ts
@@ -0,0 +1,38 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreBlockDelegate } from '@core/block/providers/delegate';
+import { AddonBlockBlogRecentHandler } from './providers/block-handler';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ IonicModule,
+ TranslateModule.forChild()
+ ],
+ exports: [
+ ],
+ providers: [
+ AddonBlockBlogRecentHandler
+ ]
+})
+export class AddonBlockBlogRecentModule {
+ constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockBlogRecentHandler) {
+ blockDelegate.registerHandler(blockHandler);
+ }
+}
diff --git a/src/addon/block/blogrecent/blogrecent.scss b/src/addon/block/blogrecent/blogrecent.scss
new file mode 100644
index 00000000000..81a03b22766
--- /dev/null
+++ b/src/addon/block/blogrecent/blogrecent.scss
@@ -0,0 +1,13 @@
+.addon-block-blog-recent core-block-pre-rendered {
+ .core-block-content {
+ ul.list {
+ list-style: none;
+ @include margin-horizontal(0);
+ -webkit-padding-start: 0;
+
+ li {
+ padding-bottom: 8px;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/addon/block/blogrecent/lang/en.json b/src/addon/block/blogrecent/lang/en.json
new file mode 100644
index 00000000000..a92c0cce56e
--- /dev/null
+++ b/src/addon/block/blogrecent/lang/en.json
@@ -0,0 +1,3 @@
+{
+ "pluginname": "Recent blog entries"
+}
\ No newline at end of file
diff --git a/src/addon/block/blogrecent/providers/block-handler.ts b/src/addon/block/blogrecent/providers/block-handler.ts
new file mode 100644
index 00000000000..55f03cb8ed7
--- /dev/null
+++ b/src/addon/block/blogrecent/providers/block-handler.ts
@@ -0,0 +1,51 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+
+import { CoreBlockHandlerData } from '@core/block/providers/delegate';
+import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block';
+import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
+
+/**
+ * Block handler.
+ */
+@Injectable()
+export class AddonBlockBlogRecentHandler extends CoreBlockBaseHandler {
+ name = 'AddonBlockBlogRecent';
+ blockName = 'blog_recent';
+
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the data needed to render the block.
+ *
+ * @param {Injector} injector Injector.
+ * @param {any} block The block to render.
+ * @param {string} contextLevel The context where the block will be used.
+ * @param {number} instanceId The instance ID associated with the context level.
+ * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
+ */
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ : CoreBlockHandlerData | Promise {
+
+ return {
+ title: 'addon.block_blogrecent.pluginname',
+ class: 'addon-block-blog-recent',
+ component: CoreBlockPreRenderedComponent
+ };
+ }
+}
diff --git a/src/addon/block/blogtags/blogtags.module.ts b/src/addon/block/blogtags/blogtags.module.ts
new file mode 100644
index 00000000000..a9ed3a0904c
--- /dev/null
+++ b/src/addon/block/blogtags/blogtags.module.ts
@@ -0,0 +1,38 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreBlockDelegate } from '@core/block/providers/delegate';
+import { AddonBlockBlogTagsHandler } from './providers/block-handler';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ IonicModule,
+ TranslateModule.forChild()
+ ],
+ exports: [
+ ],
+ providers: [
+ AddonBlockBlogTagsHandler
+ ]
+})
+export class AddonBlockBlogTagsModule {
+ constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockBlogTagsHandler) {
+ blockDelegate.registerHandler(blockHandler);
+ }
+}
diff --git a/src/addon/block/blogtags/blogtags.scss b/src/addon/block/blogtags/blogtags.scss
new file mode 100644
index 00000000000..a974b45cbd0
--- /dev/null
+++ b/src/addon/block/blogtags/blogtags.scss
@@ -0,0 +1,88 @@
+.addon-block-blog-tags core-block-pre-rendered {
+ .core-block-content {
+ ul.inline-list {
+ list-style: none;
+ @include margin-horizontal(0);
+ -webkit-padding-start: 0;
+
+ li {
+ padding: .2em;
+ display: inline-block;
+
+ a {
+ @extend ion-badge;
+ @extend .badge-md;
+ text-decoration: none;
+ }
+ .s20 {
+ font-size: 1.5em;
+ font-weight: bold;
+ }
+
+ .s19 {
+ font-size: 1.5em;
+ }
+
+ .s18 {
+ font-size: 1.4em;
+ font-weight: bold;
+ }
+
+ .s17 {
+ font-size: 1.4em;
+ }
+
+ .s16 {
+ font-size: 1.3em;
+ font-weight: bold;
+ }
+
+ .s15 {
+ font-size: 1.3em;
+ }
+
+ .s14 {
+ font-size: 1.2em;
+ font-weight: bold;
+ }
+
+ .s13 {
+ font-size: 1.2em;
+ }
+
+ .s12,
+ .s11 {
+ font-size: 1.1em;
+ font-weight: bold;
+ }
+
+ .s10,
+ .s9 {
+ font-size: 1.1em;
+ }
+
+ .s8,
+ .s7 {
+ font-size: 1em;
+ font-weight: bold;
+ }
+
+ .s6,
+ .s5 {
+ font-size: 1em;
+ }
+
+ .s4,
+ .s3 {
+ font-size: 0.9em;
+ font-weight: bold;
+ }
+
+ .s2,
+ .s1 {
+ font-size: 0.9em;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/addon/block/blogtags/lang/en.json b/src/addon/block/blogtags/lang/en.json
new file mode 100644
index 00000000000..683c3aa90af
--- /dev/null
+++ b/src/addon/block/blogtags/lang/en.json
@@ -0,0 +1,3 @@
+{
+ "pluginname": "Blog tags"
+}
\ No newline at end of file
diff --git a/src/addon/block/blogtags/providers/block-handler.ts b/src/addon/block/blogtags/providers/block-handler.ts
new file mode 100644
index 00000000000..aa2a17495c9
--- /dev/null
+++ b/src/addon/block/blogtags/providers/block-handler.ts
@@ -0,0 +1,51 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+
+import { CoreBlockHandlerData } from '@core/block/providers/delegate';
+import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block';
+import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
+
+/**
+ * Block handler.
+ */
+@Injectable()
+export class AddonBlockBlogTagsHandler extends CoreBlockBaseHandler {
+ name = 'AddonBlockBlogTags';
+ blockName = 'blog_tags';
+
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the data needed to render the block.
+ *
+ * @param {Injector} injector Injector.
+ * @param {any} block The block to render.
+ * @param {string} contextLevel The context where the block will be used.
+ * @param {number} instanceId The instance ID associated with the context level.
+ * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
+ */
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ : CoreBlockHandlerData | Promise {
+
+ return {
+ title: 'addon.block_blogtags.pluginname',
+ class: 'addon-block-blog-tags',
+ component: CoreBlockPreRenderedComponent
+ };
+ }
+}
diff --git a/src/addon/block/calendarmonth/calendarmonth.module.ts b/src/addon/block/calendarmonth/calendarmonth.module.ts
new file mode 100644
index 00000000000..e3204ae8f66
--- /dev/null
+++ b/src/addon/block/calendarmonth/calendarmonth.module.ts
@@ -0,0 +1,38 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreBlockDelegate } from '@core/block/providers/delegate';
+import { AddonBlockCalendarMonthHandler } from './providers/block-handler';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ IonicModule,
+ TranslateModule.forChild()
+ ],
+ exports: [
+ ],
+ providers: [
+ AddonBlockCalendarMonthHandler
+ ]
+})
+export class AddonBlockCalendarMonthModule {
+ constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockCalendarMonthHandler) {
+ blockDelegate.registerHandler(blockHandler);
+ }
+}
diff --git a/src/addon/block/calendarmonth/lang/en.json b/src/addon/block/calendarmonth/lang/en.json
new file mode 100644
index 00000000000..86a476c29e7
--- /dev/null
+++ b/src/addon/block/calendarmonth/lang/en.json
@@ -0,0 +1,3 @@
+{
+ "pluginname": "Calendar"
+}
\ No newline at end of file
diff --git a/src/addon/block/calendarmonth/providers/block-handler.ts b/src/addon/block/calendarmonth/providers/block-handler.ts
new file mode 100644
index 00000000000..80e85edf8b1
--- /dev/null
+++ b/src/addon/block/calendarmonth/providers/block-handler.ts
@@ -0,0 +1,60 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+import { CoreBlockHandlerData } from '@core/block/providers/delegate';
+import { CoreBlockOnlyTitleComponent } from '@core/block/components/only-title-block/only-title-block';
+import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
+import { AddonCalendarProvider } from '@addon/calendar/providers/calendar';
+
+/**
+ * Block handler.
+ */
+@Injectable()
+export class AddonBlockCalendarMonthHandler extends CoreBlockBaseHandler {
+ name = 'AddonBlockCalendarMonth';
+ blockName = 'calendar_month';
+
+ constructor(private calendarProvider: AddonCalendarProvider) {
+ super();
+ }
+
+ /**
+ * Returns the data needed to render the block.
+ *
+ * @param {Injector} injector Injector.
+ * @param {any} block The block to render.
+ * @param {string} contextLevel The context where the block will be used.
+ * @param {number} instanceId The instance ID associated with the context level.
+ * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
+ */
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ : CoreBlockHandlerData | Promise {
+
+ let link = 'AddonCalendarListPage';
+ const linkParams: any = contextLevel == 'course' ? { courseId: instanceId } : {};
+
+ if (this.calendarProvider.canViewMonthInSite()) {
+ link = 'AddonCalendarIndexPage';
+ }
+
+ return {
+ title: 'addon.block_calendarmonth.pluginname',
+ class: 'addon-block-calendar-month',
+ component: CoreBlockOnlyTitleComponent,
+ link: link,
+ linkParams: linkParams
+ };
+ }
+}
diff --git a/src/addon/block/calendarupcoming/calendarupcoming.module.ts b/src/addon/block/calendarupcoming/calendarupcoming.module.ts
new file mode 100644
index 00000000000..345d1767223
--- /dev/null
+++ b/src/addon/block/calendarupcoming/calendarupcoming.module.ts
@@ -0,0 +1,38 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreBlockDelegate } from '@core/block/providers/delegate';
+import { AddonBlockCalendarUpcomingHandler } from './providers/block-handler';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ IonicModule,
+ TranslateModule.forChild()
+ ],
+ exports: [
+ ],
+ providers: [
+ AddonBlockCalendarUpcomingHandler
+ ]
+})
+export class AddonBlockCalendarUpcomingModule {
+ constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockCalendarUpcomingHandler) {
+ blockDelegate.registerHandler(blockHandler);
+ }
+}
diff --git a/src/addon/block/calendarupcoming/lang/en.json b/src/addon/block/calendarupcoming/lang/en.json
new file mode 100644
index 00000000000..4faba6dd20e
--- /dev/null
+++ b/src/addon/block/calendarupcoming/lang/en.json
@@ -0,0 +1,3 @@
+{
+ "pluginname": " Upcoming events"
+}
\ No newline at end of file
diff --git a/src/addon/block/calendarupcoming/providers/block-handler.ts b/src/addon/block/calendarupcoming/providers/block-handler.ts
new file mode 100644
index 00000000000..c58a0d08038
--- /dev/null
+++ b/src/addon/block/calendarupcoming/providers/block-handler.ts
@@ -0,0 +1,61 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+import { CoreBlockHandlerData } from '@core/block/providers/delegate';
+import { CoreBlockOnlyTitleComponent } from '@core/block/components/only-title-block/only-title-block';
+import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
+import { AddonCalendarProvider } from '@addon/calendar/providers/calendar';
+
+/**
+ * Block handler.
+ */
+@Injectable()
+export class AddonBlockCalendarUpcomingHandler extends CoreBlockBaseHandler {
+ name = 'AddonBlockCalendarUpcoming';
+ blockName = 'calendar_upcoming';
+
+ constructor(private calendarProvider: AddonCalendarProvider) {
+ super();
+ }
+
+ /**
+ * Returns the data needed to render the block.
+ *
+ * @param {Injector} injector Injector.
+ * @param {any} block The block to render.
+ * @param {string} contextLevel The context where the block will be used.
+ * @param {number} instanceId The instance ID associated with the context level.
+ * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
+ */
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ : CoreBlockHandlerData | Promise {
+
+ let link = 'AddonCalendarListPage';
+ const linkParams: any = contextLevel == 'course' ? { courseId: instanceId } : {};
+
+ if (this.calendarProvider.canViewMonthInSite()) {
+ link = 'AddonCalendarIndexPage';
+ linkParams.upcoming = true;
+ }
+
+ return {
+ title: 'addon.block_calendarupcoming.pluginname',
+ class: 'addon-block-calendar-upcoming',
+ component: CoreBlockOnlyTitleComponent,
+ link: link,
+ linkParams: linkParams
+ };
+ }
+}
diff --git a/src/addon/block/comments/comments.module.ts b/src/addon/block/comments/comments.module.ts
new file mode 100644
index 00000000000..dce565e7da6
--- /dev/null
+++ b/src/addon/block/comments/comments.module.ts
@@ -0,0 +1,38 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreBlockDelegate } from '@core/block/providers/delegate';
+import { AddonBlockCommentsHandler } from './providers/block-handler';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ IonicModule,
+ TranslateModule.forChild()
+ ],
+ exports: [
+ ],
+ providers: [
+ AddonBlockCommentsHandler
+ ]
+})
+export class AddonBlockCommentsModule {
+ constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockCommentsHandler) {
+ blockDelegate.registerHandler(blockHandler);
+ }
+}
diff --git a/src/addon/block/comments/lang/en.json b/src/addon/block/comments/lang/en.json
new file mode 100644
index 00000000000..adcbcabae8f
--- /dev/null
+++ b/src/addon/block/comments/lang/en.json
@@ -0,0 +1,3 @@
+{
+ "pluginname": "Comments"
+}
\ No newline at end of file
diff --git a/src/addon/block/comments/providers/block-handler.ts b/src/addon/block/comments/providers/block-handler.ts
new file mode 100644
index 00000000000..ada6e16549d
--- /dev/null
+++ b/src/addon/block/comments/providers/block-handler.ts
@@ -0,0 +1,53 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+import { CoreBlockHandlerData } from '@core/block/providers/delegate';
+import { CoreBlockOnlyTitleComponent } from '@core/block/components/only-title-block/only-title-block';
+import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
+
+/**
+ * Block handler.
+ */
+@Injectable()
+export class AddonBlockCommentsHandler extends CoreBlockBaseHandler {
+ name = 'AddonBlockComments';
+ blockName = 'comments';
+
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the data needed to render the block.
+ *
+ * @param {Injector} injector Injector.
+ * @param {any} block The block to render.
+ * @param {string} contextLevel The context where the block will be used.
+ * @param {number} instanceId The instance ID associated with the context level.
+ * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
+ */
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ : CoreBlockHandlerData | Promise {
+
+ return {
+ title: 'addon.block_comments.pluginname',
+ class: 'addon-block-comments',
+ component: CoreBlockOnlyTitleComponent,
+ link: 'CoreCommentsViewerPage',
+ linkParams: { contextLevel: contextLevel, instanceId: instanceId,
+ componentName: 'block_comments', area: 'page_comments', itemId: 0 }
+ };
+ }
+}
diff --git a/src/addon/block/completionstatus/completionstatus.module.ts b/src/addon/block/completionstatus/completionstatus.module.ts
new file mode 100644
index 00000000000..37d448a93b2
--- /dev/null
+++ b/src/addon/block/completionstatus/completionstatus.module.ts
@@ -0,0 +1,38 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreBlockDelegate } from '@core/block/providers/delegate';
+import { AddonBlockCompletionStatusHandler } from './providers/block-handler';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ IonicModule,
+ TranslateModule.forChild()
+ ],
+ exports: [
+ ],
+ providers: [
+ AddonBlockCompletionStatusHandler
+ ]
+})
+export class AddonBlockCompletionStatusModule {
+ constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockCompletionStatusHandler) {
+ blockDelegate.registerHandler(blockHandler);
+ }
+}
diff --git a/src/addon/block/completionstatus/lang/en.json b/src/addon/block/completionstatus/lang/en.json
new file mode 100644
index 00000000000..fe57356daba
--- /dev/null
+++ b/src/addon/block/completionstatus/lang/en.json
@@ -0,0 +1,3 @@
+{
+ "pluginname": "Course completion status"
+}
\ No newline at end of file
diff --git a/src/addon/block/completionstatus/providers/block-handler.ts b/src/addon/block/completionstatus/providers/block-handler.ts
new file mode 100644
index 00000000000..88fca4ec859
--- /dev/null
+++ b/src/addon/block/completionstatus/providers/block-handler.ts
@@ -0,0 +1,52 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+import { CoreBlockHandlerData } from '@core/block/providers/delegate';
+import { CoreBlockOnlyTitleComponent } from '@core/block/components/only-title-block/only-title-block';
+import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
+
+/**
+ * Block handler.
+ */
+@Injectable()
+export class AddonBlockCompletionStatusHandler extends CoreBlockBaseHandler {
+ name = 'AddonBlockCompletionStatus';
+ blockName = 'completionstatus';
+
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the data needed to render the block.
+ *
+ * @param {Injector} injector Injector.
+ * @param {any} block The block to render.
+ * @param {string} contextLevel The context where the block will be used.
+ * @param {number} instanceId The instance ID associated with the context level.
+ * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
+ */
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ : CoreBlockHandlerData | Promise {
+
+ return {
+ title: 'addon.block_completionstatus.pluginname',
+ class: 'addon-block-completion-status',
+ component: CoreBlockOnlyTitleComponent,
+ link: 'AddonCourseCompletionReportPage',
+ linkParams: { courseId: instanceId }
+ };
+ }
+}
diff --git a/src/addon/block/glossaryrandom/glossaryrandom.module.ts b/src/addon/block/glossaryrandom/glossaryrandom.module.ts
new file mode 100644
index 00000000000..e6d23895706
--- /dev/null
+++ b/src/addon/block/glossaryrandom/glossaryrandom.module.ts
@@ -0,0 +1,38 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreBlockDelegate } from '@core/block/providers/delegate';
+import { AddonBlockGlossaryRandomHandler } from './providers/block-handler';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ IonicModule,
+ TranslateModule.forChild()
+ ],
+ exports: [
+ ],
+ providers: [
+ AddonBlockGlossaryRandomHandler
+ ]
+})
+export class AddonBlockGlossaryRandomModule {
+ constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockGlossaryRandomHandler) {
+ blockDelegate.registerHandler(blockHandler);
+ }
+}
diff --git a/src/addon/block/glossaryrandom/lang/en.json b/src/addon/block/glossaryrandom/lang/en.json
new file mode 100644
index 00000000000..1ae4de38c6e
--- /dev/null
+++ b/src/addon/block/glossaryrandom/lang/en.json
@@ -0,0 +1,3 @@
+{
+ "pluginname": "Random glossary entry"
+}
\ No newline at end of file
diff --git a/src/addon/block/glossaryrandom/providers/block-handler.ts b/src/addon/block/glossaryrandom/providers/block-handler.ts
new file mode 100644
index 00000000000..d639ccb16fa
--- /dev/null
+++ b/src/addon/block/glossaryrandom/providers/block-handler.ts
@@ -0,0 +1,49 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+import { CoreBlockHandlerData } from '@core/block/providers/delegate';
+import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block';
+import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
+
+/**
+ * Block handler.
+ */
+@Injectable()
+export class AddonBlockGlossaryRandomHandler extends CoreBlockBaseHandler {
+ name = 'AddonBlockGlossaryRandom';
+ blockName = 'glossary_random';
+
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the data needed to render the block.
+ *
+ * @param {Injector} injector Injector.
+ * @param {any} block The block to render.
+ * @param {string} contextLevel The context where the block will be used.
+ * @param {number} instanceId The instance ID associated with the context level.
+ * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
+ */
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ : CoreBlockHandlerData | Promise {
+ return {
+ title: block.contents.title || 'addon.block_glossaryrandom.pluginname',
+ class: 'addon-block-glossary-random',
+ component: CoreBlockPreRenderedComponent
+ };
+ }
+}
diff --git a/src/addon/block/html/html.module.ts b/src/addon/block/html/html.module.ts
new file mode 100644
index 00000000000..1b81cf4adb1
--- /dev/null
+++ b/src/addon/block/html/html.module.ts
@@ -0,0 +1,36 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicModule } from 'ionic-angular';
+import { CoreBlockDelegate } from '@core/block/providers/delegate';
+import { AddonBlockHtmlHandler } from './providers/block-handler';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ IonicModule
+ ],
+ exports: [
+ ],
+ providers: [
+ AddonBlockHtmlHandler
+ ]
+})
+export class AddonBlockHtmlModule {
+ constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockHtmlHandler) {
+ blockDelegate.registerHandler(blockHandler);
+ }
+}
diff --git a/src/addon/block/html/providers/block-handler.ts b/src/addon/block/html/providers/block-handler.ts
new file mode 100644
index 00000000000..a5603fb53fc
--- /dev/null
+++ b/src/addon/block/html/providers/block-handler.ts
@@ -0,0 +1,50 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+import { CoreBlockHandlerData } from '@core/block/providers/delegate';
+import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block';
+import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
+
+/**
+ * Block handler.
+ */
+@Injectable()
+export class AddonBlockHtmlHandler extends CoreBlockBaseHandler {
+ name = 'AddonBlockHtml';
+ blockName = 'html';
+
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the data needed to render the block.
+ *
+ * @param {Injector} injector Injector.
+ * @param {any} block The block to render.
+ * @param {string} contextLevel The context where the block will be used.
+ * @param {number} instanceId The instance ID associated with the context level.
+ * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
+ */
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ : CoreBlockHandlerData | Promise {
+
+ return {
+ title: block.contents.title,
+ class: 'addon-block-html',
+ component: CoreBlockPreRenderedComponent
+ };
+ }
+}
diff --git a/src/addon/block/learningplans/lang/en.json b/src/addon/block/learningplans/lang/en.json
new file mode 100644
index 00000000000..0a7f81e2282
--- /dev/null
+++ b/src/addon/block/learningplans/lang/en.json
@@ -0,0 +1,3 @@
+{
+ "pluginname": "Learning plans"
+}
\ No newline at end of file
diff --git a/src/addon/block/learningplans/learningplans.module.ts b/src/addon/block/learningplans/learningplans.module.ts
new file mode 100644
index 00000000000..167178d7014
--- /dev/null
+++ b/src/addon/block/learningplans/learningplans.module.ts
@@ -0,0 +1,40 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreBlockDelegate } from '@core/block/providers/delegate';
+import { AddonBlockLearningPlansHandler } from './providers/block-handler';
+import { CoreBlockComponentsModule } from '@core/block/components/components.module';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ IonicModule,
+ CoreBlockComponentsModule,
+ TranslateModule.forChild()
+ ],
+ exports: [
+ ],
+ providers: [
+ AddonBlockLearningPlansHandler
+ ]
+})
+export class AddonBlockLearningPlansModule {
+ constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockLearningPlansHandler) {
+ blockDelegate.registerHandler(blockHandler);
+ }
+}
diff --git a/src/addon/block/learningplans/providers/block-handler.ts b/src/addon/block/learningplans/providers/block-handler.ts
new file mode 100644
index 00000000000..fa98be63855
--- /dev/null
+++ b/src/addon/block/learningplans/providers/block-handler.ts
@@ -0,0 +1,51 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+import { CoreBlockHandlerData } from '@core/block/providers/delegate';
+import { CoreBlockOnlyTitleComponent } from '@core/block/components/only-title-block/only-title-block';
+import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
+
+/**
+ * Block handler.
+ */
+@Injectable()
+export class AddonBlockLearningPlansHandler extends CoreBlockBaseHandler {
+ name = 'AddonBlockLearningPlans';
+ blockName = 'lp';
+
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the data needed to render the block.
+ *
+ * @param {Injector} injector Injector.
+ * @param {any} block The block to render.
+ * @param {string} contextLevel The context where the block will be used.
+ * @param {number} instanceId The instance ID associated with the context level.
+ * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
+ */
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ : CoreBlockHandlerData | Promise {
+
+ return {
+ title: 'addon.block_learningplans.pluginname',
+ class: 'addon-block-learning-plans',
+ component: CoreBlockOnlyTitleComponent,
+ link: 'AddonCompetencyPlanListPage'
+ };
+ }
+}
diff --git a/src/addon/block/myoverview/components/myoverview/addon-block-myoverview.html b/src/addon/block/myoverview/components/myoverview/addon-block-myoverview.html
index 598c204f0ed..294a499c44a 100644
--- a/src/addon/block/myoverview/components/myoverview/addon-block-myoverview.html
+++ b/src/addon/block/myoverview/components/myoverview/addon-block-myoverview.html
@@ -19,11 +19,11 @@ {{ 'addon.block_myoverview.pluginname' |Â translate }}
{{ 'addon.block_myoverview.all' | translate }} ∫
- {{ 'addon.block_myoverview.inprogress' | translate }}
- {{ 'addon.block_myoverview.future' | translate }}
- {{ 'addon.block_myoverview.past' | translate }}
- {{ 'addon.block_myoverview.favourites' | translate }}
- {{ 'addon.block_myoverview.hiddencourses' | translate }}
+ {{ 'addon.block_myoverview.inprogress' | translate }}
+ {{ 'addon.block_myoverview.future' | translate }}
+ {{ 'addon.block_myoverview.past' | translate }}
+ {{ 'addon.block_myoverview.favourites' | translate }}
+ {{ 'addon.block_myoverview.hiddencourses' | translate }}
diff --git a/src/addon/block/myoverview/components/myoverview/myoverview.ts b/src/addon/block/myoverview/components/myoverview/myoverview.ts
index b36202f7e09..ed68b920586 100644
--- a/src/addon/block/myoverview/components/myoverview/myoverview.ts
+++ b/src/addon/block/myoverview/components/myoverview/myoverview.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, OnInit, Input, OnDestroy, ViewChild, Injector } from '@angular/core';
+import { Component, OnInit, Input, OnDestroy, ViewChild, Injector, OnChanges, SimpleChange } from '@angular/core';
import { Searchbar } from 'ionic-angular';
import { CoreEventsProvider } from '@providers/events';
import { CoreUtilsProvider } from '@providers/utils/utils';
@@ -32,7 +32,7 @@ import { CoreBlockBaseComponent } from '@core/block/classes/base-block-component
selector: 'addon-block-myoverview',
templateUrl: 'addon-block-myoverview.html'
})
-export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implements OnInit, OnDestroy {
+export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implements OnInit, OnChanges, OnDestroy {
@ViewChild('searchbar') searchbar: Searchbar;
@Input() downloadEnabled: boolean;
@@ -64,10 +64,14 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
showSortFilter = false;
downloadCourseEnabled: boolean;
downloadCoursesEnabled: boolean;
+ disableInProgress = false;
+ disablePast = false;
+ disableFuture = false;
+ disableFavourite = false;
+ disableHidden = false;
protected prefetchIconsInitialized = false;
protected isDestroyed;
- protected downloadButtonObserver;
protected coursesObserver;
protected updateSiteObserver;
protected courseIds = [];
@@ -87,18 +91,6 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
*/
ngOnInit(): void {
// Refresh the enabled flags if enabled.
- this.downloadButtonObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED,
- (data) => {
- const wasEnabled = this.downloadEnabled;
-
- this.downloadEnabled = data.enabled;
-
- if (!wasEnabled && this.downloadEnabled && this.loaded) {
- // Download all courses is enabled now, initialize it.
- this.initPrefetchCoursesIcons();
- }
- });
-
this.downloadCourseEnabled = !this.coursesProvider.isDownloadCourseDisabledInSite();
this.downloadCoursesEnabled = !this.coursesProvider.isDownloadCoursesDisabledInSite();
@@ -128,6 +120,16 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
});
}
+ /**
+ * Detect changes on input properties.
+ */
+ ngOnChanges(changes: {[name: string]: SimpleChange}): void {
+ if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) {
+ // Download all courses is enabled now, initialize it.
+ this.initPrefetchCoursesIcons();
+ }
+ }
+
/**
* Perform the invalidate content function.
*
@@ -173,12 +175,17 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
this.courses.filter = '';
this.showFilter = false;
+ this.disableInProgress = this.courses.inprogress.length === 0;
+ this.disablePast = this.courses.past.length === 0;
+ this.disableFuture = this.courses.future.length === 0;
this.showSelectorFilter = courses.length > 0 && (this.courses.past.length > 0 || this.courses.future.length > 0 ||
- typeof courses[0].enddate != 'undefined');
+ typeof courses[0].enddate != 'undefined');
this.showHidden = this.showSelectorFilter && typeof courses[0].hidden != 'undefined';
+ this.disableHidden = this.courses.hidden.length === 0;
this.showFavourite = this.showSelectorFilter && typeof courses[0].isfavourite != 'undefined';
- if (!this.showSelectorFilter) {
- // No selector, show all.
+ this.disableFavourite = this.courses.favourite.length === 0;
+ if (!this.showSelectorFilter || (this.selectedFilter === 'inprogress' && this.disableInProgress)) {
+ // No selector, or the default option is disabled, show all.
this.selectedFilter = 'all';
}
this.filteredCourses = this.courses[this.selectedFilter];
@@ -350,6 +357,5 @@ export class AddonBlockMyOverviewComponent extends CoreBlockBaseComponent implem
this.isDestroyed = true;
this.coursesObserver && this.coursesObserver.off();
this.updateSiteObserver && this.updateSiteObserver.off();
- this.downloadButtonObserver && this.downloadButtonObserver.off();
}
}
diff --git a/src/addon/block/myoverview/providers/block-handler.ts b/src/addon/block/myoverview/providers/block-handler.ts
index 5f723467cfd..e0a593b9a18 100644
--- a/src/addon/block/myoverview/providers/block-handler.ts
+++ b/src/addon/block/myoverview/providers/block-handler.ts
@@ -50,7 +50,7 @@ export class AddonBlockMyOverviewHandler extends CoreBlockBaseHandler {
* @param {number} instanceId The instance ID associated with the context level.
* @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
*/
- getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
: CoreBlockHandlerData | Promise {
return {
diff --git a/src/addon/block/newsitems/lang/en.json b/src/addon/block/newsitems/lang/en.json
new file mode 100644
index 00000000000..83b98129739
--- /dev/null
+++ b/src/addon/block/newsitems/lang/en.json
@@ -0,0 +1,3 @@
+{
+ "pluginname": "Latest announcements"
+}
\ No newline at end of file
diff --git a/src/addon/block/newsitems/newsitems.module.ts b/src/addon/block/newsitems/newsitems.module.ts
new file mode 100644
index 00000000000..a3364ea5ea0
--- /dev/null
+++ b/src/addon/block/newsitems/newsitems.module.ts
@@ -0,0 +1,38 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreBlockDelegate } from '@core/block/providers/delegate';
+import { AddonBlockNewsItemsHandler } from './providers/block-handler';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ IonicModule,
+ TranslateModule.forChild()
+ ],
+ exports: [
+ ],
+ providers: [
+ AddonBlockNewsItemsHandler
+ ]
+})
+export class AddonBlockNewsItemsModule {
+ constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockNewsItemsHandler) {
+ blockDelegate.registerHandler(blockHandler);
+ }
+}
diff --git a/src/addon/block/newsitems/newsitems.scss b/src/addon/block/newsitems/newsitems.scss
new file mode 100644
index 00000000000..8b0c46287b9
--- /dev/null
+++ b/src/addon/block/newsitems/newsitems.scss
@@ -0,0 +1,26 @@
+.addon-block-news-items core-block-pre-rendered {
+ .core-block-content {
+ .unlist {
+ list-style-type: none;
+ @include margin-horizontal(0);
+ -webkit-padding-start: 0;
+
+ li.post {
+ padding-bottom: 16px;
+ }
+ li.post:last-child {
+ padding-bottom: 0;
+ }
+ }
+ }
+
+ // Hide RSS link.
+ .core-block-footer {
+ a {
+ display: none;
+ }
+ a:first-child {
+ display: inline;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/addon/block/newsitems/providers/block-handler.ts b/src/addon/block/newsitems/providers/block-handler.ts
new file mode 100644
index 00000000000..c077474d8a0
--- /dev/null
+++ b/src/addon/block/newsitems/providers/block-handler.ts
@@ -0,0 +1,49 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+import { CoreBlockHandlerData } from '@core/block/providers/delegate';
+import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block';
+import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
+
+/**
+ * Block handler.
+ */
+@Injectable()
+export class AddonBlockNewsItemsHandler extends CoreBlockBaseHandler {
+ name = 'AddonBlockNewsItems';
+ blockName = 'news_items';
+
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the data needed to render the block.
+ *
+ * @param {Injector} injector Injector.
+ * @param {any} block The block to render.
+ * @param {string} contextLevel The context where the block will be used.
+ * @param {number} instanceId The instance ID associated with the context level.
+ * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
+ */
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ : CoreBlockHandlerData | Promise {
+ return {
+ title: 'addon.block_newsitems.pluginname',
+ class: 'addon-block-news-items',
+ component: CoreBlockPreRenderedComponent
+ };
+ }
+}
diff --git a/src/addon/block/onlineusers/lang/en.json b/src/addon/block/onlineusers/lang/en.json
new file mode 100644
index 00000000000..4bc6cd41263
--- /dev/null
+++ b/src/addon/block/onlineusers/lang/en.json
@@ -0,0 +1,3 @@
+{
+ "pluginname": "Online users"
+}
\ No newline at end of file
diff --git a/src/addon/block/onlineusers/onlineusers.module.ts b/src/addon/block/onlineusers/onlineusers.module.ts
new file mode 100644
index 00000000000..0df61ad7a1f
--- /dev/null
+++ b/src/addon/block/onlineusers/onlineusers.module.ts
@@ -0,0 +1,38 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreBlockDelegate } from '@core/block/providers/delegate';
+import { AddonBlockOnlineUsersHandler } from './providers/block-handler';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ IonicModule,
+ TranslateModule.forChild()
+ ],
+ exports: [
+ ],
+ providers: [
+ AddonBlockOnlineUsersHandler
+ ]
+})
+export class AddonBlockOnlineUsersModule {
+ constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockOnlineUsersHandler) {
+ blockDelegate.registerHandler(blockHandler);
+ }
+}
diff --git a/src/addon/block/onlineusers/onlineusers.scss b/src/addon/block/onlineusers/onlineusers.scss
new file mode 100644
index 00000000000..009a347b807
--- /dev/null
+++ b/src/addon/block/onlineusers/onlineusers.scss
@@ -0,0 +1,48 @@
+.addon-block-online-users core-block-pre-rendered .core-block-content {
+ max-height: 200px;
+ overflow-y: auto;
+ .item-inner,
+ .input-wrapper {
+ overflow-y: visible;
+ align-self: start;
+ }
+
+ .list {
+ @include margin-horizontal(0);
+ -webkit-padding-start: 0;
+
+ li.listentry {
+ clear: both;
+ list-style-type: none;
+
+ .user {
+ @include float(start);
+ position: relative;
+ padding-bottom: 16px;
+
+ .core-adapted-img-container {
+ display: inline;
+ @include margin-horizontal(0, 8px);
+ }
+
+ .userpicture {
+ vertical-align: text-bottom;
+ }
+ }
+
+ .message {
+ @include float(end);
+ margin-top: 3px;
+ }
+
+ .uservisibility { // No support on the app.
+ display: none;
+ }
+ }
+ }
+
+ .info {
+ text-align: center;
+ }
+
+}
\ No newline at end of file
diff --git a/src/addon/block/onlineusers/providers/block-handler.ts b/src/addon/block/onlineusers/providers/block-handler.ts
new file mode 100644
index 00000000000..353835c830c
--- /dev/null
+++ b/src/addon/block/onlineusers/providers/block-handler.ts
@@ -0,0 +1,49 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+import { CoreBlockHandlerData } from '@core/block/providers/delegate';
+import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block';
+import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
+
+/**
+ * Block handler.
+ */
+@Injectable()
+export class AddonBlockOnlineUsersHandler extends CoreBlockBaseHandler {
+ name = 'AddonBlockOnlineUsers';
+ blockName = 'online_users';
+
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the data needed to render the block.
+ *
+ * @param {Injector} injector Injector.
+ * @param {any} block The block to render.
+ * @param {string} contextLevel The context where the block will be used.
+ * @param {number} instanceId The instance ID associated with the context level.
+ * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
+ */
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ : CoreBlockHandlerData | Promise {
+ return {
+ title: 'addon.block_onlineusers.pluginname',
+ class: 'addon-block-online-users',
+ component: CoreBlockPreRenderedComponent
+ };
+ }
+}
diff --git a/src/addon/block/privatefiles/lang/en.json b/src/addon/block/privatefiles/lang/en.json
new file mode 100644
index 00000000000..bba9d4bc04c
--- /dev/null
+++ b/src/addon/block/privatefiles/lang/en.json
@@ -0,0 +1,3 @@
+{
+ "pluginname": "Private files"
+}
\ No newline at end of file
diff --git a/src/addon/block/privatefiles/privatefiles.module.ts b/src/addon/block/privatefiles/privatefiles.module.ts
new file mode 100644
index 00000000000..c9617c3724c
--- /dev/null
+++ b/src/addon/block/privatefiles/privatefiles.module.ts
@@ -0,0 +1,38 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreBlockDelegate } from '@core/block/providers/delegate';
+import { AddonBlockPrivateFilesHandler } from './providers/block-handler';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ IonicModule,
+ TranslateModule.forChild()
+ ],
+ exports: [
+ ],
+ providers: [
+ AddonBlockPrivateFilesHandler
+ ]
+})
+export class AddonBlockPrivateFilesModule {
+ constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockPrivateFilesHandler) {
+ blockDelegate.registerHandler(blockHandler);
+ }
+}
diff --git a/src/addon/block/privatefiles/providers/block-handler.ts b/src/addon/block/privatefiles/providers/block-handler.ts
new file mode 100644
index 00000000000..f0284df9747
--- /dev/null
+++ b/src/addon/block/privatefiles/providers/block-handler.ts
@@ -0,0 +1,52 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+import { CoreBlockHandlerData } from '@core/block/providers/delegate';
+import { CoreBlockOnlyTitleComponent } from '@core/block/components/only-title-block/only-title-block';
+import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
+
+/**
+ * Block handler.
+ */
+@Injectable()
+export class AddonBlockPrivateFilesHandler extends CoreBlockBaseHandler {
+ name = 'AddonBlockPrivateFiles';
+ blockName = 'private_files';
+
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the data needed to render the block.
+ *
+ * @param {Injector} injector Injector.
+ * @param {any} block The block to render.
+ * @param {string} contextLevel The context where the block will be used.
+ * @param {number} instanceId The instance ID associated with the context level.
+ * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
+ */
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ : CoreBlockHandlerData | Promise {
+
+ return {
+ title: 'addon.block_privatefiles.pluginname',
+ class: 'addon-block-private-files',
+ component: CoreBlockOnlyTitleComponent,
+ link: 'AddonFilesListPage',
+ linkParams: {root: 'my'}
+ };
+ }
+}
diff --git a/src/addon/block/recentactivity/lang/en.json b/src/addon/block/recentactivity/lang/en.json
new file mode 100644
index 00000000000..29f7996e2a6
--- /dev/null
+++ b/src/addon/block/recentactivity/lang/en.json
@@ -0,0 +1,3 @@
+{
+ "pluginname": "Recent activity"
+}
\ No newline at end of file
diff --git a/src/addon/block/recentactivity/providers/block-handler.ts b/src/addon/block/recentactivity/providers/block-handler.ts
new file mode 100644
index 00000000000..ac69af02b3f
--- /dev/null
+++ b/src/addon/block/recentactivity/providers/block-handler.ts
@@ -0,0 +1,51 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+
+import { CoreBlockHandlerData } from '@core/block/providers/delegate';
+import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block';
+import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
+
+/**
+ * Block handler.
+ */
+@Injectable()
+export class AddonBlockRecentActivityHandler extends CoreBlockBaseHandler {
+ name = 'AddonBlockRecentActivity';
+ blockName = 'recent_activity';
+
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the data needed to render the block.
+ *
+ * @param {Injector} injector Injector.
+ * @param {any} block The block to render.
+ * @param {string} contextLevel The context where the block will be used.
+ * @param {number} instanceId The instance ID associated with the context level.
+ * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
+ */
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ : CoreBlockHandlerData | Promise {
+
+ return {
+ title: 'addon.block_recentactivity.pluginname',
+ class: 'addon-block-recent-activity',
+ component: CoreBlockPreRenderedComponent
+ };
+ }
+}
diff --git a/src/addon/block/recentactivity/recentactivity.module.ts b/src/addon/block/recentactivity/recentactivity.module.ts
new file mode 100644
index 00000000000..cd0136763ef
--- /dev/null
+++ b/src/addon/block/recentactivity/recentactivity.module.ts
@@ -0,0 +1,38 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreBlockDelegate } from '@core/block/providers/delegate';
+import { AddonBlockRecentActivityHandler } from './providers/block-handler';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ IonicModule,
+ TranslateModule.forChild()
+ ],
+ exports: [
+ ],
+ providers: [
+ AddonBlockRecentActivityHandler
+ ]
+})
+export class AddonBlockRecentActivityModule {
+ constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockRecentActivityHandler) {
+ blockDelegate.registerHandler(blockHandler);
+ }
+}
diff --git a/src/addon/block/recentactivity/recentactivity.scss b/src/addon/block/recentactivity/recentactivity.scss
new file mode 100644
index 00000000000..0eb73e10255
--- /dev/null
+++ b/src/addon/block/recentactivity/recentactivity.scss
@@ -0,0 +1,20 @@
+.addon-block-recent-activity core-block-pre-rendered {
+ .core-block-content {
+ .activitydate, .activityhead {
+ text-align: center;
+ }
+
+ .unlist {
+ list-style: none;
+ @include margin-horizontal(0);
+ -webkit-padding-start: 0;
+ li {
+ margin-bottom: 1em;
+
+ .head .date {
+ @include float(end);
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/addon/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts b/src/addon/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts
index 2d962d17cfc..92697e4f0b7 100644
--- a/src/addon/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts
+++ b/src/addon/block/recentlyaccessedcourses/components/recentlyaccessedcourses/recentlyaccessedcourses.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, OnInit, OnDestroy, Injector, Input } from '@angular/core';
+import { Component, OnInit, OnDestroy, Injector, Input, OnChanges, SimpleChange } from '@angular/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreSitesProvider } from '@providers/sites';
@@ -30,7 +30,7 @@ import { CoreBlockBaseComponent } from '@core/block/classes/base-block-component
selector: 'addon-block-recentlyaccessedcourses',
templateUrl: 'addon-block-recentlyaccessedcourses.html'
})
-export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnDestroy {
+export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnChanges, OnDestroy {
@Input() downloadEnabled: boolean;
courses = [];
@@ -41,7 +41,6 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom
protected prefetchIconsInitialized = false;
protected isDestroyed;
- protected downloadButtonObserver;
protected coursesObserver;
protected courseIds = [];
protected fetchContentDefaultError = 'Error getting recent courses data.';
@@ -59,18 +58,6 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom
* Component being initialized.
*/
ngOnInit(): void {
- // Refresh the enabled flags if enabled.
- this.downloadButtonObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED,
- (data) => {
- const wasEnabled = this.downloadEnabled;
-
- this.downloadEnabled = data.enabled;
-
- if (!wasEnabled && this.downloadEnabled && this.loaded) {
- // Download all courses is enabled now, initialize it.
- this.initPrefetchCoursesIcons();
- }
- });
this.coursesObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, () => {
this.refreshContent();
@@ -79,6 +66,16 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom
super.ngOnInit();
}
+ /**
+ * Detect changes on input properties.
+ */
+ ngOnChanges(changes: {[name: string]: SimpleChange}): void {
+ if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) {
+ // Download all courses is enabled now, initialize it.
+ this.initPrefetchCoursesIcons();
+ }
+ }
+
/**
* Perform the invalidate content function.
*
@@ -155,6 +152,5 @@ export class AddonBlockRecentlyAccessedCoursesComponent extends CoreBlockBaseCom
ngOnDestroy(): void {
this.isDestroyed = true;
this.coursesObserver && this.coursesObserver.off();
- this.downloadButtonObserver && this.downloadButtonObserver.off();
}
}
diff --git a/src/addon/block/recentlyaccessedcourses/providers/block-handler.ts b/src/addon/block/recentlyaccessedcourses/providers/block-handler.ts
index a1117bd5d6b..3af9cf0dd36 100644
--- a/src/addon/block/recentlyaccessedcourses/providers/block-handler.ts
+++ b/src/addon/block/recentlyaccessedcourses/providers/block-handler.ts
@@ -38,7 +38,7 @@ export class AddonBlockRecentlyAccessedCoursesHandler extends CoreBlockBaseHandl
* @param {number} instanceId The instance ID associated with the context level.
* @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
*/
- getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
: CoreBlockHandlerData | Promise {
return {
diff --git a/src/addon/block/recentlyaccesseditems/providers/block-handler.ts b/src/addon/block/recentlyaccesseditems/providers/block-handler.ts
index 2963017e876..dfadcc54271 100644
--- a/src/addon/block/recentlyaccesseditems/providers/block-handler.ts
+++ b/src/addon/block/recentlyaccesseditems/providers/block-handler.ts
@@ -38,7 +38,7 @@ export class AddonBlockRecentlyAccessedItemsHandler extends CoreBlockBaseHandler
* @param {number} instanceId The instance ID associated with the context level.
* @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
*/
- getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
: CoreBlockHandlerData | Promise {
return {
diff --git a/src/addon/block/rssclient/lang/en.json b/src/addon/block/rssclient/lang/en.json
new file mode 100644
index 00000000000..18282971b14
--- /dev/null
+++ b/src/addon/block/rssclient/lang/en.json
@@ -0,0 +1,3 @@
+{
+ "pluginname": "Remote RSS feeds"
+}
\ No newline at end of file
diff --git a/src/addon/block/rssclient/providers/block-handler.ts b/src/addon/block/rssclient/providers/block-handler.ts
new file mode 100644
index 00000000000..8976f218250
--- /dev/null
+++ b/src/addon/block/rssclient/providers/block-handler.ts
@@ -0,0 +1,51 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+
+import { CoreBlockHandlerData } from '@core/block/providers/delegate';
+import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block';
+import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
+
+/**
+ * Block handler.
+ */
+@Injectable()
+export class AddonBlockRssClientHandler extends CoreBlockBaseHandler {
+ name = 'AddonBlockRssClient';
+ blockName = 'rss_client';
+
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the data needed to render the block.
+ *
+ * @param {Injector} injector Injector.
+ * @param {any} block The block to render.
+ * @param {string} contextLevel The context where the block will be used.
+ * @param {number} instanceId The instance ID associated with the context level.
+ * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
+ */
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ : CoreBlockHandlerData | Promise {
+
+ return {
+ title: block.contents.title || 'addon.block_rssclient.pluginname',
+ class: 'addon-block-rss-client',
+ component: CoreBlockPreRenderedComponent
+ };
+ }
+}
diff --git a/src/addon/block/rssclient/rssclient.module.ts b/src/addon/block/rssclient/rssclient.module.ts
new file mode 100644
index 00000000000..b98c39ae544
--- /dev/null
+++ b/src/addon/block/rssclient/rssclient.module.ts
@@ -0,0 +1,38 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreBlockDelegate } from '@core/block/providers/delegate';
+import { AddonBlockRssClientHandler } from './providers/block-handler';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ IonicModule,
+ TranslateModule.forChild()
+ ],
+ exports: [
+ ],
+ providers: [
+ AddonBlockRssClientHandler
+ ]
+})
+export class AddonBlockRssClientModule {
+ constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockRssClientHandler) {
+ blockDelegate.registerHandler(blockHandler);
+ }
+}
diff --git a/src/addon/block/rssclient/rssclient.scss b/src/addon/block/rssclient/rssclient.scss
new file mode 100644
index 00000000000..33b37b0e75d
--- /dev/null
+++ b/src/addon/block/rssclient/rssclient.scss
@@ -0,0 +1,19 @@
+.addon-block-rss-client core-block-pre-rendered {
+ .core-block-content {
+ .list {
+ list-style: none;
+ @include margin-horizontal(0);
+ -webkit-padding-start: 0;
+
+ li {
+ border-top: 1px solid $gray;
+ padding: 5px;
+ padding-bottom: 8px;
+ }
+
+ li:first-child {
+ border-top-width: 0;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/addon/block/selfcompletion/lang/en.json b/src/addon/block/selfcompletion/lang/en.json
new file mode 100644
index 00000000000..32521695a4b
--- /dev/null
+++ b/src/addon/block/selfcompletion/lang/en.json
@@ -0,0 +1,3 @@
+{
+ "pluginname": "Self completion"
+}
\ No newline at end of file
diff --git a/src/addon/block/selfcompletion/providers/block-handler.ts b/src/addon/block/selfcompletion/providers/block-handler.ts
new file mode 100644
index 00000000000..57d716e5a41
--- /dev/null
+++ b/src/addon/block/selfcompletion/providers/block-handler.ts
@@ -0,0 +1,52 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+import { CoreBlockHandlerData } from '@core/block/providers/delegate';
+import { CoreBlockOnlyTitleComponent } from '@core/block/components/only-title-block/only-title-block';
+import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
+
+/**
+ * Block handler.
+ */
+@Injectable()
+export class AddonBlockSelfCompletionHandler extends CoreBlockBaseHandler {
+ name = 'AddonBlockSelfCompletion';
+ blockName = 'selfcompletion';
+
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the data needed to render the block.
+ *
+ * @param {Injector} injector Injector.
+ * @param {any} block The block to render.
+ * @param {string} contextLevel The context where the block will be used.
+ * @param {number} instanceId The instance ID associated with the context level.
+ * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
+ */
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ : CoreBlockHandlerData | Promise {
+
+ return {
+ title: 'addon.block_selfcompletion.pluginname',
+ class: 'addon-block-self-completion',
+ component: CoreBlockOnlyTitleComponent,
+ link: 'AddonCourseCompletionReportPage',
+ linkParams: { courseId: instanceId }
+ };
+ }
+}
diff --git a/src/addon/block/selfcompletion/selfcompletion.module.ts b/src/addon/block/selfcompletion/selfcompletion.module.ts
new file mode 100644
index 00000000000..b101210b2d7
--- /dev/null
+++ b/src/addon/block/selfcompletion/selfcompletion.module.ts
@@ -0,0 +1,38 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreBlockDelegate } from '@core/block/providers/delegate';
+import { AddonBlockSelfCompletionHandler } from './providers/block-handler';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ IonicModule,
+ TranslateModule.forChild()
+ ],
+ exports: [
+ ],
+ providers: [
+ AddonBlockSelfCompletionHandler
+ ]
+})
+export class AddonBlockSelfCompletionModule {
+ constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockSelfCompletionHandler) {
+ blockDelegate.registerHandler(blockHandler);
+ }
+}
diff --git a/src/addon/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html b/src/addon/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html
index e1a3c10a527..f9a5b7f825d 100644
--- a/src/addon/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html
+++ b/src/addon/block/sitemainmenu/components/sitemainmenu/addon-block-sitemainmenu.html
@@ -2,9 +2,11 @@
{{ 'addon.block_sitemainmenu.pluginname' |Â translate }}
-
-
-
+
+
+
+
-
+
+
diff --git a/src/addon/block/sitemainmenu/components/sitemainmenu/sitemainmenu.ts b/src/addon/block/sitemainmenu/components/sitemainmenu/sitemainmenu.ts
index 0f335521904..23bfb74ca8f 100644
--- a/src/addon/block/sitemainmenu/components/sitemainmenu/sitemainmenu.ts
+++ b/src/addon/block/sitemainmenu/components/sitemainmenu/sitemainmenu.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, OnInit, Injector } from '@angular/core';
+import { Component, OnInit, Injector, Input } from '@angular/core';
import { CoreSitesProvider } from '@providers/sites';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
@@ -28,7 +28,9 @@ import { CoreBlockBaseComponent } from '@core/block/classes/base-block-component
templateUrl: 'addon-block-sitemainmenu.html'
})
export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent implements OnInit {
- block: any;
+ @Input() downloadEnabled: boolean;
+
+ mainMenuBlock: any;
siteHomeId: number;
protected fetchContentDefaultError = 'Error getting main menu data.';
@@ -60,9 +62,9 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
promises.push(this.courseProvider.invalidateSections(this.siteHomeId));
promises.push(this.siteHomeProvider.invalidateNewsForum(this.siteHomeId));
- if (this.block && this.block.modules) {
+ if (this.mainMenuBlock && this.mainMenuBlock.modules) {
// Invalidate modules prefetch data.
- promises.push(this.prefetchDelegate.invalidateModules(this.block.modules, this.siteHomeId));
+ promises.push(this.prefetchDelegate.invalidateModules(this.mainMenuBlock.modules, this.siteHomeId));
}
return Promise.all(promises);
@@ -75,11 +77,11 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
*/
protected fetchContent(): Promise {
return this.courseProvider.getSections(this.siteHomeId, false, true).then((sections) => {
- this.block = sections.find((section) => section.section == 0);
+ this.mainMenuBlock = sections.find((section) => section.section == 0);
- if (this.block) {
- this.block.hasContent = this.courseHelper.sectionHasContent(this.block);
- this.courseHelper.addHandlerDataForModules([this.block], this.siteHomeId);
+ if (this.mainMenuBlock) {
+ this.mainMenuBlock.hasContent = this.courseHelper.sectionHasContent(this.mainMenuBlock);
+ this.courseHelper.addHandlerDataForModules([this.mainMenuBlock], this.siteHomeId);
// Check if Site Home displays announcements. If so, remove it from the main menu block.
const currentSite = this.sitesProvider.getCurrentSite(),
@@ -92,15 +94,15 @@ export class AddonBlockSiteMainMenuComponent extends CoreBlockBaseComponent impl
hasNewsItem = items.find((item) => { return item == '0'; });
}
- if (hasNewsItem && this.block.modules) {
+ if (hasNewsItem && this.mainMenuBlock.modules) {
// Remove forum activity (news one only) from the main menu block to prevent duplicates.
return this.siteHomeProvider.getNewsForum(this.siteHomeId).then((forum) => {
// Search the module that belongs to site news.
- for (let i = 0; i < this.block.modules.length; i++) {
- const module = this.block.modules[i];
+ for (let i = 0; i < this.mainMenuBlock.modules.length; i++) {
+ const module = this.mainMenuBlock.modules[i];
if (module.modname == 'forum' && module.instance == forum.id) {
- this.block.modules.splice(i, 1);
+ this.mainMenuBlock.modules.splice(i, 1);
break;
}
}
diff --git a/src/addon/block/sitemainmenu/providers/block-handler.ts b/src/addon/block/sitemainmenu/providers/block-handler.ts
index 8c699aff0ea..dcaf4fe01a4 100644
--- a/src/addon/block/sitemainmenu/providers/block-handler.ts
+++ b/src/addon/block/sitemainmenu/providers/block-handler.ts
@@ -38,7 +38,7 @@ export class AddonBlockSiteMainMenuHandler extends CoreBlockBaseHandler {
* @param {number} instanceId The instance ID associated with the context level.
* @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
*/
- getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
: CoreBlockHandlerData | Promise {
return {
diff --git a/src/addon/block/starredcourses/components/starredcourses/starredcourses.ts b/src/addon/block/starredcourses/components/starredcourses/starredcourses.ts
index ffa235134ec..ca25f3354be 100644
--- a/src/addon/block/starredcourses/components/starredcourses/starredcourses.ts
+++ b/src/addon/block/starredcourses/components/starredcourses/starredcourses.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, OnInit, OnDestroy, Injector, Input } from '@angular/core';
+import { Component, OnInit, OnDestroy, Injector, Input, OnChanges, SimpleChange } from '@angular/core';
import { CoreEventsProvider } from '@providers/events';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreSitesProvider } from '@providers/sites';
@@ -30,7 +30,7 @@ import { CoreBlockBaseComponent } from '@core/block/classes/base-block-component
selector: 'addon-block-starredcourses',
templateUrl: 'addon-block-starredcourses.html'
})
-export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnDestroy {
+export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent implements OnInit, OnChanges, OnDestroy {
@Input() downloadEnabled: boolean;
courses = [];
@@ -41,7 +41,6 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im
protected prefetchIconsInitialized = false;
protected isDestroyed;
- protected downloadButtonObserver;
protected coursesObserver;
protected courseIds = [];
protected fetchContentDefaultError = 'Error getting starred courses data.';
@@ -59,18 +58,6 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im
* Component being initialized.
*/
ngOnInit(): void {
- // Refresh the enabled flags if enabled.
- this.downloadButtonObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_DASHBOARD_DOWNLOAD_ENABLED_CHANGED,
- (data) => {
- const wasEnabled = this.downloadEnabled;
-
- this.downloadEnabled = data.enabled;
-
- if (!wasEnabled && this.downloadEnabled && this.loaded) {
- // Download all courses is enabled now, initialize it.
- this.initPrefetchCoursesIcons();
- }
- });
this.coursesObserver = this.eventsProvider.on(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, () => {
this.refreshContent();
@@ -79,6 +66,16 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im
super.ngOnInit();
}
+ /**
+ * Detect changes on input properties.
+ */
+ ngOnChanges(changes: {[name: string]: SimpleChange}): void {
+ if (changes.downloadEnabled && !changes.downloadEnabled.previousValue && this.downloadEnabled && this.loaded) {
+ // Download all courses is enabled now, initialize it.
+ this.initPrefetchCoursesIcons();
+ }
+ }
+
/**
* Perform the invalidate content function.
*
@@ -155,6 +152,5 @@ export class AddonBlockStarredCoursesComponent extends CoreBlockBaseComponent im
ngOnDestroy(): void {
this.isDestroyed = true;
this.coursesObserver && this.coursesObserver.off();
- this.downloadButtonObserver && this.downloadButtonObserver.off();
}
}
diff --git a/src/addon/block/starredcourses/providers/block-handler.ts b/src/addon/block/starredcourses/providers/block-handler.ts
index 7675876b127..c7abc34854a 100644
--- a/src/addon/block/starredcourses/providers/block-handler.ts
+++ b/src/addon/block/starredcourses/providers/block-handler.ts
@@ -38,7 +38,7 @@ export class AddonBlockStarredCoursesHandler extends CoreBlockBaseHandler {
* @param {number} instanceId The instance ID associated with the context level.
* @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
*/
- getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
: CoreBlockHandlerData | Promise {
return {
diff --git a/src/addon/block/tags/lang/en.json b/src/addon/block/tags/lang/en.json
new file mode 100644
index 00000000000..a4080dd788f
--- /dev/null
+++ b/src/addon/block/tags/lang/en.json
@@ -0,0 +1,3 @@
+{
+ "pluginname": "Tags"
+}
\ No newline at end of file
diff --git a/src/addon/block/tags/providers/block-handler.ts b/src/addon/block/tags/providers/block-handler.ts
new file mode 100644
index 00000000000..f1c7d4cd83e
--- /dev/null
+++ b/src/addon/block/tags/providers/block-handler.ts
@@ -0,0 +1,51 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+
+import { CoreBlockHandlerData } from '@core/block/providers/delegate';
+import { CoreBlockPreRenderedComponent } from '@core/block/components/pre-rendered-block/pre-rendered-block';
+import { CoreBlockBaseHandler } from '@core/block/classes/base-block-handler';
+
+/**
+ * Block handler.
+ */
+@Injectable()
+export class AddonBlockTagsHandler extends CoreBlockBaseHandler {
+ name = 'AddonBlockTags';
+ blockName = 'tags';
+
+ constructor() {
+ super();
+ }
+
+ /**
+ * Returns the data needed to render the block.
+ *
+ * @param {Injector} injector Injector.
+ * @param {any} block The block to render.
+ * @param {string} contextLevel The context where the block will be used.
+ * @param {number} instanceId The instance ID associated with the context level.
+ * @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
+ */
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ : CoreBlockHandlerData | Promise {
+
+ return {
+ title: 'addon.block_tags.pluginname',
+ class: 'addon-block-tags',
+ component: CoreBlockPreRenderedComponent
+ };
+ }
+}
diff --git a/src/addon/block/tags/tags.module.ts b/src/addon/block/tags/tags.module.ts
new file mode 100644
index 00000000000..4b57911c0f0
--- /dev/null
+++ b/src/addon/block/tags/tags.module.ts
@@ -0,0 +1,38 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreBlockDelegate } from '@core/block/providers/delegate';
+import { AddonBlockTagsHandler } from './providers/block-handler';
+
+@NgModule({
+ declarations: [
+ ],
+ imports: [
+ IonicModule,
+ TranslateModule.forChild()
+ ],
+ exports: [
+ ],
+ providers: [
+ AddonBlockTagsHandler
+ ]
+})
+export class AddonBlockTagsModule {
+ constructor(blockDelegate: CoreBlockDelegate, blockHandler: AddonBlockTagsHandler) {
+ blockDelegate.registerHandler(blockHandler);
+ }
+}
diff --git a/src/addon/block/tags/tags.scss b/src/addon/block/tags/tags.scss
new file mode 100644
index 00000000000..f4c54d1673c
--- /dev/null
+++ b/src/addon/block/tags/tags.scss
@@ -0,0 +1,106 @@
+.addon-block-tags core-block-pre-rendered {
+ .core-block-content {
+ .tag_cloud {
+ text-align: center;
+ ul.inline-list {
+ list-style: none;
+ @include margin-horizontal(0);
+ -webkit-padding-start: 0;
+
+ li {
+ padding: .2em;
+ display: inline-block;
+
+ a {
+ @extend ion-badge;
+ @extend .badge-md;
+ text-decoration: none;
+ }
+ .s20 {
+ font-size: 2.7em;
+ }
+
+ .s19 {
+ font-size: 2.6em;
+ }
+
+ .s18 {
+ font-size: 2.5em;
+ }
+
+ .s17 {
+ font-size: 2.4em;
+ }
+
+ .s16 {
+ font-size: 2.3em;
+ }
+
+ .s15 {
+ font-size: 2.2em;
+ }
+
+ .s14 {
+ font-size: 2.1em;
+ }
+
+ .s13 {
+ font-size: 2em;
+ }
+
+ .s12 {
+ font-size: 1.9em;
+ }
+
+ .s11 {
+ font-size: 1.8em;
+ }
+
+ .s10 {
+ font-size: 1.7em;
+ }
+
+ .s9 {
+ font-size: 1.6em;
+ }
+
+ .s8 {
+ font-size: 1.5em;
+ }
+
+ .s7 {
+ font-size: 1.4em;
+ }
+
+ .s6 {
+ font-size: 1.3em;
+ }
+
+ .s5 {
+ font-size: 1.2em;
+ }
+
+ .s4 {
+ font-size: 1.1em;
+ }
+
+ .s3 {
+ font-size: 1em;
+ }
+
+ .s2 {
+ font-size: 0.9em;
+ }
+
+ .s1 {
+ font-size: 0.8em;
+ }
+
+ .s0 {
+ font-size: 0.7em;
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/addon/block/timeline/providers/block-handler.ts b/src/addon/block/timeline/providers/block-handler.ts
index 2e89d60fbe5..8973fbc5dae 100644
--- a/src/addon/block/timeline/providers/block-handler.ts
+++ b/src/addon/block/timeline/providers/block-handler.ts
@@ -55,7 +55,7 @@ export class AddonBlockTimelineHandler extends CoreBlockBaseHandler {
* @param {number} instanceId The instance ID associated with the context level.
* @return {CoreBlockHandlerData|Promise} Data or promise resolved with the data.
*/
- getDisplayData?(injector: Injector, block: any, contextLevel: string, instanceId: number)
+ getDisplayData(injector: Injector, block: any, contextLevel: string, instanceId: number)
: CoreBlockHandlerData | Promise {
return {
diff --git a/src/addon/blog/blog.module.ts b/src/addon/blog/blog.module.ts
index f3137274535..b697ba27a3d 100644
--- a/src/addon/blog/blog.module.ts
+++ b/src/addon/blog/blog.module.ts
@@ -17,12 +17,14 @@ import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate';
import { CoreUserDelegate } from '@core/user/providers/user-delegate';
import { CoreCourseOptionsDelegate } from '@core/course/providers/options-delegate';
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
+import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate';
import { AddonBlogProvider } from './providers/blog';
import { AddonBlogMainMenuHandler } from './providers/mainmenu-handler';
import { AddonBlogUserHandler } from './providers/user-handler';
import { AddonBlogCourseOptionHandler } from './providers/course-option-handler';
import { AddonBlogComponentsModule } from './components/components.module';
import { AddonBlogIndexLinkHandler } from './providers/index-link-handler';
+import { AddonBlogTagAreaHandler } from './providers/tag-area-handler';
@NgModule({
declarations: [
@@ -35,17 +37,20 @@ import { AddonBlogIndexLinkHandler } from './providers/index-link-handler';
AddonBlogMainMenuHandler,
AddonBlogUserHandler,
AddonBlogCourseOptionHandler,
- AddonBlogIndexLinkHandler
+ AddonBlogIndexLinkHandler,
+ AddonBlogTagAreaHandler
]
})
export class AddonBlogModule {
constructor(mainMenuDelegate: CoreMainMenuDelegate, menuHandler: AddonBlogMainMenuHandler,
userHandler: AddonBlogUserHandler, userDelegate: CoreUserDelegate,
courseOptionHandler: AddonBlogCourseOptionHandler, courseOptionsDelegate: CoreCourseOptionsDelegate,
- linkHandler: AddonBlogIndexLinkHandler, contentLinksDelegate: CoreContentLinksDelegate) {
+ linkHandler: AddonBlogIndexLinkHandler, contentLinksDelegate: CoreContentLinksDelegate,
+ tagAreaDelegate: CoreTagAreaDelegate, tagAreaHandler: AddonBlogTagAreaHandler) {
mainMenuDelegate.registerHandler(menuHandler);
userDelegate.registerHandler(userHandler);
courseOptionsDelegate.registerHandler(courseOptionHandler);
contentLinksDelegate.registerHandler(linkHandler);
+ tagAreaDelegate.registerHandler(tagAreaHandler);
}
}
diff --git a/src/addon/blog/components/components.module.ts b/src/addon/blog/components/components.module.ts
index 0e56fcc3fda..efba08d7d7b 100644
--- a/src/addon/blog/components/components.module.ts
+++ b/src/addon/blog/components/components.module.ts
@@ -20,6 +20,7 @@ import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
import { CoreCommentsComponentsModule } from '@core/comments/components/components.module';
+import { CoreTagComponentsModule } from '@core/tag/components/components.module';
import { AddonBlogEntriesComponent } from './entries/entries';
@NgModule({
@@ -33,7 +34,8 @@ import { AddonBlogEntriesComponent } from './entries/entries';
CoreComponentsModule,
CoreDirectivesModule,
CorePipesModule,
- CoreCommentsComponentsModule
+ CoreCommentsComponentsModule,
+ CoreTagComponentsModule
],
providers: [
],
diff --git a/src/addon/blog/components/entries/addon-blog-entries.html b/src/addon/blog/components/entries/addon-blog-entries.html
index 670e4d276c1..b3a41ae72b6 100644
--- a/src/addon/blog/components/entries/addon-blog-entries.html
+++ b/src/addon/blog/components/entries/addon-blog-entries.html
@@ -4,9 +4,9 @@
-
+
{{ 'addon.blog.showonlyyourentries' |Â translate }}
-
+ >
@@ -29,6 +29,10 @@
+ 0">
+ {{ 'core.tag.tags' | translate }}:
+
+
diff --git a/src/addon/blog/components/entries/entries.ts b/src/addon/blog/components/entries/entries.ts
index b66db02a32b..aa66beaef40 100644
--- a/src/addon/blog/components/entries/entries.ts
+++ b/src/addon/blog/components/entries/entries.ts
@@ -15,10 +15,12 @@
import { Component, Input, OnInit, ViewChild } from '@angular/core';
import { Content } from 'ionic-angular';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
+import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreSitesProvider } from '@providers/sites';
import { CoreUserProvider } from '@core/user/providers/user';
import { AddonBlogProvider } from '../../providers/blog';
import { CoreCommentsProvider } from '@core/comments/providers/comments';
+import { CoreTagProvider } from '@core/tag/providers/tag';
/**
* Component that displays the blog entries.
@@ -37,6 +39,9 @@ export class AddonBlogEntriesComponent implements OnInit {
protected filter = {};
protected pageLoaded = 0;
+ protected userPageLoaded = 0;
+ protected canLoadMoreEntries = false;
+ protected canLoadMoreUserEntries = true;
@ViewChild(Content) content: Content;
@@ -45,14 +50,15 @@ export class AddonBlogEntriesComponent implements OnInit {
loadMoreError = false;
entries = [];
currentUserId: number;
- showMyIssuesToggle = false;
+ showMyEntriesToggle = false;
onlyMyEntries = false;
component = AddonBlogProvider.COMPONENT;
commentsEnabled: boolean;
+ tagsEnabled: boolean;
constructor(protected blogProvider: AddonBlogProvider, protected domUtils: CoreDomUtilsProvider,
- protected userProvider: CoreUserProvider, sitesProvider: CoreSitesProvider,
- protected commentsProvider: CoreCommentsProvider) {
+ protected userProvider: CoreUserProvider, sitesProvider: CoreSitesProvider, protected utils: CoreUtilsProvider,
+ protected commentsProvider: CoreCommentsProvider, private tagProvider: CoreTagProvider) {
this.currentUserId = sitesProvider.getCurrentSiteUserId();
}
@@ -63,6 +69,7 @@ export class AddonBlogEntriesComponent implements OnInit {
if (this.userId) {
this.filter['userid'] = this.userId;
}
+ this.showMyEntriesToggle = !this.userId;
if (this.courseId) {
this.filter['courseid'] = this.courseId;
@@ -85,6 +92,7 @@ export class AddonBlogEntriesComponent implements OnInit {
}
this.commentsEnabled = !this.commentsProvider.areCommentsDisabledInSite();
+ this.tagsEnabled = this.tagProvider.areTagsAvailableInSite();
this.fetchEntries().then(() => {
this.blogProvider.logView(this.filter).catch(() => {
@@ -104,9 +112,12 @@ export class AddonBlogEntriesComponent implements OnInit {
if (refresh) {
this.pageLoaded = 0;
+ this.userPageLoaded = 0;
}
- return this.blogProvider.getEntries(this.filter, this.pageLoaded).then((result) => {
+ const loadPage = this.onlyMyEntries ? this.userPageLoaded : this.pageLoaded;
+
+ return this.blogProvider.getEntries(this.filter, loadPage).then((result) => {
const promises = result.entries.map((entry) => {
switch (entry.publishstate) {
case 'draft':
@@ -131,16 +142,25 @@ export class AddonBlogEntriesComponent implements OnInit {
});
if (refresh) {
- this.showMyIssuesToggle = false;
this.entries = result.entries;
} else {
- this.entries = this.entries.concat(result.entries);
+ this.entries = this.utils.uniqueArray(this.entries.concat(result.entries), 'id').sort((a, b) => {
+ return b.created - a.created;
+ });
}
- this.canLoadMore = result.totalentries > this.entries.length;
- this.pageLoaded++;
-
- this.showMyIssuesToggle = !this.userId;
+ if (this.onlyMyEntries) {
+ const count = this.entries.filter((entry) => {
+ return entry.userid == this.currentUserId;
+ }).length;
+ this.canLoadMoreUserEntries = result.totalentries > count;
+ this.canLoadMore = this.canLoadMoreUserEntries;
+ this.userPageLoaded++;
+ } else {
+ this.canLoadMoreEntries = result.totalentries > this.entries.length;
+ this.canLoadMore = this.canLoadMoreEntries;
+ this.pageLoaded++;
+ }
return Promise.all(promises);
}).catch((message) => {
@@ -151,6 +171,30 @@ export class AddonBlogEntriesComponent implements OnInit {
});
}
+ /**
+ * Toggle between showing only my entries or not.
+ *
+ * @param {boolean} enabled If true, filter my entries. False otherwise.
+ */
+ onlyMyEntriesToggleChanged(enabled: boolean): void {
+ if (enabled) {
+ const count = this.entries.filter((entry) => {
+ return entry.userid == this.currentUserId;
+ }).length;
+ this.userPageLoaded = Math.floor(count / AddonBlogProvider.ENTRIES_PER_PAGE);
+ this.filter['userid'] = this.currentUserId;
+
+ if (count == 0 && this.canLoadMoreUserEntries) {
+ // First time but no entry loaded. Try to load some.
+ this.loadMore();
+ }
+ } else {
+ delete this.filter['userid'];
+ }
+
+ this.canLoadMore = enabled ? this.canLoadMoreUserEntries : this.canLoadMoreEntries;
+ }
+
/**
* Function to load more entries.
*
@@ -169,7 +213,23 @@ export class AddonBlogEntriesComponent implements OnInit {
* @param {any} refresher Refresher instance.
*/
refresh(refresher?: any): void {
- this.blogProvider.invalidateEntries(this.filter).finally(() => {
+ const promises = this.entries.map((entry) => {
+ return this.commentsProvider.invalidateCommentsData('user', entry.userid, this.component, entry.id, 'format_blog');
+ });
+
+ promises.push(this.blogProvider.invalidateEntries(this.filter));
+
+ if (this.showMyEntriesToggle) {
+ this.filter['userid'] = this.currentUserId;
+ promises.push(this.blogProvider.invalidateEntries(this.filter));
+
+ if (!this.onlyMyEntries) {
+ delete this.filter['userid'];
+ }
+
+ }
+
+ this.utils.allPromises(promises).finally(() => {
this.fetchEntries(true).finally(() => {
if (refresher) {
refresher.complete();
diff --git a/src/addon/blog/providers/course-option-handler.ts b/src/addon/blog/providers/course-option-handler.ts
index 9c28973fb2f..5617d6c9258 100644
--- a/src/addon/blog/providers/course-option-handler.ts
+++ b/src/addon/blog/providers/course-option-handler.ts
@@ -78,10 +78,10 @@ export class AddonBlogCourseOptionHandler implements CoreCourseOptionsHandler {
* Returns the data needed to render the handler.
*
* @param {Injector} injector Injector.
- * @param {number} courseId The course ID.
+ * @param {number} course The course.
* @return {CoreCourseOptionsHandlerData|Promise} Data or promise resolved with the data.
*/
- getDisplayData(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise {
+ getDisplayData(injector: Injector, course: any): CoreCourseOptionsHandlerData | Promise {
return {
title: 'addon.blog.blog',
class: 'addon-blog-handler',
diff --git a/src/addon/blog/providers/index-link-handler.ts b/src/addon/blog/providers/index-link-handler.ts
index 189fad7a34c..0670b322b53 100644
--- a/src/addon/blog/providers/index-link-handler.ts
+++ b/src/addon/blog/providers/index-link-handler.ts
@@ -15,7 +15,7 @@
import { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
-import { CoreLoginHelperProvider } from '@core/login/providers/helper';
+import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { AddonBlogProvider } from './blog';
/**
@@ -27,7 +27,7 @@ export class AddonBlogIndexLinkHandler extends CoreContentLinksHandlerBase {
featureName = 'CoreUserDelegate_AddonBlog:blogs';
pattern = /\/blog\/index\.php/;
- constructor(private blogProvider: AddonBlogProvider, private loginHelper: CoreLoginHelperProvider) {
+ constructor(private blogProvider: AddonBlogProvider, private linkHelper: CoreContentLinksHelperProvider) {
super();
}
@@ -53,8 +53,7 @@ export class AddonBlogIndexLinkHandler extends CoreContentLinksHandlerBase {
return [{
action: (siteId, navCtrl?): void => {
- // Always use redirect to make it the new history root (to avoid "loops" in history).
- this.loginHelper.redirect('AddonBlogEntriesPage', pageParams, siteId);
+ this.linkHelper.goInSite(navCtrl, 'AddonBlogEntriesPage', pageParams, siteId, !Object.keys(pageParams).length);
}
}];
}
diff --git a/src/addon/blog/providers/tag-area-handler.ts b/src/addon/blog/providers/tag-area-handler.ts
new file mode 100644
index 00000000000..41413d82437
--- /dev/null
+++ b/src/addon/blog/providers/tag-area-handler.ts
@@ -0,0 +1,58 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate';
+import { CoreTagHelperProvider } from '@core/tag/providers/helper';
+import { CoreTagFeedComponent } from '@core/tag/components/feed/feed';
+import { AddonBlogProvider } from './blog';
+
+/**
+ * Handler to support tags.
+ */
+@Injectable()
+export class AddonBlogTagAreaHandler implements CoreTagAreaHandler {
+ name = 'AddonBlogTagAreaHandler';
+ type = 'core/post';
+
+ constructor(private tagHelper: CoreTagHelperProvider, private blogProvider: AddonBlogProvider) {}
+
+ /**
+ * Whether or not the handler is enabled on a site level.
+ * @return {boolean|Promise} Whether or not the handler is enabled on a site level.
+ */
+ isEnabled(): boolean | Promise {
+ return this.blogProvider.isPluginEnabled();
+ }
+
+ /**
+ * Parses the rendered content of a tag index and returns the items.
+ *
+ * @param {string} content Rendered content.
+ * @return {any[]|Promise} Area items (or promise resolved with the items).
+ */
+ parseContent(content: string): any[] | Promise {
+ return this.tagHelper.parseFeedContent(content);
+ }
+
+ /**
+ * Get the component to use to display items.
+ *
+ * @param {Injector} injector Injector.
+ * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found.
+ */
+ getComponent(injector: Injector): any | Promise {
+ return CoreTagFeedComponent;
+ }
+}
diff --git a/src/addon/blog/providers/user-handler.ts b/src/addon/blog/providers/user-handler.ts
index 039b9ed56ef..fe19563b8c1 100644
--- a/src/addon/blog/providers/user-handler.ts
+++ b/src/addon/blog/providers/user-handler.ts
@@ -63,7 +63,7 @@ export class AddonBlogUserHandler implements CoreUserProfileHandler {
action: (event, navCtrl, user, courseId): void => {
event.preventDefault();
event.stopPropagation();
- // Always use redirect to make it the new history root (to avoid "loops" in history).
+
this.linkHelper.goInSite(navCtrl, 'AddonBlogEntriesPage', { userId: user.id, courseId: courseId });
}
};
diff --git a/src/addon/calendar/calendar.module.ts b/src/addon/calendar/calendar.module.ts
index 3146e6e690f..28765fad695 100644
--- a/src/addon/calendar/calendar.module.ts
+++ b/src/addon/calendar/calendar.module.ts
@@ -14,18 +14,26 @@
import { NgModule } from '@angular/core';
import { AddonCalendarProvider } from './providers/calendar';
+import { AddonCalendarOfflineProvider } from './providers/calendar-offline';
import { AddonCalendarHelperProvider } from './providers/helper';
+import { AddonCalendarSyncProvider } from './providers/calendar-sync';
import { AddonCalendarMainMenuHandler } from './providers/mainmenu-handler';
+import { AddonCalendarSyncCronHandler } from './providers/sync-cron-handler';
+import { AddonCalendarViewLinkHandler } from './providers/view-link-handler';
import { CoreMainMenuDelegate } from '@core/mainmenu/providers/delegate';
+import { CoreCronDelegate } from '@providers/cron';
import { CoreInitDelegate } from '@providers/init';
import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
import { CoreLoginHelperProvider } from '@core/login/providers/helper';
import { CoreUpdateManagerProvider } from '@providers/update-manager';
+import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
// List of providers (without handlers).
export const ADDON_CALENDAR_PROVIDERS: any[] = [
AddonCalendarProvider,
- AddonCalendarHelperProvider
+ AddonCalendarOfflineProvider,
+ AddonCalendarHelperProvider,
+ AddonCalendarSyncProvider
];
@NgModule({
@@ -35,15 +43,24 @@ export const ADDON_CALENDAR_PROVIDERS: any[] = [
],
providers: [
AddonCalendarProvider,
+ AddonCalendarOfflineProvider,
AddonCalendarHelperProvider,
- AddonCalendarMainMenuHandler
+ AddonCalendarSyncProvider,
+ AddonCalendarMainMenuHandler,
+ AddonCalendarSyncCronHandler,
+ AddonCalendarViewLinkHandler
]
})
export class AddonCalendarModule {
constructor(mainMenuDelegate: CoreMainMenuDelegate, calendarHandler: AddonCalendarMainMenuHandler,
initDelegate: CoreInitDelegate, calendarProvider: AddonCalendarProvider, loginHelper: CoreLoginHelperProvider,
- localNotificationsProvider: CoreLocalNotificationsProvider, updateManager: CoreUpdateManagerProvider) {
+ localNotificationsProvider: CoreLocalNotificationsProvider, updateManager: CoreUpdateManagerProvider,
+ cronDelegate: CoreCronDelegate, syncHandler: AddonCalendarSyncCronHandler,
+ contentLinksDelegate: CoreContentLinksDelegate, viewLinkHandler: AddonCalendarViewLinkHandler) {
+
mainMenuDelegate.registerHandler(calendarHandler);
+ cronDelegate.register(syncHandler);
+ contentLinksDelegate.registerHandler(viewLinkHandler);
initDelegate.ready().then(() => {
calendarProvider.scheduleAllSitesEventsNotifications();
@@ -58,7 +75,13 @@ export class AddonCalendarModule {
return;
}
- loginHelper.redirect('AddonCalendarListPage', {eventId: data.eventid}, data.siteId);
+ // Check which page we should load.
+ calendarProvider.canViewMonth(data.siteId).then((canView) => {
+ const pageName = canView ? 'AddonCalendarIndexPage' : 'AddonCalendarListPage';
+
+ loginHelper.redirect(pageName, {eventId: data.eventid}, data.siteId);
+ });
+
});
});
}
diff --git a/src/addon/calendar/components/calendar/addon-calendar-calendar.html b/src/addon/calendar/components/calendar/addon-calendar-calendar.html
new file mode 100644
index 00000000000..093b97064f5
--- /dev/null
+++ b/src/addon/calendar/components/calendar/addon-calendar-calendar.html
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ periodName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ day.shortname | translate }}
+ {{ day.fullname | translate }}
+
+
+
+
+
+
+
+ {{ day.mday }}
+
+
+
+
+
+
+
+
+
+
+
+ {{ event.timestart * 1000 | coreFormatDate: timeFormat }}
+
+ {{event.name}}
+
+
+
4" class="addon-calendar-day-more">{{ 'core.nummore' | translate:{$a: day.filteredEvents.length - 3} }}
+
+
+
+
+
+
+
diff --git a/src/addon/calendar/components/calendar/calendar.scss b/src/addon/calendar/components/calendar/calendar.scss
new file mode 100644
index 00000000000..b04fe3d64c5
--- /dev/null
+++ b/src/addon/calendar/components/calendar/calendar.scss
@@ -0,0 +1,196 @@
+
+$calendar-event-category-color: $purple !default; // Purple.
+$calendar-event-course-color: $red !default; // Red.
+$calendar-event-group-color: $yellow !default; // Yellow.
+$calendar-event-user-color: $blue !default; // Blue.
+$calendar-event-site-color: $green !default; // Green.
+$calendar-today-bgcolor: $core-color !default;
+$calendar-today-color: $white !default;
+$calendar-border-color: $gray !default;
+
+ion-app.app-root page-addon-calendar-list,
+ion-app.app-root page-addon-calendar-day,
+ion-app.app-root addon-calendar-upcoming-events {
+
+ .item.addon-calendar-event {
+ > .icon {
+ color: white;
+ border-radius: 50%;
+ width: 36px;
+ height: 36px;
+ line-height: 36px;
+
+ &.fa {
+ font-size: 20px;
+ &::before {
+ width: 1.9em;
+ }
+ }
+ }
+ > .core-module-icon {
+ margin: 9px 8px 9px 8px;
+ }
+
+ &.addon-calendar-eventtype-category > .icon {
+ background-color: $calendar-event-category-color;
+ }
+ &.addon-calendar-eventtype-course > .icon {
+ background-color: $calendar-event-course-color;
+ }
+ &.addon-calendar-eventtype-group > .icon {
+ background-color: $calendar-event-group-color;
+ }
+ &.addon-calendar-eventtype-user > .icon {
+ background-color: $calendar-event-user-color;
+ }
+ &.addon-calendar-eventtype-site > .icon {
+ background-color: $calendar-event-site-color;
+ }
+ }
+}
+
+ion-app.app-root addon-calendar-calendar {
+
+ .addon-calendar-navigation {
+ @include padding(5px, 10px, null, 10px);
+ }
+
+ .addon-calendar-months {
+ background-color: white;
+ padding: 0;
+ }
+
+ .addon-calendar-day {
+ border-bottom: 1px solid $calendar-border-color;
+ @include border-end(1px, solid, $calendar-border-color);
+ overflow: hidden;
+ min-height: 60px;
+
+ &:first-child {
+ @include padding(null, null, null, 10px);
+ }
+ &:last-child {
+ @include border-end(0, null, null);
+ @include padding(null, 8px, null, null);
+ }
+
+ &.addon-calendar-event-past-day > .addon-calendar-dot-types,
+ &.addon-calendar-event-past-day > .addon-calendar-day-events {
+ opacity: 0.5;
+ }
+
+ .addon-calendar-day-number {
+ margin: 0;
+
+ span {
+ line-height: 24px;
+ font-weight: 500;
+ display: inline-block;
+ margin: 3px;
+ width: max-content;
+ width: 24px;
+ height: 24px;
+ text-align: center;
+ }
+ }
+
+ @include media-breakpoint-up(md) {
+ .addon-calendar-day-number {
+ text-align: left;
+ }
+ }
+
+ &.today .addon-calendar-day-number span {
+ background-color: $calendar-today-bgcolor;
+ color: $calendar-today-color;
+
+ border-radius: 50%;
+ }
+ &.dayblank {
+ background-color: $gray-lighter;
+ }
+
+ .addon-calendar-event {
+ margin-top: 0.6em;
+ margin-bottom: 0.6em;
+ overflow: hidden;
+ white-space: nowrap;
+
+ &.addon-calendar-event-past {
+ opacity: 0.5;
+ }
+
+ .addon-calendar-event-name {
+ font-weight: 500;
+ }
+ }
+
+ .addon-calendar-day-more {
+ @include margin(0.6em, null, 0.6em, 4px);
+ }
+
+ .addon-calendar-dot-types {
+ margin: 0;
+ }
+ }
+
+ .addon-calendar-period {
+ flex-grow: 3;
+ h3 {
+ margin-top: 10px;
+ font-size: 1.6rem;
+ }
+ }
+
+ .addon-calendar-weekday {
+ border-bottom: 1px solid $list-md-border-color;
+ }
+
+ .addon-calendar-day-events {
+ @include text-align('start');
+
+ ion-icon {
+ @include margin-horizontal(null, 2px);
+ font-size: 1em;
+ }
+ }
+
+ .addon-calendar-event, .addon-calendar-day-number, .addon-calendar-day-more {
+ cursor: pointer;
+ }
+
+ .calendar_event_type {
+ display: inline-block;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ border: 1px solid white;
+ @include margin-horizontal(1px, 1px);
+
+
+
+ &.calendar_event_category {
+ background-color: $calendar-event-category-color;
+ }
+ &.calendar_event_course {
+ background-color: $calendar-event-course-color;
+ }
+ &.calendar_event_group {
+ background-color: $calendar-event-group-color;
+ }
+ &.calendar_event_user {
+ background-color: $calendar-event-user-color;
+ }
+ &.calendar_event_site {
+ background-color: $calendar-event-site-color;
+ }
+ }
+
+ .core-module-icon {
+ @include margin-horizontal(1px, 1px);
+ width: 16px;
+ height: 16px;
+ display: inline-block;
+ vertical-align: bottom;
+ }
+}
\ No newline at end of file
diff --git a/src/addon/calendar/components/calendar/calendar.ts b/src/addon/calendar/components/calendar/calendar.ts
new file mode 100644
index 00000000000..269310608ef
--- /dev/null
+++ b/src/addon/calendar/components/calendar/calendar.ts
@@ -0,0 +1,515 @@
+// (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, OnDestroy, OnInit, Input, OnChanges, SimpleChange, Output, EventEmitter } from '@angular/core';
+import { CoreEventsProvider } from '@providers/events';
+import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
+import { CoreSitesProvider } from '@providers/sites';
+import { CoreDomUtilsProvider } from '@providers/utils/dom';
+import { CoreTimeUtilsProvider } from '@providers/utils/time';
+import { CoreUtilsProvider } from '@providers/utils/utils';
+import { AddonCalendarProvider } from '../../providers/calendar';
+import { AddonCalendarHelperProvider } from '../../providers/helper';
+import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline';
+import { CoreCoursesProvider } from '@core/courses/providers/courses';
+import { CoreAppProvider } from '@providers/app';
+
+/**
+ * Component that displays a calendar.
+ */
+@Component({
+ selector: 'addon-calendar-calendar',
+ templateUrl: 'addon-calendar-calendar.html',
+})
+export class AddonCalendarCalendarComponent implements OnInit, OnChanges, OnDestroy {
+ @Input() initialYear: number | string; // Initial year to load.
+ @Input() initialMonth: number | string; // Initial month to load.
+ @Input() courseId: number | string;
+ @Input() categoryId: number | string; // Category ID the course belongs to.
+ @Input() canNavigate?: string | boolean; // Whether to include arrows to change the month. Defaults to true.
+ @Input() displayNavButtons?: string | boolean; // Whether to display nav buttons created by this component. Defaults to true.
+ @Output() onEventClicked = new EventEmitter();
+ @Output() onDayClicked = new EventEmitter<{day: number, month: number, year: number}>();
+
+ periodName: string;
+ weekDays: any[];
+ weeks: any[];
+ loaded = false;
+ timeFormat: string;
+ isCurrentMonth: boolean;
+ isPastMonth: boolean;
+
+ protected year: number;
+ protected month: number;
+ protected categoriesRetrieved = false;
+ protected categories = {};
+ protected currentSiteId: string;
+ protected offlineEvents: {[monthId: string]: {[day: number]: any[]}} = {}; // Offline events classified in month & day.
+ protected offlineEditedEventsIds = []; // IDs of events edited in offline.
+ protected deletedEvents = []; // Events deleted in offline.
+ protected currentTime: number;
+
+ // Observers.
+ protected undeleteEventObserver: any;
+ protected obsDefaultTimeChange: any;
+
+ constructor(eventsProvider: CoreEventsProvider,
+ sitesProvider: CoreSitesProvider,
+ localNotificationsProvider: CoreLocalNotificationsProvider,
+ private calendarProvider: AddonCalendarProvider,
+ private calendarHelper: AddonCalendarHelperProvider,
+ private calendarOffline: AddonCalendarOfflineProvider,
+ private domUtils: CoreDomUtilsProvider,
+ private timeUtils: CoreTimeUtilsProvider,
+ private utils: CoreUtilsProvider,
+ private coursesProvider: CoreCoursesProvider,
+ private appProvider: CoreAppProvider) {
+
+ this.currentSiteId = sitesProvider.getCurrentSiteId();
+
+ if (localNotificationsProvider.isAvailable()) {
+ // Re-schedule events if default time changes.
+ this.obsDefaultTimeChange = eventsProvider.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
+ this.weeks.forEach((week) => {
+ week.days.forEach((day) => {
+ calendarProvider.scheduleEventsNotifications(day.events);
+ });
+ });
+ }, this.currentSiteId);
+ }
+
+ // Listen for events "undeleted" (offline).
+ this.undeleteEventObserver = eventsProvider.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, (data) => {
+ if (data && data.eventId) {
+ // Mark it as undeleted, no need to refresh.
+ this.undeleteEvent(data.eventId);
+
+ // Remove it from the list of deleted events if it's there.
+ const index = this.deletedEvents.indexOf(data.eventId);
+ if (index != -1) {
+ this.deletedEvents.splice(index, 1);
+ }
+ }
+ }, this.currentSiteId);
+ }
+
+ /**
+ * Component loaded.
+ */
+ ngOnInit(): void {
+ const now = new Date();
+
+ this.year = this.initialYear ? Number(this.initialYear) : now.getFullYear();
+ this.month = this.initialMonth ? Number(this.initialMonth) : now.getMonth() + 1;
+
+ this.calculateIsCurrentMonth();
+
+ this.fetchData();
+ }
+
+ /**
+ * Detect changes on input properties.
+ */
+ ngOnChanges(changes: {[name: string]: SimpleChange}): void {
+ this.canNavigate = typeof this.canNavigate == 'undefined' ? true : this.utils.isTrueOrOne(this.canNavigate);
+ this.displayNavButtons = typeof this.displayNavButtons == 'undefined' ? true :
+ this.utils.isTrueOrOne(this.displayNavButtons);
+
+ if ((changes.courseId || changes.categoryId) && this.weeks) {
+ this.filterEvents();
+ }
+ }
+
+ /**
+ * Fetch contacts.
+ *
+ * @param {boolean} [refresh=false] True if we are refreshing events.
+ * @return {Promise} Promise resolved when done.
+ */
+ fetchData(refresh: boolean = false): Promise {
+ const promises = [];
+
+ promises.push(this.loadCategories());
+
+ // Get offline events.
+ promises.push(this.calendarOffline.getAllEditedEvents().then((events) => {
+ // Format data.
+ events.forEach((event) => {
+ event.offline = true;
+ this.calendarHelper.formatEventData(event);
+ });
+
+ // Classify them by month.
+ this.offlineEvents = this.calendarHelper.classifyIntoMonths(events);
+
+ // Get the IDs of events edited in offline.
+ const filtered = events.filter((event) => {
+ return event.id > 0;
+ });
+ this.offlineEditedEventsIds = filtered.map((event) => {
+ return event.id;
+ });
+ }));
+
+ // Get events deleted in offline.
+ promises.push(this.calendarOffline.getAllDeletedEventsIds().then((ids) => {
+ this.deletedEvents = ids;
+ }));
+
+ // Get time format to use.
+ promises.push(this.calendarProvider.getCalendarTimeFormat().then((value) => {
+ this.timeFormat = value;
+ }));
+
+ return Promise.all(promises).then(() => {
+ return this.fetchEvents();
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
+ }).finally(() => {
+ this.loaded = true;
+ });
+ }
+
+ /**
+ * Fetch the events for current month.
+ *
+ * @return {Promise} Promise resolved when done.
+ */
+ fetchEvents(): Promise {
+ // Don't pass courseId and categoryId, we'll filter them locally.
+ return this.calendarProvider.getMonthlyEvents(this.year, this.month).catch((error) => {
+ if (!this.appProvider.isOnline()) {
+ // Allow navigating to non-cached months in offline (behave as if using emergency cache).
+ return this.calendarHelper.getOfflineMonthWeeks(this.year, this.month);
+ } else {
+ return Promise.reject(error);
+ }
+ }).then((result) => {
+ // Calculate the period name. We don't use the one in result because it's in server's language.
+ this.periodName = this.timeUtils.userDate(new Date(this.year, this.month - 1).getTime(), 'core.strftimemonthyear');
+
+ this.weekDays = this.calendarProvider.getWeekDays(result.daynames[0].dayno);
+ this.weeks = result.weeks;
+
+ this.calculateIsCurrentMonth();
+
+ if (this.isCurrentMonth) {
+ const currentDay = new Date().getDate();
+ let isPast = true;
+
+ this.weeks.forEach((week) => {
+ week.days.some((day) => {
+ day.istoday = day.mday == currentDay;
+ day.ispast = isPast && !day.istoday;
+ isPast = day.ispast;
+
+ if (day.istoday) {
+ day.events.forEach((event) => {
+ event.ispast = this.isEventPast(event);
+ });
+
+ return true;
+ }
+
+ return day.istoday;
+ });
+ });
+ }
+
+ // Merge the online events with offline data.
+ this.mergeEvents();
+
+ // Filter events by course.
+ this.filterEvents();
+ });
+ }
+
+ /**
+ * Load categories to be able to filter events.
+ *
+ * @return {Promise} Promise resolved when done.
+ */
+ protected loadCategories(): Promise {
+ if (this.categoriesRetrieved) {
+ // Already retrieved, stop.
+ return Promise.resolve();
+ }
+
+ return this.coursesProvider.getCategories(0, true).then((cats) => {
+ this.categoriesRetrieved = true;
+ this.categories = {};
+
+ // Index categories by ID.
+ cats.forEach((category) => {
+ this.categories[category.id] = category;
+ });
+ }).catch(() => {
+ // Ignore errors.
+ });
+ }
+
+ /**
+ * Filter events to only display events belonging to a certain course.
+ */
+ filterEvents(): void {
+ const courseId = this.courseId ? Number(this.courseId) : undefined,
+ categoryId = this.categoryId ? Number(this.categoryId) : undefined;
+
+ this.weeks.forEach((week) => {
+ week.days.forEach((day) => {
+ if (!courseId || courseId < 0) {
+ day.filteredEvents = day.events;
+ } else {
+ day.filteredEvents = day.events.filter((event) => {
+ return this.calendarHelper.shouldDisplayEvent(event, courseId, categoryId, this.categories);
+ });
+ }
+
+ // Re-calculate some properties.
+ this.calendarHelper.calculateDayData(day, day.filteredEvents);
+ });
+ });
+ }
+
+ /**
+ * Refresh events.
+ *
+ * @param {boolean} [afterChange] Whether the refresh is done after an event has changed or has been synced.
+ * @return {Promise} Promise resolved when done.
+ */
+ refreshData(afterChange?: boolean): Promise {
+ const promises = [];
+
+ // Don't invalidate monthly events after a change, it has already been handled.
+ if (!afterChange) {
+ promises.push(this.calendarProvider.invalidateMonthlyEvents(this.year, this.month));
+ }
+ promises.push(this.coursesProvider.invalidateCategories(0, true));
+ promises.push(this.calendarProvider.invalidateTimeFormat());
+
+ this.categoriesRetrieved = false; // Get categories again.
+
+ return Promise.all(promises).then(() => {
+ return this.fetchData(true);
+ });
+ }
+
+ /**
+ * Load next month.
+ */
+ loadNext(): void {
+ this.increaseMonth();
+
+ this.loaded = false;
+
+ this.fetchEvents().catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
+ this.decreaseMonth();
+ }).finally(() => {
+ this.loaded = true;
+ });
+ }
+
+ /**
+ * Load previous month.
+ */
+ loadPrevious(): void {
+ this.decreaseMonth();
+
+ this.loaded = false;
+
+ this.fetchEvents().catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
+ this.increaseMonth();
+ }).finally(() => {
+ this.loaded = true;
+ });
+ }
+
+ /**
+ * An event was clicked.
+ *
+ * @param {any} calendarEvent Calendar event..
+ * @param {MouseEvent} event Mouse event.
+ */
+ eventClicked(calendarEvent: any, event: MouseEvent): void {
+ this.onEventClicked.emit(calendarEvent.id);
+ event.stopPropagation();
+ }
+
+ /**
+ * A day was clicked.
+ *
+ * @param {number} day Day.
+ */
+ dayClicked(day: number): void {
+ this.onDayClicked.emit({day: day, month: this.month, year: this.year});
+ }
+
+ /**
+ * Check if user is viewing the current month.
+ */
+ calculateIsCurrentMonth(): void {
+ const now = new Date();
+
+ this.currentTime = this.timeUtils.timestamp();
+
+ this.isCurrentMonth = this.year == now.getFullYear() && this.month == now.getMonth() + 1;
+ this.isPastMonth = this.year < now.getFullYear() || (this.year == now.getFullYear() && this.month < now.getMonth() + 1);
+ }
+
+ /**
+ * Go to current month.
+ */
+ goToCurrentMonth(): void {
+ const now = new Date(),
+ initialMonth = this.month,
+ initialYear = this.year;
+
+ this.month = now.getMonth() + 1;
+ this.year = now.getFullYear();
+
+ this.loaded = false;
+
+ this.fetchEvents().then(() => {
+ this.isCurrentMonth = true;
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
+ this.year = initialYear;
+ this.month = initialMonth;
+ }).finally(() => {
+ this.loaded = true;
+ });
+ }
+
+ /**
+ * Decrease the current month.
+ */
+ protected decreaseMonth(): void {
+ if (this.month === 1) {
+ this.month = 12;
+ this.year--;
+ } else {
+ this.month--;
+ }
+ }
+
+ /**
+ * Increase the current month.
+ */
+ protected increaseMonth(): void {
+ if (this.month === 12) {
+ this.month = 1;
+ this.year++;
+ } else {
+ this.month++;
+ }
+ }
+
+ /**
+ * Merge online events with the offline events of that period.
+ */
+ protected mergeEvents(): void {
+ const monthOfflineEvents = this.offlineEvents[this.calendarHelper.getMonthId(this.year, this.month)];
+
+ this.weeks.forEach((week) => {
+ week.days.forEach((day) => {
+
+ // Format online events.
+ day.events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper));
+
+ // Schedule notifications for the events retrieved (only future events will be scheduled).
+ this.calendarProvider.scheduleEventsNotifications(day.events);
+
+ if (monthOfflineEvents || this.deletedEvents.length) {
+ // There is offline data, merge it.
+
+ if (this.deletedEvents.length) {
+ // Mark as deleted the events that were deleted in offline.
+ day.events.forEach((event) => {
+ event.deleted = this.deletedEvents.indexOf(event.id) != -1;
+ });
+ }
+
+ if (this.offlineEditedEventsIds.length) {
+ // Remove the online events that were modified in offline.
+ day.events = day.events.filter((event) => {
+ return this.offlineEditedEventsIds.indexOf(event.id) == -1;
+ });
+ }
+
+ if (monthOfflineEvents && monthOfflineEvents[day.mday]) {
+ // Add the offline events (either new or edited).
+ day.events = this.sortEvents(day.events.concat(monthOfflineEvents[day.mday]));
+ }
+ }
+ });
+ });
+ }
+
+ /**
+ * Sort events by timestart.
+ *
+ * @param {any[]} events List to sort.
+ */
+ protected sortEvents(events: any[]): any[] {
+ return events.sort((a, b) => {
+ if (a.timestart == b.timestart) {
+ return a.timeduration - b.timeduration;
+ }
+
+ return a.timestart - b.timestart;
+ });
+ }
+
+ /**
+ * Undelete a certain event.
+ *
+ * @param {number} eventId Event ID.
+ */
+ protected undeleteEvent(eventId: number): void {
+ if (!this.weeks) {
+ return;
+ }
+
+ this.weeks.forEach((week) => {
+ week.days.forEach((day) => {
+ const event = day.events.find((event) => {
+ return event.id == eventId;
+ });
+
+ if (event) {
+ event.deleted = false;
+ }
+ });
+ });
+ }
+
+ /**
+ * Returns if the event is in the past or not.
+ * @param {any} event Event object.
+ * @return {boolean} True if it's in the past.
+ */
+ isEventPast(event: any): boolean {
+ return (event.timestart + event.timeduration) < this.currentTime;
+ }
+
+ /**
+ * Component destroyed.
+ */
+ ngOnDestroy(): void {
+ this.undeleteEventObserver && this.undeleteEventObserver.off();
+ this.obsDefaultTimeChange && this.obsDefaultTimeChange.off();
+ }
+}
diff --git a/src/addon/calendar/components/components.module.ts b/src/addon/calendar/components/components.module.ts
new file mode 100644
index 00000000000..3928f6dd914
--- /dev/null
+++ b/src/addon/calendar/components/components.module.ts
@@ -0,0 +1,45 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+import { IonicModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreComponentsModule } from '@components/components.module';
+import { CoreDirectivesModule } from '@directives/directives.module';
+import { CorePipesModule } from '@pipes/pipes.module';
+import { AddonCalendarCalendarComponent } from '../components/calendar/calendar';
+import { AddonCalendarUpcomingEventsComponent } from '../components/upcoming-events/upcoming-events';
+
+@NgModule({
+ declarations: [
+ AddonCalendarCalendarComponent,
+ AddonCalendarUpcomingEventsComponent
+ ],
+ imports: [
+ CommonModule,
+ IonicModule,
+ TranslateModule.forChild(),
+ CoreComponentsModule,
+ CoreDirectivesModule,
+ CorePipesModule
+ ],
+ providers: [
+ ],
+ exports: [
+ AddonCalendarCalendarComponent,
+ AddonCalendarUpcomingEventsComponent
+ ]
+})
+export class AddonCalendarComponentsModule {}
diff --git a/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html b/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html
new file mode 100644
index 00000000000..68f1746083b
--- /dev/null
+++ b/src/addon/calendar/components/upcoming-events/addon-calendar-upcoming-events.html
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'core.notsent' | translate }}
+
+
+
+ {{ 'core.deletedoffline' | translate }}
+
+
+
+
+
+
diff --git a/src/addon/calendar/components/upcoming-events/upcoming-events.ts b/src/addon/calendar/components/upcoming-events/upcoming-events.ts
new file mode 100644
index 00000000000..d56a3470bd4
--- /dev/null
+++ b/src/addon/calendar/components/upcoming-events/upcoming-events.ts
@@ -0,0 +1,339 @@
+// (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, OnDestroy, OnInit, Input, OnChanges, SimpleChange, Output, EventEmitter } from '@angular/core';
+import { CoreEventsProvider } from '@providers/events';
+import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
+import { CoreSitesProvider } from '@providers/sites';
+import { CoreDomUtilsProvider } from '@providers/utils/dom';
+import { AddonCalendarProvider } from '../../providers/calendar';
+import { AddonCalendarHelperProvider } from '../../providers/helper';
+import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline';
+import { CoreCoursesProvider } from '@core/courses/providers/courses';
+import { CoreConstants } from '@core/constants';
+
+/**
+ * Component that displays upcoming events.
+ */
+@Component({
+ selector: 'addon-calendar-upcoming-events',
+ templateUrl: 'addon-calendar-upcoming-events.html',
+})
+export class AddonCalendarUpcomingEventsComponent implements OnInit, OnChanges, OnDestroy {
+ @Input() courseId: number | string;
+ @Input() categoryId: number | string; // Category ID the course belongs to.
+ @Output() onEventClicked = new EventEmitter();
+
+ filteredEvents = [];
+ loaded = false;
+
+ protected year: number;
+ protected month: number;
+ protected categoriesRetrieved = false;
+ protected categories = {};
+ protected currentSiteId: string;
+ protected events = []; // Events (both online and offline).
+ protected onlineEvents = [];
+ protected offlineEvents = []; // Offline events.
+ protected deletedEvents = []; // Events deleted in offline.
+ protected lookAhead: number;
+ protected timeFormat: string;
+
+ // Observers.
+ protected undeleteEventObserver: any;
+ protected obsDefaultTimeChange: any;
+
+ constructor(eventsProvider: CoreEventsProvider,
+ sitesProvider: CoreSitesProvider,
+ localNotificationsProvider: CoreLocalNotificationsProvider,
+ private calendarProvider: AddonCalendarProvider,
+ private calendarHelper: AddonCalendarHelperProvider,
+ private calendarOffline: AddonCalendarOfflineProvider,
+ private domUtils: CoreDomUtilsProvider,
+ private coursesProvider: CoreCoursesProvider) {
+
+ this.currentSiteId = sitesProvider.getCurrentSiteId();
+
+ if (localNotificationsProvider.isAvailable()) {
+ // Re-schedule events if default time changes.
+ this.obsDefaultTimeChange = eventsProvider.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
+ calendarProvider.scheduleEventsNotifications(this.onlineEvents);
+ }, this.currentSiteId);
+ }
+
+ // Listen for events "undeleted" (offline).
+ this.undeleteEventObserver = eventsProvider.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, (data) => {
+ if (data && data.eventId) {
+ // Mark it as undeleted, no need to refresh.
+ this.undeleteEvent(data.eventId);
+
+ // Remove it from the list of deleted events if it's there.
+ const index = this.deletedEvents.indexOf(data.eventId);
+ if (index != -1) {
+ this.deletedEvents.splice(index, 1);
+ }
+ }
+ }, this.currentSiteId);
+ }
+
+ /**
+ * Component loaded.
+ */
+ ngOnInit(): void {
+ this.fetchData();
+ }
+
+ /**
+ * Detect changes on input properties.
+ */
+ ngOnChanges(changes: {[name: string]: SimpleChange}): void {
+ if (changes.courseId || changes.categoryId) {
+ this.filterEvents();
+ }
+ }
+
+ /**
+ * Fetch data.
+ *
+ * @param {boolean} [refresh=false] True if we are refreshing events.
+ * @return {Promise} Promise resolved when done.
+ */
+ fetchData(refresh: boolean = false): Promise {
+ const promises = [];
+
+ promises.push(this.loadCategories());
+
+ // Get offline events.
+ promises.push(this.calendarOffline.getAllEditedEvents().then((events) => {
+ // Format data.
+ events.forEach((event) => {
+ event.offline = true;
+ this.calendarHelper.formatEventData(event);
+ });
+
+ this.offlineEvents = this.sortEvents(events);
+ }));
+
+ // Get events deleted in offline.
+ promises.push(this.calendarOffline.getAllDeletedEventsIds().then((ids) => {
+ this.deletedEvents = ids;
+ }));
+
+ // Get user preferences.
+ promises.push(this.calendarProvider.getCalendarLookAhead().then((value) => {
+ this.lookAhead = value;
+ }));
+
+ promises.push(this.calendarProvider.getCalendarTimeFormat().then((value) => {
+ this.timeFormat = value;
+ }));
+
+ return Promise.all(promises).then(() => {
+ return this.fetchEvents();
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
+ }).finally(() => {
+ this.loaded = true;
+ });
+ }
+
+ /**
+ * Fetch upcoming events.
+ *
+ * @return {Promise} Promise resolved when done.
+ */
+ fetchEvents(): Promise {
+ // Don't pass courseId and categoryId, we'll filter them locally.
+ return this.calendarProvider.getUpcomingEvents().then((result) => {
+ const promises = [];
+
+ this.onlineEvents = result.events;
+
+ this.onlineEvents.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper));
+
+ // Schedule notifications for the events retrieved.
+ this.calendarProvider.scheduleEventsNotifications(this.onlineEvents);
+
+ // Merge the online events with offline data.
+ this.events = this.mergeEvents();
+
+ // Filter events by course.
+ this.filterEvents();
+
+ // Re-calculate the formatted time so it uses the device date.
+ this.events.forEach((event) => {
+ promises.push(this.calendarProvider.formatEventTime(event, this.timeFormat).then((time) => {
+ event.formattedtime = time;
+ }));
+ });
+
+ return Promise.all(promises);
+ });
+ }
+
+ /**
+ * Load categories to be able to filter events.
+ *
+ * @return {Promise} Promise resolved when done.
+ */
+ protected loadCategories(): Promise {
+ if (this.categoriesRetrieved) {
+ // Already retrieved, stop.
+ return Promise.resolve();
+ }
+
+ return this.coursesProvider.getCategories(0, true).then((cats) => {
+ this.categoriesRetrieved = true;
+ this.categories = {};
+
+ // Index categories by ID.
+ cats.forEach((category) => {
+ this.categories[category.id] = category;
+ });
+ }).catch(() => {
+ // Ignore errors.
+ });
+ }
+
+ /**
+ * Filter events to only display events belonging to a certain course.
+ */
+ filterEvents(): void {
+ const courseId = this.courseId ? Number(this.courseId) : undefined,
+ categoryId = this.categoryId ? Number(this.categoryId) : undefined;
+
+ if (!courseId || courseId < 0) {
+ this.filteredEvents = this.events;
+ } else {
+ this.filteredEvents = this.events.filter((event) => {
+ return this.calendarHelper.shouldDisplayEvent(event, courseId, categoryId, this.categories);
+ });
+ }
+ }
+
+ /**
+ * Refresh events.
+ *
+ * @param {boolean} [afterChange] Whether the refresh is done after an event has changed or has been synced.
+ * @return {Promise} Promise resolved when done.
+ */
+ refreshData(afterChange?: boolean): Promise {
+ const promises = [];
+
+ // Don't invalidate upcoming events after a change, it has already been handled.
+ if (!afterChange) {
+ promises.push(this.calendarProvider.invalidateAllUpcomingEvents());
+ }
+ promises.push(this.coursesProvider.invalidateCategories(0, true));
+ promises.push(this.calendarProvider.invalidateLookAhead());
+ promises.push(this.calendarProvider.invalidateTimeFormat());
+
+ this.categoriesRetrieved = false; // Get categories again.
+
+ return Promise.all(promises).then(() => {
+ return this.fetchData(true);
+ });
+ }
+
+ /**
+ * An event was clicked.
+ *
+ * @param {any} event Event.
+ */
+ eventClicked(event: any): void {
+ this.onEventClicked.emit(event.id);
+ }
+
+ /**
+ * Merge online events with the offline events of that period.
+ *
+ * @return {any[]} Merged events.
+ */
+ protected mergeEvents(): any[] {
+ if (!this.offlineEvents.length && !this.deletedEvents.length) {
+ // No offline events, nothing to merge.
+ return this.onlineEvents;
+ }
+
+ const start = Date.now() / 1000,
+ end = start + (CoreConstants.SECONDS_DAY * this.lookAhead);
+ let result = this.onlineEvents;
+
+ if (this.deletedEvents.length) {
+ // Mark as deleted the events that were deleted in offline.
+ result.forEach((event) => {
+ event.deleted = this.deletedEvents.indexOf(event.id) != -1;
+ });
+ }
+
+ if (this.offlineEvents.length) {
+ // Remove the online events that were modified in offline.
+ result = result.filter((event) => {
+ const offlineEvent = this.offlineEvents.find((ev) => {
+ return ev.id == event.id;
+ });
+
+ return !offlineEvent;
+ });
+ }
+
+ // Now get the offline events that belong to this period.
+ const periodOfflineEvents = this.offlineEvents.filter((event) => {
+ return (event.timestart >= start || event.timestart + event.timeduration >= start) && event.timestart <= end;
+ });
+
+ // Merge both arrays and sort them.
+ result = result.concat(periodOfflineEvents);
+
+ return this.sortEvents(result);
+ }
+
+ /**
+ * Sort events by timestart.
+ *
+ * @param {any[]} events List to sort.
+ */
+ protected sortEvents(events: any[]): any[] {
+ return events.sort((a, b) => {
+ if (a.timestart == b.timestart) {
+ return a.timeduration - b.timeduration;
+ }
+
+ return a.timestart - b.timestart;
+ });
+ }
+
+ /**
+ * Undelete a certain event.
+ *
+ * @param {number} eventId Event ID.
+ */
+ protected undeleteEvent(eventId: number): void {
+ const event = this.onlineEvents.find((event) => {
+ return event.id == eventId;
+ });
+
+ if (event) {
+ event.deleted = false;
+ }
+ }
+
+ /**
+ * Component destroyed.
+ */
+ ngOnDestroy(): void {
+ this.undeleteEventObserver && this.undeleteEventObserver.off();
+ this.obsDefaultTimeChange && this.obsDefaultTimeChange.off();
+ }
+}
diff --git a/src/addon/calendar/lang/en.json b/src/addon/calendar/lang/en.json
index 6ccb04caaf5..ea79d56e51c 100644
--- a/src/addon/calendar/lang/en.json
+++ b/src/addon/calendar/lang/en.json
@@ -1,16 +1,59 @@
{
+ "allday": "All day",
"calendar": "Calendar",
+ "calendarevent": "Calendar event",
"calendarevents": "Calendar events",
"calendarreminders": "Calendar reminders",
+ "confirmeventdelete": "Are you sure you want to delete the \"{{$a}}\" event?",
+ "confirmeventseriesdelete": "The \"{{$a.name}}\" event is part of a series. Do you want to delete just this event, or all {{$a.count}} events in the series?",
+ "currentmonth": "Current Month",
+ "daynext": "Next day",
+ "dayprev": "Previous day",
"defaultnotificationtime": "Default notification time",
+ "deleteallevents": "Delete all events",
+ "deleteevent": "Delete event",
+ "deleteoneevent": "Delete this event",
+ "durationminutes": "Duration in minutes",
+ "durationnone": "Without duration",
+ "durationuntil": "Until",
+ "editevent": "Editing event",
"errorloadevent": "Error loading event.",
"errorloadevents": "Error loading events.",
+ "eventcalendareventdeleted": "Calendar event deleted",
+ "eventduration": "Duration",
"eventendtime": "End time",
+ "eventkind": "Type of event",
+ "eventname": "Event title",
"eventstarttime": "Start time",
+ "eventtype": "Event type",
+ "fri": "Fri",
+ "friday": "Friday",
"gotoactivity": "Go to activity",
+ "invalidtimedurationminutes": "The duration in minutes you have entered is invalid. Please enter the duration in minutes greater than 0 or select no duration.",
+ "invalidtimedurationuntil": "The date and time you selected for duration until is before the start time of the event. Please correct this before proceeding.",
+ "mon": "Mon",
+ "monday": "Monday",
+ "monthlyview": "Monthly view",
+ "newevent": "New event",
"noevents": "There are no events",
+ "nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event",
"reminders": "Reminders",
+ "repeatedevents": "Repeated events",
+ "repeateditall": "Also apply changes to the other {{$a}} events in this repeat series",
+ "repeateditthis": "Apply changes to this event only",
+ "repeatevent": "Repeat this event",
+ "repeatweeksl": "Repeat weekly, creating altogether",
+ "sat": "Sat",
+ "saturday": "Saturday",
"setnewreminder": "Set a new reminder",
+ "sun": "Sun",
+ "sunday": "Sunday",
+ "thu": "Thu",
+ "thursday": "Thursday",
+ "today": "Today",
+ "tomorrow": "Tomorrow",
+ "tue": "Tue",
+ "tuesday": "Tuesday",
"typeclose": "Close event",
"typecourse": "Course event",
"typecategory": "Category event",
@@ -19,5 +62,10 @@
"typegroup": "Group event",
"typeopen": "Open event",
"typesite": "Site event",
- "typeuser": "User event"
+ "typeuser": "User event",
+ "upcomingevents": "Upcoming events",
+ "wed": "Wed",
+ "wednesday": "Wednesday",
+ "when": "When",
+ "yesterday": "Yesterday"
}
\ No newline at end of file
diff --git a/src/addon/calendar/pages/day/day.html b/src/addon/calendar/pages/day/day.html
new file mode 100644
index 00000000000..34335a7dee5
--- /dev/null
+++ b/src/addon/calendar/pages/day/day.html
@@ -0,0 +1,75 @@
+
+
+ {{ 'addon.calendar.calendarevents' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ periodName }}
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'core.hasdatatosync' | translate:{$a: 'core.day' | translate} }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'core.notsent' | translate }}
+
+
+
+ {{ 'core.deletedoffline' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/addon/calendar/pages/day/day.module.ts b/src/addon/calendar/pages/day/day.module.ts
new file mode 100644
index 00000000000..c4e33dbb9df
--- /dev/null
+++ b/src/addon/calendar/pages/day/day.module.ts
@@ -0,0 +1,35 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicPageModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreComponentsModule } from '@components/components.module';
+import { CoreDirectivesModule } from '@directives/directives.module';
+import { CorePipesModule } from '@pipes/pipes.module';
+import { AddonCalendarDayPage } from './day';
+
+@NgModule({
+ declarations: [
+ AddonCalendarDayPage,
+ ],
+ imports: [
+ CoreComponentsModule,
+ CoreDirectivesModule,
+ CorePipesModule,
+ IonicPageModule.forChild(AddonCalendarDayPage),
+ TranslateModule.forChild()
+ ],
+})
+export class AddonCalendarDayPageModule {}
diff --git a/src/addon/calendar/pages/day/day.scss b/src/addon/calendar/pages/day/day.scss
new file mode 100644
index 00000000000..4918c1312ec
--- /dev/null
+++ b/src/addon/calendar/pages/day/day.scss
@@ -0,0 +1,9 @@
+page-addon-calendar-day {
+ .addon-calendar-period {
+ flex-grow: 3;
+ h3 {
+ margin-top: 10px;
+ font-size: 1.6rem;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/addon/calendar/pages/day/day.ts b/src/addon/calendar/pages/day/day.ts
new file mode 100644
index 00000000000..c3b566f889f
--- /dev/null
+++ b/src/addon/calendar/pages/day/day.ts
@@ -0,0 +1,702 @@
+// (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 OFx ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, OnInit, OnDestroy, NgZone } from '@angular/core';
+import { IonicPage, NavParams, NavController } from 'ionic-angular';
+import { CoreAppProvider } from '@providers/app';
+import { CoreEventsProvider } from '@providers/events';
+import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
+import { CoreSitesProvider } from '@providers/sites';
+import { CoreDomUtilsProvider } from '@providers/utils/dom';
+import { CoreTimeUtilsProvider } from '@providers/utils/time';
+import { AddonCalendarProvider } from '../../providers/calendar';
+import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline';
+import { AddonCalendarHelperProvider } from '../../providers/helper';
+import { AddonCalendarSyncProvider } from '../../providers/calendar-sync';
+import { CoreCoursesProvider } from '@core/courses/providers/courses';
+import { CoreCoursesHelperProvider } from '@core/courses/providers/helper';
+import { Network } from '@ionic-native/network';
+import * as moment from 'moment';
+
+/**
+ * Page that displays the calendar events for a certain day.
+ */
+@IonicPage({ segment: 'addon-calendar-day' })
+@Component({
+ selector: 'page-addon-calendar-day',
+ templateUrl: 'day.html',
+})
+export class AddonCalendarDayPage implements OnInit, OnDestroy {
+
+ protected currentSiteId: string;
+ protected year: number;
+ protected month: number;
+ protected day: number;
+ protected categories = {};
+ protected events = []; // Events (both online and offline).
+ protected onlineEvents = [];
+ protected offlineEvents = {}; // Offline events.
+ protected offlineEditedEventsIds = []; // IDs of events edited in offline.
+ protected deletedEvents = []; // Events deleted in offline.
+ protected timeFormat: string;
+ protected currentMoment: moment.Moment;
+ protected currentTime: number;
+
+ // Observers.
+ protected newEventObserver: any;
+ protected discardedObserver: any;
+ protected editEventObserver: any;
+ protected deleteEventObserver: any;
+ protected undeleteEventObserver: any;
+ protected syncObserver: any;
+ protected manualSyncObserver: any;
+ protected onlineObserver: any;
+ protected obsDefaultTimeChange: any;
+
+ periodName: string;
+ filteredEvents = [];
+ courseId: number;
+ categoryId: number;
+ canCreate = false;
+ courses: any[];
+ loaded = false;
+ hasOffline = false;
+ isOnline = false;
+ syncIcon: string;
+ isCurrentDay: boolean;
+ isPastDay: boolean;
+
+ constructor(localNotificationsProvider: CoreLocalNotificationsProvider,
+ navParams: NavParams,
+ network: Network,
+ zone: NgZone,
+ sitesProvider: CoreSitesProvider,
+ private navCtrl: NavController,
+ private domUtils: CoreDomUtilsProvider,
+ private timeUtils: CoreTimeUtilsProvider,
+ private calendarProvider: AddonCalendarProvider,
+ private calendarOffline: AddonCalendarOfflineProvider,
+ private calendarHelper: AddonCalendarHelperProvider,
+ private calendarSync: AddonCalendarSyncProvider,
+ private eventsProvider: CoreEventsProvider,
+ private coursesProvider: CoreCoursesProvider,
+ private coursesHelper: CoreCoursesHelperProvider,
+ private appProvider: CoreAppProvider) {
+
+ const now = new Date();
+
+ this.year = navParams.get('year') || now.getFullYear();
+ this.month = navParams.get('month') || (now.getMonth() + 1);
+ this.day = navParams.get('day') || now.getDate();
+ this.courseId = navParams.get('courseId');
+ this.currentSiteId = sitesProvider.getCurrentSiteId();
+
+ if (localNotificationsProvider.isAvailable()) {
+ // Re-schedule events if default time changes.
+ this.obsDefaultTimeChange = eventsProvider.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
+ calendarProvider.scheduleEventsNotifications(this.onlineEvents);
+ }, this.currentSiteId);
+ }
+
+ // Listen for events added. When an event is added, reload the data.
+ this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => {
+ if (data && data.event) {
+ this.loaded = false;
+ this.refreshData(true, false, true);
+ }
+ }, this.currentSiteId);
+
+ // Listen for new event discarded event. When it does, reload the data.
+ this.discardedObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => {
+ this.loaded = false;
+ this.refreshData(true, false, true);
+ }, this.currentSiteId);
+
+ // Listen for events edited. When an event is edited, reload the data.
+ this.editEventObserver = eventsProvider.on(AddonCalendarProvider.EDIT_EVENT_EVENT, (data) => {
+ if (data && data.event) {
+ this.loaded = false;
+ this.refreshData(true, false, true);
+ }
+ }, this.currentSiteId);
+
+ // Refresh data if calendar events are synchronized automatically.
+ this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, (data) => {
+ this.loaded = false;
+ this.refreshData(false, false, true);
+ }, this.currentSiteId);
+
+ // Refresh data if calendar events are synchronized manually but not by this page.
+ this.manualSyncObserver = eventsProvider.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data) => {
+ if (data && (data.source != 'day' || data.year != this.year || data.month != this.month || data.day != this.day)) {
+ this.loaded = false;
+ this.refreshData(false, false, true);
+ }
+ }, this.currentSiteId);
+
+ // Update the events when an event is deleted.
+ this.deleteEventObserver = eventsProvider.on(AddonCalendarProvider.DELETED_EVENT_EVENT, (data) => {
+ if (data && !data.sent) {
+ // Event was deleted in offline. Just mark it as deleted, no need to refresh.
+ this.hasOffline = this.markAsDeleted(data.eventId, true) || this.hasOffline;
+ this.deletedEvents.push(data.eventId);
+ } else {
+ this.loaded = false;
+ this.refreshData(false, false, true);
+ }
+ }, this.currentSiteId);
+
+ // Listen for events "undeleted" (offline).
+ this.undeleteEventObserver = eventsProvider.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, (data) => {
+ if (data && data.eventId) {
+ // Mark it as undeleted, no need to refresh.
+ const found = this.markAsDeleted(data.eventId, false);
+
+ // Remove it from the list of deleted events if it's there.
+ const index = this.deletedEvents.indexOf(data.eventId);
+ if (index != -1) {
+ this.deletedEvents.splice(index, 1);
+ }
+
+ if (found) {
+ // The deleted event belongs to current list. Re-calculate "hasOffline".
+ this.hasOffline = false;
+
+ if (this.events.length != this.onlineEvents.length) {
+ this.hasOffline = true;
+ } else {
+ const event = this.events.find((event) => {
+ return event.deleted || event.offline;
+ });
+
+ this.hasOffline = !!event;
+ }
+ }
+ }
+ }, this.currentSiteId);
+
+ // Refresh online status when changes.
+ this.onlineObserver = network.onchange().subscribe(() => {
+ // Execute the callback in the Angular zone, so change detection doesn't stop working.
+ zone.run(() => {
+ this.isOnline = this.appProvider.isOnline();
+ });
+ });
+ }
+
+ /**
+ * View loaded.
+ */
+ ngOnInit(): void {
+ this.calculateCurrentMoment();
+ this.calculateIsCurrentDay();
+
+ this.fetchData(true, false);
+ }
+
+ /**
+ * Fetch all the data required for the view.
+ *
+ * @param {boolean} [sync] Whether it should try to synchronize offline events.
+ * @param {boolean} [showErrors] Whether to show sync errors to the user.
+ * @return {Promise} Promise resolved when done.
+ */
+ fetchData(sync?: boolean, showErrors?: boolean): Promise {
+
+ this.syncIcon = 'spinner';
+ this.isOnline = this.appProvider.isOnline();
+
+ const promise = sync ? this.sync() : Promise.resolve();
+
+ return promise.then(() => {
+ const promises = [];
+
+ // Load courses for the popover.
+ promises.push(this.coursesHelper.getCoursesForPopover(this.courseId).then((data) => {
+ this.courses = data.courses;
+ this.categoryId = data.categoryId;
+ }));
+
+ // Get categories.
+ promises.push(this.loadCategories());
+
+ // Get offline events.
+ promises.push(this.calendarOffline.getAllEditedEvents().then((events) => {
+ // Format data.
+ events.forEach((event) => {
+ event.offline = true;
+ this.calendarHelper.formatEventData(event);
+ });
+
+ // Classify them by month & day.
+ this.offlineEvents = this.calendarHelper.classifyIntoMonths(events);
+
+ // // Get the IDs of events edited in offline.
+ const filtered = events.filter((event) => {
+ return event.id > 0;
+ });
+ this.offlineEditedEventsIds = filtered.map((event) => {
+ return event.id;
+ });
+ }));
+
+ // Get events deleted in offline.
+ promises.push(this.calendarOffline.getAllDeletedEventsIds().then((ids) => {
+ this.deletedEvents = ids;
+ }));
+
+ // Check if user can create events.
+ promises.push(this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => {
+ this.canCreate = canEdit;
+ }));
+
+ // Get user preferences.
+ promises.push(this.calendarProvider.getCalendarTimeFormat().then((value) => {
+ this.timeFormat = value;
+ }));
+
+ return Promise.all(promises);
+ }).then(() => {
+ return this.fetchEvents();
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
+ }).finally(() => {
+ this.loaded = true;
+ this.syncIcon = 'sync';
+ });
+ }
+
+ /**
+ * Fetch the events for current day.
+ *
+ * @return {Promise} Promise resolved when done.
+ */
+ fetchEvents(): Promise {
+ // Don't pass courseId and categoryId, we'll filter them locally.
+ return this.calendarProvider.getDayEvents(this.year, this.month, this.day).catch((error) => {
+ if (!this.appProvider.isOnline()) {
+ // Allow navigating to non-cached days in offline (behave as if using emergency cache).
+ return Promise.resolve({ events: [] });
+ } else {
+ return Promise.reject(error);
+ }
+ }).then((result) => {
+ const promises = [];
+
+ // Calculate the period name. We don't use the one in result because it's in server's language.
+ this.periodName = this.timeUtils.userDate(new Date(this.year, this.month - 1, this.day).getTime(),
+ 'core.strftimedaydate');
+
+ this.onlineEvents = result.events;
+ this.onlineEvents.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper));
+
+ // Schedule notifications for the events retrieved (only future events will be scheduled).
+ this.calendarProvider.scheduleEventsNotifications(this.onlineEvents);
+
+ // Merge the online events with offline data.
+ this.events = this.mergeEvents();
+
+ // Filter events by course.
+ this.filterEvents();
+
+ this.calculateIsCurrentDay();
+
+ // Re-calculate the formatted time so it uses the device date.
+ const dayTime = this.currentMoment.unix() * 1000;
+ this.events.forEach((event) => {
+ event.ispast = this.isPastDay || (this.isCurrentDay && this.isEventPast(event));
+ promises.push(this.calendarProvider.formatEventTime(event, this.timeFormat, true, dayTime).then((time) => {
+ event.formattedtime = time;
+ }));
+ });
+
+ return Promise.all(promises);
+ });
+ }
+
+ /**
+ * Merge online events with the offline events of that period.
+ *
+ * @return {any[]} Merged events.
+ */
+ protected mergeEvents(): any[] {
+ this.hasOffline = false;
+
+ if (!Object.keys(this.offlineEvents).length && !this.deletedEvents.length) {
+ // No offline events, nothing to merge.
+ return this.onlineEvents;
+ }
+
+ const monthOfflineEvents = this.offlineEvents[this.calendarHelper.getMonthId(this.year, this.month)],
+ dayOfflineEvents = monthOfflineEvents && monthOfflineEvents[this.day];
+ let result = this.onlineEvents;
+
+ if (this.deletedEvents.length) {
+ // Mark as deleted the events that were deleted in offline.
+ result.forEach((event) => {
+ event.deleted = this.deletedEvents.indexOf(event.id) != -1;
+
+ if (event.deleted) {
+ this.hasOffline = true;
+ }
+ });
+ }
+
+ if (this.offlineEditedEventsIds.length) {
+ // Remove the online events that were modified in offline.
+ result = result.filter((event) => {
+ return this.offlineEditedEventsIds.indexOf(event.id) == -1;
+ });
+
+ if (result.length != this.onlineEvents.length) {
+ this.hasOffline = true;
+ }
+ }
+
+ if (dayOfflineEvents && dayOfflineEvents.length) {
+ // Add the offline events (either new or edited).
+ this.hasOffline = true;
+ result = this.sortEvents(result.concat(dayOfflineEvents));
+ }
+
+ return result;
+ }
+
+ /**
+ * Filter events to only display events belonging to a certain course.
+ */
+ protected filterEvents(): void {
+ if (!this.courseId || this.courseId < 0) {
+ this.filteredEvents = this.events;
+ } else {
+ this.filteredEvents = this.events.filter((event) => {
+ return this.calendarHelper.shouldDisplayEvent(event, this.courseId, this.categoryId, this.categories);
+ });
+ }
+ }
+
+ /**
+ * Sort events by timestart.
+ *
+ * @param {any[]} events List to sort.
+ */
+ protected sortEvents(events: any[]): any[] {
+ return events.sort((a, b) => {
+ if (a.timestart == b.timestart) {
+ return a.timeduration - b.timeduration;
+ }
+
+ return a.timestart - b.timestart;
+ });
+ }
+
+ /**
+ * Refresh the data.
+ *
+ * @param {any} [refresher] Refresher.
+ * @param {Function} [done] Function to call when done.
+ * @param {boolean} [showErrors] Whether to show sync errors to the user.
+ * @return {Promise} Promise resolved when done.
+ */
+ doRefresh(refresher?: any, done?: () => void, showErrors?: boolean): Promise {
+ if (this.loaded) {
+ return this.refreshData(true, showErrors).finally(() => {
+ refresher && refresher.complete();
+ done && done();
+ });
+ }
+
+ return Promise.resolve();
+ }
+
+ /**
+ * Refresh the data.
+ *
+ * @param {boolean} [sync] Whether it should try to synchronize offline events.
+ * @param {boolean} [showErrors] Whether to show sync errors to the user.
+ * @param {boolean} [afterChange] Whether the refresh is done after an event has changed or has been synced.
+ * @return {Promise} Promise resolved when done.
+ */
+ refreshData(sync?: boolean, showErrors?: boolean, afterChange?: boolean): Promise {
+ this.syncIcon = 'spinner';
+
+ const promises = [];
+
+ // Don't invalidate day events after a change, it has already been handled.
+ if (!afterChange) {
+ promises.push(this.calendarProvider.invalidateDayEvents(this.year, this.month, this.day));
+ }
+ promises.push(this.calendarProvider.invalidateAllowedEventTypes());
+ promises.push(this.coursesProvider.invalidateCategories(0, true));
+ promises.push(this.calendarProvider.invalidateTimeFormat());
+
+ return Promise.all(promises).finally(() => {
+ return this.fetchData(sync, showErrors);
+ });
+ }
+
+ /**
+ * Load categories to be able to filter events.
+ *
+ * @return {Promise} Promise resolved when done.
+ */
+ protected loadCategories(): Promise {
+ return this.coursesProvider.getCategories(0, true).then((cats) => {
+ this.categories = {};
+
+ // Index categories by ID.
+ cats.forEach((category) => {
+ this.categories[category.id] = category;
+ });
+ }).catch(() => {
+ // Ignore errors.
+ });
+ }
+
+ /**
+ * Try to synchronize offline events.
+ *
+ * @param {boolean} [showErrors] Whether to show sync errors to the user.
+ * @return {Promise} Promise resolved when done.
+ */
+ protected sync(showErrors?: boolean): Promise {
+ return this.calendarSync.syncEvents().then((result) => {
+ if (result.warnings && result.warnings.length) {
+ this.domUtils.showErrorModal(result.warnings[0]);
+ }
+
+ if (result.updated) {
+ // Trigger a manual sync event.
+ result.source = 'day';
+ result.day = this.day;
+ result.month = this.month;
+ result.year = this.year;
+
+ this.eventsProvider.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId);
+ }
+ }).catch((error) => {
+ if (showErrors) {
+ this.domUtils.showErrorModalDefault(error, 'core.errorsync', true);
+ }
+ });
+ }
+
+ /**
+ * Navigate to a particular event.
+ *
+ * @param {number} eventId Event to load.
+ */
+ gotoEvent(eventId: number): void {
+ if (eventId < 0) {
+ // It's an offline event, go to the edit page.
+ this.openEdit(eventId);
+ } else {
+ this.navCtrl.push('AddonCalendarEventPage', {
+ id: eventId
+ });
+ }
+ }
+
+ /**
+ * Show the context menu.
+ *
+ * @param {MouseEvent} event Event.
+ */
+ openCourseFilter(event: MouseEvent): void {
+ this.coursesHelper.selectCourse(event, this.courses, this.courseId).then((result) => {
+ if (typeof result.courseId != 'undefined') {
+ this.courseId = result.courseId > 0 ? result.courseId : undefined;
+ this.categoryId = result.courseId > 0 ? result.categoryId : undefined;
+
+ // Course viewed has changed, check if the user can create events for this course calendar.
+ this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => {
+ this.canCreate = canEdit;
+ });
+
+ this.filterEvents();
+ }
+ });
+ }
+
+ /**
+ * Open page to create/edit an event.
+ *
+ * @param {number} [eventId] Event ID to edit.
+ */
+ openEdit(eventId?: number): void {
+ const params: any = {};
+
+ if (eventId) {
+ params.eventId = eventId;
+ } else {
+ // It's a new event, set the time.
+ params.timestamp = moment().year(this.year).month(this.month - 1).date(this.day).unix() * 1000;
+ }
+
+ if (this.courseId) {
+ params.courseId = this.courseId;
+ }
+
+ this.navCtrl.push('AddonCalendarEditEventPage', params);
+ }
+
+ /**
+ * Calculate current moment.
+ */
+ calculateCurrentMoment(): void {
+ this.currentMoment = moment().year(this.year).month(this.month - 1).date(this.day);
+ }
+
+ /**
+ * Check if user is viewing the current day.
+ */
+ calculateIsCurrentDay(): void {
+ const now = new Date();
+
+ this.currentTime = this.timeUtils.timestamp();
+
+ this.isCurrentDay = this.year == now.getFullYear() && this.month == now.getMonth() + 1 && this.day == now.getDate();
+ this.isPastDay = this.year < now.getFullYear() || (this.year == now.getFullYear() && this.month < now.getMonth()) ||
+ (this.year == now.getFullYear() && this.month == now.getMonth() + 1 && this.day < now.getDate());
+ }
+
+ /**
+ * Go to current day.
+ */
+ goToCurrentDay(): void {
+ const now = new Date(),
+ initialDay = this.day,
+ initialMonth = this.month,
+ initialYear = this.year;
+
+ this.day = now.getDate();
+ this.month = now.getMonth() + 1;
+ this.year = now.getFullYear();
+ this.calculateCurrentMoment();
+
+ this.loaded = false;
+
+ this.fetchEvents().then(() => {
+ this.isCurrentDay = true;
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
+
+ this.year = initialYear;
+ this.month = initialMonth;
+ this.day = initialDay;
+ this.calculateCurrentMoment();
+ }).finally(() => {
+ this.loaded = true;
+ });
+ }
+
+ /**
+ * Load next month.
+ */
+ loadNext(): void {
+ this.increaseDay();
+
+ this.loaded = false;
+
+ this.fetchEvents().catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
+ this.decreaseDay();
+ }).finally(() => {
+ this.loaded = true;
+ });
+ }
+
+ /**
+ * Load previous month.
+ */
+ loadPrevious(): void {
+ this.decreaseDay();
+
+ this.loaded = false;
+
+ this.fetchEvents().catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
+ this.increaseDay();
+ }).finally(() => {
+ this.loaded = true;
+ });
+ }
+
+ /**
+ * Decrease the current day.
+ */
+ protected decreaseDay(): void {
+ this.currentMoment.subtract(1, 'day');
+
+ this.year = this.currentMoment.year();
+ this.month = this.currentMoment.month() + 1;
+ this.day = this.currentMoment.date();
+ }
+
+ /**
+ * Increase the current day.
+ */
+ protected increaseDay(): void {
+ this.currentMoment.add(1, 'day');
+
+ this.year = this.currentMoment.year();
+ this.month = this.currentMoment.month() + 1;
+ this.day = this.currentMoment.date();
+ }
+
+ /**
+ * Find an event and mark it as deleted.
+ *
+ * @param {number} eventId Event ID.
+ * @param {boolean} deleted Whether to mark it as deleted or not.
+ * @return {boolean} Whether the event was found.
+ */
+ protected markAsDeleted(eventId: number, deleted: boolean): boolean {
+ const event = this.onlineEvents.find((event) => {
+ return event.id == eventId;
+ });
+
+ if (event) {
+ event.deleted = deleted;
+
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns if the event is in the past or not.
+ * @param {any} event Event object.
+ * @return {boolean} True if it's in the past.
+ */
+ isEventPast(event: any): boolean {
+ return (event.timestart + event.timeduration) < this.currentTime;
+ }
+
+ /**
+ * Page destroyed.
+ */
+ ngOnDestroy(): void {
+ this.newEventObserver && this.newEventObserver.off();
+ this.discardedObserver && this.discardedObserver.off();
+ this.editEventObserver && this.editEventObserver.off();
+ this.deleteEventObserver && this.deleteEventObserver.off();
+ this.undeleteEventObserver && this.undeleteEventObserver.off();
+ this.syncObserver && this.syncObserver.off();
+ this.manualSyncObserver && this.manualSyncObserver.off();
+ this.onlineObserver && this.onlineObserver.unsubscribe();
+ this.obsDefaultTimeChange && this.obsDefaultTimeChange.off();
+ }
+}
diff --git a/src/addon/calendar/pages/edit-event/edit-event.html b/src/addon/calendar/pages/edit-event/edit-event.html
new file mode 100644
index 00000000000..9d79a7e5d9f
--- /dev/null
+++ b/src/addon/calendar/pages/edit-event/edit-event.html
@@ -0,0 +1,159 @@
+
+
+ {{ title | translate }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/addon/calendar/pages/edit-event/edit-event.module.ts b/src/addon/calendar/pages/edit-event/edit-event.module.ts
new file mode 100644
index 00000000000..c5b20e969b5
--- /dev/null
+++ b/src/addon/calendar/pages/edit-event/edit-event.module.ts
@@ -0,0 +1,33 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicPageModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreComponentsModule } from '@components/components.module';
+import { CoreDirectivesModule } from '@directives/directives.module';
+import { AddonCalendarEditEventPage } from './edit-event';
+
+@NgModule({
+ declarations: [
+ AddonCalendarEditEventPage,
+ ],
+ imports: [
+ CoreComponentsModule,
+ CoreDirectivesModule,
+ IonicPageModule.forChild(AddonCalendarEditEventPage),
+ TranslateModule.forChild()
+ ],
+})
+export class AddonCalendarEditEventPageModule {}
diff --git a/src/addon/calendar/pages/edit-event/edit-event.scss b/src/addon/calendar/pages/edit-event/edit-event.scss
new file mode 100644
index 00000000000..6426ce3f192
--- /dev/null
+++ b/src/addon/calendar/pages/edit-event/edit-event.scss
@@ -0,0 +1,35 @@
+ion-app.app-root page-addon-calendar-edit-event {
+ .addon-calendar-radio-container ion-item:not(.addon-calendar-radio-title) {
+ &.item-ios {
+ @include padding-horizontal($item-ios-padding-start * 2, null);
+
+ ion-input {
+ @include padding-horizontal($datetime-ios-padding-start - $text-input-ios-margin-start, null);
+ }
+ }
+ &.item-md {
+ @include padding-horizontal($item-md-padding-start * 2, null);
+
+ ion-input {
+ @include padding-horizontal($datetime-md-padding-start - $text-input-md-margin-start, null);
+ }
+ }
+ &.item-wp {
+ @include padding-horizontal($item-wp-padding-start * 2, null);
+
+ ion-input {
+ @include padding-horizontal($datetime-wp-padding-start - $text-input-wp-margin-start, null);
+ }
+ }
+ }
+
+ .addon-calendar-eventtype-container.item-select-disabled {
+ ion-label, ion-select {
+ opacity: 1;
+ }
+
+ .select-icon {
+ display: none;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/addon/calendar/pages/edit-event/edit-event.ts b/src/addon/calendar/pages/edit-event/edit-event.ts
new file mode 100644
index 00000000000..b8d8ef9f409
--- /dev/null
+++ b/src/addon/calendar/pages/edit-event/edit-event.ts
@@ -0,0 +1,596 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, OnInit, OnDestroy, Optional, ViewChild } from '@angular/core';
+import { FormControl, FormGroup, FormBuilder, Validators } from '@angular/forms';
+import { IonicPage, NavController, NavParams } from 'ionic-angular';
+import { TranslateService } from '@ngx-translate/core';
+import { CoreEventsProvider } from '@providers/events';
+import { CoreGroupsProvider } from '@providers/groups';
+import { CoreSitesProvider } from '@providers/sites';
+import { CoreSyncProvider } from '@providers/sync';
+import { CoreDomUtilsProvider } from '@providers/utils/dom';
+import { CoreTextUtilsProvider } from '@providers/utils/text';
+import { CoreTimeUtilsProvider } from '@providers/utils/time';
+import { CoreUtilsProvider } from '@providers/utils/utils';
+import { CoreCoursesProvider } from '@core/courses/providers/courses';
+import { CoreSplitViewComponent } from '@components/split-view/split-view';
+import { CoreRichTextEditorComponent } from '@components/rich-text-editor/rich-text-editor.ts';
+import { AddonCalendarProvider } from '../../providers/calendar';
+import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline';
+import { AddonCalendarHelperProvider } from '../../providers/helper';
+import { AddonCalendarSyncProvider } from '../../providers/calendar-sync';
+import { CoreSite } from '@classes/site';
+
+/**
+ * Page that displays a form to create/edit an event.
+ */
+@IonicPage({ segment: 'addon-calendar-edit-event' })
+@Component({
+ selector: 'page-addon-calendar-edit-event',
+ templateUrl: 'edit-event.html',
+})
+export class AddonCalendarEditEventPage implements OnInit, OnDestroy {
+
+ @ViewChild(CoreRichTextEditorComponent) descriptionEditor: CoreRichTextEditorComponent;
+
+ title: string;
+ dateFormat: string;
+ component = AddonCalendarProvider.COMPONENT;
+ loaded = false;
+ hasOffline = false;
+ eventTypes = [];
+ categories = [];
+ courses = [];
+ groups = [];
+ loadingGroups = false;
+ courseGroupSet = false;
+ advanced = false;
+ errors: any;
+ event: any; // The event object (when editing an event).
+
+ // Form variables.
+ eventForm: FormGroup;
+ eventTypeControl: FormControl;
+ groupControl: FormControl;
+ descriptionControl: FormControl;
+
+ protected eventId: number;
+ protected courseId: number;
+ protected originalData: any;
+ protected currentSite: CoreSite;
+ protected types: any; // Object with the supported types.
+ protected showAll: boolean;
+ protected isDestroyed = false;
+ protected error = false;
+ protected gotEventData = false;
+
+ constructor(navParams: NavParams,
+ private navCtrl: NavController,
+ private translate: TranslateService,
+ private domUtils: CoreDomUtilsProvider,
+ private textUtils: CoreTextUtilsProvider,
+ private timeUtils: CoreTimeUtilsProvider,
+ private eventsProvider: CoreEventsProvider,
+ private groupsProvider: CoreGroupsProvider,
+ sitesProvider: CoreSitesProvider,
+ private coursesProvider: CoreCoursesProvider,
+ private utils: CoreUtilsProvider,
+ private calendarProvider: AddonCalendarProvider,
+ private calendarOffline: AddonCalendarOfflineProvider,
+ private calendarHelper: AddonCalendarHelperProvider,
+ private calendarSync: AddonCalendarSyncProvider,
+ private fb: FormBuilder,
+ private syncProvider: CoreSyncProvider,
+ @Optional() private svComponent: CoreSplitViewComponent) {
+
+ this.eventId = navParams.get('eventId');
+ this.courseId = navParams.get('courseId');
+ this.title = this.eventId ? 'addon.calendar.editevent' : 'addon.calendar.newevent';
+
+ const timestamp = navParams.get('timestamp');
+
+ this.currentSite = sitesProvider.getCurrentSite();
+ this.errors = {
+ required: this.translate.instant('core.required')
+ };
+
+ // Calculate format to use. ion-datetime doesn't support escaping characters ([]), so we remove them.
+ this.dateFormat = this.timeUtils.convertPHPToMoment(this.translate.instant('core.strftimedatetimeshort'))
+ .replace(/[\[\]]/g, '');
+
+ // Initialize form variables.
+ this.eventForm = new FormGroup({});
+ this.eventTypeControl = this.fb.control('', Validators.required);
+ this.groupControl = this.fb.control('');
+ this.descriptionControl = this.fb.control('');
+
+ const currentDate = this.timeUtils.toDatetimeFormat(timestamp);
+
+ this.eventForm.addControl('name', this.fb.control('', Validators.required));
+ this.eventForm.addControl('timestart', this.fb.control(currentDate, Validators.required));
+ this.eventForm.addControl('eventtype', this.eventTypeControl);
+ this.eventForm.addControl('categoryid', this.fb.control(''));
+ this.eventForm.addControl('courseid', this.fb.control(this.courseId));
+ this.eventForm.addControl('groupcourseid', this.fb.control(''));
+ this.eventForm.addControl('groupid', this.groupControl);
+ this.eventForm.addControl('description', this.descriptionControl);
+ this.eventForm.addControl('location', this.fb.control(''));
+ this.eventForm.addControl('duration', this.fb.control(0));
+ this.eventForm.addControl('timedurationuntil', this.fb.control(currentDate));
+ this.eventForm.addControl('timedurationminutes', this.fb.control(''));
+ this.eventForm.addControl('repeat', this.fb.control(false));
+ this.eventForm.addControl('repeats', this.fb.control('1'));
+ this.eventForm.addControl('repeateditall', this.fb.control(1));
+ }
+
+ /**
+ * Component being initialized.
+ */
+ ngOnInit(): void {
+ this.fetchData().finally(() => {
+ this.originalData = this.utils.clone(this.eventForm.value);
+ this.loaded = true;
+ });
+ }
+
+ /**
+ * Fetch the data needed to render the form.
+ *
+ * @param {boolean} [refresh] Whether it's refreshing data.
+ * @return {Promise} Promise resolved when done.
+ */
+ protected fetchData(refresh?: boolean): Promise {
+ let accessInfo;
+
+ this.error = false;
+
+ // Get access info.
+ return this.calendarProvider.getAccessInformation(this.courseId).then((info) => {
+ accessInfo = info;
+
+ return this.calendarProvider.getAllowedEventTypes(this.courseId);
+ }).then((types) => {
+ this.types = types;
+
+ const promises = [],
+ eventTypes = this.calendarHelper.getEventTypeOptions(types);
+
+ if (!eventTypes.length) {
+ return Promise.reject(this.translate.instant('addon.calendar.nopermissiontoupdatecalendar'));
+ }
+
+ if (this.eventId && !this.gotEventData) {
+ // Editing an event, get the event data. Wait for sync first.
+
+ promises.push(this.calendarSync.waitForSync(AddonCalendarSyncProvider.SYNC_ID).then(() => {
+ // Do not block if the scope is already destroyed.
+ if (!this.isDestroyed) {
+ this.syncProvider.blockOperation(AddonCalendarProvider.COMPONENT, this.eventId);
+ }
+
+ const promises = [];
+
+ // Get the event offline data if there's any.
+ promises.push(this.calendarOffline.getEvent(this.eventId).then((event) => {
+ this.hasOffline = true;
+
+ return event;
+ }).catch(() => {
+ // No offline data.
+ this.hasOffline = false;
+ }));
+
+ if (this.eventId > 0) {
+ // It's an online event. get its data from server.
+ promises.push(this.calendarProvider.getEventById(this.eventId).then((event) => {
+ this.event = event;
+ if (event && event.repeatid) {
+ event.othereventscount = event.eventcount ? event.eventcount - 1 : '';
+ }
+
+ return event;
+ }));
+ }
+
+ return Promise.all(promises).then((result) => {
+ this.gotEventData = true;
+
+ const event = result[0] || result[1]; // Use offline data first.
+
+ if (event) {
+ // Load the data in the form.
+ return this.loadEventData(event, !!result[0]);
+ }
+ });
+ }));
+ }
+
+ if (types.category) {
+ // Get the categories.
+ promises.push(this.coursesProvider.getCategories(0, true).then((cats) => {
+ this.categories = cats;
+ }));
+ }
+
+ this.showAll = this.utils.isTrueOrOne(this.currentSite.getStoredConfig('calendar_adminseesall')) &&
+ accessInfo.canmanageentries;
+
+ if (types.course || types.groups) {
+ // Get the courses.
+ const promise = this.showAll ? this.coursesProvider.getCoursesByField() : this.coursesProvider.getUserCourses();
+
+ promises.push(promise.then((courses) => {
+ if (this.showAll) {
+ // Remove site home from the list of courses.
+ const siteHomeId = this.currentSite.getSiteHomeId();
+ courses = courses.filter((course) => {
+ return course.id != siteHomeId;
+ });
+ }
+
+ // Format the name of the courses.
+ const subPromises = [];
+ courses.forEach((course) => {
+ subPromises.push(this.textUtils.formatText(course.fullname).then((text) => {
+ course.fullname = text;
+ }).catch(() => {
+ // Ignore errors.
+ }));
+ });
+
+ return Promise.all(subPromises).then(() => {
+ // Sort courses by name.
+ this.courses = courses.sort((a, b) => {
+ const compareA = a.fullname.toLowerCase(),
+ compareB = b.fullname.toLowerCase();
+
+ return compareA.localeCompare(compareB);
+ });
+ });
+ }));
+ }
+
+ return Promise.all(promises).then(() => {
+ if (!this.eventTypeControl.value) {
+ // Initialize event type value. If course is allowed, select it first.
+ if (types.course) {
+ this.eventTypeControl.setValue(AddonCalendarProvider.TYPE_COURSE);
+ } else {
+ this.eventTypeControl.setValue(eventTypes[0].value);
+ }
+ }
+
+ this.eventTypes = eventTypes;
+ });
+
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'Error getting data.');
+ this.error = true;
+
+ if (!this.svComponent || !this.svComponent.isOn()) {
+ this.originalData = null; // Avoid asking for confirmation.
+ this.navCtrl.pop();
+ }
+ });
+ }
+
+ /**
+ * Load an event data into the form.
+ *
+ * @param {any} event Event data.
+ * @param {boolean} isOffline Whether the data is from offline or not.
+ * @return {Promise} Promise resolved when done.
+ */
+ protected loadEventData(event: any, isOffline: boolean): Promise {
+ const courseId = event.course ? event.course.id : event.courseid;
+
+ this.eventForm.controls.name.setValue(event.name);
+ this.eventForm.controls.timestart.setValue(this.timeUtils.toDatetimeFormat(event.timestart * 1000));
+ this.eventForm.controls.eventtype.setValue(event.eventtype);
+ this.eventForm.controls.categoryid.setValue(event.categoryid || '');
+ this.eventForm.controls.courseid.setValue(courseId || '');
+ this.eventForm.controls.groupcourseid.setValue(event.groupcourseid || courseId || '');
+ this.eventForm.controls.groupid.setValue(event.groupid || '');
+ this.eventForm.controls.description.setValue(event.description);
+ this.eventForm.controls.location.setValue(event.location);
+
+ if (isOffline) {
+ // It's an offline event, use the data as it is.
+ this.eventForm.controls.duration.setValue(event.duration);
+ this.eventForm.controls.timedurationuntil.setValue(
+ this.timeUtils.toDatetimeFormat((event.timedurationuntil * 1000) || Date.now()));
+ this.eventForm.controls.timedurationminutes.setValue(event.timedurationminutes || '');
+ this.eventForm.controls.repeat.setValue(!!event.repeat);
+ this.eventForm.controls.repeats.setValue(event.repeats || '1');
+ this.eventForm.controls.repeateditall.setValue(event.repeateditall || 1);
+ } else {
+ // Online event, we'll have to calculate the data.
+
+ if (event.timeduration > 0) {
+ this.eventForm.controls.duration.setValue(1);
+ this.eventForm.controls.timedurationuntil.setValue(this.timeUtils.toDatetimeFormat(
+ (event.timestart + event.timeduration) * 1000));
+ } else {
+ // No duration.
+ this.eventForm.controls.duration.setValue(0);
+ this.eventForm.controls.timedurationuntil.setValue(this.timeUtils.toDatetimeFormat());
+ }
+
+ this.eventForm.controls.timedurationminutes.setValue('');
+ this.eventForm.controls.repeat.setValue(!!event.repeatid);
+ this.eventForm.controls.repeats.setValue(event.eventcount || '1');
+ this.eventForm.controls.repeateditall.setValue(1);
+ }
+
+ if (event.eventtype == 'group' && courseId) {
+ return this.loadGroups(courseId);
+ }
+
+ return Promise.resolve();
+ }
+
+ /**
+ * Pull to refresh.
+ *
+ * @param {any} refresher Refresher.
+ */
+ refreshData(refresher: any): void {
+ const promises = [
+ this.calendarProvider.invalidateAccessInformation(this.courseId),
+ this.calendarProvider.invalidateAllowedEventTypes(this.courseId)
+ ];
+
+ if (this.types) {
+ if (this.types.category) {
+ promises.push(this.coursesProvider.invalidateCategories(0, true));
+ }
+ if (this.types.course || this.types.groups) {
+ if (this.showAll) {
+ promises.push(this.coursesProvider.invalidateCoursesByField());
+ } else {
+ promises.push(this.coursesProvider.invalidateUserCourses());
+ }
+ }
+ }
+
+ Promise.all(promises).finally(() => {
+ this.fetchData(true).finally(() => {
+ refresher.complete();
+ });
+ });
+ }
+
+ /**
+ * A course was selected, get its groups.
+ *
+ * @param {number} courseId Course ID.
+ */
+ groupCourseSelected(courseId: number): void {
+ if (!courseId) {
+ return;
+ }
+
+ const modal = this.domUtils.showModalLoading();
+
+ this.loadGroups(courseId).then(() => {
+ this.groupControl.setValue('');
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'Error getting data.');
+ }).finally(() => {
+ modal.dismiss();
+ });
+ }
+
+ /**
+ * Load groups of a certain course.
+ *
+ * @param {number} courseId Course ID.
+ * @return {Promise} Promise resolved when done.
+ */
+ protected loadGroups(courseId: number): Promise {
+ this.loadingGroups = true;
+
+ return this.groupsProvider.getUserGroupsInCourse(courseId).then((groups) => {
+ this.groups = groups;
+ this.courseGroupSet = true;
+ }).finally(() => {
+ this.loadingGroups = false;
+ });
+ }
+
+ /**
+ * Show or hide advanced form fields.
+ */
+ toggleAdvanced(): void {
+ this.advanced = !this.advanced;
+ }
+
+ /**
+ * Create the event.
+ */
+ submit(): void {
+ // Validate data.
+ const formData = this.eventForm.value,
+ timeStartDate = this.timeUtils.convertToTimestamp(formData.timestart),
+ timeUntilDate = this.timeUtils.convertToTimestamp(formData.timedurationuntil),
+ timeDurationMinutes = parseInt(formData.timedurationminutes || '', 10);
+ let error;
+
+ if (formData.eventtype == AddonCalendarProvider.TYPE_COURSE && !formData.courseid) {
+ error = 'core.selectacourse';
+ } else if (formData.eventtype == AddonCalendarProvider.TYPE_GROUP && !formData.groupcourseid) {
+ error = 'core.selectacourse';
+ } else if (formData.eventtype == AddonCalendarProvider.TYPE_GROUP && !formData.groupid) {
+ error = 'core.selectagroup';
+ } else if (formData.eventtype == AddonCalendarProvider.TYPE_CATEGORY && !formData.categoryid) {
+ error = 'core.selectacategory';
+ } else if (formData.duration == 1 && timeStartDate > timeUntilDate) {
+ error = 'addon.calendar.invalidtimedurationuntil';
+ } else if (formData.duration == 2 && (isNaN(timeDurationMinutes) || timeDurationMinutes < 1)) {
+ error = 'addon.calendar.invalidtimedurationminutes';
+ }
+
+ if (error) {
+ // Show error and stop.
+ this.domUtils.showErrorModal(this.translate.instant(error));
+
+ return;
+ }
+
+ // Format the data to send.
+ const data: any = {
+ name: formData.name,
+ eventtype: formData.eventtype,
+ timestart: timeStartDate,
+ description: {
+ text: formData.description,
+ format: 1
+ },
+ location: formData.location,
+ duration: formData.duration,
+ repeat: formData.repeat
+ };
+
+ if (formData.eventtype == AddonCalendarProvider.TYPE_COURSE) {
+ data.courseid = formData.courseid;
+ } else if (formData.eventtype == AddonCalendarProvider.TYPE_GROUP) {
+ data.groupcourseid = formData.groupcourseid;
+ data.groupid = formData.groupid;
+ } else if (formData.eventtype == AddonCalendarProvider.TYPE_CATEGORY) {
+ data.categoryid = formData.categoryid;
+ }
+
+ if (formData.duration == 1) {
+ data.timedurationuntil = timeUntilDate;
+ } else if (formData.duration == 2) {
+ data.timedurationminutes = formData.timedurationminutes;
+ }
+
+ if (formData.repeat) {
+ data.repeats = Number(formData.repeats);
+ }
+
+ if (this.event && this.event.repeatid) {
+ data.repeatid = this.event.repeatid;
+ data.repeateditall = formData.repeateditall;
+ }
+
+ // Send the data.
+ const modal = this.domUtils.showModalLoading('core.sending', true);
+ let event;
+
+ this.calendarProvider.submitEvent(this.eventId, data).then((result) => {
+ event = result.event;
+
+ if (result.sent) {
+ // Event created or edited, invalidate right days & months.
+ const numberOfRepetitions = formData.repeat ? formData.repeats :
+ (data.repeateditall && this.event.othereventscount ? this.event.othereventscount + 1 : 1);
+
+ return this.calendarHelper.refreshAfterChangeEvent(result.event, numberOfRepetitions).catch(() => {
+ // Ignore errors.
+ });
+ }
+ }).then(() => {
+ this.returnToList(event);
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'Error sending data.');
+ }).finally(() => {
+ modal.dismiss();
+ });
+ }
+
+ /**
+ * Convenience function to update or return to event list depending on device.
+ *
+ * @param {number} [event] Event.
+ */
+ protected returnToList(event?: any): void {
+ // Unblock the sync because the view will be destroyed and the sync process could be triggered before ngOnDestroy.
+ this.unblockSync();
+
+ if (this.eventId > 0) {
+ // Editing an event.
+ const data: any = {
+ event: event
+ };
+ this.eventsProvider.trigger(AddonCalendarProvider.EDIT_EVENT_EVENT, data, this.currentSite.getId());
+ } else {
+ if (event) {
+ const data: any = {
+ event: event
+ };
+ this.eventsProvider.trigger(AddonCalendarProvider.NEW_EVENT_EVENT, data, this.currentSite.getId());
+ } else {
+ this.eventsProvider.trigger(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, {}, this.currentSite.getId());
+ }
+ }
+
+ if (this.svComponent && this.svComponent.isOn()) {
+ // Empty form.
+ this.hasOffline = false;
+ this.eventForm.reset(this.originalData);
+ this.originalData = this.utils.clone(this.eventForm.value);
+ } else {
+ this.originalData = null; // Avoid asking for confirmation.
+ this.navCtrl.pop();
+ }
+ }
+
+ /**
+ * Discard an offline saved discussion.
+ */
+ discard(): void {
+ this.domUtils.showConfirm(this.translate.instant('core.areyousure')).then(() => {
+ this.calendarOffline.deleteEvent(this.eventId).then(() => {
+ this.returnToList();
+ }).catch(() => {
+ // Shouldn't happen.
+ this.domUtils.showErrorModal('Error discarding event.');
+ });
+ }).catch(() => {
+ // Cancelled.
+ });
+ }
+
+ /**
+ * Check if we can leave the page or not.
+ *
+ * @return {boolean|Promise} Resolved if we can leave it, rejected if not.
+ */
+ ionViewCanLeave(): boolean | Promise {
+
+ if (this.calendarHelper.hasEventDataChanged(this.eventForm.value, this.originalData)) {
+ // Show confirmation if some data has been modified.
+ return this.domUtils.showConfirm(this.translate.instant('core.confirmcanceledit'));
+ } else {
+ return Promise.resolve();
+ }
+ }
+
+ protected unblockSync(): void {
+ if (this.eventId) {
+ this.syncProvider.unblockOperation(AddonCalendarProvider.COMPONENT, this.eventId);
+ }
+ }
+
+ /**
+ * Page destroyed.
+ */
+ ngOnDestroy(): void {
+ this.unblockSync();
+ this.isDestroyed = true;
+ }
+}
diff --git a/src/addon/calendar/pages/event/event.html b/src/addon/calendar/pages/event/event.html
index 3c40c931aff..366327f6661 100644
--- a/src/addon/calendar/pages/event/event.html
+++ b/src/addon/calendar/pages/event/event.html
@@ -1,39 +1,69 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+ {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendarevent' | translate} }}
+
+
-
+
+
-
+ {{ 'addon.calendar.eventname' | translate }}
+
+
+ {{ 'core.deletedoffline' | translate }}
+
-
- {{ 'addon.calendar.eventstarttime' | translate}}
- {{ event.timestart * 1000 | coreFormatDate }}
+
+ {{ 'addon.calendar.when' | translate }}
+
+
+ {{ 'core.deletedoffline' | translate }}
+
- 0">
- {{ 'addon.calendar.eventendtime' | translate}}
- {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate }}
+
+ {{ 'addon.calendar.eventtype' | translate }}
+ {{ 'addon.calendar.type' + event.formattedType | translate }}
{{ 'core.course' | translate}}
+
+ {{ 'core.group' | translate}}
+ {{ groupName }}
+
{{ 'core.category' | translate}}
-
- {{event.moduleName}}
-
+ {{ 'core.description' | translate}}
diff --git a/src/addon/calendar/pages/event/event.scss b/src/addon/calendar/pages/event/event.scss
new file mode 100644
index 00000000000..6a991373758
--- /dev/null
+++ b/src/addon/calendar/pages/event/event.scss
@@ -0,0 +1,5 @@
+ion-app.app-root page-addon-calendar-event {
+ .card ion-note {
+ font-size: 1.6rem;
+ }
+}
diff --git a/src/addon/calendar/pages/event/event.ts b/src/addon/calendar/pages/event/event.ts
index 568eac629a8..8edc9b96eac 100644
--- a/src/addon/calendar/pages/event/event.ts
+++ b/src/addon/calendar/pages/event/event.ts
@@ -12,18 +12,25 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, ViewChild } from '@angular/core';
-import { IonicPage, Content, NavParams } from 'ionic-angular';
+import { Component, ViewChild, Optional, OnDestroy, NgZone } from '@angular/core';
+import { IonicPage, Content, NavParams, NavController } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { AddonCalendarProvider } from '../../providers/calendar';
import { AddonCalendarHelperProvider } from '../../providers/helper';
+import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline';
+import { AddonCalendarSyncProvider } from '../../providers/calendar-sync';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
+import { CoreAppProvider } from '@providers/app';
+import { CoreEventsProvider } from '@providers/events';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreSitesProvider } from '@providers/sites';
import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
+import { CoreGroupsProvider } from '@providers/groups';
+import { CoreSplitViewComponent } from '@components/split-view/split-view';
+import { Network } from '@ionic-native/network';
/**
* Page that displays a single calendar event.
@@ -33,19 +40,25 @@ import { CoreTimeUtilsProvider } from '@providers/utils/time';
selector: 'page-addon-calendar-event',
templateUrl: 'event.html',
})
-export class AddonCalendarEventPage {
+export class AddonCalendarEventPage implements OnDestroy {
@ViewChild(Content) content: Content;
protected eventId;
protected siteHomeId: number;
+ protected editEventObserver: any;
+ protected syncObserver: any;
+ protected manualSyncObserver: any;
+ protected onlineObserver: any;
+ protected currentSiteId: string;
+
eventLoaded: boolean;
notificationFormat: string;
notificationMin: string;
notificationMax: string;
notificationTimeText: string;
event: any = {};
- title: string;
courseName: string;
+ groupName: string;
courseUrl = '';
notificationsEnabled = false;
moduleUrl = '';
@@ -53,16 +66,33 @@ export class AddonCalendarEventPage {
currentTime: number;
defaultTime: number;
reminders: any[];
+ canEdit = false;
+ canDelete = false;
+ hasOffline = false;
+ isOnline = false;
+ syncIcon: string; // Sync icon.
+ isSplitViewOn = false;
constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams,
private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider,
private calendarHelper: AddonCalendarHelperProvider, private sitesProvider: CoreSitesProvider,
localNotificationsProvider: CoreLocalNotificationsProvider, private courseProvider: CoreCourseProvider,
- private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider) {
+ private textUtils: CoreTextUtilsProvider, private timeUtils: CoreTimeUtilsProvider,
+ private groupsProvider: CoreGroupsProvider, @Optional() private svComponent: CoreSplitViewComponent,
+ private navCtrl: NavController, private eventsProvider: CoreEventsProvider, network: Network, zone: NgZone,
+ private calendarSync: AddonCalendarSyncProvider, private appProvider: CoreAppProvider,
+ private calendarOffline: AddonCalendarOfflineProvider) {
this.eventId = navParams.get('id');
this.notificationsEnabled = localNotificationsProvider.isAvailable();
this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId();
+ this.currentSiteId = sitesProvider.getCurrentSiteId();
+ this.isSplitViewOn = this.svComponent && this.svComponent.isOn();
+
+ // Check if site supports editing and deleting. No need to check allowed types, event.canedit already does it.
+ this.canEdit = this.calendarProvider.canEditEventsInSite();
+ this.canDelete = this.calendarProvider.canDeleteEventsInSite();
+
if (this.notificationsEnabled) {
this.calendarProvider.getEventReminders(this.eventId).then((reminders) => {
this.reminders = reminders;
@@ -72,38 +102,123 @@ export class AddonCalendarEventPage {
this.defaultTime = defaultTime * 60;
});
- // Calculate format to use. ion-datetime doesn't support escaping characters ([]), so we remove them.
- this.notificationFormat = this.timeUtils.convertPHPToMoment(this.translate.instant('core.strftimedatetimeshort'))
- .replace(/[\[\]]/g, '');
+ // Calculate format to use.
+ this.notificationFormat = this.timeUtils.fixFormatForDatetime(this.timeUtils.convertPHPToMoment(
+ this.translate.instant('core.strftimedatetime')));
}
+
+ // Listen for event edited. If current event is edited, reload the data.
+ this.editEventObserver = eventsProvider.on(AddonCalendarProvider.EDIT_EVENT_EVENT, (data) => {
+ if (data && data.event && data.event.id == this.eventId) {
+ this.eventLoaded = false;
+ this.refreshEvent(true, false);
+ }
+ }, this.currentSiteId);
+
+ // Refresh data if this calendar event is synchronized automatically.
+ this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, this.checkSyncResult.bind(this, false),
+ this.currentSiteId);
+
+ // Refresh data if calendar events are synchronized manually but not by this page.
+ this.manualSyncObserver = eventsProvider.on(AddonCalendarSyncProvider.MANUAL_SYNCED, this.checkSyncResult.bind(this, true),
+ this.currentSiteId);
+
+ // Refresh online status when changes.
+ this.onlineObserver = network.onchange().subscribe(() => {
+ // Execute the callback in the Angular zone, so change detection doesn't stop working.
+ zone.run(() => {
+ this.isOnline = this.appProvider.isOnline();
+ });
+ });
}
/**
* View loaded.
*/
ionViewDidLoad(): void {
- this.fetchEvent().finally(() => {
- this.eventLoaded = true;
- });
+ this.syncIcon = 'spinner';
+
+ this.fetchEvent();
}
/**
* Fetches the event and updates the view.
*
+ * @param {boolean} [sync] Whether it should try to synchronize offline events.
+ * @param {boolean} [showErrors] Whether to show sync errors to the user.
* @return {Promise} Promise resolved when done.
*/
- fetchEvent(): Promise {
+ fetchEvent(sync?: boolean, showErrors?: boolean): Promise {
const currentSite = this.sitesProvider.getCurrentSite(),
- canGetById = this.calendarProvider.isGetEventByIdAvailable();
- let promise;
+ canGetById = this.calendarProvider.isGetEventByIdAvailableInSite();
+ let promise,
+ deleted = false;
+
+ this.isOnline = this.appProvider.isOnline();
- if (canGetById) {
- promise = this.calendarProvider.getEventById(this.eventId);
+ if (sync) {
+ // Try to synchronize offline events.
+ promise = this.calendarSync.syncEvents().then((result) => {
+ if (result.warnings && result.warnings.length) {
+ this.domUtils.showErrorModal(result.warnings[0]);
+ }
+
+ if (result.deleted && result.deleted.indexOf(this.eventId) != -1) {
+ // This event was deleted during the sync.
+ deleted = true;
+ }
+
+ if (result.updated) {
+ // Trigger a manual sync event.
+ result.source = 'event';
+
+ this.eventsProvider.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId);
+ }
+ }).catch((error) => {
+ if (showErrors) {
+ this.domUtils.showErrorModalDefault(error, 'core.errorsync', true);
+ }
+ });
} else {
- promise = this.calendarProvider.getEvent(this.eventId);
+ promise = Promise.resolve();
}
- return promise.then((event) => {
+ return promise.then(() => {
+ if (deleted) {
+ return;
+ }
+
+ const promises = [];
+
+ // Get the event data.
+ if (canGetById) {
+ promises.push(this.calendarProvider.getEventById(this.eventId));
+ } else {
+ promises.push(this.calendarProvider.getEvent(this.eventId));
+ }
+
+ // Get offline data.
+ promises.push(this.calendarOffline.getEvent(this.eventId).catch(() => {
+ // No offline data.
+ }));
+
+ return Promise.all(promises).then((results) => {
+ if (results[1]) {
+ // There is offline data, apply it.
+ this.hasOffline = true;
+ Object.assign(results[0], results[1]);
+ } else {
+ this.hasOffline = false;
+ }
+
+ return results[0];
+ });
+
+ }).then((event) => {
+ if (deleted) {
+ return;
+ }
+
const promises = [];
this.calendarHelper.formatEventData(event);
@@ -120,8 +235,6 @@ export class AddonCalendarEventPage {
this.courseUrl = '';
this.moduleUrl = '';
- // Guess event title.
- let title = this.translate.instant('addon.calendar.type' + event.eventtype);
if (event.moduleIcon) {
// It's a module event, translate the module name to the current language.
const name = this.courseProvider.translateModuleName(event.modulename);
@@ -129,29 +242,14 @@ export class AddonCalendarEventPage {
event.moduleName = name;
}
- // Calculate the title of the page;
- if (title == 'addon.calendar.type' + event.eventtype) {
- title = this.translate.instant('core.mod_' + event.modulename + '.' + event.eventtype);
-
- if (title == 'core.mod_' + event.modulename + '.' + event.eventtype) {
- title = name;
- }
- }
-
// Get the module URL.
if (canGetById) {
this.moduleUrl = event.url;
}
- } else {
- if (title == 'addon.calendar.type' + event.eventtype) {
- title = event.name;
- }
}
- this.title = title;
-
// If the event belongs to a course, get the course name and the URL to view it.
- if (canGetById && event.course) {
+ if (canGetById && event.course && event.course.id != this.siteHomeId) {
this.courseName = event.course.fullname;
this.courseUrl = event.course.viewurl;
} else if (event.courseid && event.courseid != this.siteHomeId) {
@@ -165,7 +263,22 @@ export class AddonCalendarEventPage {
}));
}
- if (canGetById && event.iscategoryevent) {
+ // If it's a group event, get the name of the group.
+ const courseId = canGetById && event.course ? event.course.id : event.courseid;
+ if (courseId && event.groupid) {
+ promises.push(this.groupsProvider.getUserGroupsInCourse(event.courseid).then((groups) => {
+ const group = groups.find((group) => {
+ return group.id == event.groupid;
+ });
+
+ this.groupName = group ? group.name : '';
+ }).catch(() => {
+ // Error getting groups, just don't show the group name.
+ this.groupName = '';
+ }));
+ }
+
+ if (canGetById && event.iscategoryevent && event.category) {
this.categoryPath = event.category.nestedname;
}
@@ -175,9 +288,24 @@ export class AddonCalendarEventPage {
event.encodedLocation = this.textUtils.buildAddressURL(event.location);
}
+ // Check if event was deleted in offine.
+ promises.push(this.calendarOffline.isEventDeleted(this.eventId).then((deleted) => {
+ event.deleted = deleted;
+ }));
+
+ // Re-calculate the formatted time so it uses the device date.
+ promises.push(this.calendarProvider.getCalendarTimeFormat().then((timeFormat) => {
+ this.calendarProvider.formatEventTime(event, timeFormat).then((time) => {
+ event.formattedtime = time;
+ });
+ }));
+
return Promise.all(promises);
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevent', true);
+ }).finally(() => {
+ this.eventLoaded = true;
+ this.syncIcon = 'sync';
});
}
@@ -228,16 +356,195 @@ export class AddonCalendarEventPage {
});
}
+ /**
+ * Refresh the data.
+ *
+ * @param {any} [refresher] Refresher.
+ * @param {Function} [done] Function to call when done.
+ * @param {boolean} [showErrors] Whether to show sync errors to the user.
+ * @return {Promise} Promise resolved when done.
+ */
+ doRefresh(refresher?: any, done?: () => void, showErrors?: boolean): Promise {
+ if (this.eventLoaded) {
+ return this.refreshEvent(true, showErrors).finally(() => {
+ refresher && refresher.complete();
+ done && done();
+ });
+ }
+
+ return Promise.resolve();
+ }
+
/**
* Refresh the event.
*
- * @param {any} refresher Refresher.
+ * @param {boolean} [sync] Whether it should try to synchronize offline events.
+ * @param {boolean} [showErrors] Whether to show sync errors to the user.
+ * @return {Promise} Promise resolved when done.
+ */
+ refreshEvent(sync?: boolean, showErrors?: boolean): Promise {
+ this.syncIcon = 'spinner';
+
+ const promises = [];
+
+ promises.push(this.calendarProvider.invalidateEvent(this.eventId));
+ promises.push(this.calendarProvider.invalidateTimeFormat());
+
+ return Promise.all(promises).catch(() => {
+ // Ignore errors.
+ }).then(() => {
+ return this.fetchEvent(sync, showErrors);
+ });
+ }
+
+ /**
+ * Open the page to edit the event.
+ */
+ openEdit(): void {
+ // Decide which navCtrl to use. If this page is inside a split view, use the split view's master nav.
+ const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
+ navCtrl.push('AddonCalendarEditEventPage', {eventId: this.eventId});
+ }
+
+ /**
+ * Delete the event.
*/
- refreshEvent(refresher: any): void {
- this.calendarProvider.invalidateEvent(this.eventId).finally(() => {
- this.fetchEvent().finally(() => {
- refresher.complete();
+ deleteEvent(): void {
+ const title = this.translate.instant('addon.calendar.deleteevent'),
+ options: any = {};
+ let message: string;
+
+ if (this.event.eventcount > 1) {
+ // It's a repeated event.
+ message = this.translate.instant('addon.calendar.confirmeventseriesdelete',
+ {$a: {name: this.event.name, count: this.event.eventcount}});
+
+ options.inputs = [
+ {
+ type: 'radio',
+ name: 'deleteall',
+ checked: true,
+ value: false,
+ label: this.translate.instant('addon.calendar.deleteoneevent')
+ },
+ {
+ type: 'radio',
+ name: 'deleteall',
+ checked: false,
+ value: true,
+ label: this.translate.instant('addon.calendar.deleteallevents')
+ }
+ ];
+ } else {
+ // Not repeated, display a simple confirm.
+ message = this.translate.instant('addon.calendar.confirmeventdelete', {$a: this.event.name});
+ }
+
+ this.domUtils.showConfirm(message, title, undefined, undefined, options).then((deleteAll) => {
+
+ const modal = this.domUtils.showModalLoading('core.sending', true);
+
+ this.calendarProvider.deleteEvent(this.event.id, this.event.name, deleteAll).then((sent) => {
+ let promise;
+
+ if (sent) {
+ // Event deleted, invalidate right days & months.
+ promise = this.calendarHelper.refreshAfterChangeEvent(this.event, deleteAll ? this.event.eventcount : 1)
+ .catch(() => {
+ // Ignore errors.
+ });
+ } else {
+ promise = Promise.resolve();
+ }
+
+ return promise.then(() => {
+ // Trigger an event.
+ this.eventsProvider.trigger(AddonCalendarProvider.DELETED_EVENT_EVENT, {
+ eventId: this.eventId,
+ sent: sent
+ }, this.sitesProvider.getCurrentSiteId());
+
+ if (sent) {
+ this.domUtils.showToast('addon.calendar.eventcalendareventdeleted', true, 3000, undefined, false);
+
+ // Event deleted, close the view.
+ if (!this.svComponent || !this.svComponent.isOn()) {
+ this.navCtrl.pop();
+ }
+ } else {
+ // Event deleted in offline, just mark it as deleted.
+ this.event.deleted = true;
+ }
+ });
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'Error deleting event.');
+ }).finally(() => {
+ modal.dismiss();
});
+ }, () => {
+ // User canceled.
});
}
+
+ /**
+ * Undo delete the event.
+ */
+ undoDelete(): void {
+ const modal = this.domUtils.showModalLoading('core.sending', true);
+
+ this.calendarOffline.unmarkDeleted(this.event.id).then(() => {
+
+ // Trigger an event.
+ this.eventsProvider.trigger(AddonCalendarProvider.UNDELETED_EVENT_EVENT, {
+ eventId: this.eventId
+ }, this.sitesProvider.getCurrentSiteId());
+
+ this.event.deleted = false;
+
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'Error undeleting event.');
+ }).finally(() => {
+ modal.dismiss();
+ });
+ }
+
+ /**
+ * Check the result of an automatic sync or a manual sync not done by this page.
+ *
+ * @param {boolean} isManual Whether it's a manual sync.
+ * @param {any} data Sync result.
+ */
+ protected checkSyncResult(isManual: boolean, data: any): void {
+ if (!data) {
+ return;
+ }
+
+ if (data.deleted && data.deleted.indexOf(this.eventId) != -1) {
+ this.domUtils.showToast('addon.calendar.eventcalendareventdeleted', true, 3000, undefined, false);
+
+ // Event was deleted, close the view.
+ if (!this.svComponent || !this.svComponent.isOn()) {
+ this.navCtrl.pop();
+ }
+ } else if (data.events && (!isManual || data.source != 'event')) {
+ const event = data.events.find((ev) => {
+ return ev.id == this.eventId;
+ });
+
+ if (event) {
+ this.eventLoaded = false;
+ this.refreshEvent();
+ }
+ }
+ }
+
+ /**
+ * Page destroyed.
+ */
+ ngOnDestroy(): void {
+ this.editEventObserver && this.editEventObserver.off();
+ this.syncObserver && this.syncObserver.off();
+ this.manualSyncObserver && this.manualSyncObserver.off();
+ this.onlineObserver && this.onlineObserver.unsubscribe();
+ }
}
diff --git a/src/addon/calendar/pages/index/index.html b/src/addon/calendar/pages/index/index.html
new file mode 100644
index 00000000000..d2d6d38b089
--- /dev/null
+++ b/src/addon/calendar/pages/index/index.html
@@ -0,0 +1,38 @@
+
+
+ {{ (showCalendar ? 'addon.calendar.calendarevents' : 'addon.calendar.upcomingevents') | translate }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendar' | translate} }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/addon/calendar/pages/index/index.module.ts b/src/addon/calendar/pages/index/index.module.ts
new file mode 100644
index 00000000000..bf925e79931
--- /dev/null
+++ b/src/addon/calendar/pages/index/index.module.ts
@@ -0,0 +1,37 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicPageModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreComponentsModule } from '@components/components.module';
+import { CoreDirectivesModule } from '@directives/directives.module';
+import { CorePipesModule } from '@pipes/pipes.module';
+import { AddonCalendarComponentsModule } from '../../components/components.module';
+import { AddonCalendarIndexPage } from './index';
+
+@NgModule({
+ declarations: [
+ AddonCalendarIndexPage,
+ ],
+ imports: [
+ CoreComponentsModule,
+ CoreDirectivesModule,
+ CorePipesModule,
+ AddonCalendarComponentsModule,
+ IonicPageModule.forChild(AddonCalendarIndexPage),
+ TranslateModule.forChild()
+ ],
+})
+export class AddonCalendarIndexPageModule {}
diff --git a/src/addon/calendar/pages/index/index.ts b/src/addon/calendar/pages/index/index.ts
new file mode 100644
index 00000000000..8f3d5a129da
--- /dev/null
+++ b/src/addon/calendar/pages/index/index.ts
@@ -0,0 +1,379 @@
+// (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 OFx ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Component, OnInit, OnDestroy, ViewChild, NgZone } from '@angular/core';
+import { IonicPage, NavParams, NavController } from 'ionic-angular';
+import { CoreAppProvider } from '@providers/app';
+import { CoreEventsProvider } from '@providers/events';
+import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
+import { CoreSitesProvider } from '@providers/sites';
+import { CoreDomUtilsProvider } from '@providers/utils/dom';
+import { AddonCalendarProvider } from '../../providers/calendar';
+import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline';
+import { AddonCalendarHelperProvider } from '../../providers/helper';
+import { AddonCalendarCalendarComponent } from '../../components/calendar/calendar';
+import { AddonCalendarUpcomingEventsComponent } from '../../components/upcoming-events/upcoming-events';
+import { AddonCalendarSyncProvider } from '../../providers/calendar-sync';
+import { CoreCoursesHelperProvider } from '@core/courses/providers/helper';
+import { Network } from '@ionic-native/network';
+
+/**
+ * Page that displays the calendar events.
+ */
+@IonicPage({ segment: 'addon-calendar-index' })
+@Component({
+ selector: 'page-addon-calendar-index',
+ templateUrl: 'index.html',
+})
+export class AddonCalendarIndexPage implements OnInit, OnDestroy {
+ @ViewChild(AddonCalendarCalendarComponent) calendarComponent: AddonCalendarCalendarComponent;
+ @ViewChild(AddonCalendarUpcomingEventsComponent) upcomingEventsComponent: AddonCalendarUpcomingEventsComponent;
+
+ protected eventId: number;
+ protected currentSiteId: string;
+
+ // Observers.
+ protected newEventObserver: any;
+ protected discardedObserver: any;
+ protected editEventObserver: any;
+ protected deleteEventObserver: any;
+ protected undeleteEventObserver: any;
+ protected syncObserver: any;
+ protected manualSyncObserver: any;
+ protected onlineObserver: any;
+
+ year: number;
+ month: number;
+ courseId: number;
+ categoryId: number;
+ canCreate = false;
+ courses: any[];
+ notificationsEnabled = false;
+ loaded = false;
+ hasOffline = false;
+ isOnline = false;
+ syncIcon: string;
+ showCalendar = true;
+ loadUpcoming = false;
+
+ constructor(localNotificationsProvider: CoreLocalNotificationsProvider,
+ navParams: NavParams,
+ network: Network,
+ zone: NgZone,
+ sitesProvider: CoreSitesProvider,
+ private navCtrl: NavController,
+ private domUtils: CoreDomUtilsProvider,
+ private calendarProvider: AddonCalendarProvider,
+ private calendarOffline: AddonCalendarOfflineProvider,
+ private calendarHelper: AddonCalendarHelperProvider,
+ private calendarSync: AddonCalendarSyncProvider,
+ private eventsProvider: CoreEventsProvider,
+ private coursesHelper: CoreCoursesHelperProvider,
+ private appProvider: CoreAppProvider) {
+
+ this.courseId = navParams.get('courseId');
+ this.eventId = navParams.get('eventId') || false;
+ this.year = navParams.get('year');
+ this.month = navParams.get('month');
+ this.notificationsEnabled = localNotificationsProvider.isAvailable();
+ this.currentSiteId = sitesProvider.getCurrentSiteId();
+ this.loadUpcoming = !!navParams.get('upcoming');
+ this.showCalendar = !this.loadUpcoming;
+
+ // Listen for events added. When an event is added, reload the data.
+ this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => {
+ if (data && data.event) {
+ this.loaded = false;
+ this.refreshData(true, false, true);
+ }
+ }, this.currentSiteId);
+
+ // Listen for new event discarded event. When it does, reload the data.
+ this.discardedObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => {
+ this.loaded = false;
+ this.refreshData(true, false, true);
+ }, this.currentSiteId);
+
+ // Listen for events edited. When an event is edited, reload the data.
+ this.editEventObserver = eventsProvider.on(AddonCalendarProvider.EDIT_EVENT_EVENT, (data) => {
+ if (data && data.event) {
+ this.loaded = false;
+ this.refreshData(true, false, true);
+ }
+ }, this.currentSiteId);
+
+ // Refresh data if calendar events are synchronized automatically.
+ this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, (data) => {
+ this.loaded = false;
+ this.refreshData(false, false, true);
+ }, this.currentSiteId);
+
+ // Refresh data if calendar events are synchronized manually but not by this page.
+ this.manualSyncObserver = eventsProvider.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data) => {
+ if (data && data.source != 'index') {
+ this.loaded = false;
+ this.refreshData(false, false, true);
+ }
+ }, this.currentSiteId);
+
+ // Update the events when an event is deleted.
+ this.deleteEventObserver = eventsProvider.on(AddonCalendarProvider.DELETED_EVENT_EVENT, (data) => {
+ this.loaded = false;
+ this.refreshData(false, false, true);
+ }, this.currentSiteId);
+
+ // Update the "hasOffline" property if an event deleted in offline is restored.
+ this.undeleteEventObserver = eventsProvider.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, (data) => {
+ this.calendarOffline.hasOfflineData().then((hasOffline) => {
+ this.hasOffline = hasOffline;
+ });
+ }, this.currentSiteId);
+
+ // Refresh online status when changes.
+ this.onlineObserver = network.onchange().subscribe(() => {
+ // Execute the callback in the Angular zone, so change detection doesn't stop working.
+ zone.run(() => {
+ this.isOnline = this.appProvider.isOnline();
+ });
+ });
+ }
+
+ /**
+ * View loaded.
+ */
+ ngOnInit(): void {
+ if (this.eventId) {
+ // There is an event to load, open the event in a new state.
+ this.gotoEvent(this.eventId);
+ }
+
+ this.fetchData(true, false);
+ }
+
+ /**
+ * Fetch all the data required for the view.
+ *
+ * @param {boolean} [sync] Whether it should try to synchronize offline events.
+ * @param {boolean} [showErrors] Whether to show sync errors to the user.
+ * @return {Promise} Promise resolved when done.
+ */
+ fetchData(sync?: boolean, showErrors?: boolean): Promise {
+
+ this.syncIcon = 'spinner';
+ this.isOnline = this.appProvider.isOnline();
+
+ let promise;
+
+ if (sync) {
+ // Try to synchronize offline events.
+ promise = this.calendarSync.syncEvents().then((result) => {
+ if (result.warnings && result.warnings.length) {
+ this.domUtils.showErrorModal(result.warnings[0]);
+ }
+
+ if (result.updated) {
+ // Trigger a manual sync event.
+ result.source = 'index';
+
+ this.eventsProvider.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId);
+ }
+ }).catch((error) => {
+ if (showErrors) {
+ this.domUtils.showErrorModalDefault(error, 'core.errorsync', true);
+ }
+ });
+ } else {
+ promise = Promise.resolve();
+ }
+
+ return promise.then(() => {
+ const promises = [];
+
+ this.hasOffline = false;
+
+ // Load courses for the popover.
+ promises.push(this.coursesHelper.getCoursesForPopover(this.courseId).then((data) => {
+ this.courses = data.courses;
+ this.categoryId = data.categoryId;
+ }));
+
+ // Check if user can create events.
+ promises.push(this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => {
+ this.canCreate = canEdit;
+ }));
+
+ // Check if there is offline data.
+ promises.push(this.calendarOffline.hasOfflineData().then((hasOffline) => {
+ this.hasOffline = hasOffline;
+ }));
+
+ return Promise.all(promises);
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'addon.calendar.errorloadevents', true);
+ }).finally(() => {
+ this.loaded = true;
+ this.syncIcon = 'sync';
+ });
+ }
+
+ /**
+ * Refresh the data.
+ *
+ * @param {any} [refresher] Refresher.
+ * @param {Function} [done] Function to call when done.
+ * @param {boolean} [showErrors] Whether to show sync errors to the user.
+ * @return {Promise} Promise resolved when done.
+ */
+ doRefresh(refresher?: any, done?: () => void, showErrors?: boolean): Promise {
+ if (this.loaded) {
+ return this.refreshData(true, showErrors).finally(() => {
+ refresher && refresher.complete();
+ done && done();
+ });
+ }
+
+ return Promise.resolve();
+ }
+
+ /**
+ * Refresh the data.
+ *
+ * @param {boolean} [sync] Whether it should try to synchronize offline events.
+ * @param {boolean} [showErrors] Whether to show sync errors to the user.
+ * @param {boolean} [afterChange] Whether the refresh is done after an event has changed or has been synced.
+ * @return {Promise} Promise resolved when done.
+ */
+ refreshData(sync?: boolean, showErrors?: boolean, afterChange?: boolean): Promise {
+ this.syncIcon = 'spinner';
+
+ const promises = [];
+
+ promises.push(this.calendarProvider.invalidateAllowedEventTypes());
+
+ // Refresh the sub-component.
+ if (this.showCalendar && this.calendarComponent) {
+ promises.push(this.calendarComponent.refreshData(afterChange));
+ } else if (!this.showCalendar && this.upcomingEventsComponent) {
+ promises.push(this.upcomingEventsComponent.refreshData(afterChange));
+ }
+
+ return Promise.all(promises).finally(() => {
+ return this.fetchData(sync, showErrors);
+ });
+ }
+
+ /**
+ * Navigate to a particular event.
+ *
+ * @param {number} eventId Event to load.
+ */
+ gotoEvent(eventId: number): void {
+ if (eventId < 0) {
+ // It's an offline event, go to the edit page.
+ this.openEdit(eventId);
+ } else {
+ this.navCtrl.push('AddonCalendarEventPage', {
+ id: eventId
+ });
+ }
+ }
+
+ /**
+ * View a certain day.
+ *
+ * @param {any} data Data with the year, month and day.
+ */
+ gotoDay(data: any): void {
+ const params: any = {
+ day: data.day,
+ month: data.month,
+ year: data.year
+ };
+
+ if (this.courseId) {
+ params.courseId = this.courseId;
+ }
+
+ this.navCtrl.push('AddonCalendarDayPage', params);
+ }
+
+ /**
+ * Show the context menu.
+ *
+ * @param {MouseEvent} event Event.
+ */
+ openCourseFilter(event: MouseEvent): void {
+ this.coursesHelper.selectCourse(event, this.courses, this.courseId).then((result) => {
+ if (typeof result.courseId != 'undefined') {
+ this.courseId = result.courseId > 0 ? result.courseId : undefined;
+ this.categoryId = result.courseId > 0 ? result.categoryId : undefined;
+
+ // Course viewed has changed, check if the user can create events for this course calendar.
+ this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => {
+ this.canCreate = canEdit;
+ });
+ }
+ });
+ }
+
+ /**
+ * Open page to create/edit an event.
+ *
+ * @param {number} [eventId] Event ID to edit.
+ */
+ openEdit(eventId?: number): void {
+ const params: any = {};
+
+ if (eventId) {
+ params.eventId = eventId;
+ }
+ if (this.courseId) {
+ params.courseId = this.courseId;
+ }
+
+ this.navCtrl.push('AddonCalendarEditEventPage', params);
+ }
+
+ /**
+ * Open calendar events settings.
+ */
+ openSettings(): void {
+ this.navCtrl.push('AddonCalendarSettingsPage');
+ }
+
+ /**
+ * Toogle display: monthly view or upcoming events.
+ */
+ toggleDisplay(): void {
+ this.showCalendar = !this.showCalendar;
+
+ if (!this.showCalendar) {
+ this.loadUpcoming = true;
+ }
+ }
+
+ /**
+ * Page destroyed.
+ */
+ ngOnDestroy(): void {
+ this.newEventObserver && this.newEventObserver.off();
+ this.discardedObserver && this.discardedObserver.off();
+ this.editEventObserver && this.editEventObserver.off();
+ this.deleteEventObserver && this.deleteEventObserver.off();
+ this.undeleteEventObserver && this.undeleteEventObserver.off();
+ this.syncObserver && this.syncObserver.off();
+ this.manualSyncObserver && this.manualSyncObserver.off();
+ this.onlineObserver && this.onlineObserver.unsubscribe();
+ }
+}
diff --git a/src/addon/calendar/pages/list/list.html b/src/addon/calendar/pages/list/list.html
index e5cdc3eb09b..3ae4f4c0d52 100644
--- a/src/addon/calendar/pages/list/list.html
+++ b/src/addon/calendar/pages/list/list.html
@@ -3,20 +3,27 @@
{{ 'addon.calendar.calendarevents' | translate }}
-
+
+
+
-
+
+
+
+ {{ 'core.hasdatatosync' | translate:{$a: 'addon.calendar.calendar' | translate} }}
+
+
@@ -25,7 +32,7 @@
{{ event.timestart * 1000 | coreFormatDate: "strftimedayshort" }}
-
+
@@ -34,11 +41,26 @@
- {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimetime" }}
- {{ (event.timestart + event.timeduration) * 1000 | coreFormatDate: "strftimedatetimeshort" }}
+
+
+ {{ 'core.notsent' | translate }}
+
+
+
+ {{ 'core.deletedoffline' | translate }}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/addon/calendar/pages/list/list.scss b/src/addon/calendar/pages/list/list.scss
new file mode 100644
index 00000000000..9f40d974653
--- /dev/null
+++ b/src/addon/calendar/pages/list/list.scss
@@ -0,0 +1,5 @@
+ion-app.app-root page-addon-calendar-list {
+ ion-note {
+ max-width: 30%;
+ }
+}
diff --git a/src/addon/calendar/pages/list/list.ts b/src/addon/calendar/pages/list/list.ts
index cd8523d9007..0b864a78ab9 100644
--- a/src/addon/calendar/pages/list/list.ts
+++ b/src/addon/calendar/pages/list/list.ts
@@ -12,21 +12,25 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, ViewChild, OnDestroy } from '@angular/core';
-import { IonicPage, Content, PopoverController, NavParams, NavController } from 'ionic-angular';
-import { TranslateService } from '@ngx-translate/core';
+import { Component, ViewChild, OnDestroy, NgZone } from '@angular/core';
+import { IonicPage, Content, NavParams, NavController } from 'ionic-angular';
import { AddonCalendarProvider } from '../../providers/calendar';
+import { AddonCalendarOfflineProvider } from '../../providers/calendar-offline';
import { AddonCalendarHelperProvider } from '../../providers/helper';
+import { AddonCalendarSyncProvider } from '../../providers/calendar-sync';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
+import { CoreCoursesHelperProvider } from '@core/courses/providers/helper';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
+import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreSitesProvider } from '@providers/sites';
import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
-import { CoreCoursePickerMenuPopoverComponent } from '@components/course-picker-menu/course-picker-menu-popover';
import { CoreEventsProvider } from '@providers/events';
import { CoreAppProvider } from '@providers/app';
import { CoreSplitViewComponent } from '@components/split-view/split-view';
import * as moment from 'moment';
+import { Network } from '@ionic-native/network';
+import { CoreConstants } from '@core/constants';
/**
* Page that displays the list of calendar events.
@@ -40,47 +44,167 @@ export class AddonCalendarListPage implements OnDestroy {
@ViewChild(Content) content: Content;
@ViewChild(CoreSplitViewComponent) splitviewCtrl: CoreSplitViewComponent;
+ protected initialTime = 0;
protected daysLoaded = 0;
protected emptyEventsTimes = 0; // Variable to identify consecutive calls returning 0 events.
protected categoriesRetrieved = false;
protected getCategories = false;
- protected allCourses = {
- id: -1,
- fullname: this.translate.instant('core.fulllistofcourses'),
- category: -1
- };
protected categories = {};
protected siteHomeId: number;
protected obsDefaultTimeChange: any;
protected eventId: number;
+ protected newEventObserver: any;
+ protected discardedObserver: any;
+ protected editEventObserver: any;
+ protected deleteEventObserver: any;
+ protected undeleteEventObserver: any;
+ protected syncObserver: any;
+ protected manualSyncObserver: any;
+ protected onlineObserver: any;
+ protected currentSiteId: string;
+ protected onlineEvents = [];
+ protected offlineEvents = [];
+ protected deletedEvents = [];
courses: any[];
eventsLoaded = false;
- events = [];
+ events = []; // Events (both online and offline).
notificationsEnabled = false;
filteredEvents = [];
canLoadMore = false;
loadMoreError = false;
- filter = {
- course: this.allCourses
- };
-
- constructor(private translate: TranslateService, private calendarProvider: AddonCalendarProvider, navParams: NavParams,
+ courseId: number;
+ categoryId: number;
+ canCreate = false;
+ hasOffline = false;
+ isOnline = false;
+ syncIcon: string; // Sync icon.
+
+ constructor(private calendarProvider: AddonCalendarProvider, navParams: NavParams,
private domUtils: CoreDomUtilsProvider, private coursesProvider: CoreCoursesProvider, private utils: CoreUtilsProvider,
- private calendarHelper: AddonCalendarHelperProvider, sitesProvider: CoreSitesProvider,
- localNotificationsProvider: CoreLocalNotificationsProvider, private popoverCtrl: PopoverController,
- eventsProvider: CoreEventsProvider, private navCtrl: NavController, appProvider: CoreAppProvider) {
+ private calendarHelper: AddonCalendarHelperProvider, sitesProvider: CoreSitesProvider, zone: NgZone,
+ localNotificationsProvider: CoreLocalNotificationsProvider, private coursesHelper: CoreCoursesHelperProvider,
+ private eventsProvider: CoreEventsProvider, private navCtrl: NavController, private appProvider: CoreAppProvider,
+ private calendarOffline: AddonCalendarOfflineProvider, private calendarSync: AddonCalendarSyncProvider,
+ network: Network, private timeUtils: CoreTimeUtilsProvider) {
this.siteHomeId = sitesProvider.getCurrentSite().getSiteHomeId();
this.notificationsEnabled = localNotificationsProvider.isAvailable();
+ this.currentSiteId = sitesProvider.getCurrentSiteId();
+
if (this.notificationsEnabled) {
// Re-schedule events if default time changes.
this.obsDefaultTimeChange = eventsProvider.on(AddonCalendarProvider.DEFAULT_NOTIFICATION_TIME_CHANGED, () => {
- calendarProvider.scheduleEventsNotifications(this.events);
- }, sitesProvider.getCurrentSiteId());
+ calendarProvider.scheduleEventsNotifications(this.onlineEvents);
+ }, this.currentSiteId);
}
this.eventId = navParams.get('eventId') || false;
+ this.courseId = navParams.get('courseId');
+
+ // Listen for events added. When an event is added, reload the data.
+ this.newEventObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_EVENT, (data) => {
+ if (data && data.event) {
+ if (this.splitviewCtrl.isOn()) {
+ // Discussion added, clear details page.
+ this.splitviewCtrl.emptyDetails();
+ }
+
+ this.eventsLoaded = false;
+ this.refreshEvents(true, false).finally(() => {
+
+ // In tablet mode try to open the event (only if it's an online event).
+ if (this.splitviewCtrl.isOn() && data.event.id > 0) {
+ this.gotoEvent(data.event.id);
+ }
+ });
+ }
+ }, this.currentSiteId);
+
+ // Listen for new event discarded event. When it does, reload the data.
+ this.discardedObserver = eventsProvider.on(AddonCalendarProvider.NEW_EVENT_DISCARDED_EVENT, () => {
+ if (this.splitviewCtrl.isOn()) {
+ // Discussion added, clear details page.
+ this.splitviewCtrl.emptyDetails();
+ }
+
+ this.eventsLoaded = false;
+ this.refreshEvents(true, false);
+ }, this.currentSiteId);
+
+ // Listen for events edited. When an event is edited, reload the data.
+ this.editEventObserver = eventsProvider.on(AddonCalendarProvider.EDIT_EVENT_EVENT, (data) => {
+ if (data && data.event) {
+ this.eventsLoaded = false;
+ this.refreshEvents(true, false);
+ }
+ }, this.currentSiteId);
+
+ // Refresh data if calendar events are synchronized automatically.
+ this.syncObserver = eventsProvider.on(AddonCalendarSyncProvider.AUTO_SYNCED, (data) => {
+ this.eventsLoaded = false;
+ this.refreshEvents();
+
+ if (this.splitviewCtrl.isOn() && this.eventId && data && data.deleted && data.deleted.indexOf(this.eventId) != -1) {
+ // Current selected event was deleted. Clear details.
+ this.splitviewCtrl.emptyDetails();
+ }
+ }, this.currentSiteId);
+
+ // Refresh data if calendar events are synchronized manually but not by this page.
+ this.manualSyncObserver = eventsProvider.on(AddonCalendarSyncProvider.MANUAL_SYNCED, (data) => {
+ if (data && data.source != 'list') {
+ this.eventsLoaded = false;
+ this.refreshEvents();
+ }
+
+ if (this.splitviewCtrl.isOn() && this.eventId && data && data.deleted && data.deleted.indexOf(this.eventId) != -1) {
+ // Current selected event was deleted. Clear details.
+ this.splitviewCtrl.emptyDetails();
+ }
+ }, this.currentSiteId);
+
+ // Update the list when an event is deleted.
+ this.deleteEventObserver = eventsProvider.on(AddonCalendarProvider.DELETED_EVENT_EVENT, (data) => {
+ if (data && !data.sent) {
+ // Event was deleted in offline. Just mark it as deleted, no need to refresh.
+ this.markAsDeleted(data.eventId, true);
+ this.deletedEvents.push(data.eventId);
+ this.hasOffline = true;
+ } else {
+ // Event deleted, clear the details if needed and refresh the view.
+ if (this.splitviewCtrl.isOn()) {
+ this.splitviewCtrl.emptyDetails();
+ }
+
+ this.eventsLoaded = false;
+ this.refreshEvents();
+ }
+ }, this.currentSiteId);
+
+ // Listen for events "undeleted" (offline).
+ this.undeleteEventObserver = eventsProvider.on(AddonCalendarProvider.UNDELETED_EVENT_EVENT, (data) => {
+ if (data && data.eventId) {
+ // Mark it as undeleted, no need to refresh.
+ this.markAsDeleted(data.eventId, false);
+
+ // Remove it from the list of deleted events if it's there.
+ const index = this.deletedEvents.indexOf(data.eventId);
+ if (index != -1) {
+ this.deletedEvents.splice(index, 1);
+ }
+
+ this.hasOffline = !!this.offlineEvents.length || !!this.deletedEvents.length;
+ }
+ }, this.currentSiteId);
+
+ // Refresh online status when changes.
+ this.onlineObserver = network.onchange().subscribe(() => {
+ // Execute the callback in the Angular zone, so change detection doesn't stop working.
+ zone.run(() => {
+ this.isOnline = this.appProvider.isOnline();
+ });
+ });
}
/**
@@ -92,13 +216,17 @@ export class AddonCalendarListPage implements OnDestroy {
this.gotoEvent(this.eventId);
}
- this.fetchData().then(() => {
+ this.syncIcon = 'spinner';
+
+ this.fetchData(false, true, false).then(() => {
if (!this.eventId && this.splitviewCtrl.isOn() && this.events.length > 0) {
- // Take first and load it.
- this.gotoEvent(this.events[0].id);
+ // Take first online event and load it. If no online event, load the first offline.
+ if (this.onlineEvents[0]) {
+ this.gotoEvent(this.onlineEvents[0].id);
+ } else {
+ this.gotoEvent(this.offlineEvents[0].id);
+ }
}
- }).finally(() => {
- this.eventsLoaded = true;
});
}
@@ -106,19 +234,80 @@ export class AddonCalendarListPage implements OnDestroy {
* Fetch all the data required for the view.
*
* @param {boolean} [refresh] Empty events array first.
+ * @param {boolean} [sync] Whether it should try to synchronize offline events.
+ * @param {boolean} [showErrors] Whether to show sync errors to the user.
* @return {Promise} Promise resolved when done.
*/
- fetchData(refresh: boolean = false): Promise {
+ fetchData(refresh?: boolean, sync?: boolean, showErrors?: boolean): Promise {
+ this.initialTime = this.timeUtils.timestamp();
this.daysLoaded = 0;
this.emptyEventsTimes = 0;
+ this.isOnline = this.appProvider.isOnline();
+
+ let promise;
+
+ if (sync) {
+ // Try to synchronize offline events.
+ promise = this.calendarSync.syncEvents().then((result) => {
+ if (result.warnings && result.warnings.length) {
+ this.domUtils.showErrorModal(result.warnings[0]);
+ }
- // Load courses for the popover.
- return this.coursesProvider.getUserCourses(false).then((courses) => {
- // Add "All courses".
- courses.unshift(this.allCourses);
- this.courses = courses;
+ if (result.updated) {
+ // Trigger a manual sync event.
+ result.source = 'list';
- return this.fetchEvents(refresh);
+ this.eventsProvider.trigger(AddonCalendarSyncProvider.MANUAL_SYNCED, result, this.currentSiteId);
+ }
+ }).catch((error) => {
+ if (showErrors) {
+ this.domUtils.showErrorModalDefault(error, 'core.errorsync', true);
+ }
+ });
+ } else {
+ promise = Promise.resolve();
+ }
+
+ return promise.then(() => {
+
+ const promises = [];
+
+ this.hasOffline = false;
+
+ promises.push(this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => {
+ this.canCreate = canEdit;
+ }));
+
+ // Load courses for the popover.
+ promises.push(this.coursesHelper.getCoursesForPopover(this.courseId).then((result) => {
+ this.courses = result.courses;
+ this.categoryId = result.categoryId;
+
+ return this.fetchEvents(refresh);
+ }));
+
+ // Get offline events.
+ promises.push(this.calendarOffline.getAllEditedEvents().then((events) => {
+ this.hasOffline = this.hasOffline || !!events.length;
+
+ // Format data and sort by timestart.
+ events.forEach((event) => {
+ event.offline = true;
+ this.calendarHelper.formatEventData(event);
+ });
+ this.offlineEvents = this.sortEvents(events);
+ }));
+
+ // Get events deleted in offline.
+ promises.push(this.calendarOffline.getAllDeletedEventsIds().then((ids) => {
+ this.hasOffline = this.hasOffline || !!ids.length;
+ this.deletedEvents = ids;
+ }));
+
+ return Promise.all(promises);
+ }).finally(() => {
+ this.eventsLoaded = true;
+ this.syncIcon = 'sync';
});
}
@@ -128,40 +317,41 @@ export class AddonCalendarListPage implements OnDestroy {
* @param {boolean} [refresh] Empty events array first.
* @return {Promise} Promise resolved when done.
*/
- fetchEvents(refresh: boolean = false): Promise {
+ fetchEvents(refresh?: boolean): Promise {
this.loadMoreError = false;
- return this.calendarProvider.getEventsList(this.daysLoaded, AddonCalendarProvider.DAYS_INTERVAL).then((events) => {
- this.daysLoaded += AddonCalendarProvider.DAYS_INTERVAL;
- if (events.length === 0) {
+ return this.calendarProvider.getEventsList(this.initialTime, this.daysLoaded, AddonCalendarProvider.DAYS_INTERVAL)
+ .then((onlineEvents) => {
+
+ if (onlineEvents.length === 0) {
this.emptyEventsTimes++;
if (this.emptyEventsTimes > 5) { // Stop execution if we retrieve empty list 6 consecutive times.
this.canLoadMore = false;
if (refresh) {
- this.events = [];
+ this.onlineEvents = [];
this.filteredEvents = [];
+ this.events = this.offlineEvents;
}
} else {
// No events returned, load next events.
+ this.daysLoaded += AddonCalendarProvider.DAYS_INTERVAL;
+
return this.fetchEvents();
}
} else {
- // Sort the events by timestart, they're ordered by id.
- events.sort((a, b) => {
- if (a.timestart == b.timestart) {
- return a.timeduration - b.timeduration;
- }
+ onlineEvents.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper));
- return a.timestart - b.timestart;
- });
+ // Get the merged events of this period.
+ const events = this.mergeEvents(onlineEvents);
- events.forEach(this.calendarHelper.formatEventData.bind(this.calendarHelper));
- this.getCategories = this.shouldLoadCategories(events);
+ this.getCategories = this.shouldLoadCategories(onlineEvents);
if (refresh) {
+ this.onlineEvents = onlineEvents;
this.events = events;
} else {
// Filter events with same ID. Repeated events are returned once per WS call, show them only once.
+ this.onlineEvents = this.utils.mergeArraysWithoutDuplicates(this.onlineEvents, onlineEvents, 'id');
this.events = this.utils.mergeArraysWithoutDuplicates(this.events, events, 'id');
}
this.filteredEvents = this.getFilteredEvents();
@@ -174,7 +364,9 @@ export class AddonCalendarListPage implements OnDestroy {
this.canLoadMore = true;
// Schedule notifications for the events retrieved (might have new events).
- this.calendarProvider.scheduleEventsNotifications(this.events);
+ this.calendarProvider.scheduleEventsNotifications(this.onlineEvents);
+
+ this.daysLoaded += AddonCalendarProvider.DAYS_INTERVAL;
}
// Resize the content so infinite loading is able to calculate if it should load more items or not.
@@ -211,55 +403,14 @@ export class AddonCalendarListPage implements OnDestroy {
* @return {any[]} Filtered events.
*/
protected getFilteredEvents(): any[] {
- if (this.filter.course.id == -1) {
+ if (!this.courseId) {
// No filter, display everything.
return this.events;
}
- return this.events.filter(this.shouldDisplayEvent.bind(this));
- }
-
- /**
- * Check if an event should be displayed based on the filter.
- *
- * @param {any} event Event object.
- * @return {boolean} Whether it should be displayed.
- */
- protected shouldDisplayEvent(event: any): boolean {
- if (event.eventtype == 'user' || event.eventtype == 'site') {
- // User or site event, display it.
- return true;
- }
-
- if (event.eventtype == 'category') {
- if (!event.categoryid || !Object.keys(this.categories).length) {
- // We can't tell if the course belongs to the category, display them all.
- return true;
- }
- if (event.categoryid == this.filter.course.category) {
- // The event is in the same category as the course, display it.
- return true;
- }
-
- // Check parent categories.
- let category = this.categories[this.filter.course.category];
- while (category) {
- if (!category.parent) {
- // Category doesn't have parent, stop.
- break;
- }
-
- if (event.categoryid == category.parent) {
- return true;
- }
- category = this.categories[category.parent];
- }
-
- return false;
- }
-
- // Show the event if it is from site home or if it matches the selected course.
- return event.courseid === this.siteHomeId || event.courseid == this.filter.course.id;
+ return this.events.filter((event) => {
+ return this.calendarHelper.shouldDisplayEvent(event, this.courseId, this.categoryId, this.categories);
+ });
}
/**
@@ -297,25 +448,112 @@ export class AddonCalendarListPage implements OnDestroy {
});
}
+ /**
+ * Merge a period of online events with the offline events of that period.
+ *
+ * @param {any[]} onlineEvents Online events.
+ * @return {any[]} Merged events.
+ */
+ protected mergeEvents(onlineEvents: any[]): any[] {
+ if (!this.offlineEvents.length && !this.deletedEvents.length) {
+ // No offline events, nothing to merge.
+ return onlineEvents;
+ }
+
+ const start = this.initialTime + (CoreConstants.SECONDS_DAY * this.daysLoaded),
+ end = start + (CoreConstants.SECONDS_DAY * AddonCalendarProvider.DAYS_INTERVAL) - 1;
+ let result = onlineEvents;
+
+ if (this.deletedEvents.length) {
+ // Mark as deleted the events that were deleted in offline.
+ result.forEach((event) => {
+ event.deleted = this.deletedEvents.indexOf(event.id) != -1;
+ });
+ }
+
+ if (this.offlineEvents.length) {
+ // Remove the online events that were modified in offline.
+ result = result.filter((event) => {
+ const offlineEvent = this.offlineEvents.find((ev) => {
+ return ev.id == event.id;
+ });
+
+ return !offlineEvent;
+ });
+ }
+
+ // Now get the offline events that belong to this period.
+ const periodOfflineEvents = this.offlineEvents.filter((event) => {
+ if (this.daysLoaded == 0 && event.timestart < start) {
+ // Display offline events that are previous to current time to allow editing them.
+ return true;
+ }
+
+ return (event.timestart >= start || event.timestart + event.timeduration >= start) && event.timestart <= end;
+ });
+
+ // Merge both arrays and sort them.
+ result = result.concat(periodOfflineEvents);
+
+ return this.sortEvents(result);
+ }
+
+ /**
+ * Sort events by timestart.
+ *
+ * @param {any[]} events List to sort.
+ */
+ protected sortEvents(events: any[]): any[] {
+ return events.sort((a, b) => {
+ if (a.timestart == b.timestart) {
+ return a.timeduration - b.timeduration;
+ }
+
+ return a.timestart - b.timestart;
+ });
+ }
+
+ /**
+ * Refresh the data.
+ *
+ * @param {any} [refresher] Refresher.
+ * @param {Function} [done] Function to call when done.
+ * @param {boolean} [showErrors] Whether to show sync errors to the user.
+ * @return {Promise} Promise resolved when done.
+ */
+ doRefresh(refresher?: any, done?: () => void, showErrors?: boolean): Promise {
+ if (this.eventsLoaded) {
+ return this.refreshEvents(true, showErrors).finally(() => {
+ refresher && refresher.complete();
+ done && done();
+ });
+ }
+
+ return Promise.resolve();
+ }
+
/**
* Refresh the events.
*
- * @param {any} refresher Refresher.
+ * @param {boolean} [sync] Whether it should try to synchronize offline events.
+ * @param {boolean} [showErrors] Whether to show sync errors to the user.
+ * @return {Promise} Promise resolved when done.
*/
- refreshEvents(refresher: any): void {
+ refreshEvents(sync?: boolean, showErrors?: boolean): Promise {
+ this.syncIcon = 'spinner';
+
const promises = [];
promises.push(this.calendarProvider.invalidateEventsList());
+ promises.push(this.calendarProvider.invalidateAllowedEventTypes());
if (this.categoriesRetrieved) {
promises.push(this.coursesProvider.invalidateCategories(0, true));
this.categoriesRetrieved = false;
}
- Promise.all(promises).finally(() => {
- this.fetchData(true).finally(() => {
- refresher.complete();
- });
+ return Promise.all(promises).finally(() => {
+ return this.fetchData(true, sync, showErrors);
});
}
@@ -359,21 +597,41 @@ export class AddonCalendarListPage implements OnDestroy {
* @param {MouseEvent} event Event.
*/
openCourseFilter(event: MouseEvent): void {
- const popover = this.popoverCtrl.create(CoreCoursePickerMenuPopoverComponent, {
- courses: this.courses,
- courseId: this.filter.course.id
- });
- popover.onDidDismiss((course) => {
- if (course) {
- this.filter.course = course;
- this.domUtils.scrollToTop(this.content);
+ this.coursesHelper.selectCourse(event, this.courses, this.courseId).then((result) => {
+ if (typeof result.courseId != 'undefined') {
+ this.courseId = result.courseId > 0 ? result.courseId : undefined;
+ this.categoryId = result.courseId > 0 ? result.categoryId : undefined;
+
+ // Course viewed has changed, check if the user can create events for this course calendar.
+ this.calendarHelper.canEditEvents(this.courseId).then((canEdit) => {
+ this.canCreate = canEdit;
+ });
this.filteredEvents = this.getFilteredEvents();
+
+ this.domUtils.scrollToTop(this.content);
}
});
- popover.present({
- ev: event
- });
+ }
+
+ /**
+ * Open page to create/edit an event.
+ *
+ * @param {number} [eventId] Event ID to edit.
+ */
+ openEdit(eventId?: number): void {
+ this.eventId = undefined;
+
+ const params: any = {};
+
+ if (eventId) {
+ params.eventId = eventId;
+ }
+ if (this.courseId) {
+ params.courseId = this.courseId;
+ }
+
+ this.splitviewCtrl.push('AddonCalendarEditEventPage', params);
}
/**
@@ -390,7 +648,31 @@ export class AddonCalendarListPage implements OnDestroy {
*/
gotoEvent(eventId: number): void {
this.eventId = eventId;
- this.splitviewCtrl.push('AddonCalendarEventPage', { id: eventId });
+
+ if (eventId < 0) {
+ // It's an offline event, go to the edit page.
+ this.openEdit(eventId);
+ } else {
+ this.splitviewCtrl.push('AddonCalendarEventPage', {
+ id: eventId
+ });
+ }
+ }
+
+ /**
+ * Find an event and mark it as deleted.
+ *
+ * @param {number} eventId Event ID.
+ * @param {boolean} deleted Whether to mark it as deleted or not.
+ */
+ protected markAsDeleted(eventId: number, deleted: boolean): void {
+ const event = this.onlineEvents.find((event) => {
+ return event.id == eventId;
+ });
+
+ if (event) {
+ event.deleted = deleted;
+ }
}
/**
@@ -398,5 +680,13 @@ export class AddonCalendarListPage implements OnDestroy {
*/
ngOnDestroy(): void {
this.obsDefaultTimeChange && this.obsDefaultTimeChange.off();
+ this.newEventObserver && this.newEventObserver.off();
+ this.discardedObserver && this.discardedObserver.off();
+ this.editEventObserver && this.editEventObserver.off();
+ this.deleteEventObserver && this.deleteEventObserver.off();
+ this.undeleteEventObserver && this.undeleteEventObserver.off();
+ this.syncObserver && this.syncObserver.off();
+ this.manualSyncObserver && this.manualSyncObserver.off();
+ this.onlineObserver && this.onlineObserver.unsubscribe();
}
}
diff --git a/src/addon/calendar/providers/calendar-offline.ts b/src/addon/calendar/providers/calendar-offline.ts
new file mode 100644
index 00000000000..5b0f024362a
--- /dev/null
+++ b/src/addon/calendar/providers/calendar-offline.ts
@@ -0,0 +1,389 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
+import { CoreUtilsProvider } from '@providers/utils/utils';
+
+/**
+ * Service to handle offline calendar events.
+ */
+@Injectable()
+export class AddonCalendarOfflineProvider {
+
+ // Variables for database.
+ static EVENTS_TABLE = 'addon_calendar_offline_events';
+ static DELETED_EVENTS_TABLE = 'addon_calendar_deleted_events';
+
+ protected siteSchema: CoreSiteSchema = {
+ name: 'AddonCalendarOfflineProvider',
+ version: 1,
+ tables: [
+ {
+ name: AddonCalendarOfflineProvider.EVENTS_TABLE,
+ columns: [
+ {
+ name: 'id', // Negative for offline entries.
+ type: 'INTEGER',
+ primaryKey: true
+ },
+ {
+ name: 'name',
+ type: 'TEXT',
+ notNull: true
+ },
+ {
+ name: 'timestart',
+ type: 'INTEGER',
+ notNull: true
+ },
+ {
+ name: 'eventtype',
+ type: 'TEXT',
+ notNull: true
+ },
+ {
+ name: 'categoryid',
+ type: 'INTEGER',
+ },
+ {
+ name: 'courseid',
+ type: 'INTEGER',
+ },
+ {
+ name: 'groupcourseid',
+ type: 'INTEGER',
+ },
+ {
+ name: 'groupid',
+ type: 'INTEGER',
+ },
+ {
+ name: 'description',
+ type: 'TEXT',
+ },
+ {
+ name: 'location',
+ type: 'TEXT',
+ },
+ {
+ name: 'duration',
+ type: 'INTEGER',
+ },
+ {
+ name: 'timedurationuntil',
+ type: 'INTEGER',
+ },
+ {
+ name: 'timedurationminutes',
+ type: 'INTEGER',
+ },
+ {
+ name: 'repeat',
+ type: 'INTEGER',
+ },
+ {
+ name: 'repeats',
+ type: 'INTEGER',
+ },
+ {
+ name: 'repeatid',
+ type: 'INTEGER',
+ },
+ {
+ name: 'repeateditall',
+ type: 'INTEGER',
+ },
+ {
+ name: 'userid',
+ type: 'INTEGER',
+ },
+ {
+ name: 'timecreated',
+ type: 'INTEGER',
+ }
+ ]
+ },
+ {
+ name: AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE,
+ columns: [
+ {
+ name: 'id',
+ type: 'INTEGER',
+ primaryKey: true
+ },
+ {
+ name: 'name', // Save the name to be able to notify the user.
+ type: 'TEXT',
+ notNull: true
+ },
+ {
+ name: 'repeat',
+ type: 'INTEGER'
+ },
+ {
+ name: 'timemodified',
+ type: 'INTEGER',
+ }
+ ]
+ }
+ ]
+ };
+
+ constructor(private sitesProvider: CoreSitesProvider, private utils: CoreUtilsProvider) {
+ this.sitesProvider.registerSiteSchema(this.siteSchema);
+ }
+
+ /**
+ * Delete an offline event.
+ *
+ * @param {number} eventId Event ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved if deleted, rejected if failure.
+ */
+ deleteEvent(eventId: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ const conditions: any = {
+ id: eventId
+ };
+
+ return site.getDb().deleteRecords(AddonCalendarOfflineProvider.EVENTS_TABLE, conditions);
+ });
+ }
+
+ /**
+ * Get the IDs of all the events created/edited/deleted in offline.
+ *
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the IDs.
+ */
+ getAllEventsIds(siteId?: string): Promise {
+ const promises = [];
+
+ promises.push(this.getAllDeletedEventsIds(siteId));
+ promises.push(this.getAllEditedEventsIds(siteId));
+
+ return Promise.all(promises).then((result) => {
+ return this.utils.mergeArraysWithoutDuplicates(result[0], result[1]);
+ });
+ }
+
+ /**
+ * Get all the events deleted in offline.
+ *
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with all the events deleted in offline.
+ */
+ getAllDeletedEvents(siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.getDb().getRecords(AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE);
+ });
+ }
+
+ /**
+ * Get the IDs of all the events deleted in offline.
+ *
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the IDs of all the events deleted in offline.
+ */
+ getAllDeletedEventsIds(siteId?: string): Promise {
+ return this.getAllDeletedEvents(siteId).then((events) => {
+ return events.map((event) => {
+ return event.id;
+ });
+ });
+ }
+
+ /**
+ * Get all the events created/edited in offline.
+ *
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with events.
+ */
+ getAllEditedEvents(siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.getDb().getRecords(AddonCalendarOfflineProvider.EVENTS_TABLE);
+ });
+ }
+
+ /**
+ * Get the IDs of all the events created/edited in offline.
+ *
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with events IDs.
+ */
+ getAllEditedEventsIds(siteId?: string): Promise {
+ return this.getAllEditedEvents(siteId).then((events) => {
+ return events.map((event) => {
+ return event.id;
+ });
+ });
+ }
+
+ /**
+ * Get an event deleted in offline.
+ *
+ * @param {number} eventId Event ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the deleted event.
+ */
+ getDeletedEvent(eventId: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ const conditions: any = {
+ id: eventId
+ };
+
+ return site.getDb().getRecord(AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE, conditions);
+ });
+ }
+
+ /**
+ * Get an offline event.
+ *
+ * @param {number} eventId Event ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the event.
+ */
+ getEvent(eventId: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ const conditions: any = {
+ id: eventId
+ };
+
+ return site.getDb().getRecord(AddonCalendarOfflineProvider.EVENTS_TABLE, conditions);
+ });
+ }
+
+ /**
+ * Check if there are offline events to send.
+ *
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with boolean: true if has offline events, false otherwise.
+ */
+ hasEditedEvents(siteId?: string): Promise {
+ return this.getAllEditedEvents(siteId).then((events) => {
+ return !!events.length;
+ }).catch(() => {
+ // No offline data found, return false.
+ return false;
+ });
+ }
+
+ /**
+ * Check whether there's offline data for a site.
+ *
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with boolean: true if has offline data, false otherwise.
+ */
+ hasOfflineData(siteId?: string): Promise {
+ return this.getAllEventsIds(siteId).then((ids) => {
+ return ids.length > 0;
+ });
+ }
+
+ /**
+ * Check if an event is deleted.
+ *
+ * @param {number} eventId Event ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with boolean: whether the event is deleted.
+ */
+ isEventDeleted(eventId: number, siteId?: string): Promise {
+ return this.getDeletedEvent(eventId, siteId).then((event) => {
+ return !!event;
+ }).catch(() => {
+ return false;
+ });
+ }
+
+ /**
+ * Mark an event as deleted.
+ *
+ * @param {number} eventId Event ID to delete.
+ * @param {number} name Name of the event to delete.
+ * @param {boolean} [deleteAll] If it's a repeated event. whether to delete all events of the series.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when done.
+ */
+ markDeleted(eventId: number, name: string, deleteAll?: boolean, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ const event = {
+ id: eventId,
+ name: name || '',
+ repeat: deleteAll ? 1 : 0,
+ timemodified: Date.now()
+ };
+
+ return site.getDb().insertRecord(AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE, event);
+ });
+ }
+
+ /**
+ * Offline version for adding a new discussion to a forum.
+ *
+ * @param {number} eventId Event ID. If it's a new event, set it to undefined/null.
+ * @param {any} data Event data.
+ * @param {number} [timeCreated] The time the event was created. If not defined, current time.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the stored event.
+ */
+ saveEvent(eventId: number, data: any, timeCreated?: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ timeCreated = timeCreated || Date.now();
+
+ const event = {
+ id: eventId || -timeCreated,
+ name: data.name,
+ timestart: data.timestart,
+ eventtype: data.eventtype,
+ categoryid: data.categoryid || null,
+ courseid: data.courseid || null,
+ groupcourseid: data.groupcourseid || null,
+ groupid: data.groupid || null,
+ description: data.description && data.description.text,
+ location: data.location,
+ duration: data.duration,
+ timedurationuntil: data.timedurationuntil,
+ timedurationminutes: data.timedurationminutes,
+ repeat: data.repeat ? 1 : 0,
+ repeats: data.repeats,
+ repeatid: data.repeatid,
+ repeateditall: data.repeateditall ? 1 : 0,
+ timecreated: timeCreated,
+ userid: site.getUserId()
+ };
+
+ return site.getDb().insertRecord(AddonCalendarOfflineProvider.EVENTS_TABLE, event).then(() => {
+ return event;
+ });
+ });
+ }
+
+ /**
+ * Unmark an event as deleted.
+ *
+ * @param {number} eventId Event ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved if deleted, rejected if failure.
+ */
+ unmarkDeleted(eventId: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ const conditions: any = {
+ id: eventId
+ };
+
+ return site.getDb().deleteRecords(AddonCalendarOfflineProvider.DELETED_EVENTS_TABLE, conditions);
+ });
+ }
+}
diff --git a/src/addon/calendar/providers/calendar-sync.ts b/src/addon/calendar/providers/calendar-sync.ts
new file mode 100644
index 00000000000..bc9d96f3373
--- /dev/null
+++ b/src/addon/calendar/providers/calendar-sync.ts
@@ -0,0 +1,300 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { TranslateService } from '@ngx-translate/core';
+import { CoreSyncBaseProvider } from '@classes/base-sync';
+import { CoreCourseProvider } from '@core/course/providers/course';
+import { CoreAppProvider } from '@providers/app';
+import { CoreLoggerProvider } from '@providers/logger';
+import { CoreEventsProvider } from '@providers/events';
+import { CoreSitesProvider } from '@providers/sites';
+import { CoreSyncProvider } from '@providers/sync';
+import { CoreTextUtilsProvider } from '@providers/utils/text';
+import { CoreTimeUtilsProvider } from '@providers/utils/time';
+import { CoreUtilsProvider } from '@providers/utils/utils';
+import { AddonCalendarProvider } from './calendar';
+import { AddonCalendarOfflineProvider } from './calendar-offline';
+import { AddonCalendarHelperProvider } from './helper';
+
+/**
+ * Service to sync calendar.
+ */
+@Injectable()
+export class AddonCalendarSyncProvider extends CoreSyncBaseProvider {
+
+ static AUTO_SYNCED = 'addon_calendar_autom_synced';
+ static MANUAL_SYNCED = 'addon_calendar_manual_synced';
+ static SYNC_ID = 'calendar';
+
+ constructor(translate: TranslateService,
+ appProvider: CoreAppProvider,
+ courseProvider: CoreCourseProvider,
+ private eventsProvider: CoreEventsProvider,
+ loggerProvider: CoreLoggerProvider,
+ sitesProvider: CoreSitesProvider,
+ syncProvider: CoreSyncProvider,
+ textUtils: CoreTextUtilsProvider,
+ timeUtils: CoreTimeUtilsProvider,
+ private utils: CoreUtilsProvider,
+ private calendarProvider: AddonCalendarProvider,
+ private calendarOffline: AddonCalendarOfflineProvider,
+ private calendarHelper: AddonCalendarHelperProvider) {
+
+ super('AddonCalendarSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate,
+ timeUtils);
+ }
+
+ /**
+ * Try to synchronize all events in a certain site or in all sites.
+ *
+ * @param {string} [siteId] Site ID to sync. If not defined, sync all sites.
+ * @param {boolean} [force] Wether to force sync not depending on last execution.
+ * @return {Promise} Promise resolved if sync is successful, rejected if sync fails.
+ */
+ syncAllEvents(siteId?: string, force?: boolean): Promise {
+ return this.syncOnSites('all calendar events', this.syncAllEventsFunc.bind(this), [force], siteId);
+ }
+
+ /**
+ * Sync all events on a site.
+ *
+ * @param {string} siteId Site ID to sync.
+ * @param {boolean} [force] Wether to force sync not depending on last execution.
+ * @return {Promise} Promise resolved if sync is successful, rejected if sync fails.
+ */
+ protected syncAllEventsFunc(siteId: string, force?: boolean): Promise {
+
+ const promise = force ? this.syncEvents(siteId) : this.syncEventsIfNeeded(siteId);
+
+ return promise.then((result) => {
+ if (result && result.updated) {
+ // Sync successful, send event.
+ this.eventsProvider.trigger(AddonCalendarSyncProvider.AUTO_SYNCED, {
+ warnings: result.warnings,
+ events: result.events,
+ deleted: result.deleted
+ }, siteId);
+ }
+ });
+ }
+
+ /**
+ * Sync a site events only if a certain time has passed since the last time.
+ *
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when the events are synced or if it doesn't need to be synced.
+ */
+ syncEventsIfNeeded(siteId?: string): Promise {
+ siteId = siteId || this.sitesProvider.getCurrentSiteId();
+
+ return this.isSyncNeeded(AddonCalendarSyncProvider.SYNC_ID, siteId).then((needed) => {
+ if (needed) {
+ return this.syncEvents(siteId);
+ }
+ });
+ }
+
+ /**
+ * Synchronize all offline events of a certain site.
+ *
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved if sync is successful, rejected otherwise.
+ */
+ syncEvents(siteId?: string): Promise {
+ siteId = siteId || this.sitesProvider.getCurrentSiteId();
+
+ if (this.isSyncing(AddonCalendarSyncProvider.SYNC_ID, siteId)) {
+ // There's already a sync ongoing for this site, return the promise.
+ return this.getOngoingSync(AddonCalendarSyncProvider.SYNC_ID, siteId);
+ }
+
+ this.logger.debug('Try to sync calendar events for site ' + siteId);
+
+ const result = {
+ warnings: [],
+ events: [],
+ deleted: [],
+ toinvalidate: [],
+ updated: false
+ };
+ let offlineEventIds: number[];
+
+ // Get offline events.
+ const syncPromise = this.calendarOffline.getAllEventsIds(siteId).catch(() => {
+ // No offline data found, return empty list.
+ return [];
+ }).then((eventIds) => {
+ offlineEventIds = eventIds;
+
+ if (!eventIds.length) {
+ // Nothing to sync.
+ return;
+ } else if (!this.appProvider.isOnline()) {
+ // Cannot sync in offline.
+ return Promise.reject(null);
+ }
+
+ const promises = [];
+
+ offlineEventIds.forEach((eventId) => {
+ promises.push(this.syncOfflineEvent(eventId, result, siteId));
+ });
+
+ return this.utils.allPromises(promises);
+ }).then(() => {
+ if (result.updated) {
+
+ // Data has been sent to server. Now invalidate the WS calls.
+ const promises = [
+ this.calendarProvider.invalidateEventsList(siteId),
+ this.calendarHelper.refreshAfterChangeEvents(result.toinvalidate, siteId)
+ ];
+
+ return Promise.all(promises).catch(() => {
+ // Ignore errors.
+ });
+ }
+ }).then(() => {
+ // Sync finished, set sync time.
+ return this.setSyncTime(AddonCalendarSyncProvider.SYNC_ID, siteId).catch(() => {
+ // Ignore errors.
+ });
+ }).then(() => {
+ // All done, return the result.
+ return result;
+ });
+
+ return this.addOngoingSync(AddonCalendarSyncProvider.SYNC_ID, syncPromise, siteId);
+ }
+
+ /**
+ * Synchronize an offline event.
+ *
+ * @param {number} eventId The event ID to sync.
+ * @param {any} result Object where to store the result of the sync.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved if sync is successful, rejected otherwise.
+ */
+ protected syncOfflineEvent(eventId: number, result: any, siteId?: string): Promise {
+
+ // Verify that event isn't blocked.
+ if (this.syncProvider.isBlocked(AddonCalendarProvider.COMPONENT, eventId, siteId)) {
+ this.logger.debug('Cannot sync event ' + eventId + ' because it is blocked.');
+
+ return Promise.reject(this.translate.instant('core.errorsyncblocked',
+ {$a: this.translate.instant('addon.calendar.calendarevent')}));
+ }
+
+ // First of all, check if the event has been deleted.
+ return this.calendarOffline.getDeletedEvent(eventId, siteId).then((data) => {
+ // Delete the event.
+ return this.calendarProvider.deleteEventOnline(data.id, data.repeat, siteId).then(() => {
+ result.updated = true;
+ result.deleted.push(eventId);
+
+ // Event sent, delete the offline data.
+ const promises = [];
+
+ promises.push(this.calendarOffline.unmarkDeleted(eventId, siteId));
+ promises.push(this.calendarOffline.deleteEvent(eventId, siteId).catch(() => {
+ // Ignore errors, maybe there was no edit data.
+ }));
+
+ // We need the event data to invalidate it. Get it from local DB.
+ promises.push(this.calendarProvider.getEventFromLocalDb(eventId, siteId).then((event) => {
+ result.toinvalidate.push({
+ event: event,
+ repeated: data.repeat ? event.eventcount : 1
+ });
+ }).catch(() => {
+ // Ignore errors.
+ }));
+
+ return Promise.all(promises);
+ }).catch((error) => {
+
+ if (this.utils.isWebServiceError(error)) {
+ // The WebService has thrown an error, this means that the event cannot be created. Delete it.
+ result.updated = true;
+
+ const promises = [];
+
+ promises.push(this.calendarOffline.unmarkDeleted(eventId, siteId));
+ promises.push(this.calendarOffline.deleteEvent(eventId, siteId).catch(() => {
+ // Ignore errors, maybe there was no edit data.
+ }));
+
+ return Promise.all(promises).then(() => {
+ // Event deleted, add a warning.
+ result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', {
+ component: this.translate.instant('addon.calendar.calendarevent'),
+ name: data.name,
+ error: this.textUtils.getErrorMessageFromError(error)
+ }));
+ });
+ }
+
+ // Local error, reject.
+ return Promise.reject(error);
+ });
+ }, () => {
+
+ // Not deleted. Now get the event data.
+ return this.calendarOffline.getEvent(eventId, siteId).then((event) => {
+ // Try to send the data.
+ const data = this.utils.clone(event); // Clone the object because it will be modified in the submit function.
+
+ data.description = {
+ text: data.description,
+ format: 1
+ };
+
+ return this.calendarProvider.submitEventOnline(eventId > 0 ? eventId : undefined, data, siteId).then((newEvent) => {
+ result.updated = true;
+ result.events.push(newEvent);
+
+ // Add data to invalidate.
+ const numberOfRepetitions = data.repeat ? data.repeats :
+ (data.repeateditall && newEvent.repeatid ? newEvent.eventcount : 1);
+
+ result.toinvalidate.push({
+ event: newEvent,
+ repeated: numberOfRepetitions
+ });
+
+ // Event sent, delete the offline data.
+ return this.calendarOffline.deleteEvent(event.id, siteId);
+ }).catch((error) => {
+ if (this.utils.isWebServiceError(error)) {
+ // The WebService has thrown an error, this means that the event cannot be created. Delete it.
+ result.updated = true;
+
+ return this.calendarOffline.deleteEvent(event.id, siteId).then(() => {
+ // Event deleted, add a warning.
+ result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', {
+ component: this.translate.instant('addon.calendar.calendarevent'),
+ name: event.name,
+ error: this.textUtils.getErrorMessageFromError(error)
+ }));
+ });
+ }
+
+ // Local error, reject.
+ return Promise.reject(error);
+ });
+ });
+ });
+ }
+}
diff --git a/src/addon/calendar/providers/calendar.ts b/src/addon/calendar/providers/calendar.ts
index 40dcd2208c4..892cb35cd91 100644
--- a/src/addon/calendar/providers/calendar.ts
+++ b/src/addon/calendar/providers/calendar.ts
@@ -15,15 +15,23 @@
import { Injectable } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
-import { CoreSite } from '@classes/site';
+import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { CoreCoursesProvider } from '@core/courses/providers/courses';
+import { CoreAppProvider } from '@providers/app';
+import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
+import { CoreUrlUtilsProvider } from '@providers/utils/url';
+import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreGroupsProvider } from '@providers/groups';
import { CoreConstants } from '@core/constants';
import { CoreLocalNotificationsProvider } from '@providers/local-notifications';
import { CoreConfigProvider } from '@providers/config';
import { ILocalNotification } from '@ionic-native/local-notifications';
import { SQLiteDB } from '@classes/sqlitedb';
+import { AddonCalendarOfflineProvider } from './calendar-offline';
+import { CoreUserProvider } from '@core/user/providers/user';
+import { TranslateService } from '@ngx-translate/core';
+import * as moment from 'moment';
/**
* Service to handle calendar events.
@@ -35,14 +43,61 @@ export class AddonCalendarProvider {
static DEFAULT_NOTIFICATION_TIME_CHANGED = 'AddonCalendarDefaultNotificationTimeChangedEvent';
static DEFAULT_NOTIFICATION_TIME_SETTING = 'mmaCalendarDefaultNotifTime';
static DEFAULT_NOTIFICATION_TIME = 60;
+ static STARTING_WEEK_DAY = 'addon_calendar_starting_week_day';
+ static NEW_EVENT_EVENT = 'addon_calendar_new_event';
+ static NEW_EVENT_DISCARDED_EVENT = 'addon_calendar_new_event_discarded';
+ static EDIT_EVENT_EVENT = 'addon_calendar_edit_event';
+ static DELETED_EVENT_EVENT = 'addon_calendar_deleted_event';
+ static UNDELETED_EVENT_EVENT = 'addon_calendar_undeleted_event';
+ static TYPE_CATEGORY = 'category';
+ static TYPE_COURSE = 'course';
+ static TYPE_GROUP = 'group';
+ static TYPE_SITE = 'site';
+ static TYPE_USER = 'user';
+
+ static CALENDAR_TF_24 = '%H:%M'; // Calendar time in 24 hours format.
+ static CALENDAR_TF_12 = '%I:%M %p'; // Calendar time in 12 hours format.
+
protected ROOT_CACHE_KEY = 'mmaCalendar:';
+ protected weekDays = [
+ {
+ shortname: 'addon.calendar.sun',
+ fullname: 'addon.calendar.sunday'
+ },
+ {
+ shortname: 'addon.calendar.mon',
+ fullname: 'addon.calendar.monday'
+ },
+ {
+ shortname: 'addon.calendar.tue',
+ fullname: 'addon.calendar.tuesday'
+ },
+ {
+ shortname: 'addon.calendar.wed',
+ fullname: 'addon.calendar.wednesday'
+ },
+ {
+ shortname: 'addon.calendar.thu',
+ fullname: 'addon.calendar.thursday'
+ },
+ {
+ shortname: 'addon.calendar.fri',
+ fullname: 'addon.calendar.friday'
+ },
+ {
+ shortname: 'addon.calendar.sat',
+ fullname: 'addon.calendar.saturday'
+ }
+ ];
+
// Variables for database.
- static EVENTS_TABLE = 'addon_calendar_events_2';
+ static EVENTS_TABLE = 'addon_calendar_events_3';
static REMINDERS_TABLE = 'addon_calendar_reminders';
protected siteSchema: CoreSiteSchema = {
name: 'AddonCalendarProvider',
- version: 2,
+ version: 3,
+ canBeCleared: [ AddonCalendarProvider.EVENTS_TABLE ],
tables: [
{
name: AddonCalendarProvider.EVENTS_TABLE,
@@ -124,6 +179,82 @@ export class AddonCalendarProvider {
{
name: 'subscriptionid',
type: 'INTEGER'
+ },
+ {
+ name: 'location',
+ type: 'TEXT'
+ },
+ {
+ name: 'eventcount',
+ type: 'INTEGER'
+ },
+ {
+ name: 'timesort',
+ type: 'INTEGER'
+ },
+ {
+ name: 'category',
+ type: 'TEXT'
+ },
+ {
+ name: 'course',
+ type: 'TEXT'
+ },
+ {
+ name: 'subscription',
+ type: 'TEXT'
+ },
+ {
+ name: 'canedit',
+ type: 'INTEGER'
+ },
+ {
+ name: 'candelete',
+ type: 'INTEGER'
+ },
+ {
+ name: 'deleteurl',
+ type: 'TEXT'
+ },
+ {
+ name: 'editurl',
+ type: 'TEXT'
+ },
+ {
+ name: 'viewurl',
+ type: 'TEXT'
+ },
+ {
+ name: 'formattedtime',
+ type: 'TEXT'
+ },
+ {
+ name: 'isactionevent',
+ type: 'INTEGER'
+ },
+ {
+ name: 'url',
+ type: 'TEXT'
+ },
+ {
+ name: 'islastday',
+ type: 'INTEGER'
+ },
+ {
+ name: 'popupname',
+ type: 'TEXT'
+ },
+ {
+ name: 'mindaytimestamp',
+ type: 'INTEGER'
+ },
+ {
+ name: 'maxdaytimestamp',
+ type: 'INTEGER'
+ },
+ {
+ name: 'draggable',
+ type: 'INTEGER'
}
]
},
@@ -150,67 +281,142 @@ export class AddonCalendarProvider {
}
],
migrate(db: SQLiteDB, oldVersion: number, siteId: string): Promise | void {
- if (oldVersion < 2) {
+ if (oldVersion < 3) {
const newTable = AddonCalendarProvider.EVENTS_TABLE;
- const oldTable = 'addon_calendar_events';
+ let oldTable = 'addon_calendar_events_2';
- return db.tableExists(oldTable).then(() => {
- return db.getAllRecords(oldTable).then((events) => {
- const now = Math.round(Date.now() / 1000);
+ return db.tableExists(oldTable).catch(() => {
+ // The v2 table doesn't exist, try with v1.
+ oldTable = 'addon_calendar_events';
- return Promise.all(events.map((event) => {
- if (event.notificationtime == 0) {
- // No reminders.
- return Promise.resolve();
- }
-
- let time;
-
- if (event.notificationtime == -1) {
- time = -1;
- } else {
- time = event.timestart - event.notificationtime * 60;
-
- if (time < now) {
- // Old reminder, just not add this.
- return Promise.resolve();
- }
- }
+ return db.tableExists(oldTable);
+ }).then(() => {
+ // Move the records from the old table.
+ // Move the records from the old table.
+ return db.getAllRecords(oldTable).then((events) => {
+ const promises = [];
- const reminder = {
- eventid: event.id,
- time: time
- };
-
- // Cancel old notification.
- this.localNotificationsProvider.cancel(event.id, AddonCalendarProvider.COMPONENT, siteId);
-
- return db.insertRecord(AddonCalendarProvider.REMINDERS_TABLE, reminder);
- })).then(() => {
- // Move the records from the old table.
- return db.insertRecordsFrom(newTable, oldTable, undefined, 'id, name, description, format, eventtype,\
- courseid, timestart, timeduration, categoryid, groupid, userid, instance, modulename, timemodified,\
- repeatid, visible, uuid, sequence, subscriptionid');
- }).then(() => {
- return db.dropTable(oldTable);
+ events.forEach((event) => {
+ promises.push(db.insertRecord(newTable, event));
});
+
+ return Promise.all(promises);
});
+ }).then(() => {
+ return db.dropTable(oldTable);
}).catch(() => {
// Old table does not exist, ignore.
});
}
- }
+ },
};
protected logger;
- constructor(logger: CoreLoggerProvider, private sitesProvider: CoreSitesProvider, private groupsProvider: CoreGroupsProvider,
- private coursesProvider: CoreCoursesProvider, private timeUtils: CoreTimeUtilsProvider,
- private localNotificationsProvider: CoreLocalNotificationsProvider, private configProvider: CoreConfigProvider) {
+ constructor(logger: CoreLoggerProvider,
+ private sitesProvider: CoreSitesProvider,
+ private groupsProvider: CoreGroupsProvider,
+ private coursesProvider: CoreCoursesProvider,
+ private textUtils: CoreTextUtilsProvider,
+ private timeUtils: CoreTimeUtilsProvider,
+ private urlUtils: CoreUrlUtilsProvider,
+ private localNotificationsProvider: CoreLocalNotificationsProvider,
+ private configProvider: CoreConfigProvider,
+ private utils: CoreUtilsProvider,
+ private calendarOffline: AddonCalendarOfflineProvider,
+ private appProvider: CoreAppProvider,
+ private translate: TranslateService,
+ private userProvider: CoreUserProvider) {
+
this.logger = logger.getInstance('AddonCalendarProvider');
this.sitesProvider.registerSiteSchema(this.siteSchema);
}
+ /**
+ * Check if a certain site allows deleting events.
+ *
+ * @param {string} [siteId] Site Id. If not defined, use current site.
+ * @return {Promise} Promise resolved with true if can delete.
+ * @since 3.3
+ */
+ canDeleteEvents(siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return this.canDeleteEventsInSite(site);
+ }).catch(() => {
+ return false;
+ });
+ }
+
+ /**
+ * Check if a certain site allows deleting events.
+ *
+ * @param {CoreSite} [site] Site. If not defined, use current site.
+ * @return {boolean} Whether events can be deleted.
+ * @since 3.3
+ */
+ canDeleteEventsInSite(site?: CoreSite): boolean {
+ site = site || this.sitesProvider.getCurrentSite();
+
+ return site.wsAvailable('core_calendar_delete_calendar_events');
+ }
+
+ /**
+ * Check if a certain site allows creating and editing events.
+ *
+ * @param {string} [siteId] Site Id. If not defined, use current site.
+ * @return {Promise} Promise resolved with true if can create/edit.
+ * @since 3.7.1
+ */
+ canEditEvents(siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return this.canEditEventsInSite(site);
+ }).catch(() => {
+ return false;
+ });
+ }
+
+ /**
+ * Check if a certain site allows creating and editing events.
+ *
+ * @param {CoreSite} [site] Site. If not defined, use current site.
+ * @return {boolean} Whether events can be created and edited.
+ * @since 3.7.1
+ */
+ canEditEventsInSite(site?: CoreSite): boolean {
+ site = site || this.sitesProvider.getCurrentSite();
+
+ // The WS to create/edit events requires a fix that was integrated in 3.7.1.
+ return site.isVersionGreaterEqualThan('3.7.1');
+ }
+
+ /**
+ * Check if a certain site allows viewing events in monthly view.
+ *
+ * @param {string} [siteId] Site Id. If not defined, use current site.
+ * @return {Promise} Promise resolved with true if monthly view is supported.
+ * @since 3.4
+ */
+ canViewMonth(siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return this.canViewMonthInSite(site);
+ }).catch(() => {
+ return false;
+ });
+ }
+
+ /**
+ * Check if a certain site allows viewing events in monthly view.
+ *
+ * @param {CoreSite} [site] Site. If not defined, use current site.
+ * @return {boolean} Whether monthly view is supported.
+ * @since 3.4
+ */
+ canViewMonthInSite(site?: CoreSite): boolean {
+ site = site || this.sitesProvider.getCurrentSite();
+
+ return site.wsAvailable('core_calendar_get_calendar_monthly_view');
+ }
+
/**
* Removes expired events from local DB.
*
@@ -219,23 +425,97 @@ export class AddonCalendarProvider {
*/
cleanExpiredEvents(siteId?: string): Promise {
return this.sitesProvider.getSite(siteId).then((site) => {
+ if (this.canViewMonthInSite(site)) {
+ // Site supports monthly view, don't clean expired events because user can see past events.
+ return;
+ }
+
return site.getDb().getRecordsSelect(AddonCalendarProvider.EVENTS_TABLE, 'timestart + timeduration < ?',
[this.timeUtils.timestamp()]).then((events) => {
return Promise.all(events.map((event) => {
- return this.deleteEvent(event.id, siteId);
+ return this.deleteLocalEvent(event.id, siteId);
}));
});
});
}
/**
- * Delete event cancelling all the reminders and notifications.
+ * Delete an event.
+ *
+ * @param {number} eventId Event ID to delete.
+ * @param {string} name Name of the event to delete.
+ * @param {boolean} [deleteAll] If it's a repeated event. whether to delete all events of the series.
+ * @param {boolean} [forceOffline] True to always save it in offline.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when done.
+ */
+ deleteEvent(eventId: number, name: string, deleteAll?: boolean, forceOffline?: boolean, siteId?: string): Promise {
+
+ siteId = siteId || this.sitesProvider.getCurrentSiteId();
+
+ // Function to store the submission to be synchronized later.
+ const storeOffline = (): Promise => {
+ return this.calendarOffline.markDeleted(eventId, name, deleteAll, siteId).then(() => {
+ return false;
+ });
+ };
+
+ if (forceOffline || !this.appProvider.isOnline()) {
+ // App is offline, store the action.
+ return storeOffline();
+ }
+
+ // If the event is already stored, discard it first.
+ return this.calendarOffline.unmarkDeleted(eventId, siteId).then(() => {
+ return this.deleteEventOnline(eventId, deleteAll, siteId).then(() => {
+ return true;
+ }).catch((error) => {
+ if (error && !this.utils.isWebServiceError(error)) {
+ // Couldn't connect to server, store in offline.
+ return storeOffline();
+ } else {
+ // The WebService has thrown an error, reject.
+ return Promise.reject(error);
+ }
+ });
+ });
+ }
+
+ /**
+ * Delete an event. It will fail if offline or cannot connect.
+ *
+ * @param {number} eventId Event ID to delete.
+ * @param {boolean} [deleteAll] If it's a repeated event. whether to delete all events of the series.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when done.
+ */
+ deleteEventOnline(eventId: number, deleteAll?: boolean, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+
+ const params = {
+ events: [
+ {
+ eventid: eventId,
+ repeat: deleteAll ? 1 : 0
+ }
+ ]
+ },
+ preSets = {
+ responseExpected: false
+ };
+
+ return site.write('core_calendar_delete_calendar_events', params, preSets);
+ });
+ }
+
+ /**
+ * Delete a locally stored event cancelling all the reminders and notifications.
*
* @param {number} eventId Event ID.
* @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site.
* @return {Promise} Resolved when done.
*/
- protected deleteEvent(eventId: number, siteId?: string): Promise {
+ protected deleteLocalEvent(eventId: number, siteId?: string): Promise {
return this.sitesProvider.getSite(siteId).then((site) => {
siteId = site.getId();
@@ -255,6 +535,131 @@ export class AddonCalendarProvider {
});
}
+ /**
+ * Check if event ends the same day or not.
+ *
+ * @param {any} event Event info.
+ * @return {boolean} If the .
+ */
+ endsSameDay(event: any): boolean {
+ if (!event.timeduration) {
+ // No duration.
+ return true;
+ }
+
+ // Check if day has changed.
+ return moment(event.timestart * 1000).isSame((event.timestart + event.timeduration) * 1000, 'day');
+ }
+
+ /**
+ * Format event time. Similar to calendar_format_event_time.
+ *
+ * @param {any} event Event to format.
+ * @param {string} format Calendar time format (from getCalendarTimeFormat).
+ * @param {boolean} [useCommonWords=true] Whether to use common words like "Today", "Yesterday", etc.
+ * @param {number} [seenDay] Timestamp of day currently seen. If set, the function will not add links to this day.
+ * @param {number} [showTime=0] Determine the show time GMT timestamp.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the formatted event time.
+ */
+ formatEventTime(event: any, format: string, useCommonWords: boolean = true, seenDay?: number, showTime: number = 0,
+ siteId?: string): Promise {
+
+ const start = event.timestart * 1000,
+ end = (event.timestart + event.timeduration) * 1000;
+ let time;
+
+ if (event.timeduration) {
+
+ if (moment(start).isSame(end, 'day')) {
+ // Event starts and ends the same day.
+ if (event.timeduration == CoreConstants.SECONDS_DAY) {
+ time = this.translate.instant('addon.calendar.allday');
+ } else {
+ time = this.timeUtils.userDate(start, format) + ' » ' +
+ this.timeUtils.userDate(end, format);
+ }
+
+ } else {
+ // Event lasts more than one day.
+ const timeStart = this.timeUtils.userDate(start, format),
+ timeEnd = this.timeUtils.userDate(end, format),
+ promises = [];
+
+ // Don't use common words when the event lasts more than one day.
+ let dayStart = this.getDayRepresentation(start, false) + ', ',
+ dayEnd = this.getDayRepresentation(end, false) + ', ';
+
+ // Add links to the days if needed.
+ if (dayStart && (!seenDay || !moment(seenDay).isSame(start, 'day'))) {
+ promises.push(this.getViewUrl('day', event.timestart, undefined, siteId).then((url) => {
+ dayStart = this.urlUtils.buildLink(url, dayStart);
+ }));
+ }
+ if (dayEnd && (!seenDay || !moment(seenDay).isSame(end, 'day'))) {
+ promises.push(this.getViewUrl('day', end / 1000, undefined, siteId).then((url) => {
+ dayEnd = this.urlUtils.buildLink(url, dayEnd);
+ }));
+ }
+
+ return Promise.all(promises).then(() => {
+ return dayStart + timeStart + ' » ' + dayEnd + timeEnd;
+ });
+ }
+ } else {
+ // There is no time duration.
+ time = this.timeUtils.userDate(start, format);
+ }
+
+ if (!showTime) {
+ // Display day + time.
+ if (seenDay && moment(seenDay).isSame(start, 'day')) {
+ // This day is currently being displayed, don't add an link.
+ return Promise.resolve(this.getDayRepresentation(start, useCommonWords) + ', ' + time);
+ } else {
+ // Add link to view the day.
+ return this.getViewUrl('day', event.timestart, undefined, siteId).then((url) => {
+ return this.urlUtils.buildLink(url, this.getDayRepresentation(start, useCommonWords)) + ', ' + time;
+ });
+ }
+ } else {
+ return Promise.resolve(time);
+ }
+ }
+
+ /**
+ * Get access information for a calendar (either course calendar or site calendar).
+ *
+ * @param {number} [courseId] Course ID. If not defined, site calendar.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with object with access information.
+ * @since 3.7
+ */
+ getAccessInformation(courseId?: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ const params: any = {},
+ preSets = {
+ cacheKey: this.getAccessInformationCacheKey(courseId)
+ };
+
+ if (courseId) {
+ params.courseid = courseId;
+ }
+
+ return site.read('core_calendar_get_calendar_access_information', params, preSets);
+ });
+ }
+
+ /**
+ * Get cache key for calendar access information WS calls.
+ *
+ * @param {number} [courseId] Course ID.
+ * @return {string} Cache key.
+ */
+ protected getAccessInformationCacheKey(courseId?: number): string {
+ return this.ROOT_CACHE_KEY + 'accessInformation:' + (courseId || 0);
+ }
+
/**
* Get all calendar events from local Db.
*
@@ -267,6 +672,128 @@ export class AddonCalendarProvider {
});
}
+ /**
+ * Get the type of events a user can create (either course calendar or site calendar).
+ *
+ * @param {number} [courseId] Course ID. If not defined, site calendar.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with an object indicating the types.
+ * @since 3.7
+ */
+ getAllowedEventTypes(courseId?: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ const params: any = {},
+ preSets = {
+ cacheKey: this.getAllowedEventTypesCacheKey(courseId)
+ };
+
+ if (courseId) {
+ params.courseid = courseId;
+ }
+
+ return site.read('core_calendar_get_allowed_event_types', params, preSets).then((response) => {
+ // Convert the array to an object.
+ const result = {};
+
+ if (response.allowedeventtypes) {
+ response.allowedeventtypes.map((type) => {
+ result[type] = true;
+ });
+ }
+
+ return result;
+ });
+ });
+ }
+
+ /**
+ * Get cache key for calendar allowed event types WS calls.
+ *
+ * @param {number} [courseId] Course ID.
+ * @return {string} Cache key.
+ */
+ protected getAllowedEventTypesCacheKey(courseId?: number): string {
+ return this.ROOT_CACHE_KEY + 'allowedEventTypes:' + (courseId || 0);
+ }
+
+ /**
+ * Get the "look ahead" for a certain user.
+ *
+ * @param {string} [siteId] ID of the site. If not defined, use current site.
+ * @return {Promise} Promise resolved with the look ahead (number of days).
+ */
+ getCalendarLookAhead(siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return this.userProvider.getUserPreference('calendar_lookahead').catch((error) => {
+ // Ignore errors.
+ }).then((value): any => {
+ if (value != null) {
+ return value;
+ }
+
+ return site.getStoredConfig('calendar_lookahead');
+ });
+ });
+ }
+
+ /**
+ * Get the time format to use in calendar.
+ *
+ * @param {string} [siteId] ID of the site. If not defined, use current site.
+ * @return {Promise} Promise resolved with the format.
+ */
+ getCalendarTimeFormat(siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return this.userProvider.getUserPreference('calendar_timeformat').catch((error) => {
+ // Ignore errors.
+ }).then((format) => {
+
+ if (!format || format === '0') {
+ format = site.getStoredConfig('calendar_site_timeformat');
+ }
+
+ if (format === AddonCalendarProvider.CALENDAR_TF_12) {
+ format = this.translate.instant('core.strftimetime12');
+ } else if (format === AddonCalendarProvider.CALENDAR_TF_24) {
+ format = this.translate.instant('core.strftimetime24');
+ }
+
+ return format && format !== '0' ? format : this.translate.instant('core.strftimetime');
+ });
+ });
+ }
+
+ /**
+ * Return the representation day. Equivalent to Moodle's calendar_day_representation.
+ *
+ * @param {number} time Timestamp to get the day from.
+ * @param {boolean} [useCommonWords=true] Whether to use common words like "Today", "Yesterday", etc.
+ * @return {string} The formatted date/time.
+ */
+ getDayRepresentation(time: number, useCommonWords: boolean = true): string {
+
+ if (!useCommonWords) {
+ // We don't want words, just a date.
+ return this.timeUtils.userDate(time, 'core.strftimedayshort');
+ }
+
+ const date = moment(time),
+ today = moment();
+
+ if (date.isSame(today, 'day')) {
+ return this.translate.instant('addon.calendar.today');
+
+ } else if (date.isSame(today.clone().subtract(1, 'days'), 'day')) {
+ return this.translate.instant('addon.calendar.yesterday');
+
+ } else if (date.isSame(today.clone().add(1, 'days'), 'day')) {
+ return this.translate.instant('addon.calendar.tomorrow');
+
+ } else {
+ return this.timeUtils.userDate(time, 'core.strftimedayshort');
+ }
+ }
+
/**
* Get the configured default notification time.
*
@@ -339,6 +866,10 @@ export class AddonCalendarProvider {
return site.read('core_calendar_get_calendar_event_by_id', data, preSets).then((response) => {
return response.event;
+ }).catch((error) => {
+ return this.getEventFromLocalDb(id).catch(() => {
+ return Promise.reject(error);
+ });
});
});
}
@@ -362,46 +893,158 @@ export class AddonCalendarProvider {
*/
getEventFromLocalDb(id: number, siteId?: string): Promise {
return this.sitesProvider.getSite(siteId).then((site) => {
- return site.getDb().getRecord(AddonCalendarProvider.EVENTS_TABLE, { id: id });
+ return site.getDb().getRecord(AddonCalendarProvider.EVENTS_TABLE, { id: id }).then((event) => {
+ if (this.isGetEventByIdAvailableInSite(site)) {
+ // Calculate data to match the new WS.
+ event.descriptionformat = event.format;
+ event.iscourseevent = event.eventtype == AddonCalendarProvider.TYPE_COURSE;
+ event.iscategoryevent = event.eventtype == AddonCalendarProvider.TYPE_CATEGORY;
+ event.normalisedeventtype = this.getEventType(event);
+ event.category = this.textUtils.parseJSON(event.category, null);
+ event.course = this.textUtils.parseJSON(event.course, null);
+ event.subscription = this.textUtils.parseJSON(event.subscription, null);
+ }
+
+ return event;
+ });
+ });
+ }
+
+ /**
+ * Adds an event reminder and schedule a new notification.
+ *
+ * @param {any} event Event to update its notification time.
+ * @param {number} time New notification setting timestamp.
+ * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site.
+ * @return {Promise} Promise resolved when the notification is updated.
+ */
+ addEventReminder(event: any, time: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ const reminder = {
+ eventid: event.id,
+ time: time
+ };
+
+ return site.getDb().insertRecord(AddonCalendarProvider.REMINDERS_TABLE, reminder).then((reminderId) => {
+ return this.scheduleEventNotification(event, reminderId, time, site.getId());
+ });
+ });
+ }
+
+ /**
+ * Return the normalised event type.
+ * Activity events are normalised to be course events.
+ *
+ * @param {any} event The event to get its type.
+ * @return {string} Event type.
+ */
+ getEventType(event: any): string {
+ if (event.modulename) {
+ return 'course';
+ }
+
+ return event.eventtype;
+ }
+
+ /**
+ * Remove an event reminder and cancel the notification.
+ *
+ * @param {number} id Reminder ID.
+ * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site.
+ * @return {Promise} Promise resolved when the notification is updated.
+ */
+ deleteEventReminder(id: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ if (this.localNotificationsProvider.isAvailable()) {
+ this.localNotificationsProvider.cancel(id, AddonCalendarProvider.COMPONENT, site.getId());
+ }
+
+ return site.getDb().deleteRecords(AddonCalendarProvider.REMINDERS_TABLE, {id: id});
+ });
+ }
+
+ /**
+ * Get calendar events for a certain day.
+ *
+ * @param {number} year Year to get.
+ * @param {number} month Month to get.
+ * @param {number} day Day to get.
+ * @param {number} [courseId] Course to get.
+ * @param {number} [categoryId] Category to get.
+ * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down).
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the response.
+ */
+ getDayEvents(year: number, month: number, day: number, courseId?: number, categoryId?: number, ignoreCache?: boolean,
+ siteId?: string): Promise {
+
+ return this.sitesProvider.getSite(siteId).then((site) => {
+
+ const data: any = {
+ year: year,
+ month: month,
+ day: day
+ };
+
+ if (courseId) {
+ data.courseid = courseId;
+ }
+ if (categoryId) {
+ data.categoryid = categoryId;
+ }
+
+ const preSets: CoreSiteWSPreSets = {
+ cacheKey: this.getDayEventsCacheKey(year, month, day, courseId, categoryId),
+ updateFrequency: CoreSite.FREQUENCY_SOMETIMES
+ };
+
+ if (ignoreCache) {
+ preSets.getFromCache = false;
+ preSets.emergencyCache = false;
+ }
+
+ return site.read('core_calendar_get_calendar_day_view', data, preSets).then((response) => {
+ this.storeEventsInLocalDB(response.events, siteId);
+
+ return response;
+ });
});
}
/**
- * Adds an event reminder and schedule a new notification.
+ * Get prefix cache key for day events WS calls.
*
- * @param {any} event Event to update its notification time.
- * @param {number} time New notification setting timestamp.
- * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site.
- * @return {Promise} Promise resolved when the notification is updated.
+ * @return {string} Prefix Cache key.
*/
- addEventReminder(event: any, time: number, siteId?: string): Promise {
- return this.sitesProvider.getSite(siteId).then((site) => {
- const reminder = {
- eventid: event.id,
- time: time
- };
-
- return site.getDb().insertRecord(AddonCalendarProvider.REMINDERS_TABLE, reminder).then((reminderId) => {
- return this.scheduleEventNotification(event, reminderId, time, site.getId());
- });
- });
+ protected getDayEventsPrefixCacheKey(): string {
+ return this.ROOT_CACHE_KEY + 'day:';
}
/**
- * Remove an event reminder and cancel the notification.
+ * Get prefix cache key for a certain day for day events WS calls.
*
- * @param {number} id Reminder ID.
- * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site.
- * @return {Promise} Promise resolved when the notification is updated.
+ * @param {number} year Year to get.
+ * @param {number} month Month to get.
+ * @param {number} day Day to get.
+ * @return {string} Prefix Cache key.
*/
- deleteEventReminder(id: number, siteId?: string): Promise {
- return this.sitesProvider.getSite(siteId).then((site) => {
- if (this.localNotificationsProvider.isAvailable()) {
- this.localNotificationsProvider.cancel(id, AddonCalendarProvider.COMPONENT, site.getId());
- }
+ protected getDayEventsDayPrefixCacheKey(year: number, month: number, day: number): string {
+ return this.getDayEventsPrefixCacheKey() + year + ':' + month + ':' + day + ':';
+ }
- return site.getDb().deleteRecords(AddonCalendarProvider.REMINDERS_TABLE, {id: id});
- });
+ /**
+ * Get cache key for day events WS calls.
+ *
+ * @param {number} year Year to get.
+ * @param {number} month Month to get.
+ * @param {number} day Day to get.
+ * @param {number} [courseId] Course to get.
+ * @param {number} [categoryId] Category to get.
+ * @return {string} Cache key.
+ */
+ protected getDayEventsCacheKey(year: number, month: number, day: number, courseId?: number, categoryId?: number): string {
+ return this.getDayEventsDayPrefixCacheKey(year, month, day) + (courseId ? courseId : '') + ':' +
+ (categoryId ? categoryId : '');
}
/**
@@ -421,16 +1064,20 @@ export class AddonCalendarProvider {
* Get the events in a certain period. The period is calculated like this:
* start time: now + daysToStart
* end time: start time + daysInterval
- * E.g. using provider.getEventsList(30, 30) is going to get the events starting after 30 days from now
+ * E.g. using provider.getEventsList(undefined, 30, 30) is going to get the events starting after 30 days from now
* and ending before 60 days from now.
*
- * @param {number} [daysToStart=0] Number of days from now to start getting events.
+ * @param {number} [initialTime] Timestamp when the first fetch was done. If not defined, current time.
+ * @param {number} [daysToStart=0] Number of days from now to start getting events.
* @param {number} [daysInterval=30] Number of days between timestart and timeend.
* @param {string} [siteId] Site to get the events from. If not defined, use current site.
* @return {Promise} Promise to be resolved when the participants are retrieved.
*/
- getEventsList(daysToStart: number = 0, daysInterval: number = AddonCalendarProvider.DAYS_INTERVAL, siteId?: string)
- : Promise {
+ getEventsList(initialTime?: number, daysToStart: number = 0, daysInterval: number = AddonCalendarProvider.DAYS_INTERVAL,
+ siteId?: string): Promise {
+
+ initialTime = initialTime || this.timeUtils.timestamp();
+
return this.sitesProvider.getSite(siteId).then((site) => {
siteId = site.getId();
const promises = [];
@@ -445,9 +1092,8 @@ export class AddonCalendarProvider {
}));
return Promise.all(promises).then(() => {
- const now = this.timeUtils.timestamp(),
- start = now + (CoreConstants.SECONDS_DAY * daysToStart),
- end = start + (CoreConstants.SECONDS_DAY * daysInterval),
+ const start = initialTime + (CoreConstants.SECONDS_DAY * daysToStart),
+ end = start + (CoreConstants.SECONDS_DAY * daysInterval) - 1,
data = {
options: {
userevents: 1,
@@ -472,11 +1118,15 @@ export class AddonCalendarProvider {
const preSets = {
cacheKey: this.getEventsListCacheKey(daysToStart, daysInterval),
getCacheUsingCacheKey: true,
+ uniqueCacheKey: true,
updateFrequency: CoreSite.FREQUENCY_SOMETIMES
};
return site.read('core_calendar_get_calendar_events', data, preSets).then((response) => {
- this.storeEventsInLocalDB(response.events, siteId);
+ if (!this.canViewMonthInSite(site)) {
+ // Store events only in 3.1-3.3. In 3.4+ we'll use the new WS that return more info.
+ this.storeEventsInLocalDB(response.events, siteId);
+ }
return response.events;
});
@@ -504,6 +1154,262 @@ export class AddonCalendarProvider {
return this.getEventsListPrefixCacheKey() + daysToStart + ':' + daysInterval;
}
+ /**
+ * Get calendar events from local Db that have the same repeatid.
+ *
+ * @param {number} [repeatId] Repeat Id of the event.
+ * @param {string} [siteId] ID of the site the event belongs to. If not defined, use current site.
+ * @return {Promise} Promise resolved with all the events.
+ */
+ getLocalEventsByRepeatIdFromLocalDb(repeatId: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.getDb().getRecords(AddonCalendarProvider.EVENTS_TABLE, {repeatid: repeatId});
+ });
+ }
+ /**
+ * Get monthly calendar events.
+ *
+ * @param {number} year Year to get.
+ * @param {number} month Month to get.
+ * @param {number} [courseId] Course to get.
+ * @param {number} [categoryId] Category to get.
+ * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down).
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the response.
+ */
+ getMonthlyEvents(year: number, month: number, courseId?: number, categoryId?: number, ignoreCache?: boolean, siteId?: string)
+ : Promise {
+
+ return this.sitesProvider.getSite(siteId).then((site) => {
+
+ const data: any = {
+ year: year,
+ month: month
+ };
+
+ // This parameter requires Moodle 3.5.
+ if (site.isVersionGreaterEqualThan('3.5')) {
+ // Set mini to 1 to prevent returning the course selector HTML.
+ data.mini = 1;
+ }
+
+ if (courseId) {
+ data.courseid = courseId;
+ }
+ if (categoryId) {
+ data.categoryid = categoryId;
+ }
+
+ const preSets: CoreSiteWSPreSets = {
+ cacheKey: this.getMonthlyEventsCacheKey(year, month, courseId, categoryId),
+ updateFrequency: CoreSite.FREQUENCY_SOMETIMES
+ };
+
+ if (ignoreCache) {
+ preSets.getFromCache = false;
+ preSets.emergencyCache = false;
+ }
+
+ return site.read('core_calendar_get_calendar_monthly_view', data, preSets).then((response) => {
+ response.weeks.forEach((week) => {
+ week.days.forEach((day) => {
+ this.storeEventsInLocalDB(day.events, siteId);
+ });
+ });
+
+ // Store starting week day preference, we need it in offline to show months that are not in cache.
+ if (this.appProvider.isOnline()) {
+ this.configProvider.set(AddonCalendarProvider.STARTING_WEEK_DAY, response.daynames[0].dayno);
+ }
+
+ return response;
+ });
+ });
+ }
+
+ /**
+ * Get prefix cache key for monthly events WS calls.
+ *
+ * @return {string} Prefix Cache key.
+ */
+ protected getMonthlyEventsPrefixCacheKey(): string {
+ return this.ROOT_CACHE_KEY + 'monthly:';
+ }
+
+ /**
+ * Get prefix cache key for a certain month for monthly events WS calls.
+ *
+ * @param {number} year Year to get.
+ * @param {number} month Month to get.
+ * @return {string} Prefix Cache key.
+ */
+ protected getMonthlyEventsMonthPrefixCacheKey(year: number, month: number): string {
+ return this.getMonthlyEventsPrefixCacheKey() + year + ':' + month + ':';
+ }
+
+ /**
+ * Get cache key for monthly events WS calls.
+ *
+ * @param {number} year Year to get.
+ * @param {number} month Month to get.
+ * @param {number} [courseId] Course to get.
+ * @param {number} [categoryId] Category to get.
+ * @return {string} Cache key.
+ */
+ protected getMonthlyEventsCacheKey(year: number, month: number, courseId?: number, categoryId?: number): string {
+ return this.getMonthlyEventsMonthPrefixCacheKey(year, month) + (courseId ? courseId : '') + ':' +
+ (categoryId ? categoryId : '');
+ }
+
+ /**
+ * Get upcoming calendar events.
+ *
+ * @param {number} [courseId] Course to get.
+ * @param {number} [categoryId] Category to get.
+ * @param {boolean} [ignoreCache] True if it should ignore cached data (it will always fail in offline or server down).
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the response.
+ */
+ getUpcomingEvents(courseId?: number, categoryId?: number, ignoreCache?: boolean, siteId?: string): Promise {
+
+ return this.sitesProvider.getSite(siteId).then((site) => {
+
+ const data: any = {};
+
+ if (courseId) {
+ data.courseid = courseId;
+ }
+ if (categoryId) {
+ data.categoryid = categoryId;
+ }
+
+ const preSets: CoreSiteWSPreSets = {
+ cacheKey: this.getUpcomingEventsCacheKey(courseId, categoryId),
+ updateFrequency: CoreSite.FREQUENCY_SOMETIMES
+ };
+
+ if (ignoreCache) {
+ preSets.getFromCache = false;
+ preSets.emergencyCache = false;
+ }
+
+ return site.read('core_calendar_get_calendar_upcoming_view', data, preSets).then((response) => {
+ this.storeEventsInLocalDB(response.events, siteId);
+
+ return response;
+ });
+ });
+ }
+
+ /**
+ * Get prefix cache key for upcoming events WS calls.
+ *
+ * @return {string} Prefix Cache key.
+ */
+ protected getUpcomingEventsPrefixCacheKey(): string {
+ return this.ROOT_CACHE_KEY + 'upcoming:';
+ }
+
+ /**
+ * Get cache key for upcoming events WS calls.
+ *
+ * @param {number} [courseId] Course to get.
+ * @param {number} [categoryId] Category to get.
+ * @return {string} Cache key.
+ */
+ protected getUpcomingEventsCacheKey(courseId?: number, categoryId?: number): string {
+ return this.getUpcomingEventsPrefixCacheKey() + (courseId ? courseId : '') + ':' + (categoryId ? categoryId : '');
+ }
+
+ /**
+ * Get URL to view a calendar.
+ *
+ * @param {string} view The view to load: 'month', 'day', 'upcoming', etc.
+ * @param {number} [time] Time to load. If not defined, current time.
+ * @param {string} [courseId] Course to load. If not defined, all courses.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the URL.x
+ */
+ getViewUrl(view: string, time?: number, courseId?: string, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ let url = this.textUtils.concatenatePaths(site.getURL(), 'calendar/view.php?view=' + view);
+
+ if (time) {
+ url += '&time=' + time;
+ }
+
+ if (courseId) {
+ url += '&course=' + courseId;
+ }
+
+ return url;
+ });
+ }
+
+ /**
+ * Get the week days, already ordered according to a specified starting day.
+ *
+ * @param {number} [startingDay=0] Starting day. 0=Sunday, 1=Monday, ...
+ * @return {any[]} Week days.
+ */
+ getWeekDays(startingDay?: number): any[] {
+ startingDay = startingDay || 0;
+
+ return this.weekDays.slice(startingDay).concat(this.weekDays.slice(0, startingDay));
+ }
+
+ /**
+ * Invalidates access information.
+ *
+ * @param {number} [courseId] Course ID. If not defined, site calendar.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when the data is invalidated.
+ */
+ invalidateAccessInformation(courseId?: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.invalidateWsCacheForKey(this.getAccessInformationCacheKey(courseId));
+ });
+ }
+
+ /**
+ * Invalidates allowed event types.
+ *
+ * @param {number} [courseId] Course ID. If not defined, site calendar.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when the data is invalidated.
+ */
+ invalidateAllowedEventTypes(courseId?: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.invalidateWsCacheForKey(this.getAllowedEventTypesCacheKey(courseId));
+ });
+ }
+
+ /**
+ * Invalidates day events for all days.
+ *
+ * @param {string} [siteId] Site Id. If not defined, use current site.
+ * @return {Promise} Promise resolved when the data is invalidated.
+ */
+ invalidateAllDayEvents(siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.invalidateWsCacheForKeyStartingWith(this.getDayEventsPrefixCacheKey());
+ });
+ }
+
+ /**
+ * Invalidates day events for a certain day.
+ *
+ * @param {number} year Year.
+ * @param {number} month Month.
+ * @param {number} day Day.
+ * @return {Promise} Promise resolved when the data is invalidated.
+ */
+ invalidateDayEvents(year: number, month: number, day: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.invalidateWsCacheForKeyStartingWith(this.getDayEventsDayPrefixCacheKey(year, month, day));
+ });
+ }
+
/**
* Invalidates events list and all the single events and related info.
*
@@ -537,6 +1443,77 @@ export class AddonCalendarProvider {
});
}
+ /**
+ * Invalidates monthly events for all months.
+ *
+ * @param {string} [siteId] Site Id. If not defined, use current site.
+ * @return {Promise} Promise resolved when the data is invalidated.
+ */
+ invalidateAllMonthlyEvents(siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.invalidateWsCacheForKeyStartingWith(this.getMonthlyEventsPrefixCacheKey());
+ });
+ }
+
+ /**
+ * Invalidates monthly events for a certain months.
+ *
+ * @param {number} year Year.
+ * @param {number} month Month.
+ * @return {Promise} Promise resolved when the data is invalidated.
+ */
+ invalidateMonthlyEvents(year: number, month: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.invalidateWsCacheForKeyStartingWith(this.getMonthlyEventsMonthPrefixCacheKey(year, month));
+ });
+ }
+
+ /**
+ * Invalidates upcoming events for all courses and categories.
+ *
+ * @param {string} [siteId] Site Id. If not defined, use current site.
+ * @return {Promise} Promise resolved when the data is invalidated.
+ */
+ invalidateAllUpcomingEvents(siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.invalidateWsCacheForKeyStartingWith(this.getUpcomingEventsPrefixCacheKey());
+ });
+ }
+
+ /**
+ * Invalidates upcoming events for a certain course or category.
+ *
+ * @param {number} [courseId] Course ID.
+ * @param {number} [categoryId] Category ID.
+ * @param {string} [siteId] Site Id. If not defined, use current site.
+ * @return {Promise} Promise resolved when the data is invalidated.
+ */
+ invalidateUpcomingEvents(courseId?: number, categoryId?: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.invalidateWsCacheForKeyStartingWith(this.getUpcomingEventsCacheKey(courseId, categoryId));
+ });
+ }
+
+ /**
+ * Invalidates look ahead setting.
+ *
+ * @param {string} [siteId] Site Id. If not defined, use current site.
+ * @return {Promise} Promise resolved when the data is invalidated.
+ */
+ invalidateLookAhead(siteId?: string): Promise {
+ return this.userProvider.invalidateUserPreference('calendar_lookahead', siteId);
+ }
+
+ /**
+ * Invalidates time format setting.
+ *
+ * @param {string} [siteId] Site Id. If not defined, use current site.
+ * @return {Promise} Promise resolved when the data is invalidated.
+ */
+ invalidateTimeFormat(siteId?: string): Promise {
+ return this.userProvider.invalidateUserPreference('calendar_timeformat', siteId);
+ }
+
/**
* Check if Calendar is disabled in a certain site.
*
@@ -564,11 +1541,29 @@ export class AddonCalendarProvider {
/**
* Check if the get event by ID WS is available.
*
+ * @param {string} [siteId] Site Id. If not defined, use current site.
+ * @return {Promise} Promise resolved with true if available.
+ * @since 3.4
+ */
+ isGetEventByIdAvailable(siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return this.isGetEventByIdAvailableInSite(site);
+ }).catch(() => {
+ return false;
+ });
+ }
+
+ /**
+ * Check if the get event by ID WS is available in a certain site.
+ *
+ * @param {CoreSite} [site] Site. If not defined, use current site.
* @return {boolean} Whether it's available.
* @since 3.4
*/
- isGetEventByIdAvailable(): boolean {
- return this.sitesProvider.wsAvailableInCurrentSite('core_calendar_get_calendar_event_by_id');
+ isGetEventByIdAvailableInSite(site?: CoreSite): boolean {
+ site = site || this.sitesProvider.getCurrentSite();
+
+ return site.wsAvailable('core_calendar_get_calendar_event_by_id');
}
/**
@@ -591,7 +1586,7 @@ export class AddonCalendarProvider {
return this.isDisabled(siteId).then((disabled) => {
if (!disabled) {
// Get first events.
- return this.getEventsList(undefined, undefined, siteId).then((events) => {
+ return this.getEventsList(undefined, undefined, undefined, siteId).then((events) => {
return this.scheduleEventsNotifications(events, siteId);
});
}
@@ -687,7 +1682,7 @@ export class AddonCalendarProvider {
if (timeEnd <= new Date().getTime()) {
// The event has finished already, don't schedule it.
- return this.deleteEvent(event.id, siteId);
+ return this.deleteLocalEvent(event.id, siteId);
}
return this.getEventReminders(event.id, siteId).then((reminders) => {
@@ -736,11 +1731,12 @@ export class AddonCalendarProvider {
}
});
}).then(() => {
+ // Don't store data that can be calculated like formattedtime, iscategoryevent, etc.
const eventRecord = {
id: event.id,
name: event.name,
description: event.description,
- format: event.format,
+ format: event.descriptionformat || event.format,
eventtype: event.eventtype,
courseid: event.courseid,
timestart: event.timestart,
@@ -755,7 +1751,25 @@ export class AddonCalendarProvider {
visible: event.visible,
uuid: event.uuid,
sequence: event.sequence,
- subscriptionid: event.subscriptionid
+ subscriptionid: event.subscriptionid,
+ location: event.location,
+ eventcount: event.eventcount,
+ timesort: event.timesort,
+ category: event.category ? JSON.stringify(event.category) : undefined,
+ course: event.course ? JSON.stringify(event.course) : undefined,
+ subscription: event.subscription ? JSON.stringify(event.subscription) : undefined,
+ canedit: event.canedit ? 1 : 0,
+ candelete: event.candelete ? 1 : 0,
+ deleteurl: event.deleteurl,
+ editurl: event.editurl,
+ viewurl: event.viewurl,
+ isactionevent: event.isactionevent ? 1 : 0,
+ url: event.url,
+ islastday: event.islastday ? 1 : 0,
+ popupname: event.popupname,
+ mindaytimestamp: event.mindaytimestamp,
+ maxdaytimestamp: event.maxdaytimestamp,
+ draggable: event.draggable,
};
return site.getDb().insertRecord(AddonCalendarProvider.EVENTS_TABLE, eventRecord);
@@ -780,4 +1794,88 @@ export class AddonCalendarProvider {
}));
});
}
+
+ /**
+ * Submit a calendar event.
+ *
+ * @param {number} eventId ID of the event. If undefined/null, create a new event.
+ * @param {any} formData Form data.
+ * @param {number} [timeCreated] The time the event was created. Only if modifying a new offline event.
+ * @param {boolean} [forceOffline] True to always save it in offline.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise<{sent: boolean, event: any}>} Promise resolved with the event and a boolean indicating if data was
+ * sent to server or stored in offline.
+ */
+ submitEvent(eventId: number, formData: any, timeCreated?: number, forceOffline?: boolean, siteId?: string):
+ Promise<{sent: boolean, event: any}> {
+
+ siteId = siteId || this.sitesProvider.getCurrentSiteId();
+
+ // Function to store the event to be synchronized later.
+ const storeOffline = (): Promise<{sent: boolean, event: any}> => {
+ return this.calendarOffline.saveEvent(eventId, formData, timeCreated, siteId).then((event) => {
+ return {sent: false, event: event};
+ });
+ };
+
+ if (forceOffline || !this.appProvider.isOnline()) {
+ // App is offline, store the event.
+ return storeOffline();
+ }
+
+ // If the event is already stored, discard it first.
+ return this.calendarOffline.deleteEvent(eventId, siteId).then(() => {
+ return this.submitEventOnline(eventId, formData, siteId).then((event) => {
+ return {sent: true, event: event};
+ }).catch((error) => {
+ if (error && !this.utils.isWebServiceError(error)) {
+ // Couldn't connect to server, store in offline.
+ return storeOffline();
+ } else {
+ // The WebService has thrown an error, reject.
+ return Promise.reject(error);
+ }
+ });
+ });
+ }
+
+ /**
+ * Submit an event, either to create it or to edit it. It will fail if offline or cannot connect.
+ *
+ * @param {number} eventId ID of the event. If undefined/null, create a new event.
+ * @param {any} formData Form data.
+ * @param {string} [siteId] Site ID. If not provided, current site.
+ * @return {Promise} Promise resolved when done.
+ */
+ submitEventOnline(eventId: number, formData: any, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ // Add data that is "hidden" in web.
+ formData.id = eventId || 0;
+ formData.userid = site.getUserId();
+ formData.visible = 1;
+ formData.instance = 0;
+
+ if (eventId > 0) {
+ formData['_qf__core_calendar_local_event_forms_update'] = 1;
+ } else {
+ formData['_qf__core_calendar_local_event_forms_create'] = 1;
+ }
+
+ const params = {
+ formdata: this.utils.objectToGetParams(formData)
+ };
+
+ return site.write('core_calendar_submit_create_update_form', params).then((result) => {
+ if (result.validationerror) {
+ // Simulate a WS error.
+ return Promise.reject({
+ message: this.translate.instant('core.invalidformdata'),
+ errorcode: 'validationerror'
+ });
+ }
+
+ return result.event;
+ });
+ });
+ }
}
diff --git a/src/addon/calendar/providers/helper.ts b/src/addon/calendar/providers/helper.ts
index a1a85759dcd..7fd2226599b 100644
--- a/src/addon/calendar/providers/helper.ts
+++ b/src/addon/calendar/providers/helper.ts
@@ -14,7 +14,13 @@
import { Injectable } from '@angular/core';
import { CoreLoggerProvider } from '@providers/logger';
+import { CoreSitesProvider } from '@providers/sites';
import { CoreCourseProvider } from '@core/course/providers/course';
+import { AddonCalendarProvider } from './calendar';
+import { CoreConstants } from '@core/constants';
+import { CoreConfigProvider } from '@providers/config';
+import { CoreUtilsProvider } from '@providers/utils/utils';
+import * as moment from 'moment';
/**
* Service that provides some features regarding lists of courses and categories.
@@ -31,10 +37,94 @@ export class AddonCalendarHelperProvider {
category: 'fa-cubes'
};
- constructor(logger: CoreLoggerProvider, private courseProvider: CoreCourseProvider) {
+ constructor(logger: CoreLoggerProvider,
+ private courseProvider: CoreCourseProvider,
+ private sitesProvider: CoreSitesProvider,
+ private calendarProvider: AddonCalendarProvider,
+ private configProvider: CoreConfigProvider,
+ private utils: CoreUtilsProvider) {
this.logger = logger.getInstance('AddonCalendarHelperProvider');
}
+ /**
+ * Calculate some day data based on a list of events for that day.
+ *
+ * @param {any} day Day.
+ * @param {any[]} events Events.
+ */
+ calculateDayData(day: any, events: any[]): void {
+ day.hasevents = events.length > 0;
+ day.haslastdayofevent = false;
+
+ const types = {};
+ events.forEach((event) => {
+ types[event.formattedType || event.eventtype] = true;
+
+ if (event.islastday) {
+ day.haslastdayofevent = true;
+ }
+ });
+
+ day.calendareventtypes = Object.keys(types);
+ }
+
+ /**
+ * Check if current user can create/edit events.
+ *
+ * @param {number} [courseId] Course ID. If not defined, site calendar.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with boolean: whether the user can create events.
+ */
+ canEditEvents(courseId?: number, siteId?: string): Promise {
+ return this.calendarProvider.canEditEvents(siteId).then((canEdit) => {
+ if (!canEdit) {
+ return false;
+ }
+
+ // Site allows creating events. Check if the user has permissions to do so.
+ return this.calendarProvider.getAllowedEventTypes(courseId, siteId).then((types) => {
+ return Object.keys(types).length > 0;
+ });
+ }).catch(() => {
+ return false;
+ });
+ }
+
+ /**
+ * Classify events into their respective months and days. If an event duration covers more than one day,
+ * it will be included in all the days it lasts.
+ *
+ * @param {any[]} events Events to classify.
+ * @return {{[monthId: string]: {[day: number]: any[]}}} Object with the classified events.
+ */
+ classifyIntoMonths(events: any[]): {[monthId: string]: {[day: number]: any[]}} {
+
+ const result = {};
+
+ events.forEach((event) => {
+ const treatedDay = moment(new Date(event.timestart * 1000)),
+ endDay = moment(new Date((event.timestart + (event.timeduration || 0)) * 1000));
+
+ // Add the event to all the days it lasts.
+ while (!treatedDay.isAfter(endDay, 'day')) {
+ const monthId = this.getMonthId(treatedDay.year(), treatedDay.month() + 1),
+ day = treatedDay.date();
+
+ if (!result[monthId]) {
+ result[monthId] = {};
+ }
+ if (!result[monthId][day]) {
+ result[monthId][day] = [];
+ }
+ result[monthId][day].push(event);
+
+ treatedDay.add(1, 'day'); // Treat next day.
+ }
+ });
+
+ return result;
+ }
+
/**
* Convenience function to format some event data to be rendered.
*
@@ -46,5 +136,334 @@ export class AddonCalendarHelperProvider {
e.icon = this.courseProvider.getModuleIconSrc(e.modulename);
e.moduleIcon = e.icon;
}
+
+ e.formattedType = this.calendarProvider.getEventType(e);
+
+ if (typeof e.duration != 'undefined') {
+ // It's an offline event, add some calculated data.
+ e.format = 1;
+ e.visible = 1;
+
+ if (e.duration == 1) {
+ e.timeduration = e.timedurationuntil - e.timestart;
+ } else if (e.duration == 2) {
+ e.timeduration = e.timedurationminutes * CoreConstants.SECONDS_MINUTE;
+ } else {
+ e.timeduration = 0;
+ }
+ }
+ }
+
+ /**
+ * Get options (name & value) for each allowed event type.
+ *
+ * @param {any} eventTypes Result of getAllowedEventTypes.
+ * @return {{name: string, value: string}[]} Options.
+ */
+ getEventTypeOptions(eventTypes: any): {name: string, value: string}[] {
+ const options = [];
+
+ if (eventTypes.user) {
+ options.push({name: 'core.user', value: AddonCalendarProvider.TYPE_USER});
+ }
+ if (eventTypes.group) {
+ options.push({name: 'core.group', value: AddonCalendarProvider.TYPE_GROUP});
+ }
+ if (eventTypes.course) {
+ options.push({name: 'core.course', value: AddonCalendarProvider.TYPE_COURSE});
+ }
+ if (eventTypes.category) {
+ options.push({name: 'core.category', value: AddonCalendarProvider.TYPE_CATEGORY});
+ }
+ if (eventTypes.site) {
+ options.push({name: 'core.site', value: AddonCalendarProvider.TYPE_SITE});
+ }
+
+ return options;
+ }
+
+ /**
+ * Get the month "id" (year + month).
+ *
+ * @param {number} year Year.
+ * @param {number} month Month.
+ * @return {string} The "id".
+ */
+ getMonthId(year: number, month: number): string {
+ return year + '#' + month;
+ }
+
+ /**
+ * Get weeks of a month in offline (with no events).
+ *
+ * The result has the same structure than getMonthlyEvents, but it only contains fields that are actually used by the app.
+ *
+ * @param {number} year Year to get.
+ * @param {number} month Month to get.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the response.
+ */
+ getOfflineMonthWeeks(year: number, month: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ // Get starting week day user preference, fallback to site configuration.
+ const startWeekDay = site.getStoredConfig('calendar_startwday');
+
+ return this.configProvider.get(AddonCalendarProvider.STARTING_WEEK_DAY, startWeekDay);
+ }).then((startWeekDay) => {
+ const today = moment();
+ const isCurrentMonth = today.year() == year && today.month() == month - 1;
+ const weeks = [];
+
+ let date = moment({year, month: month - 1, date: 1});
+ for (let mday = 1; mday <= date.daysInMonth(); mday++) {
+ date = moment({year, month: month - 1, date: mday});
+
+ // Add new week and calculate prepadding.
+ if (!weeks.length || date.day() == startWeekDay) {
+ const prepaddingLength = (date.day() - startWeekDay + 7) % 7;
+ const prepadding = [];
+ for (let i = 0; i < prepaddingLength; i++) {
+ prepadding.push(i);
+ }
+ weeks.push({ prepadding, postpadding: [], days: []});
+ }
+
+ // Calculate postpadding of last week.
+ if (mday == date.daysInMonth()) {
+ const postpaddingLength = (startWeekDay - date.day() + 6) % 7;
+ const postpadding = [];
+ for (let i = 0; i < postpaddingLength; i++) {
+ postpadding.push(i);
+ }
+ weeks[weeks.length - 1].postpadding = postpadding;
+ }
+
+ // Add day to current week.
+ weeks[weeks.length - 1].days.push({
+ events: [],
+ hasevents: false,
+ mday: date.date(),
+ isweekend: date.day() == 0 || date.day() == 6,
+ istoday: isCurrentMonth && today.date() == date.date(),
+ calendareventtypes: [],
+ });
+ }
+
+ return {weeks, daynames: [{dayno: startWeekDay}]};
+ });
+ }
+
+ /**
+ * Check if the data of an event has changed.
+ *
+ * @param {any} data Current data.
+ * @param {any} [original] Original data.
+ * @return {boolean} True if data has changed, false otherwise.
+ */
+ hasEventDataChanged(data: any, original?: any): boolean {
+ if (!original) {
+ // There is no original data, assume it hasn't changed.
+ return false;
+ }
+
+ // Check the fields that don't depend on any other.
+ if (data.name != original.name || data.timestart != original.timestart || data.eventtype != original.eventtype ||
+ data.description != original.description || data.location != original.location ||
+ data.duration != original.duration || data.repeat != original.repeat) {
+ return true;
+ }
+
+ // Check data that depends on eventtype.
+ if ((data.eventtype == AddonCalendarProvider.TYPE_CATEGORY && data.categoryid != original.categoryid) ||
+ (data.eventtype == AddonCalendarProvider.TYPE_COURSE && data.courseid != original.courseid) ||
+ (data.eventtype == AddonCalendarProvider.TYPE_GROUP && data.groupcourseid != original.groupcourseid &&
+ data.groupid != original.groupid)) {
+ return true;
+ }
+
+ // Check data that depends on duration.
+ if ((data.duration == 1 && data.timedurationuntil != original.timedurationuntil) ||
+ (data.duration == 2 && data.timedurationminutes != original.timedurationminutes)) {
+ return true;
+ }
+
+ if (data.repeat && data.repeats != original.repeats) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if an event should be displayed based on the filter.
+ *
+ * @param {any} event Event object.
+ * @param {number} courseId Course ID to filter.
+ * @param {number} categoryId Category ID the course belongs to.
+ * @param {any} categories Categories indexed by ID.
+ * @return {boolean} Whether it should be displayed.
+ */
+ shouldDisplayEvent(event: any, courseId: number, categoryId: number, categories: any): boolean {
+ if (event.eventtype == 'user' || event.eventtype == 'site') {
+ // User or site event, display it.
+ return true;
+ }
+
+ if (event.eventtype == 'category') {
+ if (!event.categoryid || !Object.keys(categories).length) {
+ // We can't tell if the course belongs to the category, display them all.
+ return true;
+ }
+
+ if (event.categoryid == categoryId) {
+ // The event is in the same category as the course, display it.
+ return true;
+ }
+
+ // Check parent categories.
+ let category = categories[categoryId];
+ while (category) {
+ if (!category.parent) {
+ // Category doesn't have parent, stop.
+ break;
+ }
+
+ if (event.categoryid == category.parent) {
+ return true;
+ }
+ category = categories[category.parent];
+ }
+
+ return false;
+ }
+
+ // Show the event if it is from site home or if it matches the selected course.
+ return event.course && (event.course.id == this.sitesProvider.getCurrentSiteHomeId() || event.course.id == courseId);
+ }
+
+ /**
+ * Refresh the month & day for several created/edited/deleted events, and invalidate the months & days
+ * for their repeated events if needed.
+ *
+ * @param {{event: any, repeated: number}[]} events Events that have been touched and number of times each event is repeated.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Resolved when done.
+ */
+ refreshAfterChangeEvents(events: {event: any, repeated: number}[], siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ const fetchTimestarts = [],
+ invalidateTimestarts = [];
+
+ // Always fetch upcoming events.
+ const upcomingPromise = this.calendarProvider.getUpcomingEvents(undefined, undefined, true, site.id).catch(() => {
+ // Ignore errors.
+ });
+
+ // Invalidate the events and get the timestarts so we can invalidate months & days.
+ return this.utils.allPromises([upcomingPromise].concat(events.map((eventData) => {
+
+ if (eventData.repeated > 1) {
+ if (eventData.event.repeatid) {
+ // Being edited or deleted.
+ // We need to calculate the days to invalidate because the event date could have changed.
+ // We don't know if the repeated events are before or after this one, invalidate them all.
+ fetchTimestarts.push(eventData.event.timestart);
+
+ for (let i = 1; i < eventData.repeated; i++) {
+ invalidateTimestarts.push(eventData.event.timestart + CoreConstants.SECONDS_DAY * 7 * i);
+ invalidateTimestarts.push(eventData.event.timestart - CoreConstants.SECONDS_DAY * 7 * i);
+ }
+
+ // Get the repeated events to invalidate them.
+ return this.calendarProvider.getLocalEventsByRepeatIdFromLocalDb(eventData.event.repeatid, site.id)
+ .then((events) => {
+
+ return this.utils.allPromises(events.map((event) => {
+ return this.calendarProvider.invalidateEvent(event.id);
+ }));
+ });
+ } else {
+ // Being added.
+ let time = eventData.event.timestart;
+ fetchTimestarts.push(time);
+
+ while (eventData.repeated > 1) {
+ time += CoreConstants.SECONDS_DAY * 7;
+ eventData.repeated--;
+ invalidateTimestarts.push(time);
+ }
+
+ return Promise.resolve();
+ }
+ } else {
+ // Not repeated.
+ fetchTimestarts.push(eventData.event.timestart);
+
+ return this.calendarProvider.invalidateEvent(eventData.event.id);
+ }
+
+ }))).finally(() => {
+ const treatedMonths = {},
+ treatedDays = {};
+
+ return this.utils.allPromises([
+ this.calendarProvider.invalidateAllUpcomingEvents(),
+
+ // Fetch or invalidate months and days.
+ this.utils.allPromises(fetchTimestarts.concat(invalidateTimestarts).map((time, index) => {
+ const promises = [],
+ day = moment(new Date(time * 1000)),
+ monthId = this.getMonthId(day.year(), day.month() + 1),
+ dayId = monthId + '#' + day.date();
+
+ if (!treatedMonths[monthId]) {
+ // Month not treated already, do it now.
+ treatedMonths[monthId] = monthId;
+
+ if (index < fetchTimestarts.length) {
+ promises.push(this.calendarProvider.getMonthlyEvents(day.year(), day.month() + 1, undefined,
+ undefined, true, site.id).catch(() => {
+
+ // Ignore errors.
+ }));
+ } else {
+ promises.push(this.calendarProvider.invalidateMonthlyEvents(day.year(), day.month() + 1, site.id));
+ }
+ }
+
+ if (!treatedDays[dayId]) {
+ // Day not invalidated already, do it now.
+ treatedDays[dayId] = dayId;
+
+ if (index < fetchTimestarts.length) {
+ promises.push(this.calendarProvider.getDayEvents(day.year(), day.month() + 1, day.date(),
+ undefined, undefined, true, site.id).catch(() => {
+
+ // Ignore errors.
+ }));
+ } else {
+ promises.push(this.calendarProvider.invalidateDayEvents(day.year(), day.month() + 1, day.date(),
+ site.id));
+ }
+ }
+
+ return this.utils.allPromises(promises);
+ }))
+ ]);
+ });
+ });
+ }
+
+ /**
+ * Refresh the month & day for a created/edited/deleted event, and invalidate the months & days
+ * for their repeated events if needed.
+ *
+ * @param {any} event Event that has been touched.
+ * @param {number} repeated Number of times the event is repeated.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Resolved when done.
+ */
+ refreshAfterChangeEvent(event: any, repeated: number, siteId?: string): Promise {
+ return this.refreshAfterChangeEvents([{event: event, repeated: repeated}], siteId);
}
}
diff --git a/src/addon/calendar/providers/mainmenu-handler.ts b/src/addon/calendar/providers/mainmenu-handler.ts
index 2769f0e0bfb..0568eeb01ea 100644
--- a/src/addon/calendar/providers/mainmenu-handler.ts
+++ b/src/addon/calendar/providers/mainmenu-handler.ts
@@ -44,7 +44,7 @@ export class AddonCalendarMainMenuHandler implements CoreMainMenuHandler {
return {
icon: 'calendar',
title: 'addon.calendar.calendar',
- page: 'AddonCalendarListPage',
+ page: this.calendarProvider.canViewMonthInSite() ? 'AddonCalendarIndexPage' : 'AddonCalendarListPage',
class: 'addon-calendar-handler'
};
}
diff --git a/src/addon/calendar/providers/sync-cron-handler.ts b/src/addon/calendar/providers/sync-cron-handler.ts
new file mode 100644
index 00000000000..6aabcca742f
--- /dev/null
+++ b/src/addon/calendar/providers/sync-cron-handler.ts
@@ -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 { Injectable } from '@angular/core';
+import { CoreCronHandler } from '@providers/cron';
+import { AddonCalendarSyncProvider } from './calendar-sync';
+
+/**
+ * Synchronization cron handler.
+ */
+@Injectable()
+export class AddonCalendarSyncCronHandler implements CoreCronHandler {
+ name = 'AddonCalendarSyncCronHandler';
+
+ constructor(private calendarSync: AddonCalendarSyncProvider) {}
+
+ /**
+ * Execute the process.
+ * Receives the ID of the site affected, undefined for all sites.
+ *
+ * @param {string} [siteId] ID of the site affected, undefined for all sites.
+ * @param {boolean} [force] Wether the execution is forced (manual sync).
+ * @return {Promise} Promise resolved when done, rejected if failure.
+ */
+ execute(siteId?: string, force?: boolean): Promise {
+ return this.calendarSync.syncAllEvents(siteId, force);
+ }
+
+ /**
+ * Get the time between consecutive executions.
+ *
+ * @return {number} Time between consecutive executions (in ms).
+ */
+ getInterval(): number {
+ return this.calendarSync.syncInterval;
+ }
+}
diff --git a/src/addon/calendar/providers/view-link-handler.ts b/src/addon/calendar/providers/view-link-handler.ts
new file mode 100644
index 00000000000..e0ba641c072
--- /dev/null
+++ b/src/addon/calendar/providers/view-link-handler.ts
@@ -0,0 +1,113 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
+import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
+import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
+import { AddonCalendarProvider } from './calendar';
+
+/**
+ * Content links handler for calendar view page.
+ */
+@Injectable()
+export class AddonCalendarViewLinkHandler extends CoreContentLinksHandlerBase {
+ name = 'AddonCalendarViewLinkHandler';
+ pattern = /\/calendar\/view\.php/;
+
+ protected SUPPORTED_VIEWS = ['month', 'mini', 'minithree', 'day', 'upcoming', 'upcoming_mini'];
+
+ constructor(private linkHelper: CoreContentLinksHelperProvider, private calendarProvider: AddonCalendarProvider) {
+ super();
+ }
+
+ /**
+ * Get the list of actions for a link (url).
+ *
+ * @param {string[]} siteIds List of sites the URL belongs to.
+ * @param {string} url The URL to treat.
+ * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
+ * @param {number} [courseId] Course ID related to the URL. Optional but recommended.
+ * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions.
+ */
+ getActions(siteIds: string[], url: string, params: any, courseId?: number):
+ CoreContentLinksAction[] | Promise {
+ return [{
+ action: (siteId, navCtrl?): void => {
+ if (!params.view || params.view == 'month' || params.view == 'mini' || params.view == 'minithree') {
+ // Monthly view, open the calendar tab.
+ const stateParams: any = {
+ courseId: params.course
+ },
+ timestamp = params.time ? params.time * 1000 : Date.now();
+
+ const date = new Date(timestamp);
+ stateParams.year = date.getFullYear();
+ stateParams.month = date.getMonth() + 1;
+
+ this.linkHelper.goInSite(navCtrl, 'AddonCalendarIndexPage', stateParams, siteId); // @todo: Add checkMenu param.
+
+ } else if (params.view == 'day') {
+ // Daily view, open the page.
+ const stateParams: any = {
+ courseId: params.course
+ },
+ timestamp = params.time ? params.time * 1000 : Date.now();
+
+ const date = new Date(timestamp);
+ stateParams.year = date.getFullYear();
+ stateParams.month = date.getMonth() + 1;
+ stateParams.day = date.getDate();
+
+ this.linkHelper.goInSite(navCtrl, 'AddonCalendarDayPage', stateParams, siteId);
+
+ } else if (params.view == 'upcoming' || params.view == 'upcoming_mini') {
+ // Upcoming view, open the calendar tab.
+ const stateParams: any = {
+ courseId: params.course,
+ upcoming: true,
+ };
+
+ this.linkHelper.goInSite(navCtrl, 'AddonCalendarIndexPage', stateParams, siteId); // @todo: Add checkMenu param.
+
+ }
+ }
+ }];
+ }
+
+ /**
+ * Check if the handler is enabled for a certain site (site + user) and a URL.
+ * If not defined, defaults to true.
+ *
+ * @param {string} siteId The site ID.
+ * @param {string} url The URL to treat.
+ * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
+ * @param {number} [courseId] Course ID related to the URL. Optional but recommended.
+ * @return {boolean|Promise} Whether the handler is enabled for the URL and site.
+ */
+ isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise {
+ if (params.view && this.SUPPORTED_VIEWS.indexOf(params.view) == -1) {
+ // This type of view isn't supported in the app.
+ return false;
+ }
+
+ return this.calendarProvider.isDisabled(siteId).then((disabled) => {
+ if (disabled) {
+ return false;
+ }
+
+ return this.calendarProvider.canViewMonth(siteId);
+ });
+ }
+}
diff --git a/src/addon/competency/pages/competency/competency.html b/src/addon/competency/pages/competency/competency.html
index dc048e44659..4f66d9dc1c4 100644
--- a/src/addon/competency/pages/competency/competency.html
+++ b/src/addon/competency/pages/competency/competency.html
@@ -76,7 +76,7 @@ {{ 'addon.competency.evidence' | translate }}
{{ 'addon.competency.noevidence' | translate }}
-
+
{{ evidence.actionuser.fullname }}
{{ evidence.timemodified * 1000 | coreFormatDate }}
diff --git a/src/addon/competency/pages/competency/competency.ts b/src/addon/competency/pages/competency/competency.ts
index dc7e76c9d16..c3930b9452b 100644
--- a/src/addon/competency/pages/competency/competency.ts
+++ b/src/addon/competency/pages/competency/competency.ts
@@ -151,15 +151,4 @@ export class AddonCompetencyCompetencyPage {
const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
navCtrl.push('AddonCompetencyCompetencySummaryPage', {competencyId});
}
-
- /**
- * Opens the profile of a user.
- *
- * @param {number} userId
- */
- openUserProfile(userId: number): void {
- // Decide which navCtrl to use. If this page is inside a split view, use the split view's master nav.
- const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
- navCtrl.push('CoreUserProfilePage', {userId, courseId: this.courseId});
- }
}
diff --git a/src/addon/competency/providers/course-option-handler.ts b/src/addon/competency/providers/course-option-handler.ts
index f05219300ee..2f817699504 100644
--- a/src/addon/competency/providers/course-option-handler.ts
+++ b/src/addon/competency/providers/course-option-handler.ts
@@ -63,10 +63,10 @@ export class AddonCompetencyCourseOptionHandler implements CoreCourseOptionsHand
* Returns the data needed to render the handler.
*
* @param {Injector} injector Injector.
- * @param {number} courseId The course ID.
+ * @param {number} course The course.
* @return {CoreCourseOptionsHandlerData|Promise} Data or promise resolved with the data.
*/
- getDisplayData?(injector: Injector, courseId: number): CoreCourseOptionsHandlerData | Promise {
+ getDisplayData?(injector: Injector, course: any): CoreCourseOptionsHandlerData | Promise {
return {
title: 'addon.competency.competencies',
class: 'addon-competency-course-handler',
diff --git a/src/addon/competency/providers/plans-link-handler.ts b/src/addon/competency/providers/plans-link-handler.ts
index a99280f3adc..da08f33dacd 100644
--- a/src/addon/competency/providers/plans-link-handler.ts
+++ b/src/addon/competency/providers/plans-link-handler.ts
@@ -15,7 +15,7 @@
import { Injectable } from '@angular/core';
import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
-import { CoreLoginHelperProvider } from '@core/login/providers/helper';
+import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { AddonCompetencyProvider } from './competency';
/**
@@ -26,7 +26,7 @@ export class AddonCompetencyPlansLinkHandler extends CoreContentLinksHandlerBase
name = 'AddonCompetencyPlansLinkHandler';
pattern = /\/admin\/tool\/lp\/plans\.php/;
- constructor(private loginHelper: CoreLoginHelperProvider, private competencyProvider: AddonCompetencyProvider) {
+ constructor(private linkHelper: CoreContentLinksHelperProvider, private competencyProvider: AddonCompetencyProvider) {
super();
}
@@ -44,8 +44,8 @@ export class AddonCompetencyPlansLinkHandler extends CoreContentLinksHandlerBase
return [{
action: (siteId, navCtrl?): void => {
- // Always use redirect to make it the new history root (to avoid "loops" in history).
- this.loginHelper.redirect('AddonCompetencyPlanListPage', { userId: params.userid }, siteId);
+ this.linkHelper.goInSite(navCtrl, 'AddonCompetencyPlanListPage', { userId: params.userid }, siteId,
+ typeof params.userid == 'undefined');
}
}];
}
diff --git a/src/addon/competency/providers/user-handler.ts b/src/addon/competency/providers/user-handler.ts
index 3b04349f6cb..7cdbaadac11 100644
--- a/src/addon/competency/providers/user-handler.ts
+++ b/src/addon/competency/providers/user-handler.ts
@@ -111,7 +111,7 @@ export class AddonCompetencyUserHandler implements CoreUserProfileHandler {
action: (event, navCtrl, user, courseId): void => {
event.preventDefault();
event.stopPropagation();
- // Always use redirect to make it the new history root (to avoid "loops" in history).
+
this.linkHelper.goInSite(navCtrl, 'AddonCompetencyCourseCompetenciesPage', {courseId, userId: user.id});
}
};
@@ -123,7 +123,7 @@ export class AddonCompetencyUserHandler implements CoreUserProfileHandler {
action: (event, navCtrl, user, courseId): void => {
event.preventDefault();
event.stopPropagation();
- // Always use redirect to make it the new history root (to avoid "loops" in history).
+
this.linkHelper.goInSite(navCtrl, 'AddonCompetencyPlanListPage', {userId: user.id});
}
};
diff --git a/src/addon/coursecompletion/providers/course-option-handler.ts b/src/addon/coursecompletion/providers/course-option-handler.ts
index d6a08f6e98e..d0483b68a78 100644
--- a/src/addon/coursecompletion/providers/course-option-handler.ts
+++ b/src/addon/coursecompletion/providers/course-option-handler.ts
@@ -97,6 +97,12 @@ export class AddonCourseCompletionCourseOptionHandler implements CoreCourseOptio
return this.courseCompletionProvider.getCompletion(course.id, undefined, {
getFromCache: false,
emergencyCache: false
+ }).catch((error) => {
+ if (error && error.errorcode == 'notenroled') {
+ // Not enrolled error, probably a teacher. Ignore error.
+ } else {
+ return Promise.reject(error);
+ }
});
}
}
diff --git a/src/addon/coursecompletion/providers/coursecompletion.ts b/src/addon/coursecompletion/providers/coursecompletion.ts
index 262bb8e3b9a..0a79a47669f 100644
--- a/src/addon/coursecompletion/providers/coursecompletion.ts
+++ b/src/addon/coursecompletion/providers/coursecompletion.ts
@@ -110,6 +110,7 @@ export class AddonCourseCompletionProvider {
preSets.cacheKey = this.getCompletionCacheKey(courseId, userId);
preSets.updateFrequency = preSets.updateFrequency || CoreSite.FREQUENCY_SOMETIMES;
+ preSets.cacheErrors = ['notenroled'];
return site.read('core_completion_get_course_completion_status', data, preSets).then((data) => {
if (data.completionstatus) {
diff --git a/src/addon/messages/pages/discussion/discussion.scss b/src/addon/messages/pages/discussion/discussion.scss
index ddffe09009d..f17702e2435 100644
--- a/src/addon/messages/pages/discussion/discussion.scss
+++ b/src/addon/messages/pages/discussion/discussion.scss
@@ -5,9 +5,6 @@ $item-message-note-font-size: 75% !default;
$item-message-mine-bg: $gray-light !default;
ion-app.app-root page-addon-messages-discussion {
- .toolbar-title {
- padding: 0;
- }
ion-content {
background-color: $gray-lighter !important;
@@ -49,7 +46,6 @@ ion-app.app-root page-addon-messages-discussion {
@include core-transition(width);
// This is needed to display bubble tails.
overflow: visible;
- contain: none;
core-format-text > p:only-child {
display: inline;
@@ -193,6 +189,8 @@ ion-app.app-root page-addon-messages-discussion {
}
.toolbar-title {
+ padding: 0;
+
img {
@include margin-horizontal(null, 6px);
}
@@ -209,6 +207,10 @@ ion-app.app-root page-addon-messages-discussion {
ion-icon {
@include margin-horizontal(6px, null);
}
+
+ &.toolbar-title-ios {
+ justify-content: center;
+ }
}
}
diff --git a/src/addon/messages/pages/group-conversations/group-conversations.scss b/src/addon/messages/pages/group-conversations/group-conversations.scss
index 80246e1a20c..d4dd9a82162 100644
--- a/src/addon/messages/pages/group-conversations/group-conversations.scss
+++ b/src/addon/messages/pages/group-conversations/group-conversations.scss
@@ -40,6 +40,6 @@ ion-app.app-root .addon-message-discussion {
ion-app.app-root .addon-message-discussion {
h2 {
- margin-top: 6px;
+ margin-top: 10px;
}
}
\ No newline at end of file
diff --git a/src/addon/messages/providers/contact-request-link-handler.ts b/src/addon/messages/providers/contact-request-link-handler.ts
index 1c262f601e4..aebf2e8cd32 100644
--- a/src/addon/messages/providers/contact-request-link-handler.ts
+++ b/src/addon/messages/providers/contact-request-link-handler.ts
@@ -43,7 +43,6 @@ export class AddonMessagesContactRequestLinkHandler extends CoreContentLinksHand
CoreContentLinksAction[] | Promise {
return [{
action: (siteId, navCtrl?): void => {
- // Always use redirect to make it the new history root (to avoid "loops" in history).
this.linkHelper.goInSite(navCtrl, 'AddonMessagesContactsPage', {}, siteId);
}
}];
diff --git a/src/addon/messages/providers/discussion-link-handler.ts b/src/addon/messages/providers/discussion-link-handler.ts
index 869e9461b2c..b916798c621 100644
--- a/src/addon/messages/providers/discussion-link-handler.ts
+++ b/src/addon/messages/providers/discussion-link-handler.ts
@@ -49,7 +49,6 @@ export class AddonMessagesDiscussionLinkHandler extends CoreContentLinksHandlerB
const stateParams = {
userId: parseInt(params.id || params.user2, 10)
};
- // Always use redirect to make it the new history root (to avoid "loops" in history).
this.linkHelper.goInSite(navCtrl, 'AddonMessagesDiscussionPage', stateParams, siteId);
}
}];
diff --git a/src/addon/messages/providers/index-link-handler.ts b/src/addon/messages/providers/index-link-handler.ts
index 974c6ddeff2..400bd4eb1af 100644
--- a/src/addon/messages/providers/index-link-handler.ts
+++ b/src/addon/messages/providers/index-link-handler.ts
@@ -49,7 +49,6 @@ export class AddonMessagesIndexLinkHandler extends CoreContentLinksHandlerBase {
pageName = 'AddonMessagesGroupConversationsPage';
}
- // Always use redirect to make it the new history root (to avoid "loops" in history).
this.linkHelper.goInSite(navCtrl, pageName, undefined, siteId);
}
}];
diff --git a/src/addon/messages/providers/user-send-message-handler.ts b/src/addon/messages/providers/user-send-message-handler.ts
index a80b90adeef..8fd8982d9d1 100644
--- a/src/addon/messages/providers/user-send-message-handler.ts
+++ b/src/addon/messages/providers/user-send-message-handler.ts
@@ -76,7 +76,6 @@ export class AddonMessagesSendMessageUserHandler implements CoreUserProfileHandl
showKeyboard: true,
userId: user.id
};
- // Always use redirect to make it the new history root (to avoid "loops" in history).
this.linkHelper.goInSite(navCtrl, 'AddonMessagesDiscussionPage', pageParams);
}
};
diff --git a/src/addon/mod/assign/components/index/index.ts b/src/addon/mod/assign/components/index/index.ts
index 013c6b3783c..26d57888e58 100644
--- a/src/addon/mod/assign/components/index/index.ts
+++ b/src/addon/mod/assign/components/index/index.ts
@@ -204,8 +204,7 @@ export class AddonModAssignIndexComponent extends CoreCourseModuleMainActivityCo
this.showNumbers = groupInfo.groups.length == 0 ||
this.sitesProvider.getCurrentSite().isVersionGreaterEqualThan('3.5');
- return this.setGroup(this.group || (groupInfo.groups && groupInfo.groups[0] && groupInfo.groups[0].id) ||
- 0);
+ return this.setGroup(this.groupsProvider.validateGroupId(this.group, groupInfo));
});
}
diff --git a/src/addon/mod/assign/components/submission/addon-mod-assign-submission.html b/src/addon/mod/assign/components/submission/addon-mod-assign-submission.html
index 0ff33520222..06bebe6cb40 100644
--- a/src/addon/mod/assign/components/submission/addon-mod-assign-submission.html
+++ b/src/addon/mod/assign/components/submission/addon-mod-assign-submission.html
@@ -1,7 +1,7 @@
-
+
-
+
{{ user.fullname }}
@@ -110,7 +110,7 @@ {{ 'addon.mod_assign.attemptnumber' | translate }}
0">
{{ 'addon.mod_assign.userswhoneedtosubmit' | translate: {$a: ''} }}
-
+
{{ user.fullname }}
@@ -208,7 +208,7 @@
{{ 'addon.mod_assign.attemptsettings' | translate }}
-
+
{{ 'addon.mod_assign.gradedby' | translate }}
{{ grader.fullname }}
diff --git a/src/addon/mod/assign/components/submission/submission.ts b/src/addon/mod/assign/components/submission/submission.ts
index c3f655855e4..49489a9a547 100644
--- a/src/addon/mod/assign/components/submission/submission.ts
+++ b/src/addon/mod/assign/components/submission/submission.ts
@@ -620,17 +620,6 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
});
}
- /**
- * Open a user profile.
- *
- * @param {number} userId User to open.
- */
- openUserProfile(userId: number): void {
- // Open a user profile. If this component is inside a split view, use the master nav to open it.
- const navCtrl = this.splitviewCtrl ? this.splitviewCtrl.getMasterNav() : this.navCtrl;
- navCtrl.push('CoreUserProfilePage', { userId: userId, courseId: this.courseId });
- }
-
/**
* Set the submission status name and class.
*
@@ -909,8 +898,8 @@ export class AddonModAssignSubmissionComponent implements OnInit, OnDestroy {
if (this.assign.teamsubmission) {
if (response.lastattempt.submissiongroup) {
// Get the name of the group.
- promises.push(this.groupsProvider.getActivityAllowedGroups(this.assign.cmid).then((groups) => {
- groups.forEach((group) => {
+ promises.push(this.groupsProvider.getActivityAllowedGroups(this.assign.cmid).then((result) => {
+ result.groups.forEach((group) => {
if (group.id == response.lastattempt.submissiongroup) {
this.lastAttempt.submissiongroupname = group.name;
}
diff --git a/src/addon/mod/assign/pages/submission-list/submission-list.ts b/src/addon/mod/assign/pages/submission-list/submission-list.ts
index abf2cd77834..a8bcf8c6dec 100644
--- a/src/addon/mod/assign/pages/submission-list/submission-list.ts
+++ b/src/addon/mod/assign/pages/submission-list/submission-list.ts
@@ -128,7 +128,7 @@ export class AddonModAssignSubmissionListPage implements OnInit, OnDestroy {
return this.groupsProvider.getActivityGroupInfo(this.assign.cmid, false).then((groupInfo) => {
this.groupInfo = groupInfo;
- return this.setGroup(this.groupId || (groupInfo.groups && groupInfo.groups[0] && groupInfo.groups[0].id) || 0);
+ return this.setGroup(this.groupsProvider.validateGroupId(this.groupId, groupInfo));
});
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'Error getting assigment data.');
diff --git a/src/addon/mod/assign/providers/helper.ts b/src/addon/mod/assign/providers/helper.ts
index f0c93976667..5f8f5df2195 100644
--- a/src/addon/mod/assign/providers/helper.ts
+++ b/src/addon/mod/assign/providers/helper.ts
@@ -165,11 +165,11 @@ export class AddonModAssignHelperProvider {
}
// If no participants returned and all groups specified, get participants by groups.
- return this.groupsProvider.getActivityAllowedGroupsIfEnabled(assign.cmid, undefined, siteId).then((userGroups) => {
+ return this.groupsProvider.getActivityGroupInfo(assign.cmid, false, undefined, siteId).then((info) => {
const promises = [],
participants = {};
- userGroups.forEach((userGroup) => {
+ info.groups.forEach((userGroup) => {
promises.push(this.assignProvider.listParticipants(assign.id, userGroup.id, ignoreCache, siteId)
.then((parts) => {
// Do not get repeated users.
diff --git a/src/addon/mod/assign/providers/prefetch-handler.ts b/src/addon/mod/assign/providers/prefetch-handler.ts
index a5f1e6fc913..9c76f2ec8ea 100644
--- a/src/addon/mod/assign/providers/prefetch-handler.ts
+++ b/src/addon/mod/assign/providers/prefetch-handler.ts
@@ -293,9 +293,9 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
return this.assignProvider.getSubmissions(assign.id, true, siteId).then((data) => {
const promises = [];
- if (data.canviewsubmissions) {
- // Teacher, prefetch all submissions.
- promises.push(this.groupsProvider.getActivityGroupInfo(assign.cmid, false, undefined, siteId).then((groupInfo) => {
+ promises.push(this.groupsProvider.getActivityGroupInfo(assign.cmid, false, undefined, siteId).then((groupInfo) => {
+ if (data.canviewsubmissions) {
+ // Teacher, prefetch all submissions.
const groupProms = [];
if (!groupInfo.groups || groupInfo.groups.length == 0) {
groupInfo.groups = [{id: 0}];
@@ -354,8 +354,8 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
});
return Promise.all(groupProms);
- }));
- }
+ }
+ }));
// Prefetch own submission, we need to do this for teachers too so the response with error is cached.
promises.push(
@@ -370,9 +370,6 @@ export class AddonModAssignPrefetchHandler extends CoreCourseActivityPrefetchHan
})
);
- promises.push(this.groupsProvider.activityHasGroups(assign.cmid, siteId, true));
- promises.push(this.groupsProvider.getActivityAllowedGroups(assign.cmid, undefined, siteId, true));
-
return Promise.all(promises);
});
}
diff --git a/src/addon/mod/assign/submission/comments/component/addon-mod-assign-submission-comments.html b/src/addon/mod/assign/submission/comments/component/addon-mod-assign-submission-comments.html
index be2ba466f32..8040cdfeb15 100644
--- a/src/addon/mod/assign/submission/comments/component/addon-mod-assign-submission-comments.html
+++ b/src/addon/mod/assign/submission/comments/component/addon-mod-assign-submission-comments.html
@@ -1,4 +1,4 @@
-
+
{{plugin.name}}
diff --git a/src/addon/mod/assign/submission/comments/component/comments.ts b/src/addon/mod/assign/submission/comments/component/comments.ts
index 98c8be9e0fd..bd6bb57c2b6 100644
--- a/src/addon/mod/assign/submission/comments/component/comments.ts
+++ b/src/addon/mod/assign/submission/comments/component/comments.ts
@@ -48,7 +48,7 @@ export class AddonModAssignSubmissionCommentsComponent extends AddonModAssignSub
/**
* Show the comments.
*/
- showComments(): void {
- this.commentsComponent && this.commentsComponent.openComments();
+ showComments(e?: Event): void {
+ this.commentsComponent && this.commentsComponent.openComments(e);
}
}
diff --git a/src/addon/mod/book/book.module.ts b/src/addon/mod/book/book.module.ts
index cc06e008df6..ea425d1c8c3 100644
--- a/src/addon/mod/book/book.module.ts
+++ b/src/addon/mod/book/book.module.ts
@@ -22,6 +22,8 @@ import { AddonModBookPrefetchHandler } from './providers/prefetch-handler';
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
+import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate';
+import { AddonModBookTagAreaHandler } from './providers/tag-area-handler';
// List of providers (without handlers).
export const ADDON_MOD_BOOK_PROVIDERS: any[] = [
@@ -39,18 +41,21 @@ export const ADDON_MOD_BOOK_PROVIDERS: any[] = [
AddonModBookModuleHandler,
AddonModBookLinkHandler,
AddonModBookListLinkHandler,
- AddonModBookPrefetchHandler
+ AddonModBookPrefetchHandler,
+ AddonModBookTagAreaHandler
]
})
export class AddonModBookModule {
constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModBookModuleHandler,
contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModBookLinkHandler,
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModBookPrefetchHandler,
- listLinkHandler: AddonModBookListLinkHandler) {
+ listLinkHandler: AddonModBookListLinkHandler, tagAreaDelegate: CoreTagAreaDelegate,
+ tagAreaHandler: AddonModBookTagAreaHandler) {
moduleDelegate.registerHandler(moduleHandler);
contentLinksDelegate.registerHandler(linkHandler);
contentLinksDelegate.registerHandler(listLinkHandler);
prefetchDelegate.registerHandler(prefetchHandler);
+ tagAreaDelegate.registerHandler(tagAreaHandler);
}
}
diff --git a/src/addon/mod/book/components/components.module.ts b/src/addon/mod/book/components/components.module.ts
index 54e83ef50dd..4cc338b6eeb 100644
--- a/src/addon/mod/book/components/components.module.ts
+++ b/src/addon/mod/book/components/components.module.ts
@@ -20,6 +20,7 @@ import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
import { AddonModBookIndexComponent } from './index/index';
+import { CoreTagComponentsModule } from '@core/tag/components/components.module';
@NgModule({
declarations: [
@@ -31,7 +32,8 @@ import { AddonModBookIndexComponent } from './index/index';
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
- CoreCourseComponentsModule
+ CoreCourseComponentsModule,
+ CoreTagComponentsModule
],
providers: [
],
diff --git a/src/addon/mod/book/components/index/addon-mod-book-index.html b/src/addon/mod/book/components/index/addon-mod-book-index.html
index 6f4e0be3ab0..13cc19b7e3e 100644
--- a/src/addon/mod/book/components/index/addon-mod-book-index.html
+++ b/src/addon/mod/book/components/index/addon-mod-book-index.html
@@ -21,6 +21,10 @@
0 && previousChapter" [next]="nextChapter > 0 && nextChapter" (action)="changeChapter($event)">
+
0">
+ {{ 'core.tag.tags' | translate }}:
+
+
0 && previousChapter" [next]="nextChapter > 0 && nextChapter" (action)="changeChapter($event)">
diff --git a/src/addon/mod/book/components/index/index.ts b/src/addon/mod/book/components/index/index.ts
index 1b154d37392..569060aa335 100644
--- a/src/addon/mod/book/components/index/index.ts
+++ b/src/addon/mod/book/components/index/index.ts
@@ -19,6 +19,7 @@ import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component';
import { AddonModBookProvider, AddonModBookContentsMap, AddonModBookTocChapter } from '../../providers/book';
import { AddonModBookPrefetchHandler } from '../../providers/prefetch-handler';
+import { CoreTagProvider } from '@core/tag/providers/tag';
/**
* Component that displays a book.
@@ -34,6 +35,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
chapterContent: string;
previousChapter: string;
nextChapter: string;
+ tagsEnabled: boolean;
protected chapters: AddonModBookTocChapter[];
protected currentChapter: string;
@@ -41,7 +43,7 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
constructor(injector: Injector, private bookProvider: AddonModBookProvider, private courseProvider: CoreCourseProvider,
private appProvider: CoreAppProvider, private prefetchDelegate: AddonModBookPrefetchHandler,
- private modalCtrl: ModalController, @Optional() private content: Content) {
+ private modalCtrl: ModalController, private tagProvider: CoreTagProvider, @Optional() private content: Content) {
super(injector);
}
@@ -51,6 +53,8 @@ export class AddonModBookIndexComponent extends CoreCourseModuleMainResourceComp
ngOnInit(): void {
super.ngOnInit();
+ this.tagsEnabled = this.tagProvider.areTagsAvailableInSite();
+
this.loadContent();
}
diff --git a/src/addon/mod/book/lang/en.json b/src/addon/mod/book/lang/en.json
index 7d1140fe4dd..4f8f32f54ac 100644
--- a/src/addon/mod/book/lang/en.json
+++ b/src/addon/mod/book/lang/en.json
@@ -1,5 +1,6 @@
{
"errorchapter": "Error reading chapter of book.",
"modulenameplural": "Books",
+ "tagarea_book_chapters": "Book chapters",
"toc": "Table of contents"
}
\ No newline at end of file
diff --git a/src/addon/mod/book/providers/book.ts b/src/addon/mod/book/providers/book.ts
index 1655ed164a1..74fd32706ba 100644
--- a/src/addon/mod/book/providers/book.ts
+++ b/src/addon/mod/book/providers/book.ts
@@ -24,6 +24,7 @@ import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
import { CoreSite } from '@classes/site';
+import { CoreTagItem } from '@core/tag/providers/tag';
/**
* A book chapter inside the toc list.
@@ -52,7 +53,13 @@ export interface AddonModBookTocChapter {
* Map of book contents. For each chapter it has its index URL and the list of paths of the files the chapter has. Each path
* is identified by the relative path in the book, and the value is the URL of the file.
*/
-export type AddonModBookContentsMap = {[chapter: string]: {indexUrl?: string, paths: {[path: string]: string}}};
+export type AddonModBookContentsMap = {
+ [chapter: string]: {
+ indexUrl?: string,
+ paths: {[path: string]: string},
+ tags?: CoreTagItem[]
+ }
+};
/**
* Service that provides some features for books.
@@ -203,8 +210,9 @@ export class AddonModBookProvider {
map[chapter] = map[chapter] || { paths: {} };
if (content.filename == 'index.html' && filepathIsChapter) {
- // Index of the chapter, set indexUrl of the chapter.
+ // Index of the chapter, set indexUrl and tags of the chapter.
map[chapter].indexUrl = content.fileurl;
+ map[chapter].tags = content.tags;
} else {
if (filepathIsChapter) {
// It's a file in the root folder OR the WS isn't returning the filepath as it should (MDL-53671).
diff --git a/src/addon/mod/book/providers/link-handler.ts b/src/addon/mod/book/providers/link-handler.ts
index 899978d4b90..631885cbeca 100644
--- a/src/addon/mod/book/providers/link-handler.ts
+++ b/src/addon/mod/book/providers/link-handler.ts
@@ -46,7 +46,7 @@ export class AddonModBookLinkHandler extends CoreContentLinksModuleIndexHandler
return [{
action: (siteId, navCtrl?): void => {
this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId, undefined,
- this.useModNameToGetModule ? this.modName : undefined, modParams);
+ this.useModNameToGetModule ? this.modName : undefined, modParams, navCtrl);
}
}];
}
diff --git a/src/addon/mod/book/providers/tag-area-handler.ts b/src/addon/mod/book/providers/tag-area-handler.ts
new file mode 100644
index 00000000000..47c456cb655
--- /dev/null
+++ b/src/addon/mod/book/providers/tag-area-handler.ts
@@ -0,0 +1,75 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate';
+import { CoreTagHelperProvider } from '@core/tag/providers/helper';
+import { CoreTagFeedComponent } from '@core/tag/components/feed/feed';
+import { CoreCourseProvider } from '@core/course/providers/course';
+import { CoreUrlUtilsProvider } from '@providers/utils/url';
+import { AddonModBookProvider } from './book';
+
+/**
+ * Handler to support tags.
+ */
+@Injectable()
+export class AddonModBookTagAreaHandler implements CoreTagAreaHandler {
+ name = 'AddonModBookTagAreaHandler';
+ type = 'mod_book/book_chapters';
+
+ constructor(private tagHelper: CoreTagHelperProvider, private bookProvider: AddonModBookProvider,
+ private courseProvider: CoreCourseProvider, private urlUtils: CoreUrlUtilsProvider) {}
+
+ /**
+ * Whether or not the handler is enabled on a site level.
+ * @return {boolean|Promise
} Whether or not the handler is enabled on a site level.
+ */
+ isEnabled(): boolean | Promise {
+ return this.bookProvider.isPluginEnabled();
+ }
+
+ /**
+ * Parses the rendered content of a tag index and returns the items.
+ *
+ * @param {string} content Rendered content.
+ * @return {any[]|Promise} Area items (or promise resolved with the items).
+ */
+ parseContent(content: string): any[] | Promise {
+ const items = this.tagHelper.parseFeedContent(content);
+
+ // Find module ids of the returned books, they are needed by the link delegate.
+ return Promise.all(items.map((item) => {
+ const params = this.urlUtils.extractUrlParams(item.url);
+ if (params.b && !params.id) {
+ const bookId = parseInt(params.b, 10);
+
+ return this.courseProvider.getModuleBasicInfoByInstance(bookId, 'book').then((module) => {
+ item.url += '&id=' + module.id;
+ });
+ }
+ })).then(() => {
+ return items;
+ });
+ }
+
+ /**
+ * Get the component to use to display items.
+ *
+ * @param {Injector} injector Injector.
+ * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found.
+ */
+ getComponent(injector: Injector): any | Promise {
+ return CoreTagFeedComponent;
+ }
+}
diff --git a/src/addon/mod/chat/pages/chat/chat.ts b/src/addon/mod/chat/pages/chat/chat.ts
index 99bcbec9416..41bd0e1cafc 100644
--- a/src/addon/mod/chat/pages/chat/chat.ts
+++ b/src/addon/mod/chat/pages/chat/chat.ts
@@ -65,7 +65,7 @@ export class AddonModChatChatPage {
this.logger = logger.getInstance('AddonModChoiceChoicePage');
this.currentUserBeep = 'beep ' + sitesProvider.getCurrentSiteUserId();
this.isOnline = this.appProvider.isOnline();
- this.onlineObserver = network.onchange().subscribe((online) => {
+ this.onlineObserver = network.onchange().subscribe(() => {
// Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => {
this.isOnline = this.appProvider.isOnline();
diff --git a/src/addon/mod/chat/pages/sessions/sessions.ts b/src/addon/mod/chat/pages/sessions/sessions.ts
index 35f26cb3364..373a38dce4c 100644
--- a/src/addon/mod/chat/pages/sessions/sessions.ts
+++ b/src/addon/mod/chat/pages/sessions/sessions.ts
@@ -70,14 +70,7 @@ export class AddonModChatSessionsPage {
return this.groupsProvider.getActivityGroupInfo(this.cmId, false).then((groupInfo) => {
this.groupInfo = groupInfo;
-
- if (groupInfo.groups && groupInfo.groups.length > 0) {
- if (!groupInfo.groups.find((group) => group.id === this.groupId)) {
- this.groupId = groupInfo.groups[0].id;
- }
- } else {
- this.groupId = 0;
- }
+ this.groupId = this.groupsProvider.validateGroupId(this.groupId, groupInfo);
return this.chatProvider.getSessions(this.chatId, this.groupId, this.showAll);
}).then((sessions) => {
diff --git a/src/addon/mod/chat/pages/users/users.ts b/src/addon/mod/chat/pages/users/users.ts
index a9f4f175a05..90e59df6d94 100644
--- a/src/addon/mod/chat/pages/users/users.ts
+++ b/src/addon/mod/chat/pages/users/users.ts
@@ -44,7 +44,7 @@ export class AddonModChatUsersPage {
this.sessionId = navParams.get('sessionId');
this.isOnline = this.appProvider.isOnline();
this.currentUserId = this.sitesProvider.getCurrentSiteUserId();
- this.onlineObserver = network.onchange().subscribe((online) => {
+ this.onlineObserver = network.onchange().subscribe(() => {
// Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => {
this.isOnline = this.appProvider.isOnline();
diff --git a/src/addon/mod/data/components/action/action.ts b/src/addon/mod/data/components/action/action.ts
index 9e96c3d1aaf..b96dc078dc6 100644
--- a/src/addon/mod/data/components/action/action.ts
+++ b/src/addon/mod/data/components/action/action.ts
@@ -20,6 +20,7 @@ import { AddonModDataOfflineProvider } from '../../providers/offline';
import { CoreSitesProvider } from '@providers/sites';
import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
import { CoreUserProvider } from '@core/user/providers/user';
+import { CoreTagProvider } from '@core/tag/providers/tag';
/**
* Component that displays a database action.
@@ -41,13 +42,16 @@ export class AddonModDataActionComponent implements OnInit {
rootUrl: string;
url: string;
userPicture: string;
+ tagsEnabled: boolean;
constructor(protected injector: Injector, protected dataProvider: AddonModDataProvider,
protected dataOffline: AddonModDataOfflineProvider, protected eventsProvider: CoreEventsProvider,
sitesProvider: CoreSitesProvider, protected userProvider: CoreUserProvider, private navCtrl: NavController,
- protected linkHelper: CoreContentLinksHelperProvider, private dataHelper: AddonModDataHelperProvider) {
+ protected linkHelper: CoreContentLinksHelperProvider, private dataHelper: AddonModDataHelperProvider,
+ private tagProvider: CoreTagProvider) {
this.rootUrl = sitesProvider.getCurrentSite().getURL();
this.siteId = sitesProvider.getCurrentSiteId();
+ this.tagsEnabled = this.tagProvider.areTagsAvailableInSite();
}
/**
diff --git a/src/addon/mod/data/components/action/addon-mod-data-action.html b/src/addon/mod/data/components/action/addon-mod-data-action.html
index 41a44e5fafc..b6c9e9924f3 100644
--- a/src/addon/mod/data/components/action/addon-mod-data-action.html
+++ b/src/addon/mod/data/components/action/addon-mod-data-action.html
@@ -32,3 +32,5 @@
{{entry.fullname}}
+
+
diff --git a/src/addon/mod/data/components/components.module.ts b/src/addon/mod/data/components/components.module.ts
index 3470ae8724e..ef12a46b350 100644
--- a/src/addon/mod/data/components/components.module.ts
+++ b/src/addon/mod/data/components/components.module.ts
@@ -25,6 +25,7 @@ import { AddonModDataFieldPluginComponent } from './field-plugin/field-plugin';
import { AddonModDataActionComponent } from './action/action';
import { CoreCompileHtmlComponentModule } from '@core/compile/components/compile-html/compile-html.module';
import { CoreCommentsComponentsModule } from '@core/comments/components/components.module';
+import { CoreTagComponentsModule } from '@core/tag/components/components.module';
@NgModule({
declarations: [
@@ -41,7 +42,8 @@ import { CoreCommentsComponentsModule } from '@core/comments/components/componen
CorePipesModule,
CoreCourseComponentsModule,
CoreCompileHtmlComponentModule,
- CoreCommentsComponentsModule
+ CoreCommentsComponentsModule,
+ CoreTagComponentsModule
],
providers: [
],
diff --git a/src/addon/mod/data/components/index/index.ts b/src/addon/mod/data/components/index/index.ts
index 37a806f4321..bc346384205 100644
--- a/src/addon/mod/data/components/index/index.ts
+++ b/src/addon/mod/data/components/index/index.ts
@@ -85,7 +85,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
private prefetchHandler: AddonModDataPrefetchHandler,
private timeUtils: CoreTimeUtilsProvider,
private groupsProvider: CoreGroupsProvider,
- private commentsProvider: CoreCommentsProvider,
private modalCtrl: ModalController,
private utils: CoreUtilsProvider,
protected navCtrl: NavController) {
@@ -152,8 +151,12 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
promises.push(this.dataProvider.invalidateDatabaseAccessInformationData(this.data.id));
promises.push(this.groupsProvider.invalidateActivityGroupInfo(this.data.coursemodule));
promises.push(this.dataProvider.invalidateEntriesData(this.data.id));
+
if (this.hasComments) {
- promises.push(this.commentsProvider.invalidateCommentsByInstance('module', this.data.coursemodule));
+ this.eventsProvider.trigger(CoreCommentsProvider.REFRESH_COMMENTS_EVENT, {
+ contextLevel: 'module',
+ instanceId: this.data.coursemodule
+ }, this.sitesProvider.getCurrentSiteId());
}
}
@@ -192,6 +195,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
return this.dataProvider.getDatabase(this.courseId, this.module.id).then((data) => {
this.data = data;
+ this.hasComments = data.comments;
this.description = data.intro || data.description;
this.dataRetrieved.emit(data);
@@ -226,16 +230,9 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
canSearch = true;
canAdd = accessData.canaddentry;
- return this.groupsProvider.getActivityGroupInfo(this.data.coursemodule, accessData.canmanageentries)
- .then((groupInfo) => {
+ return this.groupsProvider.getActivityGroupInfo(this.data.coursemodule).then((groupInfo) => {
this.groupInfo = groupInfo;
-
- // Check selected group is accessible.
- if (groupInfo && groupInfo.groups && groupInfo.groups.length > 0) {
- if (!groupInfo.groups.some((group) => this.selectedGroup == group.id)) {
- this.selectedGroup = groupInfo.groups[0].id;
- }
- }
+ this.selectedGroup = this.groupsProvider.validateGroupId(this.selectedGroup, groupInfo);
});
}).then(() => {
return this.dataProvider.getFields(this.data.id).then((fields) => {
@@ -265,7 +262,6 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
* @return {Promise} Resolved then done.
*/
protected fetchEntriesData(): Promise {
- this.hasComments = false;
return this.dataProvider.getDatabaseAccessInformation(this.data.id, this.selectedGroup).then((accessData) => {
// Update values for current group.
@@ -299,14 +295,14 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
if (!this.isEmpty) {
this.entries = entries.offlineEntries.concat(entries.entries);
- let entriesHTML = this.data.listtemplateheader || '';
+ let entriesHTML = this.dataHelper.getTemplate(this.data, 'listtemplateheader', this.fieldsArray);
// Get first entry from the whole list.
if (!this.search.searching || !this.firstEntry) {
this.firstEntry = this.entries[0].id;
}
- const template = this.data.listtemplate || this.dataHelper.getDefaultTemplate('list', this.fieldsArray);
+ const template = this.dataHelper.getTemplate(this.data, 'listtemplate', this.fieldsArray);
const entriesById = {};
this.entries.forEach((entry, index) => {
@@ -318,7 +314,7 @@ export class AddonModDataIndexComponent extends CoreCourseModuleMainActivityComp
entriesHTML += this.dataHelper.displayShowFields(template, this.fieldsArray, entry, offset, 'list', actions);
});
- entriesHTML += this.data.listtemplatefooter || '';
+ entriesHTML += this.dataHelper.getTemplate(this.data, 'listtemplatefooter', this.fieldsArray);
this.entriesRendered = entriesHTML;
diff --git a/src/addon/mod/data/data.module.ts b/src/addon/mod/data/data.module.ts
index 9babfa364bf..158865aecd3 100644
--- a/src/addon/mod/data/data.module.ts
+++ b/src/addon/mod/data/data.module.ts
@@ -33,6 +33,8 @@ import { AddonModDataSyncCronHandler } from './providers/sync-cron-handler';
import { AddonModDataOfflineProvider } from './providers/offline';
import { AddonModDataFieldsDelegate } from './providers/fields-delegate';
import { AddonModDataDefaultFieldHandler } from './providers/default-field-handler';
+import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate';
+import { AddonModDataTagAreaHandler } from './providers/tag-area-handler';
import { AddonModDataFieldModule } from './fields/field.module';
import { CoreUpdateManagerProvider } from '@providers/update-manager';
@@ -67,7 +69,8 @@ export const ADDON_MOD_DATA_PROVIDERS: any[] = [
AddonModDataEditLinkHandler,
AddonModDataListLinkHandler,
AddonModDataSyncCronHandler,
- AddonModDataDefaultFieldHandler
+ AddonModDataDefaultFieldHandler,
+ AddonModDataTagAreaHandler
]
})
export class AddonModDataModule {
@@ -77,7 +80,8 @@ export class AddonModDataModule {
cronDelegate: CoreCronDelegate, syncHandler: AddonModDataSyncCronHandler, updateManager: CoreUpdateManagerProvider,
approveLinkHandler: AddonModDataApproveLinkHandler, deleteLinkHandler: AddonModDataDeleteLinkHandler,
showLinkHandler: AddonModDataShowLinkHandler, editLinkHandler: AddonModDataEditLinkHandler,
- listLinkHandler: AddonModDataListLinkHandler) {
+ listLinkHandler: AddonModDataListLinkHandler, tagAreaDelegate: CoreTagAreaDelegate,
+ tagAreaHandler: AddonModDataTagAreaHandler) {
moduleDelegate.registerHandler(moduleHandler);
prefetchDelegate.registerHandler(prefetchHandler);
@@ -88,6 +92,7 @@ export class AddonModDataModule {
contentLinksDelegate.registerHandler(editLinkHandler);
contentLinksDelegate.registerHandler(listLinkHandler);
cronDelegate.register(syncHandler);
+ tagAreaDelegate.registerHandler(tagAreaHandler);
// Allow migrating the tables from the old app to the new schema.
updateManager.registerSiteTableMigration({
diff --git a/src/addon/mod/data/fields/date/component/date.ts b/src/addon/mod/data/fields/date/component/date.ts
index 5b17bf4a5ee..c846032a85b 100644
--- a/src/addon/mod/data/fields/date/component/date.ts
+++ b/src/addon/mod/data/fields/date/component/date.ts
@@ -42,19 +42,19 @@ export class AddonModDataFieldDateComponent extends AddonModDataFieldPluginCompo
let val;
- // Calculate format to use. ion-datetime doesn't support escaping characters ([]), so we remove them.
- this.format = this.timeUtils.convertPHPToMoment(this.translate.instant('core.strftimedatefullshort'))
- .replace(/[\[\]]/g, '');
+ // Calculate format to use.
+ this.format = this.timeUtils.fixFormatForDatetime(this.timeUtils.convertPHPToMoment(
+ this.translate.instant('core.strftimedate')));
if (this.mode == 'search') {
this.addControl('f_' + this.field.id + '_z');
val = this.search['f_' + this.field.id + '_y'] ? new Date(this.search['f_' + this.field.id + '_y'] + '-' +
this.search['f_' + this.field.id + '_m'] + '-' + this.search['f_' + this.field.id + '_d']) : new Date();
- this.search['f_' + this.field.id] = val.toISOString();
+ this.search['f_' + this.field.id] = this.timeUtils.toDatetimeFormat(val.getTime());
} else {
val = this.value && this.value.content ? new Date(parseInt(this.value.content, 10) * 1000) : new Date();
- val = val.toISOString();
+ val = this.timeUtils.toDatetimeFormat(val.getTime());
}
this.addControl('f_' + this.field.id, val);
diff --git a/src/addon/mod/data/fields/date/providers/handler.ts b/src/addon/mod/data/fields/date/providers/handler.ts
index f36d166b702..bf9d6507932 100644
--- a/src/addon/mod/data/fields/date/providers/handler.ts
+++ b/src/addon/mod/data/fields/date/providers/handler.ts
@@ -15,6 +15,7 @@ import { Injector, Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { AddonModDataFieldHandler } from '../../../providers/fields-delegate';
import { AddonModDataFieldDateComponent } from '../component/date';
+import { CoreTimeUtilsProvider } from '@providers/utils/time';
/**
* Handler for date data field plugin.
@@ -24,7 +25,7 @@ export class AddonModDataFieldDateHandler implements AddonModDataFieldHandler {
name = 'AddonModDataFieldDateHandler';
type = 'date';
- constructor(private translate: TranslateService) { }
+ constructor(private translate: TranslateService, private timeUtils: CoreTimeUtilsProvider) { }
/**
* Return the Component to use to display the plugin data.
@@ -129,7 +130,7 @@ export class AddonModDataFieldDateHandler implements AddonModDataFieldHandler {
input = inputData[fieldName] && inputData[fieldName].substr(0, 10) || '';
originalFieldData = (originalFieldData && originalFieldData.content &&
- new Date(originalFieldData.content * 1000).toISOString().substr(0, 10)) || '';
+ this.timeUtils.toDatetimeFormat(originalFieldData.content * 1000).substr(0, 10)) || '';
return input != originalFieldData;
}
diff --git a/src/addon/mod/data/fields/multimenu/providers/handler.ts b/src/addon/mod/data/fields/multimenu/providers/handler.ts
index 7c98f1b8c03..0522bc30478 100644
--- a/src/addon/mod/data/fields/multimenu/providers/handler.ts
+++ b/src/addon/mod/data/fields/multimenu/providers/handler.ts
@@ -126,7 +126,7 @@ export class AddonModDataFieldMultimenuHandler implements AddonModDataFieldHandl
* @return {any} Data overriden
*/
overrideData(originalContent: any, offlineContent: any, offlineFiles?: any): any {
- originalContent.content = (offlineContent[''] && offlineContent[''].join('###')) || '';
+ originalContent.content = (offlineContent[''] && offlineContent[''].join('##')) || '';
return originalContent;
}
diff --git a/src/addon/mod/data/lang/en.json b/src/addon/mod/data/lang/en.json
index f358c48a870..219ec090c49 100644
--- a/src/addon/mod/data/lang/en.json
+++ b/src/addon/mod/data/lang/en.json
@@ -10,6 +10,7 @@
"confirmdeleterecord": "Are you sure you want to delete this entry?",
"descending": "Descending",
"disapprove": "Undo approval",
+ "edittagsnotsupported": "Sorry, editing tags is not supported by the app.",
"emptyaddform": "You did not fill out any fields!",
"entrieslefttoadd": "You must add {{$a.entriesleft}} more entry/entries in order to complete this activity",
"entrieslefttoaddtoview": "You must add {{$a.entrieslefttoview}} more entry/entries before you can view other participants' entries.",
@@ -34,8 +35,10 @@
"recorddisapproved": "Entry unapproved",
"resetsettings": "Reset filters",
"search": "Search",
+ "searchbytagsnotsupported": "Sorry, searching by tags is not supported by the app.",
"selectedrequired": "All selected required",
"single": "View single",
+ "tagarea_data_records": "Data records",
"timeadded": "Time added",
"timemodified": "Time modified",
"usedate": "Include in search."
diff --git a/src/addon/mod/data/pages/edit/edit.ts b/src/addon/mod/data/pages/edit/edit.ts
index 35e74cd068f..babfa127947 100644
--- a/src/addon/mod/data/pages/edit/edit.ts
+++ b/src/addon/mod/data/pages/edit/edit.ts
@@ -28,6 +28,7 @@ import { AddonModDataHelperProvider } from '../../providers/helper';
import { AddonModDataOfflineProvider } from '../../providers/offline';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataComponentsModule } from '../../components/components.module';
+import { CoreTagProvider } from '@core/tag/providers/tag';
/**
* Page that displays the view edit page.
@@ -68,7 +69,8 @@ export class AddonModDataEditPage {
protected courseProvider: CoreCourseProvider, protected dataProvider: AddonModDataProvider,
protected dataOffline: AddonModDataOfflineProvider, protected dataHelper: AddonModDataHelperProvider,
sitesProvider: CoreSitesProvider, protected navCtrl: NavController, protected translate: TranslateService,
- protected eventsProvider: CoreEventsProvider, protected fileUploaderProvider: CoreFileUploaderProvider) {
+ protected eventsProvider: CoreEventsProvider, protected fileUploaderProvider: CoreFileUploaderProvider,
+ private tagProvider: CoreTagProvider) {
this.module = params.get('module') || {};
this.entryId = params.get('entryId') || null;
this.courseId = params.get('courseId');
@@ -131,16 +133,9 @@ export class AddonModDataEditPage {
return this.dataProvider.getDatabaseAccessInformation(data.id);
}).then((accessData) => {
if (this.entryId) {
- return this.groupsProvider.getActivityGroupInfo(this.data.coursemodule, accessData.canmanageentries)
- .then((groupInfo) => {
+ return this.groupsProvider.getActivityGroupInfo(this.data.coursemodule).then((groupInfo) => {
this.groupInfo = groupInfo;
-
- // Check selected group is accessible.
- if (groupInfo && groupInfo.groups && groupInfo.groups.length > 0) {
- if (!groupInfo.groups.some((group) => this.selectedGroup == group.id)) {
- this.selectedGroup = groupInfo.groups[0].id;
- }
- }
+ this.selectedGroup = this.groupsProvider.validateGroupId(this.selectedGroup, groupInfo);
});
}
}).then(() => {
@@ -287,7 +282,7 @@ export class AddonModDataEditPage {
let replace,
render,
- template = this.data.addtemplate || this.dataHelper.getDefaultTemplate('add', this.fieldsArray);
+ template = this.dataHelper.getTemplate(this.data, 'addtemplate', this.fieldsArray);
// Replace the fields found on template.
this.fieldsArray.forEach((field) => {
@@ -309,6 +304,11 @@ export class AddonModDataEditPage {
template = template.replace(replace, 'field_' + field.id);
});
+ // Editing tags is not supported.
+ replace = new RegExp('##tags##', 'gi');
+ const message = '{{ \'addon.mod_data.edittagsnotsupported\' | translate }}
';
+ template = template.replace(replace, this.tagProvider.areTagsAvailableInSite() ? message : '');
+
return template;
}
diff --git a/src/addon/mod/data/pages/entry/entry.ts b/src/addon/mod/data/pages/entry/entry.ts
index 51e95ce9ce2..95f96d8fca4 100644
--- a/src/addon/mod/data/pages/entry/entry.ts
+++ b/src/addon/mod/data/pages/entry/entry.ts
@@ -27,6 +27,7 @@ import { AddonModDataSyncProvider } from '../../providers/sync';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataComponentsModule } from '../../components/components.module';
import { CoreCommentsProvider } from '@core/comments/providers/comments';
+import { CoreCommentsCommentsComponent } from '@core/comments/components/comments/comments';
/**
* Page that displays the view entry page.
@@ -38,6 +39,7 @@ import { CoreCommentsProvider } from '@core/comments/providers/comments';
})
export class AddonModDataEntryPage implements OnDestroy {
@ViewChild(Content) content: Content;
+ @ViewChild(CoreCommentsCommentsComponent) comments: CoreCommentsCommentsComponent;
protected module: any;
protected entryId: number;
@@ -149,21 +151,14 @@ export class AddonModDataEntryPage implements OnDestroy {
}).then((accessData) => {
this.access = accessData;
- return this.groupsProvider.getActivityGroupInfo(this.data.coursemodule, accessData.canmanageentries)
- .then((groupInfo) => {
+ return this.groupsProvider.getActivityGroupInfo(this.data.coursemodule).then((groupInfo) => {
this.groupInfo = groupInfo;
-
- // Check selected group is accessible.
- if (groupInfo && groupInfo.groups && groupInfo.groups.length > 0) {
- if (!groupInfo.groups.some((group) => this.selectedGroup == group.id)) {
- this.selectedGroup = groupInfo.groups[0].id;
- }
- }
+ this.selectedGroup = this.groupsProvider.validateGroupId(this.selectedGroup, groupInfo);
});
}).then(() => {
const actions = this.dataHelper.getActions(this.data, this.access, this.entry);
- const template = this.data.singletemplate || this.dataHelper.getDefaultTemplate('single', this.fieldsArray);
+ const template = this.dataHelper.getTemplate(this.data, 'singletemplate', this.fieldsArray);
this.entryHtml = this.dataHelper.displayShowFields(template, this.fieldsArray, this.entry, this.offset, 'show',
actions);
this.showComments = actions.comments;
@@ -221,6 +216,13 @@ export class AddonModDataEntryPage implements OnDestroy {
promises.push(this.dataProvider.invalidateEntryData(this.data.id, this.entryId));
promises.push(this.groupsProvider.invalidateActivityGroupInfo(this.data.coursemodule));
promises.push(this.dataProvider.invalidateEntriesData(this.data.id));
+
+ if (this.data.comments && this.entry && this.entry.id > 0 && this.commentsEnabled && this.comments) {
+ // Refresh comments. Don't add it to promises because we don't want the comments fetch to block the entry fetch.
+ this.comments.doRefresh().catch(() => {
+ // Ignore errors.
+ });
+ }
}
return Promise.all(promises).finally(() => {
diff --git a/src/addon/mod/data/pages/search/search.ts b/src/addon/mod/data/pages/search/search.ts
index 4ca20b5d04e..e61153d3c9f 100644
--- a/src/addon/mod/data/pages/search/search.ts
+++ b/src/addon/mod/data/pages/search/search.ts
@@ -21,6 +21,7 @@ import { CoreTextUtilsProvider } from '@providers/utils/text';
import { AddonModDataComponentsModule } from '../../components/components.module';
import { AddonModDataFieldsDelegate } from '../../providers/fields-delegate';
import { AddonModDataHelperProvider } from '../../providers/helper';
+import { CoreTagProvider } from '@core/tag/providers/tag';
/**
* Page that displays the search modal.
@@ -42,7 +43,8 @@ export class AddonModDataSearchPage {
constructor(params: NavParams, private viewCtrl: ViewController, fb: FormBuilder, protected utils: CoreUtilsProvider,
protected domUtils: CoreDomUtilsProvider, protected fieldsDelegate: AddonModDataFieldsDelegate,
- protected textUtils: CoreTextUtilsProvider, protected dataHelper: AddonModDataHelperProvider) {
+ protected textUtils: CoreTextUtilsProvider, protected dataHelper: AddonModDataHelperProvider,
+ private tagProvider: CoreTagProvider) {
this.search = params.get('search');
this.fields = params.get('fields');
this.data = params.get('data');
@@ -89,7 +91,7 @@ export class AddonModDataSearchPage {
search: this.search.advanced
};
- let template = this.data.asearchtemplate || this.dataHelper.getDefaultTemplate('asearch', this.fieldsArray),
+ let template = this.dataHelper.getTemplate(this.data, 'asearchtemplate', this.fieldsArray),
replace, render;
// Replace the fields found on template.
@@ -117,9 +119,10 @@ export class AddonModDataSearchPage {
[placeholder]="\'addon.mod_data.authorlastname\' | translate" formControlName="lastname">';
template = template.replace(replace, render);
- // Tags are unsupported right now.
+ // Searching by tags is not supported.
replace = new RegExp('##tags##', 'gi');
- template = template.replace(replace, '');
+ const message = '{{ \'addon.mod_data.searchbytagsnotsupported\' | translate }}
';
+ template = template.replace(replace, this.tagProvider.areTagsAvailableInSite() ? message : '');
return template;
}
diff --git a/src/addon/mod/data/providers/helper.ts b/src/addon/mod/data/providers/helper.ts
index b5aaadcec53..2938c6c7281 100644
--- a/src/addon/mod/data/providers/helper.ts
+++ b/src/addon/mod/data/providers/helper.ts
@@ -158,11 +158,12 @@ export class AddonModDataHelperProvider {
* @param {any} entry Entry.
* @param {number} offset Entry offset.
* @param {string} mode Mode list or show.
- * @param {AddonModDataOfflineAction[]} actions Actions that can be performed to the record.
+ * @param {{[name: string]: boolean}} actions Actions that can be performed to the record.
* @return {string} Generated HTML.
*/
displayShowFields(template: string, fields: any[], entry: any, offset: number, mode: string,
- actions: AddonModDataOfflineAction[]): string {
+ actions: {[name: string]: boolean}): string {
+
if (!template) {
return '';
}
@@ -337,7 +338,7 @@ export class AddonModDataHelperProvider {
approved: !data.approval || data.manageapproved,
canmanageentry: true,
fullname: site.getInfo().fullname,
- contents: [],
+ contents: {},
}
});
}
@@ -357,9 +358,9 @@ export class AddonModDataHelperProvider {
* @param {any} database Database activity.
* @param {any} accessInfo Access info to the activity.
* @param {any} record Entry or record where the actions will be performed.
- * @return {any} Keyed with the action names and boolean to evalute if it can or cannot be done.
+ * @return {{[name: string]: boolean}} Keyed with the action names and boolean to evalute if it can or cannot be done.
*/
- getActions(database: any, accessInfo: any, record: any): any {
+ getActions(database: any, accessInfo: any, record: any): {[name: string]: boolean} {
return {
more: true,
moreurl: true,
@@ -367,6 +368,7 @@ export class AddonModDataHelperProvider {
userpicture: true,
timeadded: true,
timemodified: true,
+ tags: true,
edit: record.canmanageentry && !record.deleted, // This already checks capabilities and readonly period.
delete: record.canmanageentry,
@@ -377,7 +379,6 @@ export class AddonModDataHelperProvider {
comments: database.comments,
// Unsupported actions.
- tags: false,
delcheck: false,
export: false
};
@@ -410,10 +411,14 @@ export class AddonModDataHelperProvider {
* @param {any[]} fields List of database fields.
* @return {string} Template HTML.
*/
- getDefaultTemplate( type: 'add' | 'list' | 'single' | 'asearch', fields: any[]): string {
+ getDefaultTemplate(type: string, fields: any[]): string {
+ if (type == 'listtemplateheader' || type == 'listtemplatefooter') {
+ return '';
+ }
+
const html = [];
- if (type == 'list') {
+ if (type == 'listtemplate') {
html.push('##delcheck## ');
}
@@ -432,7 +437,7 @@ export class AddonModDataHelperProvider {
);
});
- if (type == 'list') {
+ if (type == 'listtemplate') {
html.push(
'',
'',
@@ -440,7 +445,7 @@ export class AddonModDataHelperProvider {
' ',
' '
);
- } else if (type == 'single') {
+ } else if (type == 'singletemplate') {
html.push(
'',
'',
@@ -448,7 +453,7 @@ export class AddonModDataHelperProvider {
' ',
' '
);
- } else if (type == 'asearch') {
+ } else if (type == 'asearchtemplate') {
html.push(
'',
'Author first name: ',
@@ -467,7 +472,7 @@ export class AddonModDataHelperProvider {
''
);
- if (type == 'list') {
+ if (type == 'listtemplate') {
html.push(' ');
}
@@ -583,6 +588,28 @@ export class AddonModDataHelperProvider {
});
}
+ /**
+ * Returns the template of a certain type.
+ *
+ * @param {any} data Database object.
+ * @param {string} type Type of template.
+ * @param {any[]} fields List of database fields.
+ * @return {string} Template HTML.
+ */
+ getTemplate(data: any, type: string, fields: any[]): string {
+ let template = data[type] || this.getDefaultTemplate(type, fields);
+
+ // Try to fix syntax errors so the template can be parsed by Angular.
+ template = this.domUtils.fixHtml(template);
+
+ // Add core-link directive to links.
+ template = template.replace(/]*href="[^>]*)>/ig, (match, attributes) => {
+ return ' ';
+ });
+
+ return template;
+ }
+
/**
* Check if data has been changed by the user.
*
diff --git a/src/addon/mod/data/providers/tag-area-handler.ts b/src/addon/mod/data/providers/tag-area-handler.ts
new file mode 100644
index 00000000000..f7fb150984a
--- /dev/null
+++ b/src/addon/mod/data/providers/tag-area-handler.ts
@@ -0,0 +1,58 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate';
+import { CoreTagHelperProvider } from '@core/tag/providers/helper';
+import { CoreTagFeedComponent } from '@core/tag/components/feed/feed';
+import { AddonModDataProvider } from './data';
+
+/**
+ * Handler to support tags.
+ */
+@Injectable()
+export class AddonModDataTagAreaHandler implements CoreTagAreaHandler {
+ name = 'AddonModDataTagAreaHandler';
+ type = 'mod_data/data_records';
+
+ constructor(private tagHelper: CoreTagHelperProvider, private dataProvider: AddonModDataProvider) {}
+
+ /**
+ * Whether or not the handler is enabled on a site level.
+ * @return {boolean|Promise} Whether or not the handler is enabled on a site level.
+ */
+ isEnabled(): boolean | Promise {
+ return this.dataProvider.isPluginEnabled();
+ }
+
+ /**
+ * Parses the rendered content of a tag index and returns the items.
+ *
+ * @param {string} content Rendered content.
+ * @return {any[]|Promise} Area items (or promise resolved with the items).
+ */
+ parseContent(content: string): any[] | Promise {
+ return this.tagHelper.parseFeedContent(content);
+ }
+
+ /**
+ * Get the component to use to display items.
+ *
+ * @param {Injector} injector Injector.
+ * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found.
+ */
+ getComponent(injector: Injector): any | Promise {
+ return CoreTagFeedComponent;
+ }
+}
diff --git a/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html b/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html
index bc403c9e012..8a70e2dfc55 100644
--- a/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html
+++ b/src/addon/mod/feedback/components/index/addon-mod-feedback-index.html
@@ -45,7 +45,7 @@
-
+
{{ 'core.groupsseparate' | translate }}
{{ 'core.groupsvisible' | translate }}
diff --git a/src/addon/mod/feedback/components/index/index.ts b/src/addon/mod/feedback/components/index/index.ts
index 953a8c8bd82..eefc27a7e92 100644
--- a/src/addon/mod/feedback/components/index/index.ts
+++ b/src/addon/mod/feedback/components/index/index.ts
@@ -195,14 +195,14 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity
}
return this.fetchFeedbackOverviewData(this.access);
- }).then(() => {
- // All data obtained, now fill the context menu.
+ }).finally(() => {
+ // Now fill the context menu.
this.fillContextMenu(refresh);
// Check if there are responses stored in offline.
- return this.feedbackOffline.hasFeedbackOfflineData(this.feedback.id);
- }).then((hasOffline) => {
- this.hasOffline = hasOffline;
+ return this.feedbackOffline.hasFeedbackOfflineData(this.feedback.id).then((hasOffline) => {
+ this.hasOffline = hasOffline;
+ });
});
}
@@ -269,7 +269,7 @@ export class AddonModFeedbackIndexComponent extends CoreCourseModuleMainActivity
return this.groupsProvider.getActivityGroupInfo(cmId).then((groupInfo) => {
this.groupInfo = groupInfo;
- return this.setGroup(this.group);
+ return this.setGroup(this.groupsProvider.validateGroupId(this.group, groupInfo));
});
}
diff --git a/src/addon/mod/feedback/pages/form/form.html b/src/addon/mod/feedback/pages/form/form.html
index 2725d3525a6..9bf4f4d1a0a 100644
--- a/src/addon/mod/feedback/pages/form/form.html
+++ b/src/addon/mod/feedback/pages/form/form.html
@@ -30,7 +30,7 @@ {{ 'addon.mod_feedback.mode' | translate }}
- {{ 'addon.mod_feedback.numberoutofrange' | translate }} [{{item.rangefrom}}, {{item.rangeto}}]
+ {{ 'addon.mod_feedback.numberoutofrange' | translate }} [{{item.rangefrom}}, {{item.rangeto}}]
diff --git a/src/addon/mod/feedback/pages/form/form.ts b/src/addon/mod/feedback/pages/form/form.ts
index 65a21bec8ac..f1da26ca8b4 100644
--- a/src/addon/mod/feedback/pages/form/form.ts
+++ b/src/addon/mod/feedback/pages/form/form.ts
@@ -82,10 +82,10 @@ export class AddonModFeedbackFormPage implements OnDestroy {
this.currentSite = sitesProvider.getCurrentSite();
// Refresh online status when changes.
- this.onlineObserver = network.onchange().subscribe((online) => {
+ this.onlineObserver = network.onchange().subscribe(() => {
// Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => {
- this.offline = !online;
+ this.offline = !this.appProvider.isOnline();
});
});
}
diff --git a/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.ts b/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.ts
index 1ca3de97404..4f8c210b72b 100644
--- a/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.ts
+++ b/src/addon/mod/feedback/pages/nonrespondents/nonrespondents.ts
@@ -78,6 +78,7 @@ export class AddonModFeedbackNonRespondentsPage {
return this.groupsProvider.getActivityGroupInfo(this.moduleId).then((groupInfo) => {
this.groupInfo = groupInfo;
+ this.selectedGroup = this.groupsProvider.validateGroupId(this.selectedGroup, groupInfo);
return this.loadGroupUsers(this.selectedGroup);
}).catch((message) => {
diff --git a/src/addon/mod/feedback/pages/respondents/respondents.ts b/src/addon/mod/feedback/pages/respondents/respondents.ts
index e5d555231a3..4a4a669ea52 100644
--- a/src/addon/mod/feedback/pages/respondents/respondents.ts
+++ b/src/addon/mod/feedback/pages/respondents/respondents.ts
@@ -99,6 +99,7 @@ export class AddonModFeedbackRespondentsPage {
return this.groupsProvider.getActivityGroupInfo(this.moduleId).then((groupInfo) => {
this.groupInfo = groupInfo;
+ this.selectedGroup = this.groupsProvider.validateGroupId(this.selectedGroup, groupInfo);
return this.loadGroupAttempts(this.selectedGroup);
}).catch((message) => {
diff --git a/src/addon/mod/forum/components/components.module.ts b/src/addon/mod/forum/components/components.module.ts
index 0f3bf1b10d5..06cdabd01e2 100644
--- a/src/addon/mod/forum/components/components.module.ts
+++ b/src/addon/mod/forum/components/components.module.ts
@@ -21,6 +21,7 @@ import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
import { CoreRatingComponentsModule } from '@core/rating/components/components.module';
+import { CoreTagComponentsModule } from '@core/tag/components/components.module';
import { AddonModForumIndexComponent } from './index/index';
import { AddonModForumPostComponent } from './post/post';
@@ -37,7 +38,8 @@ import { AddonModForumPostComponent } from './post/post';
CoreDirectivesModule,
CorePipesModule,
CoreCourseComponentsModule,
- CoreRatingComponentsModule
+ CoreRatingComponentsModule,
+ CoreTagComponentsModule
],
providers: [
],
diff --git a/src/addon/mod/forum/components/index/addon-mod-forum-index.html b/src/addon/mod/forum/components/index/addon-mod-forum-index.html
index 8a7dae44043..4c8be80bf33 100644
--- a/src/addon/mod/forum/components/index/addon-mod-forum-index.html
+++ b/src/addon/mod/forum/components/index/addon-mod-forum-index.html
@@ -31,17 +31,17 @@
{{ availabilityMessage }}
-
-
+
+
+
-
-
- {{ selectedSortOrder.label | translate }}
-
-
-
+
+
+ {{ selectedSortOrder.label | translate }}
+
+
+
-
@@ -95,9 +95,9 @@
-
-
+
+
diff --git a/src/addon/mod/forum/components/post/addon-mod-forum-post.html b/src/addon/mod/forum/components/post/addon-mod-forum-post.html
index cb8ffc1b328..81107408cd5 100644
--- a/src/addon/mod/forum/components/post/addon-mod-forum-post.html
+++ b/src/addon/mod/forum/components/post/addon-mod-forum-post.html
@@ -1,6 +1,6 @@
-
+
@@ -30,6 +30,10 @@
+ 0">
+ {{ 'core.tag.tags' | translate }}:
+
+
diff --git a/src/addon/mod/forum/components/post/post.ts b/src/addon/mod/forum/components/post/post.ts
index 581c7eefe1c..d6a9ca54d1d 100644
--- a/src/addon/mod/forum/components/post/post.ts
+++ b/src/addon/mod/forum/components/post/post.ts
@@ -14,10 +14,9 @@
import { Component, Input, Output, Optional, EventEmitter, OnInit, OnDestroy } from '@angular/core';
import { FormControl } from '@angular/forms';
-import { NavController, Content } from 'ionic-angular';
+import { Content } from 'ionic-angular';
import { TranslateService } from '@ngx-translate/core';
import { CoreFileUploaderProvider } from '@core/fileuploader/providers/fileuploader';
-import { CoreSplitViewComponent } from '@components/split-view/split-view';
import { CoreSyncProvider } from '@providers/sync';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
@@ -26,6 +25,7 @@ import { AddonModForumHelperProvider } from '../../providers/helper';
import { AddonModForumOfflineProvider } from '../../providers/offline';
import { AddonModForumSyncProvider } from '../../providers/sync';
import { CoreRatingInfo } from '@core/rating/providers/rating';
+import { CoreTagProvider } from '@core/tag/providers/tag';
/**
* Components that shows a discussion post, its attachments and the action buttons allowed (reply, etc.).
@@ -53,11 +53,11 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
uniqueId: string;
advanced = false; // Display all form fields.
+ tagsEnabled: boolean;
protected syncId: string;
constructor(
- private navCtrl: NavController,
private uploaderProvider: CoreFileUploaderProvider,
private syncProvider: CoreSyncProvider,
private domUtils: CoreDomUtilsProvider,
@@ -67,9 +67,10 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
private forumHelper: AddonModForumHelperProvider,
private forumOffline: AddonModForumOfflineProvider,
private forumSync: AddonModForumSyncProvider,
- @Optional() private svComponent: CoreSplitViewComponent,
+ private tagProvider: CoreTagProvider,
@Optional() private content: Content) {
this.onPostChange = new EventEmitter();
+ this.tagsEnabled = this.tagProvider.areTagsAvailableInSite();
}
/**
@@ -79,17 +80,6 @@ export class AddonModForumPostComponent implements OnInit, OnDestroy {
this.uniqueId = this.post.id ? 'reply' + this.post.id : 'edit' + this.post.parent;
}
- /**
- * Opens the profile of a user.
- *
- * @param {number} userId
- */
- openUserProfile(userId: number): void {
- // Decide which navCtrl to use. If this page is inside a split view, use the split view's master nav.
- const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
- navCtrl.push('CoreUserProfilePage', {userId, courseId: this.courseId});
- }
-
/**
* Set data to new post, clearing temporary files and updating original data.
*
diff --git a/src/addon/mod/forum/forum.module.ts b/src/addon/mod/forum/forum.module.ts
index 215c343e647..b8463c714ea 100644
--- a/src/addon/mod/forum/forum.module.ts
+++ b/src/addon/mod/forum/forum.module.ts
@@ -18,6 +18,7 @@ import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
import { CorePushNotificationsDelegate } from '@core/pushnotifications/providers/delegate';
+import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate';
import { AddonModForumProvider } from './providers/forum';
import { AddonModForumOfflineProvider } from './providers/offline';
import { AddonModForumHelperProvider } from './providers/helper';
@@ -28,7 +29,9 @@ import { AddonModForumSyncCronHandler } from './providers/sync-cron-handler';
import { AddonModForumIndexLinkHandler } from './providers/index-link-handler';
import { AddonModForumDiscussionLinkHandler } from './providers/discussion-link-handler';
import { AddonModForumListLinkHandler } from './providers/list-link-handler';
+import { AddonModForumPostLinkHandler } from './providers/post-link-handler';
import { AddonModForumPushClickHandler } from './providers/push-click-handler';
+import { AddonModForumTagAreaHandler } from './providers/tag-area-handler';
import { AddonModForumComponentsModule } from './components/components.module';
import { CoreUpdateManagerProvider } from '@providers/update-manager';
@@ -56,8 +59,10 @@ export const ADDON_MOD_FORUM_PROVIDERS: any[] = [
AddonModForumSyncCronHandler,
AddonModForumIndexLinkHandler,
AddonModForumListLinkHandler,
+ AddonModForumPostLinkHandler,
AddonModForumDiscussionLinkHandler,
- AddonModForumPushClickHandler
+ AddonModForumPushClickHandler,
+ AddonModForumTagAreaHandler
]
})
export class AddonModForumModule {
@@ -66,7 +71,9 @@ export class AddonModForumModule {
cronDelegate: CoreCronDelegate, syncHandler: AddonModForumSyncCronHandler, linksDelegate: CoreContentLinksDelegate,
indexHandler: AddonModForumIndexLinkHandler, discussionHandler: AddonModForumDiscussionLinkHandler,
updateManager: CoreUpdateManagerProvider, listLinkHandler: AddonModForumListLinkHandler,
- pushNotificationsDelegate: CorePushNotificationsDelegate, pushClickHandler: AddonModForumPushClickHandler) {
+ pushNotificationsDelegate: CorePushNotificationsDelegate, pushClickHandler: AddonModForumPushClickHandler,
+ postLinkHandler: AddonModForumPostLinkHandler, tagAreaDelegate: CoreTagAreaDelegate,
+ tagAreaHandler: AddonModForumTagAreaHandler) {
moduleDelegate.registerHandler(moduleHandler);
prefetchDelegate.registerHandler(prefetchHandler);
@@ -74,7 +81,9 @@ export class AddonModForumModule {
linksDelegate.registerHandler(indexHandler);
linksDelegate.registerHandler(discussionHandler);
linksDelegate.registerHandler(listLinkHandler);
+ linksDelegate.registerHandler(postLinkHandler);
pushNotificationsDelegate.registerClickHandler(pushClickHandler);
+ tagAreaDelegate.registerHandler(tagAreaHandler);
// Allow migrating the tables from the old app to the new schema.
updateManager.registerSiteTablesMigration([
diff --git a/src/addon/mod/forum/lang/en.json b/src/addon/mod/forum/lang/en.json
index 93e72d2c2d7..dbfac5fd3be 100644
--- a/src/addon/mod/forum/lang/en.json
+++ b/src/addon/mod/forum/lang/en.json
@@ -50,6 +50,7 @@
"reply": "Reply",
"replyplaceholder": "Write your reply...",
"subject": "Subject",
+ "tagarea_forum_posts": "Forum posts",
"thisforumhasduedate": "The due date for posting to this forum is {{$a}}.",
"thisforumisdue": "The due date for posting to this forum was {{$a}}.",
"unlockdiscussion": "Unlock this discussion",
diff --git a/src/addon/mod/forum/pages/discussion/discussion.ts b/src/addon/mod/forum/pages/discussion/discussion.ts
index f8514dd5f9f..7c395fc576b 100644
--- a/src/addon/mod/forum/pages/discussion/discussion.ts
+++ b/src/addon/mod/forum/pages/discussion/discussion.ts
@@ -115,7 +115,7 @@ export class AddonModForumDiscussionPage implements OnDestroy {
this.postId = navParams.get('postId');
this.isOnline = this.appProvider.isOnline();
- this.onlineObserver = network.onchange().subscribe((online) => {
+ this.onlineObserver = network.onchange().subscribe(() => {
// Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => {
this.isOnline = this.appProvider.isOnline();
@@ -354,6 +354,9 @@ export class AddonModForumDiscussionPage implements OnDestroy {
return Promise.reject('Invalid forum discussion.');
}
+ this.defaultSubject = this.translate.instant('addon.mod_forum.re') + ' ' + this.discussion.subject;
+ this.replyData.subject = this.defaultSubject;
+
if (this.discussion.userfullname && this.discussion.parent == 0 && this.forum.type == 'single') {
// Hide author for first post and type single.
this.discussion.userfullname = null;
diff --git a/src/addon/mod/forum/pages/new-discussion/new-discussion.ts b/src/addon/mod/forum/pages/new-discussion/new-discussion.ts
index be2fb73833d..337966e1d20 100644
--- a/src/addon/mod/forum/pages/new-discussion/new-discussion.ts
+++ b/src/addon/mod/forum/pages/new-discussion/new-discussion.ts
@@ -136,14 +136,14 @@ export class AddonModForumNewDiscussionPage implements OnDestroy {
const promises = [];
if (mode === CoreGroupsProvider.SEPARATEGROUPS || mode === CoreGroupsProvider.VISIBLEGROUPS) {
- promises.push(this.groupsProvider.getActivityAllowedGroups(this.cmId).then((forumGroups) => {
+ promises.push(this.groupsProvider.getActivityAllowedGroups(this.cmId).then((result) => {
let promise;
if (mode === CoreGroupsProvider.VISIBLEGROUPS) {
// We need to check which of the returned groups the user can post to.
- promise = this.validateVisibleGroups(forumGroups);
+ promise = this.validateVisibleGroups(result.groups);
} else {
// WS already filters groups, no need to do it ourselves. Add "All participants" if needed.
- promise = this.addAllParticipantsOption(forumGroups, true);
+ promise = this.addAllParticipantsOption(result.groups, true);
}
return promise.then((forumGroups) => {
diff --git a/src/addon/mod/forum/providers/forum.ts b/src/addon/mod/forum/providers/forum.ts
index 3659e11efb9..6a8d7aa0fb3 100644
--- a/src/addon/mod/forum/providers/forum.ts
+++ b/src/addon/mod/forum/providers/forum.ts
@@ -254,13 +254,13 @@ export class AddonModForumProvider {
formatDiscussionsGroups(cmId: number, discussions: any[]): Promise {
discussions = this.utils.clone(discussions);
- return this.groupsProvider.getActivityAllowedGroups(cmId).then((forumGroups) => {
+ return this.groupsProvider.getActivityAllowedGroups(cmId).then((result) => {
const strAllParts = this.translate.instant('core.allparticipants');
const strAllGroups = this.translate.instant('core.allgroups');
// Turn groups into an object where each group is identified by id.
const groups = {};
- forumGroups.forEach((fg) => {
+ result.groups.forEach((fg) => {
groups[fg.id] = fg;
});
diff --git a/src/addon/mod/forum/providers/index-link-handler.ts b/src/addon/mod/forum/providers/index-link-handler.ts
index 4beb1226f9c..2b7b23e7c2b 100644
--- a/src/addon/mod/forum/providers/index-link-handler.ts
+++ b/src/addon/mod/forum/providers/index-link-handler.ts
@@ -16,6 +16,9 @@ import { Injectable } from '@angular/core';
import { CoreContentLinksModuleIndexHandler } from '@core/contentlinks/classes/module-index-handler';
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
import { AddonModForumProvider } from './forum';
+import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
+import { CoreCourseProvider } from '@core/course/providers/course';
+import { CoreDomUtilsProvider } from '@providers/utils/dom';
/**
* Handler to treat links to forum index.
@@ -24,8 +27,12 @@ import { AddonModForumProvider } from './forum';
export class AddonModForumIndexLinkHandler extends CoreContentLinksModuleIndexHandler {
name = 'AddonModForumIndexLinkHandler';
- constructor(courseHelper: CoreCourseHelperProvider, protected forumProvider: AddonModForumProvider) {
+ constructor(courseHelper: CoreCourseHelperProvider, protected forumProvider: AddonModForumProvider,
+ private courseProvider: CoreCourseProvider, private domUtils: CoreDomUtilsProvider) {
super(courseHelper, 'AddonModForum', 'forum');
+
+ // Match the view.php URL with an id param.
+ this.pattern = new RegExp('\/mod\/forum\/view\.php.*([\&\?](f|id)=\\d+)');
}
/**
@@ -41,4 +48,36 @@ export class AddonModForumIndexLinkHandler extends CoreContentLinksModuleIndexHa
isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise {
return true;
}
+
+ /**
+ * Get the list of actions for a link (url).
+ *
+ * @param {string[]} siteIds List of sites the URL belongs to.
+ * @param {string} url The URL to treat.
+ * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
+ * @param {number} [courseId] Course ID related to the URL. Optional but recommended.
+ * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions.
+ */
+ getActions(siteIds: string[], url: string, params: any, courseId?: number):
+ CoreContentLinksAction[] | Promise {
+
+ if (typeof params.f != 'undefined') {
+ return [{
+ action: (siteId, navCtrl?): void => {
+ const modal = this.domUtils.showModalLoading(),
+ forumId = parseInt(params.f, 10);
+
+ this.courseProvider.getModuleBasicInfoByInstance(forumId, 'forum', siteId).then((module) => {
+ this.courseHelper.navigateToModule(parseInt(module.id, 10), siteId, module.course, undefined,
+ undefined, undefined, navCtrl);
+ }).finally(() => {
+ // Just in case. In fact we need to dismiss the modal before showing a toast or error message.
+ modal.dismiss();
+ });
+ }
+ }];
+ }
+
+ return super.getActions(siteIds, url, params, courseId);
+ }
}
diff --git a/src/addon/mod/forum/providers/post-link-handler.ts b/src/addon/mod/forum/providers/post-link-handler.ts
new file mode 100644
index 00000000000..73d691dc199
--- /dev/null
+++ b/src/addon/mod/forum/providers/post-link-handler.ts
@@ -0,0 +1,84 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
+import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
+import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
+import { CoreCourseProvider } from '@core/course/providers/course';
+import { CoreDomUtilsProvider } from '@providers/utils/dom';
+
+/**
+ * Content links handler for forum new discussion.
+ * Match mod/forum/post.php?forum=6 with a valid data.
+ */
+@Injectable()
+export class AddonModForumPostLinkHandler extends CoreContentLinksHandlerBase {
+ name = 'AddonModForumPostLinkHandler';
+ featureName = 'CoreCourseModuleDelegate_AddonModForum';
+ pattern = /\/mod\/forum\/post\.php.*([\?\&](forum)=\d+)/;
+
+ constructor(private linkHelper: CoreContentLinksHelperProvider, private courseProvider: CoreCourseProvider,
+ private domUtils: CoreDomUtilsProvider) {
+ super();
+ }
+
+ /**
+ * Get the list of actions for a link (url).
+ *
+ * @param {string[]} siteIds List of sites the URL belongs to.
+ * @param {string} url The URL to treat.
+ * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
+ * @param {number} [courseId] Course ID related to the URL. Optional but recommended.
+ * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions.
+ */
+ getActions(siteIds: string[], url: string, params: any, courseId?: number):
+ CoreContentLinksAction[] | Promise {
+
+ return [{
+ action: (siteId, navCtrl?): void => {
+ const modal = this.domUtils.showModalLoading(),
+ forumId = parseInt(params.forum, 10);
+
+ this.courseProvider.getModuleBasicInfoByInstance(forumId, 'forum', siteId).then((module) => {
+ const pageParams = {
+ courseId: module.course,
+ cmId: module.id,
+ forumId: module.instance,
+ timeCreated: 0,
+ };
+
+ return this.linkHelper.goInSite(navCtrl, 'AddonModForumNewDiscussionPage', pageParams, siteId);
+ }).finally(() => {
+ // Just in case. In fact we need to dismiss the modal before showing a toast or error message.
+ modal.dismiss();
+ });
+ }
+ }];
+ }
+
+ /**
+ * Check if the handler is enabled for a certain site (site + user) and a URL.
+ * If not defined, defaults to true.
+ *
+ * @param {string} siteId The site ID.
+ * @param {string} url The URL to treat.
+ * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
+ * @param {number} [courseId] Course ID related to the URL. Optional but recommended.
+ * @return {boolean|Promise} Whether the handler is enabled for the URL and site.
+ */
+ isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise {
+ return typeof params.forum != 'undefined';
+ }
+}
diff --git a/src/addon/mod/forum/providers/prefetch-handler.ts b/src/addon/mod/forum/providers/prefetch-handler.ts
index 6241dce1923..e698a4977a5 100644
--- a/src/addon/mod/forum/providers/prefetch-handler.ts
+++ b/src/addon/mod/forum/providers/prefetch-handler.ts
@@ -255,7 +255,7 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand
}
// Activity uses groups, prefetch allowed groups.
- return this.groupsProvider.getActivityAllowedGroups(forum.cmid).then((groups) => {
+ return this.groupsProvider.getActivityAllowedGroups(forum.cmid).then((result) => {
if (mode === CoreGroupsProvider.SEPARATEGROUPS) {
// Groups are already filtered by WS. Prefetch canAddDiscussionToAll to determine if user can pin/attach.
return this.forumProvider.canAddDiscussionToAll(forum.id).catch(() => {
@@ -278,7 +278,7 @@ export class AddonModForumPrefetchHandler extends CoreCourseActivityPrefetchHand
// The user can't post to all groups, let's check which groups he can post to.
const groupPromises = [];
- groups.forEach((group) => {
+ result.groups.forEach((group) => {
groupPromises.push(this.forumProvider.canAddDiscussion(forum.id, group.id).catch(() => {
// Ignore errors.
}));
diff --git a/src/addon/mod/forum/providers/sync.ts b/src/addon/mod/forum/providers/sync.ts
index 5ad34cbb600..fcfbc2743ec 100644
--- a/src/addon/mod/forum/providers/sync.ts
+++ b/src/addon/mod/forum/providers/sync.ts
@@ -228,8 +228,8 @@ export class AddonModForumSyncProvider extends CoreSyncBaseProvider {
if (data.groupid == AddonModForumProvider.ALL_GROUPS) {
// Fetch all group ids.
groupsPromise = this.forumProvider.getForumById(data.courseid, data.forumid, siteId).then((forum) => {
- return this.groupsProvider.getActivityAllowedGroups(forum.cmid).then((groups) => {
- return groups.map((group) => group.id);
+ return this.groupsProvider.getActivityAllowedGroups(forum.cmid).then((result) => {
+ return result.groups.map((group) => group.id);
});
});
} else {
diff --git a/src/addon/mod/forum/providers/tag-area-handler.ts b/src/addon/mod/forum/providers/tag-area-handler.ts
new file mode 100644
index 00000000000..02505b31c04
--- /dev/null
+++ b/src/addon/mod/forum/providers/tag-area-handler.ts
@@ -0,0 +1,57 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate';
+import { CoreTagHelperProvider } from '@core/tag/providers/helper';
+import { CoreTagFeedComponent } from '@core/tag/components/feed/feed';
+
+/**
+ * Handler to support tags.
+ */
+@Injectable()
+export class AddonModForumTagAreaHandler implements CoreTagAreaHandler {
+ name = 'AddonModForumTagAreaHandler';
+ type = 'mod_forum/forum_posts';
+
+ constructor(private tagHelper: CoreTagHelperProvider) {}
+
+ /**
+ * Whether or not the handler is enabled on a site level.
+ * @return {boolean|Promise} Whether or not the handler is enabled on a site level.
+ */
+ isEnabled(): boolean | Promise {
+ return true;
+ }
+
+ /**
+ * Parses the rendered content of a tag index and returns the items.
+ *
+ * @param {string} content Rendered content.
+ * @return {any[]|Promise} Area items (or promise resolved with the items).
+ */
+ parseContent(content: string): any[] | Promise {
+ return this.tagHelper.parseFeedContent(content);
+ }
+
+ /**
+ * Get the component to use to display items.
+ *
+ * @param {Injector} injector Injector.
+ * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found.
+ */
+ getComponent(injector: Injector): any | Promise {
+ return CoreTagFeedComponent;
+ }
+}
diff --git a/src/addon/mod/glossary/components/index/index.ts b/src/addon/mod/glossary/components/index/index.ts
index 558eac5311b..ed3d807e930 100644
--- a/src/addon/mod/glossary/components/index/index.ts
+++ b/src/addon/mod/glossary/components/index/index.ts
@@ -269,7 +269,7 @@ export class AddonModGlossaryIndexComponent extends CoreCourseModuleMainActivity
this.viewMode = 'cat';
this.fetchFunction = this.glossaryProvider.getEntriesByCategory;
this.fetchInvalidate = this.glossaryProvider.invalidateEntriesByCategory;
- this.fetchArguments = [this.glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATERGORIES];
+ this.fetchArguments = [this.glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATEGORIES];
this.getDivider = (entry: any): string => entry.categoryname;
this.showDivider = (entry?: any, previous?: any): boolean => {
return !previous || this.getDivider(entry) != this.getDivider(previous);
diff --git a/src/addon/mod/glossary/glossary.module.ts b/src/addon/mod/glossary/glossary.module.ts
index ab114c54bc1..e3520fbe72a 100644
--- a/src/addon/mod/glossary/glossary.module.ts
+++ b/src/addon/mod/glossary/glossary.module.ts
@@ -17,6 +17,7 @@ import { CoreCronDelegate } from '@providers/cron';
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
+import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate';
import { AddonModGlossaryProvider } from './providers/glossary';
import { AddonModGlossaryOfflineProvider } from './providers/offline';
import { AddonModGlossaryHelperProvider } from './providers/helper';
@@ -27,6 +28,8 @@ import { AddonModGlossarySyncCronHandler } from './providers/sync-cron-handler';
import { AddonModGlossaryIndexLinkHandler } from './providers/index-link-handler';
import { AddonModGlossaryEntryLinkHandler } from './providers/entry-link-handler';
import { AddonModGlossaryListLinkHandler } from './providers/list-link-handler';
+import { AddonModGlossaryEditLinkHandler } from './providers/edit-link-handler';
+import { AddonModGlossaryTagAreaHandler } from './providers/tag-area-handler';
import { AddonModGlossaryComponentsModule } from './components/components.module';
import { CoreUpdateManagerProvider } from '@providers/update-manager';
@@ -54,7 +57,9 @@ export const ADDON_MOD_GLOSSARY_PROVIDERS: any[] = [
AddonModGlossarySyncCronHandler,
AddonModGlossaryIndexLinkHandler,
AddonModGlossaryEntryLinkHandler,
- AddonModGlossaryListLinkHandler
+ AddonModGlossaryListLinkHandler,
+ AddonModGlossaryEditLinkHandler,
+ AddonModGlossaryTagAreaHandler
]
})
export class AddonModGlossaryModule {
@@ -62,7 +67,9 @@ export class AddonModGlossaryModule {
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModGlossaryPrefetchHandler,
cronDelegate: CoreCronDelegate, syncHandler: AddonModGlossarySyncCronHandler, linksDelegate: CoreContentLinksDelegate,
indexHandler: AddonModGlossaryIndexLinkHandler, discussionHandler: AddonModGlossaryEntryLinkHandler,
- updateManager: CoreUpdateManagerProvider, listLinkHandler: AddonModGlossaryListLinkHandler) {
+ updateManager: CoreUpdateManagerProvider, listLinkHandler: AddonModGlossaryListLinkHandler,
+ editLinkHandler: AddonModGlossaryEditLinkHandler, tagAreaDelegate: CoreTagAreaDelegate,
+ tagAreaHandler: AddonModGlossaryTagAreaHandler) {
moduleDelegate.registerHandler(moduleHandler);
prefetchDelegate.registerHandler(prefetchHandler);
@@ -70,6 +77,8 @@ export class AddonModGlossaryModule {
linksDelegate.registerHandler(indexHandler);
linksDelegate.registerHandler(discussionHandler);
linksDelegate.registerHandler(listLinkHandler);
+ linksDelegate.registerHandler(editLinkHandler);
+ tagAreaDelegate.registerHandler(tagAreaHandler);
// Allow migrating the tables from the old app to the new schema.
updateManager.registerSiteTableMigration({
diff --git a/src/addon/mod/glossary/lang/en.json b/src/addon/mod/glossary/lang/en.json
index 18e5ff7bcb9..ba4329f33db 100644
--- a/src/addon/mod/glossary/lang/en.json
+++ b/src/addon/mod/glossary/lang/en.json
@@ -26,5 +26,6 @@
"linking": "Auto-linking",
"modulenameplural": "Glossaries",
"noentriesfound": "No entries were found.",
- "searchquery": "Search query"
+ "searchquery": "Search query",
+ "tagarea_glossary_entries": "Glossary entries"
}
diff --git a/src/addon/mod/glossary/pages/edit/edit.html b/src/addon/mod/glossary/pages/edit/edit.html
index f5c2381a0e7..5678fcba899 100644
--- a/src/addon/mod/glossary/pages/edit/edit.html
+++ b/src/addon/mod/glossary/pages/edit/edit.html
@@ -19,7 +19,7 @@
0">
{{ 'addon.mod_glossary.categories' | translate }}
-
+
{{ category.name }}
diff --git a/src/addon/mod/glossary/pages/entry/entry.html b/src/addon/mod/glossary/pages/entry/entry.html
index f34e46d2777..5954398ff4e 100644
--- a/src/addon/mod/glossary/pages/entry/entry.html
+++ b/src/addon/mod/glossary/pages/entry/entry.html
@@ -28,6 +28,10 @@
+ 0">
+ {{ 'core.tag.tags' | translate }}:
+
+
{{ 'addon.mod_glossary.entrypendingapproval' | translate }}
diff --git a/src/addon/mod/glossary/pages/entry/entry.module.ts b/src/addon/mod/glossary/pages/entry/entry.module.ts
index cc69e9dc4cf..7309430914e 100644
--- a/src/addon/mod/glossary/pages/entry/entry.module.ts
+++ b/src/addon/mod/glossary/pages/entry/entry.module.ts
@@ -19,6 +19,7 @@ import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CorePipesModule } from '@pipes/pipes.module';
import { CoreRatingComponentsModule } from '@core/rating/components/components.module';
+import { CoreTagComponentsModule } from '@core/tag/components/components.module';
import { AddonModGlossaryEntryPage } from './entry';
@NgModule({
@@ -31,7 +32,8 @@ import { AddonModGlossaryEntryPage } from './entry';
CorePipesModule,
IonicPageModule.forChild(AddonModGlossaryEntryPage),
TranslateModule.forChild(),
- CoreRatingComponentsModule
+ CoreRatingComponentsModule,
+ CoreTagComponentsModule
],
})
export class AddonModForumDiscussionPageModule {}
diff --git a/src/addon/mod/glossary/pages/entry/entry.ts b/src/addon/mod/glossary/pages/entry/entry.ts
index 7bbf4f0bf19..a5b38641d0c 100644
--- a/src/addon/mod/glossary/pages/entry/entry.ts
+++ b/src/addon/mod/glossary/pages/entry/entry.ts
@@ -16,6 +16,7 @@ import { Component } from '@angular/core';
import { IonicPage, NavParams } from 'ionic-angular';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreRatingInfo } from '@core/rating/providers/rating';
+import { CoreTagProvider } from '@core/tag/providers/tag';
import { AddonModGlossaryProvider } from '../../providers/glossary';
/**
@@ -35,15 +36,18 @@ export class AddonModGlossaryEntryPage {
showAuthor = false;
showDate = false;
ratingInfo: CoreRatingInfo;
+ tagsEnabled: boolean;
protected courseId: number;
protected entryId: number;
constructor(navParams: NavParams,
private domUtils: CoreDomUtilsProvider,
- private glossaryProvider: AddonModGlossaryProvider) {
+ private glossaryProvider: AddonModGlossaryProvider,
+ private tagProvider: CoreTagProvider) {
this.courseId = navParams.get('courseId');
this.entryId = navParams.get('entryId');
+ this.tagsEnabled = this.tagProvider.areTagsAvailableInSite();
}
/**
diff --git a/src/addon/mod/glossary/providers/edit-link-handler.ts b/src/addon/mod/glossary/providers/edit-link-handler.ts
new file mode 100644
index 00000000000..c86557aee79
--- /dev/null
+++ b/src/addon/mod/glossary/providers/edit-link-handler.ts
@@ -0,0 +1,88 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable } from '@angular/core';
+import { CoreContentLinksHandlerBase } from '@core/contentlinks/classes/base-handler';
+import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
+import { CoreContentLinksHelperProvider } from '@core/contentlinks/providers/helper';
+import { CoreCourseProvider } from '@core/course/providers/course';
+import { CoreDomUtilsProvider } from '@providers/utils/dom';
+import { AddonModGlossaryProvider } from './glossary';
+
+/**
+ * Content links handler for glossary new entry.
+ * Match mod/glossary/edit.php?cmid=6 with a valid data.
+ * Currently it only supports new entry.
+ */
+@Injectable()
+export class AddonModGlossaryEditLinkHandler extends CoreContentLinksHandlerBase {
+ name = 'AddonModGlossaryEditLinkHandler';
+ featureName = 'CoreCourseModuleDelegate_AddonModGlossary';
+ pattern = /\/mod\/glossary\/edit\.php.*([\?\&](cmid)=\d+)/;
+
+ constructor(private linkHelper: CoreContentLinksHelperProvider, private courseProvider: CoreCourseProvider,
+ private domUtils: CoreDomUtilsProvider, private glossaryProvider: AddonModGlossaryProvider) {
+ super();
+ }
+
+ /**
+ * Get the list of actions for a link (url).
+ *
+ * @param {string[]} siteIds List of sites the URL belongs to.
+ * @param {string} url The URL to treat.
+ * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
+ * @param {number} [courseId] Course ID related to the URL. Optional but recommended.
+ * @return {CoreContentLinksAction[]|Promise} List of (or promise resolved with list of) actions.
+ */
+ getActions(siteIds: string[], url: string, params: any, courseId?: number):
+ CoreContentLinksAction[] | Promise {
+
+ return [{
+ action: (siteId, navCtrl?): void => {
+ const modal = this.domUtils.showModalLoading(),
+ cmId = parseInt(params.cmid, 10);
+
+ this.courseProvider.getModuleBasicInfo(cmId, siteId).then((module) => {
+ return this.glossaryProvider.getGlossary(module.course, module.id).then((glossary) => {
+ const pageParams = {
+ courseId: module.course,
+ module: module,
+ glossary: glossary,
+ entry: null // It does not support entry editing.
+ };
+
+ this.linkHelper.goInSite(navCtrl, 'AddonModGlossaryEditPage', pageParams, siteId);
+ });
+ }).finally(() => {
+ // Just in case. In fact we need to dismiss the modal before showing a toast or error message.
+ modal.dismiss();
+ });
+ }
+ }];
+ }
+
+ /**
+ * Check if the handler is enabled for a certain site (site + user) and a URL.
+ * If not defined, defaults to true.
+ *
+ * @param {string} siteId The site ID.
+ * @param {string} url The URL to treat.
+ * @param {any} params The params of the URL. E.g. 'mysite.com?id=1' -> {id: 1}
+ * @param {number} [courseId] Course ID related to the URL. Optional but recommended.
+ * @return {boolean|Promise} Whether the handler is enabled for the URL and site.
+ */
+ isEnabled(siteId: string, url: string, params: any, courseId?: number): boolean | Promise {
+ return typeof params.cmid != 'undefined';
+ }
+}
diff --git a/src/addon/mod/glossary/providers/entry-link-handler.ts b/src/addon/mod/glossary/providers/entry-link-handler.ts
index 953aeb41a23..99acce7b731 100644
--- a/src/addon/mod/glossary/providers/entry-link-handler.ts
+++ b/src/addon/mod/glossary/providers/entry-link-handler.ts
@@ -27,7 +27,7 @@ import { AddonModGlossaryProvider } from './glossary';
export class AddonModGlossaryEntryLinkHandler extends CoreContentLinksHandlerBase {
name = 'AddonModGlossaryEntryLinkHandler';
featureName = 'CoreCourseModuleDelegate_AddonModGlossary';
- pattern = /\/mod\/glossary\/showentry\.php.*([\&\?]eid=\d+)/;
+ pattern = /\/mod\/glossary\/(showentry|view)\.php.*([\&\?](eid|g|mode|hook)=\d+)/;
constructor(
private domUtils: CoreDomUtilsProvider,
@@ -51,7 +51,13 @@ export class AddonModGlossaryEntryLinkHandler extends CoreContentLinksHandlerBas
return [{
action: (siteId, navCtrl?): void => {
const modal = this.domUtils.showModalLoading();
- const entryId = parseInt(params.eid, 10);
+ let entryId;
+ if (params.mode == 'entry') {
+ entryId = parseInt(params.hook, 10);
+ } else {
+ entryId = parseInt(params.eid, 10);
+ }
+
let promise;
if (courseId) {
diff --git a/src/addon/mod/glossary/providers/glossary.ts b/src/addon/mod/glossary/providers/glossary.ts
index bd6862d688a..cd494187f3b 100644
--- a/src/addon/mod/glossary/providers/glossary.ts
+++ b/src/addon/mod/glossary/providers/glossary.ts
@@ -17,7 +17,7 @@ import { TranslateService } from '@ngx-translate/core';
import { CoreSite } from '@classes/site';
import { CoreAppProvider } from '@providers/app';
import { CoreFilepoolProvider } from '@providers/filepool';
-import { CoreSitesProvider } from '@providers/sites';
+import { CoreSitesProvider, CoreSiteSchema } from '@providers/sites';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreCourseLogHelperProvider } from '@core/course/providers/log-helper';
@@ -32,13 +32,40 @@ export class AddonModGlossaryProvider {
static COMPONENT = 'mmaModGlossary';
static LIMIT_ENTRIES = 25;
static LIMIT_CATEGORIES = 10;
- static SHOW_ALL_CATERGORIES = 0;
+ static SHOW_ALL_CATEGORIES = 0;
static SHOW_NOT_CATEGORISED = -1;
static ADD_ENTRY_EVENT = 'addon_mod_glossary_add_entry';
protected ROOT_CACHE_KEY = 'mmaModGlossary:';
+ // Variables for database.
+ static ENTRIES_TABLE = 'addon_mod_glossary_entry_glossaryid';
+ protected siteSchema: CoreSiteSchema = {
+ name: 'AddonModGlossaryProvider',
+ version: 1,
+ tables: [
+ {
+ name: AddonModGlossaryProvider.ENTRIES_TABLE,
+ columns: [
+ {
+ name: 'entryid',
+ type: 'INTEGER',
+ primaryKey: true
+ },
+ {
+ name: 'glossaryid',
+ type: 'INTEGER',
+ },
+ {
+ name: 'pagefrom',
+ type: 'INTEGER',
+ }
+ ]
+ }
+ ]
+ };
+
constructor(private appProvider: CoreAppProvider,
private sitesProvider: CoreSitesProvider,
private filepoolProvider: CoreFilepoolProvider,
@@ -46,7 +73,10 @@ export class AddonModGlossaryProvider {
private textUtils: CoreTextUtilsProvider,
private utils: CoreUtilsProvider,
private glossaryOffline: AddonModGlossaryOfflineProvider,
- private logHelper: CoreCourseLogHelperProvider) {}
+ private logHelper: CoreCourseLogHelperProvider) {
+
+ this.sitesProvider.registerSiteSchema(this.siteSchema);
+ }
/**
* Get the course glossary cache key.
@@ -118,12 +148,13 @@ export class AddonModGlossaryProvider {
* @param {string} sort The direction of the order: ASC or DESC
* @param {number} from Start returning records from here.
* @param {number} limit Number of records to return.
- * @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false.
+ * @param {boolean} omitExpires True to always get the value from cache. If data isn't cached, it will call the WS.
+ * @param {boolean} forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise} Resolved with the entries.
*/
getEntriesByAuthor(glossaryId: number, letter: string, field: string, sort: string, from: number, limit: number,
- forceCache: boolean, siteId?: string): Promise {
+ omitExpires: boolean, forceOffline: boolean, siteId?: string): Promise {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
id: glossaryId,
@@ -135,7 +166,8 @@ export class AddonModGlossaryProvider {
};
const preSets = {
cacheKey: this.getEntriesByAuthorCacheKey(glossaryId, letter, field, sort),
- omitExpires: forceCache,
+ omitExpires: omitExpires,
+ forceOffline: forceOffline,
updateFrequency: CoreSite.FREQUENCY_SOMETIMES
};
@@ -165,16 +197,18 @@ export class AddonModGlossaryProvider {
* Get entries by category.
*
* @param {number} glossaryId Glossary Id.
- * @param {string} categoryId The category ID. Use constant SHOW_ALL_CATERGORIES for all categories, or
+ * @param {string} categoryId The category ID. Use constant SHOW_ALL_CATEGORIES for all categories, or
* constant SHOW_NOT_CATEGORISED for uncategorised entries.
* @param {number} from Start returning records from here.
* @param {number} limit Number of records to return.
- * @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false.
+ * @param {boolean} omitExpires True to always get the value from cache. If data isn't cached, it will call the WS.
+ * @param {boolean} forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise} Resolved with the entries.
*/
- getEntriesByCategory(glossaryId: number, categoryId: number, from: number, limit: number, forceCache: boolean,
- siteId?: string): Promise {
+ getEntriesByCategory(glossaryId: number, categoryId: number, from: number, limit: number, omitExpires: boolean,
+ forceOffline: boolean, siteId?: string): Promise {
+
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
id: glossaryId,
@@ -184,7 +218,8 @@ export class AddonModGlossaryProvider {
};
const preSets = {
cacheKey: this.getEntriesByCategoryCacheKey(glossaryId, categoryId),
- omitExpires: forceCache,
+ omitExpires: omitExpires,
+ forceOffline: forceOffline,
updateFrequency: CoreSite.FREQUENCY_SOMETIMES
};
@@ -196,7 +231,7 @@ export class AddonModGlossaryProvider {
* Invalidate cache of entries by category.
*
* @param {number} glossaryId Glossary Id.
- * @param {string} categoryId The category ID. Use constant SHOW_ALL_CATERGORIES for all categories, or
+ * @param {string} categoryId The category ID. Use constant SHOW_ALL_CATEGORIES for all categories, or
* constant SHOW_NOT_CATEGORISED for uncategorised entries.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise} Resolved when data is invalidated.
@@ -213,7 +248,7 @@ export class AddonModGlossaryProvider {
* Get the entries by category cache key.
*
* @param {number} glossaryId Glossary Id.
- * @param {string} categoryId The category ID. Use constant SHOW_ALL_CATERGORIES for all categories, or
+ * @param {string} categoryId The category ID. Use constant SHOW_ALL_CATEGORIES for all categories, or
* constant SHOW_NOT_CATEGORISED for uncategorised entries.
* @return {string} Cache key.
*/
@@ -241,12 +276,14 @@ export class AddonModGlossaryProvider {
* @param {string} sort The direction of the order.
* @param {number} from Start returning records from here.
* @param {number} limit Number of records to return.
- * @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false.
+ * @param {boolean} omitExpires True to always get the value from cache. If data isn't cached, it will call the WS.
+ * @param {boolean} forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise} Resolved with the entries.
*/
- getEntriesByDate(glossaryId: number, order: string, sort: string, from: number, limit: number, forceCache: boolean,
- siteId?: string): Promise {
+ getEntriesByDate(glossaryId: number, order: string, sort: string, from: number, limit: number, omitExpires: boolean,
+ forceOffline: boolean, siteId?: string): Promise {
+
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
id: glossaryId,
@@ -257,7 +294,8 @@ export class AddonModGlossaryProvider {
};
const preSets = {
cacheKey: this.getEntriesByDateCacheKey(glossaryId, order, sort),
- omitExpires: forceCache,
+ omitExpires: omitExpires,
+ forceOffline: forceOffline,
updateFrequency: CoreSite.FREQUENCY_SOMETIMES
};
@@ -300,12 +338,14 @@ export class AddonModGlossaryProvider {
* @param {string} letter A letter, or a special keyword.
* @param {number} from Start returning records from here.
* @param {number} limit Number of records to return.
- * @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false.
+ * @param {boolean} omitExpires True to always get the value from cache. If data isn't cached, it will call the WS.
+ * @param {boolean} forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS.
* @param {string} [siteId] Site ID. If not defined, current site.
- * @return {Promise} Resolved with the entries.
+ * @return {Promise} Resolved with the entries.
*/
- getEntriesByLetter(glossaryId: number, letter: string, from: number, limit: number, forceCache: boolean, siteId?: string):
- Promise {
+ getEntriesByLetter(glossaryId: number, letter: string, from: number, limit: number, omitExpires: boolean, forceOffline: boolean,
+ siteId?: string): Promise {
+
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
id: glossaryId,
@@ -315,11 +355,22 @@ export class AddonModGlossaryProvider {
};
const preSets = {
cacheKey: this.getEntriesByLetterCacheKey(glossaryId, letter),
- omitExpires: forceCache,
+ omitExpires: omitExpires,
+ forceOffline: forceOffline,
updateFrequency: CoreSite.FREQUENCY_SOMETIMES
};
- return site.read('mod_glossary_get_entries_by_letter', params, preSets);
+ return site.read('mod_glossary_get_entries_by_letter', params, preSets).then((result) => {
+
+ if (limit == AddonModGlossaryProvider.LIMIT_ENTRIES) {
+ // Store entries in background, don't block the user for this.
+ this.storeEntries(glossaryId, result.entries, from, site.getId()).catch(() => {
+ // Ignore errors.
+ });
+ }
+
+ return result;
+ });
});
}
@@ -364,12 +415,13 @@ export class AddonModGlossaryProvider {
* @param {string} sort The direction of the order.
* @param {number} from Start returning records from here.
* @param {number} limit Number of records to return.
- * @param {boolean} forceCache True to always get the value from cache, false otherwise. Default false.
+ * @param {boolean} omitExpires True to always get the value from cache. If data isn't cached, it will call the WS.
+ * @param {boolean} forceOffline True to always get the value from cache. If data isn't cached, it won't call the WS.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise} Resolved with the entries.
*/
getEntriesBySearch(glossaryId: number, query: string, fullSearch: boolean, order: string, sort: string, from: number,
- limit: number, forceCache: boolean, siteId?: string): Promise {
+ limit: number, omitExpires: boolean, forceOffline: boolean, siteId?: string): Promise {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
id: glossaryId,
@@ -382,7 +434,8 @@ export class AddonModGlossaryProvider {
};
const preSets = {
cacheKey: this.getEntriesBySearchCacheKey(glossaryId, query, fullSearch, order, sort),
- omitExpires: forceCache,
+ omitExpires: omitExpires,
+ forceOffline: forceOffline,
updateFrequency: CoreSite.FREQUENCY_SOMETIMES
};
@@ -497,7 +550,7 @@ export class AddonModGlossaryProvider {
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise} Promise resolved with the entry.
*/
- getEntry(entryId: number, siteId?: string): Promise<{entry: any, ratinginfo: CoreRatingInfo}> {
+ getEntry(entryId: number, siteId?: string): Promise<{entry: any, ratinginfo: CoreRatingInfo, from?: number}> {
return this.sitesProvider.getSite(siteId).then((site) => {
const params = {
id: entryId
@@ -513,6 +566,74 @@ export class AddonModGlossaryProvider {
} else {
return Promise.reject(null);
}
+ }).catch((error) => {
+ // Entry not found. Search it in the list of entries.
+ let glossaryId;
+
+ const searchEntry = (from: number, loadNext: boolean): Promise => {
+ // Get the entries from this "page" and check if the entry we're looking for is in it.
+ return this.getEntriesByLetter(glossaryId, 'ALL', from, AddonModGlossaryProvider.LIMIT_ENTRIES, false, true,
+ siteId).then((result) => {
+
+ for (let i = 0; i < result.entries.length; i++) {
+ const entry = result.entries[i];
+ if (entry.id == entryId) {
+ // Entry found, return it.
+ return {
+ entry: entry,
+ from: from
+ };
+ }
+ }
+
+ const nextFrom = from + result.entries.length;
+ if (nextFrom < result.count && loadNext) {
+ // Get the next "page".
+ return searchEntry(nextFrom, true);
+ }
+
+ // No more pages and the entry wasn't found. Reject.
+ return Promise.reject(null);
+ });
+ };
+
+ return this.getStoredDataForEntry(entryId, site.getId()).then((data) => {
+ glossaryId = data.glossaryId;
+
+ if (typeof data.from != 'undefined') {
+ return searchEntry(data.from, false).catch(() => {
+ // Entry not found in that page. Search all pages.
+ return searchEntry(0, true);
+ });
+ }
+
+ // Page not specified, search all pages.
+ return searchEntry(0, true);
+ }).catch(() => {
+ return Promise.reject(error);
+ });
+ });
+ });
+ }
+
+ /**
+ * Get a glossary ID and the "from" of a given entry.
+ *
+ * @param {number} entryId Entry ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with the glossary ID and the "from".
+ */
+ getStoredDataForEntry(entryId: number, siteId?: string): Promise<{glossaryId: number, from: number}> {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ const conditions = {
+ entryid: entryId
+ };
+
+ return site.getDb().getRecord(AddonModGlossaryProvider.ENTRIES_TABLE, conditions).then((record) => {
+ return {
+ glossaryId: record.glossaryid,
+ from: record.pagefrom
+ };
});
});
}
@@ -524,19 +645,21 @@ export class AddonModGlossaryProvider {
* @param {any[]} fetchArguments Arguments to call the fetching.
* @param {number} [limitFrom=0] Number of entries already fetched, so fetch will be done from this number.
* @param {number} [limitNum] Number of records to return. Defaults to LIMIT_ENTRIES.
- * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false.
+ * @param {boolean} [omitExpires=false] True to always get the value from cache. If data isn't cached, it will call the WS.
+ * @param {boolean} [forceOffline=false] True to always get the value from cache. If data isn't cached, it won't call the WS.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise} Promise resolved with the response.
*/
fetchEntries(fetchFunction: Function, fetchArguments: any[], limitFrom: number = 0, limitNum?: number,
- forceCache: boolean = false, siteId?: string): Promise {
+ omitExpires: boolean = false, forceOffline: boolean = false, siteId?: string): Promise {
limitNum = limitNum || AddonModGlossaryProvider.LIMIT_ENTRIES;
siteId = siteId || this.sitesProvider.getCurrentSiteId();
const args = fetchArguments.slice();
args.push(limitFrom);
args.push(limitNum);
- args.push(forceCache);
+ args.push(omitExpires);
+ args.push(forceOffline);
args.push(siteId);
return fetchFunction.apply(this, args);
@@ -547,18 +670,21 @@ export class AddonModGlossaryProvider {
*
* @param {Function} fetchFunction Function to fetch.
* @param {any[]} fetchArguments Arguments to call the fetching.
- * @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false.
+ * @param {boolean} [omitExpires=false] True to always get the value from cache. If data isn't cached, it will call the WS.
+ * @param {boolean} [forceOffline=false] True to always get the value from cache. If data isn't cached, it won't call the WS.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise} Promise resolved with all entrries.
*/
- fetchAllEntries(fetchFunction: Function, fetchArguments: any[], forceCache: boolean = false, siteId?: string): Promise {
+ fetchAllEntries(fetchFunction: Function, fetchArguments: any[], omitExpires: boolean = false, forceOffline: boolean = false,
+ siteId?: string): Promise {
siteId = siteId || this.sitesProvider.getCurrentSiteId();
const entries = [];
const limitNum = AddonModGlossaryProvider.LIMIT_ENTRIES;
const fetchMoreEntries = (): Promise => {
- return this.fetchEntries(fetchFunction, fetchArguments, entries.length, limitNum, forceCache, siteId).then((result) => {
+ return this.fetchEntries(fetchFunction, fetchArguments, entries.length, limitNum, omitExpires, forceOffline, siteId)
+ .then((result) => {
Array.prototype.push.apply(entries, result.entries);
return entries.length < result.count ? fetchMoreEntries() : entries;
@@ -633,7 +759,8 @@ export class AddonModGlossaryProvider {
const promises = [];
if (!onlyEntriesList) {
- promises.push(this.fetchAllEntries(this.getEntriesByLetter, [glossary.id, 'ALL'], true, siteId).then((entries) => {
+ promises.push(this.fetchAllEntries(this.getEntriesByLetter, [glossary.id, 'ALL'], true, false, siteId)
+ .then((entries) => {
return this.invalidateEntries(entries, siteId);
}));
}
@@ -644,7 +771,7 @@ export class AddonModGlossaryProvider {
promises.push(this.invalidateEntriesByLetter(glossary.id, 'ALL', siteId));
break;
case 'cat':
- promises.push(this.invalidateEntriesByCategory(glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATERGORIES,
+ promises.push(this.invalidateEntriesByCategory(glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATEGORIES,
siteId));
break;
case 'date':
@@ -850,7 +977,7 @@ export class AddonModGlossaryProvider {
// If we get here, there's no offline entry with this name, check online.
// Get entries from the cache.
- return this.fetchAllEntries(this.getEntriesByLetter, [glossaryId, 'ALL'], true, siteId).then((entries) => {
+ return this.fetchAllEntries(this.getEntriesByLetter, [glossaryId, 'ALL'], true, false, siteId).then((entries) => {
// Check if there's any entry with the same concept.
return entries.some((entry) => entry.concept == concept);
});
@@ -906,4 +1033,44 @@ export class AddonModGlossaryProvider {
return this.logHelper.logSingle('mod_glossary_view_entry', params, AddonModGlossaryProvider.COMPONENT, glossaryId, name,
'glossary', {entryid: entryId}, siteId);
}
+
+ /**
+ * Store several entries so we can determine their glossaryId in offline.
+ *
+ * @param {number} glossaryId Glossary ID the entries belongs to.
+ * @param {any[]} entries Entries.
+ * @param {number} from The "page" the entries belong to.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when done.
+ */
+ protected storeEntries(glossaryId: number, entries: any[], from: number, siteId?: string): Promise {
+ const promises = [];
+
+ entries.forEach((entry) => {
+ promises.push(this.storeEntryId(glossaryId, entry.id, from, siteId));
+ });
+
+ return Promise.all(promises);
+ }
+
+ /**
+ * Store an entry so we can determine its glossaryId in offline.
+ *
+ * @param {number} glossaryId Glossary ID the entry belongs to.
+ * @param {number} entryId Entry ID.
+ * @param {number} from The "page" the entry belongs to.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when done.
+ */
+ protected storeEntryId(glossaryId: number, entryId: number, from: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ const entry = {
+ entryid: entryId,
+ glossaryid: glossaryId,
+ pagefrom: from
+ };
+
+ return site.getDb().insertRecord(AddonModGlossaryProvider.ENTRIES_TABLE, entry);
+ });
+ }
}
diff --git a/src/addon/mod/glossary/providers/prefetch-handler.ts b/src/addon/mod/glossary/providers/prefetch-handler.ts
index ba5be62f39c..9235c4b54e5 100644
--- a/src/addon/mod/glossary/providers/prefetch-handler.ts
+++ b/src/addon/mod/glossary/providers/prefetch-handler.ts
@@ -139,17 +139,17 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH
break;
case 'cat': // Not implemented.
promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByCategory,
- [glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATERGORIES], false, siteId));
+ [glossary.id, AddonModGlossaryProvider.SHOW_ALL_CATEGORIES], false, false, siteId));
break;
case 'date':
promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByDate,
- [glossary.id, 'CREATION', 'DESC'], false, siteId));
+ [glossary.id, 'CREATION', 'DESC'], false, false, siteId));
promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByDate,
- [glossary.id, 'UPDATE', 'DESC'], false, siteId));
+ [glossary.id, 'UPDATE', 'DESC'], false, false, siteId));
break;
case 'author':
promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByAuthor,
- [glossary.id, 'ALL', 'LASTNAME', 'ASC'], false, siteId));
+ [glossary.id, 'ALL', 'LASTNAME', 'ASC'], false, false, siteId));
break;
default:
}
@@ -157,13 +157,12 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH
// Fetch all entries to get information from.
promises.push(this.glossaryProvider.fetchAllEntries(this.glossaryProvider.getEntriesByLetter,
- [glossary.id, 'ALL'], false, siteId).then((entries) => {
+ [glossary.id, 'ALL'], false, false, siteId).then((entries) => {
const promises = [];
const avatars = {}; // List of user avatars, preventing duplicates.
entries.forEach((entry) => {
- // Fetch individual entries.
- promises.push(this.glossaryProvider.getEntry(entry.id, siteId));
+ // Don't fetch individual entries, it's too many WS calls.
if (entry.userpictureurl) {
avatars[entry.userpictureurl] = true;
@@ -180,6 +179,13 @@ export class AddonModGlossaryPrefetchHandler extends CoreCourseActivityPrefetchH
return Promise.all(promises);
}));
+ // Get all categories.
+ promises.push(this.glossaryProvider.getAllCategories(glossary.id));
+
+ // Prefetch data for link handlers.
+ promises.push(this.courseProvider.getModuleBasicInfo(module.id, siteId));
+ promises.push(this.courseProvider.getModuleBasicInfoByInstance(glossary.id, 'glossary', siteId));
+
return Promise.all(promises);
});
}
diff --git a/src/addon/mod/glossary/providers/tag-area-handler.ts b/src/addon/mod/glossary/providers/tag-area-handler.ts
new file mode 100644
index 00000000000..4c62f698dcb
--- /dev/null
+++ b/src/addon/mod/glossary/providers/tag-area-handler.ts
@@ -0,0 +1,57 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate';
+import { CoreTagHelperProvider } from '@core/tag/providers/helper';
+import { CoreTagFeedComponent } from '@core/tag/components/feed/feed';
+
+/**
+ * Handler to support tags.
+ */
+@Injectable()
+export class AddonModGlossaryTagAreaHandler implements CoreTagAreaHandler {
+ name = 'AddonModGlossaryTagAreaHandler';
+ type = 'mod_glossary/glossary_entries';
+
+ constructor(private tagHelper: CoreTagHelperProvider) {}
+
+ /**
+ * Whether or not the handler is enabled on a site level.
+ * @return {boolean|Promise} Whether or not the handler is enabled on a site level.
+ */
+ isEnabled(): boolean | Promise {
+ return true;
+ }
+
+ /**
+ * Parses the rendered content of a tag index and returns the items.
+ *
+ * @param {string} content Rendered content.
+ * @return {any[]|Promise} Area items (or promise resolved with the items).
+ */
+ parseContent(content: string): any[] | Promise {
+ return this.tagHelper.parseFeedContent(content);
+ }
+
+ /**
+ * Get the component to use to display items.
+ *
+ * @param {Injector} injector Injector.
+ * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found.
+ */
+ getComponent(injector: Injector): any | Promise {
+ return CoreTagFeedComponent;
+ }
+}
diff --git a/src/addon/mod/imscp/components/components.module.ts b/src/addon/mod/imscp/components/components.module.ts
index 259c6c729cd..37a1cbeb251 100644
--- a/src/addon/mod/imscp/components/components.module.ts
+++ b/src/addon/mod/imscp/components/components.module.ts
@@ -20,12 +20,10 @@ import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
import { AddonModImscpIndexComponent } from './index/index';
-import { AddonModImscpTocPopoverComponent } from './toc-popover/toc-popover';
@NgModule({
declarations: [
AddonModImscpIndexComponent,
- AddonModImscpTocPopoverComponent,
],
imports: [
CommonModule,
@@ -38,12 +36,10 @@ import { AddonModImscpTocPopoverComponent } from './toc-popover/toc-popover';
providers: [
],
exports: [
- AddonModImscpIndexComponent,
- AddonModImscpTocPopoverComponent
+ AddonModImscpIndexComponent
],
entryComponents: [
- AddonModImscpIndexComponent,
- AddonModImscpTocPopoverComponent
+ AddonModImscpIndexComponent
]
})
export class AddonModImscpComponentsModule {}
diff --git a/src/addon/mod/imscp/components/index/index.ts b/src/addon/mod/imscp/components/index/index.ts
index 11e53d2b2e2..3b793da191d 100644
--- a/src/addon/mod/imscp/components/index/index.ts
+++ b/src/addon/mod/imscp/components/index/index.ts
@@ -13,13 +13,12 @@
// limitations under the License.
import { Component, Injector } from '@angular/core';
-import { PopoverController } from 'ionic-angular';
+import { ModalController } from 'ionic-angular';
import { CoreAppProvider } from '@providers/app';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseModuleMainResourceComponent } from '@core/course/classes/main-resource-component';
import { AddonModImscpProvider } from '../../providers/imscp';
import { AddonModImscpPrefetchHandler } from '../../providers/prefetch-handler';
-import { AddonModImscpTocPopoverComponent } from '../../components/toc-popover/toc-popover';
/**
* Component that displays a IMSCP.
@@ -40,7 +39,7 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
nextItem = '';
constructor(injector: Injector, private imscpProvider: AddonModImscpProvider, private courseProvider: CoreCourseProvider,
- private appProvider: CoreAppProvider, private popoverCtrl: PopoverController,
+ private appProvider: CoreAppProvider, private modalCtrl: ModalController,
private imscpPrefetch: AddonModImscpPrefetchHandler) {
super(injector);
}
@@ -148,17 +147,23 @@ export class AddonModImscpIndexComponent extends CoreCourseModuleMainResourceCom
* @param {MouseEvent} event Event.
*/
showToc(event: MouseEvent): void {
- const popover = this.popoverCtrl.create(AddonModImscpTocPopoverComponent, { items: this.items });
-
- popover.onDidDismiss((itemId) => {
- if (!itemId) {
- // Not valid, probably a category.
- return;
+ // Create the toc modal.
+ const modal = this.modalCtrl.create('AddonModImscpTocPage', {
+ items: this.items,
+ selected: this.currentItem
+ }, { cssClass: 'core-modal-lateral',
+ showBackdrop: true,
+ enableBackdropDismiss: true,
+ enterAnimation: 'core-modal-lateral-transition',
+ leaveAnimation: 'core-modal-lateral-transition' });
+
+ modal.onDidDismiss((itemId) => {
+ if (itemId) {
+ this.loadItem(itemId);
}
- this.loadItem(itemId);
});
- popover.present({
+ modal.present({
ev: event
});
}
diff --git a/src/addon/mod/imscp/components/toc-popover/addon-mod-imscp-toc-popover.html b/src/addon/mod/imscp/components/toc-popover/addon-mod-imscp-toc-popover.html
deleted file mode 100644
index 6bda8d59448..00000000000
--- a/src/addon/mod/imscp/components/toc-popover/addon-mod-imscp-toc-popover.html
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
- {{item.title}}
-
-
diff --git a/src/addon/mod/imscp/lang/en.json b/src/addon/mod/imscp/lang/en.json
index 4abb95089a5..441eea59866 100644
--- a/src/addon/mod/imscp/lang/en.json
+++ b/src/addon/mod/imscp/lang/en.json
@@ -1,5 +1,6 @@
{
"deploymenterror": "Content package error!",
"modulenameplural": "IMS content packages",
- "showmoduledescription": "Show description"
+ "showmoduledescription": "Show description",
+ "toc": "TOC"
}
\ No newline at end of file
diff --git a/src/addon/mod/imscp/pages/toc/toc.html b/src/addon/mod/imscp/pages/toc/toc.html
new file mode 100644
index 00000000000..035a5d6a067
--- /dev/null
+++ b/src/addon/mod/imscp/pages/toc/toc.html
@@ -0,0 +1,19 @@
+
+
+ {{ 'addon.mod_imscp.toc' | translate }}
+
+
+
+
+
+
+
+
+
+
+
+ {{item.title}}
+
+
+
+
diff --git a/src/addon/mod/imscp/pages/toc/toc.module.ts b/src/addon/mod/imscp/pages/toc/toc.module.ts
new file mode 100644
index 00000000000..28f970142eb
--- /dev/null
+++ b/src/addon/mod/imscp/pages/toc/toc.module.ts
@@ -0,0 +1,31 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { NgModule } from '@angular/core';
+import { IonicPageModule } from 'ionic-angular';
+import { TranslateModule } from '@ngx-translate/core';
+import { CoreDirectivesModule } from '@directives/directives.module';
+import { AddonModImscpTocPage } from './toc';
+
+@NgModule({
+ declarations: [
+ AddonModImscpTocPage,
+ ],
+ imports: [
+ CoreDirectivesModule,
+ IonicPageModule.forChild(AddonModImscpTocPage),
+ TranslateModule.forChild()
+ ],
+})
+export class AddonModImscpTocPageModule {}
diff --git a/src/addon/mod/imscp/components/toc-popover/toc-popover.ts b/src/addon/mod/imscp/pages/toc/toc.ts
similarity index 73%
rename from src/addon/mod/imscp/components/toc-popover/toc-popover.ts
rename to src/addon/mod/imscp/pages/toc/toc.ts
index 115c1241f22..1880e4813ec 100644
--- a/src/addon/mod/imscp/components/toc-popover/toc-popover.ts
+++ b/src/addon/mod/imscp/pages/toc/toc.ts
@@ -13,20 +13,23 @@
// limitations under the License.
import { Component } from '@angular/core';
-import { NavParams, ViewController } from 'ionic-angular';
+import { IonicPage, NavParams, ViewController } from 'ionic-angular';
/**
- * Component to display the TOC of a IMSCP.
+ * Modal to display the TOC of a imscp.
*/
+@IonicPage({ segment: 'addon-mod-imscp-toc-modal' })
@Component({
- selector: 'addon-mod-imscp-toc-popover',
- templateUrl: 'addon-mod-imscp-toc-popover.html'
+ selector: 'page-addon-mod-imscp-toc',
+ templateUrl: 'toc.html'
})
-export class AddonModImscpTocPopoverComponent {
+export class AddonModImscpTocPage {
items = [];
+ selected: string;
constructor(navParams: NavParams, private viewCtrl: ViewController) {
this.items = navParams.get('items') || [];
+ this.selected = navParams.get('selected');
}
/**
@@ -47,4 +50,11 @@ export class AddonModImscpTocPopoverComponent {
getNumberForPadding(n: number): number[] {
return new Array(n);
}
+
+ /**
+ * Close modal.
+ */
+ closeModal(): void {
+ this.viewCtrl.dismiss();
+ }
}
diff --git a/src/addon/mod/label/label.scss b/src/addon/mod/label/label.scss
index 9f6514ba6cc..46d2a860030 100644
--- a/src/addon/mod/label/label.scss
+++ b/src/addon/mod/label/label.scss
@@ -1,4 +1,4 @@
-a.core-course-module-handler.addon-mod-label-handler {
+.item.core-course-module-handler.addon-mod-label-handler {
align-items: center;
&:hover {
@@ -6,14 +6,14 @@ a.core-course-module-handler.addon-mod-label-handler {
}
}
-.md a.core-course-module-handler.addon-mod-label-handler .item-inner {
+.md .item.core-course-module-handler.addon-mod-label-handler .item-inner {
padding-bottom: $item-md-padding-bottom;
}
-.ios a.core-course-module-handler.addon-mod-label-handler .item-inner {
+.ios .item.core-course-module-handler.addon-mod-label-handler .item-inner {
padding-bottom: $item-ios-padding-bottom;
}
-.wp a.core-course-module-handler.addon-mod-label-handler .item-inner {
+.wp .item.core-course-module-handler.addon-mod-label-handler .item-inner {
padding-bottom: $item-wp-padding-bottom;
}
diff --git a/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html b/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html
index 55cc163c85d..655de63019e 100644
--- a/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html
+++ b/src/addon/mod/lesson/components/index/addon-mod-lesson-index.html
@@ -16,7 +16,7 @@
-
+
diff --git a/src/addon/mod/lesson/components/index/index.ts b/src/addon/mod/lesson/components/index/index.ts
index 8a0acdba1c3..b179e79937b 100644
--- a/src/addon/mod/lesson/components/index/index.ts
+++ b/src/addon/mod/lesson/components/index/index.ts
@@ -223,7 +223,7 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
return this.groupsProvider.getActivityGroupInfo(this.module.id).then((groupInfo) => {
this.groupInfo = groupInfo;
- return this.setGroup(this.group || 0);
+ return this.setGroup(this.groupsProvider.validateGroupId(this.group, groupInfo));
}).finally(() => {
this.reportLoaded = true;
});
@@ -384,10 +384,19 @@ export class AddonModLessonIndexComponent extends CoreCourseModuleMainActivityCo
});
}
+ /**
+ * First tab selected.
+ */
+ indexSelected(): void {
+ this.selectedTab = 0;
+ }
+
/**
* Reports tab selected.
*/
reportsSelected(): void {
+ this.selectedTab = 1;
+
if (!this.groupInfo) {
this.fetchReportData().catch((error) => {
this.domUtils.showErrorModalDefault(error, 'Error getting report.');
diff --git a/src/addon/mod/lesson/pages/player/player.scss b/src/addon/mod/lesson/pages/player/player.scss
index e1ac00eb4fc..cb4a4caf9c9 100644
--- a/src/addon/mod/lesson/pages/player/player.scss
+++ b/src/addon/mod/lesson/pages/player/player.scss
@@ -11,7 +11,6 @@ ion-app.app-root page-addon-mod-lesson-player {
}
.addon-mod_lesson-pagebuttons .button-block {
- contain: content;
height: 100%;
display: flex;
flex-direction: column;
diff --git a/src/addon/mod/lesson/pages/player/player.ts b/src/addon/mod/lesson/pages/player/player.ts
index ab32eb9d670..a19c4e48fcc 100644
--- a/src/addon/mod/lesson/pages/player/player.ts
+++ b/src/addon/mod/lesson/pages/player/player.ts
@@ -25,6 +25,7 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreUrlUtilsProvider } from '@providers/utils/url';
import { CoreUtilsProvider } from '@providers/utils/utils';
+import { MoodleMobileApp } from '../../../../../app/app.component';
import { AddonModLessonProvider } from '../../providers/lesson';
import { AddonModLessonOfflineProvider } from '../../providers/lesson-offline';
import { AddonModLessonSyncProvider } from '../../providers/lesson-sync';
@@ -85,7 +86,8 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy {
protected lessonHelper: AddonModLessonHelperProvider, protected lessonSync: AddonModLessonSyncProvider,
protected lessonOfflineProvider: AddonModLessonOfflineProvider, protected cdr: ChangeDetectorRef,
modalCtrl: ModalController, protected navCtrl: NavController, protected appProvider: CoreAppProvider,
- protected utils: CoreUtilsProvider, protected urlUtils: CoreUrlUtilsProvider, protected fb: FormBuilder) {
+ protected utils: CoreUtilsProvider, protected urlUtils: CoreUrlUtilsProvider, protected fb: FormBuilder,
+ protected mmApp: MoodleMobileApp) {
this.lessonId = navParams.get('lessonId');
this.courseId = navParams.get('courseId');
@@ -145,6 +147,13 @@ export class AddonModLessonPlayerPage implements OnInit, OnDestroy {
return Promise.resolve();
}
+ /**
+ * Runs when the page is about to leave and no longer be the active page.
+ */
+ ionViewWillLeave(): void {
+ this.mmApp.closeModal();
+ }
+
/**
* A button was clicked.
*
diff --git a/src/addon/mod/lesson/pages/user-retake/user-retake.ts b/src/addon/mod/lesson/pages/user-retake/user-retake.ts
index 8a69c73ded2..394f781761a 100644
--- a/src/addon/mod/lesson/pages/user-retake/user-retake.ts
+++ b/src/addon/mod/lesson/pages/user-retake/user-retake.ts
@@ -19,6 +19,7 @@ import { CoreSitesProvider } from '@providers/sites';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
+import { CoreUtilsProvider } from '@providers/utils/utils';
import { CoreUserProvider } from '@core/user/providers/user';
import { AddonModLessonProvider } from '../../providers/lesson';
import { AddonModLessonHelperProvider } from '../../providers/helper';
@@ -44,11 +45,13 @@ export class AddonModLessonUserRetakePage implements OnInit {
protected lessonId: number; // The lesson ID the retake belongs to.
protected userId: number; // User ID to see the retakes.
protected retakeNumber: number; // Number of the initial retake to see.
+ protected previousSelectedRetake: number; // To be able to detect the previous selected retake when it has changed.
constructor(navParams: NavParams, sitesProvider: CoreSitesProvider, protected textUtils: CoreTextUtilsProvider,
protected translate: TranslateService, protected domUtils: CoreDomUtilsProvider,
protected userProvider: CoreUserProvider, protected timeUtils: CoreTimeUtilsProvider,
- protected lessonProvider: AddonModLessonProvider, protected lessonHelper: AddonModLessonHelperProvider) {
+ protected lessonProvider: AddonModLessonProvider, protected lessonHelper: AddonModLessonHelperProvider,
+ protected utils: CoreUtilsProvider) {
this.lessonId = navParams.get('lessonId');
this.courseId = navParams.get('courseId');
@@ -75,7 +78,8 @@ export class AddonModLessonUserRetakePage implements OnInit {
this.loaded = false;
this.setRetake(retakeNumber).catch((error) => {
- this.domUtils.showErrorModalDefault(error, 'Error getting attempt.');
+ this.selectedRetake = this.previousSelectedRetake;
+ this.domUtils.showErrorModal(this.utils.addDataNotDownloadedError(error, 'Error getting attempt.'));
}).finally(() => {
this.loaded = true;
});
@@ -128,7 +132,7 @@ export class AddonModLessonUserRetakePage implements OnInit {
student.bestgrade = this.textUtils.roundToDecimals(student.bestgrade, 2);
student.attempts.forEach((retake) => {
- if (this.retakeNumber == retake.try) {
+ if (!this.selectedRetake && this.retakeNumber == retake.try) {
// The retake specified as parameter exists. Use it.
this.selectedRetake = this.retakeNumber;
}
@@ -223,6 +227,7 @@ export class AddonModLessonUserRetakePage implements OnInit {
}
this.retake = data;
+ this.previousSelectedRetake = this.selectedRetake;
});
}
}
diff --git a/src/addon/mod/lesson/providers/grade-link-handler.ts b/src/addon/mod/lesson/providers/grade-link-handler.ts
index f71c50586c6..f76eb8b52ed 100644
--- a/src/addon/mod/lesson/providers/grade-link-handler.ts
+++ b/src/addon/mod/lesson/providers/grade-link-handler.ts
@@ -70,7 +70,7 @@ export class AddonModLessonGradeLinkHandler extends CoreContentLinksModuleGradeH
this.linkHelper.goInSite(navCtrl, 'AddonModLessonUserRetakePage', pageParams, siteId);
} else {
// User cannot view the report, go to lesson index.
- this.courseHelper.navigateToModule(moduleId, siteId, courseId, module.section);
+ this.courseHelper.navigateToModule(moduleId, siteId, courseId, module.section, undefined, undefined, navCtrl);
}
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'core.course.errorgetmodule', true);
diff --git a/src/addon/mod/lesson/providers/index-link-handler.ts b/src/addon/mod/lesson/providers/index-link-handler.ts
index 244c55fcd5b..16d77cd6e40 100644
--- a/src/addon/mod/lesson/providers/index-link-handler.ts
+++ b/src/addon/mod/lesson/providers/index-link-handler.ts
@@ -19,6 +19,7 @@ import { CoreContentLinksAction } from '@core/contentlinks/providers/delegate';
import { CoreCourseProvider } from '@core/course/providers/course';
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
import { AddonModLessonProvider } from './lesson';
+import { NavController } from 'ionic-angular';
/**
* Handler to treat links to lesson index.
@@ -51,9 +52,10 @@ export class AddonModLessonIndexLinkHandler extends CoreContentLinksModuleIndexH
/* Ignore the pageid param. If we open the lesson player with a certain page and the user hasn't started
the lesson, an error is thrown: could not find lesson_timer records. */
if (params.userpassword) {
- this.navigateToModuleWithPassword(parseInt(params.id, 10), courseId, params.userpassword, siteId);
+ this.navigateToModuleWithPassword(parseInt(params.id, 10), courseId, params.userpassword, siteId, navCtrl);
} else {
- this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId);
+ this.courseHelper.navigateToModule(parseInt(params.id, 10), siteId, courseId,
+ undefined, undefined, undefined, navCtrl);
}
}
}];
@@ -80,9 +82,11 @@ export class AddonModLessonIndexLinkHandler extends CoreContentLinksModuleIndexH
* @param {number} courseId Course ID.
* @param {string} password Password.
* @param {string} siteId Site ID.
+ * @param {NavController} navCtrl Navigation controller.
* @return {Promise} Promise resolved when navigated.
*/
- protected navigateToModuleWithPassword(moduleId: number, courseId: number, password: string, siteId: string): Promise {
+ protected navigateToModuleWithPassword(moduleId: number, courseId: number, password: string, siteId: string,
+ navCtrl?: NavController): Promise {
const modal = this.domUtils.showModalLoading();
// Get the module.
@@ -93,11 +97,12 @@ export class AddonModLessonIndexLinkHandler extends CoreContentLinksModuleIndexH
return this.lessonProvider.storePassword(parseInt(module.instance, 10), password, siteId).catch(() => {
// Ignore errors.
}).then(() => {
- return this.courseHelper.navigateToModule(moduleId, siteId, courseId, module.section);
+ return this.courseHelper.navigateToModule(moduleId, siteId, courseId, module.section,
+ undefined, undefined, navCtrl);
});
}).catch(() => {
// Error, go to index page.
- return this.courseHelper.navigateToModule(moduleId, siteId, courseId);
+ return this.courseHelper.navigateToModule(moduleId, siteId, courseId, undefined, undefined, undefined, navCtrl);
}).finally(() => {
modal.dismiss();
});
diff --git a/src/addon/mod/lesson/providers/prefetch-handler.ts b/src/addon/mod/lesson/providers/prefetch-handler.ts
index a594a7493b7..bbd7666c66c 100644
--- a/src/addon/mod/lesson/providers/prefetch-handler.ts
+++ b/src/addon/mod/lesson/providers/prefetch-handler.ts
@@ -366,11 +366,10 @@ export class AddonModLessonPrefetchHandler extends CoreCourseActivityPrefetchHan
if (accessInfo.canviewreports) {
// Prefetch reports data.
- promises.push(this.groupsProvider.getActivityAllowedGroupsIfEnabled(module.id, undefined, siteId, true)
- .then((groups) => {
+ promises.push(this.groupsProvider.getActivityGroupInfo(module.id, false, undefined, siteId, true).then((info) => {
const subPromises = [];
- groups.forEach((group) => {
+ info.groups.forEach((group) => {
subPromises.push(this.lessonProvider.getRetakesOverview(lesson.id, group.id, false, true, siteId));
});
diff --git a/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html b/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html
index bb204717c70..7f7cb5b7444 100644
--- a/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html
+++ b/src/addon/mod/quiz/components/index/addon-mod-quiz-index.html
@@ -90,7 +90,7 @@ {{ 'addon.mod_quiz.summaryofattempts' | translate }}
{{ 'addon.mod_quiz.noquestions' | translate }}
-
+
{{ 'addon.mod_quiz.errorquestionsnotsupported' | translate }}
{{ type }}
@@ -109,6 +109,13 @@ {{ 'addon.mod_quiz.summaryofattempts' | translate }}
{{ 'core.hasdatatosync' | translate: {$a: moduleName} }}
+
+
+ {{ 'addon.mod_quiz.canattemptbutnotsubmit' |Â translate }}
+ {{ 'addon.mod_quiz.warningquestionsnotsupported' | translate }}
+ {{ type }}
+
+
@@ -117,7 +124,7 @@ {{ 'addon.mod_quiz.summaryofattempts' | translate }}
-
+
{{ 'core.openinbrowser' | translate }}
diff --git a/src/addon/mod/quiz/components/index/index.ts b/src/addon/mod/quiz/components/index/index.ts
index 32eef2714e3..cabdb8406c0 100644
--- a/src/addon/mod/quiz/components/index/index.ts
+++ b/src/addon/mod/quiz/components/index/index.ts
@@ -38,6 +38,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
now: number; // Current time.
syncTime: string; // Last synchronization time.
hasOffline: boolean; // Whether the quiz has offline data.
+ hasSupportedQuestions: boolean; // Whether the quiz has at least 1 supported question.
accessRules: string[]; // List of access rules of the quiz.
unsupportedRules: string[]; // List of unsupported access rules of the quiz.
unsupportedQuestions: string[]; // List of unsupported question types of the quiz.
@@ -214,6 +215,9 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
// Get question types in the quiz.
return this.quizProvider.getQuizRequiredQtypes(this.quizData.id).then((types) => {
this.unsupportedQuestions = this.quizProvider.getUnsupportedQuestions(types);
+ this.hasSupportedQuestions = !!types.find((type) => {
+ return type != 'random' && this.unsupportedQuestions.indexOf(type) == -1;
+ });
return this.getAttempts();
});
@@ -301,7 +305,7 @@ export class AddonModQuizIndexComponent extends CoreCourseModuleMainActivityComp
this.buttonText = '';
} else if (this.quizAccessInfo.canattempt && this.preventMessages.length) {
this.buttonText = '';
- } else if (this.unsupportedQuestions.length || this.unsupportedRules.length || !this.behaviourSupported) {
+ } else if (!this.hasSupportedQuestions || this.unsupportedRules.length || !this.behaviourSupported) {
this.buttonText = '';
}
}
diff --git a/src/addon/mod/quiz/lang/en.json b/src/addon/mod/quiz/lang/en.json
index c5be9879f18..3a0b0431e4f 100644
--- a/src/addon/mod/quiz/lang/en.json
+++ b/src/addon/mod/quiz/lang/en.json
@@ -5,6 +5,7 @@
"attemptnumber": "Attempt",
"attemptquiznow": "Attempt quiz now",
"attemptstate": "State",
+ "canattemptbutnotsubmit": "You can attempt this quiz in the app, but you will need to submit the attempt in browser for the following reasons:",
"cannotsubmitquizdueto": "This quiz attempt cannot be submitted for the following reasons:",
"clearchoice": "Clear my choice",
"comment": "Comment",
@@ -23,7 +24,7 @@
"errorgetquestions": "Error getting questions.",
"errorgetquiz": "Error getting quiz data.",
"errorparsequestions": "An error occurred while reading the questions. Please attempt this quiz in a web browser.",
- "errorquestionsnotsupported": "This quiz can't be attempted in the app because it contains questions not supported by the app:",
+ "errorquestionsnotsupported": "This quiz can't be attempted in the app because it only contains questions not supported by the app:",
"errorrulesnotsupported": "This quiz can't be attempted in the app because it has access rules not supported by the app:",
"errorsaveattempt": "An error occurred while saving the attempt data.",
"feedback": "Feedback",
@@ -77,5 +78,6 @@
"warningattemptfinished": "Offline attempt discarded as it was finished on the site or not found.",
"warningdatadiscarded": "Some offline answers were discarded because the questions were modified online.",
"warningdatadiscardedfromfinished": "Attempt unfinished because some offline answers were discarded. Please review your answers then resubmit the attempt.",
+ "warningquestionsnotsupported": "This quiz contains questions not supported by the app:",
"yourfinalgradeis": "Your final grade for this quiz is {{$a}}."
}
\ No newline at end of file
diff --git a/src/addon/mod/quiz/pages/player/player.ts b/src/addon/mod/quiz/pages/player/player.ts
index 75da900d873..af65f378a99 100644
--- a/src/addon/mod/quiz/pages/player/player.ts
+++ b/src/addon/mod/quiz/pages/player/player.ts
@@ -23,6 +23,7 @@ import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { CoreQuestionHelperProvider } from '@core/question/providers/helper';
import { CoreQuestionComponent } from '@core/question/components/question/question';
+import { MoodleMobileApp } from '../../../../../app/app.component';
import { AddonModQuizProvider } from '../../providers/quiz';
import { AddonModQuizSyncProvider } from '../../providers/quiz-sync';
import { AddonModQuizHelperProvider } from '../../providers/helper';
@@ -80,7 +81,7 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
protected timeUtils: CoreTimeUtilsProvider, protected quizProvider: AddonModQuizProvider,
protected quizHelper: AddonModQuizHelperProvider, protected quizSync: AddonModQuizSyncProvider,
protected questionHelper: CoreQuestionHelperProvider, protected cdr: ChangeDetectorRef,
- modalCtrl: ModalController, protected navCtrl: NavController) {
+ modalCtrl: ModalController, protected navCtrl: NavController, protected mmApp: MoodleMobileApp) {
this.quizId = navParams.get('quizId');
this.courseId = navParams.get('courseId');
@@ -157,6 +158,13 @@ export class AddonModQuizPlayerPage implements OnInit, OnDestroy {
return Promise.resolve();
}
+ /**
+ * Runs when the page is about to leave and no longer be the active page.
+ */
+ ionViewWillLeave(): void {
+ this.mmApp.closeModal();
+ }
+
/**
* Abort the quiz.
*/
diff --git a/src/addon/mod/quiz/providers/quiz.ts b/src/addon/mod/quiz/providers/quiz.ts
index cb36b5928b5..114963f9e5c 100644
--- a/src/addon/mod/quiz/providers/quiz.ts
+++ b/src/addon/mod/quiz/providers/quiz.ts
@@ -649,10 +649,18 @@ export class AddonModQuizProvider {
const messages = [];
questions.forEach((question) => {
- let message = this.questionDelegate.getPreventSubmitMessage(question);
- if (message) {
- message = this.translate.instant(message);
- messages.push(this.translate.instant('core.question.questionmessage', {$a: question.slot, $b: message}));
+ if (question.type != 'random' && !this.questionDelegate.isQuestionSupported(question.type)) {
+ // The question isn't supported.
+ messages.push(this.translate.instant('core.question.questionmessage', {
+ $a: question.slot,
+ $b: this.translate.instant('core.question.errorquestionnotsupported', {$a: question.type})
+ }));
+ } else {
+ let message = this.questionDelegate.getPreventSubmitMessage(question);
+ if (message) {
+ message = this.translate.instant(message);
+ messages.push(this.translate.instant('core.question.questionmessage', {$a: question.slot, $b: message}));
+ }
}
});
diff --git a/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html b/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html
index 20ca8751fbb..5aeb8dec523 100644
--- a/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html
+++ b/src/addon/mod/scorm/components/index/addon-mod-scorm-index.html
@@ -31,38 +31,38 @@ {{ 'addon.mod_scorm.attempts' | translate }}
= 0">
- {{ 'addon.mod_scorm.noattemptsallowed' | translate }}
- {{ 'core.unlimited' |Â translate }}
- 0">{{ scorm.maxattempt }}
+ {{ 'addon.mod_scorm.noattemptsallowed' | translate }}
+ {{ 'core.unlimited' |Â translate }}
+ 0">{{ scorm.maxattempt }}
= 0">
- {{ 'addon.mod_scorm.noattemptsmade' | translate }}
- {{ scorm.numAttempts }}
+ {{ 'addon.mod_scorm.noattemptsmade' | translate }}
+ {{ scorm.numAttempts }}
- {{ 'addon.mod_scorm.gradeforattempt' | translate }} {{attempt.number}}
- {{ attempt.grade }}
- {{ 'addon.mod_scorm.cannotcalculategrade' | translate }}
+ {{ 'addon.mod_scorm.gradeforattempt' | translate }} {{attempt.number}}
+ {{ attempt.grade }}
+ {{ 'addon.mod_scorm.cannotcalculategrade' | translate }}
- {{ 'addon.mod_scorm.gradeforattempt' | translate }} {{attempt.number}}
- {{ attempt.grade }}
- {{ 'addon.mod_scorm.cannotcalculategrade' | translate }}
- {{ 'addon.mod_scorm.offlineattemptnote' | translate }}
- scorm.maxattempt">{{ 'addon.mod_scorm.offlineattemptovermax' | translate }}
+ {{ 'addon.mod_scorm.gradeforattempt' | translate }} {{attempt.number}}
+ {{ attempt.grade }}
+ {{ 'addon.mod_scorm.cannotcalculategrade' | translate }}
+ {{ 'addon.mod_scorm.offlineattemptnote' | translate }}
+ scorm.maxattempt">{{ 'addon.mod_scorm.offlineattemptovermax' | translate }}
- {{ 'addon.mod_scorm.grademethod' | translate }}
- {{ scorm.gradeMethodReadable }}
+ {{ 'addon.mod_scorm.grademethod' | translate }}
+ {{ scorm.gradeMethodReadable }}
- {{ 'addon.mod_scorm.gradereported' | translate }}
- {{ scorm.grade }}
- {{ 'addon.mod_scorm.cannotcalculategrade' | translate }}
+ {{ 'addon.mod_scorm.gradereported' | translate }}
+ {{ scorm.grade }}
+ {{ 'addon.mod_scorm.cannotcalculategrade' | translate }}
- {{ 'core.lastsync' | translate }}
+ {{ 'core.lastsync' | translate }}
{{ syncTime }}
@@ -130,7 +130,7 @@ {{ 'addon.mod_scorm.contents' | translate }}
- {{ 'addon.mod_scorm.mode' |Â translate }}
+ {{ 'addon.mod_scorm.mode' |Â translate }}
{{ 'addon.mod_scorm.browse' |Â translate }}
diff --git a/src/addon/mod/scorm/pages/player/player.ts b/src/addon/mod/scorm/pages/player/player.ts
index 63d148aca83..93024042c75 100644
--- a/src/addon/mod/scorm/pages/player/player.ts
+++ b/src/addon/mod/scorm/pages/player/player.ts
@@ -270,24 +270,31 @@ export class AddonModScormPlayerPage implements OnInit, OnDestroy {
sco.image = this.scormProvider.getScoStatusIcon(sco, this.scorm.incomplete);
});
- // Determine current SCO if we received an ID..
- if (this.initialScoId > 0) {
- // SCO set by parameter, get it from TOC.
- this.currentSco = this.scormHelper.getScoFromToc(this.toc, this.initialScoId);
- }
-
if (!this.currentSco) {
- // No SCO defined. Get the first valid one.
- return this.scormHelper.getFirstSco(this.scorm.id, this.attempt, this.toc, this.organizationId, this.offline)
- .then((sco) => {
-
- if (sco) {
- this.currentSco = sco;
- } else {
- // We couldn't find a SCO to load: they're all inactive or without launch URL.
- this.errorMessage = 'addon.mod_scorm.errornovalidsco';
- }
- });
+ if (this.newAttempt) {
+ // Creating a new attempt, use the first SCO defined by the SCORM.
+ this.initialScoId = this.scorm.launch;
+ }
+
+ // Determine current SCO if we received an ID.
+ if (this.initialScoId > 0) {
+ // SCO set by parameter, get it from TOC.
+ this.currentSco = this.scormHelper.getScoFromToc(this.toc, this.initialScoId);
+ }
+
+ if (!this.currentSco) {
+ // No SCO defined. Get the first valid one.
+ return this.scormHelper.getFirstSco(this.scorm.id, this.attempt, this.toc, this.organizationId, this.mode,
+ this.offline).then((sco) => {
+
+ if (sco) {
+ this.currentSco = sco;
+ } else {
+ // We couldn't find a SCO to load: they're all inactive or without launch URL.
+ this.errorMessage = 'addon.mod_scorm.errornovalidsco';
+ }
+ });
+ }
}
}).finally(() => {
this.loadingToc = false;
diff --git a/src/addon/mod/scorm/providers/helper.ts b/src/addon/mod/scorm/providers/helper.ts
index 95dd3c31b9d..ec0e1523b92 100644
--- a/src/addon/mod/scorm/providers/helper.ts
+++ b/src/addon/mod/scorm/providers/helper.ts
@@ -199,12 +199,15 @@ export class AddonModScormHelperProvider {
* @param {number} attempt Attempt number.
* @param {any[]} [toc] SCORM's TOC. If not provided, it will be calculated.
* @param {string} [organization] Organization to use.
+ * @param {string} [mode] Mode.
* @param {boolean} [offline] Whether the attempt is offline.
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise} Promise resolved with the first SCO.
*/
- getFirstSco(scormId: number, attempt: number, toc?: any[], organization?: string, offline?: boolean, siteId?: string)
- : Promise {
+ getFirstSco(scormId: number, attempt: number, toc?: any[], organization?: string, mode?: string, offline?: boolean,
+ siteId?: string): Promise {
+
+ mode = mode || AddonModScormProvider.MODENORMAL;
let promise;
if (toc && toc.length) {
@@ -215,15 +218,20 @@ export class AddonModScormHelperProvider {
}
return promise.then((scos) => {
+
// Search the first valid SCO.
for (let i = 0; i < scos.length; i++) {
const sco = scos[i];
- // Return the first valid and incomplete SCO.
- if (sco.isvisible && sco.prereq && sco.launch && this.scormProvider.isStatusIncomplete(sco.status)) {
+ if (sco.isvisible && sco.launch && sco.prereq &&
+ (mode != AddonModScormProvider.MODENORMAL || this.scormProvider.isStatusIncomplete(sco.status))) {
+ // In browse/review mode return the first visible sco. In normal mode, first incomplete sco.
return sco;
}
}
+
+ // No "valid" SCO, load the first one. In web it loads the first child because the toc contains the organization SCO.
+ return scos[0];
});
}
diff --git a/src/addon/mod/wiki/components/components.module.ts b/src/addon/mod/wiki/components/components.module.ts
index 39372cfe232..638be75aa64 100644
--- a/src/addon/mod/wiki/components/components.module.ts
+++ b/src/addon/mod/wiki/components/components.module.ts
@@ -19,6 +19,7 @@ import { TranslateModule } from '@ngx-translate/core';
import { CoreComponentsModule } from '@components/components.module';
import { CoreDirectivesModule } from '@directives/directives.module';
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
+import { CoreTagComponentsModule } from '@core/tag/components/components.module';
import { AddonModWikiIndexComponent } from './index/index';
import { AddonModWikiSubwikiPickerComponent } from './subwiki-picker/subwiki-picker';
@@ -33,7 +34,8 @@ import { AddonModWikiSubwikiPickerComponent } from './subwiki-picker/subwiki-pic
TranslateModule.forChild(),
CoreComponentsModule,
CoreDirectivesModule,
- CoreCourseComponentsModule
+ CoreCourseComponentsModule,
+ CoreTagComponentsModule
],
providers: [
],
diff --git a/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html b/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html
index 3d883459522..2ee9b5e8dd6 100644
--- a/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html
+++ b/src/addon/mod/wiki/components/index/addon-mod-wiki-index.html
@@ -50,6 +50,11 @@
+
+ 0">
+ {{ 'core.tag.tags' | translate }}:
+
+
diff --git a/src/addon/mod/wiki/components/index/index.ts b/src/addon/mod/wiki/components/index/index.ts
index 941417a6868..cb91ef07282 100644
--- a/src/addon/mod/wiki/components/index/index.ts
+++ b/src/addon/mod/wiki/components/index/index.ts
@@ -23,6 +23,7 @@ import { AddonModWikiOfflineProvider } from '../../providers/wiki-offline';
import { AddonModWikiSyncProvider } from '../../providers/wiki-sync';
import { CoreTabsComponent } from '@components/tabs/tabs';
import { AddonModWikiSubwikiPickerComponent } from '../../components/subwiki-picker/subwiki-picker';
+import { CoreTagProvider } from '@core/tag/providers/tag';
/**
* Component that displays a wiki entry page.
@@ -64,6 +65,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
subwikis: [],
count: 0
};
+ tagsEnabled: boolean;
protected syncEventName = AddonModWikiSyncProvider.AUTO_SYNCED;
protected currentSubwiki: any; // Current selected subwiki.
@@ -81,10 +83,12 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
constructor(injector: Injector, protected wikiProvider: AddonModWikiProvider, @Optional() protected content: Content,
protected wikiOffline: AddonModWikiOfflineProvider, protected wikiSync: AddonModWikiSyncProvider,
protected navCtrl: NavController, protected utils: CoreUtilsProvider, protected groupsProvider: CoreGroupsProvider,
- protected userProvider: CoreUserProvider, private popoverCtrl: PopoverController) {
+ protected userProvider: CoreUserProvider, private popoverCtrl: PopoverController,
+ private tagProvider: CoreTagProvider) {
super(injector, content);
this.pageStr = this.translate.instant('addon.mod_wiki.wikipage');
+ this.tagsEnabled = this.tagProvider.areTagsAvailableInSite();
}
/**
@@ -265,44 +269,34 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp
this.componentId = this.module.id;
// Get real groupmode, in case it's forced by the course.
- return this.groupsProvider.getActivityGroupMode(this.wiki.coursemodule).then((groupMode) => {
-
- if (groupMode === CoreGroupsProvider.SEPARATEGROUPS || groupMode === CoreGroupsProvider.VISIBLEGROUPS) {
- // Get the groups available for the user.
- promise = this.groupsProvider.getActivityAllowedGroups(this.wiki.coursemodule);
- } else {
- promise = Promise.resolve([]);
- }
-
- return promise.then((userGroups) => {
- return this.fetchSubwikis(this.wiki.id).then(() => {
- // Get the subwiki list data from the cache.
- const subwikiList = this.wikiProvider.getSubwikiList(this.wiki.id);
-
- if (!subwikiList) {
- // Not found in cache, create a new one.
- return this.createSubwikiList(userGroups);
- }
-
- this.subwikiData.count = subwikiList.count;
- this.setSelectedWiki(this.subwikiId, this.userId, this.groupId);
-
- // If nothing was selected using nav params, use the selected from cache.
- if (!this.isAnySubwikiSelected()) {
- this.setSelectedWiki(subwikiList.subwikiSelected, subwikiList.userSelected,
- subwikiList.groupSelected);
- }
+ return this.groupsProvider.getActivityGroupInfo(this.wiki.coursemodule).then((groupInfo) => {
+ return this.fetchSubwikis(this.wiki.id).then(() => {
+ // Get the subwiki list data from the cache.
+ const subwikiList = this.wikiProvider.getSubwikiList(this.wiki.id);
+
+ if (!subwikiList) {
+ // Not found in cache, create a new one.
+ return this.createSubwikiList(groupInfo.groups);
+ }
- this.subwikiData.subwikis = subwikiList.subwikis;
- });
- }).then(() => {
+ this.subwikiData.count = subwikiList.count;
+ this.setSelectedWiki(this.subwikiId, this.userId, this.groupId);
- if (!this.isAnySubwikiSelected() || this.subwikiData.count <= 0) {
- return Promise.reject(this.translate.instant('addon.mod_wiki.errornowikiavailable'));
+ // If nothing was selected using nav params, use the selected from cache.
+ if (!this.isAnySubwikiSelected()) {
+ this.setSelectedWiki(subwikiList.subwikiSelected, subwikiList.userSelected,
+ subwikiList.groupSelected);
}
- }).then(() => {
- return this.fetchWikiPage();
+
+ this.subwikiData.subwikis = subwikiList.subwikis;
});
+ }).then(() => {
+
+ if (!this.isAnySubwikiSelected() || this.subwikiData.count <= 0) {
+ return Promise.reject(this.translate.instant('addon.mod_wiki.errornowikiavailable'));
+ }
+ }).then(() => {
+ return this.fetchWikiPage();
});
});
}).then(() => {
diff --git a/src/addon/mod/wiki/lang/en.json b/src/addon/mod/wiki/lang/en.json
index 29ed054ce25..246965b9c61 100644
--- a/src/addon/mod/wiki/lang/en.json
+++ b/src/addon/mod/wiki/lang/en.json
@@ -14,6 +14,7 @@
"pageexists": "This page already exists.",
"pagename": "Page name",
"subwiki": "Sub-wiki",
+ "tagarea_wiki_pages": "Wiki pages",
"titleshouldnotbeempty": "The title should not be empty",
"viewpage": "View page",
"wikipage": "Wiki page",
diff --git a/src/addon/mod/wiki/providers/edit-link-handler.ts b/src/addon/mod/wiki/providers/edit-link-handler.ts
index 5c74cb4d75b..512c8c65f3d 100644
--- a/src/addon/mod/wiki/providers/edit-link-handler.ts
+++ b/src/addon/mod/wiki/providers/edit-link-handler.ts
@@ -50,7 +50,7 @@ export class AddonModWikiEditLinkHandler extends CoreContentLinksHandlerBase {
let section = '';
if (typeof params.section != 'undefined') {
- section = this.textUtils.decodeURIComponent(params.section.replace(/\+/g, ' '));
+ section = params.section.replace(/\+/g, ' ');
}
const pageParams = {
diff --git a/src/addon/mod/wiki/providers/prefetch-handler.ts b/src/addon/mod/wiki/providers/prefetch-handler.ts
index 5aaa5d158b6..5acebea4cd6 100644
--- a/src/addon/mod/wiki/providers/prefetch-handler.ts
+++ b/src/addon/mod/wiki/providers/prefetch-handler.ts
@@ -192,12 +192,7 @@ export class AddonModWikiPrefetchHandler extends CoreCourseActivityPrefetchHandl
});
// Fetch group data.
- promises.push(this.groupsProvider.getActivityGroupMode(module.id, siteId).then((groupMode) => {
- if (groupMode === CoreGroupsProvider.SEPARATEGROUPS || groupMode === CoreGroupsProvider.VISIBLEGROUPS) {
- // Get the groups available for the user.
- return this.groupsProvider.getActivityAllowedGroups(module.id, userId, siteId);
- }
- }));
+ promises.push(this.groupsProvider.getActivityGroupInfo(module.id, false, userId, siteId));
// Fetch info to provide wiki links.
promises.push(this.wikiProvider.getWiki(courseId, module.id, false, siteId).then((wiki) => {
diff --git a/src/addon/mod/wiki/providers/tag-area-handler.ts b/src/addon/mod/wiki/providers/tag-area-handler.ts
new file mode 100644
index 00000000000..0f3cab521b8
--- /dev/null
+++ b/src/addon/mod/wiki/providers/tag-area-handler.ts
@@ -0,0 +1,57 @@
+// (C) Copyright 2015 Martin Dougiamas
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { Injectable, Injector } from '@angular/core';
+import { CoreTagAreaHandler } from '@core/tag/providers/area-delegate';
+import { CoreTagHelperProvider } from '@core/tag/providers/helper';
+import { CoreTagFeedComponent } from '@core/tag/components/feed/feed';
+
+/**
+ * Handler to support tags.
+ */
+@Injectable()
+export class AddonModWikiTagAreaHandler implements CoreTagAreaHandler {
+ name = 'AddonModWikiTagAreaHandler';
+ type = 'mod_wiki/wiki_pages';
+
+ constructor(private tagHelper: CoreTagHelperProvider) {}
+
+ /**
+ * Whether or not the handler is enabled on a site level.
+ * @return {boolean|Promise} Whether or not the handler is enabled on a site level.
+ */
+ isEnabled(): boolean | Promise {
+ return true;
+ }
+
+ /**
+ * Parses the rendered content of a tag index and returns the items.
+ *
+ * @param {string} content Rendered content.
+ * @return {any[]|Promise} Area items (or promise resolved with the items).
+ */
+ parseContent(content: string): any[] | Promise {
+ return this.tagHelper.parseFeedContent(content);
+ }
+
+ /**
+ * Get the component to use to display items.
+ *
+ * @param {Injector} injector Injector.
+ * @return {any|Promise} The component (or promise resolved with component) to use, undefined if not found.
+ */
+ getComponent(injector: Injector): any | Promise {
+ return CoreTagFeedComponent;
+ }
+}
diff --git a/src/addon/mod/wiki/wiki.module.ts b/src/addon/mod/wiki/wiki.module.ts
index 539b962dd30..6396cac7490 100644
--- a/src/addon/mod/wiki/wiki.module.ts
+++ b/src/addon/mod/wiki/wiki.module.ts
@@ -17,6 +17,7 @@ import { CoreCronDelegate } from '@providers/cron';
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
+import { CoreTagAreaDelegate } from '@core/tag/providers/area-delegate';
import { AddonModWikiComponentsModule } from './components/components.module';
import { AddonModWikiProvider } from './providers/wiki';
import { AddonModWikiOfflineProvider } from './providers/wiki-offline';
@@ -29,6 +30,7 @@ import { AddonModWikiPageOrMapLinkHandler } from './providers/page-or-map-link-h
import { AddonModWikiCreateLinkHandler } from './providers/create-link-handler';
import { AddonModWikiEditLinkHandler } from './providers/edit-link-handler';
import { AddonModWikiListLinkHandler } from './providers/list-link-handler';
+import { AddonModWikiTagAreaHandler } from './providers/tag-area-handler';
import { CoreUpdateManagerProvider } from '@providers/update-manager';
// List of providers (without handlers).
@@ -55,7 +57,8 @@ export const ADDON_MOD_WIKI_PROVIDERS: any[] = [
AddonModWikiPageOrMapLinkHandler,
AddonModWikiCreateLinkHandler,
AddonModWikiEditLinkHandler,
- AddonModWikiListLinkHandler
+ AddonModWikiListLinkHandler,
+ AddonModWikiTagAreaHandler
]
})
export class AddonModWikiModule {
@@ -64,7 +67,8 @@ export class AddonModWikiModule {
cronDelegate: CoreCronDelegate, syncHandler: AddonModWikiSyncCronHandler, linksDelegate: CoreContentLinksDelegate,
indexHandler: AddonModWikiIndexLinkHandler, pageOrMapHandler: AddonModWikiPageOrMapLinkHandler,
createHandler: AddonModWikiCreateLinkHandler, editHandler: AddonModWikiEditLinkHandler,
- updateManager: CoreUpdateManagerProvider, listLinkHandler: AddonModWikiListLinkHandler) {
+ updateManager: CoreUpdateManagerProvider, listLinkHandler: AddonModWikiListLinkHandler,
+ tagAreaDelegate: CoreTagAreaDelegate, tagAreaHandler: AddonModWikiTagAreaHandler) {
moduleDelegate.registerHandler(moduleHandler);
prefetchDelegate.registerHandler(prefetchHandler);
@@ -74,6 +78,7 @@ export class AddonModWikiModule {
linksDelegate.registerHandler(createHandler);
linksDelegate.registerHandler(editHandler);
linksDelegate.registerHandler(listLinkHandler);
+ tagAreaDelegate.registerHandler(tagAreaHandler);
// Allow migrating the tables from the old app to the new schema.
updateManager.registerSiteTableMigration({
diff --git a/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html b/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html
index 2c9f9be1942..c90bb99ae47 100644
--- a/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html
+++ b/src/addon/mod/workshop/components/index/addon-mod-workshop-index.html
@@ -144,7 +144,7 @@ {{ 'addon.mod_workshop.gradesreport' | translate }}
{{ 'core.groupsseparate' | translate }}
{{ 'core.groupsvisible' | translate }}
-
+
{{groupOpt.name}}
diff --git a/src/addon/mod/workshop/components/index/index.ts b/src/addon/mod/workshop/components/index/index.ts
index e158670bf95..6e96123a247 100644
--- a/src/addon/mod/workshop/components/index/index.ts
+++ b/src/addon/mod/workshop/components/index/index.ts
@@ -201,19 +201,9 @@ export class AddonModWorkshopIndexComponent extends CoreCourseModuleMainActivity
this.access = accessData;
if (accessData.canviewallsubmissions) {
- return this.groupsProvider.getActivityGroupInfo(this.workshop.coursemodule,
- accessData.canviewallsubmissions).then((groupInfo) => {
+ return this.groupsProvider.getActivityGroupInfo(this.workshop.coursemodule).then((groupInfo) => {
this.groupInfo = groupInfo;
-
- // Check selected group is accessible.
- if (groupInfo && groupInfo.groups && groupInfo.groups.length > 0) {
- const found = groupInfo.groups.some((group) => {
- return group.id == this.group;
- });
- if (!found) {
- this.group = groupInfo.groups[0].id;
- }
- }
+ this.group = this.groupsProvider.validateGroupId(this.group, groupInfo);
});
}
}).then(() => {
diff --git a/src/addon/notes/components/list/addon-notes-list.html b/src/addon/notes/components/list/addon-notes-list.html
index 2c069ca78a8..a959b746f43 100644
--- a/src/addon/notes/components/list/addon-notes-list.html
+++ b/src/addon/notes/components/list/addon-notes-list.html
@@ -1,4 +1,7 @@
+
+
+
@@ -35,8 +38,20 @@ {{user.fullname}}
{{note.userfullname}}
- {{note.lastmodified | coreDateDayOrTime}}
- {{ 'core.notsent' | translate }}
+
+ {{note.lastmodified | coreDateDayOrTime}}
+
+
+ {{ 'core.notsent' | translate }}
+
+ {{ 'core.deletedoffline' | translate }}
+
+
+
+
+
+
+
diff --git a/src/addon/notes/components/list/list.ts b/src/addon/notes/components/list/list.ts
index 0150edbfd01..cf6f54afa39 100644
--- a/src/addon/notes/components/list/list.ts
+++ b/src/addon/notes/components/list/list.ts
@@ -14,12 +14,15 @@
import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core';
import { Content, ModalController } from 'ionic-angular';
+import { TranslateService } from '@ngx-translate/core';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreEventsProvider } from '@providers/events';
import { CoreSitesProvider } from '@providers/sites';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreUserProvider } from '@core/user/providers/user';
+import { coreSlideInOut } from '@classes/animations';
import { AddonNotesProvider } from '../../providers/notes';
+import { AddonNotesOfflineProvider } from '../../providers/notes-offline';
import { AddonNotesSyncProvider } from '../../providers/notes-sync';
/**
@@ -28,6 +31,7 @@ import { AddonNotesSyncProvider } from '../../providers/notes-sync';
@Component({
selector: 'addon-notes-list',
templateUrl: 'addon-notes-list.html',
+ animations: [coreSlideInOut]
})
export class AddonNotesListComponent implements OnInit, OnDestroy {
@Input() courseId: number;
@@ -44,11 +48,15 @@ export class AddonNotesListComponent implements OnInit, OnDestroy {
hasOffline = false;
notesLoaded = false;
user: any;
+ showDelete = false;
+ canDeleteNotes = false;
+ currentUserId: number;
constructor(private domUtils: CoreDomUtilsProvider, private textUtils: CoreTextUtilsProvider,
sitesProvider: CoreSitesProvider, eventsProvider: CoreEventsProvider, private modalCtrl: ModalController,
private notesProvider: AddonNotesProvider, private notesSync: AddonNotesSyncProvider,
- private userProvider: CoreUserProvider) {
+ private userProvider: CoreUserProvider, private translate: TranslateService,
+ private notesOffline: AddonNotesOfflineProvider) {
// Refresh data if notes are synchronized automatically.
this.syncObserver = eventsProvider.on(AddonNotesSyncProvider.AUTO_SYNCED, (data) => {
if (data.courseId == this.courseId) {
@@ -64,6 +72,8 @@ export class AddonNotesListComponent implements OnInit, OnDestroy {
this.fetchNotes(false);
}
}, sitesProvider.getCurrentSiteId());
+
+ this.currentUserId = sitesProvider.getCurrentSiteUserId();
}
/**
@@ -93,24 +103,35 @@ export class AddonNotesListComponent implements OnInit, OnDestroy {
return this.notesProvider.getNotes(this.courseId, this.userId).then((notes) => {
notes = notes[this.type + 'notes'] || [];
- this.hasOffline = notes.some((note) => note.offline);
+ return this.notesProvider.setOfflineDeletedNotes(notes, this.courseId).then((notes) => {
- if (this.userId) {
- this.notes = notes;
+ this.hasOffline = notes.some((note) => note.offline || note.deleted);
- // Get the user profile to retrieve the user image.
- return this.userProvider.getProfile(this.userId, this.courseId, true).then((user) => {
- this.user = user;
- });
- } else {
- return this.notesProvider.getNotesUserData(notes, this.courseId).then((notes) => {
+ if (this.userId) {
this.notes = notes;
- });
- }
+
+ // Get the user profile to retrieve the user image.
+ return this.userProvider.getProfile(this.userId, this.courseId, true).then((user) => {
+ this.user = user;
+ });
+ } else {
+ return this.notesProvider.getNotesUserData(notes, this.courseId).then((notes) => {
+ this.notes = notes;
+ });
+ }
+ });
});
}).catch((message) => {
this.domUtils.showErrorModal(message);
}).finally(() => {
+ let canDelete = this.notes && this.notes.length > 0;
+ if (canDelete && this.type == 'personal') {
+ canDelete = this.notes.find((note) => {
+ return note.usermodified == this.currentUserId;
+ });
+ }
+ this.canDeleteNotes = canDelete;
+
this.notesLoaded = true;
this.refreshIcon = 'refresh';
this.syncIcon = 'sync';
@@ -151,6 +172,7 @@ export class AddonNotesListComponent implements OnInit, OnDestroy {
/**
* Add a new Note to user and course.
+ *
* @param {Event} e Event.
*/
addNote(e: Event): void {
@@ -164,7 +186,7 @@ export class AddonNotesListComponent implements OnInit, OnDestroy {
this.notesLoaded = false;
}
- this.refreshNotes(true);
+ this.refreshNotes(false);
} else if (data && data.type && data.type != this.type) {
this.type = data.type;
this.typeChanged();
@@ -173,6 +195,53 @@ export class AddonNotesListComponent implements OnInit, OnDestroy {
modal.present();
}
+ /**
+ * Delete a note.
+ *
+ * @param {Event} e Click event.
+ * @param {any} note Note to delete.
+ */
+ deleteNote(e: Event, note: any): void {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.domUtils.showConfirm(this.translate.instant('addon.notes.deleteconfirm')).then(() => {
+ this.notesProvider.deleteNote(note, this.courseId).then(() => {
+ this.showDelete = false;
+
+ this.refreshNotes(false);
+
+ this.domUtils.showToast('addon.notes.eventnotedeleted', true, 3000);
+ }).catch((error) => {
+ this.domUtils.showErrorModalDefault(error, 'Delete note failed.');
+ });
+ }).catch(() => {
+ // User cancelled, nothing to do.
+ });
+ }
+
+ /**
+ * Restore a note.
+ *
+ * @param {Event} e Click event.
+ * @param {any} note Note to delete.
+ */
+ undoDeleteNote(e: Event, note: any): void {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.notesOffline.undoDeleteNote(note.id).then(() => {
+ this.refreshNotes(true);
+ });
+ }
+
+ /**
+ * Toggle delete.
+ */
+ toggleDelete(): void {
+ this.showDelete = !this.showDelete;
+ }
+
/**
* Tries to synchronize course notes.
*
diff --git a/src/addon/notes/lang/en.json b/src/addon/notes/lang/en.json
index 3317484cdd8..c8256d0c4b3 100644
--- a/src/addon/notes/lang/en.json
+++ b/src/addon/notes/lang/en.json
@@ -1,7 +1,9 @@
{
"addnewnote": "Add a new note",
"coursenotes": "Course notes",
+ "deleteconfirm": "Delete this note?",
"eventnotecreated": "Note created",
+ "eventnotedeleted": "Note deleted",
"nonotes": "There are no notes of this type yet",
"note": "Note",
"notes": "Notes",
diff --git a/src/addon/notes/providers/notes-offline.ts b/src/addon/notes/providers/notes-offline.ts
index 486fa0111d1..28bae97bfb5 100644
--- a/src/addon/notes/providers/notes-offline.ts
+++ b/src/addon/notes/providers/notes-offline.ts
@@ -26,9 +26,10 @@ export class AddonNotesOfflineProvider {
// Variables for database.
static NOTES_TABLE = 'addon_notes_offline_notes';
+ static NOTES_DELETED_TABLE = 'addon_notes_deleted_offline_notes';
protected siteSchema: CoreSiteSchema = {
name: 'AddonNotesOfflineProvider',
- version: 1,
+ version: 2,
tables: [
{
name: AddonNotesOfflineProvider.NOTES_TABLE,
@@ -63,6 +64,24 @@ export class AddonNotesOfflineProvider {
}
],
primaryKeys: ['userid', 'content', 'created']
+ },
+ {
+ name: AddonNotesOfflineProvider.NOTES_DELETED_TABLE,
+ columns: [
+ {
+ name: 'noteid',
+ type: 'INTEGER',
+ primaryKey: true
+ },
+ {
+ name: 'deleted',
+ type: 'INTEGER'
+ },
+ {
+ name: 'courseid',
+ type: 'INTEGER'
+ }
+ ]
}
]
};
@@ -73,7 +92,7 @@ export class AddonNotesOfflineProvider {
}
/**
- * Delete a note.
+ * Delete an offline note.
*
* @param {number} userId User ID the note is about.
* @param {string} content The note content.
@@ -81,7 +100,7 @@ export class AddonNotesOfflineProvider {
* @param {string} [siteId] Site ID. If not defined, current site.
* @return {Promise} Promise resolved if deleted, rejected if failure.
*/
- deleteNote(userId: number, content: string, timecreated: number, siteId?: string): Promise {
+ deleteOfflineNote(userId: number, content: string, timecreated: number, siteId?: string): Promise {
return this.sitesProvider.getSite(siteId).then((site) => {
return site.getDb().deleteRecords(AddonNotesOfflineProvider.NOTES_TABLE, {
userid: userId,
@@ -91,6 +110,31 @@ export class AddonNotesOfflineProvider {
});
}
+ /**
+ * Get all offline deleted notes.
+ *
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with notes.
+ */
+ getAllDeletedNotes(siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.getDb().getRecords(AddonNotesOfflineProvider.NOTES_DELETED_TABLE);
+ });
+ }
+
+ /**
+ * Get course offline deleted notes.
+ *
+ * @param {number} courseId Course ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved with notes.
+ */
+ getCourseDeletedNotes(courseId: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.getDb().getRecords(AddonNotesOfflineProvider.NOTES_DELETED_TABLE, {courseid: courseId});
+ });
+ }
+
/**
* Get all offline notes.
*
@@ -246,4 +290,40 @@ export class AddonNotesOfflineProvider {
});
});
}
+
+ /**
+ * Delete a note offline to be sent later.
+ *
+ * @param {number} noteId Note ID.
+ * @param {number} courseId Course ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved if stored, rejected if failure.
+ */
+ deleteNote(noteId: number, courseId: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ const now = this.timeUtils.timestamp();
+ const data = {
+ noteid: noteId,
+ courseid: courseId,
+ deleted: now
+ };
+
+ return site.getDb().insertRecord(AddonNotesOfflineProvider.NOTES_DELETED_TABLE, data).then(() => {
+ return data;
+ });
+ });
+ }
+
+ /**
+ * Undo delete a note.
+ *
+ * @param {number} noteId Note ID.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved if deleted, rejected if failure.
+ */
+ undoDeleteNote(noteId: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ return site.getDb().deleteRecords(AddonNotesOfflineProvider.NOTES_DELETED_TABLE, { noteid: noteId });
+ });
+ }
}
diff --git a/src/addon/notes/providers/notes-sync.ts b/src/addon/notes/providers/notes-sync.ts
index 77b7039c3e5..cebf92d73d8 100644
--- a/src/addon/notes/providers/notes-sync.ts
+++ b/src/addon/notes/providers/notes-sync.ts
@@ -63,18 +63,24 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider {
* @return {Promise} Promise resolved if sync is successful, rejected if sync fails.
*/
private syncAllNotesFunc(siteId: string, force: boolean): Promise {
- return this.notesOffline.getAllNotes(siteId).then((notes) => {
+ const proms = [];
+
+ proms.push(this.notesOffline.getAllNotes(siteId));
+ proms.push(this.notesOffline.getAllDeletedNotes(siteId));
+
+ return Promise.all(proms).then((notesArray) => {
// Get all the courses to be synced.
- const courseIds = [];
- notes.forEach((note) => {
- if (courseIds.indexOf(note.courseid) == -1) {
- courseIds.push(note.courseid);
- }
+ const courseIds = {};
+ notesArray.forEach((notes) => {
+ notes.forEach((note) => {
+ courseIds[note.courseid] = note.courseid;
+ });
});
-
// Sync all courses.
- const promises = courseIds.map((courseId) => {
- const promise = force ? this.syncNotes(courseId, siteId) : this.syncNotesIfNeeded(courseId, siteId);
+ const promises = Object.keys(courseIds).map((courseId) => {
+ const cId = parseInt(courseIds[courseId], 10);
+
+ const promise = force ? this.syncNotes(cId, siteId) : this.syncNotesIfNeeded(cId, siteId);
return promise.then((warnings) => {
if (typeof warnings != 'undefined') {
@@ -124,9 +130,12 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider {
this.logger.debug('Try to sync notes for course ' + courseId);
const warnings = [];
+ const errors = [];
+
+ const proms = [];
// Get offline notes to be sent.
- const syncPromise = this.notesOffline.getNotesForCourse(courseId, siteId).then((notes) => {
+ proms.push(this.notesOffline.getNotesForCourse(courseId, siteId).then((notes) => {
if (!notes.length) {
// Nothing to sync.
return;
@@ -157,12 +166,6 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider {
}
});
- // Fetch the notes from server to be sure they're up to date.
- return this.notesProvider.invalidateNotes(courseId, undefined, siteId).then(() => {
- return this.notesProvider.getNotes(courseId, undefined, false, true, siteId);
- }).catch(() => {
- // Ignore errors.
- });
}).catch((error) => {
if (this.utils.isWebServiceError(error)) {
// It's a WebService error, this means the user cannot send notes.
@@ -174,26 +177,69 @@ export class AddonNotesSyncProvider extends CoreSyncBaseProvider {
}).then(() => {
// Notes were sent, delete them from local DB.
const promises = notes.map((note) => {
- return this.notesOffline.deleteNote(note.userid, note.content, note.created, siteId);
+ return this.notesOffline.deleteOfflineNote(note.userid, note.content, note.created, siteId);
});
return Promise.all(promises);
- }).then(() => {
- if (errors && errors.length) {
- // At least an error occurred, get course name and add errors to warnings array.
- return this.coursesProvider.getUserCourse(courseId, true, siteId).catch(() => {
- // Ignore errors.
- return {};
- }).then((course) => {
- errors.forEach((error) => {
- warnings.push(this.translate.instant('addon.notes.warningnotenotsent', {
- course: course.fullname ? course.fullname : courseId,
- error: error
- }));
- });
- });
+ });
+ }));
+
+ // Get offline notes to be sent.
+ proms.push(this.notesOffline.getCourseDeletedNotes(courseId, siteId).then((notes) => {
+ if (!notes.length) {
+ // Nothing to sync.
+ return;
+ } else if (!this.appProvider.isOnline()) {
+ // Cannot sync in offline.
+ return Promise.reject(this.translate.instant('core.networkerrormsg'));
+ }
+
+ // Format the notes to be sent.
+ const notesToDelete = notes.map((note) => {
+ return note.noteid;
+ });
+
+ // Delete the notes.
+ return this.notesProvider.deleteNotesOnline(notesToDelete, courseId, siteId).catch((error) => {
+ if (this.utils.isWebServiceError(error)) {
+ // It's a WebService error, this means the user cannot send notes.
+ errors.push(error);
+ } else {
+ // Not a WebService error, reject the synchronization to try again.
+ return Promise.reject(error);
}
+ }).then(() => {
+ // Notes were sent, delete them from local DB.
+ const promises = notes.map((noteId) => {
+ return this.notesOffline.undoDeleteNote(noteId, siteId);
+ });
+
+ return Promise.all(promises);
+ });
+ }));
+
+ const syncPromise = Promise.all(proms).then(() => {
+ // Fetch the notes from server to be sure they're up to date.
+ return this.notesProvider.invalidateNotes(courseId, undefined, siteId).then(() => {
+ return this.notesProvider.getNotes(courseId, undefined, false, true, siteId);
+ }).catch(() => {
+ // Ignore errors.
});
+ }).then(() => {
+ if (errors && errors.length) {
+ // At least an error occurred, get course name and add errors to warnings array.
+ return this.coursesProvider.getUserCourse(courseId, true, siteId).catch(() => {
+ // Ignore errors.
+ return {};
+ }).then((course) => {
+ errors.forEach((error) => {
+ warnings.push(this.translate.instant('addon.notes.warningnotenotsent', {
+ course: course.fullname ? course.fullname : courseId,
+ error: error
+ }));
+ });
+ });
+ }
}).then(() => {
// All done, return the warnings.
return warnings;
diff --git a/src/addon/notes/providers/notes.ts b/src/addon/notes/providers/notes.ts
index 006a78093dd..82f095e4173 100644
--- a/src/addon/notes/providers/notes.ts
+++ b/src/addon/notes/providers/notes.ts
@@ -133,6 +133,72 @@ export class AddonNotesProvider {
});
}
+ /**
+ * Delete a note.
+ *
+ * @param {any} note Note object to delete.
+ * @param {number} courseId Course ID where the note belongs.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when deleted, rejected otherwise. Promise resolved doesn't mean that notes
+ * have been deleted, the resolve param can contain errors for notes not deleted.
+ */
+ deleteNote(note: any, courseId: number, siteId?: string): Promise {
+ siteId = siteId || this.sitesProvider.getCurrentSiteId();
+
+ if (note.offline) {
+ return this.notesOffline.deleteOfflineNote(note.userid, note.content, note.created, siteId);
+ }
+
+ // Convenience function to store the action to be synchronized later.
+ const storeOffline = (): Promise => {
+ return this.notesOffline.deleteNote(note.id, courseId, siteId).then(() => {
+ return false;
+ });
+ };
+
+ if (!this.appProvider.isOnline()) {
+ // App is offline, store the note.
+ return storeOffline();
+ }
+
+ // Send note to server.
+ return this.deleteNotesOnline([note.id], courseId, siteId).then(() => {
+ return true;
+ }).catch((error) => {
+ if (this.utils.isWebServiceError(error)) {
+ // It's a WebService error, the user cannot send the note so don't store it.
+ return Promise.reject(error);
+ }
+
+ // Error sending note, store it to retry later.
+ return storeOffline();
+ });
+ }
+
+ /**
+ * Delete a note. It will fail if offline or cannot connect.
+ *
+ * @param {number[]} noteIds Note IDs to delete.
+ * @param {number} courseId Course ID where the note belongs.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} Promise resolved when deleted, rejected otherwise. Promise resolved doesn't mean that notes
+ * have been deleted, the resolve param can contain errors for notes not deleted.
+ */
+ deleteNotesOnline(noteIds: number[], courseId: number, siteId?: string): Promise {
+ return this.sitesProvider.getSite(siteId).then((site) => {
+ const data = {
+ notes: noteIds
+ };
+
+ return site.write('core_notes_delete_notes', data).then((response) => {
+ // A note was deleted, invalidate the course notes.
+ return this.invalidateNotes(courseId, undefined, siteId).catch(() => {
+ // Ignore errors.
+ });
+ });
+ });
+ }
+
/**
* Returns whether or not the notes plugin is enabled for a certain site.
*
@@ -267,6 +333,24 @@ export class AddonNotesProvider {
});
}
+ /**
+ * Get offline deleted notes and set the state.
+ *
+ * @param {any[]} notes Array of notes.
+ * @param {number} courseId ID of the course the notes belong to.
+ * @param {string} [siteId] Site ID. If not defined, current site.
+ * @return {Promise} [description]
+ */
+ setOfflineDeletedNotes(notes: any[], courseId: number, siteId?: string): Promise {
+ return this.notesOffline.getCourseDeletedNotes(courseId, siteId).then((deletedNotes) => {
+ notes.forEach((note) => {
+ note.deleted = deletedNotes.some((n) => n.noteid == note.id);
+ });
+
+ return notes;
+ });
+ }
+
/**
* Get user data for notes since they only have userid.
*
diff --git a/src/addon/notes/providers/user-handler.ts b/src/addon/notes/providers/user-handler.ts
index c7a37d061ac..cd9ce80152d 100644
--- a/src/addon/notes/providers/user-handler.ts
+++ b/src/addon/notes/providers/user-handler.ts
@@ -99,7 +99,7 @@ export class AddonNotesUserHandler implements CoreUserProfileHandler {
action: (event, navCtrl, user, courseId): void => {
event.preventDefault();
event.stopPropagation();
- // Always use redirect to make it the new history root (to avoid "loops" in history).
+
this.linkHelper.goInSite(navCtrl, 'AddonNotesListPage', { userId: user.id, courseId: courseId });
}
};
diff --git a/src/addon/notifications/components/actions/actions.ts b/src/addon/notifications/components/actions/actions.ts
index 2d56bc7688a..c2a6f23bb0d 100644
--- a/src/addon/notifications/components/actions/actions.ts
+++ b/src/addon/notifications/components/actions/actions.ts
@@ -31,7 +31,8 @@ export class AddonNotificationsActionsComponent implements OnInit {
actions: CoreContentLinksAction[] = [];
- constructor(private contentLinksDelegate: CoreContentLinksDelegate, private sitesProvider: CoreSitesProvider) {}
+ constructor(private contentLinksDelegate: CoreContentLinksDelegate, private sitesProvider: CoreSitesProvider,
+ public navCtrl: NavController) {}
/**
* Component being initialized.
diff --git a/src/addon/notifications/components/actions/addon-notifications-actions.html b/src/addon/notifications/components/actions/addon-notifications-actions.html
index 14425705d19..1f386603bf5 100644
--- a/src/addon/notifications/components/actions/addon-notifications-actions.html
+++ b/src/addon/notifications/components/actions/addon-notifications-actions.html
@@ -1,6 +1,6 @@
0" justify-content-around>
-
+
{{ action.message | translate }}
diff --git a/src/addon/notifications/pages/list/list.ts b/src/addon/notifications/pages/list/list.ts
index 3b8d1751ad1..1ab97b2be9f 100644
--- a/src/addon/notifications/pages/list/list.ts
+++ b/src/addon/notifications/pages/list/list.ts
@@ -41,8 +41,10 @@ export class AddonNotificationsListPage {
canMarkAllNotificationsAsRead = false;
loadingMarkAllNotificationsAsRead = false;
+ protected isCurrentView: boolean;
protected cronObserver: CoreEventObserver;
protected pushObserver: Subscription;
+ protected pendingRefresh = false;
constructor(navParams: NavParams, private domUtils: CoreDomUtilsProvider, private eventsProvider: CoreEventsProvider,
private sitesProvider: CoreSitesProvider, private textUtils: CoreTextUtilsProvider,
@@ -55,17 +57,24 @@ export class AddonNotificationsListPage {
* View loaded.
*/
ionViewDidLoad(): void {
- this.fetchNotifications().finally(() => {
- this.notificationsLoaded = true;
- });
+ this.fetchNotifications();
- this.cronObserver = this.eventsProvider.on(AddonNotificationsProvider.READ_CRON_EVENT, () => this.refreshNotifications(),
- this.sitesProvider.getCurrentSiteId());
+ this.cronObserver = this.eventsProvider.on(AddonNotificationsProvider.READ_CRON_EVENT, () => {
+ if (this.isCurrentView) {
+ this.notificationsLoaded = false;
+ this.refreshNotifications();
+ }
+ }, this.sitesProvider.getCurrentSiteId());
this.pushObserver = this.pushNotificationsDelegate.on('receive').subscribe((notification) => {
// New notification received. If it's from current site, refresh the data.
- if (this.utils.isTrueOrOne(notification.notif) && this.sitesProvider.isCurrentSite(notification.site)) {
+ if (this.isCurrentView && this.utils.isTrueOrOne(notification.notif) &&
+ this.sitesProvider.isCurrentSite(notification.site)) {
+
+ this.notificationsLoaded = false;
this.refreshNotifications();
+ } else if (!this.isCurrentView) {
+ this.pendingRefresh = true;
}
});
}
@@ -93,6 +102,8 @@ export class AddonNotificationsListPage {
}).catch((error) => {
this.domUtils.showErrorModalDefault(error, 'addon.notifications.errorgetnotifications', true);
this.loadMoreError = true; // Set to prevent infinite calls with infinite-loading.
+ }).finally(() => {
+ this.notificationsLoaded = true;
});
}
@@ -110,9 +121,7 @@ export class AddonNotificationsListPage {
// All marked as read, refresh the list.
this.notificationsLoaded = false;
- return this.refreshNotifications().finally(() => {
- this.notificationsLoaded = true;
- });
+ return this.refreshNotifications();
});
}
@@ -198,6 +207,27 @@ export class AddonNotificationsListPage {
notification.mobiletext = this.textUtils.replaceNewLines(text, ' ');
}
+ /**
+ * User entered the page.
+ */
+ ionViewDidEnter(): void {
+ this.isCurrentView = true;
+
+ if (this.pendingRefresh) {
+ this.pendingRefresh = false;
+ this.notificationsLoaded = false;
+
+ this.refreshNotifications();
+ }
+ }
+
+ /**
+ * User left the page.
+ */
+ ionViewDidLeave(): void {
+ this.isCurrentView = false;
+ }
+
/**
* Page destroyed.
*/
diff --git a/src/addon/storagemanager/providers/coursemenu-handler.ts b/src/addon/storagemanager/providers/coursemenu-handler.ts
index e2aad3def21..69a578fe08d 100644
--- a/src/addon/storagemanager/providers/coursemenu-handler.ts
+++ b/src/addon/storagemanager/providers/coursemenu-handler.ts
@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Injectable } from '@angular/core';
+import { Injectable, Injector } from '@angular/core';
import { CoreCourseOptionsMenuHandler, CoreCourseOptionsMenuHandlerData } from '@core/course/providers/options-delegate';
/**
@@ -49,9 +49,11 @@ export class AddonStorageManagerCourseMenuHandler implements CoreCourseOptionsMe
/**
* Returns the data needed to render the handler.
*
+ * @param {Injector} injector Injector.
+ * @param {any} course The course.
* @return {CoreCourseOptionsMenuHandlerData} Data needed to render the handler.
*/
- getMenuDisplayData(): CoreCourseOptionsMenuHandlerData {
+ getMenuDisplayData(injector: Injector, course: any): CoreCourseOptionsMenuHandlerData {
return {
icon: 'cube',
title: 'addon.storagemanager.managestorage',
diff --git a/src/addon/userprofilefield/datetime/component/datetime.ts b/src/addon/userprofilefield/datetime/component/datetime.ts
index c43a61bc022..8da732a6459 100644
--- a/src/addon/userprofilefield/datetime/component/datetime.ts
+++ b/src/addon/userprofilefield/datetime/component/datetime.ts
@@ -47,9 +47,9 @@ export class AddonUserProfileFieldDatetimeComponent implements OnInit {
// Check if it's only date or it has time too.
const hasTime = this.utils.isTrueOrOne(field.param3);
- // Calculate format to use. ion-datetime doesn't support escaping characters ([]), so we remove them.
- field.format = this.timeUtils.convertPHPToMoment(this.translate.instant('core.' +
- (hasTime ? 'strftimedatetimeshort' : 'strftimedatefullshort'))).replace(/[\[\]]/g, '');
+ // Calculate format to use.
+ field.format = this.timeUtils.fixFormatForDatetime(this.timeUtils.convertPHPToMoment(
+ this.translate.instant('core.' + (hasTime ? 'strftimedatetime' : 'strftimedate'))));
// Check min value.
if (field.param1) {
diff --git a/src/addon/userprofilefield/datetime/providers/handler.ts b/src/addon/userprofilefield/datetime/providers/handler.ts
index 65a7a6f6689..29d3d321ece 100644
--- a/src/addon/userprofilefield/datetime/providers/handler.ts
+++ b/src/addon/userprofilefield/datetime/providers/handler.ts
@@ -14,6 +14,7 @@
// limitations under the License.
import { Injectable, Injector } from '@angular/core';
import { CoreUserProfileFieldHandler, CoreUserProfileFieldHandlerData } from '@core/user/providers/user-profile-field-delegate';
+import { CoreTimeUtilsProvider } from '@providers/utils/time';
import { AddonUserProfileFieldDatetimeComponent } from '../component/datetime';
/**
@@ -24,7 +25,7 @@ export class AddonUserProfileFieldDatetimeHandler implements CoreUserProfileFiel
name = 'AddonUserProfileFieldDatetime';
type = 'datetime';
- constructor() {
+ constructor(protected timeUtils: CoreTimeUtilsProvider) {
// Nothing to do.
}
@@ -50,12 +51,10 @@ export class AddonUserProfileFieldDatetimeHandler implements CoreUserProfileFiel
const name = 'profile_field_' + field.shortname;
if (formValues[name]) {
- const milliseconds = new Date(formValues[name]).getTime();
-
return {
type: 'datetime',
name: 'profile_field_' + field.shortname,
- value: Math.round(milliseconds / 1000)
+ value: this.timeUtils.convertToTimestamp(formValues[name])
};
}
}
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 1be81e471a8..7df4951b2a3 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -25,6 +25,7 @@ import { CoreCustomURLSchemesProvider } from '@providers/urlschemes';
import { CoreLoginHelperProvider } from '@core/login/providers/helper';
import { Keyboard } from '@ionic-native/keyboard';
import { ScreenOrientation } from '@ionic-native/screen-orientation';
+import { CoreLoginSitesPage } from '@core/login/pages/sites/sites';
@Component({
templateUrl: 'app.html'
@@ -37,10 +38,10 @@ export class MoodleMobileApp implements OnInit {
protected lastUrls = {};
protected lastInAppUrl: string;
- constructor(private platform: Platform, logger: CoreLoggerProvider, keyboard: Keyboard,
+ constructor(private platform: Platform, logger: CoreLoggerProvider, keyboard: Keyboard, private app: IonicApp,
private eventsProvider: CoreEventsProvider, private loginHelper: CoreLoginHelperProvider, private zone: NgZone,
private appProvider: CoreAppProvider, private langProvider: CoreLangProvider, private sitesProvider: CoreSitesProvider,
- private screenOrientation: ScreenOrientation, app: IonicApp, private urlSchemesProvider: CoreCustomURLSchemesProvider,
+ private screenOrientation: ScreenOrientation, private urlSchemesProvider: CoreCustomURLSchemesProvider,
private utils: CoreUtilsProvider, private urlUtils: CoreUrlUtilsProvider) {
this.logger = logger.getInstance('AppComponent');
@@ -64,6 +65,11 @@ export class MoodleMobileApp implements OnInit {
app.setElementClass('platform-windows', true);
}
}
+
+ // Register back button action to allow closing modals before anything else.
+ this.appProvider.registerBackButtonAction(() => {
+ return this.closeModal();
+ }, 2000);
});
}
@@ -74,7 +80,9 @@ export class MoodleMobileApp implements OnInit {
ngOnInit(): void {
this.eventsProvider.on(CoreEventsProvider.LOGOUT, () => {
// Go to sites page when user is logged out.
- this.appProvider.getRootNavController().setRoot('CoreLoginSitesPage');
+ // Due to DeepLinker, we need to use the ViewCtrl instead of name.
+ // Otherwise some pages are re-created when they shouldn't.
+ this.appProvider.getRootNavController().setRoot(CoreLoginSitesPage);
// Unload lang custom strings.
this.langProvider.clearCustomStrings();
@@ -286,4 +294,21 @@ export class MoodleMobileApp implements OnInit {
document.body.classList.remove(tempClass);
});
}
+
+ /**
+ * Close one modal if any.
+ *
+ * @return {boolean} True if one modal was present.
+ */
+ closeModal(): boolean {
+ // Following function is hidden in Ionic Code, however there's no solution for that.
+ const portal = this.app._getActivePortal();
+ if (portal) {
+ portal.pop();
+
+ return true;
+ }
+
+ return false;
+ }
}
diff --git a/src/app/app.ios.scss b/src/app/app.ios.scss
index 2655a22768c..8eeaf67a367 100644
--- a/src/app/app.ios.scss
+++ b/src/app/app.ios.scss
@@ -99,16 +99,22 @@ ion-app.app-root.ios {
background-color: $checkbox-ios-background-color-off;
}
- // File Uploader
- // In iOS the input is 1 level higher, so the styles are different.
- .action-sheet-ios input.core-fileuploader-file-handler-input {
- position: absolute;
- @include position(null, 0, null, 0);
- min-width: 100%;
- min-height: $action-sheet-ios-button-min-height;
- opacity: 0;
- outline: none;
- z-index: 100;
- cursor: pointer;
+
+ .action-sheet-ios {
+ .action-sheet-title {
+ font-size: 2rem;
+ }
+ // File Uploader
+ // In iOS the input is 1 level higher, so the styles are different.
+ input.core-fileuploader-file-handler-input {
+ position: absolute;
+ @include position(null, 0, null, 0);
+ min-width: 100%;
+ min-height: $action-sheet-ios-button-min-height;
+ opacity: 0;
+ outline: none;
+ z-index: 100;
+ cursor: pointer;
+ }
}
}
\ No newline at end of file
diff --git a/src/app/app.md.scss b/src/app/app.md.scss
index 8a774558556..a97ce6e952b 100644
--- a/src/app/app.md.scss
+++ b/src/app/app.md.scss
@@ -70,18 +70,14 @@ ion-app.app-root.md {
padding-top: 0;
margin-top: $action-sheet-md-title-padding-top;
}
- .action-sheet-cancel {
- color: $red;
- }
- .action-sheet-wrapper {
- bottom: 0;
- top: initial;
- max-height: 50%;
- height: 100%;
- }
- .action-sheet-selected {
- color: $core-color;
+ @media (min-height: 500px) {
+ .action-sheet-wrapper {
+ bottom: 0;
+ top: initial;
+ max-height: 50%;
+ height: 100%;
+ }
}
}
}
diff --git a/src/app/app.module.ts b/src/app/app.module.ts
index 4a174e54242..c843dce5429 100644
--- a/src/app/app.module.ts
+++ b/src/app/app.module.ts
@@ -15,7 +15,7 @@
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { NgModule, COMPILER_OPTIONS } from '@angular/core';
-import { IonicApp, IonicModule, Platform, Content, ScrollEvent, Config } from 'ionic-angular';
+import { IonicApp, IonicModule, Platform, Content, ScrollEvent, Config, Refresher } from 'ionic-angular';
import { assert } from 'ionic-angular/util/util';
import { HttpModule } from '@angular/http';
import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
@@ -81,6 +81,7 @@ import { CoreQuestionModule } from '@core/question/question.module';
import { CoreCommentsModule } from '@core/comments/comments.module';
import { CoreBlockModule } from '@core/block/block.module';
import { CoreRatingModule } from '@core/rating/rating.module';
+import { CoreTagModule } from '@core/tag/tag.module';
// Addon modules.
import { AddonBadgesModule } from '@addon/badges/badges.module';
@@ -91,12 +92,30 @@ import { AddonCourseCompletionModule } from '@addon/coursecompletion/coursecompl
import { AddonUserProfileFieldModule } from '@addon/userprofilefield/userprofilefield.module';
import { AddonFilesModule } from '@addon/files/files.module';
import { AddonBlockActivityModulesModule } from '@addon/block/activitymodules/activitymodules.module';
+import { AddonBlockBadgesModule } from '@addon/block/badges/badges.module';
+import { AddonBlockBlogMenuModule } from '@addon/block/blogmenu/blogmenu.module';
+import { AddonBlockBlogTagsModule } from '@addon/block/blogtags/blogtags.module';
+import { AddonBlockBlogRecentModule } from '@addon/block/blogrecent/blogrecent.module';
+import { AddonBlockCalendarMonthModule } from '@addon/block/calendarmonth/calendarmonth.module';
+import { AddonBlockCalendarUpcomingModule } from '@addon/block/calendarupcoming/calendarupcoming.module';
+import { AddonBlockCommentsModule } from '@addon/block/comments/comments.module';
+import { AddonBlockCompletionStatusModule } from '@addon/block/completionstatus/completionstatus.module';
+import { AddonBlockGlossaryRandomModule } from '@addon/block/glossaryrandom/glossaryrandom.module';
+import { AddonBlockHtmlModule } from '@addon/block/html/html.module';
import { AddonBlockMyOverviewModule } from '@addon/block/myoverview/myoverview.module';
+import { AddonBlockNewsItemsModule } from '@addon/block/newsitems/newsitems.module';
+import { AddonBlockOnlineUsersModule } from '@addon/block/onlineusers/onlineusers.module';
+import { AddonBlockLearningPlansModule } from '@addon/block/learningplans/learningplans.module';
+import { AddonBlockPrivateFilesModule } from '@addon/block/privatefiles/privatefiles.module';
import { AddonBlockSiteMainMenuModule } from '@addon/block/sitemainmenu/sitemainmenu.module';
import { AddonBlockTimelineModule } from '@addon/block/timeline/timeline.module';
import { AddonBlockRecentlyAccessedCoursesModule } from '@addon/block/recentlyaccessedcourses/recentlyaccessedcourses.module';
import { AddonBlockRecentlyAccessedItemsModule } from '@addon/block/recentlyaccesseditems/recentlyaccesseditems.module';
+import { AddonBlockRecentActivityModule } from '@addon/block/recentactivity/recentactivity.module';
+import { AddonBlockRssClientModule } from '@addon/block/rssclient/rssclient.module';
import { AddonBlockStarredCoursesModule } from '@addon/block/starredcourses/starredcourses.module';
+import { AddonBlockSelfCompletionModule } from '@addon/block/selfcompletion/selfcompletion.module';
+import { AddonBlockTagsModule } from '@addon/block/tags/tags.module';
import { AddonModAssignModule } from '@addon/mod/assign/assign.module';
import { AddonModBookModule } from '@addon/mod/book/book.module';
import { AddonModChatModule } from '@addon/mod/chat/chat.module';
@@ -166,6 +185,8 @@ export const CORE_PROVIDERS: any[] = [
CoreCustomURLSchemesProvider
];
+export const WP_PROVIDER: any = null;
+
@NgModule({
declarations: [
MoodleMobileApp
@@ -205,6 +226,7 @@ export const CORE_PROVIDERS: any[] = [
CoreBlockModule,
CoreRatingModule,
CorePushNotificationsModule,
+ CoreTagModule,
AddonBadgesModule,
AddonBlogModule,
AddonCalendarModule,
@@ -213,12 +235,30 @@ export const CORE_PROVIDERS: any[] = [
AddonUserProfileFieldModule,
AddonFilesModule,
AddonBlockActivityModulesModule,
+ AddonBlockBadgesModule,
+ AddonBlockBlogMenuModule,
+ AddonBlockBlogRecentModule,
+ AddonBlockBlogTagsModule,
+ AddonBlockCalendarMonthModule,
+ AddonBlockCalendarUpcomingModule,
+ AddonBlockCommentsModule,
+ AddonBlockCompletionStatusModule,
+ AddonBlockGlossaryRandomModule,
+ AddonBlockHtmlModule,
+ AddonBlockLearningPlansModule,
AddonBlockMyOverviewModule,
+ AddonBlockNewsItemsModule,
+ AddonBlockOnlineUsersModule,
+ AddonBlockPrivateFilesModule,
AddonBlockSiteMainMenuModule,
AddonBlockTimelineModule,
AddonBlockRecentlyAccessedCoursesModule,
AddonBlockRecentlyAccessedItemsModule,
+ AddonBlockRecentActivityModule,
+ AddonBlockRssClientModule,
AddonBlockStarredCoursesModule,
+ AddonBlockSelfCompletionModule,
+ AddonBlockTagsModule,
AddonModAssignModule,
AddonModBookModule,
AddonModChatModule,
@@ -333,6 +373,9 @@ export class AppModule {
// Decorate ion-content.
this.decorateIonContent();
+
+ // Patch ion-refresher.
+ this.patchIonRefresher();
}
/**
@@ -542,4 +585,24 @@ export class AppModule {
this._orientationObs && this._orientationObs.off();
};
}
+
+ /**
+ * Patch ion-refresher to fix video menus and possibly other fixed positioned elements.
+ */
+ patchIonRefresher(): void {
+ /**
+ * Original code: https://github.com/ionic-team/ionic/blob/v3.9.3/src/components/refresher/refresher.ts#L468
+ * Changed: translateZ(0px) is not added to the CSS transform.
+ */
+ Refresher.prototype._setCss = function(y: number, duration: string, overflowVisible: boolean, delay: string): void {
+ this._appliedStyles = (y > 0);
+
+ const content = this._content;
+ const Css = this._plt.Css;
+ content.setScrollElementStyle(Css.transform, ((y > 0) ? 'translateY(' + y + 'px)' : ''));
+ content.setScrollElementStyle(Css.transitionDuration, duration);
+ content.setScrollElementStyle(Css.transitionDelay, delay);
+ content.setScrollElementStyle('overflow', (overflowVisible ? 'hidden' : ''));
+ };
+ }
}
diff --git a/src/app/app.scss b/src/app/app.scss
index 41759e8eff1..594dccfe368 100644
--- a/src/app/app.scss
+++ b/src/app/app.scss
@@ -315,6 +315,11 @@ ion-app.app-root {
}
}
+ // All external files should be banned from copying it.
+ ion-avatar, img, audio, video, iframe, [core-external-content], [role="presentation"], [data-original-src], [src] {
+ user-select: none !important;
+ }
+
.core-media-adapt-width {
max-width: 100%;
}
@@ -383,11 +388,11 @@ ion-app.app-root {
ion-select {
position: relative; // Ionic fix. Button can occupy all page if not.
- color: $core-select-placeholder-color;
+ color: $core-select-color;
align-self: start;
.select-icon .select-icon-inner {
- color: $core-select-placeholder-color;
+ color: $core-select-color;
}
&.select-disabled .select-icon .select-icon-inner {
@@ -406,10 +411,18 @@ ion-app.app-root {
}
}
+ .item-label-stacked ion-select[multiple="true"] {
+ width: 100%;
+ }
+
+ ion-select .select-placeholder {
+ color: $core-select-placeholder-color;
+ }
+
ion-select.core-button-select,
.core-button-select {
background-color: white;
- color: $core-select-placeholder-color;
+ color: $core-select-color;
white-space: normal;
align-self: start;
max-width: none;
@@ -441,7 +454,7 @@ ion-app.app-root {
}
.select-icon .select-icon-inner {
- color: $core-select-placeholder-color;
+ color: $core-select-color;
}
ion-icon:last-child {
@@ -456,11 +469,6 @@ ion-app.app-root {
}
}
- .col > .button-block {
- contain: content;
- }
-
-
// File uploader.
// -------------------------
.core-fileuploader-file-handler {
@@ -599,6 +607,16 @@ ion-app.app-root {
.action-sheet-group {
overflow: auto;
}
+
+ .action-sheet-wrapper {
+ .action-sheet-button.action-sheet-cancel {
+ color: $core-action-sheet-cancel-color;
+ }
+ .action-sheet-selected {
+ color: $core-color;
+ }
+ }
+
.alert-message {
overflow-y: auto;
}
@@ -649,13 +667,16 @@ ion-app.app-root {
.toolbar img.core-bar-button-image,
.toolbar .core-bar-button-image img {
padding: 0;
- width: 100%;
- height: 100%;
- max-width: $core-toolbar-button-image-width - 1;
- max-height: $core-toolbar-button-image-width - 1;
+ width: $core-toolbar-button-image-width;
+ height: $core-toolbar-button-image-width;
+ max-width: $core-toolbar-button-image-width;
border-radius: 50%;
}
+ .header .toolbar-ios {
+ height: $toolbar-ios-height;
+ }
+
// Footer with auto height.
.footer.footer-adjustable {
height: auto;
@@ -697,15 +718,21 @@ ion-app.app-root {
.core-#{$color-name}-item.item-input {
border-bottom: 0 !important;
- &.item-md .item-inner {
+ &.item-md .item-inner,
+ &.item-md.item-input.ng-valid.item-input-has-value:not(.input-has-focus):not(.item-input-has-focus) .item-inner,
+ &.item-md.item-input.ng-valid.input-has-value:not(.input-has-focus):not(.item-input-has-focus) .item-inner {
@include md-input-highlight($color-base);
}
- &.item-ios .item-inner {
+ &.item-ios .item-inner,
+ &.item-ios.item-input.ng-valid.item-input-has-value:not(.input-has-focus):not(.item-input-has-focus) .item-inner,
+ &.item-ios.item-input.ng-valid.input-has-value:not(.input-has-focus):not(.item-input-has-focus) .item-inner {
@include ios-input-highlight($color-base);
}
- &.item-wp .item-inner {
+ &.item-wp .text-input,
+ &.item-wp.item-input.ng-valid.item-input-has-value:not(.input-has-focus):not(.item-input-has-focus) .text-input,
+ &.item-wp.item-input.ng-valid.input-has-value:not(.input-has-focus):not(.item-input-has-focus) .text-input {
border-color: $color-base;
}
}
@@ -999,6 +1026,11 @@ ion-app.app-root {
ion-alert .alert-checkbox-button .alert-checkbox-label {
white-space: normal;
}
+
+ ion-backdrop {
+ transition: opacity 100ms ease-in-out;
+ opacity: .1;
+ }
}
@each $color-name, $color-base, $color-contrast in get-colors($colors) {
@@ -1069,21 +1101,19 @@ details summary {
line-height: 28px;
}
-// Fix iframes in fullscreen mode.
-//
-// Ionic sets "contain: strict" to some elements. This enables paint containment,
-// which changes behaviour of fixed positioned elements and seems to break iframes
-// in fullscreen mode. See https://www.w3.org/TR/css-contain-1/#containment-paint
-ion-app,
-ion-nav,
-ion-tab,
-ion-tabs,
-.app-root,
-.ion-page,
-ion-modal,
-.modal-wrapper,
-.split-pane {
- contain: size layout style;
+// Ionic sets the "contain" CSS property to some elements. This enables CSS
+// containment, which changes how elements are positioned and sized, breaking
+// fixed positioned elements, iframes in full screen mode, subtitle menus in
+// videos and potentially more things. CSS containment is not supported in iOS
+// and Android 4.4, so it can introduce inconsistencies across devices.
+// See https://www.w3.org/TR/css-contain-1
+* {
+ contain: none !important;
+}
+
+// Lower z-index for ion-item-divider so it is displayed below video menus.
+ion-item-divider {
+ z-index: 2; // Ionic default is 100.
}
// Highlight text.
@@ -1108,9 +1138,6 @@ ion-app.platform-desktop {
.button-block[text-wrap] {
height: auto;
- // Changed from "strict" because the size depends on child elements.
- contain: content;
-
// Add vertical padding, we cannot rely on a fixed height + centering like in normal buttons.
.item-md & {
padding-top: .5357em;
@@ -1152,3 +1179,15 @@ ion-app.platform-desktop {
min-height: $button-ios-small-height;
}
}
+
+// Make funnel icon have iOS look.
+.ion-md-funnel::before {
+ content: "\f182";
+}
+
+// Fix icon size in lists, to prevent them scaling with text.
+.item, .item-inner {
+ > ion-icon {
+ font-size: 28px;
+ }
+}
diff --git a/src/assets/img/splash.png b/src/assets/img/splash.png
new file mode 100644
index 00000000000..e7889ccf91e
Binary files /dev/null and b/src/assets/img/splash.png differ
diff --git a/src/assets/img/splash_logo.png b/src/assets/img/splash_logo.png
deleted file mode 100644
index 8c9fdfa9a89..00000000000
Binary files a/src/assets/img/splash_logo.png and /dev/null differ
diff --git a/src/assets/lang/en.json b/src/assets/lang/en.json
index 7ed0d48128f..831223df192 100644
--- a/src/assets/lang/en.json
+++ b/src/assets/lang/en.json
@@ -27,6 +27,16 @@
"addon.badges.version": "Version",
"addon.badges.warnexpired": "(This badge has expired!)",
"addon.block_activitymodules.pluginname": "Activities",
+ "addon.block_badges.pluginname": "Latest badges",
+ "addon.block_blogmenu.pluginname": "Blog menu",
+ "addon.block_blogrecent.pluginname": "Recent blog entries",
+ "addon.block_blogtags.pluginname": "Blog tags",
+ "addon.block_calendarmonth.pluginname": "Calendar",
+ "addon.block_calendarupcoming.pluginname": " Upcoming events",
+ "addon.block_comments.pluginname": "Comments",
+ "addon.block_completionstatus.pluginname": "Course completion status",
+ "addon.block_glossaryrandom.pluginname": "Random glossary entry",
+ "addon.block_learningplans.pluginname": "Learning plans",
"addon.block_myoverview.all": "All",
"addon.block_myoverview.favourites": "Starred",
"addon.block_myoverview.future": "Future",
@@ -38,13 +48,20 @@
"addon.block_myoverview.past": "Past",
"addon.block_myoverview.pluginname": "Course overview",
"addon.block_myoverview.title": "Course name",
+ "addon.block_newsitems.pluginname": "Latest announcements",
+ "addon.block_onlineusers.pluginname": "Online users",
+ "addon.block_privatefiles.pluginname": "Private files",
+ "addon.block_recentactivity.pluginname": "Recent activity",
"addon.block_recentlyaccessedcourses.nocourses": "No recent courses",
"addon.block_recentlyaccessedcourses.pluginname": "Recently accessed courses",
"addon.block_recentlyaccesseditems.noitems": "No recent items",
"addon.block_recentlyaccesseditems.pluginname": "Recently accessed items",
+ "addon.block_rssclient.pluginname": "Remote RSS feeds",
+ "addon.block_selfcompletion.pluginname": "Self completion",
"addon.block_sitemainmenu.pluginname": "Main menu",
"addon.block_starredcourses.nocourses": "No starred courses",
"addon.block_starredcourses.pluginname": "Starred courses",
+ "addon.block_tags.pluginname": "Tags",
"addon.block_timeline.duedate": "Due date",
"addon.block_timeline.next30days": "Next 30 days",
"addon.block_timeline.next3months": "Next 3 months",
@@ -66,18 +83,61 @@
"addon.blog.publishtoworld": "Anyone in the world",
"addon.blog.showonlyyourentries": "Show only your entries",
"addon.blog.siteblogheading": "Site blog",
+ "addon.calendar.allday": "All day",
"addon.calendar.calendar": "Calendar",
+ "addon.calendar.calendarevent": "Calendar event",
"addon.calendar.calendarevents": "Calendar events",
"addon.calendar.calendarreminders": "Calendar reminders",
+ "addon.calendar.confirmeventdelete": "Are you sure you want to delete the \"{{$a}}\" event?",
+ "addon.calendar.confirmeventseriesdelete": "The \"{{$a.name}}\" event is part of a series. Do you want to delete just this event, or all {{$a.count}} events in the series?",
+ "addon.calendar.currentmonth": "Current Month",
+ "addon.calendar.daynext": "Next day",
+ "addon.calendar.dayprev": "Previous day",
"addon.calendar.defaultnotificationtime": "Default notification time",
+ "addon.calendar.deleteallevents": "Delete all events",
+ "addon.calendar.deleteevent": "Delete event",
+ "addon.calendar.deleteoneevent": "Delete this event",
+ "addon.calendar.durationminutes": "Duration in minutes",
+ "addon.calendar.durationnone": "Without duration",
+ "addon.calendar.durationuntil": "Until",
+ "addon.calendar.editevent": "Editing event",
"addon.calendar.errorloadevent": "Error loading event.",
"addon.calendar.errorloadevents": "Error loading events.",
+ "addon.calendar.eventcalendareventdeleted": "Calendar event deleted",
+ "addon.calendar.eventduration": "Duration",
"addon.calendar.eventendtime": "End time",
+ "addon.calendar.eventkind": "Type of event",
+ "addon.calendar.eventname": "Event title",
"addon.calendar.eventstarttime": "Start time",
+ "addon.calendar.eventtype": "Event type",
+ "addon.calendar.fri": "Fri",
+ "addon.calendar.friday": "Friday",
"addon.calendar.gotoactivity": "Go to activity",
+ "addon.calendar.invalidtimedurationminutes": "The duration in minutes you have entered is invalid. Please enter the duration in minutes greater than 0 or select no duration.",
+ "addon.calendar.invalidtimedurationuntil": "The date and time you selected for duration until is before the start time of the event. Please correct this before proceeding.",
+ "addon.calendar.mon": "Mon",
+ "addon.calendar.monday": "Monday",
+ "addon.calendar.monthlyview": "Monthly view",
+ "addon.calendar.newevent": "New event",
"addon.calendar.noevents": "There are no events",
+ "addon.calendar.nopermissiontoupdatecalendar": "Sorry, but you do not have permission to update the calendar event",
"addon.calendar.reminders": "Reminders",
+ "addon.calendar.repeatedevents": "Repeated events",
+ "addon.calendar.repeateditall": "Also apply changes to the other {{$a}} events in this repeat series",
+ "addon.calendar.repeateditthis": "Apply changes to this event only",
+ "addon.calendar.repeatevent": "Repeat this event",
+ "addon.calendar.repeatweeksl": "Repeat weekly, creating altogether",
+ "addon.calendar.sat": "Sat",
+ "addon.calendar.saturday": "Saturday",
"addon.calendar.setnewreminder": "Set a new reminder",
+ "addon.calendar.sun": "Sun",
+ "addon.calendar.sunday": "Sunday",
+ "addon.calendar.thu": "Thu",
+ "addon.calendar.thursday": "Thursday",
+ "addon.calendar.today": "Today",
+ "addon.calendar.tomorrow": "Tomorrow",
+ "addon.calendar.tue": "Tue",
+ "addon.calendar.tuesday": "Tuesday",
"addon.calendar.typecategory": "Category event",
"addon.calendar.typeclose": "Close event",
"addon.calendar.typecourse": "Course event",
@@ -87,6 +147,11 @@
"addon.calendar.typeopen": "Open event",
"addon.calendar.typesite": "Site event",
"addon.calendar.typeuser": "User event",
+ "addon.calendar.upcomingevents": "Upcoming events",
+ "addon.calendar.wed": "Wed",
+ "addon.calendar.wednesday": "Wednesday",
+ "addon.calendar.when": "When",
+ "addon.calendar.yesterday": "Yesterday",
"addon.competency.activities": "Activities",
"addon.competency.competencies": "Competencies",
"addon.competency.competenciesmostoftennotproficientincourse": "Competencies most often not proficient in this course",
@@ -355,6 +420,7 @@
"addon.mod_assign_submission_onlinetext.wordlimitexceeded": "The word limit for this assignment is {{$a.limit}} words and you are attempting to submit {{$a.count}} words. Please review your submission and try again.",
"addon.mod_book.errorchapter": "Error reading chapter of book.",
"addon.mod_book.modulenameplural": "Books",
+ "addon.mod_book.tagarea_book_chapters": "Book chapters",
"addon.mod_book.toc": "Table of contents",
"addon.mod_chat.beep": "Beep",
"addon.mod_chat.chatreport": "Chat sessions",
@@ -414,6 +480,7 @@
"addon.mod_data.confirmdeleterecord": "Are you sure you want to delete this entry?",
"addon.mod_data.descending": "Descending",
"addon.mod_data.disapprove": "Undo approval",
+ "addon.mod_data.edittagsnotsupported": "Sorry, editing tags is not supported by the app.",
"addon.mod_data.emptyaddform": "You did not fill out any fields!",
"addon.mod_data.entrieslefttoadd": "You must add {{$a.entriesleft}} more entry/entries in order to complete this activity",
"addon.mod_data.entrieslefttoaddtoview": "You must add {{$a.entrieslefttoview}} more entry/entries before you can view other participants' entries.",
@@ -438,8 +505,10 @@
"addon.mod_data.recorddisapproved": "Entry unapproved",
"addon.mod_data.resetsettings": "Reset filters",
"addon.mod_data.search": "Search",
+ "addon.mod_data.searchbytagsnotsupported": "Sorry, searching by tags is not supported by the app.",
"addon.mod_data.selectedrequired": "All selected required",
"addon.mod_data.single": "View single",
+ "addon.mod_data.tagarea_data_records": "Data records",
"addon.mod_data.timeadded": "Time added",
"addon.mod_data.timemodified": "Time modified",
"addon.mod_data.usedate": "Include in search.",
@@ -532,6 +601,7 @@
"addon.mod_forum.reply": "Reply",
"addon.mod_forum.replyplaceholder": "Write your reply...",
"addon.mod_forum.subject": "Subject",
+ "addon.mod_forum.tagarea_forum_posts": "Forum posts",
"addon.mod_forum.thisforumhasduedate": "The due date for posting to this forum is {{$a}}.",
"addon.mod_forum.thisforumisdue": "The due date for posting to this forum was {{$a}}.",
"addon.mod_forum.unlockdiscussion": "Unlock this discussion",
@@ -566,9 +636,11 @@
"addon.mod_glossary.modulenameplural": "Glossaries",
"addon.mod_glossary.noentriesfound": "No entries were found.",
"addon.mod_glossary.searchquery": "Search query",
+ "addon.mod_glossary.tagarea_glossary_entries": "Glossary entries",
"addon.mod_imscp.deploymenterror": "Content package error!",
"addon.mod_imscp.modulenameplural": "IMS content packages",
"addon.mod_imscp.showmoduledescription": "Show description",
+ "addon.mod_imscp.toc": "TOC",
"addon.mod_lesson.answer": "Answer",
"addon.mod_lesson.attempt": "Attempt: {{$a}}",
"addon.mod_lesson.attemptheader": "Attempt",
@@ -665,6 +737,7 @@
"addon.mod_quiz.attemptnumber": "Attempt",
"addon.mod_quiz.attemptquiznow": "Attempt quiz now",
"addon.mod_quiz.attemptstate": "State",
+ "addon.mod_quiz.canattemptbutnotsubmit": "You can attempt this quiz in the app, but you will need to submit the attempt in browser for the following reasons:",
"addon.mod_quiz.cannotsubmitquizdueto": "This quiz attempt cannot be submitted for the following reasons:",
"addon.mod_quiz.clearchoice": "Clear my choice",
"addon.mod_quiz.comment": "Comment",
@@ -683,7 +756,7 @@
"addon.mod_quiz.errorgetquestions": "Error getting questions.",
"addon.mod_quiz.errorgetquiz": "Error getting quiz data.",
"addon.mod_quiz.errorparsequestions": "An error occurred while reading the questions. Please attempt this quiz in a web browser.",
- "addon.mod_quiz.errorquestionsnotsupported": "This quiz can't be attempted in the app because it contains questions not supported by the app:",
+ "addon.mod_quiz.errorquestionsnotsupported": "This quiz can't be attempted in the app because it only contains questions not supported by the app:",
"addon.mod_quiz.errorrulesnotsupported": "This quiz can't be attempted in the app because it has access rules not supported by the app:",
"addon.mod_quiz.errorsaveattempt": "An error occurred while saving the attempt data.",
"addon.mod_quiz.feedback": "Feedback",
@@ -737,6 +810,7 @@
"addon.mod_quiz.warningattemptfinished": "Offline attempt discarded as it was finished on the site or not found.",
"addon.mod_quiz.warningdatadiscarded": "Some offline answers were discarded because the questions were modified online.",
"addon.mod_quiz.warningdatadiscardedfromfinished": "Attempt unfinished because some offline answers were discarded. Please review your answers then resubmit the attempt.",
+ "addon.mod_quiz.warningquestionsnotsupported": "This quiz contains questions not supported by the app:",
"addon.mod_quiz.yourfinalgradeis": "Your final grade for this quiz is {{$a}}.",
"addon.mod_resource.errorwhileloadingthecontent": "Error while loading the content.",
"addon.mod_resource.modifieddate": "Modified {{$a}}",
@@ -820,6 +894,7 @@
"addon.mod_wiki.pageexists": "This page already exists.",
"addon.mod_wiki.pagename": "Page name",
"addon.mod_wiki.subwiki": "Sub-wiki",
+ "addon.mod_wiki.tagarea_wiki_pages": "Wiki pages",
"addon.mod_wiki.titleshouldnotbeempty": "The title should not be empty",
"addon.mod_wiki.viewpage": "View page",
"addon.mod_wiki.wikipage": "Wiki page",
@@ -898,7 +973,9 @@
"addon.mod_workshop_assessment_rubric.mustchooseone": "You have to select one of these items",
"addon.notes.addnewnote": "Add a new note",
"addon.notes.coursenotes": "Course notes",
+ "addon.notes.deleteconfirm": "Delete this note?",
"addon.notes.eventnotecreated": "Note created",
+ "addon.notes.eventnotedeleted": "Note deleted",
"addon.notes.nonotes": "There are no notes of this type yet",
"addon.notes.note": "Note",
"addon.notes.notes": "Notes",
@@ -1231,6 +1308,7 @@
"core.answered": "Answered",
"core.areyousure": "Are you sure?",
"core.back": "Back",
+ "core.block.blocks": "Blocks",
"core.cancel": "Cancel",
"core.cannotconnect": "Cannot connect: Verify that you have correctly typed the URL and that your site uses Moodle 2.4 or later.",
"core.cannotdownloadfiles": "File downloading is disabled. Please contact your site administrator.",
@@ -1245,9 +1323,16 @@
"core.clicktohideshow": "Click to expand or collapse",
"core.clicktoseefull": "Click to see full contents.",
"core.close": "Close",
- "core.comments": "Comments",
- "core.commentscount": "Comments ({{$a}})",
- "core.commentsnotworking": "Comments cannot be retrieved",
+ "core.comments.addcomment": "Add a comment...",
+ "core.comments.comments": "Comments",
+ "core.comments.commentscount": "Comments ({{$a}})",
+ "core.comments.commentsnotworking": "Comments cannot be retrieved",
+ "core.comments.deletecommentbyon": "Delete comment posted by {{$a.user}} on {{$a.time}}",
+ "core.comments.eventcommentcreated": "Comment created",
+ "core.comments.eventcommentdeleted": "Comment deleted",
+ "core.comments.nocomments": "No comments",
+ "core.comments.savecomment": "Save comment",
+ "core.comments.warningcommentsnotsent": "Couldn't sync comments. {{error}}",
"core.completion-alt-auto-fail": "Completed: {{$a}} (did not achieve pass grade)",
"core.completion-alt-auto-n": "Not completed: {{$a}}",
"core.completion-alt-auto-n-override": "Not completed: {{$a.modname}} (set by {{$a.overrideuser}})",
@@ -1260,6 +1345,8 @@
"core.completion-alt-manual-y-override": "Completed: {{$a.modname}} (set by {{$a.overrideuser}}). Select to mark as not complete.",
"core.confirmcanceledit": "Are you sure you want to leave this page? All changes will be lost.",
"core.confirmdeletefile": "Are you sure you want to delete this file?",
+ "core.confirmgotabroot": "Are you sure you want to go back to {{name}}?",
+ "core.confirmgotabrootdefault": "Are you sure you want to go to the initial page of the current tab?",
"core.confirmloss": "Are you sure? All changes will be lost.",
"core.confirmopeninbrowser": "Do you want to open it in a web browser?",
"core.considereddigitalminor": "You are too young to create an account on this site.",
@@ -1306,6 +1393,7 @@
"core.course.warningmanualcompletionmodified": "The manual completion of an activity was modified on the site.",
"core.course.warningofflinemanualcompletiondeleted": "Some offline manual completion of course '{{name}}' has been deleted. {{error}}",
"core.coursedetails": "Course details",
+ "core.coursenogroups": "You are not a member of any group of this course.",
"core.courses.addtofavourites": "Star this course",
"core.courses.allowguests": "This course allows guest users to enter",
"core.courses.availablecourses": "Available courses",
@@ -1317,12 +1405,13 @@
"core.courses.enrolme": "Enrol me",
"core.courses.errorloadcategories": "An error occurred while loading categories.",
"core.courses.errorloadcourses": "An error occurred while loading courses.",
- "core.courses.errorloadplugins": "The plugins required by this course could not be loaded correctly. Please restart the app to try again.",
+ "core.courses.errorloadplugins": "The plugins required by this course could not be loaded correctly. Please reload the app to try again.",
"core.courses.errorsearching": "An error occurred while searching.",
"core.courses.errorselfenrol": "An error occurred while self enrolling.",
"core.courses.filtermycourses": "Filter my courses",
"core.courses.frontpage": "Front page",
"core.courses.hidecourse": "Hide from view",
+ "core.courses.ignore": "Ignore",
"core.courses.mycourses": "My courses",
"core.courses.mymoodle": "Dashboard",
"core.courses.nocourses": "No course information to show.",
@@ -1333,6 +1422,7 @@
"core.courses.password": "Enrolment key",
"core.courses.paymentrequired": "This course requires a payment for entry.",
"core.courses.paypalaccepted": "PayPal payments accepted",
+ "core.courses.reload": "Reload",
"core.courses.removefromfavourites": "Unstar this course",
"core.courses.search": "Search",
"core.courses.searchcourses": "Search courses",
@@ -1383,6 +1473,7 @@
"core.erroropenfilenoextension": "Error opening file: the file doesn't have an extension.",
"core.erroropenpopup": "This activity is trying to open a popup. This is not supported in the app.",
"core.errorrenamefile": "Error renaming file. Please try again.",
+ "core.errorsomedatanotdownloaded": "If you downloaded this activity, please notice that some data isn't downloaded during the download process for performance and data usage reasons.",
"core.errorsync": "An error occurred while synchronising. Please try again.",
"core.errorsyncblocked": "This {{$a}} cannot be synchronised right now because of an ongoing process. Please try again later. If the problem persists, try restarting the app.",
"core.explanationdigitalminor": "This information is required to determine if your age is over the digital age of consent. This is the age when an individual can consent to terms and conditions and their data being legally stored and processed.",
@@ -1435,6 +1526,7 @@
"core.grades.range": "Range",
"core.grades.rank": "Rank",
"core.grades.weight": "Weight",
+ "core.group": "Group",
"core.groupsseparate": "Separate groups",
"core.groupsvisible": "Visible groups",
"core.hasdatatosync": "This {{$a}} has offline data to be synchronised.",
@@ -1446,6 +1538,7 @@
"core.image": "Image",
"core.imageviewer": "Image viewer",
"core.info": "Information",
+ "core.invalidformdata": "Incorrect form data",
"core.ios": "iOS",
"core.labelsep": ":",
"core.lastaccess": "Last access",
@@ -1465,6 +1558,8 @@
"core.login.confirmdeletesite": "Are you sure you want to delete the site {{sitename}}?",
"core.login.connect": "Connect!",
"core.login.connecttomoodle": "Connect to Moodle",
+ "core.login.connecttomoodleapp": "You are trying to connect to a regular Moodle site. Please download the official Moodle app to access this site.",
+ "core.login.connecttoworkplaceapp": "You are trying to connect to a Moodle Workplace site. Please download the Moodle Workplace app to access this site.",
"core.login.contactyouradministrator": "Contact your site administrator for further help.",
"core.login.contactyouradministratorissue": "Please ask your site administrator to check the following issue: {{$a}}",
"core.login.createaccount": "Create my new account",
@@ -1494,7 +1589,7 @@
"core.login.invalidurl": "Invalid URL specified",
"core.login.invalidvaluemax": "The maximum value is {{$a}}",
"core.login.invalidvaluemin": "The minimum value is {{$a}}",
- "core.login.legacymoodleversion": "You are trying to connect to an unsupported Moodle version. Please, download the Moodle Classic app to access this Moodle site.",
+ "core.login.legacymoodleversion": "You are trying to connect to an unsupported Moodle version. Please download the Moodle Classic app to access this Moodle site.",
"core.login.legacymoodleversiondesktop": "You are trying to connect to {{$a}} . This site is running an outdated unsupported version of Moodle which will not work with this Moodle Desktop App. If this is your site please contact your local moodle partner to get assistance to update it. See our contact page to submit a request for assistance.",
"core.login.legacymoodleversiondesktopdownloadold": " Alternatively, you can still access this site using an unsupported version of the app that can be downloaded from here.",
"core.login.localmobileunexpectedresponse": "Moodle Mobile Additional Features check returned an unexpected response. You will be authenticated using the standard mobile service.",
@@ -1595,19 +1690,20 @@
"core.never": "Never",
"core.next": "Next",
"core.no": "No",
- "core.nocomments": "No comments",
"core.nograde": "No grade",
"core.none": "None",
"core.nopasswordchangeforced": "You cannot proceed without changing your password.",
"core.nopermissionerror": "Sorry, but you do not currently have permissions to do that",
"core.nopermissions": "Sorry, but you do not currently have permissions to do that ({{$a}}).",
"core.noresults": "No results",
+ "core.noselection": "No selection",
"core.notapplicable": "n/a",
"core.notenrolledprofile": "This profile is not available because this user is not enrolled in this course.",
"core.notice": "Notice",
"core.notingroup": "Sorry, but you need to be part of a group to see this page.",
"core.notsent": "Not sent",
"core.now": "now",
+ "core.nummore": "{{$a}} more",
"core.numwords": "{{$a}} words",
"core.offline": "Offline",
"core.ok": "OK",
@@ -1670,6 +1766,9 @@
"core.sec": "sec",
"core.secs": "secs",
"core.seemoredetail": "Click here to see more detail",
+ "core.selectacategory": "Please select a category",
+ "core.selectacourse": "Select a course",
+ "core.selectagroup": "Select a group",
"core.send": "Send",
"core.sending": "Sending",
"core.serverconnection": "Error connecting to the server",
@@ -1695,6 +1794,8 @@
"core.settings.disabled": "Disabled",
"core.settings.displayformat": "Display format",
"core.settings.enabledownloadsection": "Enable download sections",
+ "core.settings.enablefirebaseanalytics": "Enable Firebase analytics",
+ "core.settings.enablefirebaseanalyticsdescription": "If enabled, the app will collect anonymous data usage.",
"core.settings.enablerichtexteditor": "Enable text editor",
"core.settings.enablerichtexteditordescription": "If enabled, a text editor will be available when entering content.",
"core.settings.enablesyncwifi": "Allow sync only when on Wi-Fi",
@@ -1703,6 +1804,8 @@
"core.settings.errorsyncsite": "Error synchronising site data. Please check your Internet connection and try again.",
"core.settings.estimatedfreespace": "Estimated free space",
"core.settings.filesystemroot": "File system root",
+ "core.settings.fontsize": "Text size",
+ "core.settings.fontsizecharacter": "A",
"core.settings.general": "General",
"core.settings.language": "Language",
"core.settings.license": "Licence",
@@ -1738,6 +1841,7 @@
"core.sharedfiles.sharedfiles": "Shared files",
"core.sharedfiles.successstorefile": "File successfully stored. Select the file to upload to your private files or use in an activity.",
"core.show": "Show",
+ "core.showless": "Show less...",
"core.showmore": "Show more...",
"core.site": "Site",
"core.sitehome.sitehome": "Site home",
@@ -1770,6 +1874,20 @@
"core.submit": "Submit",
"core.success": "Success",
"core.tablet": "Tablet",
+ "core.tag.defautltagcoll": "Default collection",
+ "core.tag.errorareanotsupported": "This tag area is not supported by the app.",
+ "core.tag.inalltagcoll": "Everywhere",
+ "core.tag.itemstaggedwith": "{{$a.tagarea}} tagged with \"{{$a.tag}}\"",
+ "core.tag.notagsfound": "No tags matching \"{{$a}}\" found",
+ "core.tag.searchtags": "Search tags",
+ "core.tag.showingfirsttags": "Showing {{$a}} most popular tags",
+ "core.tag.tag": "Tag",
+ "core.tag.tagarea_course": "Courses",
+ "core.tag.tagarea_course_modules": "Activities and resources",
+ "core.tag.tagarea_post": "Blog posts",
+ "core.tag.tagarea_user": "User interests",
+ "core.tag.tags": "Tags",
+ "core.tag.warningareasnotsupported": "Some of the tag areas are not displayed because they are not supported by the app.",
"core.teachers": "Teachers",
"core.thereisdatatosync": "There are offline {{$a}} to be synchronised.",
"core.thisdirection": "ltr",
@@ -1786,6 +1904,7 @@
"core.unlimited": "Unlimited",
"core.unzipping": "Unzipping",
"core.upgraderunning": "Site is being upgraded, please retry later.",
+ "core.user": "User",
"core.user.address": "Address",
"core.user.city": "City/town",
"core.user.contact": "Contact",
diff --git a/src/classes/site.ts b/src/classes/site.ts
index 9253f83e631..4f885fe9312 100644
--- a/src/classes/site.ts
+++ b/src/classes/site.ts
@@ -21,7 +21,7 @@ import { CoreDbProvider } from '@providers/db';
import { CoreEventsProvider } from '@providers/events';
import { CoreFileProvider } from '@providers/file';
import { CoreLoggerProvider } from '@providers/logger';
-import { CoreWSProvider, CoreWSPreSets, CoreWSFileUploadOptions } from '@providers/ws';
+import { CoreWSProvider, CoreWSPreSets, CoreWSFileUploadOptions, CoreWSAjaxPreSets } from '@providers/ws';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreTextUtilsProvider } from '@providers/utils/text';
import { CoreTimeUtilsProvider } from '@providers/utils/time';
@@ -60,6 +60,12 @@ export interface CoreSiteWSPreSets {
*/
emergencyCache?: boolean;
+ /**
+ * If true, the app won't call the WS. If the data isn't cached, the call will fail.
+ * @type {boolean}
+ */
+ forceOffline?: boolean;
+
/**
* Extra key to add to the cache when storing this call, to identify the entry.
* @type {string}
@@ -668,7 +674,12 @@ export class CoreSite {
}
const promise = this.getFromCache(method, data, preSets, false, originalData).catch(() => {
- // Do not pass those options to the core WS factory.
+ if (preSets.forceOffline) {
+ // Don't call the WS, just fail.
+ return Promise.reject(this.wsProvider.createFakeWSError('core.cannotconnect', true));
+ }
+
+ // Call the WS.
return this.callOrEnqueueRequest(method, data, preSets, wsPreSets).then((response) => {
if (preSets.saveToCache) {
this.saveToCache(method, data, response, preSets);
@@ -739,15 +750,15 @@ export class CoreSite {
// Save the error instead of deleting the cache entry so the same content is displayed in offline.
this.saveToCache(method, data, error, preSets);
- return Promise.reject(error);
- } else if (typeof preSets.emergencyCache !== 'undefined' && !preSets.emergencyCache) {
- this.logger.debug(`WS call '${method}' failed. Emergency cache is forbidden, rejecting.`);
-
return Promise.reject(error);
} else if (preSets.cacheErrors && preSets.cacheErrors.indexOf(error.errorcode) != -1) {
// Save the error instead of deleting the cache entry so the same content is displayed in offline.
this.saveToCache(method, data, error, preSets);
+ return Promise.reject(error);
+ } else if (typeof preSets.emergencyCache !== 'undefined' && !preSets.emergencyCache) {
+ this.logger.debug(`WS call '${method}' failed. Emergency cache is forbidden, rejecting.`);
+
return Promise.reject(error);
}
@@ -1043,7 +1054,7 @@ export class CoreSite {
const now = Date.now();
let expirationTime;
- preSets.omitExpires = preSets.omitExpires || !this.appProvider.isOnline();
+ preSets.omitExpires = preSets.omitExpires || preSets.forceOffline || !this.appProvider.isOnline();
if (!preSets.omitExpires) {
let expirationDelay = this.UPDATE_FREQUENCIES[preSets.updateFrequency] ||
@@ -1323,7 +1334,7 @@ export class CoreSite {
return Promise.resolve({ code: 0 });
}
- const promise = this.http.post(checkUrl, { service: service }).timeout(CoreConstants.WS_TIMEOUT).toPromise();
+ const promise = this.http.post(checkUrl, { service: service }).timeout(this.wsProvider.getRequestTimeout()).toPromise();
return promise.then((data: any) => {
if (typeof data != 'undefined' && data.errorcode === 'requirecorrectaccess') {
@@ -1432,7 +1443,30 @@ export class CoreSite {
* @return {Promise} Promise resolved with public config. Rejected with an object if error, see CoreWSProvider.callAjax.
*/
getPublicConfig(): Promise {
- return this.wsProvider.callAjax('tool_mobile_get_public_config', {}, { siteUrl: this.siteUrl }).then((config) => {
+ const preSets: CoreWSAjaxPreSets = {
+ siteUrl: this.siteUrl
+ };
+
+ return this.wsProvider.callAjax('tool_mobile_get_public_config', {}, preSets).catch((error) => {
+
+ if ((!this.getInfo() || this.isVersionGreaterEqualThan('3.8')) && error && error.errorcode == 'codingerror') {
+ // This error probably means that there is a redirect in the site. Try to use a GET request.
+ preSets.noLogin = true;
+ preSets.useGet = true;
+
+ return this.wsProvider.callAjax('tool_mobile_get_public_config', {}, preSets).catch((error2) => {
+ if (this.getInfo() && this.isVersionGreaterEqualThan('3.8')) {
+ // GET is supported, return the second error.
+ return Promise.reject(error2);
+ } else {
+ // GET not supported or we don't know if it's supported. Return first error.
+ return Promise.reject(error);
+ }
+ });
+ }
+
+ return Promise.reject(error);
+ }).then((config) => {
// Use the wwwroot returned by the server.
if (config.httpswwwroot) {
this.siteUrl = config.httpswwwroot;
diff --git a/src/components/context-menu/context-menu.scss b/src/components/context-menu/context-menu.scss
index 138e4b4c727..43c1468e5f7 100644
--- a/src/components/context-menu/context-menu.scss
+++ b/src/components/context-menu/context-menu.scss
@@ -1,4 +1,7 @@
ion-app.app-root core-context-menu-popover {
+ .list {
+ margin-bottom: 0;
+ }
.item-md ion-icon[item-start] + .item-inner,
.item-md ion-icon[item-start] + .item-input {
@include margin-horizontal(5px, null);
diff --git a/src/components/context-menu/context-menu.ts b/src/components/context-menu/context-menu.ts
index ff6413b6773..42fca4504a9 100644
--- a/src/components/context-menu/context-menu.ts
+++ b/src/components/context-menu/context-menu.ts
@@ -178,7 +178,7 @@ export class CoreContextMenuComponent implements OnInit, OnDestroy {
showContextMenu(event: MouseEvent): void {
if (!this.expanded) {
const popover = this.popoverCtrl.create(CoreContextMenuPopoverComponent,
- { title: this.title, items: this.items, id: this.uniqueId });
+ { title: this.title, items: this.items, id: this.uniqueId, showBackdrop: true });
popover.onDidDismiss(() => {
this.expanded = false;
diff --git a/src/components/empty-box/empty-box.scss b/src/components/empty-box/empty-box.scss
index 5d265575c55..e5d0c3edb0d 100644
--- a/src/components/empty-box/empty-box.scss
+++ b/src/components/empty-box/empty-box.scss
@@ -61,3 +61,8 @@ ion-app.app-root core-empty-box {
}
}
}
+
+ion-app.app-root core-block-course-blocks core-empty-box .core-empty-box {
+ position: relative;
+}
+
diff --git a/src/components/iframe/iframe.ts b/src/components/iframe/iframe.ts
index 5988289a735..722d9eae633 100644
--- a/src/components/iframe/iframe.ts
+++ b/src/components/iframe/iframe.ts
@@ -12,11 +12,15 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import { Component, Input, Output, OnInit, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange } from '@angular/core';
+import {
+ Component, Input, Output, OnInit, ViewChild, ElementRef, EventEmitter, OnChanges, SimpleChange, Optional
+} from '@angular/core';
import { DomSanitizer, SafeResourceUrl } from '@angular/platform-browser';
+import { NavController } from 'ionic-angular';
import { CoreLoggerProvider } from '@providers/logger';
import { CoreDomUtilsProvider } from '@providers/utils/dom';
import { CoreIframeUtilsProvider } from '@providers/utils/iframe';
+import { CoreSplitViewComponent } from '@components/split-view/split-view';
/**
*/
@@ -38,7 +42,9 @@ export class CoreIframeComponent implements OnInit, OnChanges {
protected IFRAME_TIMEOUT = 15000;
constructor(logger: CoreLoggerProvider, private iframeUtils: CoreIframeUtilsProvider, private domUtils: CoreDomUtilsProvider,
- private sanitizer: DomSanitizer) {
+ private sanitizer: DomSanitizer, private navCtrl: NavController,
+ @Optional() private svComponent: CoreSplitViewComponent) {
+
this.logger = logger.getInstance('CoreIframe');
this.loaded = new EventEmitter();
}
@@ -55,7 +61,8 @@ export class CoreIframeComponent implements OnInit, OnChanges {
// Show loading only with external URLs.
this.loading = !this.src || !!this.src.match(/^https?:\/\//i);
- this.iframeUtils.treatFrame(iframe);
+ const navCtrl = this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
+ this.iframeUtils.treatFrame(iframe, false, navCtrl);
if (this.loading) {
iframe.addEventListener('load', () => {
diff --git a/src/components/ion-tabs/core-ion-tabs.html b/src/components/ion-tabs/core-ion-tabs.html
index 123d340c1c4..5a4dda4b37b 100644
--- a/src/components/ion-tabs/core-ion-tabs.html
+++ b/src/components/ion-tabs/core-ion-tabs.html
@@ -1,5 +1,5 @@
-
+
diff --git a/src/components/ion-tabs/ion-tabs.scss b/src/components/ion-tabs/ion-tabs.scss
index fabba182802..31f66765086 100644
--- a/src/components/ion-tabs/ion-tabs.scss
+++ b/src/components/ion-tabs/ion-tabs.scss
@@ -23,6 +23,12 @@ ion-app.app-root core-ion-tabs {
background-color: $ion-tabs-badge-color;
}
+ &[tabsplacement="bottom"] {
+ .ion-page > ion-content > .scroll-content {
+ margin-bottom: $navbar-md-height;
+ }
+ }
+
&[tabsplacement="side"] {
.tabbar {
@include float(start);
@@ -53,6 +59,10 @@ ion-app.app-root core-ion-tabs {
position: relative;
}
}
+
+ .scroll-content, .fixed-content {
+ margin-bottom: 0 !important;
+ }
}
}
@@ -79,9 +89,6 @@ core-ion-tabs, core-ion-tab {
width: 100%;
height: 100%;
overflow: hidden;
-
- // Do not use "contain: strict" so fullscreen iframes work.
- contain: size layout style;
}
core-ion-tab {
diff --git a/src/components/ion-tabs/ion-tabs.ts b/src/components/ion-tabs/ion-tabs.ts
index deeb76a531b..914d97ab6aa 100644
--- a/src/components/ion-tabs/ion-tabs.ts
+++ b/src/components/ion-tabs/ion-tabs.ts
@@ -20,11 +20,14 @@ import {
import { CoreIonTabComponent } from './ion-tab';
import { CoreUtilsProvider, PromiseDefer } from '@providers/utils/utils';
import { CoreAppProvider } from '@providers/app';
+import { CoreDomUtilsProvider } from '@providers/utils/dom';
+import { TranslateService } from '@ngx-translate/core';
/**
- * Equivalent to ion-tabs. It has 2 improvements:
+ * Equivalent to ion-tabs. It has several improvements:
* - If a core-ion-tab is added or removed, it will be reflected in the tab bar in the right position.
* - It supports a loaded input to tell when are the tabs ready.
+ * - When the user clicks the tab again to go to root, a confirm modal is shown.
*/
@Component({
selector: 'core-ion-tabs',
@@ -73,7 +76,8 @@ export class CoreIonTabsComponent extends Tabs implements OnDestroy {
constructor(protected utils: CoreUtilsProvider, protected appProvider: CoreAppProvider, @Optional() parent: NavController,
@Optional() viewCtrl: ViewController, _app: App, config: Config, elementRef: ElementRef, _plt: Platform,
- renderer: Renderer, _linker: DeepLinker, keyboard?: Keyboard) {
+ renderer: Renderer, _linker: DeepLinker, protected domUtils: CoreDomUtilsProvider,
+ protected translate: TranslateService, keyboard?: Keyboard) {
super(parent, viewCtrl, _app, config, elementRef, _plt, renderer, _linker, keyboard);
}
@@ -272,13 +276,25 @@ export class CoreIonTabsComponent extends Tabs implements OnDestroy {
*
* @param {number|Tab} tabOrIndex Index, or the Tab instance, of the tab to select.
* @param {NavOptions} Nav options.
- * @param {boolean} [fromUrl=true] Whether to load from a URL.
+ * @param {boolean} [fromUrl] Whether to load from a URL.
+ * @param {boolean} [manualClick] Whether the user manually clicked the tab.
* @return {Promise} Promise resolved when selected.
*/
- select(tabOrIndex: number | Tab, opts: NavOptions = {}, fromUrl: boolean = false): Promise {
+ select(tabOrIndex: number | Tab, opts: NavOptions = {}, fromUrl?: boolean, manualClick?: boolean): Promise {
if (this.initialized) {
// Tabs have been initialized, select the tab.
+ if (manualClick) {
+ // If we'll go to the root of the current tab, ask the user to confirm first.
+ const tab = typeof tabOrIndex == 'number' ? this.getByIndex(tabOrIndex) : tabOrIndex;
+
+ return this.confirmGoToRoot(tab).then(() => {
+ return super.select(tabOrIndex, opts, fromUrl);
+ }, () => {
+ // User cancelled.
+ });
+ }
+
return super.select(tabOrIndex, opts, fromUrl);
} else {
// Tabs not initialized yet. Mark it as "selectedIndex" input so it's treated when the tabs are initialized.
@@ -305,11 +321,16 @@ export class CoreIonTabsComponent extends Tabs implements OnDestroy {
if (this.initialized) {
const tab = this.getByIndex(index);
if (tab) {
- return tab.goToRoot({animate: false, updateUrl: true, isNavRoot: true}).then(() => {
- // Tab not previously selected. Select it after going to root.
- if (!tab.isSelected) {
- return this.select(tab, {animate: false, updateUrl: true, isNavRoot: true});
- }
+ return this.confirmGoToRoot(tab).then(() => {
+ // User confirmed, go to root.
+ return tab.goToRoot({animate: tab.isSelected, updateUrl: true, isNavRoot: true}).then(() => {
+ // Tab not previously selected. Select it after going to root.
+ if (!tab.isSelected) {
+ return this.select(tab, {animate: false, updateUrl: true, isNavRoot: true});
+ }
+ });
+ }, () => {
+ // User cancelled.
});
}
@@ -349,4 +370,23 @@ export class CoreIonTabsComponent extends Tabs implements OnDestroy {
// Unregister the custom back button action for this page
this.unregisterBackButtonAction && this.unregisterBackButtonAction();
}
+
+ /**
+ * Confirm if the user wants to go to the root of the current tab.
+ *
+ * @param {Tab} tab Tab to go to root.
+ * @return {Promise} Promise resolved when confirmed.
+ */
+ confirmGoToRoot(tab: Tab): Promise {
+ if (!tab || !tab.isSelected || (tab.getActive() && tab.getActive().isFirst())) {
+ // Tab not selected or is already at root, no need to confirm.
+ return Promise.resolve();
+ } else {
+ if (tab.tabTitle) {
+ return this.domUtils.showConfirm(this.translate.instant('core.confirmgotabroot', {name: tab.tabTitle}));
+ } else {
+ return this.domUtils.showConfirm(this.translate.instant('core.confirmgotabrootdefault'));
+ }
+ }
+ }
}
diff --git a/src/components/loading/loading.scss b/src/components/loading/loading.scss
index 92f00180552..bc1faec9fe3 100644
--- a/src/components/loading/loading.scss
+++ b/src/components/loading/loading.scss
@@ -21,12 +21,14 @@ ion-app.app-root {
.scroll-content > core-loading,
ion-content > .scroll-content > core-loading,
+ core-tab core-loading,
.core-loading-center {
position: static !important;
}
.scroll-content > core-loading,
ion-content > .scroll-content > core-loading,
+ core-tab core-loading,
.core-loading-center,
core-loading.core-loading-loaded {
position: relative;
diff --git a/src/components/navbar-buttons/navbar-buttons.ts b/src/components/navbar-buttons/navbar-buttons.ts
index 7f4ae239be7..c38931322a7 100644
--- a/src/components/navbar-buttons/navbar-buttons.ts
+++ b/src/components/navbar-buttons/navbar-buttons.ts
@@ -25,6 +25,8 @@ import { CoreContextMenuComponent } from '../context-menu/context-menu';
* If this component indicates a position (start/end), the buttons will only be added if the header has some buttons in that
* position. If no start/end is specified, then the buttons will be added to the first found in the header.
*
+ * If this component has a "prepend" attribute, the buttons will be added before other existing buttons in the header.
+ *
* You can use the [hidden] input to hide all the inner buttons if a certain condition is met.
*
* IMPORTANT: Do not use *ngIf in the buttons inside this component, it can cause problems. Please use [hidden] instead.
@@ -92,7 +94,8 @@ export class CoreNavBarButtonsComponent implements OnInit, OnDestroy {
if (buttonsContainer) {
this.mergeContextMenus(buttonsContainer);
- this.movedChildren = this.domUtils.moveChildren(this.element, buttonsContainer);
+ const prepend = this.element.hasAttribute('prepend');
+ this.movedChildren = this.domUtils.moveChildren(this.element, buttonsContainer, prepend);
this.showHideAllElements();
} else {
diff --git a/src/components/rich-text-editor/core-rich-text-editor.html b/src/components/rich-text-editor/core-rich-text-editor.html
index 7a02610ddf7..ba2bd897f53 100644
--- a/src/components/rich-text-editor/core-rich-text-editor.html
+++ b/src/components/rich-text-editor/core-rich-text-editor.html
@@ -1,33 +1,81 @@
-