Compare commits

..

16 Commits

Author SHA1 Message Date
a725c4dcf2 feat: improve cross-uni app workflow 2024-07-15 13:13:07 +02:00
2a1a7a5d5b fix: docs generation 2024-07-09 14:30:46 +02:00
Jovan Krunić
a69b80d1d4 feat: library account adjustments
Closes #214
2024-07-04 16:40:37 +02:00
e2abc983ef fix: list item layout broken 2024-07-03 16:26:36 +02:00
913193abdb fix: elasticsearch integration spams errors 2024-07-02 17:49:38 +02:00
67ab1fd613 fix: geo.point has wrong mapping 2024-06-28 17:25:32 +02:00
Rainer Killinger
142079bf0e refactor: set app backend to dev version 2024-06-28 12:02:59 +02:00
Rainer Killinger
5050ac90eb docs: update changelogs for release
ci: publish release
2024-06-28 09:28:39 +02:00
Rainer Killinger
688bc5f2e7 refactor: add changeset 2024-06-28 09:03:54 +02:00
Rainer Killinger
ad174dd7d7 refactor: use static map files from backend 2024-06-28 08:59:08 +02:00
Rainer Killinger
26e654f5b8 fix: app license overview 2024-06-27 14:57:24 +02:00
Rainer Killinger
5439484a90 fix: changelog typo 2024-06-27 12:06:52 +02:00
Rainer Killinger
68f3366a27 refactor: adjustments for recent PAIA changes 2024-06-27 09:23:55 +00:00
Jovan Krunić
dea9a82105 fix: do not fetch remote configuration if offline
Closes #206
2024-06-17 09:55:29 +00:00
39d2801114 feat: store id cards 2024-06-12 13:51:46 +02:00
Rainer Killinger
341b209092 refactor: display id-cards in their own modal 2024-06-05 11:07:30 +00:00
118 changed files with 1603 additions and 1265 deletions

View File

@@ -0,0 +1,5 @@
---
"@openstapps/easy-ast": patch
---
Fixed docs generation

View File

@@ -0,0 +1,5 @@
---
"@openstapps/backend": patch
---
fix for geo.point mapping

View File

@@ -26,6 +26,7 @@ const config = {
'types', 'types',
'bin', 'bin',
'files', 'files',
'builders',
'engines', 'engines',
'scripts', 'scripts',
'dependencies', 'dependencies',

View File

@@ -1,3 +1,3 @@
nodejs 22.2.0 nodejs 18.19.1
pnpm 8.15.4 pnpm 8.15.4
python 3.11.5 python 3.11.5

View File

@@ -1,5 +1,18 @@
# @openstapps/backend # @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 ## 3.2.0
### Minor Changes ### Minor Changes

View File

@@ -2,7 +2,7 @@
The Goethe-Uni App got even better! The Goethe-Uni App got even better!
## Completelty new map view ## Completely new map view
We overhauled the map to offer you a clearer and faster and overview. We overhauled the map to offer you a clearer and faster and overview.

View File

@@ -1,7 +1,7 @@
{ {
"name": "@openstapps/backend", "name": "@openstapps/backend",
"description": "A reference implementation for a StApps backend", "description": "A reference implementation for a StApps backend",
"version": "3.2.0", "version": "3.3.0",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "AGPL-3.0-only", "license": "AGPL-3.0-only",

View File

@@ -25,7 +25,7 @@ export function buildDistanceFilter(
): QueryDslSpecificQueryContainer<'geo_distance'> { ): QueryDslSpecificQueryContainer<'geo_distance'> {
const geoObject: QueryDslGeoDistanceQuery = { const geoObject: QueryDslGeoDistanceQuery = {
distance: `${filter.arguments.distance}m`, distance: `${filter.arguments.distance}m`,
[`${filter.arguments.field}.point.coordinates`]: { [`${filter.arguments.field}.point`]: {
lat: filter.arguments.position[1], lat: filter.arguments.position[1],
lon: filter.arguments.position[0], lon: filter.arguments.position[0],
}, },

View File

@@ -25,7 +25,7 @@ export function buildDistanceSort(sort: SCDistanceSort): SortOptions {
mode: 'avg', mode: 'avg',
order: sort.order, order: sort.order,
unit: 'm', unit: 'm',
[`${sort.arguments.field}.point.coordinates`]: { [`${sort.arguments.field}.point`]: {
lat: sort.arguments.position[1], lat: sort.arguments.position[1],
lon: sort.arguments.position[0], lon: sort.arguments.position[0],
}, },

View File

@@ -466,7 +466,7 @@ describe('Query', function () {
const expectedFilter: QueryDslSpecificQueryContainer<'geo_distance'> = { const expectedFilter: QueryDslSpecificQueryContainer<'geo_distance'> = {
geo_distance: { geo_distance: {
'distance': '1000m', 'distance': '1000m',
'geo.point.coordinates': { 'geo.point': {
lat: 8.123, lat: 8.123,
lon: 50.123, lon: 50.123,
}, },
@@ -636,7 +636,7 @@ describe('Query', function () {
'mode': 'avg', 'mode': 'avg',
'order': 'desc', 'order': 'desc',
'unit': 'm', 'unit': 'm',
'geo.point.coordinates': { 'geo.point': {
lat: 50.123, lat: 50.123,
lon: 8.123, lon: 8.123,
}, },

View File

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

View File

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

View File

@@ -1,12 +1,21 @@
# @openstapps/minimal-connector # @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 ## 3.2.0
### Patch Changes ### Patch Changes
- Updated dependencies [912ae422] - Updated dependencies [912ae422]
- @openstapps/core@4.0.0 - @openstapps/core@3.2.0
- @openstapps/api@4.0.0 - @openstapps/api@3.2.0
- @openstapps/logger@3.0.0 - @openstapps/logger@3.0.0
## 3.1.1 ## 3.1.1

View File

@@ -1,7 +1,7 @@
{ {
"name": "@openstapps/minimal-connector", "name": "@openstapps/minimal-connector",
"description": "This is a minimal connector which serves as an example", "description": "This is a minimal connector which serves as an example",
"version": "3.2.0", "version": "3.3.0",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",

View File

@@ -1,13 +1,24 @@
# @openstapps/minimal-plugin # @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 ## 3.2.0
### Patch Changes ### Patch Changes
- Updated dependencies [912ae422] - Updated dependencies [912ae422]
- @openstapps/core@4.0.0 - @openstapps/core@3.2.0
- @openstapps/api@4.0.0 - @openstapps/api@3.2.0
- @openstapps/api-plugin@4.0.0 - @openstapps/api-plugin@3.2.0
- @openstapps/core-tools@3.0.0 - @openstapps/core-tools@3.0.0
- @openstapps/logger@3.0.0 - @openstapps/logger@3.0.0

View File

@@ -1,7 +1,7 @@
{ {
"name": "@openstapps/minimal-plugin", "name": "@openstapps/minimal-plugin",
"description": "Minimal Plugin", "description": "Minimal Plugin",
"version": "3.2.0", "version": "3.3.0",
"private": true, "private": true,
"type": "module", "type": "module",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",

12
flake.lock generated
View File

@@ -5,11 +5,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1710146030, "lastModified": 1709126324,
"narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", "narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", "rev": "d465f4819400de7c8d874d50b982301f28a84605",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1717112898, "lastModified": 1709747860,
"narHash": "sha256-7R2ZvOnvd9h8fDd65p0JnB7wXfUvreox3xFdYWd1BnY=", "narHash": "sha256-RT4zuBy579m+l8VyIQFOR66WXfcs4g1jntZUHjh6eoI=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6132b0f6e344ce2fe34fc051b72fb46e34f668e0", "rev": "58ae79ea707579c40102ddf62d84b902a987c58b",
"type": "github" "type": "github"
}, },
"original": { "original": {

141
flake.nix
View File

@@ -4,85 +4,68 @@
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
}; };
outputs = outputs = {
{ self,
self, nixpkgs,
nixpkgs, flake-utils,
flake-utils, }: let
}: aapt2buildToolsVersion = "33.0.2";
let in
aapt2buildToolsVersion = "33.0.2"; flake-utils.lib.eachDefaultSystem (system: let
in pkgs = import nixpkgs {
flake-utils.lib.eachDefaultSystem ( inherit system;
system: overlays = [
let (final: prev: rec {
pkgs = import nixpkgs { fontMin = prev.python311.withPackages (ps: with ps; [brotli fonttools] ++ (with fonttools.optional-dependencies; [woff]));
inherit system; android = prev.androidenv.composeAndroidPackages {
overlays = [ buildToolsVersions = ["30.0.3" aapt2buildToolsVersion];
(final: prev: rec { platformVersions = ["33"];
fontMin = prev.python311.withPackages ( };
ps: cypress = prev.cypress.overrideAttrs (cyPrev: rec {
with ps; version = "13.2.0";
[ src = prev.fetchzip {
brotli url = "https://cdn.cypress.io/desktop/${version}/linux-x64/cypress.zip";
fonttools hash = "sha256-9o0nprGcJhudS1LNm+T7Vf0Dwd1RBauYKI+w1FBQ3ZM=";
]
++ (with fonttools.optional-dependencies; [ woff ])
);
android = prev.androidenv.composeAndroidPackages {
buildToolsVersions = [
"30.0.3"
aapt2buildToolsVersion
];
platformVersions = [ "33" ];
}; };
cypress = prev.cypress.overrideAttrs (cyPrev: rec { });
version = "13.2.0"; nodejs = prev.nodejs_18;
src = prev.fetchzip { })
url = "https://cdn.cypress.io/desktop/${version}/linux-x64/cypress.zip"; ];
hash = "sha256-9o0nprGcJhudS1LNm+T7Vf0Dwd1RBauYKI+w1FBQ3ZM="; config = {
}; allowUnfree = true;
}); android_sdk.accept_license = true;
nodejs = prev.nodejs_22;
})
];
config = {
allowUnfree = true;
android_sdk.accept_license = true;
};
}; };
androidFhs = pkgs.buildFHSUserEnv { };
name = "android-env"; androidFhs = pkgs.buildFHSUserEnv {
targetPkgs = pkgs: with pkgs; [ ]; name = "android-env";
runScript = "bash"; targetPkgs = pkgs: with pkgs; [];
profile = '' runScript = "bash";
export ALLOW_NINJA_ENV=true profile = ''
export USE_CCACHE=1 export ALLOW_NINJA_ENV=true
export LD_LIBRARY_PATH=/usr/lib:/usr/lib32 export USE_CCACHE=1
''; export LD_LIBRARY_PATH=/usr/lib:/usr/lib32
}; '';
in };
{ in {
devShell = pkgs.mkShell rec { devShell = pkgs.mkShell rec {
nativeBuildInputs = [ androidFhs ]; nativeBuildInputs = [androidFhs];
buildInputs = with pkgs; [ buildInputs = with pkgs; [
nodejs nodejs
corepack corepack
# tools # tools
curl curl
jq jq
fontMin fontMin
cypress cypress
# android # android
jdk17 jdk17
android.androidsdk android.androidsdk
]; ];
ANDROID_JAVA_HOME = "${pkgs.jdk.home}"; ANDROID_JAVA_HOME = "${pkgs.jdk.home}";
ANDROID_SDK_ROOT = "${pkgs.android.androidsdk}/libexec/android-sdk"; ANDROID_SDK_ROOT = "${pkgs.android.androidsdk}/libexec/android-sdk";
GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_SDK_ROOT}/build-tools/${aapt2buildToolsVersion}/aapt2"; GRADLE_OPTS = "-Dorg.gradle.project.android.aapt2FromMavenOverride=${ANDROID_SDK_ROOT}/build-tools/${aapt2buildToolsVersion}/aapt2";
CYPRESS_INSTALL_BINARY = "0"; CYPRESS_INSTALL_BINARY = "0";
CYPRESS_RUN_BINARY = "${pkgs.cypress}/bin/Cypress"; CYPRESS_RUN_BINARY = "${pkgs.cypress}/bin/Cypress";
}; };
} });
);
} }

View File

@@ -0,0 +1,25 @@
{
"inputPath": "node_modules/material-symbols/material-symbols-rounded.woff2",
"outputPath": "src/assets/icons.min.woff2",
"htmlGlob": "src/**/*.html",
"scriptGlob": "src/**/*.ts",
"additionalIcons": {
"about": ["copyright", "campaign", "policy", "description", "text_snippet"],
"navigation": [
"home",
"newspaper",
"search",
"calendar_month",
"local_cafe",
"local_library",
"inventory_2",
"map",
"grade",
"account_circle",
"settings",
"info",
"rate_review",
"work"
]
}
}

View File

@@ -1,5 +1,18 @@
# @openstapps/app # @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 ## 3.2.0
### Patch Changes ### Patch Changes

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: 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 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. 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 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. 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 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. 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 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? ## How to build and start the app using the default backend?
``` ```sh
npm run build npm run build
npm run start npm run start
``` ```
@@ -86,52 +86,98 @@ addToIonicDB(
You'll need to run _Chromium_ using You'll need to run _Chromium_ using
```shell ```sh
pnpm chromium:no-cors pnpm chromium:no-cors
``` ```
### Help, I can't log in! ### Help, I can't log in!
Login services will often block hosts not coming from the production Login services will often block hosts not coming from the production
server. You can circumvent this locally by using the `:virtual-host` server.
scripts:
```shell #### Web
On the web you can circumvent this locally by using the `:virtual-host` scripts:
```sh
# Start the dev server on mobile.app.uni-frankfurt.de # Start the dev server on mobile.app.uni-frankfurt.de
pnpm start:virtual-host pnpm start:virtual-host
# Run chromium with flags that redirect mobile.app.uni-frankfurt.de to localhost:8100 # Run chromium with flags that redirect mobile.app.uni-frankfurt.de to localhost:8100
pnpm chromium:virtual-host 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 ### Running the app
Install the npm packages needed for running the app (as for any other node project which uses npm): Install the npm packages needed for running the app (as for any other node project which uses npm):
``` ```sh
npm install npm install
``` ```
Check the code for linter issues: Check the code for linter issues:
``` ```sh
npm run lint npm run lint
``` ```
Automatically fix linter issues (those where autofix is possible): Automatically fix linter issues (those where autofix is possible):
``` ```sh
npm run lint:fix npm run lint:fix
``` ```
Build the app (transpile etc.): Build the app (transpile etc.):
``` ```sh
npm run build npm run build
``` ```
Open the app in the browser: Open the app in the browser:
``` ```sh
ionic serve ionic serve
``` ```
@@ -139,7 +185,7 @@ ionic serve
Run the app for testing on an android device (with live reload in the webview / device, when files are changed): 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 build # if needed
npm run resources:android # generate needed resources (icons and splashscreens) 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 npm run docker:run:android # runs "ionic capacitor run android --livereload --external" on a selected device
@@ -159,7 +205,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): 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 npm run docker:build:android
``` ```
@@ -169,13 +215,13 @@ The mentioned `docker:*:android` npm commands are executed in a docker container
Execute unit tests: Execute unit tests:
``` ```sh
npm test npm test
``` ```
Execute e2e tests: Execute e2e tests:
``` ```sh
npm run e2e npm run e2e
``` ```

View File

@@ -16,11 +16,32 @@ android {
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~' ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
} }
} }
flavorDimensions 'uni'
productFlavors {
file('../../config').eachDir {
def config = new groovy.json.JsonSlurper().parseText(file("$it/default.json").text)
"${it.name}" {
dimension 'uni'
applicationId config.android.packageName
versionName config.appMarketingVersion
resValue 'string', 'app_name', config.appName
resValue 'string', 'title_activity_main', config.appName
resValue 'string', 'package_name', config.android.packageName
resValue 'string', 'custom_url_scheme', config.appUrlScheme
resValue 'string', 'app_host', config.appLinkHost
}
}
}
buildTypes { buildTypes {
release { release {
minifyEnabled false minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
debug {
applicationIdSuffix ".debug"
}
} }
} }

View File

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

View File

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

View File

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

View File

@@ -11,7 +11,7 @@
"schematics": {}, "schematics": {},
"architect": { "architect": {
"build": { "build": {
"builder": "@angular-devkit/build-angular:browser", "builder": "@openstapps/angular-builder:application",
"options": { "options": {
"outputPath": "www", "outputPath": "www",
"index": "src/index.html", "index": "src/index.html",

View File

@@ -1,9 +1,17 @@
import {CapacitorConfig} from '@capacitor/cli'; import {CapacitorConfig} from '@capacitor/cli';
const variant = process.env.APP_VARIANT ?? 'default';
// eslint-disable-next-line unicorn/prefer-module, @typescript-eslint/no-var-requires
const uniConfig = require(`./config/${variant}/default.json`);
const config: CapacitorConfig = { const config: CapacitorConfig = {
appId: 'de.anyschool.app', appId: uniConfig.android.packageName, // TODO: iOS bundle ID
appName: 'StApps', appName: uniConfig.appName,
webDir: 'www', webDir: 'www',
android: {
flavor: variant,
},
// TODO: iOS scheme
cordova: { cordova: {
preferences: { preferences: {
'AndroidXEnabled': 'true', 'AndroidXEnabled': 'true',

View File

@@ -0,0 +1,67 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "App Config",
"type": "object",
"properties": {
"$schema": {
"type": "string"
},
"appName": {
"type": "string",
"description": "Full app name",
"examples": ["Open StApps"]
},
"appDisplayName": {
"type": "string",
"description": "App name on mobile device homescreen (Not much space)",
"examples": ["StApps"]
},
"backendUrl": {
"type": "string",
"description": "Publicly available backend url",
"examples": ["https://your.backend.server.tld"]
},
"backendVersion": {
"type": "string",
"description": "Minimum backend version the app will request",
"examples": ["3.0.0"]
},
"appLinkHost": {
"type": "string",
"description": "Your host used for universal (deep) links",
"examples": ["your.deep.link.host.tdl"]
},
"appUrlScheme": {
"type": "string",
"description": "Custom url scheme for native app versions",
"examples": ["de.anyschool.app"]
},
"appMarketingVersion": {
"type": "string",
"description": "App marketing version used in Stores (preferably SemVer or CalVer)",
"examples": ["1.0.0"]
},
"android": {
"type": "object",
"properties": {
"packageName": {
"type": "string",
"description": "Android package name",
"examples": ["de.anyschool.app"]
}
},
"required": ["packageName"]
}
},
"required": [
"$schema",
"appName",
"appDisplayName",
"backendUrl",
"backendVersion",
"appLinkHost",
"appUrlScheme",
"appMarketingVersion",
"android"
]
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "./config.schema.json",
"appName": "Open StApps",
"appDisplayName": "StApps",
"backendUrl": "http://localhost:3000",
"backendVersion": "3.0.0",
"appLinkHost": "localhost:3000",
"appUrlScheme": "de.anyschool.app",
"appMarketingVersion": "1.0.0",
"android": {
"packageName": "de.anyschool.app"
}
}

View File

@@ -0,0 +1,13 @@
{
"$schema": "../config.schema.json",
"appName": "Uni Frankfurt",
"appDisplayName": "Uni Frankfurt",
"backendUrl": "https://mobile.server.uni-frankfurt.de",
"backendVersion": "3.1.0",
"appLinkHost": "mobile.app.uni-frankfurt.de",
"appUrlScheme": "de.unifrankfurt.app",
"appMarketingVersion": "2.3.0",
"android": {
"packageName": "de.unifrankfurt.app"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -1,43 +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/>.
*/
/** @type {import('./scripts/icon-config').IconConfig} */
const config = {
inputPath: 'node_modules/material-symbols/material-symbols-rounded.woff2',
outputPath: 'src/assets/icons.min.woff2',
htmlGlob: 'src/**/*.html',
scriptGlob: 'src/**/*.ts',
additionalIcons: {
about: ['copyright', 'campaign', 'policy', 'description', 'text_snippet'],
navigation: [
'home',
'newspaper',
'search',
'calendar_month',
'local_cafe',
'local_library',
'inventory_2',
'map',
'grade',
'account_circle',
'settings',
'info',
'rate_review',
'work',
],
},
};
export default config;

View File

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

View File

@@ -1,7 +1,7 @@
{ {
"name": "@openstapps/app", "name": "@openstapps/app",
"description": "The generic app tailored to fulfill needs of German universities, written using Ionic Framework.", "description": "The generic app tailored to fulfill needs of German universities, written using Ionic Framework.",
"version": "3.2.0", "version": "3.3.0",
"private": true, "private": true,
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"author": "Karl-Philipp Wulfert <krlwlfrt@gmail.com>", "author": "Karl-Philipp Wulfert <krlwlfrt@gmail.com>",
@@ -14,14 +14,10 @@
"Thea Schöbl <dev@theaninova.de>" "Thea Schöbl <dev@theaninova.de>"
], ],
"scripts": { "scripts": {
"analyze": "webpack-bundle-analyzer www/stats.json", "build": "ng build --configuration=production",
"build": "pnpm check-icons && ng build --configuration=production --stats-json && webpack-bundle-analyzer www/stats.json --mode static --report www/bundle-info.html --no-open",
"build:analyze": "npm run build:stats && npm run analyze",
"build:android": "ionic capacitor build android --no-open && cd android && ./gradlew clean assemble && cd ..", "build:android": "ionic capacitor build android --no-open && cd android && ./gradlew clean assemble && cd ..",
"build:prod": "ng build --configuration=production", "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", "changelog": "conventional-changelog -p angular -i src/assets/about/CHANGELOG.md -s -r 0",
"check-icons": "node scripts/check-icon-correctness.mjs",
"chromium:no-cors": "chromium --disable-web-security --user-data-dir=\".browser-data/chromium\"", "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", "chromium:virtual-host": "chromium --host-resolver-rules=\"MAP mobile.app.uni-frankfurt.de:* localhost:8100\" --ignore-certificate-errors",
"cypress:open": "cypress open", "cypress:open": "cypress open",
@@ -61,6 +57,7 @@
"@angular/router": "17.3.0", "@angular/router": "17.3.0",
"@awesome-cordova-plugins/calendar": "6.6.0", "@awesome-cordova-plugins/calendar": "6.6.0",
"@awesome-cordova-plugins/core": "6.6.0", "@awesome-cordova-plugins/core": "6.6.0",
"@capacitor-community/screen-brightness": "6.0.0",
"@capacitor/app": "5.0.7", "@capacitor/app": "5.0.7",
"@capacitor/browser": "5.2.0", "@capacitor/browser": "5.2.0",
"@capacitor/clipboard": "5.0.7", "@capacitor/clipboard": "5.0.7",
@@ -74,6 +71,7 @@
"@capacitor/local-notifications": "5.0.7", "@capacitor/local-notifications": "5.0.7",
"@capacitor/network": "5.0.7", "@capacitor/network": "5.0.7",
"@capacitor/preferences": "5.0.7", "@capacitor/preferences": "5.0.7",
"@capacitor/screen-orientation": "6.0.0",
"@capacitor/share": "5.0.7", "@capacitor/share": "5.0.7",
"@capacitor/splash-screen": "5.0.7", "@capacitor/splash-screen": "5.0.7",
"@ionic-native/core": "5.36.0", "@ionic-native/core": "5.36.0",
@@ -137,6 +135,7 @@
"@ionic/cli": "7.2.0", "@ionic/cli": "7.2.0",
"@openstapps/prettier-config": "workspace:*", "@openstapps/prettier-config": "workspace:*",
"@openstapps/tsconfig": "workspace:*", "@openstapps/tsconfig": "workspace:*",
"@openstapps/angular-builder": "workspace:*",
"@types/fontkit": "2.0.7", "@types/fontkit": "2.0.7",
"@types/geojson": "1.0.6", "@types/geojson": "1.0.6",
"@types/glob": "8.1.0", "@types/glob": "8.1.0",

View File

@@ -1,65 +0,0 @@
/*
* Copyright (C) 2022 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {openSync} from 'fontkit';
import config from '../icons.config.mjs';
import {existsSync} from 'fs';
import {getUsedIconsHtml, getUsedIconsTS} from './gather-used-icons.mjs';
import {fetchCodePointMap} from './get-code-points.mjs';
const commandName = '"npm run minify-icons"';
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);
let success = true;
// eslint-disable-next-line unicorn/prefer-top-level-await
checkAll().then(() => {
console.log();
if (success) {
console.log('All icons are present in both fonts.');
} else {
console.error('Errors occurred.');
process.exit(-1);
}
});
/**
*
*/
async function checkAll() {
check(config.additionalIcons || {});
check(await getUsedIconsTS(config.scriptGlob));
check(await getUsedIconsHtml(config.htmlGlob));
}
/**
* @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`);
}
}
}

View File

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

View File

@@ -12,24 +12,6 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * 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 { .supertext-icon {
height: 14px; height: 14px;

View File

@@ -17,7 +17,7 @@ import {ActivatedRoute} from '@angular/router';
import {SCAboutPage, SCAppConfiguration} from '@openstapps/core'; import {SCAboutPage, SCAppConfiguration} from '@openstapps/core';
import {ConfigProvider} from '../../config/config.provider'; import {ConfigProvider} from '../../config/config.provider';
import packageJson from '../../../../../package.json'; import packageJson from '../../../../../package.json';
import config from 'capacitor.config'; import config from '../../../../../config/default.json';
import {App} from '@capacitor/app'; import {App} from '@capacitor/app';
import {Capacitor} from '@capacitor/core'; import {Capacitor} from '@capacitor/core';

View File

@@ -24,13 +24,17 @@ import {
} from './errors'; } from './errors';
import {NGXLogger} from 'ngx-logger'; import {NGXLogger} from 'ngx-logger';
import {sampleIndexResponse} from '../../_helpers/data/sample-configuration'; import {sampleIndexResponse} from '../../_helpers/data/sample-configuration';
import {BehaviorSubject} from 'rxjs';
import {InternetConnectionService} from '../../util/internet-connection.service';
describe('ConfigProvider', () => { describe('ConfigProvider', () => {
let internetConnectionServiceMock: {offline$: BehaviorSubject<boolean>};
let configProvider: ConfigProvider; let configProvider: ConfigProvider;
let storageProviderSpy: jasmine.SpyObj<StorageProvider>; let storageProviderSpy: jasmine.SpyObj<StorageProvider>;
let ngxLogger: jasmine.SpyObj<NGXLogger>; let ngxLogger: jasmine.SpyObj<NGXLogger>;
beforeEach(() => { beforeEach(() => {
internetConnectionServiceMock = {offline$: new BehaviorSubject<boolean>(false)};
storageProviderSpy = jasmine.createSpyObj('StorageProvider', ['init', 'get', 'has', 'put']); storageProviderSpy = jasmine.createSpyObj('StorageProvider', ['init', 'get', 'has', 'put']);
const webHttpClientMethodSpy = jasmine.createSpyObj('StAppsWebHttpClient', ['request']); const webHttpClientMethodSpy = jasmine.createSpyObj('StAppsWebHttpClient', ['request']);
ngxLogger = jasmine.createSpyObj('NGXLogger', ['log', 'error', 'warn']); ngxLogger = jasmine.createSpyObj('NGXLogger', ['log', 'error', 'warn']);
@@ -51,6 +55,10 @@ describe('ConfigProvider', () => {
provide: NGXLogger, provide: NGXLogger,
useValue: ngxLogger, useValue: ngxLogger,
}, },
{
provide: InternetConnectionService,
useValue: internetConnectionServiceMock,
},
], ],
}); });
@@ -75,6 +83,22 @@ describe('ConfigProvider', () => {
expect(error).toEqual(new ConfigFetchError()); 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 () => { it('should init from remote and saved config not available', async () => {
storageProviderSpy.has.and.returnValue(Promise.resolve(false)); storageProviderSpy.has.and.returnValue(Promise.resolve(false));
spyOn(configProvider.client, 'handshake').and.returnValue(Promise.resolve(sampleIndexResponse)); spyOn(configProvider.client, 'handshake').and.returnValue(Promise.resolve(sampleIndexResponse));

View File

@@ -27,6 +27,8 @@ import {
SavedConfigNotAvailable, SavedConfigNotAvailable,
WrongConfigVersionInStorage, WrongConfigVersionInStorage,
} from './errors'; } from './errors';
import {InternetConnectionService} from '../../util/internet-connection.service';
import {firstValueFrom} from 'rxjs';
/** /**
* Key to store config in storage module * Key to store config in storage module
@@ -72,6 +74,7 @@ export class ConfigProvider {
private readonly storageProvider: StorageProvider, private readonly storageProvider: StorageProvider,
swHttpClient: StAppsWebHttpClient, swHttpClient: StAppsWebHttpClient,
private readonly logger: NGXLogger, private readonly logger: NGXLogger,
private readonly internetConnectionService: InternetConnectionService,
) { ) {
console.log('config init'); console.log('config init');
this.client = new Client(swHttpClient, environment.backend_url, environment.backend_version); this.client = new Client(swHttpClient, environment.backend_url, environment.backend_version);
@@ -82,9 +85,15 @@ export class ConfigProvider {
*/ */
async fetch(): Promise<SCIndexResponse> { async fetch(): Promise<SCIndexResponse> {
try { try {
return await this.client.handshake(this.scVersion); const isOffline = await firstValueFrom(this.internetConnectionService.offline$);
} catch { if (isOffline) {
throw new ConfigFetchError(); 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_;
} }
} }

View File

@@ -19,8 +19,10 @@ import {AppError} from '../../_helpers/errors';
* Error that is thrown when fetching from backend fails * Error that is thrown when fetching from backend fails
*/ */
export class ConfigFetchError extends AppError { export class ConfigFetchError extends AppError {
constructor() { constructor(reason?: string) {
super('ConfigFetchError', 'App configuration could not be fetched!'); const defaultMessage = 'App configuration could not be fetched!';
const message = reason ? `${defaultMessage} ${reason}` : defaultMessage;
super('ConfigFetchError', message);
} }
} }

View File

@@ -33,9 +33,9 @@ ion-item {
margin: var(--spacing-sm); margin: var(--spacing-sm);
ion-thumbnail { ion-thumbnail {
--ion-margin: var(--spacing-xs); --size: 36px;
margin-block: auto; margin: 0;
margin-inline: var(--spacing-md); margin-inline: var(--spacing-md);
padding: 0; padding: 0;
} }

View File

@@ -11,7 +11,7 @@ export class LibraryAccountPageComponent {
constructor(private readonly libraryAccountService: LibraryAccountService) {} constructor(private readonly libraryAccountService: LibraryAccountService) {}
async ionViewWillEnter(): Promise<void> { async ionViewWillEnter(): Promise<void> {
const patron = await this.libraryAccountService.getProfile(); const patron = await this.libraryAccountService.getPatron();
this.name = patron?.name; this.name = patron?.name;
} }
} }

View File

@@ -39,6 +39,8 @@ export class CheckedOutPageComponent {
async fetchItems() { async fetchItems() {
try { try {
// Prepare patron (status) for the items
await this.libraryAccountService.getPatron();
this.checkedOutItems = undefined; this.checkedOutItems = undefined;
this.checkedOutItems = await this.libraryAccountService.getFilteredItems([PAIADocumentStatus.Held]); this.checkedOutItems = await this.libraryAccountService.getFilteredItems([PAIADocumentStatus.Held]);
} catch { } catch {

View File

@@ -27,7 +27,7 @@
@for (checkedOutItem of checkedOutItems; track checkedOutItem) { @for (checkedOutItem of checkedOutItems; track checkedOutItem) {
<stapps-paia-item <stapps-paia-item
[item]="checkedOutItem" [item]="checkedOutItem"
[propertiesToShow]="['label', 'renewals', 'endtime']" [propertiesToShow]="['label', 'renewals', 'duedate']"
(documentAction)="onDocumentAction($event)" (documentAction)="onDocumentAction($event)"
listName="checked_out" listName="checked_out"
> >

View File

@@ -14,7 +14,8 @@
*/ */
import {Component, EventEmitter, Input, Output} from '@angular/core'; import {Component, EventEmitter, Input, Output} from '@angular/core';
import {DocumentAction, PAIADocument} from '../../../types'; import {DocumentAction, PAIADocument, PAIADocumentStatus, PAIADocumentVisualStatus} from '../../../types';
import {LibraryAccountService} from '../../library-account.service';
@Component({ @Component({
selector: 'stapps-paia-item', selector: 'stapps-paia-item',
@@ -22,7 +23,24 @@ import {DocumentAction, PAIADocument} from '../../../types';
styleUrls: ['./paiaitem.scss'], styleUrls: ['./paiaitem.scss'],
}) })
export class PAIAItemComponent { export class PAIAItemComponent {
@Input() item: PAIADocument; private _item: PAIADocument;
renewable: boolean;
visualStatus?: PAIADocumentVisualStatus;
constructor(private readonly libraryAccountService: LibraryAccountService) {}
@Input()
set item(value: PAIADocument) {
this._item = value;
void this.setRenewable();
this.visualStatus = this.getVisualStatus(Number(this.item.status));
}
get item(): PAIADocument {
return this._item;
}
@Input() @Input()
propertiesToShow: (keyof PAIADocument)[]; propertiesToShow: (keyof PAIADocument)[];
@@ -36,4 +54,23 @@ export class PAIAItemComponent {
async onClick(action: DocumentAction['action']) { async onClick(action: DocumentAction['action']) {
this.documentAction.emit({doc: this.item, action}); this.documentAction.emit({doc: this.item, action});
} }
private async setRenewable() {
const isActive = await this.libraryAccountService.isActivePatron();
this.renewable = isActive && Number(this.item.status) === PAIADocumentStatus.Held;
}
private getVisualStatus(status: PAIADocumentStatus): PAIADocumentVisualStatus | undefined {
switch (status) {
case PAIADocumentStatus.Ordered: {
return {color: 'warning', status: status, statusText: 'ordered'};
}
case PAIADocumentStatus.Provided: {
return {color: 'success', status: status, statusText: 'ready'};
}
default: {
return undefined;
}
}
}
} }

View File

@@ -14,16 +14,22 @@
--> -->
<ion-item> <ion-item>
<!-- TODO: text not selectable in Chrome, bugfix needed https://github.com/ionic-team/ionic-framework/issues/24956 -->
<ion-label class="ion-text-wrap"> <ion-label class="ion-text-wrap">
@if (item.about) { @if (item.about) {
<h2 class="name">{{ item.about }}</h2> <h2 class="name">
@if (visualStatus) {
<ion-badge [color]="visualStatus.color" slot="start">
{{ 'library.account.pages' + '.' + listName + '.' + visualStatus.statusText | translate }}
</ion-badge>
}
{{ item.about }}
</h2>
} }
@for (property of propertiesToShow; track property) { @for (property of propertiesToShow; track property) {
@if (item[property]) { @if (item[property]) {
<p> <p>
{{ 'library.account.pages' + '.' + listName + '.' + 'labels' + '.' + property | translate }}: {{ 'library.account.pages' + '.' + listName + '.' + 'labels' + '.' + property | translate }}:
@if (!['endtime', 'duedate'].includes(property)) { @if (!['starttime', 'duedate'].includes(property)) {
{{ item[property] }} {{ item[property] }}
} @else { } @else {
{{ $any(item[property]) | amDateFormat: 'll' }} {{ $any(item[property]) | amDateFormat: 'll' }}
@@ -40,7 +46,7 @@
<!-- >--> <!-- >-->
<!-- {{ 'library.account.actions.cancel.header' | translate }}</ion-button--> <!-- {{ 'library.account.actions.cancel.header' | translate }}</ion-button-->
<!-- >--> <!-- >-->
@if (item.canrenew) { @if (renewable && item.canrenew) {
<ion-button color="primary" (click)="onClick('renew')"> <ion-button color="primary" (click)="onClick('renew')">
{{ 'library.account.actions.renew.header' | translate }}</ion-button {{ 'library.account.actions.renew.header' | translate }}</ion-button
> >

View File

@@ -12,3 +12,6 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
ion-badge {
vertical-align: bottom;
}

View File

@@ -45,6 +45,8 @@ export class HoldsPageComponent {
? [PAIADocumentStatus.Reserved] ? [PAIADocumentStatus.Reserved]
: [PAIADocumentStatus.Ordered, PAIADocumentStatus.Provided]; : [PAIADocumentStatus.Ordered, PAIADocumentStatus.Provided];
try { try {
// Prepare patron (status) for the items
await this.libraryAccountService.getPatron();
this.paiaDocuments = await this.libraryAccountService.getFilteredItems(itemsStatus); this.paiaDocuments = await this.libraryAccountService.getFilteredItems(itemsStatus);
} catch { } catch {
await this.libraryAccountService.handleError(); await this.libraryAccountService.handleError();

View File

@@ -35,30 +35,20 @@
@switch (activeSegment) { @switch (activeSegment) {
@case ('orders') { @case ('orders') {
@for (hold of paiaDocuments; track hold) { @for (hold of paiaDocuments; track hold) {
@if (toNumber(hold.status) === paiaDocumentStatus.Provided) { <stapps-paia-item
<stapps-paia-item [item]="hold"
[item]="hold" [propertiesToShow]="['label', 'storage']"
[propertiesToShow]="['label', 'storage']" (documentAction)="onDocumentAction($event)"
(documentAction)="onDocumentAction($event)" listName="holds"
listName="holds" >
> </stapps-paia-item>
</stapps-paia-item>
} @else {
<stapps-paia-item
[item]="hold"
[propertiesToShow]="['label']"
(documentAction)="onDocumentAction($event)"
listName="holds"
>
</stapps-paia-item>
}
} }
} }
@case ('reservations') { @case ('reservations') {
@for (hold of paiaDocuments; track hold) { @for (hold of paiaDocuments; track hold) {
<stapps-paia-item <stapps-paia-item
[item]="hold" [item]="hold"
[propertiesToShow]="['label']" [propertiesToShow]="['label', 'starttime', 'storage', 'queue']"
(documentAction)="onDocumentAction($event)" (documentAction)="onDocumentAction($event)"
listName="holds" listName="holds"
> >

View File

@@ -20,7 +20,15 @@ import {
SCFeatureConfiguration, SCFeatureConfiguration,
SCFeatureConfigurationExtern, SCFeatureConfigurationExtern,
} from '@openstapps/core'; } from '@openstapps/core';
import {DocumentAction, PAIADocument, PAIADocumentStatus, PAIAFees, PAIAItems, PAIAPatron} from '../types'; import {
DocumentAction,
PAIADocument,
PAIADocumentStatus,
PAIAFees,
PAIAItems,
PAIAPatron,
PAIAPatronStatus,
} from '../types';
import {HebisDataProvider} from '../../hebis/hebis-data.provider'; import {HebisDataProvider} from '../../hebis/hebis-data.provider';
import {PAIATokenResponse} from '../../auth/paia/paia-token-response'; import {PAIATokenResponse} from '../../auth/paia/paia-token-response';
import {AuthHelperService} from '../../auth/auth-helper.service'; import {AuthHelperService} from '../../auth/auth-helper.service';
@@ -43,6 +51,11 @@ export class LibraryAccountService {
*/ */
authType: SCAuthorizationProviderType; authType: SCAuthorizationProviderType;
/**
* Account (Patron) status
*/
private status?: PAIAPatronStatus;
constructor( constructor(
protected requestor: Requestor = new JQueryRequestor(), protected requestor: Requestor = new JQueryRequestor(),
private readonly hebisDataProvider: HebisDataProvider, private readonly hebisDataProvider: HebisDataProvider,
@@ -60,12 +73,23 @@ export class LibraryAccountService {
this.authType = config.authProvider as SCAuthorizationProviderType; this.authType = config.authProvider as SCAuthorizationProviderType;
} }
async getProfile() { async getPatron() {
const patron = ((await this.getValidToken()) as PAIATokenResponse).patron; const patronId = ((await this.getValidToken()) as PAIATokenResponse).patron;
return { const patron = {
...(await this.performRequest<PAIAPatron>(`${this.baseUrl}/{patron}`)), ...(await this.performRequest<PAIAPatron>(`${this.baseUrl}/{patron}`)),
id: patron, id: patronId,
} as PAIAPatron; } as PAIAPatron;
// Refresh the status
this.status = Number(patron.status);
return patron;
}
async getPatronStatus() {
return this.status ?? (this.status = Number((await this.getPatron()).status) as PAIAPatronStatus);
}
async isActivePatron() {
return (await this.getPatronStatus()) === PAIAPatronStatus.Active;
} }
async getItems() { async getItems() {

View File

@@ -25,15 +25,19 @@ import {PAIAPatron} from '../../types';
export class ProfilePageComponent { export class ProfilePageComponent {
patron?: PAIAPatron; patron?: PAIAPatron;
propertiesToShow: (keyof PAIAPatron)[] = ['id', 'name', 'email', 'address', 'expires', 'note']; propertiesToShow: (keyof PAIAPatron)[] = ['id', 'name', 'email', 'address', 'expires'];
constructor(private readonly libraryAccountService: LibraryAccountService) {} constructor(private readonly libraryAccountService: LibraryAccountService) {}
async ionViewWillEnter(): Promise<void> { async ionViewWillEnter(): Promise<void> {
try { try {
this.patron = await this.libraryAccountService.getProfile(); this.patron = await this.libraryAccountService.getPatron();
} catch { } catch {
await this.libraryAccountService.handleError(); await this.libraryAccountService.handleError();
} }
} }
isUnlimitedExpiry(date = ''): boolean {
return new Date(date).getFullYear() === 9999;
}
} }

View File

@@ -36,11 +36,11 @@
@if (!['expires'].includes(property)) { @if (!['expires'].includes(property)) {
{{ patron[property] }} {{ patron[property] }}
} @else { } @else {
@if (patron[property] === '9999-12-31') { @if (isUnlimitedExpiry(patron['expires'])) {
{{ 'library.account.pages.profile.values.unlimited' | translate }} {{ 'library.account.pages.profile.values.unlimited' | translate }}
} @else { } @else {
{{ 'library.account.pages.profile.values.expires' | translate }}:&nbsp;{{ {{ 'library.account.pages.profile.values.expires' | translate }}:&nbsp;{{
patron[property] | amDateFormat: 'll' patron['expires'] | amDateFormat: 'll'
}} }}
} }
} }

View File

@@ -19,11 +19,19 @@ export interface PAIAPatron {
email?: string; email?: string;
address?: string; address?: string;
expires?: string; expires?: string;
status?: string; status?: PAIAPatronStatus;
type?: string; type?: string;
note?: string; note?: string;
} }
export enum PAIAPatronStatus {
Active = 0,
Inactive = 1,
InactiveExpired = 2,
InactiveOutstandingFees = 3,
InactiveExpiredOutstandingFees = 4,
}
/* /*
* Document representing a library item received from the HeBIS PAIA service * Document representing a library item received from the HeBIS PAIA service
* TODO: would be good to standardize the items of HeBIS PAIA to match the official PAIA documentation * TODO: would be good to standardize the items of HeBIS PAIA to match the official PAIA documentation
@@ -39,7 +47,7 @@ export interface PAIADocument {
queue?: string; queue?: string;
renewals?: string; renewals?: string;
reminder?: string; reminder?: string;
endtime?: string; starttime?: string;
duedate?: string; duedate?: string;
cancancel?: boolean; cancancel?: boolean;
canrenew?: boolean; canrenew?: boolean;
@@ -80,3 +88,9 @@ export interface DocumentAction {
action: 'cancel' | 'renew'; action: 'cancel' | 'renew';
doc: PAIADocument; doc: PAIADocument;
} }
export interface PAIADocumentVisualStatus {
color: 'warning' | 'success';
status: PAIADocumentStatus;
statusText: 'ordered' | 'ready';
}

View File

@@ -1,5 +1,6 @@
import {Directive, Input, Host} from '@angular/core'; import {Directive, Input, Host} from '@angular/core';
import {MapComponent} from '@maplibre/ngx-maplibre-gl'; import {MapComponent} from '@maplibre/ngx-maplibre-gl';
import {environment} from '../../../environments/environment';
@Directive({ @Directive({
selector: 'mgl-map[styleName]', selector: 'mgl-map[styleName]',
@@ -10,7 +11,7 @@ export class MapStyleDirective {
@Input() @Input()
set styleName(name: string) { set styleName(name: string) {
const style = `https://maps.server.uni-frankfurt.de/static/styles/${name}/style.json`; const style = `${environment.backend_url}/_static/map/styles/${name}/style.json`;
if (this.map.style) { if (this.map.style) {
this.map.mapInstance.setStyle(style); this.map.mapInstance.setStyle(style);
} else { } else {

View File

@@ -21,9 +21,9 @@ import {
SCTranslations, SCTranslations,
} from '@openstapps/core'; } from '@openstapps/core';
import {NavigationService} from './navigation.service'; import {NavigationService} from './navigation.service';
import config from 'capacitor.config';
import {SettingsProvider} from '../../settings/settings.provider'; import {SettingsProvider} from '../../settings/settings.provider';
import {BreakpointObserver} from '@angular/cdk/layout'; import {BreakpointObserver} from '@angular/cdk/layout';
import config from '../../../../../config/default.json';
/** /**
* Generated class for the MenuPage page. * Generated class for the MenuPage page.

View File

@@ -1,10 +1,14 @@
import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; import {ChangeDetectionStrategy, Component, ElementRef, Input} from '@angular/core';
import {SCIdCard} from '@openstapps/core'; import {SCIdCard} from '@openstapps/core';
import {FullScreenImageDirective} from '../../util/full-screen-image.directive';
import {ThingTranslateModule} from '../../translation/thing-translate.module'; import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {AsyncPipe, TitleCasePipe} from '@angular/common'; import {AsyncPipe, TitleCasePipe} from '@angular/common';
import {InRangeNowPipe, ToDateRangePipe} from '../../util/in-range.pipe'; import {InRangeNowPipe, ToDateRangePipe} from '../../util/in-range.pipe';
import {TranslateModule} from '@ngx-translate/core'; import {TranslateModule} from '@ngx-translate/core';
import {AnimationController, ModalController} from '@ionic/angular';
import {ScreenBrightness} from '@capacitor-community/screen-brightness';
import {ScreenOrientation} from '@capacitor/screen-orientation';
import {Capacitor} from '@capacitor/core';
import {iosDuration, iosEasing, mdDuration, mdEasing} from 'src/app/animation/easings';
@Component({ @Component({
selector: 'stapps-id-card', selector: 'stapps-id-card',
@@ -12,17 +16,130 @@ import {TranslateModule} from '@ngx-translate/core';
styleUrls: ['id-card.scss'], styleUrls: ['id-card.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, standalone: true,
hostDirectives: [FullScreenImageDirective], imports: [ThingTranslateModule, InRangeNowPipe, ToDateRangePipe, AsyncPipe, TranslateModule, TitleCasePipe],
imports: [
FullScreenImageDirective,
ThingTranslateModule,
InRangeNowPipe,
ToDateRangePipe,
AsyncPipe,
TranslateModule,
TitleCasePipe,
],
}) })
export class IdCardComponent { export class IdCardComponent {
@Input({required: true}) item: SCIdCard; @Input({required: true}) item: SCIdCard;
constructor(
private modalController: ModalController,
private animationController: AnimationController,
private elementRef: ElementRef,
) {}
async presentFullscreen() {
const top = await this.modalController.getTop();
const mode = document.querySelector('ion-app')!.getAttribute('mode');
if (top) return;
if (window.innerWidth >= 768) return;
if (Capacitor.isNativePlatform()) {
const orientation = await ScreenOrientation.orientation();
ScreenOrientation.lock({orientation: 'portrait'});
if (orientation.type.startsWith('landscape')) {
await new Promise(resolve => setTimeout(resolve, 500));
}
}
const img: HTMLImageElement = this.elementRef.nativeElement.querySelector('img')!;
const safeArea = (location: string) =>
Number(img.computedStyleMap().get(`--ion-safe-area-${location}`)!.toString().replace(/px$/, ''));
const safeAreaTop = safeArea('top');
const safeAreaBottom = safeArea('bottom');
const safeAreaLeft = safeArea('left');
const safeAreaRight = safeArea('right');
const windowWidth = window.innerWidth - safeAreaLeft - safeAreaRight;
const windowHeight = window.innerHeight - safeAreaTop - safeAreaBottom;
const isLandscape = windowWidth > windowHeight;
const nativeBounds = img.getBoundingClientRect();
const imageAspect = nativeBounds.width / nativeBounds.height;
const imgWidth = Math.min(windowHeight, windowWidth * imageAspect);
const imgHeight = imgWidth / imageAspect;
const fullscreenWidth = isLandscape ? imgHeight : imgWidth;
const fullscreenHeight = isLandscape ? imgWidth : imgHeight;
const scale = fullscreenWidth / windowWidth;
const animation = (modal: HTMLElement, leave = false) => {
const root = modal.shadowRoot!;
const sourceAnimation = this.animationController
.create()
.addElement(this.elementRef.nativeElement)
.fromTo('opacity', 0, 0);
const backdrop =
'linear-gradient(to bottom,' +
'transparent 16px,' +
'rgba(0, 0, 0, 0.3) 20%,' +
'rgba(0, 0, 0, 0.3) 80%,' +
'transparent 100%)';
const wrapperAnimation = this.animationController
.create()
.beforeStyles({'--background': 'transparent', 'margin': '0'})
.addElement(root.querySelector('.modal-wrapper')!)
.fromTo('transform', 'scale(1)', 'scale(1)')
.fromTo('opacity', '1', '1');
const backdropAnimation = this.animationController
.create()
.beforeStyles({background: backdrop, filter: 'blur(16px)'})
.addElement(root.querySelector('ion-backdrop')!)
.fromTo('opacity', leave ? 1 : 0, leave ? 0 : 1);
const small =
`translate(${nativeBounds.left - safeAreaLeft}px, ${nativeBounds.top - safeAreaTop}px) ` +
`scale(${nativeBounds.width / windowWidth}) ` +
`rotate(0) `;
const large =
`translate(${windowWidth - (windowWidth - fullscreenHeight) / 2}px, ${
(windowHeight - fullscreenWidth) / 2
}px)` +
`scale(${scale}) ` +
`rotate(${isLandscape ? 0 : 90}deg) `;
const cardAnimation = this.animationController
.create()
.addElement(modal.querySelector('stapps-id-card')!)
.beforeStyles({
'transform-origin': 'top left',
'filter': 'drop-shadow(0 0 16px rgba(0, 0, 0, 0.1))',
})
.fromTo('transform', leave ? large : small, leave ? small : large);
return this.animationController
.create()
.addElement(modal)
.easing(mode === 'ios' ? iosEasing : mdEasing)
.duration(2 * (mode === 'ios' ? iosDuration : mdDuration))
.addAnimation([wrapperAnimation, backdropAnimation, cardAnimation, sourceAnimation]);
};
const modal = await this.modalController.create({
component: IdCardComponent,
backdropDismiss: true,
mode: 'md',
componentProps: {
item: this.item,
},
presentingElement: this.elementRef.nativeElement,
enterAnimation: base => animation(base),
leaveAnimation: base => animation(base, true),
});
const dismiss = () => modal.dismiss();
window.addEventListener('click', dismiss);
modal.addEventListener('didDismiss', () => window.removeEventListener('click', dismiss));
if (Capacitor.isNativePlatform()) {
const brightness = (await ScreenBrightness.getBrightness()).brightness;
ScreenBrightness.setBrightness({brightness: 1});
modal.addEventListener('didDismiss', () => {
ScreenOrientation.unlock();
ScreenBrightness.setBrightness({brightness: brightness === 1 ? 0.5 : brightness});
});
}
await modal.present();
}
} }

View File

@@ -1,4 +1,9 @@
<img [src]="item.image" [alt]="'name' | thingTranslate: item" draggable="false" /> <img
[src]="item.image"
[alt]="'name' | thingTranslate: item"
draggable="false"
(click)="presentFullscreen()"
/>
@if (item.validity && (item.validity | toDateRange | isInRangeNow | async) === false) { @if (item.validity && (item.validity | toDateRange | isInRangeNow | async) === false) {
<div class="expired">{{ 'profile.userInfo.expired' | translate | titlecase }}</div> <div class="expired">{{ 'profile.userInfo.expired' | translate | titlecase }}</div>
} }

View File

@@ -3,11 +3,6 @@
overflow: hidden; overflow: hidden;
} }
:host:fullscreen {
margin: 0;
padding: 0;
}
img { img {
border-radius: 3mm; border-radius: 3mm;
} }

View File

@@ -19,7 +19,6 @@ import {IonicModule} from '@ionic/angular';
import {AsyncPipe, TitleCasePipe} from '@angular/common'; import {AsyncPipe, TitleCasePipe} from '@angular/common';
import {ThingTranslateModule} from '../../translation/thing-translate.module'; import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {UtilModule} from '../../util/util.module'; import {UtilModule} from '../../util/util.module';
import {FullScreenImageDirective} from '../../util/full-screen-image.directive';
import {IdCardComponent} from './id-card.component'; import {IdCardComponent} from './id-card.component';
import {TranslateModule} from '@ngx-translate/core'; import {TranslateModule} from '@ngx-translate/core';
import {Observable} from 'rxjs'; import {Observable} from 'rxjs';
@@ -36,7 +35,6 @@ import {Observable} from 'rxjs';
AsyncPipe, AsyncPipe,
ThingTranslateModule, ThingTranslateModule,
UtilModule, UtilModule,
FullScreenImageDirective,
IdCardComponent, IdCardComponent,
TranslateModule, TranslateModule,
TitleCasePipe, TitleCasePipe,

View File

@@ -1,10 +1,11 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {SCIdCard, SCThingOriginType, SCThingType, SCUserConfiguration} from '@openstapps/core'; import {SCIdCard, SCThingOriginType, SCThingType, SCUserConfiguration} from '@openstapps/core';
import {from, Observable, of} from 'rxjs'; import {from, of, Observable} from 'rxjs';
import {AuthHelperService} from '../auth/auth-helper.service'; import {AuthHelperService} from '../auth/auth-helper.service';
import {mergeMap, filter, map, startWith} from 'rxjs/operators'; import {mergeMap, concatWith, filter, map, startWith, catchError, tap} from 'rxjs/operators';
import {ConfigProvider} from '../config/config.provider'; import {ConfigProvider} from '../config/config.provider';
import {HttpClient} from '@angular/common/http'; import {HttpClient} from '@angular/common/http';
import {EncryptedStorageProvider} from '../storage/encrypted-storage.provider';
@Injectable({providedIn: 'root'}) @Injectable({providedIn: 'root'})
export class IdCardsProvider { export class IdCardsProvider {
@@ -12,18 +13,27 @@ export class IdCardsProvider {
private authHelper: AuthHelperService, private authHelper: AuthHelperService,
private config: ConfigProvider, private config: ConfigProvider,
private httpClient: HttpClient, private httpClient: HttpClient,
private encryptedStorageProvider: EncryptedStorageProvider,
) {} ) {}
getIdCards(): Observable<SCIdCard[]> { getIdCards(): Observable<SCIdCard[]> {
const feature = this.config.config.app.features.extern?.['idCards']; const feature = this.config.config.app.features.extern?.['idCards'];
const auth = this.authHelper.getProvider(feature?.authProvider ?? 'default'); const auth = this.authHelper.getProvider(feature?.authProvider ?? 'default');
const storedIdCards = from(
this.encryptedStorageProvider.get<SCIdCard[]>('id-cards') as Promise<SCIdCard[]>,
).pipe(filter(it => it !== undefined));
return auth.isAuthenticated$.pipe( return auth.isAuthenticated$.pipe(
mergeMap(isAuthenticated => mergeMap(isAuthenticated =>
isAuthenticated isAuthenticated
? feature ? feature
? from(auth.getValidToken()).pipe( ? storedIdCards.pipe(
mergeMap(token => this.fetchIdCards(feature.url, token.accessToken)), concatWith(
from(auth.getValidToken()).pipe(
mergeMap(token => this.fetchIdCards(feature.url, token.accessToken)),
catchError(() => storedIdCards),
),
),
) )
: auth.user$.pipe( : auth.user$.pipe(
filter(user => user !== undefined), filter(user => user !== undefined),
@@ -31,19 +41,20 @@ export class IdCardsProvider {
mergeMap(user => this.fetchFallbackIdCards(user)), mergeMap(user => this.fetchFallbackIdCards(user)),
startWith([]), startWith([]),
) )
: // TODO: find a better solution here (async pipe stuff...) : of([]).pipe(tap(() => this.encryptedStorageProvider.delete('id-cards'))),
of([]),
), ),
); );
} }
private fetchIdCards(url: string, token: string): Observable<SCIdCard[]> { private fetchIdCards(url: string, token: string): Observable<SCIdCard[]> {
return this.httpClient.get<SCIdCard[]>(url, { return this.httpClient
headers: { .get<SCIdCard[]>(url, {
Authorization: `Bearer ${token}`, headers: {
}, Authorization: `Bearer ${token}`,
responseType: 'json', },
}); responseType: 'json',
})
.pipe(tap(idCards => this.encryptedStorageProvider.set('id-cards', idCards)));
} }
private fetchFallbackIdCards(user: SCUserConfiguration): Observable<SCIdCard[]> { private fetchFallbackIdCards(user: SCUserConfiguration): Observable<SCIdCard[]> {

View File

@@ -4,6 +4,7 @@ import {HttpClient} from '@angular/common/http';
import {AuthHelperService} from '../auth/auth-helper.service'; import {AuthHelperService} from '../auth/auth-helper.service';
import {BehaviorSubject, firstValueFrom, of} from 'rxjs'; import {BehaviorSubject, firstValueFrom, of} from 'rxjs';
import {SCAuthorizationProviderType} from '@openstapps/core'; import {SCAuthorizationProviderType} from '@openstapps/core';
import {EncryptedStorageProvider} from '../storage/encrypted-storage.provider';
class FakeAuth { class FakeAuth {
isAuthenticated$ = new BehaviorSubject(false); isAuthenticated$ = new BehaviorSubject(false);
@@ -16,6 +17,7 @@ describe('IdCards', () => {
let configProvider: ConfigProvider; let configProvider: ConfigProvider;
let httpClient: HttpClient; let httpClient: HttpClient;
let authHelper: AuthHelperService; let authHelper: AuthHelperService;
let encryptedStorageProvider: EncryptedStorageProvider;
let fakeAuth: FakeAuth; let fakeAuth: FakeAuth;
beforeEach(() => { beforeEach(() => {
@@ -27,10 +29,14 @@ describe('IdCards', () => {
fakeAuth = new FakeAuth(); fakeAuth = new FakeAuth();
authHelper = jasmine.createSpyObj('AuthHelperService', ['getProvider']); authHelper = jasmine.createSpyObj('AuthHelperService', ['getProvider']);
authHelper.getProvider = jasmine.createSpy().and.returnValue(fakeAuth); authHelper.getProvider = jasmine.createSpy().and.returnValue(fakeAuth);
encryptedStorageProvider = jasmine.createSpyObj('EncryptedStorageProvider', ['get', 'set', 'delete']);
encryptedStorageProvider.get = jasmine.createSpy().and.resolveTo();
encryptedStorageProvider.set = jasmine.createSpy().and.resolveTo();
encryptedStorageProvider.delete = jasmine.createSpy().and.resolveTo();
}); });
it('should emit undefined if not logged in', async () => { it('should emit undefined if not logged in', async () => {
const provider = new IdCardsProvider(authHelper, configProvider, httpClient); const provider = new IdCardsProvider(authHelper, configProvider, httpClient, encryptedStorageProvider);
expect(await firstValueFrom(provider.getIdCards())).toEqual([]); expect(await firstValueFrom(provider.getIdCards())).toEqual([]);
expect(authHelper.getProvider).toHaveBeenCalledOnceWith('fakeAuth' as SCAuthorizationProviderType); expect(authHelper.getProvider).toHaveBeenCalledOnceWith('fakeAuth' as SCAuthorizationProviderType);
}); });
@@ -39,7 +45,7 @@ describe('IdCards', () => {
fakeAuth.isAuthenticated$.next(true); fakeAuth.isAuthenticated$.next(true);
httpClient.get = jasmine.createSpy().and.returnValue(of(['abc'])); httpClient.get = jasmine.createSpy().and.returnValue(of(['abc']));
fakeAuth.getValidToken = jasmine.createSpy().and.resolveTo({accessToken: 'fake-token'}); fakeAuth.getValidToken = jasmine.createSpy().and.resolveTo({accessToken: 'fake-token'});
const provider = new IdCardsProvider(authHelper, configProvider, httpClient); const provider = new IdCardsProvider(authHelper, configProvider, httpClient, encryptedStorageProvider);
expect(await firstValueFrom(provider.getIdCards())).toEqual(['abc' as never]); expect(await firstValueFrom(provider.getIdCards())).toEqual(['abc' as never]);
expect(authHelper.getProvider).toHaveBeenCalledOnceWith('fakeAuth' as SCAuthorizationProviderType); expect(authHelper.getProvider).toHaveBeenCalledOnceWith('fakeAuth' as SCAuthorizationProviderType);
// eslint-disable-next-line unicorn/no-null // eslint-disable-next-line unicorn/no-null
@@ -52,7 +58,7 @@ describe('IdCards', () => {
}); });
it('should react to logins', async () => { it('should react to logins', async () => {
const provider = new IdCardsProvider(authHelper, configProvider, httpClient); const provider = new IdCardsProvider(authHelper, configProvider, httpClient, encryptedStorageProvider);
const observable = provider.getIdCards(); const observable = provider.getIdCards();
expect(await firstValueFrom(observable)).toEqual([]); expect(await firstValueFrom(observable)).toEqual([]);
httpClient.get = jasmine.createSpy().and.returnValue(of(['abc'])); httpClient.get = jasmine.createSpy().and.returnValue(of(['abc']));

View File

@@ -0,0 +1,92 @@
import {Injectable} from '@angular/core';
import {StorageProvider} from './storage.provider';
import {Capacitor} from '@capacitor/core';
import {SecureStoragePlugin} from 'capacitor-secure-storage-plugin';
@Injectable({providedIn: 'root'})
export class EncryptedStorageProvider {
constructor(private storageProvider: StorageProvider) {}
/**
* Retrieve a large value from an encrypted storage
* Also returns undefined if a secure context is not available (i.e. web).
* @param key Unique identifier of the wanted resource in storage
* @returns The value of the resource, if found
*/
async get<T>(key: string): Promise<T | undefined> {
if (!Capacitor.isNativePlatform()) return undefined;
try {
const jwt = JSON.parse((await SecureStoragePlugin.get({key: `stapps:key:${key}`})).value);
const aesKey = await crypto.subtle.importKey('jwk', jwt, {name: 'AES-GCM'}, true, [
'encrypt',
'decrypt',
]);
const iv = await this.storageProvider.get<ArrayBuffer>(`encrypted:${key}:iv`);
const encryptedIdCards = await this.storageProvider.get<ArrayBuffer>(`encrypted:${key}`);
const decrypted = await crypto.subtle.decrypt({name: 'AES-GCM', iv}, aesKey, encryptedIdCards);
const decompressionStream = new DecompressionStream('gzip');
const writer = decompressionStream.writable.getWriter();
writer.write(decrypted);
writer.close();
const decompressed = await new Response(decompressionStream.readable).arrayBuffer();
return JSON.parse(new TextDecoder().decode(decompressed));
} catch (error) {
console.warn(error);
return undefined;
}
}
/**
* Store a large value in an encrypted storage
* Does nothing if a secure context is not available (i.e. web).
* @param key Unique identifier of the resource in storage
* @param value The value to store
* @returns A promise that resolves when the value is stored
*/
async set<T>(key: string, value: T): Promise<void> {
if (!Capacitor.isNativePlatform()) return undefined;
try {
const compressionStream = new CompressionStream('gzip');
const writer = compressionStream.writable.getWriter();
writer.write(new TextEncoder().encode(JSON.stringify(value)));
writer.close();
const encoded = await new Response(compressionStream.readable).arrayBuffer();
const iv = crypto.getRandomValues(new Uint8Array(16));
const aesKey = await crypto.subtle.generateKey({name: 'AES-GCM', length: 256}, true, [
'encrypt',
'decrypt',
]);
await Promise.all([
SecureStoragePlugin.set({
key: `stapps:key:${key}`,
value: JSON.stringify(await crypto.subtle.exportKey('jwk', aesKey)),
}),
this.storageProvider.put(`encrypted:${key}:iv`, iv),
]);
this.storageProvider.put<ArrayBuffer>(
`encrypted:${key}`,
await crypto.subtle.encrypt({name: 'AES-GCM', iv}, aesKey, encoded),
);
} catch (error) {
alert(error);
}
}
async delete(key: string): Promise<void> {
if (!Capacitor.isNativePlatform()) return;
await Promise.all([
SecureStoragePlugin.remove({key: `stapps:key:${key}`}),
this.storageProvider.delete(`encrypted:${key}:iv`),
this.storageProvider.delete(`encrypted:${key}`),
]);
}
}

View File

@@ -1,5 +0,0 @@
export function matchTagProperties(tag: string): RegExp;
export function matchPropertyContent(properties: string[]): RegExp;
export function matchPropertyAccess(objectName: string): RegExp;

View File

@@ -336,11 +336,15 @@
"title": "Titel", "title": "Titel",
"about": "Mehr Informationen", "about": "Mehr Informationen",
"label": "Signatur", "label": "Signatur",
"starttime": "Vorgemerkt am",
"endtime": "Abzuholen bis", "endtime": "Abzuholen bis",
"storage": "Abholbereit" "storage": "Abholtheke",
"queue": "Position in der Warteschlange"
}, },
"holds": "Bestellungen", "holds": "Bestellungen",
"reservations": "Vormerkungen" "reservations": "Vormerkungen",
"ordered": "Bestellt",
"ready": "Abholbereit"
}, },
"checked_out": { "checked_out": {
"title": "Deine Ausleihen", "title": "Deine Ausleihen",
@@ -348,7 +352,7 @@
"title": "Titel", "title": "Titel",
"about": "Mehr Informationen", "about": "Mehr Informationen",
"label": "Signatur", "label": "Signatur",
"endtime": "Leihfristende", "duedate": "Leihfristende",
"renewals": "Verlängerungen" "renewals": "Verlängerungen"
} }
}, },

View File

@@ -336,11 +336,15 @@
"title": "Title", "title": "Title",
"about": "More information", "about": "More information",
"label": "Shelfmark", "label": "Shelfmark",
"starttime": "Reserved on",
"endtime": "Available for pickup until", "endtime": "Available for pickup until",
"storage": "Available for pickup" "storage": "Pickup counter",
"queue": "Position in the queue"
}, },
"holds": "orders", "holds": "orders",
"reservations": "reservations" "reservations": "reservations",
"ordered": "Ordered",
"ready": "Ready for pickup"
}, },
"checked_out": { "checked_out": {
"title": "checked out items", "title": "checked out items",
@@ -348,7 +352,7 @@
"title": "Title", "title": "Title",
"about": "More information", "about": "More information",
"label": "Label", "label": "Label",
"endtime": "Due date", "duedate": "Due date",
"renewals": "Renewals" "renewals": "Renewals"
} }
}, },

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -16,12 +16,13 @@
// The build system defaults to the dev environment which uses `environment.ts`, but if you do // The build system defaults to the dev environment which uses `environment.ts`, but if you do
// `ng build --env=prod` then `environment.production.ts` will be used instead. // `ng build --env=prod` then `environment.production.ts` will be used instead.
// The list of which env maps to which file can be found in `.angular-cli.json`. // The list of which env maps to which file can be found in `.angular-cli.json`.
import config from '../../config/default.json';
export const environment = { export const environment = {
backend_url: 'https://mobile.server.uni-frankfurt.de', backend_url: config.backendUrl,
app_host: 'mobile.app.uni-frankfurt.de', app_host: config.appLinkHost,
custom_url_scheme: 'de.anyschool.app', custom_url_scheme: config.appUrlScheme,
backend_version: '3.1.0', backend_version: config.backendVersion,
production: true, production: true,
}; };

View File

@@ -16,12 +16,13 @@
// The build system defaults to the dev environment which uses `environment.ts`, but if you do // The build system defaults to the dev environment which uses `environment.ts`, but if you do
// `ng build --env=prod` then `environment.production.ts` will be used instead. // `ng build --env=prod` then `environment.production.ts` will be used instead.
// The list of which env maps to which file can be found in `.angular-cli.json`. // The list of which env maps to which file can be found in `.angular-cli.json`.
import config from '../../config/default.json';
export const environment = { export const environment = {
backend_url: 'https://mobile.server.uni-frankfurt.de', backend_url: config.backendUrl,
app_host: 'mobile.app.uni-frankfurt.de', app_host: config.appLinkHost,
custom_url_scheme: 'de.anyschool.app', custom_url_scheme: config.appUrlScheme,
backend_version: '3.1.0', backend_version: config.backendVersion,
production: false, production: false,
}; };

View File

@@ -21,9 +21,9 @@
export DEBIAN_FRONTEND=noninteractive export DEBIAN_FRONTEND=noninteractive
SCRSUFFIX="_22.x" SCRSUFFIX="_18.x"
NODENAME="Node.js 22.x" NODENAME="Node.js 18.x"
NODEREPO="node_22.x" NODEREPO="node_18.x"
NODEPKG="nodejs" NODEPKG="nodejs"
print_status() { print_status() {
@@ -94,7 +94,7 @@ node_deprecation_warning() {
"X${NODENAME}" == "XNode.js 8.x LTS Carbon" || "X${NODENAME}" == "XNode.js 8.x LTS Carbon" ||
"X${NODENAME}" == "XNode.js 9.x" || "X${NODENAME}" == "XNode.js 9.x" ||
"X${NODENAME}" == "XNode.js 10.x" || "X${NODENAME}" == "XNode.js 10.x" ||
"X${NODENAME}" == "XNode.js 11.x" || "X${NODENAME}" == "XNode.js 11.x" ||
"X${NODENAME}" == "XNode.js 12.x" || "X${NODENAME}" == "XNode.js 12.x" ||
"X${NODENAME}" == "XNode.js 13.x" || "X${NODENAME}" == "XNode.js 13.x" ||
"X${NODENAME}" == "XNode.js 14.x" || "X${NODENAME}" == "XNode.js 14.x" ||

View File

@@ -1,5 +1,5 @@
### Set base image ### Set base image
FROM cypress/base:22.0.0 FROM cypress/base:18.16.1
USER root USER root

View File

@@ -1,4 +1,4 @@
FROM node:22-alpine FROM node:18-alpine3.18
RUN apk update && apk add git curl jq && mkdir -p /opt RUN apk update && apk add git curl jq && mkdir -p /opt

View File

@@ -1,4 +1,4 @@
FROM node:22-alpine FROM node:18-alpine3.18
RUN apk update && apk add git jq curl python3 build-base RUN apk update && apk add git jq curl python3 build-base

View File

@@ -2,8 +2,8 @@
"name": "@openstapps/openstapps", "name": "@openstapps/openstapps",
"private": true, "private": true,
"engines": { "engines": {
"node": "^22.0.0", "node": ">=18.16",
"pnpm": "^8.15.4" "pnpm": ">=8"
}, },
"scripts": { "scripts": {
"build": "dotenv -c -- turbo run build", "build": "dotenv -c -- turbo run build",
@@ -13,6 +13,7 @@
"deploy": "dotenv -c -- turbo run deploy --concurrency=1", "deploy": "dotenv -c -- turbo run deploy --concurrency=1",
"dev": "dotenv -c -- turbo run dev", "dev": "dotenv -c -- turbo run dev",
"docs": "dotenv -c -- turbo run docs && typedoc && mkdir docs/api && cp packages/core/lib/api-doc.html docs/api/index.html && cp packages/core/lib/openapi.json docs/api/openapi.json && cp -r packages/core/lib/schema docs/api/schema", "docs": "dotenv -c -- turbo run docs && typedoc && mkdir docs/api && cp packages/core/lib/api-doc.html docs/api/index.html && cp packages/core/lib/openapi.json docs/api/openapi.json && cp -r packages/core/lib/schema docs/api/schema",
"docs:serve": "http-server docs -p 8080 -o",
"format": "dotenv -c -- turbo run format", "format": "dotenv -c -- turbo run format",
"format:fix": "dotenv -c -- turbo run format:fix", "format:fix": "dotenv -c -- turbo run format:fix",
"lint": "dotenv -c -- turbo run lint", "lint": "dotenv -c -- turbo run lint",
@@ -34,6 +35,7 @@
"deepmerge": "4.3.1", "deepmerge": "4.3.1",
"dotenv-cli": "7.2.1", "dotenv-cli": "7.2.1",
"glob": "10.3.10", "glob": "10.3.10",
"http-server": "14.1.1",
"junit-report-merger": "6.0.3", "junit-report-merger": "6.0.3",
"prettier": "3.1.1", "prettier": "3.1.1",
"syncpack": "12.3.0", "syncpack": "12.3.0",

View File

@@ -0,0 +1,8 @@
{
"builders": {
"application": {
"implementation": "./lib/application.js",
"schema": "./node_modules/@angular-devkit/build-angular/src/builders/browser/schema.json"
}
}
}

View File

@@ -0,0 +1,72 @@
{
"name": "@openstapps/angular-builder",
"version": "3.2.0",
"type": "module",
"license": "GPL-3.0-only",
"author": "Thea Schöbl <dev@theaninova.de>",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"files": [
"lib",
"schemas",
"builders.json",
"README.md",
"CHANGELOG.md"
],
"builders": "builders.json",
"scripts": {
"build": "tsup-node --dts",
"docs": "typedoc --json ./docs/docs.json --options ../../typedoc.base.json src/index.ts",
"format": "prettier . -c --ignore-path ../../.gitignore",
"format:fix": "prettier --write . --ignore-path ../../.gitignore",
"lint": "eslint --ext .ts src/",
"lint:fix": "eslint --fix --ext .ts src/",
"test": "c8 mocha"
},
"dependencies": {
"@angular-devkit/architect": "0.1703.0",
"@angular-devkit/build-angular": "17.3.0",
"@angular-devkit/core": "17.3.0",
"cosmiconfig": "8.1.3",
"rxjs": "7.8.1",
"fontkit": "2.0.2",
"glob": "10.3.10"
},
"devDependencies": {
"@angular-devkit/schematics": "17.3.0",
"@openstapps/prettier-config": "workspace:*",
"@openstapps/tsconfig": "workspace:*",
"@types/fontkit": "2.0.7",
"@types/glob": "8.1.0",
"@types/node": "18.15.3",
"@types/chai": "4.3.5",
"@types/chai-as-promised": "7.1.5",
"@types/chai-spies": "1.0.3",
"@types/mocha": "10.0.1",
"c8": "7.14.0",
"chai": "4.3.7",
"chai-as-promised": "7.1.1",
"chai-spies": "1.0.0",
"mocha": "10.2.0",
"mocha-junit-reporter": "2.2.0",
"tsup": "6.7.0",
"ts-node": "10.9.2",
"typescript": "5.4.2"
},
"tsup": {
"entry": [
"src/application.ts",
"src/index.ts"
],
"sourcemap": true,
"clean": true,
"format": "esm",
"outDir": "lib"
},
"prettier": "@openstapps/prettier-config",
"eslintConfig": {
"extends": [
"@openstapps"
]
}
}

View File

@@ -0,0 +1,33 @@
/// <reference types="@angular-devkit/core" />
import {BuilderContext, BuilderOutput, createBuilder} from '@angular-devkit/architect';
import {BrowserBuilderOptions, executeBrowserBuilder} from '@angular-devkit/build-angular';
import {runPrebuild} from './shared.js';
import {from, of, mergeMap, Observable} from 'rxjs';
function applicationBuilder(
input: BrowserBuilderOptions,
context: BuilderContext,
): Observable<BuilderOutput> {
if (process.env.APP_VARIANT) {
input.fileReplacements ??= [];
input.fileReplacements.push(
{
replace: 'config/default.json',
with: `config/${process.env.APP_VARIANT}/default.json`,
},
{
replace: 'src/assets/imgs/logo.png',
with: `config/${process.env.APP_VARIANT}/logo.png`,
},
{
replace: 'src/assets/icon/favicon.png',
with: `config/${process.env.APP_VARIANT}/favicon.png`,
},
);
}
return from(runPrebuild(context)).pipe(
mergeMap(it => (it ? of(it) : executeBrowserBuilder(input, context, {}))),
);
}
export default createBuilder(applicationBuilder);

View File

@@ -0,0 +1,36 @@
/*
* 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 {Font, open} from 'fontkit';
import {existsSync} from 'fs';
import {getUsedIcons} from './gather-used-icons.js';
import {fetchCodePointMap} from './get-code-points.js';
import type {IconConfig} from '../index.js';
export async function checkIconCorrectness(config: IconConfig) {
if (!existsSync(config.outputPath)) {
throw new Error('Icons have not been generated');
}
const modifiedFont = (await open(config.outputPath)) as Font;
const codePoints = await fetchCodePointMap();
for (const icon of await getUsedIcons(config)) {
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`);
}
}
}

View File

@@ -14,16 +14,10 @@
*/ */
import {glob} from 'glob'; import {glob} from 'glob';
import {readFileSync} from 'fs'; import {readFileSync} from 'fs';
import { import {matchPropertyAccess, matchPropertyContent, matchTagProperties} from './icon-match.js';
matchPropertyAccess, import {IconConfig} from '../index.js';
matchPropertyContent,
matchTagProperties,
} from '../src/app/util/ion-icon/icon-match.mjs';
/** export async function getUsedIconsHtml(pattern = 'src/**/*.html'): Promise<Record<string, string[]>> {
* @returns {Promise<Record<string, string[]>>}
*/
export async function getUsedIconsHtml(pattern = 'src/**/*.html') {
return Object.fromEntries( return Object.fromEntries(
(await glob(pattern)) (await glob(pattern))
.map(file => [ .map(file => [
@@ -39,10 +33,7 @@ export async function getUsedIconsHtml(pattern = 'src/**/*.html') {
); );
} }
/** export async function getUsedIconsTS(pattern = 'src/**/*.ts'): Promise<Record<string, string[]>> {
* @returns {Promise<Record<string, string[]>>}
*/
export async function getUsedIconsTS(pattern = 'src/**/*.ts') {
const regex = matchPropertyAccess('SCIcon'); const regex = matchPropertyAccess('SCIcon');
return Object.fromEntries( return Object.fromEntries(
(await glob(pattern)) (await glob(pattern))
@@ -50,3 +41,15 @@ export async function getUsedIconsTS(pattern = 'src/**/*.ts') {
.filter(([, values]) => values && values.length > 0), .filter(([, values]) => values && values.length > 0),
); );
} }
export async function getUsedIcons(config: IconConfig) {
return new Set(
[
config.additionalIcons ?? {},
await getUsedIconsTS(config.scriptGlob),
await getUsedIconsHtml(config.htmlGlob),
]
.map(Object.values)
.flat(2),
);
}

View File

@@ -5,15 +5,12 @@ const url =
export async function fetchCodePointMap() { export async function fetchCodePointMap() {
const icons = await fetch(url) const icons = await fetch(url)
.then(it => it.text()) .then(it => it.text())
.then(it => new Map(it.split('\n').map(it => /** @type {[string, string]} */ (it.split(' '))))); .then(it => new Map(it.split('\n').map(it => it.split(' ') as [string, string])));
if (icons.size < 100) throw new Error(`Code point map is very small, is the URL incorrect? ${url}`); if (icons.size < 100) throw new Error(`Code point map is very small, is the URL incorrect? ${url}`);
return icons; return icons;
} }
/** export async function getCodePoints(icons: string[]) {
* @param {string[]} icons
*/
export async function getCodePoints(icons) {
const codePoints = await fetchCodePointMap(); const codePoints = await fetchCodePointMap();
return icons.map(icon => { return icons.map(icon => {
const code = codePoints.get(icon); const code = codePoints.get(icon);

View File

@@ -0,0 +1,11 @@
import {cosmiconfig} from 'cosmiconfig';
import type {IconConfig} from '../index.js';
export async function getIconConfig(): Promise<IconConfig> {
const explorer = cosmiconfig('icons');
const result = await explorer.search();
if (!result) {
throw new Error('No icon configuration found');
}
return result.config;
}

View File

@@ -13,21 +13,26 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
/* eslint-disable unicorn/no-null */ /* eslint-disable unicorn/no-null */
import {matchPropertyAccess, matchPropertyContent, matchTagProperties} from './icon-match.mjs'; import {expect} from 'chai';
import {matchPropertyAccess, matchPropertyContent, matchTagProperties} from './icon-match.js';
import {describe} from 'mocha';
describe('matchTagProperties', function () { describe('matchTagProperties', function () {
const regex = matchTagProperties('test'); const regex = matchTagProperties('test');
it('should match html tag content', function () { it('should match html tag content', function () {
expect('<test content></test>'.match(regex)).toEqual([' content']); expect('<test content></test>'.match(regex)).to.deep.equal([' content']);
}); });
it('should match all tags', function () { it('should match all tags', function () {
expect('<test content1></test> <test content2></test>'.match(regex)).toEqual([' content1', ' content2']); expect('<test content1></test> <test content2></test>'.match(regex)).to.deep.equal([
' content1',
' content2',
]);
}); });
it('should not match wrong tags', function () { it('should not match wrong tags', function () {
expect('<no content></no>'.match(regex)).toEqual(null); expect('<no content></no>'.match(regex)).to.deep.equal(null);
}); });
it('should accept valid html whitespaces', function () { it('should accept valid html whitespaces', function () {
@@ -39,7 +44,7 @@ describe('matchTagProperties', function () {
</test </test
> >
`.match(regex), `.match(regex),
).toEqual(['\n content\n ']); ).to.deep.equal(['\n content\n ']);
}); });
}); });
@@ -47,15 +52,15 @@ describe('matchPropertyContent', function () {
const regex = matchPropertyContent(['test1', 'test2']); const regex = matchPropertyContent(['test1', 'test2']);
it('should match bare literals', function () { it('should match bare literals', function () {
expect(`test1="content" test2="content1"`.match(regex)).toEqual(['content', 'content1']); expect(`test1="content" test2="content1"`.match(regex)).to.deep.equal(['content', 'content1']);
}); });
it('should match angular literals', function () { it('should match angular literals', function () {
expect(`[test1]="'content'" [test2]="'content1'"`.match(regex)).toEqual(['content', 'content1']); expect(`[test1]="'content'" [test2]="'content1'"`.match(regex)).to.deep.equal(['content', 'content1']);
}); });
it('should not match wrong literals', function () { it('should not match wrong literals', function () {
expect(`no="content" [no]="'content'"`.match(regex)).toEqual(null); expect(`no="content" [no]="'content'"`.match(regex)).to.deep.equal(null);
}); });
}); });
@@ -65,23 +70,23 @@ describe('matchPropertyAccess', function () {
const regex = matchPropertyAccess(object); const regex = matchPropertyAccess(object);
it('should match property access', function () { it('should match property access', function () {
expect(`${object}.${property}`.match(regex)).toEqual([property]); expect(`${object}.${property}`.match(regex)).to.deep.equal([property]);
}); });
it('should respect whitespace', function () { it('should respect whitespace', function () {
expect(`${object}. ${property}`.match(regex)).toEqual([property]); expect(`${object}. ${property}`.match(regex)).to.deep.equal([property]);
expect(`${object} .${property}`.match(regex)).toEqual([property]); expect(`${object} .${property}`.match(regex)).to.deep.equal([property]);
expect(`${object} . ${property}`.match(regex)).toEqual([property]); expect(`${object} . ${property}`.match(regex)).to.deep.equal([property]);
expect(`${object} \n . \n ${property}`.match(regex)).toEqual([property]); expect(`${object} \n . \n ${property}`.match(regex)).to.deep.equal([property]);
}); });
it('should not include invalid trailing stuff', function () { it('should not include invalid trailing stuff', function () {
expect(`${object}.${property}!`.match(regex)).toEqual([property]); expect(`${object}.${property}!`.match(regex)).to.deep.equal([property]);
expect(`${object}.${property}.`.match(regex)).toEqual([property]); expect(`${object}.${property}.`.match(regex)).to.deep.equal([property]);
expect(`${object}.${property}-`.match(regex)).toEqual([property]); expect(`${object}.${property}-`.match(regex)).to.deep.equal([property]);
expect(`${object}.${property}]`.match(regex)).toEqual([property]); expect(`${object}.${property}]`.match(regex)).to.deep.equal([property]);
expect(`${object}.${property}}`.match(regex)).toEqual([property]); expect(`${object}.${property}}`.match(regex)).to.deep.equal([property]);
expect(`${object}.${property};`.match(regex)).toEqual([property]); expect(`${object}.${property};`.match(regex)).to.deep.equal([property]);
expect(`${object}.${property}:`.match(regex)).toEqual([property]); expect(`${object}.${property}:`.match(regex)).to.deep.equal([property]);
}); });
}); });

View File

@@ -13,25 +13,16 @@
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
/** export function matchTagProperties(tag: string) {
* @param {string} tag
*/
export function matchTagProperties(tag) {
return new RegExp(`(?<=<${tag})[\\s\\S]*?(?=>\\s*<\\/${tag}\\s*>)`, 'g'); return new RegExp(`(?<=<${tag})[\\s\\S]*?(?=>\\s*<\\/${tag}\\s*>)`, 'g');
} }
/** export function matchPropertyContent(properties: string[]) {
* @param {string[]} properties
*/
export function matchPropertyContent(properties) {
const names = properties.join('|'); const names = properties.join('|');
return new RegExp(`((?<=(${names})=")[\\w-]+(?="))|((?<=\\[(${names})]="')[\\w-]+(?='"))`, 'g'); return new RegExp(`((?<=(${names})=")[\\w-]+(?="))|((?<=\\[(${names})]="')[\\w-]+(?='"))`, 'g');
} }
/** export function matchPropertyAccess(objectName: string) {
* @param {string} objectName
*/
export function matchPropertyAccess(objectName) {
return new RegExp(`(?<=${objectName}\\s*\\.\\s*)\\w+`, 'g'); return new RegExp(`(?<=${objectName}\\s*\\.\\s*)\\w+`, 'g');
} }

View File

@@ -1,5 +1,5 @@
/* /*
* Copyright (C) 2022 StApps * Copyright (C) 2024 StApps
* This program is free software: you can redistribute it and/or modify it * 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 * under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3. * Software Foundation, version 3.
@@ -20,3 +20,9 @@ export interface IconConfig {
outputPath: string; outputPath: string;
additionalIcons?: {[purpose: string]: string[]}; additionalIcons?: {[purpose: string]: string[]};
} }
export function defineIconConfig(config: IconConfig): IconConfig {
return config;
}
export * from './icons/icon-match.js';

View File

View File

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

View File

View File

@@ -0,0 +1,20 @@
import {BuilderContext, BuilderOutput} from '@angular-devkit/architect';
import {checkIconCorrectness} from './icons/check-icon-correctness.js';
import {getIconConfig} from './icons/icon-config.js';
export async function runPrebuild(context: BuilderContext): Promise<void | BuilderOutput> {
context.reportStatus('Checking icons');
if (!process.env.SKIP_ICON_CHECK) {
const iconConfig = await getIconConfig();
try {
await checkIconCorrectness(iconConfig);
} catch (error) {
return {
success: false,
error:
(error as Error).message + '\n\nTo skip this check, set the environment variable SKIP_ICON_CHECK=1',
};
}
}
context.reportStatus('✔ Icons are correct.');
}

View File

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

View File

@@ -1,12 +1,27 @@
# @openstapps/api-cli # @openstapps/api-cli
## 3.3.0
### Minor Changes
- 688bc5f2: v3.3.0 changes
### Patch Changes
- Updated dependencies [688bc5f2]
- @openstapps/api@3.3.0
- @openstapps/core@3.3.0
- @openstapps/eslint-config@3.0.0
- @openstapps/core-tools@3.3.0
- @openstapps/logger@3.0.0
## 3.2.0 ## 3.2.0
### Patch Changes ### Patch Changes
- Updated dependencies [912ae422] - Updated dependencies [912ae422]
- @openstapps/core@4.0.0 - @openstapps/core@3.2.0
- @openstapps/api@4.0.0 - @openstapps/api@3.2.0
- @openstapps/core-tools@3.0.0 - @openstapps/core-tools@3.0.0
- @openstapps/logger@3.0.0 - @openstapps/logger@3.0.0

View File

@@ -1,7 +1,7 @@
{ {
"name": "@openstapps/api-cli", "name": "@openstapps/api-cli",
"description": "CLI client for @openstapps/api", "description": "CLI client for @openstapps/api",
"version": "3.2.0", "version": "3.3.0",
"type": "module", "type": "module",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"repository": "git@gitlab.com:openstapps/api.git", "repository": "git@gitlab.com:openstapps/api.git",

View File

@@ -1,12 +1,22 @@
# @openstapps/api-plugin # @openstapps/api-plugin
## 3.3.0
### Patch Changes
- Updated dependencies [688bc5f2]
- @openstapps/api@3.3.0
- @openstapps/core@3.3.0
- @openstapps/core-tools@3.3.0
- @openstapps/logger@3.0.0
## 3.2.0 ## 3.2.0
### Patch Changes ### Patch Changes
- Updated dependencies [912ae422] - Updated dependencies [912ae422]
- @openstapps/core@4.0.0 - @openstapps/core@3.2.0
- @openstapps/api@4.0.0 - @openstapps/api@3.2.0
- @openstapps/core-tools@3.0.0 - @openstapps/core-tools@3.0.0
- @openstapps/logger@3.0.0 - @openstapps/logger@3.0.0

View File

@@ -1,7 +1,7 @@
{ {
"name": "@openstapps/api-plugin", "name": "@openstapps/api-plugin",
"description": "Node.js library to interact with the StApps backend service", "description": "Node.js library to interact with the StApps backend service",
"version": "3.2.0", "version": "3.3.0",
"type": "module", "type": "module",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"repository": "git@gitlab.com:openstapps/api.git", "repository": "git@gitlab.com:openstapps/api.git",

View File

@@ -1,11 +1,22 @@
# @openstapps/api # @openstapps/api
## 3.3.0
### Minor Changes
- 688bc5f2: v3.3.0 changes
### Patch Changes
- Updated dependencies [688bc5f2]
- @openstapps/core@3.3.0
## 3.2.0 ## 3.2.0
### Patch Changes ### Patch Changes
- Updated dependencies [912ae422] - Updated dependencies [912ae422]
- @openstapps/core@4.0.0 - @openstapps/core@3.2.0
## 3.1.1 ## 3.1.1

View File

@@ -1,7 +1,7 @@
{ {
"name": "@openstapps/api", "name": "@openstapps/api",
"description": "Node.js library to interact with the StApps backend service", "description": "Node.js library to interact with the StApps backend service",
"version": "3.2.0", "version": "3.3.0",
"type": "module", "type": "module",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"repository": "git@gitlab.com:openstapps/api.git", "repository": "git@gitlab.com:openstapps/api.git",

View File

@@ -1,5 +1,14 @@
# @openstapps/core-tools # @openstapps/core-tools
## 3.3.0
### Patch Changes
- Updated dependencies [688bc5f2]
- @openstapps/easy-ast@3.3.0
- @openstapps/collection-utils@3.0.0
- @openstapps/logger@3.0.0
## 3.0.0 ## 3.0.0
### Major Changes ### Major Changes

View File

@@ -1,7 +1,7 @@
{ {
"name": "@openstapps/core-tools", "name": "@openstapps/core-tools",
"description": "Tools to convert and validate StAppsCore", "description": "Tools to convert and validate StAppsCore",
"version": "3.0.0", "version": "3.3.0",
"type": "module", "type": "module",
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"repository": "git@gitlab.com:openstapps/core-tools.git", "repository": "git@gitlab.com:openstapps/core-tools.git",

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