Compare commits

..

8 Commits

Author SHA1 Message Date
9c30211ba2 feat: splash transition
refactor: require app reload for setting changes

feat: require reload on setting changes

feat: new logo

feat: update to capacitor 5

feat: new logo

feat: update to capacitor 5

refactor: simplify settings provider
2024-01-03 12:27:42 +00:00
63a38e0077 feat: enable checkJs by default 2024-01-03 12:15:15 +00:00
c8b260201c feat: add direnv for nix
feat: update nix flake to not rely on buildFHSUserEnv
2024-01-03 12:15:15 +00:00
123c50d1af fix: backend tests break every year
refactor: update some backend unit tests
2024-01-03 12:57:24 +01:00
Rainer Killinger
d65e6351e9 fix: iOS build resources 2023-12-21 12:51:39 +01:00
Rainer Killinger
2c5d7403db refactor: add asdf tool versioning file 2023-12-21 12:25:38 +01:00
6ca03f463d fix: changeset crashes because it uses internal prettier version 2023-12-21 11:26:01 +01:00
Rainer Killinger
1f74a9bc82 refactor: overhaul minimal-deployment compose file 2023-12-19 16:52:58 +01:00
145 changed files with 1110 additions and 1850 deletions

View File

@@ -1,9 +0,0 @@
---
"@openstapps/node-builder": patch
"@openstapps/database": patch
"@openstapps/node-base": patch
"@openstapps/backend": patch
"@openstapps/app": patch
---
pin alpine version to 3.18 and add healthchecks

View File

@@ -0,0 +1,5 @@
---
"@openstapps/prettier-config": patch
---
Update Prettier to 3.1.1

View File

@@ -0,0 +1,5 @@
---
"@openstapps/backend": patch
---
Backend unit tests break every year

View File

@@ -0,0 +1,9 @@
---
'@openstapps/app': minor
---
Require full reload for setting & language changes
Setting changes are relatively rare, so it makes little sense
going through the effort of ensuring everything is reactive to
language changes as well as creating all the pipe bindings etc.

1
.envrc Normal file
View File

@@ -0,0 +1 @@
use flake

1
.gitignore vendored
View File

@@ -24,6 +24,7 @@ report-junit.xml
# NixOS flake
result
hsperfdata_root
.direnv/
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

3
.tool-versions Normal file
View File

@@ -0,0 +1,3 @@
nodejs 18.16.1
pnpm 8.8.0
python 3.11.5

View File

@@ -1,4 +1,4 @@
# Open StApps Monorepo
# <img src="logo-bg.svg" height="24"> Open StApps Monorepo
Refer to the [contribution guide](./CONTRIBUTING.md)

View File

@@ -9,6 +9,4 @@ ENV NODE_ENV=production
WORKDIR /app
EXPOSE 3000
HEALTHCHECK --interval=10s --timeout=10s --start-period=10s --retries=12 CMD curl -s --fail --request POST --data '{}' --header 'Content-Type: application/json' http://localhost:3000/ >/dev/null || exit 1
ENTRYPOINT ["node", "app.js"]

View File

@@ -1,5 +1,3 @@
// @ts-check
/**
* This is the database configuration for the technical university of berlin
*

View File

@@ -1,4 +1,3 @@
// @ts-check
import {SCSettingInputType, SCThingOriginType, SCThingType} from '@openstapps/core';
/** @type {import('@openstapps/core').SCLanguageSetting} */

View File

@@ -1,4 +1,3 @@
// @ts-check
/** @type {import('@openstapps/core').SCAppConfigurationMenuCategory[]} */
const menus = [
{

View File

@@ -1,4 +1,3 @@
// @ts-check
import {SCSettingInputType, SCThingOriginType, SCThingType} from '@openstapps/core';
/** @type {import('@openstapps/core').SCUserGroupSetting} */

View File

@@ -1,4 +1,3 @@
// @ts-check
import {SCThingType} from '@openstapps/core';
/** @type {import('@openstapps/core').SCBackendAggregationConfiguration[]} */

View File

@@ -1,4 +1,3 @@
// @ts-check
import {
month,
sommerRange,

View File

@@ -1,4 +1,3 @@
// @ts-check
import {SCThingType} from '@openstapps/core';
import aggregations from './aggregations.js';
import boostings from './boostings.js';
@@ -17,7 +16,7 @@ export const backend = {
hiddenTypes: [SCThingType.DateSeries, SCThingType.Diff, SCThingType.Floor],
mappingIgnoredTags: ['minlength', 'pattern', 'see', 'tjs-format'],
maxMultiSearchRouteQueries: 5,
maxRequestBodySize: 2 * 10 ** 6,
maxRequestBodySize: 2e6,
name: 'Goethe-Universität Frankfurt am Main',
namespace: '909a8cbc-8520-456c-b474-ef1525f14209',
sortableFields: [

View File

@@ -1,4 +1,3 @@
// @ts-check
import app from './app/index.js';
import {backend, internal} from './backend/index.js';

View File

@@ -1,5 +1,3 @@
// @ts-check
/**
* This is the default configuration for elasticsearch (a database)
*

View File

@@ -1,4 +1,3 @@
// @ts-check
import {readFile} from 'fs/promises';
import {SCAboutPageContentType} from '@openstapps/core';

View File

@@ -1,4 +1,3 @@
// @ts-check
/**
* Generates a range of numbers that represent consecutive calendar months
*

View File

@@ -1,4 +1,3 @@
// @ts-check
import {readFile, readdir} from 'fs/promises';
import url from 'url';
import path from 'path';

View File

@@ -1,4 +1,3 @@
// @ts-check
import {SCAboutPageContentType} from '@openstapps/core';
import {markdown} from '../../default/tools/markdown.js';

View File

@@ -1,4 +1,3 @@
// @ts-check
import {SCAboutPageContentType} from '@openstapps/core';
/** @type {import('@openstapps/core').SCAboutPage} */

View File

@@ -1,4 +1,3 @@
// @ts-check
import about from './about.js';
import imprint from './imprint.js';
import privacy from './privacy.js';

View File

@@ -1,4 +1,3 @@
// @ts-check
import {markdown} from '../../default/tools/markdown.js';
/** @type {import('@openstapps/core').SCAboutPage} */

View File

@@ -1,4 +1,3 @@
// @ts-check
import aboutPages from './about-pages/index.js';
import defaultApp from '../default/app/index.js';
import {backend as defaultBackend, internal as defaultInternal} from '../default/backend/index.js';

View File

@@ -1,4 +1,3 @@
// @ts-check
import {versions} from '../../default/tools/version.js';
/** @type {import('@openstapps/core').SCAppVersionInfo[]} */

View File

@@ -40,7 +40,7 @@
"start:debug": "cross-env STAPPS_LOG_LEVEL=31 NODE_CONFIG_ENV=elasticsearch ALLOW_NO_TRANSPORT=true node app.js",
"test": "pnpm run test:unit",
"test:integration": "sh integration-test.sh",
"test:unit": "cross-env NODE_CONFIG_ENV=elasticsearch ALLOW_NO_TRANSPORT=true STAPPS_LOG_LEVEL=0 c8 mocha"
"test:unit": "cross-env NODE_CONFIG_ENV=elasticsearch ALLOW_NO_TRANSPORT=true STAPPS_LOG_LEVEL=0 mocha --exit"
},
"dependencies": {
"@elastic/elasticsearch": "8.4.0",

View File

@@ -16,7 +16,7 @@
*/
import {Logger, SMTP} from '@openstapps/logger';
import {MailOptions} from 'nodemailer/lib/sendmail-transport';
import Queue from 'promise-queue';
/**
* A queue that can send mails in serial
*/
@@ -32,45 +32,80 @@ export class MailQueue {
static readonly VERIFICATION_TIMEOUT = 5000;
/**
* A promise that resolves when the last mail was sent
* A queue that saves mails, before the transport is ready. When
* the transport gets ready this mails are getting pushed in to
* the normal queue.
*/
last?: Promise<string>;
dryQueue: MailOptions[];
/**
* A queue that saves mails, that are being sent in series
*/
queue: Queue;
/**
* A counter for the number of verifications that failed
*/
verificationCounter: number;
/**
* Creates a mail queue
* @param transport Transport which is used for sending mails
*/
constructor(private readonly transport: SMTP) {}
constructor(private readonly transport: SMTP) {
this.queue = new Queue(1);
// this queue saves all request when the transport is not ready yet
this.dryQueue = [];
this.verificationCounter = 0;
// if the transport can be verified it should check if it was done...
this.checkForVerification();
}
/**
* Wait for the transport to be verified
* Adds a mail into the queue so it gets send when the queue is ready
* @param mail Information required for sending a mail
*/
private async waitForVerification() {
for (let i = 0; i < MailQueue.MAX_VERIFICATION_ATTEMPTS; i++) {
if (this.transport.isVerified()) {
Logger.ok('Transport for mail queue was verified. We can send mails now');
return;
}
await new Promise(resolve => setTimeout(resolve, MailQueue.VERIFICATION_TIMEOUT));
Logger.warn('Transport not verified yet. Trying to send mails here...');
private async addToQueue(mail: MailOptions) {
return this.queue.add<string>(() => this.transport.sendMail(mail));
}
/**
* Verify the given transport
*/
private checkForVerification() {
if (this.verificationCounter >= MailQueue.MAX_VERIFICATION_ATTEMPTS) {
throw new Error('Failed to initialize the SMTP transport for the mail queue');
}
if (this.transport.isVerified()) {
Logger.ok('Transport for mail queue was verified. We can send mails now');
// if the transport finally was verified send all our mails from the dry queue
for (const mail of this.dryQueue) {
void this.addToQueue(mail);
}
} else {
this.verificationCounter++;
setTimeout(() => {
Logger.warn('Transport not verified yet. Trying to send mails here...');
this.checkForVerification();
}, MailQueue.VERIFICATION_TIMEOUT);
}
throw new Error('Failed to initialize the SMTP transport for the mail queue');
}
/**
* Push a mail into the queue so it gets send when the queue is ready
* @param mail Information required for sending a mail
*/
public async push(mail: MailOptions): Promise<string> {
const previousQueue = this.last ?? this.waitForVerification();
this.last = previousQueue.then(() =>
Promise.race([
this.transport.sendMail(mail),
new Promise<string>((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), MailQueue.VERIFICATION_TIMEOUT),
),
]),
);
return this.last;
public async push(mail: MailOptions) {
if (this.transport.isVerified()) {
await this.addToQueue(mail);
} else {
// the transport has verification, but is not verified yet
// push to a dry queue which gets pushed to the real queue when the transport is verified
this.dryQueue.push(mail);
}
}
}

View File

@@ -44,7 +44,7 @@ describe('MailQueue', async function () {
clock.tick(MailQueue.VERIFICATION_TIMEOUT * (MailQueue.MAX_VERIFICATION_ATTEMPTS + 1));
};
expect(test).to.throw();
expect(() => test()).to.throw();
expect(loggerStub.callCount).to.be.equal(MailQueue.MAX_VERIFICATION_ATTEMPTS);
});

View File

@@ -14,6 +14,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
SCBook,
SCBulkAddRoute,
SCBulkDoneRoute,
SCBulkRequest,
@@ -23,29 +24,30 @@ import {
import {expect} from 'chai';
import {bulk, DEFAULT_TEST_TIMEOUT} from '../common.js';
import {testApp} from '../tests-setup.js';
import {readFile} from 'fs/promises';
import {v4} from 'uuid';
import bookFile from '@openstapps/core/test/resources/indexable/Book.2.json' assert {type: 'json'};
const book = JSON.parse(
await readFile('node_modules/@openstapps/core/test/resources/indexable/Book.2.json', 'utf8'),
).instance;
const book = bookFile.instance as SCBook;
describe('Bulk routes', async function () {
// increase timeout for the suite
this.timeout(DEFAULT_TEST_TIMEOUT);
const request: SCBulkRequest = {
expiration: bulk.expiration,
source: bulk.source,
type: bulk.type,
};
const bulkRoute = new SCBulkRoute();
const bulkAddRoute = new SCBulkAddRoute();
const bulkDoneRoute = new SCBulkDoneRoute();
let request: SCBulkRequest;
let bulkRoute: SCBulkRoute;
let bulkAddRoute: SCBulkAddRoute;
let bulkDoneRoute: SCBulkDoneRoute;
// afterEach(async function() {
// TODO: Delete saved bulks
// });
before(function () {
request = {
expiration: bulk.expiration,
source: bulk.source,
type: bulk.type,
};
bulkRoute = new SCBulkRoute();
bulkAddRoute = new SCBulkAddRoute();
bulkDoneRoute = new SCBulkDoneRoute();
});
it('should create bulk', async function () {
const {status, body, error} = await testApp

View File

@@ -21,7 +21,12 @@ import {expect} from 'chai';
describe('Index route', async function () {
// increase timeout for the suite
this.timeout(DEFAULT_TEST_TIMEOUT);
const indexRoute = new SCIndexRoute();
let indexRoute: SCIndexRoute;
before(function () {
indexRoute = new SCIndexRoute();
});
it('should respond with both app and backend configuration', async function () {
const request: SCIndexRequest = {};

View File

@@ -30,15 +30,11 @@ import chaiAsPromised from 'chai-as-promised';
import {DEFAULT_TEST_TIMEOUT} from '../common.js';
import {testApp} from '../tests-setup.js';
import {backendConfig} from '../../src/config.js';
import {readFile} from 'fs/promises';
import registerRequest from '@openstapps/core/test/resources/PluginRegisterRequest.1.json' assert {type: 'json'};
// for using promises in expectations (to.eventually.be...)
use(chaiAsPromised);
const registerRequest = JSON.parse(
await readFile('node_modules/@openstapps/core/test/resources/PluginRegisterRequest.1.json', 'utf8'),
);
// cast it because of "TS2322: Type 'string' is not assignable to type '"add"'"
export const registerAddRequest: SCPluginAdd = registerRequest.instance as SCPluginAdd;

View File

@@ -47,8 +47,8 @@ describe('Create route', async function () {
const statusCodeSuccess = 222;
const bodySuccess = {foo: true};
const sandbox = sinon.createSandbox();
const validationError = new SCValidationErrorResponse([]);
const internalServerError = new SCInternalServerErrorResponse();
let validationError: SCValidationErrorResponse;
let internalServerError: SCInternalServerErrorResponse;
beforeEach(function () {
app = express();
@@ -64,6 +64,9 @@ describe('Create route', async function () {
handler = (_request, _app) => {
return Promise.resolve(bodySuccess);
};
validationError = new SCValidationErrorResponse([]);
internalServerError = new SCInternalServerErrorResponse();
});
afterEach(function () {

View File

@@ -29,11 +29,19 @@ import {backendConfig} from '../../src/config.js';
describe('Search route', async function () {
// increase timeout for the suite
this.timeout(DEFAULT_TEST_TIMEOUT);
const searchRoute = new SCSearchRoute();
const multiSearchRoute = new SCMultiSearchRoute();
const syntaxError = new SCSyntaxErrorResponse('Foo Message');
const methodNotAllowedError = new SCMethodNotAllowedErrorResponse();
const tooManyRequestsError = new SCTooManyRequestsErrorResponse();
let searchRoute: SCSearchRoute;
let multiSearchRoute: SCMultiSearchRoute;
let syntaxError: SCSyntaxErrorResponse;
let methodNotAllowedError: SCMethodNotAllowedErrorResponse;
let tooManyRequestsError: SCTooManyRequestsErrorResponse;
before(function () {
searchRoute = new SCSearchRoute();
multiSearchRoute = new SCMultiSearchRoute();
syntaxError = new SCSyntaxErrorResponse('Foo Message');
methodNotAllowedError = new SCMethodNotAllowedErrorResponse();
tooManyRequestsError = new SCTooManyRequestsErrorResponse();
});
it('should reject GET, PUT with a valid search query', async function () {
// const expectedParams = JSON.parse(JSON.stringify(defaultParams));

View File

@@ -13,23 +13,25 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {SCThingUpdateRoute} from '@openstapps/core';
import {SCBook, SCThingUpdateRoute} from '@openstapps/core';
import chaiAsPromised from 'chai-as-promised';
import {bulkStorageMock, DEFAULT_TEST_TIMEOUT} from '../common.js';
import {expect, use} from 'chai';
import {testApp} from '../tests-setup.js';
import {readFile} from 'fs/promises';
import bookFile from '@openstapps/core/test/resources/indexable/Book.1.json' assert {type: 'json'};
use(chaiAsPromised);
const book = JSON.parse(
await readFile('node_modules/@openstapps/core/test/resources/indexable/Book.1.json', 'utf8'),
).instance;
const book = bookFile.instance as SCBook;
describe('Thing update route', async function () {
// increase timeout for the suite
this.timeout(DEFAULT_TEST_TIMEOUT);
const thingUpdateRoute = new SCThingUpdateRoute();
let thingUpdateRoute: SCThingUpdateRoute;
before(function () {
thingUpdateRoute = new SCThingUpdateRoute();
});
it('should update a thing', async function () {
const thingUpdateRouteurlPath = thingUpdateRoute.urlPath

View File

@@ -49,8 +49,11 @@ import {
} from '../../../src/storage/elasticsearch/util/index.js';
import cron from 'node-cron';
import {query} from './query.js';
import message from '@openstapps/core/test/resources/indexable/Message.1.json' assert {type: 'json'};
import book from '@openstapps/core/test/resources/indexable/Book.1.json' assert {type: 'json'};
import messageFile from '@openstapps/core/test/resources/indexable/Message.1.json' assert {type: 'json'};
import bookFile from '@openstapps/core/test/resources/indexable/Book.1.json' assert {type: 'json'};
const message = messageFile.instance as SCMessage;
const book = bookFile.instance as SCBook;
use(chaiAsPromised);
@@ -67,7 +70,16 @@ describe('Elasticsearch', function () {
const sandbox = sinon.createSandbox();
before(function () {
// eslint-disable-next-line no-console
sandbox.stub(fs, 'readFileSync').returns('{}');
sandbox.stub(backendConfig.internal.boostings.default[0], 'fields').value({
'academicTerms.acronym': {
'SS 2023': 1.05,
'WS 2023/24': 1.1,
'SoSe 2023': 1.05,
'WiSe 2023/24': 1.1,
},
});
});
after(function () {
sandbox.restore();
@@ -261,7 +273,7 @@ describe('Elasticsearch', function () {
return expect(es.init()).to.be.rejected;
});
it('should setup the monitoring if there is monitoring is set and mail queue is defined', async function () {
it('should setup the monitoring if there is monitoring is set and mail queue is defined', function () {
const config: SCConfigFile = {
...backendConfig,
internal: {
@@ -283,7 +295,7 @@ describe('Elasticsearch', function () {
const cronSetupStub = sandbox.stub(cron, 'schedule');
const es = new Elasticsearch(config, new MailQueue(getTransport(false) as unknown as SMTP));
await es.init();
es.init();
expect(cronSetupStub.called).to.be.true;
});
@@ -437,7 +449,7 @@ describe('Elasticsearch', function () {
_id: '',
_index: '',
_score: 0,
_source: message.instance as SCMessage,
_source: message,
};
sandbox.stub(es.client, 'search').resolves(searchResponse(foundObject));
@@ -467,7 +479,7 @@ describe('Elasticsearch', function () {
const object: SearchHit<SCMessage> = {
_id: '',
_index: oldIndex,
_source: message.instance as SCMessage,
_source: message,
};
sandbox.stub(es.client, 'search').resolves(searchResponse<SCMessage>(object));
sandbox.stub(es, 'prepareBulkWrite').resolves(index);
@@ -481,7 +493,7 @@ describe('Elasticsearch', function () {
sandbox.stub(es.client, 'create').resolves({result: 'not_found'} as CreateResponse);
await es.init();
return expect(es.post(message.instance as SCMessage, bulk)).to.rejectedWith('creation');
return expect(es.post(message, bulk)).to.rejectedWith('creation');
});
it('should create a new object', async function () {
@@ -494,11 +506,11 @@ describe('Elasticsearch', function () {
});
await es.init();
await es.post(message.instance as SCMessage, bulk);
await es.post(message, bulk);
expect(createStub.called).to.be.true;
expect(caughtParameter.document).to.be.eql({
...message.instance,
...message,
creation_date: caughtParameter.document.creation_date,
});
});
@@ -519,7 +531,7 @@ describe('Elasticsearch', function () {
_id: '',
_index: getIndex(),
_score: 0,
_source: message.instance as SCMessage,
_source: message,
};
sandbox.stub(es.client, 'search').resolves(searchResponse());
@@ -533,7 +545,7 @@ describe('Elasticsearch', function () {
_id: '',
_index: getIndex(),
_score: 0,
_source: message.instance as SCMessage,
_source: message,
};
sandbox.stub(es.client, 'search').resolves(searchResponse(object));
// @ts-expect-error unused
@@ -556,13 +568,13 @@ describe('Elasticsearch', function () {
_id: '123',
_index: getIndex(),
_score: 0,
_source: message.instance as SCMessage,
_source: message,
};
const objectBook: SearchHit<SCBook> = {
_id: '321',
_index: getIndex(),
_score: 0,
_source: book.instance as SCBook,
_source: book,
};
const fakeEsAggregations = {
'@all': {

View File

@@ -14,6 +14,4 @@ RUN chown elasticsearch:elasticsearch config/elasticsearch.yml
USER elasticsearch
HEALTHCHECK --interval=10s --timeout=10s --start-period=60s --retries=12 CMD curl --fail -s http://localhost:9200/ >/dev/null || exit 1
CMD ["/usr/share/elasticsearch/bin/elasticsearch"]

View File

@@ -3,4 +3,3 @@ discovery.type: "single-node"
cluster.routing.allocation.disk.threshold_enabled: false
network.bind_host: 0.0.0.0
xpack.security.enabled: false
ingest.geoip.downloader.enabled: false

View File

@@ -1,2 +1,2 @@
nginx &
node ./app.js
node ./lib/cli.js

View File

@@ -14,6 +14,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// ESM is not supported, and cts is not detected, so we use type-checked cjs instead.
/** @type {import('../src/common').ConfigFile} */
const configFile = {
activeVersions: ['1\\.0\\.\\d+', '2\\.0\\.\\d+'],

View File

@@ -46,6 +46,7 @@
"@types/dockerode": "3.3.17",
"@types/node": "18.15.3",
"@types/sha1": "1.1.3",
"config": "3.3.9",
"dockerode": "3.3.5",
"is-cidr": "4.0.2",
"mustache": "4.2.0",
@@ -58,6 +59,7 @@
"@openstapps/prettier-config": "workspace:*",
"@openstapps/tsconfig": "workspace:*",
"@types/chai": "4.3.5",
"@types/config": "3.3.0",
"@types/dockerode": "3.3.17",
"@types/mocha": "10.0.1",
"@types/mustache": "4.2.2",

View File

@@ -14,6 +14,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Logger, SMTP} from '@openstapps/logger';
import config from 'config';
import {existsSync} from 'fs';
// set transport on logger
@@ -162,7 +163,7 @@ ssl_stapling_verify on;`;
/**
* Config file
*/
export const configFile: ConfigFile = await import('../config/default.js').then(it => it.default);
export const configFile: ConfigFile = config.util.toObject();
/**
* Check if path is a specific file type

View File

@@ -1,4 +1,3 @@
// @ts-check
const fs = require("fs");
const path = require("node:path");
const child_process = require("child_process");

View File

@@ -1,4 +1,3 @@
// @ts-check
"use strict"
const rule = require('./copyright-header-rule')

View File

@@ -1,5 +1,3 @@
// @ts-check
/** @type {import('eslint').Linter.Config} */
const config = {
root: true,

View File

@@ -19,9 +19,9 @@
"test": "prettier --config index.js --check \"test/*.js\""
},
"devDependencies": {
"prettier": "3.1.0"
"prettier": "3.1.1"
},
"peerDependencies": {
"prettier": "3.1.0"
"prettier": "3.1.1"
}
}

View File

@@ -14,6 +14,7 @@
"noFallthroughCasesInSwitch": true,
"isolatedModules": true,
"allowJs": true,
"checkJs": true,
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"noImplicitAny": true,

View File

@@ -1,44 +1,62 @@
version: '3.7'
x-development-variables: &development-variables
NODE_ENV: "development"
ALLOW_NO_TRANSPORT: "true"
services:
database:
image: registry.gitlab.com/openstapps/openstapps/database:2.0.0
volumes:
- ./database:/usr/share/elasticsearch/data
image: registry.gitlab.com/openstapps/openstapps/database:3.0.0
# If you need persistence for debugging purposes uncomment the following lines
#volumes:
# - ./database:/usr/share/elasticsearch/data
expose:
- "9200"
- 9200
ports:
- 127.0.0.1:9200:9200
environment:
- bootstrap.memory_lock=true
- "ES_JAVA_OPTS=-Xms2g -Xmx2g"
- discovery.type=single-node
ulimits:
memlock:
soft: -1
hard: -1
restart: unless-stopped
backend:
image: registry.gitlab.com/openstapps/openstapps/backend:3.0.0-next.0
image: registry.gitlab.com/openstapps/openstapps/backend:3.1.0
environment:
<<: *development-variables
ES_ADDR: "http://database:9200"
NODE_CONFIG_ENV: "elasticsearch"
ALLOW_NO_TRANSPORT: "true"
NODE_APP_INSTANCE: "f-u"
PROMETHEUS_MIDDLEWARE: "false"
expose:
- 3000
ports:
- 3000:3000
links:
- "database"
- 127.0.0.1:3000:3000
labels:
- stapps.version=1.0.0
- stapps.version=4.1.0
restart: unless-stopped
depends_on:
- database
api:
image: registry.gitlab.com/openstapps/openstapps/api:3.0.0-next.0
links:
- "backend"
- database
minimal-connector:
image: registry.gitlab.com/openstapps/minimal-connector:core-0.23
container_name: minimal-connector-0.23
command: ["http://backend:3000", "minimal-connector", "f-u"]
# api:
# image: registry.gitlab.com/openstapps/openstapps/api:3.0.0
# links:
# - backend
app:
image: registry.gitlab.com/openstapps/app/executable:core-0.23
expose:
- 8100
ports:
- 8100:8100
# minimal-connector:
# image: registry.gitlab.com/openstapps/minimal-connector:core-0.23
# container_name: minimal-connector-0.23
# command: ["http://backend:3000", "minimal-connector", "f-u"]
# app:
# image: registry.gitlab.com/openstapps/app/executable:core-0.23
# expose:
# - 8100
# ports:
# - 8100:8100

34
flake.lock generated
View File

@@ -1,5 +1,23 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1701626906,
@@ -18,8 +36,24 @@
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

142
flake.nix
View File

@@ -1,77 +1,75 @@
{
description = "A Nix-flake-based development environment for OpenStApps";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
outputs = { self, nixpkgs }:
let
buildToolsVersion = "30.0.3";
overlays = [
(final: prev: rec {
nodejs = prev.nodejs-18_x;
pnpm = prev.nodePackages.pnpm;
chrome = prev.google-chrome;
firefox = prev.firefox;
webkit = prev.epiphany; # Safari-ish browser
android = prev.androidenv.composeAndroidPackages {
buildToolsVersions = [ "${buildToolsVersion}" ];
platformVersions = [ "33" ];
};
cypress = prev.cypress.overrideAttrs(cyPrev: rec {
version = "13.2.0";
src = prev.fetchzip {
url = "https://cdn.cypress.io/desktop/${version}/linux-x64/cypress.zip";
hash = "sha256-9o0nprGcJhudS1LNm+T7Vf0Dwd1RBauYKI+w1FBQ3ZM=";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = {
self,
nixpkgs,
flake-utils,
}: let
aapt2buildToolsVersion = "33.0.2";
in
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
overlays = [
(final: prev: rec {
fontMin = prev.python311.withPackages (ps: with ps; [brotli fonttools] ++ (with fonttools.optional-dependencies; [woff]));
android = prev.androidenv.composeAndroidPackages {
buildToolsVersions = ["30.0.3" aapt2buildToolsVersion];
platformVersions = ["33"];
};
});
})
];
# TODO: aarch64-linux, x68_64-darwin, aarch64-darwin
supportedSystems = [ "x86_64-linux" ];
forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f {
pkgs = import nixpkgs {
inherit overlays system;
config = {
allowUnfree = true;
android_sdk.accept_license = true;
};
cypress = prev.cypress.overrideAttrs (cyPrev: rec {
version = "13.2.0";
src = prev.fetchzip {
url = "https://cdn.cypress.io/desktop/${version}/linux-x64/cypress.zip";
hash = "sha256-9o0nprGcJhudS1LNm+T7Vf0Dwd1RBauYKI+w1FBQ3ZM=";
};
});
})
];
config = {
allowUnfree = true;
android_sdk.accept_license = true;
};
});
in
{
devShells = forEachSupportedSystem ({ pkgs }:
let
python = (pkgs.python311.withPackages(ps: with ps; [ brotli fonttools ] ++ (with fonttools.optional-dependencies; [ ufo lxml unicode woff ])));
in
{
default = (pkgs.buildFHSUserEnv {
name = "StApps Dev";
targetPkgs = pkgs: with pkgs; [
nodejs
pnpm
python
docker
# tools
curl
jq
# browsers
firefox
chrome
webkit
cypress
# android
jdk17
android.androidsdk
musl
];
runScript = "bash";
profile = ''
export CYPRESS_INSTALL_BINARY=0
export CYPRESS_RUN_BINARY=${pkgs.cypress}/bin/Cypress
export ANDROID_SDK_ROOT=${pkgs.android.androidsdk}/libexec/android-sdk
export ANDROID_JAVA_HOME=${pkgs.jdk.home}
export DOCKER_HOST=unix:///run/user/1000/docker.sock
{ dockerd-rootless & } 2>/dev/null
'';
}).env;
});
};
};
androidFhs = pkgs.buildFHSUserEnv {
name = "android-env";
targetPkgs = pkgs: with pkgs; [];
runScript = "bash";
profile = ''
export ALLOW_NINJA_ENV=true
export USE_CCACHE=1
export LD_LIBRARY_PATH=/usr/lib:/usr/lib32
'';
};
in {
devShell = pkgs.mkShell rec {
nativeBuildInputs = [androidFhs];
buildInputs = with pkgs; [
nodejs-18_x
nodePackages.pnpm
# tools
curl
jq
fontMin
# browsers
firefox
google-chrome
epiphany # Safari-ish browser
cypress
# android
jdk17
android.androidsdk
musl
];
ANDROID_JAVA_HOME = "${pkgs.jdk.home}";
ANDROID_SDK_ROOT = "${pkgs.android.androidsdk}/libexec/android-sdk";
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_SDK_ROOT}/build-tools/${aapt2buildToolsVersion}/aapt2";
CYPRESS_INSTALL_BINARY = "0";
CYPRESS_RUN_BINARY = "${pkgs.cypress}/bin/Cypress";
};
});
}

View File

@@ -1,7 +1,7 @@
# Creates a docker image with only the app as an executable unit
# Dependencies need to be installed beforehand
# Needs to be build beforehand
FROM node:18-alpine3.18
FROM node:18-alpine
WORKDIR /app
COPY www/ /app/www

View File

@@ -1,4 +1,3 @@
// @ts-check
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
@@ -13,8 +12,9 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
/** @type {import('./scripts/icon-config').IconConfig} */
const config = {
import type {IconConfig} from './scripts/icon-config';
const config: IconConfig = {
inputPath: 'node_modules/material-symbols/material-symbols-rounded.woff2',
outputPath: 'src/assets/icons.min.woff2',
htmlGlob: 'src/**/*.html',

View File

@@ -25,7 +25,7 @@ def capacitor_pods
pod 'CapacitorPreferences', :path => '../../../../node_modules/.pnpm/@capacitor+preferences@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/preferences'
pod 'CapacitorShare', :path => '../../../../node_modules/.pnpm/@capacitor+share@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/share'
pod 'CapacitorSplashScreen', :path => '../../../../node_modules/.pnpm/@capacitor+splash-screen@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/splash-screen'
pod 'TransistorsoftCapacitorBackgroundFetch', :path => '../../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@1.0.2_@capacitor+core@5.5.0/node_modules/@transistorsoft/capacitor-background-fetch'
pod 'TransistorsoftCapacitorBackgroundFetch', :path => '../../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@5.1.1_@capacitor+core@5.5.0/node_modules/@transistorsoft/capacitor-background-fetch'
pod 'CapacitorSecureStoragePlugin', :path => '../../../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.5.0/node_modules/capacitor-secure-storage-plugin'
pod 'CordovaPlugins', :path => '../capacitor-cordova-ios-plugins'
end

View File

@@ -36,7 +36,7 @@ PODS:
- CordovaPlugins (5.5.0):
- CapacitorCordova
- SwiftKeychainWrapper (4.0.1)
- TransistorsoftCapacitorBackgroundFetch (1.0.2):
- TransistorsoftCapacitorBackgroundFetch (5.1.1):
- Capacitor
DEPENDENCIES:
@@ -58,7 +58,7 @@ DEPENDENCIES:
- "CapacitorShare (from `../../../../node_modules/.pnpm/@capacitor+share@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/share`)"
- "CapacitorSplashScreen (from `../../../../node_modules/.pnpm/@capacitor+splash-screen@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/splash-screen`)"
- CordovaPlugins (from `../capacitor-cordova-ios-plugins`)
- "TransistorsoftCapacitorBackgroundFetch (from `../../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@1.0.2_@capacitor+core@5.5.0/node_modules/@transistorsoft/capacitor-background-fetch`)"
- "TransistorsoftCapacitorBackgroundFetch (from `../../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@5.1.1_@capacitor+core@5.5.0/node_modules/@transistorsoft/capacitor-background-fetch`)"
SPEC REPOS:
trunk:
@@ -102,7 +102,7 @@ EXTERNAL SOURCES:
CordovaPlugins:
:path: "../capacitor-cordova-ios-plugins"
TransistorsoftCapacitorBackgroundFetch:
:path: "../../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@1.0.2_@capacitor+core@5.5.0/node_modules/@transistorsoft/capacitor-background-fetch"
:path: "../../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@5.1.1_@capacitor+core@5.5.0/node_modules/@transistorsoft/capacitor-background-fetch"
SPEC CHECKSUMS:
Capacitor: 57890b363df14d5d2d5d8461aa23e886cb34da2a
@@ -124,8 +124,8 @@ SPEC CHECKSUMS:
CapacitorSplashScreen: 5fa2ab5e46cf5cc530cf16a51c80c7a986579ccd
CordovaPlugins: de5669381702d76ed5b1d442177a6a5fc3252a9d
SwiftKeychainWrapper: 807ba1d63c33a7d0613288512399cd1eda1e470c
TransistorsoftCapacitorBackgroundFetch: 74ca62dae7ec78639eaf3d0d1e24c595ada213dd
TransistorsoftCapacitorBackgroundFetch: ce4b3e01b898cef516e68485d2160a078016ee97
PODFILE CHECKSUM: 073b899f90bacc5049101cb9c562a168757d554e
PODFILE CHECKSUM: 229278f2c257e8ab555325c7115b2e187e8e628d
COCOAPODS: 1.13.0

View File

@@ -1,4 +1,3 @@
// @ts-check
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it

View File

@@ -21,7 +21,7 @@
"build:prod": "ng build --configuration=production",
"build:stats": "ng build --configuration=production --stats-json",
"changelog": "conventional-changelog -p angular -i src/assets/about/CHANGELOG.md -s -r 0",
"check-icons": "node scripts/check-icon-correctness.mjs",
"check-icons": "ts-node-esm scripts/check-icon-correctness.ts",
"chromium:no-cors": "chromium --disable-web-security --user-data-dir=\".browser-data/chromium\"",
"chromium:virtual-host": "chromium --host-resolver-rules=\"MAP mobile.app.uni-frankfurt.de:* localhost:8100\" --ignore-certificate-errors",
"cypress:open": "cypress open",
@@ -38,7 +38,7 @@
"licenses": "license-checker --json > src/assets/about/licenses.json && ts-node ./scripts/accumulate-licenses.ts && git add src/assets/about/licenses.json",
"lint": "ng lint && stylelint \"**/*.scss\"",
"lint:fix": "eslint --fix -c .eslintrc.json --ignore-path .eslintignore --ext .ts,.html src/ && stylelint --fix \"**/*.scss\"",
"minify-icons": "node scripts/minify-icon-font.mjs",
"minify-icons": "ts-node-esm scripts/minify-icon-font.ts",
"postinstall": "jetify && echo \"skipping jetify in production mode\"",
"preview": "http-server www --p 8101 -o",
"push": "git push && git push origin \"v$npm_package_version\"",
@@ -93,6 +93,7 @@
"cordova-plugin-calendar": "5.1.6",
"date-fns": "2.30.0",
"deepmerge": "4.3.1",
"fast-deep-equal": "3.1.3",
"form-data": "4.0.0",
"geojson": "0.5.0",
"ionic-appauth": "0.9.0",

View File

@@ -1,4 +1,3 @@
// @ts-check
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
@@ -14,14 +13,13 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import fs from 'fs';
import {omit, pickBy} from '@openstapps/collection-utils';
import {omit} from '../src/app/_helpers/collections/omit';
import {pickBy} from '../src/app/_helpers/collections/pick';
/**
* accumulate and transform licenses based on two license files
* @param {string} path
* @param {string} additionalLicensesPath
*/
function accumulateFile(path, additionalLicensesPath) {
function accumulateFile(path: string, additionalLicensesPath: string) {
const packageJson = JSON.parse(fs.readFileSync('./package.json').toString());
const dependencies = packageJson.dependencies;
@@ -30,9 +28,10 @@ function accumulateFile(path, additionalLicensesPath) {
fs.writeFileSync(
path,
JSON.stringify(
Object.entries({
...pickBy(JSON.parse(fs.readFileSync(path).toString()), (_, key) => {
const parts = /** @type {string} */ (key).split('@');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Object.entries<any>({
...pickBy(JSON.parse(fs.readFileSync(path).toString()), (_, key: string) => {
const parts = key.split('@');
return dependencies[parts.slice(0, -1).join('@')] === parts[parts.length - 1];
}),

View File

@@ -1,4 +1,3 @@
// @ts-check
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
@@ -13,18 +12,18 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {openSync} from 'fontkit';
import config from '../icons.config.mjs';
import fontkit, {Font} from 'fontkit';
import config from '../icons.config';
import {existsSync} from 'fs';
import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons.mjs';
import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons';
const commandName = '"npm run minify-icons"';
const originalFont = openSync(config.inputPath);
const originalFont = fontkit.openSync(config.inputPath);
if (!existsSync(config.outputPath)) {
console.error(`Minified font not found. Run ${commandName} first.`);
process.exit(-1);
}
const modifiedFont = openSync(config.outputPath);
const modifiedFont = fontkit.openSync(config.outputPath);
let success = true;
@@ -49,9 +48,9 @@ async function checkAll() {
}
/**
* @param {Record<string, string[]>} icons
*
*/
function check(icons) {
function check(icons: Record<string, string[]>) {
for (const [purpose, iconSet] of Object.entries(icons)) {
for (const icon of iconSet) {
if (!hasIcon(originalFont, icon)) {
@@ -66,9 +65,8 @@ function check(icons) {
}
/**
* @param {import('fontkit').Font} font
* @param {string} icon
*
*/
function hasIcon(font, icon) {
function hasIcon(font: Font, icon: string) {
return font.layout(icon).glyphs.some(it => it.isLigature);
}

View File

@@ -1,4 +1,3 @@
// @ts-check
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
@@ -15,33 +14,31 @@
*/
import {glob} from 'glob';
import {readFileSync} from 'fs';
import {matchPropertyContent, matchTagProperties} from '../src/app/util/ion-icon/icon-match.mjs';
import {matchPropertyContent, matchTagProperties} from '../src/app/util/ion-icon/icon-match';
/**
* @returns {Promise<Record<string, string[]>>}
*
*/
export async function getUsedIconsHtml(pattern = 'src/**/*.html') {
export async function getUsedIconsHtml(pattern = 'src/**/*.html'): Promise<Record<string, string[]>> {
return Object.fromEntries(
(await glob(pattern))
.map(file => [
file,
/** @type {string[]} */ (
readFileSync(file, 'utf8')
.match(matchTagProperties('ion-icon'))
?.flatMap(match => {
return match.match(matchPropertyContent(['name', 'md', 'ios']));
})
.filter(it => !!it)
) || [],
(readFileSync(file, 'utf8')
.match(matchTagProperties('ion-icon'))
?.flatMap(match => {
return match.match(matchPropertyContent(['name', 'md', 'ios']));
})
.filter(it => !!it) as string[]) || [],
])
.filter(([, values]) => values.length > 0),
);
}
/**
* @returns {Promise<Record<string, string[]>>}
*
*/
export async function getUsedIconsTS(pattern = 'src/**/*.ts') {
export async function getUsedIconsTS(pattern = 'src/**/*.ts'): Promise<Record<string, string[]>> {
return Object.fromEntries(
(await glob(pattern))
.map(file => [file, readFileSync(file, 'utf8').match(/(?<=Icon`)[\w-]+(?=`)/g) || []])

View File

@@ -1,4 +1,3 @@
// @ts-check
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
@@ -13,17 +12,18 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {openSync} from 'fontkit';
/* eslint-disable @typescript-eslint/no-non-null-assertion */
import fontkit from 'fontkit';
import {exec} from 'child_process';
import config from '../icons.config.mjs';
import config from '../icons.config';
import {statSync} from 'fs';
import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons.mjs';
import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons';
/**
* @param {string[] | string} command
* @returns {Promise<string>}
*
*/
async function run(command) {
async function run(command: string[] | string): Promise<string> {
const fullCommand = Array.isArray(command) ? command.join(' ') : command;
console.log(`>> ${fullCommand}`);
@@ -44,8 +44,7 @@ async function run(command) {
*
*/
async function minifyIconFont() {
/** @type {Set<string>} */
const icons = new Set();
const icons = new Set<string>();
for (const iconSet of [
...Object.values(config.additionalIcons || []),
@@ -58,10 +57,9 @@ async function minifyIconFont() {
}
console.log('Icons used:', [...icons.values()].sort());
const font = openSync(config.inputPath);
const font = fontkit.openSync(config.inputPath);
/** @type {string[]} */
const glyphs = ['5f-7a', '30-39'];
const glyphs: string[] = ['5f-7a', '30-39'];
for (const icon of icons) {
const iconGlyphs = font.layout(icon).glyphs;
if (iconGlyphs.length === 0) {
@@ -72,7 +70,7 @@ async function minifyIconFont() {
const codePoints = iconGlyphs
.flatMap(it => font.stringsForGlyph(it.id))
.flatMap(it => [...it])
.map(it => it.codePointAt(0).toString(16));
.map(it => it.codePointAt(0)!.toString(16));
if (codePoints.length === 0) {
if (config.codePoints?.[icon]) {
@@ -116,10 +114,8 @@ minifyIconFont();
/**
* Bytes to respective units
* @param {number} value
* @returns {string}
*/
function toByteUnit(value) {
function toByteUnit(value: number): string {
if (value < 1024) {
return `${value}B`;
} else if (value < 1024 * 1024) {

View File

@@ -0,0 +1,7 @@
{
"extends": "@openstapps/tsconfig",
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "Node"
}
}

View File

@@ -0,0 +1,80 @@
import {Animation, AnimationController} from '@ionic/angular';
import {iosDuration, iosEasing, mdDuration, mdEasing} from './easings';
/**
* Splash screen animation
*/
export function splashAnimation(animationCtl: AnimationController): Animation {
if (matchMedia('(prefers-reduced-motion: reduce)').matches) {
return animationCtl
.create()
.fromTo('opacity', 0, 1)
.duration(150)
.beforeClearStyles(['visibility'])
.addElement(document.querySelector('ion-app')!);
}
const isMd = document.querySelector('ion-app.md') !== null;
const navElement = document.querySelector('stapps-navigation-tabs')!;
const navBounds = navElement.getBoundingClientRect();
let horizontal = navBounds.width < navBounds.height;
if (window.getComputedStyle(navElement).display === 'none') {
horizontal = true;
}
const translate = (amount: number, unit = 'px') =>
`translate${horizontal ? 'X' : 'Y'}(${horizontal ? amount * -1 : amount}${unit})`;
const duration = 2 * (isMd ? mdDuration : iosDuration);
const animation = animationCtl
.create()
.duration(duration)
.easing(isMd ? mdEasing : iosEasing)
.addAnimation(
animationCtl.create().beforeClearStyles(['visibility']).addElement(document.querySelector('ion-app')!),
)
.addAnimation(
animationCtl
.create()
.fromTo('transform', translate(horizontal ? 64 : 192), translate(0))
.fromTo('opacity', 0, 1)
.addElement(document.querySelector('stapps-navigation > ion-split-pane')!),
)
.addAnimation(
animationCtl
.create()
.fromTo('transform', translate(64), translate(0))
.addElement(document.querySelectorAll('ion-split-pane > ion-menu > ion-content')),
)
.addAnimation(
animationCtl
.create()
.fromTo('transform', translate(horizontal ? 32 : -72), translate(0))
.addElement(document.querySelectorAll('ion-router-outlet > .ion-page > ion-content')!),
)
.addAnimation(
animationCtl
.create()
.fromTo('transform', translate(100, '%'), translate(0, '%'))
.addElement(document.querySelector('stapps-navigation-tabs')!),
);
if (!horizontal) {
animation.addAnimation(
animationCtl
.create()
.fromTo('background', 'none', 'none')
.addElement(document.querySelector('ion-router-outlet')!),
);
const parallax = document
.querySelector('ion-router-outlet > .ion-page > ion-content')
?.shadowRoot?.querySelector('[part=parallax]');
if (parallax) {
animation.addAnimation(
animationCtl.create().fromTo('translate', '0 256px', '0 0px').addElement(parallax),
);
}
}
return animation;
}

View File

@@ -22,28 +22,16 @@ import {environment} from '../environments/environment';
import {Capacitor} from '@capacitor/core';
import {ScheduleSyncService} from './modules/background/schedule/schedule-sync.service';
import {Keyboard, KeyboardResize} from '@capacitor/keyboard';
import {AppVersionService} from './modules/about/app-version.service';
import {SplashScreen} from '@capacitor/splash-screen';
import {AppVersionService} from './modules/about/app-version.service';
/**
* TODO
*/
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
})
export class AppComponent implements AfterContentInit {
/**
* TODO
*/
pages: Array<{
/**
* TODO
*/
component: unknown;
/**
* TODO
*/
title: string;
}>;
@@ -65,7 +53,7 @@ export class AppComponent implements AfterContentInit {
void this.initializeApp();
}
async ngAfterContentInit() {
ngAfterContentInit() {
this.scheduleSyncService.init();
void this.scheduleSyncService.enable();
this.versionService.getPendingReleaseNotes().then(notes => {
@@ -74,24 +62,11 @@ export class AppComponent implements AfterContentInit {
}
});
if (document.readyState === 'complete') {
this.hideSplash();
} else {
document.addEventListener('readystatechange', () => {
if (document.readyState === 'complete') this.hideSplash();
});
}
}
async hideSplash() {
if (Capacitor.isNativePlatform()) {
void SplashScreen.hide();
}
}
/**
* TODO
*/
async initializeApp() {
App.addListener('appUrlOpen', (event: URLOpenListenerEvent) => {
this.zone.run(() => {

View File

@@ -25,12 +25,10 @@ import moment from 'moment';
import 'moment/min/locales';
import {LoggerModule, NGXLogger, NgxLoggerLevel} from 'ngx-logger';
import SwiperCore, {FreeMode, Navigation} from 'swiper';
import {environment} from '../environments/environment';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
import {CatalogModule} from './modules/catalog/catalog.module';
import {ConfigModule} from './modules/config/config.module';
import {ConfigProvider} from './modules/config/config.provider';
import {DashboardModule} from './modules/dashboard/dashboard.module';
import {DataModule} from './modules/data/data.module';
@@ -44,7 +42,6 @@ import {SettingsProvider} from './modules/settings/settings.provider';
import {StorageModule} from './modules/storage/storage.module';
import {ThingTranslateModule} from './translation/thing-translate.module';
import {UtilModule} from './util/util.module';
import {initLogger} from './_helpers/ts-logger';
import {BrowserAnimationsModule} from '@angular/platform-browser/animations';
import {AboutModule} from './modules/about/about.module';
import {JobModule} from './modules/jobs/jobs.module';
@@ -91,28 +88,25 @@ export function initializerFactory(
) {
return async () => {
try {
initLogger(logger);
await storageProvider.init();
await configProvider.init();
await settingsProvider.init();
if (configProvider.firstSession) {
// set language from browser
await settingsProvider.setSettingValue(
'profile',
'language',
translateService.getBrowserLang() as SCSettingValue,
);
}
const languageCode = await settingsProvider.getSetting<string>('profile', 'language');
// this language will be used as a fallback when a translation isn't found in the current language
translateService.setDefaultLang('en');
translateService.use(languageCode);
moment.locale(languageCode);
const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode);
setDefaultOptions({locale: dateFnsLocale});
dateFnsConfigurationService.setLocale(dateFnsLocale);
try {
if (configProvider.firstSession) {
// set language from browser
await settingsProvider.setSettingValue(
'profile',
'language',
translateService.getBrowserLang() as SCSettingValue,
);
}
const languageCode = (await settingsProvider.getValue('profile', 'language')) as string;
// this language will be used as a fallback when a translation isn't found in the current language
translateService.setDefaultLang('en');
translateService.use(languageCode);
moment.locale(languageCode);
const dateFnsLocale = await getDateFnsLocale(languageCode as SCLanguageCode);
setDefaultOptions({locale: dateFnsLocale});
dateFnsConfigurationService.setLocale(dateFnsLocale);
await defaultAuthService.init();
await paiaAuthService.init();
} catch (error) {
@@ -151,11 +145,12 @@ export function createTranslateLoader(http: HttpClient) {
BrowserAnimationsModule,
CatalogModule,
CommonModule,
ConfigModule,
DashboardModule,
DataModule,
HebisModule,
IonicModule.forRoot(),
IonicModule.forRoot({
animated: 'Cypress' in window ? false : undefined,
}),
IonIconModule,
JobModule,
FavoritesModule,

View File

@@ -14,7 +14,7 @@
*/
import {Component, OnInit} from '@angular/core';
import {ActivatedRoute} from '@angular/router';
import {SCAboutPage, SCAppConfiguration} from '@openstapps/core';
import {SCAboutPage} from '@openstapps/core';
import {ConfigProvider} from '../../config/config.provider';
import packageJson from '../../../../../package.json';
import config from 'capacitor.config';
@@ -42,8 +42,7 @@ export class AboutPageComponent implements OnInit {
async ngOnInit() {
const route = this.route.snapshot.url.map(it => it.path).join('/');
this.content =
(this.configProvider.getValue('aboutPages') as SCAppConfiguration['aboutPages'])[route] ?? {};
this.content = this.configProvider.config.app.aboutPages[route] ?? {};
this.version = Capacitor.getPlatform() === 'web' ? 'Web' : await App.getInfo().then(info => info.version);
}
}

View File

@@ -19,7 +19,6 @@ import {FormsModule} from '@angular/forms';
import {IonicModule} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core';
import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {ConfigProvider} from '../config/config.provider';
import {AboutPageComponent} from './about-page/about-page.component';
import {MarkdownModule} from 'ngx-markdown';
import {AboutPageContentComponent} from './about-page/about-page-content.component';
@@ -64,6 +63,5 @@ const settingsRoutes: Routes = [
ScrollingModule,
UtilModule,
],
providers: [ConfigProvider],
})
export class AboutModule {}

View File

@@ -18,12 +18,7 @@ import {IPAIAAuthAction} from './paia/paia-auth-action';
import {AuthActions, IAuthAction} from 'ionic-appauth';
import {TranslateService} from '@ngx-translate/core';
import {JSONPath} from 'jsonpath-plus';
import {
SCAuthorizationProvider,
SCAuthorizationProviderType,
SCUserConfiguration,
SCUserConfigurationMap,
} from '@openstapps/core';
import {SCAuthorizationProviderType, SCUserConfiguration} from '@openstapps/core';
import {ConfigProvider} from '../config/config.provider';
import {StorageProvider} from '../storage/storage.provider';
import {DefaultAuthService} from './default-auth.service';
@@ -37,8 +32,6 @@ const AUTH_ORIGIN_PATH = 'stapps.auth.origin_path';
providedIn: 'root',
})
export class AuthHelperService {
userConfigurationMap: SCUserConfigurationMap;
constructor(
private translateService: TranslateService,
private configProvider: ConfigProvider,
@@ -47,14 +40,7 @@ export class AuthHelperService {
private paiaAuth: PAIAAuthService,
private browser: SimpleBrowser,
private alertController: AlertController,
) {
this.userConfigurationMap =
(
this.configProvider.getAnyValue('auth') as {
default: SCAuthorizationProvider;
}
).default?.endpoints.mapping ?? {};
}
) {}
public getAuthMessage(provider: SCAuthorizationProviderType, action: IAuthAction | IPAIAAuthAction) {
let message: string | undefined;
@@ -77,9 +63,10 @@ export class AuthHelperService {
name: '',
role: 'student',
};
for (const key in this.userConfigurationMap) {
const mapping = this.configProvider.config.auth.default!.endpoints.mapping;
for (const key in mapping) {
user[key as keyof SCUserConfiguration] = JSONPath({
path: this.userConfigurationMap[key as keyof SCUserConfiguration] as string,
path: mapping[key as keyof SCUserConfiguration] as string,
json: userInfo,
preventEval: true,
})[0];

View File

@@ -12,7 +12,6 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
AuthorizationRequestHandler,
AuthorizationServiceConfiguration,
@@ -24,7 +23,6 @@ import {
} from '@openid/appauth';
import {AuthActionBuilder, Browser, DefaultBrowser, EndSessionHandler, UserInfoHandler} from 'ionic-appauth';
import {ConfigProvider} from '../config/config.provider';
import {SCAuthorizationProvider} from '@openstapps/core';
import {getClientConfig, getEndpointsConfig} from './auth.provider.methods';
import {Injectable} from '@angular/core';
import {AuthService} from './auth.service';
@@ -67,12 +65,9 @@ export class DefaultAuthService extends AuthService {
}
setupConfiguration() {
const authConfig = this.configProvider.getAnyValue('auth') as {
default: SCAuthorizationProvider;
};
this.authConfig = getClientConfig('default', authConfig);
this.authConfig = getClientConfig('default', this.configProvider.config.auth);
this.localConfiguration = new AuthorizationServiceConfiguration(
getEndpointsConfig('default', authConfig),
getEndpointsConfig('default', this.configProvider.config.auth),
);
}

View File

@@ -12,7 +12,6 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
AuthorizationError,
AuthorizationRequest,
@@ -47,7 +46,6 @@ import {PAIAAuthorizationResponse} from './paia-authorization-response';
import {PAIAAuthorizationNotifier} from './paia-authorization-notifier';
import {PAIATokenResponse} from './paia-token-response';
import {IPAIAAuthAction, PAIAAuthActionBuilder} from './paia-auth-action';
import {SCAuthorizationProvider} from '@openstapps/core';
import {ConfigProvider} from '../../config/config.provider';
import {getClientConfig, getEndpointsConfig} from '../auth.provider.methods';
import {Injectable} from '@angular/core';
@@ -154,11 +152,10 @@ export class PAIAAuthService {
}
setupConfiguration() {
const authConfig = this.configProvider.getAnyValue('auth') as {
paia: SCAuthorizationProvider;
};
this.authConfig = getClientConfig('paia', authConfig);
this.localConfiguration = new AuthorizationServiceConfiguration(getEndpointsConfig('paia', authConfig));
this.authConfig = getClientConfig('paia', this.configProvider.config.auth);
this.localConfiguration = new AuthorizationServiceConfiguration(
getEndpointsConfig('paia', this.configProvider.config.auth),
);
}
protected notifyActionListers(action: IPAIAAuthAction) {

View File

@@ -12,7 +12,6 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Calendar} from '@awesome-cordova-plugins/calendar/ngx';
import {Injectable} from '@angular/core';
import {ICalEvent} from './ical/ical';
@@ -35,14 +34,14 @@ export class CalendarService {
goToDateClicked = this.goToDate.asObservable();
calendarName = 'StApps';
calendarName: string;
// eslint-disable-next-line @typescript-eslint/no-empty-function
constructor(
readonly calendar: Calendar,
private readonly configProvider: ConfigProvider,
) {
this.calendarName = (this.configProvider.getValue('name') as string) ?? 'StApps';
this.calendarName = this.configProvider.config.app.name ?? 'StApps';
}
async createCalendar(): Promise<CalendarInfo | undefined> {

View File

@@ -20,7 +20,6 @@ import {IonicModule} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core';
import {MomentModule} from 'ngx-moment';
import {DataModule} from '../data/data.module';
import {SettingsProvider} from '../settings/settings.provider';
import {CatalogComponent} from './catalog.component';
import {UtilModule} from '../../util/util.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
@@ -46,6 +45,5 @@ const catalogRoutes: Routes = [
DataModule,
UtilModule,
],
providers: [SettingsProvider],
})
export class CatalogModule {}

View File

@@ -16,12 +16,6 @@ import {TestBed} from '@angular/core/testing';
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
import {StorageProvider} from '../storage/storage.provider';
import {ConfigProvider, STORAGE_KEY_CONFIG} from './config.provider';
import {
ConfigFetchError,
ConfigInitError,
SavedConfigNotAvailable,
WrongConfigVersionInStorage,
} from './errors';
import {NGXLogger} from 'ngx-logger';
import {sampleIndexResponse} from '../../_helpers/data/sample-configuration';

View File

@@ -14,19 +14,14 @@
*/
import {Injectable} from '@angular/core';
import {Client} from '@openstapps/api';
import {SCAppConfiguration, SCIndexResponse} from '@openstapps/core';
import {SCIndexResponse} from '@openstapps/core';
import packageInfo from '@openstapps/core/package.json';
import {NGXLogger} from 'ngx-logger';
import {environment} from '../../../environments/environment';
import {StAppsWebHttpClient} from '../data/stapps-web-http-client.provider';
import {StorageProvider} from '../storage/storage.provider';
import {
ConfigFetchError,
ConfigInitError,
ConfigValueNotAvailable,
SavedConfigNotAvailable,
WrongConfigVersionInStorage,
} from './errors';
import equals from 'fast-deep-equal/es6';
import {BehaviorSubject} from 'rxjs';
/**
* Key to store config in storage module
@@ -35,6 +30,17 @@ import {
*/
export const STORAGE_KEY_CONFIG = 'stapps.config';
/**
* Makes an object deeply immutable
*/
function deepFreeze<T extends object>(object: T) {
for (const key of Object.keys(object)) {
const value = (object as Record<string, unknown>)[key];
if (typeof value === 'object' && !Object.isFrozen(value)) deepFreeze(value!);
}
return Object.freeze(object);
}
/**
* Provides configuration
*/
@@ -50,7 +56,7 @@ export class ConfigProvider {
/**
* App configuration as IndexResponse
*/
config: SCIndexResponse;
config: Readonly<SCIndexResponse>;
/**
* Version of the @openstapps/core package that app is using
@@ -62,6 +68,11 @@ export class ConfigProvider {
*/
firstSession = true;
/**
* If the config requires an update
*/
needsUpdate$ = new BehaviorSubject(false);
/**
* Constructor, initialise api client
* @param storageProvider StorageProvider to load persistent configuration
@@ -76,104 +87,35 @@ export class ConfigProvider {
this.client = new Client(swHttpClient, environment.backend_url, environment.backend_version);
}
/**
* Fetches configuration from backend
*/
async fetch(): Promise<SCIndexResponse> {
try {
return await this.client.handshake(this.scVersion);
} catch {
throw new ConfigFetchError();
}
}
/**
* Returns the value of an app configuration
* @param attribute requested attribute from app configuration
*/
public getValue(attribute: keyof SCAppConfiguration) {
if (this.config.app[attribute] !== undefined) {
return this.config.app[attribute];
}
throw new ConfigValueNotAvailable(attribute);
}
/**
* Returns a value of the configuration (not only app configuration)
* @param attribute requested attribute from the configuration
*/
public getAnyValue(attribute: keyof SCIndexResponse) {
if (this.config[attribute] !== undefined) {
return this.config[attribute];
}
throw new ConfigValueNotAvailable(attribute);
}
/**
* Initialises the ConfigProvider
* @throws ConfigInitError if no configuration could be loaded.
* @throws WrongConfigVersionInStorage if fetch failed and saved config has wrong SCVersion
*/
async init(): Promise<void> {
let loadError;
let fetchError;
// load saved configuration
try {
this.config = await this.loadLocal();
this.firstSession = false;
this.logger.log(`initialised configuration from storage`);
if (this.config.backend.SCVersion !== this.scVersion) {
loadError = new WrongConfigVersionInStorage(this.scVersion, this.config.backend.SCVersion);
this.config = (await this.storageProvider.has(STORAGE_KEY_CONFIG))
? await this.storageProvider.get<SCIndexResponse>(STORAGE_KEY_CONFIG)
: undefined!;
this.firstSession = !this.config;
const updatedConfig = this.client.handshake(this.scVersion).then(async fetchedConfig => {
if (!equals(fetchedConfig, this.config)) {
await this.storageProvider.put(STORAGE_KEY_CONFIG, fetchedConfig);
this.logger.log(`Config updated`);
this.needsUpdate$.next(true);
this.needsUpdate$.complete();
}
} catch (error) {
loadError = error;
}
// fetch remote configuration from backend
try {
const fetchedConfig: SCIndexResponse = await this.fetch();
await this.set(fetchedConfig);
this.logger.log(`initialised configuration from remote`);
} catch (error) {
fetchError = error;
}
// check for occurred errors and throw them
if (loadError !== undefined && fetchError !== undefined) {
throw new ConfigInitError();
}
if (loadError !== undefined) {
this.logger.warn(loadError);
}
if (fetchError !== undefined) {
this.logger.warn(fetchError);
}
}
return fetchedConfig;
});
/**
* Returns saved configuration from StorageModule
* @throws SavedConfigNotAvailable if no configuration could be loaded
*/
async loadLocal(): Promise<SCIndexResponse> {
// get local configuration
if (await this.storageProvider.has(STORAGE_KEY_CONFIG)) {
return this.storageProvider.get<SCIndexResponse>(STORAGE_KEY_CONFIG);
this.config ??= await updatedConfig;
this.config = deepFreeze(this.config);
if (this.config.backend.SCVersion !== this.scVersion) {
this.logger.warn(
'Incompatible config, expected',
this.scVersion,
'but got',
this.config.backend.SCVersion,
);
}
throw new SavedConfigNotAvailable();
}
/**
* Saves the configuration from the provider
* @param config configuration to save
*/
async save(config: SCIndexResponse): Promise<void> {
await this.storageProvider.put(STORAGE_KEY_CONFIG, config);
}
/**
* Sets the configuration in the module and writes it into app storage
* @param config SCIndexResponse to set
*/
async set(config: SCIndexResponse): Promise<void> {
this.config = config;
await this.save(this.config);
}
}

View File

@@ -1,65 +0,0 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {AppError} from '../../_helpers/errors';
/**
* Error that is thrown when fetching from backend fails
*/
export class ConfigFetchError extends AppError {
constructor() {
super('ConfigFetchError', 'App configuration could not be fetched!');
}
}
/**
* Error that is thrown when the ConfigProvider could be initialised
*/
export class ConfigInitError extends AppError {
constructor() {
super('ConfigInitError', 'App configuration could not be initialised!');
}
}
/**
* Error that is thrown when the requested config value is not available
*/
export class ConfigValueNotAvailable extends AppError {
constructor(valueKey: string) {
super('ConfigValueNotAvailable', `No attribute "${valueKey}" in config available!`);
}
}
/**
* Error that is thrown when no saved config is available
*/
export class SavedConfigNotAvailable extends AppError {
constructor() {
super('SavedConfigNotAvailable', 'No saved app configuration available.');
}
}
/**
* Error that is thrown when the SCVersion of the saved config is not compatible with the app
*/
export class WrongConfigVersionInStorage extends AppError {
constructor(correctVersion: string, savedVersion: string) {
super(
'WrongConfigVersionInStorage',
`The saved configs backend version ${savedVersion} ` +
`does not equal the configured backend version ${correctVersion} of the app.`,
);
}
}

View File

@@ -21,7 +21,6 @@ import {SwiperModule} from 'swiper/angular';
import {TranslateModule, TranslatePipe} from '@ngx-translate/core';
import {MomentModule} from 'ngx-moment';
import {DataModule} from '../data/data.module';
import {SettingsProvider} from '../settings/settings.provider';
import {DashboardComponent} from './dashboard.component';
import {SearchSectionComponent} from './sections/search-section/search-section.component';
import {NewsSectionComponent} from './sections/news-section/news-section.component';
@@ -70,6 +69,6 @@ const catalogRoutes: Routes = [
NewsModule,
JobModule,
],
providers: [SettingsProvider, TranslatePipe],
providers: [TranslatePipe],
})
export class DashboardModule {}

View File

@@ -32,7 +32,6 @@ import {ScheduleProvider} from '../calendar/schedule.provider';
import {GeoNavigationDirective} from '../map/geo-navigation.directive';
import {MapWidgetComponent} from '../map/widget/map-widget.component';
import {MenuModule} from '../menu/menu.module';
import {SettingsProvider} from '../settings/settings.provider';
import {StorageModule} from '../storage/storage.module';
import {ActionChipListComponent} from './chips/action-chip-list.component';
import {AddEventActionChipComponent} from './chips/data/add-event-action-chip.component';
@@ -214,7 +213,6 @@ import {ShareButtonComponent} from './elements/share-button.component';
StAppsWebHttpClient,
CalendarService,
RoutingStackService,
SettingsProvider,
{
provide: SimpleBrowser,
useFactory: browserFactory,

View File

@@ -15,13 +15,13 @@
import {Component, ContentChild, EventEmitter, Input, OnInit, Output, TemplateRef} from '@angular/core';
import {ActivatedRoute, Router} from '@angular/router';
import {ModalController} from '@ionic/angular';
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
import {SCLanguageCode, SCSaveableThing, SCThings, SCUuid} from '@openstapps/core';
import {SCSaveableThing, SCThings, SCUuid} from '@openstapps/core';
import {DataProvider, DataScope} from '../data.provider';
import {FavoritesService} from '../../favorites/favorites.service';
import {take} from 'rxjs/operators';
import {Network} from '@capacitor/network';
import {DataListContext} from '../list/data-list.component';
import {lastValueFrom} from 'rxjs';
export interface ExternalDataLoadEvent {
uid: SCUuid;
@@ -29,6 +29,13 @@ export interface ExternalDataLoadEvent {
resolve: (item: SCThings | null | undefined) => void;
}
/**
* Type guard for SCSavableThing
*/
function isSCSavableThing(thing: SCThings | SCSaveableThing): thing is SCSaveableThing {
return (thing as SCSaveableThing).data !== undefined;
}
/**
* A Component to display an SCThing detailed
*/
@@ -53,11 +60,6 @@ export class DataDetailComponent implements OnInit {
@Input() autoRouteDataPath = true;
/**
* The language of the item
*/
language: SCLanguageCode;
/**
* Indicating wether internet connectivity is given or not
*/
@@ -79,20 +81,12 @@ export class DataDetailComponent implements OnInit {
@Output() loadItem: EventEmitter<ExternalDataLoadEvent> = new EventEmitter<ExternalDataLoadEvent>();
/**
* Type guard for SCSavableThing
*/
static isSCSavableThing(thing: SCThings | SCSaveableThing): thing is SCSaveableThing {
return (thing as SCSaveableThing).data !== undefined;
}
constructor(
protected readonly route: ActivatedRoute,
router: Router,
private readonly dataProvider: DataProvider,
private readonly favoritesService: FavoritesService,
readonly modalController: ModalController,
translateService: TranslateService,
) {
this.inputItem = router.getCurrentNavigation()?.extras.state?.item;
if (!this.inputItem?.origin) {
@@ -100,10 +94,6 @@ export class DataDetailComponent implements OnInit {
// This can happen, for example, when detail views use `inPlace` list items
delete this.inputItem;
}
this.language = translateService.currentLang as SCLanguageCode;
translateService.onLangChange.subscribe((event: LangChangeEvent) => {
this.language = event.lang as SCLanguageCode;
});
this.isDisconnected = new Promise(async resolve => {
const isConnected = (await Network.getStatus()).connected;
@@ -126,13 +116,8 @@ export class DataDetailComponent implements OnInit {
)
: this.dataProvider.get(uid, DataScope.Remote)));
this.item = item
? // eslint-disable-next-line unicorn/no-null
DataDetailComponent.isSCSavableThing(item)
? item.data
: item
: // eslint-disable-next-line unicorn/no-null
null;
// eslint-disable-next-line unicorn/no-null
this.item = item ? (isSCSavableThing(item) ? item.data : item) : null;
} catch {
// eslint-disable-next-line unicorn/no-null
this.item = null;
@@ -144,14 +129,10 @@ export class DataDetailComponent implements OnInit {
await this.getItem(uid ?? '', false);
// fallback to the saved item (from favorites)
if (this.item === null) {
this.favoritesService
.get(uid)
.pipe(take(1))
.subscribe(item => {
if (item !== undefined) {
this.item = item.data;
}
});
const item = await lastValueFrom(this.favoritesService.get(uid).pipe(take(1)));
if (item) {
this.item = item.data;
}
}
}
}

View File

@@ -30,8 +30,8 @@ export class OffersInListComponent {
@Input() set offers(it: Array<SCThingThatCanBeOfferedOffer<SCAcademicPriceGroup>>) {
this._offers = it;
this.price = it[0].prices?.default;
this.settingsProvider.getSetting('profile', 'group').then(group => {
this.price = it[0].prices?.[(group.value as string).replace(/s$/, '') as never];
this.settingsProvider.getSetting<string>('profile', 'group').then(group => {
this.price = it[0].prices?.[group.replace(/s$/, '') as never];
});
const availabilities = new Set(it.map(offer => offer.availability));

View File

@@ -17,14 +17,7 @@ import {ActivatedRoute, Router} from '@angular/router';
import {Keyboard} from '@capacitor/keyboard';
import {AlertController, AnimationBuilder, AnimationController} from '@ionic/angular';
import {Capacitor} from '@capacitor/core';
import {
SCFacet,
SCFeatureConfiguration,
SCSearchFilter,
SCSearchQuery,
SCSearchSort,
SCThings,
} from '@openstapps/core';
import {SCFacet, SCSearchFilter, SCSearchQuery, SCSearchSort, SCThings} from '@openstapps/core';
import {NGXLogger} from 'ngx-logger';
import {combineLatest, Subject} from 'rxjs';
import {debounceTime, distinctUntilChanged, startWith} from 'rxjs/operators';
@@ -170,9 +163,8 @@ export class SearchPageComponent implements OnInit {
private readonly route: ActivatedRoute,
protected positionService: PositionService,
private readonly configProvider: ConfigProvider,
animationController: AnimationController,
) {
this.routeAnimation = searchPageSwitchAnimation(animationController);
this.routeAnimation = searchPageSwitchAnimation(inject(AnimationController));
}
/**
@@ -323,16 +315,6 @@ export class SearchPageComponent implements OnInit {
this.queryChanged.next();
}
});
this.settingsProvider.settingsActionChanged$
.pipe(takeUntilDestroyed(this.destroy$))
.subscribe(({type, payload}) => {
if (type === 'stapps.settings.changed') {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const {category, name, value} = payload!;
this.logger.log(`received event "settings.changed" with category:
${category}, name: ${name}, value: ${JSON.stringify(value)}`);
}
});
this.dataRoutingService
.itemSelectListener()
.pipe(takeUntilDestroyed(this.destroy$))
@@ -342,12 +324,8 @@ export class SearchPageComponent implements OnInit {
}
});
}
try {
const features = this.configProvider.getValue('features') as SCFeatureConfiguration;
this.isHebisAvailable = !!features.plugins?.['hebis-plugin']?.urlPath;
} catch (error) {
this.logger.error(error);
}
this.isHebisAvailable =
this.configProvider.config.app.features.plugins?.['hebis-plugin']?.urlPath !== undefined;
}
/**

View File

@@ -21,7 +21,6 @@ import {
SCRatingResponse,
SCRatingRoute,
SCUserGroup,
SCUserGroupSetting,
SCUuid,
} from '@openstapps/core';
import {StAppsWebHttpClient} from './stapps-web-http-client.provider';
@@ -63,9 +62,7 @@ export class RatingProvider {
}
private get userGroup(): Promise<SCUserGroup> {
return this.settingsProvider
.getSetting('profile', 'group')
.then(it => (it as SCUserGroupSetting).value as SCUserGroup);
return this.settingsProvider.getSetting<SCUserGroup>('profile', 'group');
}
private async getStoredRatings(): Promise<RatingStorage> {

View File

@@ -75,7 +75,7 @@ export class PlaceMensaService {
sort: [
{
arguments: {
field: `offers.prices.${(priceGroup.value as string).replace(/s$/, '')}`,
field: `offers.prices.${(priceGroup as string).replace(/s$/, '')}`,
},
order: 'desc',
type: 'generic',

View File

@@ -12,21 +12,13 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, OnInit} from '@angular/core';
import {AlertController, AnimationController} from '@ionic/angular';
import {ActivatedRoute, Router} from '@angular/router';
import {NGXLogger} from 'ngx-logger';
import {Component, inject, OnInit} from '@angular/core';
import {debounceTime, distinctUntilChanged, startWith, take} from 'rxjs/operators';
import {combineLatest} from 'rxjs';
import {SCThingType} from '@openstapps/core';
import {FavoritesService} from './favorites.service';
import {DataRoutingService} from '../data/data-routing.service';
import {ContextMenuService} from '../menu/context/context-menu.service';
import {SearchPageComponent} from '../data/list/search-page.component';
import {DataProvider} from '../data/data.provider';
import {SettingsProvider} from '../settings/settings.provider';
import {PositionService} from '../map/position.service';
import {ConfigProvider} from '../config/config.provider';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
/**
@@ -42,34 +34,7 @@ export class FavoritesPageComponent extends SearchPageComponent implements OnIni
showNavigation = false;
constructor(
alertController: AlertController,
dataProvider: DataProvider,
contextMenuService: ContextMenuService,
settingsProvider: SettingsProvider,
logger: NGXLogger,
dataRoutingService: DataRoutingService,
router: Router,
route: ActivatedRoute,
positionService: PositionService,
private favoritesService: FavoritesService,
configProvider: ConfigProvider,
animationController: AnimationController,
) {
super(
alertController,
dataProvider,
contextMenuService,
settingsProvider,
logger,
dataRoutingService,
router,
route,
positionService,
configProvider,
animationController,
);
}
private favoritesService = inject(FavoritesService);
ngOnInit() {
super.ngOnInit(false);
@@ -96,16 +61,6 @@ export class FavoritesPageComponent extends SearchPageComponent implements OnIni
this.queryChanged.next();
}
});
this.settingsProvider.settingsActionChanged$
.pipe(takeUntilDestroyed(this.destroy$))
.subscribe(({type, payload}) => {
if (type === 'stapps.settings.changed') {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const {category, name, value} = payload!;
this.logger.log(`received event "settings.changed" with category:
${category}, name: ${name}, value: ${JSON.stringify(value)}`);
}
});
this.dataRoutingService
.itemSelectListener()
.pipe(takeUntilDestroyed(this.destroy$))

View File

@@ -28,7 +28,6 @@ import {
} from '@openstapps/core';
import {StorageProvider} from '../storage/storage.provider';
import {DataProvider} from '../data/data.provider';
import {ThingTranslatePipe} from '../../translation/thing-translate.pipe';
import {TranslateService} from '@ngx-translate/core';
import {ThingTranslateService} from '../../translation/thing-translate.service';
import {BehaviorSubject, Observable} from 'rxjs';
@@ -41,11 +40,6 @@ import {debounceTime, map} from 'rxjs/operators';
providedIn: 'root',
})
export class FavoritesService {
/**
* Translation pipe
*/
thingTranslatePipe: ThingTranslatePipe;
favorites = new BehaviorSubject<Map<string, SCFavorite>>(new Map<string, SCFavorite>());
// using debounce time 0 allows change detection to run through async suspension
@@ -93,8 +87,8 @@ export class FavoritesService {
return items.sort((a, b) => {
return (
new Intl.Collator(this.translate.currentLang).compare(
this.thingTranslatePipe.transform(field, a),
this.thingTranslatePipe.transform(field, b),
this.thingTranslate.get(a, field) as string,
this.thingTranslate.get(b, field) as string,
) * reverse
);
});
@@ -124,7 +118,6 @@ export class FavoritesService {
private readonly translate: TranslateService,
private readonly thingTranslate: ThingTranslateService,
) {
this.thingTranslatePipe = new ThingTranslatePipe(this.translate, this.thingTranslate);
void this.emitAll();
}
@@ -185,7 +178,9 @@ export class FavoritesService {
const textFilteredItems: SCIndexableThings[] = [];
for (const item of items) {
if (
this.thingTranslatePipe.transform('name', item).toLowerCase().includes(queryText.toLowerCase())
(this.thingTranslate.get(item, 'name') as string)
.toLowerCase()
.includes(queryText.toLowerCase())
) {
textFilteredItems.push(item);
}

View File

@@ -17,7 +17,6 @@ import {DaiaAvailabilityResponse, DaiaHolding, DaiaService} from './protocol/res
import {StorageProvider} from '../storage/storage.provider';
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {ConfigProvider} from '../config/config.provider';
import {SCFeatureConfiguration} from '@openstapps/core';
import {NGXLogger} from 'ngx-logger';
import {TranslateService} from '@ngx-translate/core';
@@ -67,7 +66,7 @@ export class DaiaDataProvider {
async getAvailability(id: string): Promise<DaiaHolding[] | undefined> {
if (this.daiaServiceUrl === undefined) {
try {
const features = this.configProvider.getValue('features') as SCFeatureConfiguration;
const features = this.configProvider.config.app.features;
if (features.extern?.daia?.url) {
this.daiaServiceUrl = features.extern?.daia?.url;
} else {

View File

@@ -114,16 +114,6 @@ export class HebisSearchPageComponent extends SearchPageComponent implements OnI
this.queryChanged.next();
}
});
this.settingsProvider.settingsActionChanged$
.pipe(takeUntilDestroyed(this.destroy$))
.subscribe(({type, payload}) => {
if (type === 'stapps.settings.changed') {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const {category, name, value} = payload!;
this.logger.log(`received event "settings.changed" with category:
${category}, name: ${name}, value: ${JSON.stringify(value)}`);
}
});
this.dataRoutingService
.itemSelectListener()
.pipe(takeUntilDestroyed(this.destroy$))

View File

@@ -12,14 +12,9 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Injectable} from '@angular/core';
import {JQueryRequestor, Requestor} from '@openid/appauth';
import {
SCAuthorizationProviderType,
SCFeatureConfiguration,
SCFeatureConfigurationExtern,
} from '@openstapps/core';
import {SCAuthorizationProviderType, SCFeatureConfigurationExtern} from '@openstapps/core';
import {DocumentAction, PAIADocument, PAIADocumentStatus, PAIAFees, PAIAItems, PAIAPatron} from '../types';
import {HebisDataProvider} from '../../hebis/hebis-data.provider';
import {PAIATokenResponse} from '../../auth/paia/paia-token-response';
@@ -53,9 +48,7 @@ export class LibraryAccountService {
private readonly toastController: ToastController,
) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const config: SCFeatureConfigurationExtern = (
configProvider.getValue('features') as SCFeatureConfiguration
).extern!.paia;
const config: SCFeatureConfigurationExtern = configProvider.config.app.features.extern!.paia;
this.baseUrl = config.url;
this.authType = config.authProvider as SCAuthorizationProviderType;
}

View File

@@ -19,7 +19,6 @@ import {LeafletModule} from '@asymmetrik/ngx-leaflet';
import {LeafletMarkerClusterModule} from '@asymmetrik/ngx-leaflet-markercluster';
import {IonicModule} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core';
import {Polygon} from 'geojson';
import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {ConfigProvider} from '../config/config.provider';
import {DataFacetsProvider} from '../data/data-facets.provider';
@@ -42,7 +41,7 @@ import {GeoNavigationDirective} from './geo-navigation.directive';
*/
export function initMapConfigFactory(configProvider: ConfigProvider, mapProvider: MapProvider) {
return async () => {
mapProvider.defaultPolygon = (await configProvider.getValue('campusPolygon')) as Polygon;
mapProvider.defaultPolygon = configProvider.config.app.campusPolygon;
};
}

View File

@@ -116,7 +116,7 @@ export class MapProvider {
private positionService: PositionService,
private configProvider: ConfigProvider,
) {
this.defaultPolygon = this.configProvider.getValue('campusPolygon') as Polygon;
this.defaultPolygon = this.configProvider.config.app.campusPolygon;
}
/**

View File

@@ -13,11 +13,11 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, Input} from '@angular/core';
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
import {SCLanguage, SCThingTranslator, SCThingType, SCTranslations} from '@openstapps/core';
import {SCThingType} from '@openstapps/core';
import {ContextMenuService} from './context-menu.service';
import {FilterContext, FilterFacet, SortContext, SortContextOption} from './context-type.js';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {ThingTranslateService} from '../../../translation/thing-translate.service';
/**
* The context menu
@@ -59,11 +59,6 @@ export class ContextMenuComponent {
return this.filterOption.options.filter(it => it.buckets.length > 0);
}
/**
* Possible languages to be used for translation
*/
language: keyof SCTranslations<SCLanguage>;
/**
* Mapping of SCThingType
*/
@@ -74,22 +69,10 @@ export class ContextMenuComponent {
*/
sortOption: SortContext;
/**
* Core translator
*/
translator: SCThingTranslator;
constructor(
private translateService: TranslateService,
private readonly contextMenuService: ContextMenuService,
private readonly thingTranslateService: ThingTranslateService,
) {
this.language = this.translateService.currentLang as keyof SCTranslations<SCLanguage>;
this.translator = new SCThingTranslator(this.language);
this.translateService.onLangChange.pipe(takeUntilDestroyed()).subscribe((event: LangChangeEvent) => {
this.language = event.lang as keyof SCTranslations<SCLanguage>;
this.translator = new SCThingTranslator(this.language);
});
this.contextMenuService.filterContextChanged$.pipe(takeUntilDestroyed()).subscribe(filterContext => {
this.filterOption = filterContext;
});
@@ -109,7 +92,7 @@ export class ContextMenuComponent {
* Returns translated property value
*/
getTranslatedPropertyValue(onlyForType: SCThingType, field: string, key?: string): string | undefined {
return this.translator.translatedPropertyValue(onlyForType, field, key);
return this.thingTranslateService.translator.translatedPropertyValue(onlyForType, field, key);
}
/**

View File

@@ -13,74 +13,23 @@
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Component, OnInit} from '@angular/core';
import {LangChangeEvent, TranslateService} from '@ngx-translate/core';
import {
SCAppConfigurationMenuCategory,
SCLanguage,
SCThingTranslator,
SCTranslations,
} from '@openstapps/core';
import {NavigationService} from './navigation.service';
import config from 'capacitor.config';
import {SettingsProvider} from '../../settings/settings.provider';
import {BreakpointObserver} from '@angular/cdk/layout';
import {SCAppConfigurationMenuCategory} from '@openstapps/core';
import {ConfigProvider} from '../../config/config.provider';
/**
* Generated class for the MenuPage page.
*
* See https://ionicframework.com/docs/components/#navigation for more info on
* Ionic pages and navigation.
*/
@Component({
selector: 'stapps-navigation',
styleUrls: ['navigation.scss'],
templateUrl: 'navigation.html',
})
export class NavigationComponent implements OnInit {
showTabbar = true;
/**
* Name of the app
*/
appName = config.appName;
/**
* Possible languages to be used for translation
*/
language: keyof SCTranslations<SCLanguage>;
/**
* Menu entries from config module
*/
menu: SCAppConfigurationMenuCategory[];
/**
* Core translator
*/
translator: SCThingTranslator;
constructor(
public translateService: TranslateService,
private navigationService: NavigationService,
private settingsProvider: SettingsProvider,
private responsive: BreakpointObserver,
) {
translateService.onLangChange.subscribe((event: LangChangeEvent) => {
this.language = event.lang as keyof SCTranslations<SCLanguage>;
this.translator = new SCThingTranslator(this.language);
});
this.responsive.observe(['(min-width: 768px)']).subscribe(result => {
this.showTabbar = !result.matches;
});
}
constructor(private config: ConfigProvider) {}
async ngOnInit() {
this.language = (await this.settingsProvider.getValue(
'profile',
'language',
)) as keyof SCTranslations<SCLanguage>;
this.translator = new SCThingTranslator(this.language);
this.menu = await this.navigationService.getMenu();
this.menu = this.config.config.app.menus;
}
}

View File

@@ -34,11 +34,11 @@
class="menu-category"
>
<ion-icon slot="end" [name]="category.icon"></ion-icon>
<ion-label> {{ category.translations[language]?.title | titlecase }} </ion-label>
<ion-label> {{ 'title' | translateSimple: category | titlecase }} </ion-label>
</ion-item>
<ion-item *ngFor="let item of category.items" [rootLink]="item.route" [redirectedFrom]="item.route">
<ion-icon slot="end" [name]="item.icon"></ion-icon>
<ion-label> {{ item.translations[language]?.title | titlecase }} </ion-label>
<ion-label> {{ 'title' | translateSimple: item | titlecase }} </ion-label>
</ion-item>
</ion-list>
</ion-content>

View File

@@ -22,10 +22,11 @@ import {IonIconModule} from '../../../util/ion-icon/ion-icon.module';
import {TranslateModule} from '@ngx-translate/core';
import {RouterModule} from '@angular/router';
import {OfflineNoticeComponent} from './offline-notice.component';
import {ThingTranslateModule} from '../../../translation/thing-translate.module';
@NgModule({
declarations: [RootLinkDirective, NavigationComponent, TabsComponent, OfflineNoticeComponent],
imports: [CommonModule, IonicModule, IonIconModule, TranslateModule, RouterModule],
imports: [CommonModule, IonicModule, IonIconModule, TranslateModule, RouterModule, ThingTranslateModule],
exports: [TabsComponent, RootLinkDirective, NavigationComponent],
})
export class NavigationModule {}

View File

@@ -60,6 +60,7 @@ stapps-navigation-tabs {
}
}
stapps-offline-notice.needs-reload ~ ion-split-pane,
stapps-offline-notice.has-error ~ ion-split-pane,
stapps-offline-notice.is-offline ~ ion-split-pane {
margin-top: calc(var(--font-size-md) + 2 * var(--spacing-sm));

View File

@@ -1,40 +0,0 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {Injectable} from '@angular/core';
import {SCAppConfigurationMenuCategory} from '@openstapps/core';
import {ConfigProvider} from '../../config/config.provider';
import {NGXLogger} from 'ngx-logger';
@Injectable({
providedIn: 'root',
})
export class NavigationService {
constructor(
private configProvider: ConfigProvider,
private logger: NGXLogger,
) {}
async getMenu() {
let menu: SCAppConfigurationMenuCategory[] = [];
try {
menu = this.configProvider.getValue('menus') as SCAppConfigurationMenuCategory[];
} catch (error) {
this.logger.error(`error from loading menu entries: ${error}`);
}
return menu;
}
}

View File

@@ -17,6 +17,10 @@ import {InternetConnectionService} from '../../../util/internet-connection.servi
import {Router} from '@angular/router';
import {NGXLogger} from 'ngx-logger';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {SettingsProvider} from '../../settings/settings.provider';
import {AnimationController} from '@ionic/angular';
import {filter, race} from 'rxjs';
import {ConfigProvider} from '../../config/config.provider';
@Component({
selector: 'stapps-offline-notice',
@@ -28,12 +32,17 @@ export class OfflineNoticeComponent {
@HostBinding('class.has-error') hasError = false;
@HostBinding('class.needs-reload') needsReload = false;
@ViewChild('spinIcon', {read: ElementRef}) spinIcon: ElementRef;
constructor(
readonly offlineProvider: InternetConnectionService,
readonly router: Router,
readonly logger: NGXLogger,
readonly animationCtl: AnimationController,
settingsProvider: SettingsProvider,
configProvider: ConfigProvider,
) {
this.offlineProvider.offline$.pipe(takeUntilDestroyed()).subscribe(isOffline => {
this.isOffline = isOffline;
@@ -41,6 +50,15 @@ export class OfflineNoticeComponent {
this.offlineProvider.error$.pipe(takeUntilDestroyed()).subscribe(hasError => {
this.hasError = hasError;
});
race(
settingsProvider.needsReload$.pipe(filter(it => it)),
configProvider.needsUpdate$.pipe(filter(it => it)),
)
.pipe(takeUntilDestroyed())
.subscribe(() => {
console.log('aha!');
this.needsReload = true;
});
}
retry() {
@@ -49,4 +67,15 @@ export class OfflineNoticeComponent {
this.spinIcon.nativeElement.classList.add('spin');
this.offlineProvider.retry();
}
async reloadPage() {
await this.animationCtl
.create()
.duration(100)
.fromTo('opacity', 1, 0)
.addElement(document.querySelector('ion-app')!)
.play();
window.location.reload();
}
}

View File

@@ -20,6 +20,10 @@
<ion-icon #spinIcon slot="start" [size]="16" [weight]="800" name="refresh"></ion-icon>
<ion-label>{{ 'app.errors.CONNECTION_ERROR' | translate }}</ion-label>
</ion-button>
<ion-button class="reload" color="warning" (click)="reloadPage()">
<ion-icon slot="start" [size]="16" [weight]="800" name="refresh"></ion-icon>
<ion-label>{{ 'settings.reloadPage' | translate }}</ion-label>
</ion-button>
<ion-button class="close" fill="clear" color="light" (click)="offlineProvider.dismissError()"
><ion-icon [size]="16" [weight]="800" name="close" slot="icon-only"></ion-icon
></ion-button>

View File

@@ -28,6 +28,7 @@
transition: all 150ms ease;
&.needs-reload,
&.is-offline,
&.has-error {
transform: translateY(0);
@@ -64,6 +65,7 @@
}
}
&.needs-reload > .reload,
&.is-offline > .offline-button,
&.has-error > .close,
&.has-error > .error-button {

View File

@@ -16,6 +16,7 @@
import type {AnimationBuilder} from '@ionic/angular';
import {AnimationController} from '@ionic/angular';
import type {AnimationOptions} from '@ionic/angular/providers/nav-controller';
import {iosDuration, iosEasing, mdDuration, mdEasing} from '../../../animation/easings';
/**
*
@@ -23,10 +24,11 @@ import type {AnimationOptions} from '@ionic/angular/providers/nav-controller';
export function tabsTransition(animationController: AnimationController): AnimationBuilder {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (_baseElement: HTMLElement, options: AnimationOptions | any) => {
const duration = options.duration || 350;
const contentExitDuration = options.contentExitDuration || 100;
const isMd = document.querySelector('ion-app.md') !== null;
const duration = isMd ? mdDuration : iosDuration;
const easing = isMd ? mdEasing : iosEasing;
const rootTransition = animationController.create().duration(duration);
const rootTransition = animationController.create().duration(duration).easing(easing);
const enterTransition = animationController
.create()
@@ -39,23 +41,15 @@ export function tabsTransition(animationController: AnimationController): Animat
.addElement(options.leavingEl);
const exitTransition = animationController
.create()
.duration(contentExitDuration * 2)
.easing('cubic-bezier(0.87, 0, 0.13, 1)')
.fromTo('opacity', '1', '0')
.addElement(options.leavingEl.querySelector('ion-header'));
const contentExit = animationController
.create()
.easing('linear')
.duration(contentExitDuration)
.fromTo('opacity', '1', '0')
.addElement(options.leavingEl.querySelectorAll(':scope > *:not(ion-header)'));
const contentEnter = animationController
.create()
.delay(contentExitDuration)
.duration(duration - contentExitDuration)
.easing('cubic-bezier(0.16, 1, 0.3, 1)')
.fromTo('transform', 'scale(1.025)', 'scale(1)')
.fromTo('opacity', '0', '1')
.fromTo('transform', 'scale(1.05)', 'scale(1)')
.addElement(options.enteringEl.querySelectorAll(':scope > *:not(ion-header)'));
rootTransition.addAnimation([enterTransition, contentExit, contentEnter, exitTransition, exitZIndex]);

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