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
426 changed files with 11033 additions and 13734 deletions

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.

View File

@@ -2,13 +2,7 @@
/** @type {import('syncpack').RcFile} */
const config = {
semverGroups: [
{
range: '',
dependencies: ['**'],
packages: ['**'],
}
],
semverRange: '',
source: ['package.json', '**/package.json'],
indent: ' ',
sortFirst: [
@@ -55,7 +49,7 @@ const config = {
{
label: 'Packages should use workspace version',
dependencies: ['@openstapps/**'],
dependencyTypes: ['prod', 'dev', 'peer'],
dependencyTypes: ['prod', 'dev'],
packages: ['**'],
pinVersion: 'workspace:*',
},

View File

@@ -1,3 +1,3 @@
nodejs 18.19.1
pnpm 8.15.4
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

@@ -1,33 +1,5 @@
# @openstapps/backend
## 3.3.0
### Minor Changes
- 688bc5f2: v3.3.0 changes
### Patch Changes
- Updated dependencies [688bc5f2]
- @openstapps/core@3.3.0
- @openstapps/core-tools@3.3.0
- @openstapps/logger@3.0.0
## 3.2.0
### Minor Changes
- 912ae422: Add the ability to filter by existence of a field
### Patch Changes
- 689ac68b: pin alpine version to 3.18 and add healthchecks
- e8d72683: Backend unit tests break every year
- Updated dependencies [912ae422]
- @openstapps/core@4.0.0
- @openstapps/core-tools@3.0.0
- @openstapps/logger@3.0.0
## 3.1.2
### Patch Changes

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,15 +0,0 @@
# Goethe-Uni App 2.5
Die Goethe-Uni App ist noch besser geworden!
## Komplett neue Kartenansicht
Wir haben die Karte überarbeitet, um eine klarere und schnellere Übersicht zu bieten.
## Deutschlandticket mit an Bord
Wenn du das Upgrade des Semesterticket zum Deutschlandticket gemacht hast und eingeloggt bist, findet es sich jetzt auch in der App.
## Bibliotheksdienste sind wieder voll funktionsfähig
Aufgrund einiger Adhoc-Änderungen im Bibliothekssystem haben wir die App so angepasst, dass sie damit umgehen kann.

View File

@@ -1,15 +0,0 @@
# Goethe-Uni App 2.5
The Goethe-Uni App got even better!
## Completely new map view
We overhauled the map to offer you a clearer and faster and overview.
## Deutschlandticket included
If you upgraded your Semesterticket to a Deutschlandticket it will now reside in the App if you are logged in.
## Library services are fully functional again
Due to some adhoc changes in the library system we adjusted the app to handle them properly.

View File

@@ -1,7 +1,7 @@
{
"name": "@openstapps/backend",
"description": "A reference implementation for a StApps backend",
"version": "3.3.0",
"version": "3.1.2",
"private": true,
"type": "module",
"license": "AGPL-3.0-only",
@@ -64,7 +64,7 @@
"express-prom-bundle": "6.6.0",
"express-promise-router": "4.1.1",
"got": "12.6.0",
"moment": "2.30.1",
"moment": "2.29.4",
"morgan": "1.10.0",
"nock": "13.3.1",
"node-cache": "5.1.2",
@@ -98,9 +98,9 @@
"sinon": "15.0.4",
"sinon-express-mock": "2.2.1",
"supertest": "6.3.3",
"ts-node": "10.9.2",
"ts-node": "10.9.1",
"tsup": "6.7.0",
"typescript": "5.4.2"
"typescript": "5.1.6"
},
"tsup": {
"entry": [

View File

@@ -13,34 +13,15 @@
* 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 {
SCConfigFile,
SCPlace,
SCPlaceWithoutReferences,
SCSearchQuery,
SCSearchResponse,
SCThingWithCategoriesWithoutReferences,
SCThings,
SCUuid,
} from '@openstapps/core';
import {SCConfigFile, SCSearchQuery, SCSearchResponse, SCThings, SCUuid} from '@openstapps/core';
import {MailQueue} from '../notification/mail-queue.js';
import {Bulk} from './bulk-storage.js';
import {FeatureCollection, Point, Polygon} from 'geojson';
/**
* Creates an instance of a database
*/
export type DatabaseConstructor = new (config: SCConfigFile, mailQueue?: MailQueue) => Database;
export type SupplementaryGeoJSON = FeatureCollection<Point | Polygon, SupplementaryGeoJSONThing>;
export type SupplementaryGeoJSONThing = Pick<
Extract<SCThings, SCPlace>,
Exclude<
keyof SCPlaceWithoutReferences | keyof SCThingWithCategoriesWithoutReferences<never, never>,
'geo' | 'origin' | 'translations'
>
>;
/**
* Defines what one database class needs to have defined
*/
@@ -101,9 +82,4 @@ export interface Database {
* @param params Parameters which form a search query to search the backend data
*/
search(parameters: SCSearchQuery): Promise<SCSearchResponse>;
/**
* Get geo info for display on a map
*/
geo(): Promise<SupplementaryGeoJSON>;
}

View File

@@ -26,7 +26,7 @@ import {Logger} from '@openstapps/logger';
import moment from 'moment';
import {MailQueue} from '../../notification/mail-queue.js';
import {Bulk} from '../bulk-storage.js';
import {Database, SupplementaryGeoJSON, SupplementaryGeoJSONThing} from '../database.js';
import {Database} from '../database.js';
import {parseAggregations} from './aggregations.js';
import * as Monitoring from './monitoring.js';
import {buildQuery} from './query/query.js';
@@ -46,7 +46,6 @@ import {
} from './util/index.js';
import {noUndefined} from './util/no-undefined.js';
import {retryCatch, RetryOptions} from './util/retry.js';
import {Feature, Point, Polygon} from 'geojson';
/**
* A database interface for elasticsearch
@@ -406,49 +405,4 @@ export class Elasticsearch implements Database {
},
};
}
async geo(): Promise<SupplementaryGeoJSON> {
const searchResponse = await this.client.search<Extract<SCThings, {geo: unknown}>>({
body: {
query: {
exists: {
field: 'geo',
},
},
},
from: 0,
allow_no_indices: true,
index: ACTIVE_INDICES_ALIAS,
size: 1,
});
return {
type: 'FeatureCollection',
features: searchResponse.hits.hits
.map(thing => {
return thing._source?.geo
? ({
id: Number(thing._source.identifiers?.['OSM']) || undefined,
type: 'Feature',
geometry: thing._source.geo.polygon ?? thing._source.geo.point,
properties: {
name: thing._source.name,
sameAs: thing._source.sameAs,
image: thing._source.image,
alternateNames: thing._source.alternateNames,
description: thing._source.description,
identifiers: thing._source.identifiers,
categories: thing._source.categories,
categorySpecificValues: thing._source.categorySpecificValues,
openingHours: thing._source.openingHours,
address: thing._source.address,
uid: thing._source.uid,
type: thing._source.type,
},
} satisfies Feature<Polygon | Point, SupplementaryGeoJSONThing>)
: undefined;
})
.filter(noUndefined),
};
}
}

View File

@@ -19,29 +19,14 @@ import {QueryDslSpecificQueryContainer} from '../../types/util.js';
* Converts a geo filter to elasticsearch syntax
* @param filter A search filter for the retrieval of the data
*/
export function buildGeoFilter(filter: SCGeoFilter): QueryDslSpecificQueryContainer<'bool'> {
export function buildGeoFilter(filter: SCGeoFilter): QueryDslSpecificQueryContainer<'geo_shape'> {
return {
bool: {
should: [
{
geo_shape: {
ignore_unmapped: true,
[`${filter.arguments.field}.polygon`]: {
shape: filter.arguments.shape,
relation: filter.arguments.spatialRelation,
},
},
} satisfies QueryDslSpecificQueryContainer<'geo_shape'>,
{
geo_shape: {
ignore_unmapped: true,
[`${filter.arguments.field}.point`]: {
shape: filter.arguments.shape,
relation: filter.arguments.spatialRelation,
},
},
} satisfies QueryDslSpecificQueryContainer<'geo_shape'>,
],
geo_shape: {
ignore_unmapped: true,
[`${filter.arguments.field}.polygon`]: {
shape: filter.arguments.shape,
relation: filter.arguments.spatialRelation,
},
},
};
}

View File

@@ -21,31 +21,16 @@ import {QueryDslSpecificQueryContainer} from '../../types/util.js';
*/
export function buildValueFilter(
filter: SCSearchValueFilter,
):
| QueryDslSpecificQueryContainer<'exists'>
| QueryDslSpecificQueryContainer<'term'>
| QueryDslSpecificQueryContainer<'terms'> {
switch (typeof filter.arguments.value) {
case 'undefined': {
return {
exists: {
field: filter.arguments.field,
): QueryDslSpecificQueryContainer<'term'> | QueryDslSpecificQueryContainer<'terms'> {
return Array.isArray(filter.arguments.value)
? {
terms: {
[`${filter.arguments.field}.raw`]: filter.arguments.value,
},
};
}
case 'string': {
return {
}
: {
term: {
[`${filter.arguments.field}.raw`]: filter.arguments.value,
},
};
}
case 'object': {
return {
terms: {
[`${filter.arguments.field}.raw`]: filter.arguments.value,
},
};
}
}
}

View File

@@ -22,7 +22,7 @@ import http from 'http';
import {MailQueue} from '../src/notification/mail-queue.js';
import {Bulk, BulkStorage} from '../src/storage/bulk-storage.js';
import getPort from 'get-port';
import {Database, SupplementaryGeoJSON} from '../src/storage/database.js';
import {Database} from '../src/storage/database.js';
import {v4} from 'uuid';
import {backendConfig} from '../src/config.js';
import {getIndexUID} from '../src/storage/elasticsearch/util/index.js';
@@ -58,6 +58,7 @@ export async function startApp(): Promise<Express> {
* An elasticsearch mock
*/
export class ElasticsearchMock implements Database {
// @ts-expect-error never read
private bulk: Bulk | undefined;
private storageMock = new Map<string, SCThings>();
@@ -66,10 +67,6 @@ export class ElasticsearchMock implements Database {
// Nothing to do here
}
geo(): Promise<SupplementaryGeoJSON> {
throw new Error('Method not implemented.');
}
bulkCreated(bulk: Bulk): Promise<void> {
this.bulk = bulk;
return Promise.resolve(undefined);

View File

@@ -44,6 +44,7 @@ describe('Search route', async function () {
});
it('should reject GET, PUT with a valid search query', async function () {
// const expectedParams = JSON.parse(JSON.stringify(defaultParams));
const {status} = await testApp.get('/search').set('Accept', 'application/json').send({
query: 'Some search terms',
});

View File

@@ -479,39 +479,18 @@ describe('Query', function () {
it('should build geo filter for shapes and points', function () {
const filter = buildFilter(searchFilters.geoPoint);
const expectedFilter = {
bool: {
should: [
{
geo_shape: {
'geo.polygon': {
relation: undefined,
shape: {
coordinates: [
[50.123, 8.123],
[50.123, 8.123],
],
type: 'envelope',
},
},
'ignore_unmapped': true,
},
geo_shape: {
'geo.polygon': {
relation: undefined,
shape: {
type: 'envelope',
coordinates: [
[50.123, 8.123],
[50.123, 8.123],
],
},
{
geo_shape: {
'geo.point': {
relation: undefined,
shape: {
coordinates: [
[50.123, 8.123],
[50.123, 8.123],
],
type: 'envelope',
},
},
'ignore_unmapped': true,
},
},
],
},
'ignore_unmapped': true,
},
};
@@ -521,39 +500,18 @@ describe('Query', function () {
it('should build geo filter for shapes only', function () {
const filter = buildFilter(searchFilters.geoShape);
const expectedFilter = {
bool: {
should: [
{
geo_shape: {
'geo.polygon': {
relation: 'contains',
shape: {
coordinates: [
[50.123, 8.123],
[50.123, 8.123],
],
type: 'envelope',
},
},
'ignore_unmapped': true,
},
geo_shape: {
'geo.polygon': {
relation: 'contains',
shape: {
type: 'envelope',
coordinates: [
[50.123, 8.123],
[50.123, 8.123],
],
},
{
geo_shape: {
'geo.point': {
relation: 'contains',
shape: {
coordinates: [
[50.123, 8.123],
[50.123, 8.123],
],
type: 'envelope',
},
},
'ignore_unmapped': true,
},
},
],
},
'ignore_unmapped': true,
},
};

View File

@@ -2,7 +2,9 @@
"extends": "@openstapps/tsconfig",
"compilerOptions": {
"resolveJsonModule": true,
"useUnknownInCatchVariables": false
"useUnknownInCatchVariables": false,
"allowJs": true,
"checkJs": true
},
"exclude": ["lib", "app.js"]
"exclude": ["app.js", "lib/"]
}

View File

@@ -1,11 +1,5 @@
# @openstapps/database
## 3.2.0
### Patch Changes
- 689ac68b: pin alpine version to 3.18 and add healthchecks
## 3.0.0
### Patch Changes

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,6 +1,6 @@
{
"name": "@openstapps/database",
"version": "3.2.0",
"version": "3.0.0",
"private": true,
"files": [
"config",

View File

@@ -16,7 +16,7 @@
// ESM is not supported, and cts is not detected, so we use type-checked cjs instead.
/** @type {import('../src/common').ConfigFile} */
module.exports = {
const configFile = {
activeVersions: ['1\\.0\\.\\d+', '2\\.0\\.\\d+'],
hiddenRoutes: ['/bulk'],
logFormat: 'default',
@@ -31,3 +31,5 @@ module.exports = {
dhparam: '/etc/nginx/certs/dhparam.pem',
},
};
export default configFile;

View File

@@ -50,8 +50,8 @@
"dockerode": "3.3.5",
"is-cidr": "4.0.2",
"mustache": "4.2.0",
"semver": "7.6.0",
"typescript": "5.4.2"
"semver": "7.5.4",
"typescript": "5.1.6"
},
"devDependencies": {
"@openstapps/api-cli": "workspace:*",
@@ -65,7 +65,7 @@
"@types/mustache": "4.2.2",
"@types/node": "18.15.3",
"@types/proxyquire": "1.3.28",
"@types/semver": "7.5.8",
"@types/semver": "7.5.6",
"@types/sha1": "1.1.3",
"@types/sinon": "10.0.14",
"@types/sinon-chai": "3.2.9",
@@ -75,7 +75,7 @@
"mocha-junit-reporter": "2.2.0",
"sinon": "15.0.4",
"sinon-chai": "3.7.0",
"ts-node": "10.9.2",
"ts-node": "10.9.1",
"tsup": "6.7.0"
},
"tsup": {

View File

@@ -1,4 +1,4 @@
{
"extends": "@openstapps/tsconfig",
"exclude": ["config", "lib", "app.js"]
"exclude": ["./config/", "./lib/"]
}

View File

@@ -18,15 +18,16 @@
"devDependencies": {
"@openstapps/tsconfig": "workspace:*",
"@types/node": "18.15.3",
"eslint": "8.57.0",
"typescript": "5.4.2"
"eslint": "8.43.0",
"typescript": "5.1.6"
},
"peerDependencies": {
"@typescript-eslint/eslint-plugin": "7.2.0",
"@typescript-eslint/parser": "7.2.0",
"eslint": "8.57.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-jsdoc": "48.2.1",
"eslint-plugin-unicorn": "51.0.1"
"@typescript-eslint/eslint-plugin": "5.60.1",
"@typescript-eslint/parser": "5.60.1",
"eslint": "8.43.0",
"eslint-config-prettier": "8.8.0",
"eslint-plugin-jsdoc": "46.4.2",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-unicorn": "47.0.0"
}
}

View File

@@ -1,11 +1,5 @@
# @openstapps/prettier-config
## 3.2.0
### Patch Changes
- dbb55850: Update Prettier to 3.1.1
## 3.0.0
### Major Changes
@@ -36,7 +30,7 @@
```js
#!/usr/bin/env node
import "./lib/app.js";
import './lib/app.js';
```
- 64caebaf: Migrate to ESM
@@ -75,14 +69,11 @@
- 64caebaf: Migrated changelogs to changeset format
```js
import fs from "fs";
import fs from 'fs';
const path = "packages/logger/CHANGELOG.md";
const path = 'packages/logger/CHANGELOG.md';
fs.writeFileSync(
path,
fs.readFileSync(path, "utf8").replace(/^#+\s+\[/gm, "## ["),
);
fs.writeFileSync(path, fs.readFileSync(path, 'utf8').replace(/^#+\s+\[/gm, '## ['));
```
- 98546a97: Migrate away from @openstapps/configuration
@@ -124,7 +115,7 @@
```js
#!/usr/bin/env node
import "./lib/app.js";
import './lib/app.js';
```
- 64caebaf: Migrate to ESM
@@ -163,14 +154,11 @@
- 64caebaf: Migrated changelogs to changeset format
```js
import fs from "fs";
import fs from 'fs';
const path = "packages/logger/CHANGELOG.md";
const path = 'packages/logger/CHANGELOG.md';
fs.writeFileSync(
path,
fs.readFileSync(path, "utf8").replace(/^#+\s+\[/gm, "## ["),
);
fs.writeFileSync(path, fs.readFileSync(path, 'utf8').replace(/^#+\s+\[/gm, '## ['));
```
- 98546a97: Migrate away from @openstapps/configuration

View File

@@ -1,7 +1,7 @@
{
"name": "@openstapps/prettier-config",
"description": "StApps Prettier Config",
"version": "3.2.0",
"version": "3.0.0",
"type": "module",
"license": "GPL-3.0-only",
"repository": "git@gitlab.com:openstapps/prettier-config.git",

View File

@@ -43,8 +43,8 @@
"@openstapps/logger": "workspace:*",
"@slack/web-api": "6.8.1",
"commander": "10.0.0",
"date-fns": "3.6.0",
"glob": "10.3.10",
"date-fns": "2.30.0",
"glob": "10.2.7",
"mustache": "4.2.0"
},
"devDependencies": {
@@ -53,7 +53,7 @@
"@openstapps/tsconfig": "workspace:*",
"@types/chai": "4.3.5",
"@types/chai-as-promised": "7.1.5",
"@types/glob": "8.1.0",
"@types/glob": "8.0.1",
"@types/mocha": "10.0.1",
"@types/mustache": "4.2.2",
"@types/node": "18.15.3",
@@ -63,9 +63,9 @@
"chai-as-promised": "7.1.1",
"mocha": "10.2.0",
"mocha-junit-reporter": "2.2.0",
"ts-node": "10.9.2",
"ts-node": "10.9.1",
"tsup": "6.7.0",
"typescript": "5.4.2"
"typescript": "5.1.6"
},
"tsup": {
"entry": [

View File

@@ -1,4 +1,3 @@
{
"extends": "@openstapps/tsconfig",
"exclude": ["lib", "app.js"]
"extends": "@openstapps/tsconfig"
}

View File

@@ -1,11 +1,5 @@
# @openstapps/tsconfig
## 3.3.0
### Minor Changes
- 688bc5f2: v3.3.0 changes
## 3.0.0
### Major Changes
@@ -36,7 +30,7 @@
```js
#!/usr/bin/env node
import "./lib/app.js";
import './lib/app.js';
```
- 64caebaf: Migrate to ESM
@@ -111,7 +105,7 @@
```js
#!/usr/bin/env node
import "./lib/app.js";
import './lib/app.js';
```
- 64caebaf: Migrate to ESM

View File

@@ -1,7 +1,7 @@
{
"name": "@openstapps/tsconfig",
"description": "The tsconfig for the openstapps project",
"version": "3.3.0",
"version": "3.0.0",
"type": "commonjs",
"license": "GPL-3.0-only",
"repository": "git@gitlab.com:openstapps/eslint-config.git",

View File

@@ -19,21 +19,15 @@
"allowSyntheticDefaultImports": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "../../../lib/",
"lib": [
"ES2022",
"DOM"
],
"lib": ["ES2022", "DOM"],
"strict": true,
"target": "ES2022"
},
"ts-node": {
"transpileOnly": true
},
"exclude": [
"../../../app.js",
"../../../lib/"
]
"exclude": ["../../../app.js", "../../../lib/"]
}

View File

@@ -1,23 +1,5 @@
# @openstapps/minimal-connector
## 3.3.0
### Patch Changes
- Updated dependencies [688bc5f2]
- @openstapps/api@3.3.0
- @openstapps/core@3.3.0
- @openstapps/logger@3.0.0
## 3.2.0
### Patch Changes
- Updated dependencies [912ae422]
- @openstapps/core@3.2.0
- @openstapps/api@3.2.0
- @openstapps/logger@3.0.0
## 3.1.1
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@openstapps/minimal-connector",
"description": "This is a minimal connector which serves as an example",
"version": "3.3.0",
"version": "3.1.1",
"private": true,
"type": "module",
"license": "GPL-3.0-only",
@@ -53,9 +53,9 @@
"mocha": "10.2.0",
"mocha-junit-reporter": "2.2.0",
"nock": "13.3.1",
"ts-node": "10.9.2",
"ts-node": "10.9.1",
"tsup": "6.7.0",
"typescript": "5.4.2"
"typescript": "5.1.6"
},
"tsup": {
"entry": [

View File

@@ -1,4 +1,3 @@
{
"extends": "@openstapps/tsconfig",
"exclude": ["lib", "app.js"]
"extends": "@openstapps/tsconfig"
}

View File

@@ -1,27 +1,5 @@
# @openstapps/minimal-plugin
## 3.3.0
### Patch Changes
- Updated dependencies [688bc5f2]
- @openstapps/api@3.3.0
- @openstapps/core@3.3.0
- @openstapps/api-plugin@3.3.0
- @openstapps/core-tools@3.3.0
- @openstapps/logger@3.0.0
## 3.2.0
### Patch Changes
- Updated dependencies [912ae422]
- @openstapps/core@3.2.0
- @openstapps/api@3.2.0
- @openstapps/api-plugin@3.2.0
- @openstapps/core-tools@3.0.0
- @openstapps/logger@3.0.0
## 3.1.1
### Patch Changes

View File

@@ -1,7 +1,7 @@
{
"name": "@openstapps/minimal-plugin",
"description": "Minimal Plugin",
"version": "3.3.0",
"version": "3.1.1",
"private": true,
"type": "module",
"license": "GPL-3.0-only",
@@ -35,7 +35,7 @@
"@openstapps/logger": "workspace:*",
"commander": "10.0.0",
"express": "4.18.2",
"ts-node": "10.9.2"
"ts-node": "10.9.1"
},
"devDependencies": {
"@openstapps/eslint-config": "workspace:*",
@@ -44,7 +44,7 @@
"@types/express": "4.17.17",
"@types/node": "18.15.3",
"tsup": "6.7.0",
"typescript": "5.4.2"
"typescript": "5.1.6"
},
"tsup": {
"entry": [

View File

@@ -1,4 +1,3 @@
{
"extends": "@openstapps/tsconfig",
"exclude": ["lib", "app.js"]
"extends": "@openstapps/tsconfig"
}

12
flake.lock generated
View File

@@ -5,11 +5,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1709126324,
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
"lastModified": 1701680307,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725",
"type": "github"
},
"original": {
@@ -20,11 +20,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1709747860,
"narHash": "sha256-RT4zuBy579m+l8VyIQFOR66WXfcs4g1jntZUHjh6eoI=",
"lastModified": 1701626906,
"narHash": "sha256-ugr1QyzzwNk505ICE4VMQzonHQ9QS5W33xF2FXzFQ00=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "58ae79ea707579c40102ddf62d84b902a987c58b",
"rev": "0c6d8c783336a59f4c59d4a6daed6ab269c4b361",
"type": "github"
},
"original": {

View File

@@ -28,7 +28,6 @@
hash = "sha256-9o0nprGcJhudS1LNm+T7Vf0Dwd1RBauYKI+w1FBQ3ZM=";
};
});
nodejs = prev.nodejs_18;
})
];
config = {
@@ -50,16 +49,21 @@
devShell = pkgs.mkShell rec {
nativeBuildInputs = [androidFhs];
buildInputs = with pkgs; [
nodejs
corepack
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";

View File

@@ -5,4 +5,10 @@
# You can see what browsers were selected by your queries by running:
# npx browserslist
> 0.5% in DE and last 2 major versions and supports es6 and not dead
> 0.5%
last 2 versions
Firefox ESR
not dead
not kaios 2.5
not op_mini all
not IE 9-11

View File

@@ -1,28 +1,5 @@
# @openstapps/app
## 3.3.0
### Minor Changes
- 688bc5f2: v3.3.0 changes
### Patch Changes
- Updated dependencies [688bc5f2]
- @openstapps/api@3.2.0
- @openstapps/core@3.2.0
- @openstapps/collection-utils@3.0.0
## 3.2.0
### Patch Changes
- 689ac68b: pin alpine version to 3.18 and add healthchecks
- Updated dependencies [912ae422]
- @openstapps/core@4.0.0
- @openstapps/api@4.0.0
- @openstapps/collection-utils@3.0.0
## 3.1.2
### Patch Changes

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

@@ -50,11 +50,11 @@ the config file.
Icon font minification is done automatically, but requires you to
follow a few simple rules:
1. Use the type-safe proxy for referencing icon names in
1. Use the tagged template literal for referencing icon names in
TypeScript files and code
```ts
SCIcon.icon_name;
SCIcon`icon_name`;
```
2. When using `ion-icon` in HTML, reference either icons that went through

View File

@@ -13,25 +13,25 @@ The StApps 1.x.x (legacy app, but current app in stores) is written using Ionic
There are (`npm`) scripts defined to get the app running as quickly as possible. Those scripts (shortcuts for docker commands) are called using the syntax `npm run + <script-name>`. So we have the following commands available:
```sh
```
npm run docker:pull
```
which pulls the up-to-date image ([Dockerfile](Dockerfile)) which contains all the tools needed for building, serving and deploying the app.
```sh
```
npm run docker:enter
```
which enters the container on docker builder image, where we can run `npm install` (to install the required npm packages) and `npm build` (to build the app: convert into executable files), but also any other arbitrary commands with the tools available in the docker image.
```sh
```
npm run docker:build
```
which runs `npm install` (to install the required npm packages) and `npm build` (to build the app: convert into executable files) in the docker container which runs on the docker builder image.
```sh
```
npm run docker:serve
```
@@ -39,7 +39,7 @@ which serves the app for running it in the browser. It basically runs `ionic ser
## How to build and start the app using the default backend?
```sh
```
npm run build
npm run start
```
@@ -86,98 +86,52 @@ addToIonicDB(
You'll need to run _Chromium_ using
```sh
```shell
pnpm chromium:no-cors
```
### Help, I can't log in!
Login services will often block hosts not coming from the production
server.
server. You can circumvent this locally by using the `:virtual-host`
scripts:
#### Web
On the web you can circumvent this locally by using the `:virtual-host` scripts:
```sh
```shell
# Start the dev server on mobile.app.uni-frankfurt.de
pnpm start:virtual-host
# Run chromium with flags that redirect mobile.app.uni-frankfurt.de to localhost:8100
pnpm chromium:virtual-host
```
#### Android
On Android you will need to change the `custom_url_scheme` values
to `de.unifrankfurt.app` in the following files:
- `android/app/src/main/res/values/strings.xml`
- `src/environment/environment.ts`
Then start the app normally as you would
```sh
pnpm run:android
```
**This alone will not make auth work**, only the login flow.
If you need to test login, you have to disable live reload:
```sh
pnpm ionic capacitor run android
```
_**CAUTION:** a remote chrome debugging session can in some
cases hijack the device's ADB connection. If the connection
fails for no obvious reason, close chrome and uninstall the
app, then try again._
#### iOS
On Android you will need to change the `custom_url_scheme` value in
`src/environment/environment.ts` as well as the `CFBundleURLTypes`
in `ios/App/App/Info.plist`.
- make sure to remove any `Info.plist.orig` as capacitor might override
the modified `Info.plist` with that.
- make sure you have a valid device in XCode (Simulator or real device).
After that, run
```sh
pnpm run:ios
```
### Running the app
Install the npm packages needed for running the app (as for any other node project which uses npm):
```sh
```
npm install
```
Check the code for linter issues:
```sh
```
npm run lint
```
Automatically fix linter issues (those where autofix is possible):
```sh
```
npm run lint:fix
```
Build the app (transpile etc.):
```sh
```
npm run build
```
Open the app in the browser:
```sh
```
ionic serve
```
@@ -185,7 +139,7 @@ ionic serve
Run the app for testing on an android device (with live reload in the webview / device, when files are changed):
```sh
```
npm run build # if needed
npm run resources:android # generate needed resources (icons and splashscreens)
npm run docker:run:android # runs "ionic capacitor run android --livereload --external" on a selected device
@@ -205,7 +159,7 @@ Besides that, it is possible to monitor processes (and so the processes related
Build the (debug) app for testing on an android device (creates an APK file in the android's build outputs path):
```sh
```
npm run docker:build:android
```
@@ -215,13 +169,13 @@ The mentioned `docker:*:android` npm commands are executed in a docker container
Execute unit tests:
```sh
```
npm test
```
Execute e2e tests:
```sh
```
npm run e2e
```

View File

@@ -9,7 +9,6 @@ android {
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
dependencies {
implementation project(':capacitor-community-screen-brightness')
implementation project(':capacitor-app')
implementation project(':capacitor-browser')
implementation project(':capacitor-clipboard')
@@ -22,7 +21,6 @@ dependencies {
implementation project(':capacitor-local-notifications')
implementation project(':capacitor-network')
implementation project(':capacitor-preferences')
implementation project(':capacitor-screen-orientation')
implementation project(':capacitor-share')
implementation project(':capacitor-splash-screen')
implementation project(':transistorsoft-capacitor-background-fetch')

View File

@@ -1,8 +1,4 @@
[
{
"pkg": "@capacitor-community/screen-brightness",
"classpath": "com.elylucas.capscreenbrightness.ScreenBrightnessPlugin"
},
{
"pkg": "@capacitor/app",
"classpath": "com.capacitorjs.plugins.app.AppPlugin"
@@ -51,10 +47,6 @@
"pkg": "@capacitor/preferences",
"classpath": "com.capacitorjs.plugins.preferences.PreferencesPlugin"
},
{
"pkg": "@capacitor/screen-orientation",
"classpath": "com.capacitorjs.plugins.screenorientation.ScreenOrientationPlugin"
},
{
"pkg": "@capacitor/share",
"classpath": "com.capacitorjs.plugins.share.SharePlugin"

View File

@@ -1,57 +1,51 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
include ':capacitor-android'
project(':capacitor-android').projectDir = new File('../../../node_modules/.pnpm/@capacitor+android@5.7.3_@capacitor+core@5.7.3/node_modules/@capacitor/android/capacitor')
include ':capacitor-community-screen-brightness'
project(':capacitor-community-screen-brightness').projectDir = new File('../../../node_modules/.pnpm/@capacitor-community+screen-brightness@6.0.0_@capacitor+core@5.7.3/node_modules/@capacitor-community/screen-brightness/android')
project(':capacitor-android').projectDir = new File('../../../node_modules/.pnpm/@capacitor+android@5.5.0_@capacitor+core@5.5.0/node_modules/@capacitor/android/capacitor')
include ':capacitor-app'
project(':capacitor-app').projectDir = new File('../../../node_modules/.pnpm/@capacitor+app@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/app/android')
project(':capacitor-app').projectDir = new File('../../../node_modules/.pnpm/@capacitor+app@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/app/android')
include ':capacitor-browser'
project(':capacitor-browser').projectDir = new File('../../../node_modules/.pnpm/@capacitor+browser@5.2.0_@capacitor+core@5.7.3/node_modules/@capacitor/browser/android')
project(':capacitor-browser').projectDir = new File('../../../node_modules/.pnpm/@capacitor+browser@5.1.0_@capacitor+core@5.5.0/node_modules/@capacitor/browser/android')
include ':capacitor-clipboard'
project(':capacitor-clipboard').projectDir = new File('../../../node_modules/.pnpm/@capacitor+clipboard@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/clipboard/android')
project(':capacitor-clipboard').projectDir = new File('../../../node_modules/.pnpm/@capacitor+clipboard@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/clipboard/android')
include ':capacitor-device'
project(':capacitor-device').projectDir = new File('../../../node_modules/.pnpm/@capacitor+device@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/device/android')
project(':capacitor-device').projectDir = new File('../../../node_modules/.pnpm/@capacitor+device@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/device/android')
include ':capacitor-dialog'
project(':capacitor-dialog').projectDir = new File('../../../node_modules/.pnpm/@capacitor+dialog@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/dialog/android')
project(':capacitor-dialog').projectDir = new File('../../../node_modules/.pnpm/@capacitor+dialog@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/dialog/android')
include ':capacitor-filesystem'
project(':capacitor-filesystem').projectDir = new File('../../../node_modules/.pnpm/@capacitor+filesystem@5.2.1_@capacitor+core@5.7.3/node_modules/@capacitor/filesystem/android')
project(':capacitor-filesystem').projectDir = new File('../../../node_modules/.pnpm/@capacitor+filesystem@5.1.4_@capacitor+core@5.5.0/node_modules/@capacitor/filesystem/android')
include ':capacitor-geolocation'
project(':capacitor-geolocation').projectDir = new File('../../../node_modules/.pnpm/@capacitor+geolocation@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/geolocation/android')
project(':capacitor-geolocation').projectDir = new File('../../../node_modules/.pnpm/@capacitor+geolocation@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/geolocation/android')
include ':capacitor-haptics'
project(':capacitor-haptics').projectDir = new File('../../../node_modules/.pnpm/@capacitor+haptics@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/haptics/android')
project(':capacitor-haptics').projectDir = new File('../../../node_modules/.pnpm/@capacitor+haptics@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/haptics/android')
include ':capacitor-keyboard'
project(':capacitor-keyboard').projectDir = new File('../../../node_modules/.pnpm/@capacitor+keyboard@5.0.8_@capacitor+core@5.7.3/node_modules/@capacitor/keyboard/android')
project(':capacitor-keyboard').projectDir = new File('../../../node_modules/.pnpm/@capacitor+keyboard@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/keyboard/android')
include ':capacitor-local-notifications'
project(':capacitor-local-notifications').projectDir = new File('../../../node_modules/.pnpm/@capacitor+local-notifications@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/local-notifications/android')
project(':capacitor-local-notifications').projectDir = new File('../../../node_modules/.pnpm/@capacitor+local-notifications@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/local-notifications/android')
include ':capacitor-network'
project(':capacitor-network').projectDir = new File('../../../node_modules/.pnpm/@capacitor+network@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/network/android')
project(':capacitor-network').projectDir = new File('../../../node_modules/.pnpm/@capacitor+network@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/network/android')
include ':capacitor-preferences'
project(':capacitor-preferences').projectDir = new File('../../../node_modules/.pnpm/@capacitor+preferences@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/preferences/android')
include ':capacitor-screen-orientation'
project(':capacitor-screen-orientation').projectDir = new File('../../../node_modules/.pnpm/@capacitor+screen-orientation@6.0.0_@capacitor+core@5.7.3/node_modules/@capacitor/screen-orientation/android')
project(':capacitor-preferences').projectDir = new File('../../../node_modules/.pnpm/@capacitor+preferences@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/preferences/android')
include ':capacitor-share'
project(':capacitor-share').projectDir = new File('../../../node_modules/.pnpm/@capacitor+share@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/share/android')
project(':capacitor-share').projectDir = new File('../../../node_modules/.pnpm/@capacitor+share@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/share/android')
include ':capacitor-splash-screen'
project(':capacitor-splash-screen').projectDir = new File('../../../node_modules/.pnpm/@capacitor+splash-screen@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/splash-screen/android')
project(':capacitor-splash-screen').projectDir = new File('../../../node_modules/.pnpm/@capacitor+splash-screen@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/splash-screen/android')
include ':transistorsoft-capacitor-background-fetch'
project(':transistorsoft-capacitor-background-fetch').projectDir = new File('../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@5.2.0_@capacitor+core@5.7.3/node_modules/@transistorsoft/capacitor-background-fetch/android')
project(':transistorsoft-capacitor-background-fetch').projectDir = new File('../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@5.1.1_@capacitor+core@5.5.0/node_modules/@transistorsoft/capacitor-background-fetch/android')
include ':capacitor-secure-storage-plugin'
project(':capacitor-secure-storage-plugin').projectDir = new File('../../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.7.3/node_modules/capacitor-secure-storage-plugin/android')
project(':capacitor-secure-storage-plugin').projectDir = new File('../../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.5.0/node_modules/capacitor-secure-storage-plugin/android')

View File

@@ -21,13 +21,10 @@
"allowedCommonJsDependencies": [
"moment",
"opening_hours",
"localforage",
"i18next",
"semver",
"suncalc",
"guid-typescript",
"fast-deep-equal",
"maplibre-gl"
"leaflet",
"leaflet.markercluster",
"localforge",
"guid-typescript"
],
"aot": true,
"assets": [
@@ -35,13 +32,24 @@
"glob": "**/*",
"input": "src/assets",
"output": "assets"
},
{
"glob": "**/*",
"input": "./node_modules/leaflet/dist/images",
"output": "assets/"
}
],
"styles": [
{
"input": "src/theme/variables.scss",
"inject": true
},
{
"input": "src/global.scss",
"inject": true
}
},
"./node_modules/leaflet/dist/leaflet.css",
"./node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css"
]
},
"configurations": {
@@ -89,20 +97,20 @@
"serve": {
"builder": "@angular-devkit/build-angular:dev-server",
"options": {
"buildTarget": "app:build"
"browserTarget": "app:build"
},
"configurations": {
"production": {
"buildTarget": "app:build:production"
"browserTarget": "app:build:production"
},
"development": {
"buildTarget": "app:build:development"
"browserTarget": "app:build:development"
},
"ci": {
"buildTarget": "app:build"
"browserTarget": "app:build"
},
"fake": {
"buildTarget": "app:build:fake"
"browserTarget": "app:build:fake"
}
},
"defaultConfiguration": "development"
@@ -124,6 +132,11 @@
"glob": "**/*",
"input": "src/assets",
"output": "/assets"
},
{
"glob": "**/*",
"input": "./node_modules/leaflet/dist/images",
"output": "assets/"
}
]
}

View File

@@ -173,7 +173,7 @@ describe('dashboard', async function () {
cy.visit('/overview');
cy.get('ion-searchbar').click({scrollBehavior: 'center'});
cy.url().should('include', '/search');
cy.url().should('eq', Cypress.config().baseUrl + '/search');
cy.get('ion-searchbar').should('not.have.value');
cy.get('ion-searchbar input.searchbar-input').should('have.focus');

View File

@@ -12,9 +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 type {IconConfig} from './scripts/icon-config';
/** @type {import('./scripts/icon-config').IconConfig} */
const config = {
const config: IconConfig = {
inputPath: 'node_modules/material-symbols/material-symbols-rounded.woff2',
outputPath: 'src/assets/icons.min.woff2',
htmlGlob: 'src/**/*.html',
@@ -38,6 +38,10 @@ const config = {
'work',
],
},
codePoints: {
ios_share: 'e6b8',
fact_check: 'f0c5',
},
};
export default config;

View File

@@ -1,4 +1,4 @@
require_relative '../../../../node_modules/.pnpm/@capacitor+ios@5.7.3_@capacitor+core@5.7.3/node_modules/@capacitor/ios/scripts/pods_helpers'
require_relative '../../../../node_modules/.pnpm/@capacitor+ios@5.5.0_@capacitor+core@5.5.0/node_modules/@capacitor/ios/scripts/pods_helpers'
platform :ios, '13.0'
use_frameworks!
@@ -9,26 +9,24 @@ use_frameworks!
install! 'cocoapods', :disable_input_output_paths => true
def capacitor_pods
pod 'Capacitor', :path => '../../../../node_modules/.pnpm/@capacitor+ios@5.7.3_@capacitor+core@5.7.3/node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../../../node_modules/.pnpm/@capacitor+ios@5.7.3_@capacitor+core@5.7.3/node_modules/@capacitor/ios'
pod 'CapacitorCommunityScreenBrightness', :path => '../../../../node_modules/.pnpm/@capacitor-community+screen-brightness@6.0.0_@capacitor+core@5.7.3/node_modules/@capacitor-community/screen-brightness'
pod 'CapacitorApp', :path => '../../../../node_modules/.pnpm/@capacitor+app@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/app'
pod 'CapacitorBrowser', :path => '../../../../node_modules/.pnpm/@capacitor+browser@5.2.0_@capacitor+core@5.7.3/node_modules/@capacitor/browser'
pod 'CapacitorClipboard', :path => '../../../../node_modules/.pnpm/@capacitor+clipboard@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/clipboard'
pod 'CapacitorDevice', :path => '../../../../node_modules/.pnpm/@capacitor+device@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/device'
pod 'CapacitorDialog', :path => '../../../../node_modules/.pnpm/@capacitor+dialog@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/dialog'
pod 'CapacitorFilesystem', :path => '../../../../node_modules/.pnpm/@capacitor+filesystem@5.2.1_@capacitor+core@5.7.3/node_modules/@capacitor/filesystem'
pod 'CapacitorGeolocation', :path => '../../../../node_modules/.pnpm/@capacitor+geolocation@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/geolocation'
pod 'CapacitorHaptics', :path => '../../../../node_modules/.pnpm/@capacitor+haptics@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/haptics'
pod 'CapacitorKeyboard', :path => '../../../../node_modules/.pnpm/@capacitor+keyboard@5.0.8_@capacitor+core@5.7.3/node_modules/@capacitor/keyboard'
pod 'CapacitorLocalNotifications', :path => '../../../../node_modules/.pnpm/@capacitor+local-notifications@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/local-notifications'
pod 'CapacitorNetwork', :path => '../../../../node_modules/.pnpm/@capacitor+network@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/network'
pod 'CapacitorPreferences', :path => '../../../../node_modules/.pnpm/@capacitor+preferences@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/preferences'
pod 'CapacitorScreenOrientation', :path => '../../../../node_modules/.pnpm/@capacitor+screen-orientation@6.0.0_@capacitor+core@5.7.3/node_modules/@capacitor/screen-orientation'
pod 'CapacitorShare', :path => '../../../../node_modules/.pnpm/@capacitor+share@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/share'
pod 'CapacitorSplashScreen', :path => '../../../../node_modules/.pnpm/@capacitor+splash-screen@5.0.7_@capacitor+core@5.7.3/node_modules/@capacitor/splash-screen'
pod 'TransistorsoftCapacitorBackgroundFetch', :path => '../../../../node_modules/.pnpm/@transistorsoft+capacitor-background-fetch@5.2.0_@capacitor+core@5.7.3/node_modules/@transistorsoft/capacitor-background-fetch'
pod 'CapacitorSecureStoragePlugin', :path => '../../../../node_modules/.pnpm/capacitor-secure-storage-plugin@0.9.0_@capacitor+core@5.7.3/node_modules/capacitor-secure-storage-plugin'
pod 'Capacitor', :path => '../../../../node_modules/.pnpm/@capacitor+ios@5.5.0_@capacitor+core@5.5.0/node_modules/@capacitor/ios'
pod 'CapacitorCordova', :path => '../../../../node_modules/.pnpm/@capacitor+ios@5.5.0_@capacitor+core@5.5.0/node_modules/@capacitor/ios'
pod 'CapacitorApp', :path => '../../../../node_modules/.pnpm/@capacitor+app@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/app'
pod 'CapacitorBrowser', :path => '../../../../node_modules/.pnpm/@capacitor+browser@5.1.0_@capacitor+core@5.5.0/node_modules/@capacitor/browser'
pod 'CapacitorClipboard', :path => '../../../../node_modules/.pnpm/@capacitor+clipboard@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/clipboard'
pod 'CapacitorDevice', :path => '../../../../node_modules/.pnpm/@capacitor+device@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/device'
pod 'CapacitorDialog', :path => '../../../../node_modules/.pnpm/@capacitor+dialog@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/dialog'
pod 'CapacitorFilesystem', :path => '../../../../node_modules/.pnpm/@capacitor+filesystem@5.1.4_@capacitor+core@5.5.0/node_modules/@capacitor/filesystem'
pod 'CapacitorGeolocation', :path => '../../../../node_modules/.pnpm/@capacitor+geolocation@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/geolocation'
pod 'CapacitorHaptics', :path => '../../../../node_modules/.pnpm/@capacitor+haptics@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/haptics'
pod 'CapacitorKeyboard', :path => '../../../../node_modules/.pnpm/@capacitor+keyboard@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/keyboard'
pod 'CapacitorLocalNotifications', :path => '../../../../node_modules/.pnpm/@capacitor+local-notifications@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/local-notifications'
pod 'CapacitorNetwork', :path => '../../../../node_modules/.pnpm/@capacitor+network@5.0.6_@capacitor+core@5.5.0/node_modules/@capacitor/network'
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@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

@@ -1,7 +1,7 @@
{
"name": "@openstapps/app",
"description": "The generic app tailored to fulfill needs of German universities, written using Ionic Framework.",
"version": "3.3.0",
"version": "3.1.2",
"private": true,
"license": "GPL-3.0-only",
"author": "Karl-Philipp Wulfert <krlwlfrt@gmail.com>",
@@ -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",
@@ -35,10 +35,10 @@
"e2e": "ng e2e",
"format": "prettier . -c",
"format:fix": "prettier --write .",
"licenses": "license-checker --json > src/assets/about/licenses.json && node ./scripts/accumulate-licenses.mjs && git add src/assets/about/licenses.json",
"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\"",
@@ -52,138 +52,140 @@
"test:integration": "sh integration-test.sh"
},
"dependencies": {
"@angular/animations": "17.3.0",
"@angular/cdk": "17.3.0",
"@angular/common": "17.3.0",
"@angular/core": "17.3.0",
"@angular/forms": "17.3.0",
"@angular/platform-browser": "17.3.0",
"@angular/router": "17.3.0",
"@awesome-cordova-plugins/calendar": "6.6.0",
"@awesome-cordova-plugins/core": "6.6.0",
"@capacitor-community/screen-brightness": "6.0.0",
"@capacitor/app": "5.0.7",
"@capacitor/browser": "5.2.0",
"@capacitor/clipboard": "5.0.7",
"@capacitor/core": "5.7.3",
"@capacitor/device": "5.0.7",
"@capacitor/dialog": "5.0.7",
"@capacitor/filesystem": "5.2.1",
"@capacitor/geolocation": "5.0.7",
"@capacitor/haptics": "5.0.7",
"@capacitor/keyboard": "5.0.8",
"@capacitor/local-notifications": "5.0.7",
"@capacitor/network": "5.0.7",
"@capacitor/preferences": "5.0.7",
"@capacitor/screen-orientation": "6.0.0",
"@capacitor/share": "5.0.7",
"@capacitor/splash-screen": "5.0.7",
"@angular/animations": "16.1.4",
"@angular/cdk": "16.1.4",
"@angular/common": "16.1.4",
"@angular/core": "16.1.4",
"@angular/forms": "16.1.4",
"@angular/platform-browser": "16.1.4",
"@angular/router": "16.1.4",
"@asymmetrik/ngx-leaflet": "16.0.1",
"@asymmetrik/ngx-leaflet-markercluster": "16.0.0",
"@awesome-cordova-plugins/calendar": "5.45.0",
"@awesome-cordova-plugins/core": "5.45.0",
"@capacitor/app": "5.0.6",
"@capacitor/browser": "5.1.0",
"@capacitor/clipboard": "5.0.6",
"@capacitor/core": "5.5.0",
"@capacitor/device": "5.0.6",
"@capacitor/dialog": "5.0.6",
"@capacitor/filesystem": "5.1.4",
"@capacitor/geolocation": "5.0.6",
"@capacitor/haptics": "5.0.6",
"@capacitor/keyboard": "5.0.6",
"@capacitor/local-notifications": "5.0.6",
"@capacitor/network": "5.0.6",
"@capacitor/preferences": "5.0.6",
"@capacitor/share": "5.0.6",
"@capacitor/splash-screen": "5.0.6",
"@ionic-native/core": "5.36.0",
"@ionic/angular": "7.8.0",
"@ionic/angular": "7.1.3",
"@ionic/storage-angular": "4.0.0",
"@maplibre/ngx-maplibre-gl": "17.4.1",
"@ngx-translate/core": "15.0.0",
"@ngx-translate/http-loader": "8.0.0",
"@openid/appauth": "1.3.1",
"@openstapps/api": "workspace:*",
"@openstapps/collection-utils": "workspace:*",
"@openstapps/core": "workspace:*",
"@transistorsoft/capacitor-background-fetch": "5.2.0",
"@types/dom-view-transitions": "1.0.4",
"@transistorsoft/capacitor-background-fetch": "5.1.1",
"@types/dom-view-transitions": "1.0.1",
"capacitor-secure-storage-plugin": "0.9.0",
"cordova-plugin-calendar": "5.1.6",
"date-fns": "3.6.0",
"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",
"jsonpath-plus": "6.0.1",
"maplibre-gl": "4.0.2",
"material-symbols": "0.17.1",
"moment": "2.30.1",
"ngx-date-fns": "11.0.0",
"leaflet": "1.9.3",
"leaflet.markercluster": "1.5.3",
"material-symbols": "0.10.0",
"moment": "2.29.4",
"ngx-date-fns": "10.0.1",
"ngx-logger": "5.0.12",
"ngx-markdown": "17.1.1",
"ngx-markdown": "16.0.0",
"ngx-moment": "6.0.2",
"opening_hours": "3.8.0",
"pmtiles": "3.0.3",
"rxjs": "7.8.1",
"semver": "7.6.0",
"semver": "7.5.4",
"swiper": "8.4.5",
"tslib": "2.6.2",
"zone.js": "0.14.4"
"tslib": "2.4.1",
"zone.js": "0.13.1"
},
"devDependencies": {
"@angular-devkit/architect": "0.1703.0",
"@angular-devkit/build-angular": "17.3.0",
"@angular-devkit/core": "17.3.0",
"@angular-devkit/schematics": "17.3.0",
"@angular-eslint/builder": "17.3.0",
"@angular-eslint/eslint-plugin": "17.3.0",
"@angular-eslint/eslint-plugin-template": "17.3.0",
"@angular-eslint/schematics": "17.3.0",
"@angular-eslint/template-parser": "17.3.0",
"@angular/cli": "17.3.0",
"@angular/compiler": "17.3.0",
"@angular/compiler-cli": "17.3.0",
"@angular/language-server": "17.3.0",
"@angular/language-service": "17.3.0",
"@angular/platform-browser-dynamic": "17.3.0",
"@capacitor/android": "5.7.3",
"@capacitor/assets": "3.0.4",
"@capacitor/cli": "5.7.3",
"@capacitor/ios": "5.7.3",
"@compodoc/compodoc": "1.1.23",
"@cypress/schematic": "2.5.1",
"@ionic/angular-toolkit": "11.0.1",
"@ionic/cli": "7.2.0",
"@angular-devkit/architect": "0.1601.4",
"@angular-devkit/build-angular": "16.1.4",
"@angular-devkit/core": "16.1.4",
"@angular-devkit/schematics": "16.1.4",
"@angular-eslint/builder": "16.1.0",
"@angular-eslint/eslint-plugin": "16.1.0",
"@angular-eslint/eslint-plugin-template": "16.1.0",
"@angular-eslint/schematics": "16.1.0",
"@angular-eslint/template-parser": "16.1.0",
"@angular/cli": "16.1.4",
"@angular/compiler": "16.1.4",
"@angular/compiler-cli": "16.1.4",
"@angular/language-service": "16.1.4",
"@angular/platform-browser-dynamic": "16.1.4",
"@capacitor/android": "5.5.0",
"@capacitor/assets": "3.0.1",
"@capacitor/cli": "5.5.0",
"@capacitor/ios": "5.5.0",
"@compodoc/compodoc": "1.1.19",
"@cypress/schematic": "1.7.0",
"@ionic/angular-toolkit": "10.0.0",
"@ionic/cli": "7.1.1",
"@openstapps/prettier-config": "workspace:*",
"@openstapps/tsconfig": "workspace:*",
"@types/fontkit": "2.0.7",
"@types/fontkit": "1.8.0",
"@types/geojson": "1.0.6",
"@types/glob": "8.1.0",
"@types/jasmine": "5.1.4",
"@types/jasminewd2": "2.0.13",
"@types/glob": "8.0.1",
"@types/jasmine": "4.3.1",
"@types/jasminewd2": "2.0.10",
"@types/jsonpath": "0.2.0",
"@types/karma": "6.3.8",
"@types/karma-coverage": "2.0.3",
"@types/karma-jasmine": "4.0.5",
"@types/karma": "6.3.4",
"@types/karma-coverage": "2.0.1",
"@types/karma-jasmine": "4.0.2",
"@types/leaflet": "1.9.0",
"@types/leaflet.markercluster": "1.5.1",
"@types/node": "18.15.3",
"@types/semver": "7.5.8",
"@typescript-eslint/eslint-plugin": "7.2.0",
"@typescript-eslint/parser": "7.2.0",
"@types/semver": "7.5.6",
"@typescript-eslint/eslint-plugin": "5.60.1",
"@typescript-eslint/parser": "5.60.1",
"cordova-res": "0.15.4",
"cypress": "13.7.0",
"eslint": "8.57.0",
"eslint-plugin-jsdoc": "48.2.1",
"eslint-plugin-prettier": "5.1.3",
"eslint-plugin-unicorn": "51.0.1",
"cypress": "13.2.0",
"eslint": "8.43.0",
"eslint-plugin-jsdoc": "46.4.2",
"eslint-plugin-prettier": "4.2.1",
"eslint-plugin-unicorn": "47.0.0",
"fast-deep-equal": "3.1.3",
"fontkit": "2.0.2",
"glob": "10.3.10",
"glob": "10.2.7",
"http-server": "14.1.1",
"is-docker": "2.2.1",
"jasmine-core": "5.1.2",
"jasmine-core": "5.0.1",
"jasmine-spec-reporter": "7.0.0",
"jetifier": "2.0.0",
"junit-report-merger": "6.0.3",
"karma": "6.4.3",
"junit-report-merger": "6.0.2",
"karma": "6.4.2",
"karma-chrome-launcher": "3.2.0",
"karma-coverage": "2.2.1",
"karma-jasmine": "5.1.0",
"karma-junit-reporter": "2.0.1",
"karma-mocha-reporter": "2.2.5",
"license-checker": "25.0.1",
"stylelint": "16.3.1",
"stylelint-config-clean-order": "5.4.1",
"stylelint": "15.10.1",
"stylelint-config-clean-order": "5.0.1",
"stylelint-config-prettier-scss": "1.0.0",
"stylelint-config-recommended-scss": "14.0.0",
"stylelint-config-standard-scss": "13.0.0",
"stylelint-config-recommended-scss": "12.0.0",
"stylelint-config-standard-scss": "10.0.0",
"surge": "0.23.1",
"ts-node": "10.9.2",
"typescript": "5.4.2",
"webpack-bundle-analyzer": "4.10.1"
"ts-node": "10.9.1",
"typescript": "5.1.6",
"webpack-bundle-analyzer": "4.7.0"
},
"prettier": "@openstapps/prettier-config",
"cordova": {
"plugins": {},
"platforms": [

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
@@ -26,6 +25,7 @@ const config = {
},
},
],
ignorePath: ['.prettierignore', '../../.gitignore'],
};
export default config;

View File

@@ -12,34 +12,33 @@
* 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 {readFileSync, writeFileSync} from 'fs';
import {omit, pickBy} from '@openstapps/collection-utils';
import fs from 'fs';
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) {
const packageJson = JSON.parse(readFileSync('./package.json').toString());
function accumulateFile(path: string, additionalLicensesPath: string) {
const packageJson = JSON.parse(fs.readFileSync('./package.json').toString());
const dependencies = packageJson.dependencies;
console.log(`Accumulating licenses from ${path}`);
writeFileSync(
fs.writeFileSync(
path,
JSON.stringify(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Object.entries({
...pickBy(JSON.parse(readFileSync(path).toString()), (_, key) => {
const parts = /** @type {string} */ (key).split('@');
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];
}),
...JSON.parse(readFileSync(additionalLicensesPath).toString()),
...JSON.parse(fs.readFileSync(additionalLicensesPath).toString()),
})
.map(([key, value]) => ({
licenseText: value.licenseFile && readFileSync(value.licenseFile, 'utf8'),
licenseText: value.licenseFile && fs.readFileSync(value.licenseFile, 'utf8'),
name: key,
...omit(value, 'licenseFile', 'path'),
}))

View File

@@ -12,20 +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 {fetchCodePointMap} from './get-code-points.mjs';
import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons';
const commandName = '"npm run minify-icons"';
const originalFont = fontkit.openSync(config.inputPath);
if (!existsSync(config.outputPath)) {
console.error(`Minified font not found. Run ${commandName} first.`);
process.exit(-1);
}
/** @type {import('fontkit').Font} */
const modifiedFont = openSync(config.outputPath);
const modifiedFont = fontkit.openSync(config.outputPath);
let success = true;
@@ -50,16 +48,25 @@ async function checkAll() {
}
/**
* @param {Record<string, string[]>} icons
*
*/
async function check(icons) {
const codePoints = await fetchCodePointMap();
for (const icon of Object.values(icons).flat()) {
const codePoint = codePoints.get(icon);
if (!codePoint) throw new Error(`"${icon}" is not a valid icon`);
if (!modifiedFont.getGlyph(Number.parseInt(codePoint, 16))) {
throw new Error(`"${icon}" (code point ${codePoint}) is missing`);
function check(icons: Record<string, string[]>) {
for (const [purpose, iconSet] of Object.entries(icons)) {
for (const icon of iconSet) {
if (!hasIcon(originalFont, icon)) {
success = false;
console.error(`${purpose}: ${icon} does not exist. Typo?`);
} else if (!hasIcon(modifiedFont, icon)) {
success = false;
console.error(`${purpose}: ${icon} not found in minified font. Run ${commandName} to regenerate it.`);
}
}
}
}
/**
*
*/
function hasIcon(font: Font, icon: string) {
return font.layout(icon).glyphs.some(it => it.isLigature);
}

View File

@@ -14,39 +14,34 @@
*/
import {glob} from 'glob';
import {readFileSync} from 'fs';
import {
matchPropertyAccess,
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,
readFileSync(file, 'utf8')
(readFileSync(file, 'utf8')
.match(matchTagProperties('ion-icon'))
?.flatMap(match => {
return match.match(matchPropertyContent(['name', 'md', 'ios']));
})
.filter(it => !!it) || [],
.filter(it => !!it) as string[]) || [],
])
.filter(([, values]) => values && values.length > 0),
.filter(([, values]) => values.length > 0),
);
}
/**
* @returns {Promise<Record<string, string[]>>}
*
*/
export async function getUsedIconsTS(pattern = 'src/**/*.ts') {
const regex = matchPropertyAccess('SCIcon');
export async function getUsedIconsTS(pattern = 'src/**/*.ts'): Promise<Record<string, string[]>> {
return Object.fromEntries(
(await glob(pattern))
.map(file => [file, readFileSync(file, 'utf8').match(regex) || []])
.filter(([, values]) => values && values.length > 0),
.map(file => [file, readFileSync(file, 'utf8').match(/(?<=Icon`)[\w-]+(?=`)/g) || []])
.filter(([, values]) => values.length > 0),
);
}

View File

@@ -1,23 +0,0 @@
const url =
'https://raw.githubusercontent.com/google/material-design-icons/master/' +
'variablefont/MaterialSymbolsRounded%5BFILL%2CGRAD%2Copsz%2Cwght%5D.codepoints';
export async function fetchCodePointMap() {
const icons = await fetch(url)
.then(it => it.text())
.then(it => new Map(it.split('\n').map(it => /** @type {[string, string]} */ (it.split(' ')))));
if (icons.size < 100) throw new Error(`Code point map is very small, is the URL incorrect? ${url}`);
return icons;
}
/**
* @param {string[]} icons
*/
export async function getCodePoints(icons) {
const codePoints = await fetchCodePointMap();
return icons.map(icon => {
const code = codePoints.get(icon);
if (!code) throw new Error(`Code point for icon ${icon} not found`);
return code;
});
}

View File

@@ -1,8 +0,0 @@
import {networkInterfaces} from 'os';
console.log(
Object.entries(networkInterfaces())
.map(([, info]) => info)
.flat()
.find(info => info && !info.internal && info.family === 'IPv4')?.address,
);

View File

@@ -19,4 +19,5 @@ export interface IconConfig {
inputPath: string;
outputPath: string;
additionalIcons?: {[purpose: string]: string[]};
codePoints?: {[name: string]: string};
}

View File

@@ -14,17 +14,16 @@
*/
/* 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 {getCodePoints} from './get-code-points.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}`);
@@ -45,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,7 +56,35 @@ async function minifyIconFont() {
}
}
const glyphs = ['5f-7a', '30-39', ...(await getCodePoints([...icons]))].sort();
console.log('Icons used:', [...icons.values()].sort());
const font = fontkit.openSync(config.inputPath);
const glyphs: string[] = ['5f-7a', '30-39'];
for (const icon of icons) {
const iconGlyphs = font.layout(icon).glyphs;
if (iconGlyphs.length === 0) {
console.error(`${icon} not found in font. Typo?`);
process.exit(-1);
}
const codePoints = iconGlyphs
.flatMap(it => font.stringsForGlyph(it.id))
.flatMap(it => [...it])
.map(it => it.codePointAt(0)!.toString(16));
if (codePoints.length === 0) {
if (config.codePoints?.[icon]) {
glyphs.push(config.codePoints[icon]);
} else {
console.log();
console.error(`${icon} code point could not be determined. Add it to config.codePoints.`);
process.exit(-1);
}
}
glyphs.push(...codePoints);
}
glyphs.sort();
console.log(
await run([
@@ -88,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

@@ -21,7 +21,7 @@ import {
SCThingOriginType,
SCThingType,
} from '@openstapps/core';
import {CORE_VERSION} from '@openstapps/core';
import packageInfo from '@openstapps/core/package.json';
import {Polygon} from 'geojson';
// provides sample aggregations to be used in tests or backendless development
@@ -195,7 +195,7 @@ export const sampleIndexResponse: SCIndexResponse = {
},
auth: {},
backend: {
SCVersion: CORE_VERSION,
SCVersion: packageInfo.version,
externalRequestTimeout: 5000,
hiddenTypes: [SCThingType.DateSeries, SCThingType.Diff, SCThingType.Floor],
mappingIgnoredTags: [],

View File

@@ -14,7 +14,7 @@
*/
import {AnimationBuilder, AnimationController} from '@ionic/angular';
import {AnimationOptions} from '@ionic/angular/common/providers/nav-controller';
import {AnimationOptions} from '@ionic/angular/providers/nav-controller';
import {iosDuration, iosEasing, mdDuration, mdEasing} from './easings';
/**

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,7 +25,6 @@ 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';
@@ -43,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';
@@ -69,8 +67,6 @@ import {setDefaultOptions} from 'date-fns';
import {DateFnsConfigurationService} from 'ngx-date-fns';
import {Capacitor} from '@capacitor/core';
import {SplashScreen} from '@capacitor/splash-screen';
import maplibregl from 'maplibre-gl';
import {Protocol} from 'pmtiles';
registerLocaleData(localeDe);
@@ -92,29 +88,25 @@ export function initializerFactory(
) {
return async () => {
try {
maplibregl.addProtocol('pmtiles', new Protocol().tile);
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) {
@@ -156,7 +148,9 @@ export function createTranslateLoader(http: HttpClient) {
DashboardModule,
DataModule,
HebisModule,
IonicModule.forRoot(),
IonicModule.forRoot({
animated: 'Cypress' in window ? false : undefined,
}),
IonIconModule,
JobModule,
FavoritesModule,

View File

@@ -1,17 +1,17 @@
<!--
~ Copyright (C) 2023 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/>.
-->
~ Copyright (C) 2023 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/>.
-->
<ion-header>
<ion-toolbar color="primary" mode="ios">
@@ -24,24 +24,28 @@
</ion-header>
<ion-content parallax>
<div class="licenses-content">
@for (license of licenses; track license) {
<ion-card [href]="license.url || license.repository" rel="external" target="_blank">
<ion-card-header>
<ion-card-title>
{{ license.name }}
<ion-icon [size]="16" [weight]="300" class="supertext-icon" name="open_in_browser"></ion-icon>
</ion-card-title>
@if (license.authors || license.publisher) {
<ion-card-subtitle> {{ license.authors || license.publisher }} </ion-card-subtitle>
}
</ion-card-header>
<ion-card-content>
<ion-chip (click)="$event.preventDefault(); viewLicense(license)">
<ion-icon name="copyright"></ion-icon>
<ion-label>{{ license.licenses }} License</ion-label>
</ion-chip>
</ion-card-content>
</ion-card>
}
<ion-card
*ngFor="let license of licenses"
[href]="license.url || license.repository"
rel="external"
target="_blank"
>
<ion-card-header>
<ion-card-title>
{{ license.name }}
<ion-icon [size]="16" [weight]="300" class="supertext-icon" name="open_in_browser"></ion-icon>
</ion-card-title>
<ion-card-subtitle *ngIf="license.authors || license.publisher">
{{ license.authors || license.publisher }}
</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<ion-chip (click)="$event.preventDefault(); viewLicense(license)">
<ion-icon name="copyright"></ion-icon>
<ion-label>{{ license.licenses }} License</ion-label>
</ion-chip>
</ion-card-content>
</ion-card>
</div>
</ion-content>

View File

@@ -12,6 +12,24 @@
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
ion-content > div {
height: 100%;
}
cdk-virtual-scroll-viewport {
width: 100%;
height: 100%;
}
:host ::ng-deep {
.cdk-virtual-scroll-content-wrapper {
width: 100%;
}
}
.virtual-scroll-expander {
clear: both;
}
.supertext-icon {
height: 14px;

View File

@@ -1,56 +1,41 @@
<!--
~ Copyright (C) 2023 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/>.
-->
~ Copyright (C) 2023 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/>.
-->
@if (content.type === 'markdown') {
<markdown [data]="'value' | translateSimple: content"></markdown>
}
@if (content.type === 'section') {
<div>
@if (content.card) {
<ion-card>
<ion-card-header>
<ion-card-title>{{ 'title' | translateSimple: content }}</ion-card-title>
</ion-card-header>
<ion-card-content>
<about-page-content [content]="content.content"></about-page-content>
</ion-card-content>
</ion-card>
} @else {
<h2>{{ 'title' | translateSimple: content }}</h2>
<markdown *ngIf="content.type === 'markdown'" [data]="'value' | translateSimple : content"></markdown>
<div *ngIf="content.type ==='section'">
<ion-card *ngIf="content.card; else noCard">
<ion-card-header>
<ion-card-title>{{ 'title' | translateSimple : content }}</ion-card-title>
</ion-card-header>
<ion-card-content>
<about-page-content [content]="content.content"></about-page-content>
}
</div>
}
@if (content.type === 'table') {
<ion-grid>
@for (row of content.rows; track row) {
<ion-row>
@for (col of row; track col) {
<ion-col>
<about-page-content [content]="col"></about-page-content>
</ion-col>
}
</ion-row>
}
</ion-grid>
}
@if (content.type === 'router link') {
<ion-item [routerLink]="content.link">
@if (content.icon) {
<ion-icon [name]="$any(content.icon)" slot="start"></ion-icon>
}
<ion-label>{{ 'title' | translateSimple: content }}</ion-label>
</ion-item>
}
</ion-card-content>
</ion-card>
<ng-template #noCard>
<h2>{{ 'title' | translateSimple : content }}</h2>
<about-page-content [content]="content.content"></about-page-content>
</ng-template>
</div>
<ion-grid *ngIf="content.type === 'table'">
<ion-row *ngFor="let row of content.rows">
<ion-col *ngFor="let col of row">
<about-page-content [content]="col"></about-page-content>
</ion-col>
</ion-row>
</ion-grid>
<ion-item *ngIf="content.type === 'router link'" [routerLink]="content.link">
<ion-icon *ngIf="content.icon" [name]="content.icon" slot="start"></ion-icon>
<ion-label>{{ 'title' | translateSimple : content }}</ion-label>
</ion-item>

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

@@ -1,37 +1,32 @@
<!--
~ Copyright (C) 2023 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/>.
-->
~ Copyright (C) 2023 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/>.
-->
<ion-header>
<ion-toolbar color="primary" mode="ios">
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
@if (content) {
<ion-title>{{ 'title' | translateSimple: content }}</ion-title>
} @else {
<ion-title *ngIf="content; else titleLoading">{{ 'title' | translateSimple : content }}</ion-title>
<ng-template #titleLoading>
<ion-title><ion-skeleton-text animated="true"></ion-skeleton-text></ion-title>
}
</ng-template>
</ion-toolbar>
</ion-header>
@if (content) {
<ion-content parallax>
<ion-text>{{ 'about.VERSION_INFO' | translate: {name, version, stappsVersion} }}</ion-text>
<div class="page-content">
@for (element of content.content; track element) {
<about-page-content [content]="element"></about-page-content>
}
</div>
</ion-content>
}
<ion-content parallax *ngIf="content">
<ion-text>{{ 'about.VERSION_INFO' | translate: {name, version, stappsVersion} }}</ion-text>
<div class="page-content">
<about-page-content *ngFor="let element of content.content" [content]="element"></about-page-content>
</div>
</ion-content>

View File

@@ -5,6 +5,7 @@ import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {IonicModule, ModalController} from '@ionic/angular';
import {TranslateModule} from '@ngx-translate/core';
import {UtilModule} from '../../util/util.module';
import {CommonModule} from '@angular/common';
@Component({
selector: 'stapps-release-notes',
@@ -12,7 +13,7 @@ import {UtilModule} from '../../util/util.module';
styleUrls: ['release-notes.scss'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [UtilModule, MarkdownModule, ThingTranslateModule, IonicModule, TranslateModule],
imports: [UtilModule, MarkdownModule, ThingTranslateModule, IonicModule, TranslateModule, CommonModule],
})
export class ReleaseNotesComponent {
@Input() versionInfos: SCAppVersionInfo[];

View File

@@ -1,20 +1,18 @@
<ion-header>
<ion-toolbar>
<ion-title
><span class="ion-text-wrap">{{ 'releaseNotes.TITLE_UPDATED' | translate }}</span></ion-title
>
<ion-title><span class="ion-text-wrap">{{'releaseNotes.TITLE_UPDATED' | translate}}</span></ion-title>
<ion-buttons slot="end">
<ion-button [strong]="true" (click)="modalController.dismiss()">{{
'modal.DISMISS_NEUTRAL' | translate
}}</ion-button>
<ion-button [strong]="true" (click)="modalController.dismiss()"
>{{'modal.DISMISS_NEUTRAL' | translate}}</ion-button
>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content parallax>
@for (versionInfo of versionInfos; track versionInfo) {
<ng-container *ngFor="let versionInfo of versionInfos">
<markdown
class="content-card ion-padding"
[data]="'releaseNotes' | translateSimple: versionInfo"
></markdown>
}
</ng-container>
</ion-content>

View File

@@ -36,7 +36,7 @@
class="ion-text-wrap"
*ngIf="assessments[key].courseOfStudy | async as course; else defaultLabel"
>
{{ 'name' | thingTranslate: course }} ({{ 'academicDegree' | thingTranslate: course }})
{{ 'name' | thingTranslate : course }} ({{ 'academicDegree' | thingTranslate : course }})
</ion-label>
</div>
<ng-template #defaultLabel>

View File

@@ -14,12 +14,8 @@
-->
<ion-label [color]="passed ? undefined : 'danger'"
>{{
(_item.grade | isNumeric)
? (_item.grade | numberLocalized: 'minimumFractionDigits:1,maximumFractionDigits:1')
: ''
}}
{{ 'status' | thingTranslate: _item | titlecase }}, {{ 'attempt' | propertyNameTranslate: _item }}
{{ _item.attempt }}
>{{ (_item.grade | isNumeric) ? (_item.grade | numberLocalized :
'minimumFractionDigits:1,maximumFractionDigits:1') : '' }} {{ 'status' | thingTranslate : _item | titlecase
}}, {{ 'attempt' | propertyNameTranslate : _item }} {{ _item.attempt }}
</ion-label>
<ion-note> {{ _item.ects }} {{ 'ects' | propertyNameTranslate: _item }}</ion-note>
<ion-note> {{ _item.ects }} {{ 'ects' | propertyNameTranslate : _item }}</ion-note>

View File

@@ -1,28 +1,24 @@
<!--
~ 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/>.
-->
~ 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/>.
-->
<ion-card>
<ion-card-content>
@if (item.courseOfStudy; as courseOfStudy) {
<ion-note>
{{ $any('courseOfStudy' | propertyNameTranslate: item) | titlecase }}:
{{ 'name' | thingTranslate: $any(courseOfStudy) }} ({{
'academicDegree' | thingTranslate: $any(courseOfStudy)
}})
</ion-note>
}
<ion-note *ngIf="item.courseOfStudy as courseOfStudy">
{{ $any('courseOfStudy' | propertyNameTranslate : item) | titlecase }}: {{ 'name' | thingTranslate :
$any(courseOfStudy) }} ({{ 'academicDegree' | thingTranslate : $any(courseOfStudy) }})
</ion-note>
</ion-card-content>
</ion-card>
<ion-list class="container">

View File

@@ -14,6 +14,6 @@
-->
<div class="container">
<h2 class="name">{{ 'name' | thingTranslate: item }} {{ item.date ? (item.date | amDateFormat) : '' }}</h2>
<h2 class="name">{{ 'name' | thingTranslate : item }} {{ item.date ? (item.date | amDateFormat) : '' }}</h2>
<assessment-base-info [item]="item"></assessment-base-info>
</div>

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

@@ -1,17 +1,17 @@
<!--
~ 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/>.
-->
~ 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/>.
-->
<div>
<ion-card-header>
<ion-card-title>{{ 'schedule.toCalendar.reviewModal.TITLE' | translate }}</ion-card-title>
@@ -22,46 +22,34 @@
<ion-card-content>
<ion-list lines="none">
@for (event of iCalEvents; track event) {
<ion-item-group>
<ion-item-divider>
<ion-label>{{ event.title }}</ion-label>
@if (event.events.length > 1) {
<ion-note slot="start">
<ion-icon name="insert_page_break"></ion-icon>
</ion-note>
}
</ion-item-divider>
@for (iCalEvent of event.events; track iCalEvent) {
<ion-item>
<ion-label>
@if (iCalEvent.cancelled) {
<s><ng-container [ngTemplateOutlet]="date"></ng-container> </s>
} @else {
{{ moment(iCalEvent.start) | amDateFormat: 'll, HH:mm' }}
}
<ng-template #date> {{ moment(iCalEvent.start) | amDateFormat: 'll, HH:mm' }} </ng-template>
</ion-label>
@if (iCalEvent.rrule) {
<ion-note>
{{ iCalEvent.rrule.interval }} {{ iCalEvent.rrule.freq | sentencecase }}
</ion-note>
}
@if (iCalEvent.rrule) {
<ion-icon name="event_repeat"></ion-icon>
}
</ion-item>
}
</ion-item-group>
}
<ion-item-group *ngFor="let event of iCalEvents">
<ion-item-divider>
<ion-label>{{ event.title }}</ion-label>
<ion-note slot="start" *ngIf="event.events.length > 1">
<ion-icon name="insert_page_break"></ion-icon>
</ion-note>
</ion-item-divider>
<ion-item *ngFor="let iCalEvent of event.events">
<ion-label>
<s *ngIf="iCalEvent.cancelled; else date"
><ng-container [ngTemplateOutlet]="date"></ng-container>
</s>
<ng-template #date> {{ moment(iCalEvent.start) | amDateFormat : 'll, HH:mm' }} </ng-template>
</ion-label>
<ion-note *ngIf="iCalEvent.rrule">
{{ iCalEvent.rrule.interval }} {{ iCalEvent.rrule.freq | sentencecase }}
</ion-note>
<ion-icon *ngIf="iCalEvent.rrule" name="event_repeat"></ion-icon>
</ion-item>
</ion-item-group>
</ion-list>
</ion-card-content>
<div class="horizontal-flex">
<ion-item lines="none">
<ion-checkbox [(ngModel)]="includeCancelled">{{
'schedule.toCalendar.reviewModal.INCLUDE_CANCELLED' | translate
}}</ion-checkbox>
<ion-checkbox [(ngModel)]="includeCancelled"
>{{ 'schedule.toCalendar.reviewModal.INCLUDE_CANCELLED' | translate }}</ion-checkbox
>
</ion-item>
</div>
<div class="horizontal-flex">
@@ -69,16 +57,15 @@
{{ 'share' | translate }}
<ion-icon slot="end" md="share" ios="ios_share"></ion-icon>
</ion-button>
@if (isWeb) {
<ion-button fill="outline" (click)="download()">
{{ 'schedule.toCalendar.reviewModal.DOWNLOAD' | translate }}
<ion-icon slot="end" name="download"></ion-icon>
</ion-button>
} @else {
<ion-button fill="outline" (click)="download()" *ngIf="isWeb; else exportButton">
{{ 'schedule.toCalendar.reviewModal.DOWNLOAD' | translate }}
<ion-icon slot="end" name="download"></ion-icon>
</ion-button>
<ng-template #exportButton>
<ion-button fill="outline" (click)="toCalendar()">
{{ 'schedule.toCalendar.reviewModal.EXPORT' | translate }}
<ion-icon slot="end" name="event_upcoming"></ion-icon>
</ion-button>
}
</ng-template>
</div>
</div>

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

@@ -1,17 +1,17 @@
<!--
~ 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/>.
-->
~ 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/>.
-->
<ion-header>
<ion-toolbar color="primary" mode="ios">
@@ -23,45 +23,33 @@
</ion-toolbar>
<ion-toolbar color="primary" mode="md">
<ion-segment (ionChange)="segmentChanged($event)" [value]="selectedSemesterUID" mode="md">
@for (semester of availableSemesters; track semester) {
<ion-segment-button [value]="semester.uid">
<ion-label>{{ semester.acronym }}</ion-label>
</ion-segment-button>
}
<ion-segment-button *ngFor="let semester of availableSemesters" [value]="semester.uid">
<ion-label>{{ semester.acronym }}</ion-label>
</ion-segment-button>
</ion-segment>
</ion-toolbar>
</ion-header>
<ion-content>
@if (catalogs && catalogs.length > 0) {
<ion-list>
@for (catalog of catalogs; track catalog) {
<ion-item button="true" lines="inset" (click)="notifySelect(catalog)">
<ion-list *ngIf="catalogs && catalogs.length > 0">
<ion-item *ngFor="let catalog of catalogs" button="true" lines="inset" (click)="notifySelect(catalog)">
<ion-label>
<h2>{{ catalog.name }}</h2>
</ion-label>
</ion-item>
</ion-list>
<ion-list *ngIf="!catalogs">
<stapps-skeleton-list-item *ngFor="let skeleton of [].constructor(10)"> </stapps-skeleton-list-item>
</ion-list>
<ion-grid *ngIf="catalogs && catalogs.length === 0">
<ion-row>
<ion-col>
<div class="ion-text-center margin-top">
<ion-label>
<h2>{{ catalog.name }}</h2>
{{ 'catalog.detail.EMPTY_SEMESTER' | translate }}
</ion-label>
</ion-item>
}
</ion-list>
}
@if (!catalogs) {
<ion-list>
@for (skeleton of [].constructor(10); track skeleton) {
<stapps-skeleton-list-item> </stapps-skeleton-list-item>
}
</ion-list>
}
@if (catalogs && catalogs.length === 0) {
<ion-grid>
<ion-row>
<ion-col>
<div class="ion-text-center margin-top">
<ion-label>
{{ 'catalog.detail.EMPTY_SEMESTER' | translate }}
</ion-label>
</div>
</ion-col>
</ion-row>
</ion-grid>
}
</div>
</ion-col>
</ion-row>
</ion-grid>
</ion-content>

View File

@@ -15,6 +15,7 @@
ion-segment-button {
max-width: 100%;
text-transform: none;
}
.margin-top {

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,25 +16,15 @@ 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';
import {BehaviorSubject} from 'rxjs';
import {InternetConnectionService} from '../../util/internet-connection.service';
describe('ConfigProvider', () => {
let internetConnectionServiceMock: {offline$: BehaviorSubject<boolean>};
let configProvider: ConfigProvider;
let storageProviderSpy: jasmine.SpyObj<StorageProvider>;
let ngxLogger: jasmine.SpyObj<NGXLogger>;
beforeEach(() => {
internetConnectionServiceMock = {offline$: new BehaviorSubject<boolean>(false)};
storageProviderSpy = jasmine.createSpyObj('StorageProvider', ['init', 'get', 'has', 'put']);
const webHttpClientMethodSpy = jasmine.createSpyObj('StAppsWebHttpClient', ['request']);
ngxLogger = jasmine.createSpyObj('NGXLogger', ['log', 'error', 'warn']);
@@ -55,10 +45,6 @@ describe('ConfigProvider', () => {
provide: NGXLogger,
useValue: ngxLogger,
},
{
provide: InternetConnectionService,
useValue: internetConnectionServiceMock,
},
],
});
@@ -83,22 +69,6 @@ describe('ConfigProvider', () => {
expect(error).toEqual(new ConfigFetchError());
});
it('should throw device offline error when offline', async () => {
// eslint-disable-next-line unicorn/error-message
let error = new Error('');
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
internetConnectionServiceMock.offline$ = new BehaviorSubject<boolean>(true);
try {
await configProvider.fetch();
} catch (error_) {
error = error_ as Error;
expect(error).toBeInstanceOf(ConfigFetchError);
expect(error.message).toContain('Device is offline.');
}
});
it('should init from remote and saved config not available', async () => {
storageProviderSpy.has.and.returnValue(Promise.resolve(false));
spyOn(configProvider.client, 'handshake').and.returnValue(Promise.resolve(sampleIndexResponse));
@@ -128,9 +98,9 @@ describe('ConfigProvider', () => {
it('should throw error on wrong config version in storage', async () => {
storageProviderSpy.has.and.returnValue(Promise.resolve(true));
const wrongConfig = structuredClone(sampleIndexResponse);
const wrongConfig = JSON.parse(JSON.stringify(sampleIndexResponse));
wrongConfig.backend.SCVersion = '0.1.0';
storageProviderSpy.get.and.returnValue(Promise.resolve(wrongConfig));
storageProviderSpy.get.and.returnValue(wrongConfig);
spyOn(configProvider.client, 'handshake').and.returnValue(Promise.resolve(sampleIndexResponse));
await configProvider.init();

View File

@@ -14,21 +14,14 @@
*/
import {Injectable} from '@angular/core';
import {Client} from '@openstapps/api';
import {SCAppConfiguration, SCIndexResponse} from '@openstapps/core';
import {CORE_VERSION} 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 {InternetConnectionService} from '../../util/internet-connection.service';
import {firstValueFrom} from 'rxjs';
import equals from 'fast-deep-equal/es6';
import {BehaviorSubject} from 'rxjs';
/**
* Key to store config in storage module
@@ -37,6 +30,17 @@ import {firstValueFrom} from 'rxjs';
*/
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
*/
@@ -52,18 +56,23 @@ export class ConfigProvider {
/**
* App configuration as IndexResponse
*/
config: SCIndexResponse;
config: Readonly<SCIndexResponse>;
/**
* Version of the @openstapps/core package that app is using
*/
scVersion = CORE_VERSION;
scVersion = packageInfo.version;
/**
* First session indicator (config not found in storage)
*/
firstSession = true;
/**
* If the config requires an update
*/
needsUpdate$ = new BehaviorSubject(false);
/**
* Constructor, initialise api client
* @param storageProvider StorageProvider to load persistent configuration
@@ -74,116 +83,39 @@ export class ConfigProvider {
private readonly storageProvider: StorageProvider,
swHttpClient: StAppsWebHttpClient,
private readonly logger: NGXLogger,
private readonly internetConnectionService: InternetConnectionService,
) {
console.log('config init');
this.client = new Client(swHttpClient, environment.backend_url, environment.backend_version);
}
/**
* Fetches configuration from backend
*/
async fetch(): Promise<SCIndexResponse> {
try {
const isOffline = await firstValueFrom(this.internetConnectionService.offline$);
if (isOffline) {
throw new Error('Device is offline.');
} else {
return await this.client.handshake(this.scVersion);
}
} catch (error) {
const error_ = error instanceof Error ? new ConfigFetchError(error.message) : new ConfigFetchError();
throw error_;
}
}
/**
* 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,67 +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(reason?: string) {
const defaultMessage = 'App configuration could not be fetched!';
const message = reason ? `${defaultMessage} ${reason}` : defaultMessage;
super('ConfigFetchError', message);
}
}
/**
* 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

@@ -1,34 +1,32 @@
<!--
~ Copyright (C) 2023 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/>.
-->
~ Copyright (C) 2023 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/>.
-->
<stapps-section [title]="'dashboard.favorites.title' | translate">
<ion-button slot="button-end" fill="clear" color="medium" [routerLink]="['/favorites']">
<ion-icon slot="icon-only" name="search" [size]="24"></ion-icon>
</ion-button>
@if (items | async; as items) {
<simple-swiper @fade>
@for (item of items; track item) {
<stapps-data-list-item
[hideThumbnail]="true"
[listItemEndInteraction]="false"
[item]="item"
appearance="square"
></stapps-data-list-item>
}
</simple-swiper>
} @else {
<simple-swiper *ngIf="items | async as items; else noItems" @fade>
<stapps-data-list-item
*ngFor="let item of items"
[hideThumbnail]="true"
[listItemEndInteraction]="false"
[item]="item"
appearance="square"
></stapps-data-list-item>
</simple-swiper>
<ng-template #noItems>
<ion-item class="nothing-selected" lines="none">
<ion-label class="ion-text-wrap">
{{ 'dashboard.favorites.no_favorite_prefix' | translate }}
@@ -36,5 +34,5 @@
{{ 'dashboard.favorites.no_favorite_suffix' | translate }}
</ion-label>
</ion-item>
}
</ng-template>
</stapps-section>

View File

@@ -1,44 +1,43 @@
<!--
~ Copyright (C) 2023 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/>.
-->
~ Copyright (C) 2023 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/>.
-->
<stapps-section [title]="'dashboard.jobs.title' | translate">
<ion-button slot="button-end" fill="clear" color="medium" [routerLink]="['/jobs']">
<ion-icon slot="icon-only" name="search" [size]="24"></ion-icon>
</ion-button>
@if (jobs | async; as jobs) {
<simple-swiper @fade>
@for (item of jobs; track item) {
<stapps-data-list-item
[hideThumbnail]="true"
[item]="item"
appearance="square"
></stapps-data-list-item>
}
<ion-item [routerLink]="['/jobs']" class="more-jobs" lines="none">
<ion-label>{{ 'dashboard.jobs.title' | translate | titlecase }}</ion-label>
<ion-icon color="medium" name="read_more" [size]="40"></ion-icon>
</ion-item>
</simple-swiper>
} @else {
<simple-swiper *ngIf="jobs | async as jobs; else noItems" @fade>
<stapps-data-list-item
*ngFor="let item of jobs"
[hideThumbnail]="true"
[item]="item"
appearance="square"
></stapps-data-list-item>
<ion-item [routerLink]="['/jobs']" class="more-jobs" lines="none">
<ion-label>{{ 'dashboard.jobs.title' | translate | titlecase }}</ion-label>
<ion-icon color="medium" name="read_more" [size]="40"></ion-icon>
</ion-item>
</simple-swiper>
<ng-template #noItems>
<ion-item class="nothing-selected" lines="none">
<ion-label class="ion-text-wrap">
{{ 'dashboard.jobs.noJobs' | translate }}
</ion-label>
</ion-item>
<ion-button slot="button-end" fill="clear" color="medium" [routerLink]="['/jobs']">
<ion-icon slot="icon-only" name="search" [size]="24"></ion-icon>
</ion-button>
}
</ng-template>
</stapps-section>

View File

@@ -1,37 +1,33 @@
<!--
~ Copyright (C) 2023 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/>.
-->
~ Copyright (C) 2023 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/>.
-->
@if (dishes | async; as dishes) {
@if (dishes.length > 0) {
<simple-swiper @fade>
@for (dish of dishes; track dish) {
<stapps-data-list-item
[hideThumbnail]="true"
[item]="dish"
appearance="square"
></stapps-data-list-item>
}
</simple-swiper>
}
@if (!dishes || dishes.length === 0) {
<ion-item class="no-dishes" lines="none">
<ion-label>
{{ 'dashboard.canteens.no_dishes_available' | translate }}
</ion-label>
</ion-item>
}
} @else {
<ng-container *ngIf="dishes | async as dishes; else loading">
<simple-swiper *ngIf="dishes.length > 0" @fade>
<stapps-data-list-item
*ngFor="let dish of dishes"
[hideThumbnail]="true"
[item]="dish"
appearance="square"
></stapps-data-list-item>
</simple-swiper>
<ion-item class="no-dishes" *ngIf="!dishes || dishes.length === 0" lines="none">
<ion-label>
{{ 'dashboard.canteens.no_dishes_available' | translate }}
</ion-label>
</ion-item>
</ng-container>
<ng-template #loading>
<div class="placeholder"></div>
}
</ng-template>

View File

@@ -1,21 +1,21 @@
<!--
~ Copyright (C) 2023 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/>.
-->
~ Copyright (C) 2023 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/>.
-->
@if (items | async; as items) {
@if (items.length !== 0) {
@for (item of items; track item) {
<ng-container *ngIf="items | async as items">
<ng-container *ngIf="items.length !== 0; else nothingSelected">
<ng-container *ngFor="let item of items">
<stapps-section @fade [item]="item" [title]="'name' | thingTranslate: item">
<ion-button slot="button-end" fill="clear" color="medium" (click)="favoritesService.delete(item)">
<ion-icon slot="icon-only" name="delete" [size]="24"></ion-icon>
@@ -23,8 +23,9 @@
<stapps-opening-hours slot="subtitle" [openingHours]="$any(item).openingHours"></stapps-opening-hours>
<stapps-mensa-section-content [item]="item"></stapps-mensa-section-content>
</stapps-section>
}
} @else {
</ng-container>
</ng-container>
<ng-template #nothingSelected>
<stapps-section [title]="'dashboard.canteens.title' | translate">
<ion-item class="nothing-selected" lines="none">
<ion-label class="ion-text-wrap">
@@ -34,5 +35,5 @@
</ion-label>
</ion-item>
</stapps-section>
}
}
</ng-template>
</ng-container>

View File

@@ -1,33 +1,29 @@
<!--
~ Copyright (C) 2023 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/>.
-->
~ Copyright (C) 2023 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/>.
-->
<stapps-section [title]="'dashboard.news.title' | translate">
<ion-button size="small" slot="button-end" fill="clear" color="medium" [routerLink]="['/news']">
<ion-icon slot="icon-only" name="read_more"></ion-icon>
</ion-button>
@if (news | async; as news) {
<simple-swiper class="news-swiper card-swiper" @fade>
@for (newsItem of news; track newsItem) {
<stapps-news-item [item]="newsItem"> </stapps-news-item>
}
<ion-item [routerLink]="['/news']" class="more-news" lines="none">
<ion-label>{{ 'dashboard.news.moreNews' | translate | titlecase }}</ion-label>
<ion-thumbnail class="ion-margin-end">
<ion-icon color="medium" name="read_more" [size]="150"></ion-icon>
</ion-thumbnail>
</ion-item>
</simple-swiper>
}
<simple-swiper class="news-swiper card-swiper" *ngIf="news | async as news" @fade>
<stapps-news-item *ngFor="let newsItem of news" [item]="newsItem"> </stapps-news-item>
<ion-item [routerLink]="['/news']" class="more-news" lines="none">
<ion-label>{{ 'dashboard.news.moreNews' | translate | titlecase }}</ion-label>
<ion-thumbnail class="ion-margin-end">
<ion-icon color="medium" name="read_more" [size]="150"></ion-icon>
</ion-thumbnail>
</ion-item>
</simple-swiper>
</stapps-section>

View File

@@ -63,10 +63,10 @@ simple-swiper {
.title {
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
white-space: break-spaces;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
}

View File

@@ -14,7 +14,7 @@
*/
import {AnimationController} from '@ionic/angular';
import {AnimationOptions} from '@ionic/angular/common/providers/nav-controller';
import {AnimationOptions} from '@ionic/angular/providers/nav-controller';
/**
*

View File

@@ -1,25 +1,19 @@
<!--
~ Copyright (C) 2023 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/>.
-->
~ Copyright (C) 2023 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/>.
-->
@if (applicable.locate) {
<stapps-locate-action-chip [item]="item"></stapps-locate-action-chip>
}
@if (applicable.navigate) {
<stapps-navigate-action-chip [item]="$any(item)"></stapps-navigate-action-chip>
}
<stapps-locate-action-chip *ngIf="applicable.locate" [item]="item"></stapps-locate-action-chip>
<stapps-navigate-action-chip *ngIf="applicable.navigate" [item]="$any(item)"></stapps-navigate-action-chip>
<!-- Add Event Chip needs to load data and should be the last -->
@if (applicable.event) {
<stapps-add-event-action-chip [item]="item"></stapps-add-event-action-chip>
}
<stapps-add-event-action-chip *ngIf="applicable.event" [item]="item"></stapps-add-event-action-chip>

View File

@@ -26,7 +26,6 @@ import {AddEventStates, AddEventStatesMap} from './add-event-action-chip.config'
import {EditEventSelectionComponent} from '../edit-event-selection.component';
import {AddEventReviewModalComponent} from '../../../calendar/add-event-review-modal.component';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
import {MaterialSymbol} from 'material-symbols';
/**
* Shows a horizontal list of action chips
@@ -56,7 +55,7 @@ export class AddEventActionChipComponent {
/**
* Icon
*/
icon: MaterialSymbol;
icon: string;
/**
* Current state of icon fill

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