mirror of
https://gitlab.com/openstapps/openstapps.git
synced 2026-01-08 22:42:54 +00:00
feat: app release notes
This commit is contained in:
@@ -22,6 +22,7 @@ const app = {
|
||||
name: 'Goethe-Uni',
|
||||
privacyPolicyUrl: 'https://mobile.server.uni-frankfurt.de/_static/privacy.md',
|
||||
settings: [userGroupSetting, languageSetting],
|
||||
versionHistory: [],
|
||||
};
|
||||
|
||||
export default app;
|
||||
|
||||
42
backend/backend/config/default/tools/version.js
Normal file
42
backend/backend/config/default/tools/version.js
Normal file
@@ -0,0 +1,42 @@
|
||||
// @ts-check
|
||||
import {readFile} from 'fs/promises';
|
||||
|
||||
/**
|
||||
* @example version(1, import.meta.url)
|
||||
* @param options {Omit<import('@openstapps/core').SCAppVersionInfo, 'releaseNotes' | 'translations'>}
|
||||
* @param base {string}
|
||||
* @returns {Promise<import('@openstapps/core').SCAppVersionInfo>}
|
||||
*/
|
||||
export async function version(options, base) {
|
||||
const de = await readFile(new URL(`${options.version}.de.md`, base), 'utf8');
|
||||
const en = await readFile(new URL(`${options.version}.en.md`, base), 'utf8');
|
||||
|
||||
return {
|
||||
...options,
|
||||
releaseNotes: de,
|
||||
translations: {
|
||||
en: {
|
||||
releaseNotes: en,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param infos {Record<string, import('@openstapps/core').SCAppVersionInfo['published']>}
|
||||
* @param base {string} Base path of the file as `import.meta.url`
|
||||
* @returns {Promise<import('@openstapps/core').SCAppVersionInfo[]>}
|
||||
*/
|
||||
export async function versions(infos, base) {
|
||||
return Promise.all(
|
||||
Object.entries(infos).map(([versionName, published]) =>
|
||||
version(
|
||||
{
|
||||
published,
|
||||
version: versionName,
|
||||
},
|
||||
base,
|
||||
),
|
||||
),
|
||||
).then(it => it.sort(({version: a}, {version: b}) => -a.localeCompare(b, undefined, {numeric: true})));
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
import aboutPages from './about-pages/index.js';
|
||||
import defaultApp from '../default/app/index.js';
|
||||
import {backend as defaultBackend, internal as defaultInternal} from '../default/backend/index.js';
|
||||
import versionHistory from './version-history/index.js';
|
||||
|
||||
/**
|
||||
* This is the default configuration for the Goethe university of Frankfurt
|
||||
@@ -76,6 +77,7 @@ const config = {
|
||||
} */
|
||||
},
|
||||
},
|
||||
versionHistory,
|
||||
aboutPages,
|
||||
},
|
||||
backend: defaultBackend,
|
||||
|
||||
52
backend/backend/config/f-u/version-history/3.1.0.de.md
Normal file
52
backend/backend/config/f-u/version-history/3.1.0.de.md
Normal file
@@ -0,0 +1,52 @@
|
||||
# Goethe-Uni App 3.1
|
||||
|
||||
Wir freuen uns euch mehr in der Goethe-Uni App
|
||||
bieten zu können.
|
||||
|
||||
## Navigation zu Gebäuden und Orten
|
||||
|
||||
Als eines der Ergebnisse des Ideenwettbewerbs wurde jetzt
|
||||
ein Navigationsfeature in die App integriert.
|
||||
|
||||
Orte auf der Karte, Mensen, sowie sogar Termine (wenn hinterlegt)
|
||||
bieten jetzt direkt die Option eine Verbindung zu finden, gestützt
|
||||
durch die Karten App auf deinem Gerät.
|
||||
|
||||
## Integration der Jobbörse
|
||||
|
||||
Jobs findest du ab sofort auch in der Goethe-Uni App.
|
||||
|
||||
Auch das ist ein Ergebnis des Ideenwettbewerbs,
|
||||
und wir freuen uns es euch hier präsentieren zu können!
|
||||
|
||||
## Der Umweltscore
|
||||
|
||||
Der Umweltscore für Gerichte wird nun auch in der App angezeigt.
|
||||
|
||||
> Nachhaltigkeit, Umweltschutz, Gesundheit und Klimawandel sind
|
||||
> zentrale Begriffe im gesellschaftlichen Miteinander.
|
||||
> Unsere Ernährung spielt hierbei eine wichtige Rolle.
|
||||
> Das Studierendenwerk Frankfurt am Main zeichnet seine Speisenpläne
|
||||
> ab sofort mit einem Umweltscore aus.
|
||||
> Anhand dieser Bewertung können Sie direkt ersehen,
|
||||
> welchen Einfluss Ihre Essenauswahl auf das Klima hat.
|
||||
|
||||
## Weitere Verbesserungen
|
||||
|
||||
### Performance
|
||||
|
||||
Die Performance der App beim Navigieren wurde stark verbessert und ist datensparender.
|
||||
|
||||
### Kalender
|
||||
|
||||
Die Kalenderabschnitte haben jetzt neue Namen bekommen:
|
||||
|
||||
- Der _Kalender_ zeigt Termine für spezifische Tage
|
||||
- Die _Wochenübersicht_ ist ein Stundenplan mit allen Termine, die sich wiederholen (z. B. Vorlesungen)
|
||||
- Die _Einzeltermine_ zeigen alle Termine, die sich nicht wiederholen
|
||||
(z. B. Klausuren)
|
||||
|
||||
### Meine App
|
||||
|
||||
Der "Meine Kurse" Abschnitt wurde überarbeitet, und zeigt jetzt Termine
|
||||
für die nächsten Tage und mit mehr Details an.
|
||||
49
backend/backend/config/f-u/version-history/3.1.0.en.md
Normal file
49
backend/backend/config/f-u/version-history/3.1.0.en.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Goethe-Uni App 3.1
|
||||
|
||||
The Goethe-Uni App got even better!
|
||||
|
||||
## Navigation to buildings and places
|
||||
|
||||
As part of the "Ideenwettbewerb," the idea competition,
|
||||
we have now integrated a navigation feature into the app.
|
||||
|
||||
Orte auf der Karte, Mensen, sowie sogar Termine (wenn hinterlegt)
|
||||
bieten jetzt direkt die Option eine Verbindung zu finden, gestützt
|
||||
durch die Karten App auf deinem Gerät.
|
||||
|
||||
## Integration of the job market
|
||||
|
||||
Jobs are now also available in the Goethe-Uni App.
|
||||
|
||||
This feature is also a result of the idea competition,
|
||||
and we're happy to be able to present it to you here!
|
||||
|
||||
## The environment score
|
||||
|
||||
The environment score for dishes is now displayed inside the app.
|
||||
|
||||
> Sustainability, environment protection, health, and climate change are
|
||||
> central topics in how we live today in our society.
|
||||
> Our eating habits play an important role in it.
|
||||
> The "Studierendenwerk Frankfurt am Main" is marking up its menus
|
||||
> from now on with the so-called "Umweltscore," the environment score.
|
||||
> Based on this rating, you can see the impact your meal choice would have on our climate.
|
||||
|
||||
## Further improvements
|
||||
|
||||
### Performance
|
||||
|
||||
The performance while navigating around the app has been heavily improved and requires less data to work.
|
||||
|
||||
### Calendar
|
||||
|
||||
The calendar sections have new names:
|
||||
|
||||
- The _calendar_ shows appointments on specific days
|
||||
- The _week overview_ is a schedule with all events that repeat (e.g. lectures)
|
||||
- The _single events_ show all appointments that don't repeat (e.g. exams)
|
||||
|
||||
### My App
|
||||
|
||||
The "my courses" section has been revamped,
|
||||
and now shows events for the next days and with more detail.
|
||||
12
backend/backend/config/f-u/version-history/index.js
Normal file
12
backend/backend/config/f-u/version-history/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
// @ts-check
|
||||
import {versions} from '../../default/tools/version.js';
|
||||
|
||||
/** @type {import('@openstapps/core').SCAppVersionInfo[]} */
|
||||
const versionHistory = await versions(
|
||||
{
|
||||
'3.1.0': {},
|
||||
},
|
||||
import.meta.url,
|
||||
);
|
||||
|
||||
export default versionHistory;
|
||||
@@ -17,7 +17,7 @@
|
||||
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||
import {TestBed} from '@angular/core/testing';
|
||||
|
||||
import {Platform} from '@ionic/angular';
|
||||
import {ModalController, Platform} from '@ionic/angular';
|
||||
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {ThingTranslateService} from './translation/thing-translate.service';
|
||||
@@ -45,6 +45,7 @@ describe('AppComponent', () => {
|
||||
let platformIsSpy;
|
||||
let storageProvider: jasmine.SpyObj<StorageProvider>;
|
||||
let simpleBrowser: jasmine.SpyObj<SimpleBrowser>;
|
||||
let modalController: jasmine.SpyObj<ModalController>;
|
||||
|
||||
beforeEach(() => {
|
||||
platformReadySpy = Promise.resolve();
|
||||
@@ -71,6 +72,7 @@ describe('AppComponent', () => {
|
||||
ngxLogger = jasmine.createSpyObj('NGXLogger', ['log', 'error', 'warn']);
|
||||
storageProvider = jasmine.createSpyObj('StorageProvider', ['init', 'get', 'has', 'put']);
|
||||
simpleBrowser = jasmine.createSpyObj('SimpleBrowser', ['open']);
|
||||
modalController = jasmine.createSpyObj('ModalController', ['create', 'dismiss', 'getTop']);
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [RouterTestingModule.withRoutes([]), HttpClientTestingModule, AuthModule],
|
||||
@@ -85,6 +87,7 @@ describe('AppComponent', () => {
|
||||
{provide: NGXLogger, useValue: ngxLogger},
|
||||
{provide: StorageProvider, useValue: storageProvider},
|
||||
{provide: SimpleBrowser, useValue: simpleBrowser},
|
||||
{provide: ModalController, useValue: modalController},
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||
}).compileComponents();
|
||||
|
||||
@@ -22,6 +22,7 @@ import {environment} from '../environments/environment';
|
||||
import {Capacitor} from '@capacitor/core';
|
||||
import {ScheduleSyncService} from './modules/background/schedule/schedule-sync.service';
|
||||
import {Keyboard, KeyboardResize} from '@capacitor/keyboard';
|
||||
import {AppVersionService} from './modules/about/app-version.service';
|
||||
import {SplashScreen} from '@capacitor/splash-screen';
|
||||
|
||||
/**
|
||||
@@ -59,13 +60,19 @@ export class AppComponent implements AfterContentInit {
|
||||
private readonly authHelper: AuthHelperService,
|
||||
private readonly toastController: ToastController,
|
||||
private readonly scheduleSyncService: ScheduleSyncService,
|
||||
private readonly versionService: AppVersionService,
|
||||
) {
|
||||
void this.initializeApp();
|
||||
}
|
||||
|
||||
ngAfterContentInit() {
|
||||
async ngAfterContentInit() {
|
||||
this.scheduleSyncService.init();
|
||||
void this.scheduleSyncService.enable();
|
||||
this.versionService.getPendingReleaseNotes().then(notes => {
|
||||
if (notes) {
|
||||
this.versionService.presentReleaseNotes(notes);
|
||||
}
|
||||
});
|
||||
|
||||
if (document.readyState === 'complete') {
|
||||
requestIdleCallback(this.hideSplash.bind(this));
|
||||
|
||||
@@ -18,6 +18,8 @@ import {SCAboutPage, SCAppConfiguration} from '@openstapps/core';
|
||||
import {ConfigProvider} from '../../config/config.provider';
|
||||
import packageJson from '../../../../../package.json';
|
||||
import config from 'capacitor.config';
|
||||
import {App} from '@capacitor/app';
|
||||
import {Capacitor} from '@capacitor/core';
|
||||
|
||||
@Component({
|
||||
selector: 'about-page',
|
||||
@@ -27,9 +29,11 @@ import config from 'capacitor.config';
|
||||
export class AboutPageComponent implements OnInit {
|
||||
content: SCAboutPage;
|
||||
|
||||
appName = config.appName;
|
||||
name = config.appName;
|
||||
|
||||
version = packageJson.version;
|
||||
stappsVersion = packageJson.version;
|
||||
|
||||
version: string;
|
||||
|
||||
constructor(private readonly route: ActivatedRoute, private readonly configProvider: ConfigProvider) {}
|
||||
|
||||
@@ -37,5 +41,6 @@ export class AboutPageComponent implements OnInit {
|
||||
const route = this.route.snapshot.url.map(it => it.path).join('/');
|
||||
this.content =
|
||||
(this.configProvider.getValue('aboutPages') as SCAppConfiguration['aboutPages'])[route] ?? {};
|
||||
this.version = Capacitor.getPlatform() === 'web' ? 'Web' : await App.getInfo().then(info => info.version);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content parallax *ngIf="content">
|
||||
<ion-text>{{ appName }} v{{ version }}</ion-text>
|
||||
<ion-text>{{ 'about.VERSION_INFO' | translate: {name, version, stappsVersion} }}</ion-text>
|
||||
<div class="page-content">
|
||||
<about-page-content *ngFor="let element of content.content" [content]="element"></about-page-content>
|
||||
</div>
|
||||
|
||||
60
frontend/app/src/app/modules/about/app-version.service.ts
Normal file
60
frontend/app/src/app/modules/about/app-version.service.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {StorageProvider} from '../storage/storage.provider';
|
||||
import {ConfigProvider} from '../config/config.provider';
|
||||
import {ModalController} from '@ionic/angular';
|
||||
import {Capacitor} from '@capacitor/core';
|
||||
import {ReleaseNotesComponent} from './release-notes.component';
|
||||
import {SCAppVersionInfo} from '@openstapps/core';
|
||||
import {App} from '@capacitor/app';
|
||||
|
||||
export const RELEASE_NOTES_SHOWN_KEY = 'release_notes_shown';
|
||||
|
||||
@Injectable({providedIn: 'root'})
|
||||
export class AppVersionService {
|
||||
constructor(
|
||||
private storage: StorageProvider,
|
||||
private config: ConfigProvider,
|
||||
private modalController: ModalController,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Get the release notes of the latest published version
|
||||
*/
|
||||
get publishedVersions() {
|
||||
const platform = Capacitor.getPlatform() as 'android' | 'ios' | 'web';
|
||||
return this.config.config.app.versionHistory?.filter(({published}) => published[platform] !== undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest release notes that have not been presented yet
|
||||
*/
|
||||
async getPendingReleaseNotes() {
|
||||
if (Capacitor.getPlatform() === 'web') {
|
||||
return;
|
||||
}
|
||||
const storedVersion = (await this.storage.has(RELEASE_NOTES_SHOWN_KEY))
|
||||
? await this.storage.get<string>(RELEASE_NOTES_SHOWN_KEY)
|
||||
: '';
|
||||
const currentVersion = await App.getInfo().then(info => info.version);
|
||||
return this.publishedVersions?.find(({version}) => {
|
||||
const wasNotShown = version.localeCompare(storedVersion, undefined, {numeric: true}) === 1;
|
||||
const isNotFutureVersion = version.localeCompare(currentVersion, undefined, {numeric: true}) <= 0;
|
||||
return wasNotShown && isNotFutureVersion;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Present release notes
|
||||
*/
|
||||
async presentReleaseNotes(version: SCAppVersionInfo) {
|
||||
const modal = await this.modalController.create({
|
||||
component: ReleaseNotesComponent,
|
||||
componentProps: {
|
||||
versionInfo: version,
|
||||
},
|
||||
});
|
||||
await modal.present();
|
||||
await modal.onDidDismiss();
|
||||
await this.storage.put(RELEASE_NOTES_SHOWN_KEY, version.version);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
|
||||
import {SCAppVersionInfo} from '@openstapps/core';
|
||||
import {MarkdownModule} from 'ngx-markdown';
|
||||
import {ThingTranslateModule} from '../../translation/thing-translate.module';
|
||||
import {IonicModule, ModalController} from '@ionic/angular';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {UtilModule} from '../../util/util.module';
|
||||
|
||||
@Component({
|
||||
selector: 'stapps-release-notes',
|
||||
templateUrl: 'release-notes.html',
|
||||
styleUrls: ['release-notes.scss'],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [UtilModule, MarkdownModule, ThingTranslateModule, IonicModule, TranslateModule],
|
||||
})
|
||||
export class ReleaseNotesComponent {
|
||||
@Input() versionInfo: SCAppVersionInfo;
|
||||
|
||||
constructor(readonly modalController: ModalController) {}
|
||||
}
|
||||
16
frontend/app/src/app/modules/about/release-notes.html
Normal file
16
frontend/app/src/app/modules/about/release-notes.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title><span class="ion-text-wrap">{{'releaseNotes.TITLE_UPDATED' | translate}}</span></ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button [strong]="true" (click)="modalController.dismiss()"
|
||||
>{{'modal.DISMISS_NEUTRAL' | translate}}</ion-button
|
||||
>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
<ion-content parallax>
|
||||
<markdown
|
||||
class="content-card ion-padding"
|
||||
[data]="'releaseNotes' | translateSimple: versionInfo"
|
||||
></markdown>
|
||||
</ion-content>
|
||||
13
frontend/app/src/app/modules/about/release-notes.scss
Normal file
13
frontend/app/src/app/modules/about/release-notes.scss
Normal file
@@ -0,0 +1,13 @@
|
||||
@import '../../../theme/util/mixins';
|
||||
|
||||
ion-title {
|
||||
padding-block: var(--spacing-md);
|
||||
}
|
||||
|
||||
.content-card {
|
||||
@include border-radius-in-parallax(var(--border-radius-default));
|
||||
|
||||
display: block;
|
||||
margin: var(--spacing-md);
|
||||
background: var(--ion-item-background);
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import {DaytimeKeyPipe} from './daytime-key.pipe';
|
||||
import {LazyPipe} from './lazy.pipe';
|
||||
import {NextDateInListPipe} from './next-date-in-list.pipe';
|
||||
import {EditModalComponent} from './edit-modal.component';
|
||||
import {BrowserModule} from '@angular/platform-browser';
|
||||
import {IonicModule} from '@ionic/angular';
|
||||
import {TranslateModule} from '@ngx-translate/core';
|
||||
import {ElementSizeChangeDirective} from './element-size-change.directive';
|
||||
@@ -33,10 +32,11 @@ import {SectionComponent} from './section.component';
|
||||
import {RouterModule} from '@angular/router';
|
||||
import {IonContentParallaxDirective} from './ion-content-parallax.directive';
|
||||
import {FormatDistanceToNowStrictPipeModule, FormatRelativeToNowPipeModule} from 'ngx-date-fns';
|
||||
import {CommonModule} from '@angular/common';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule,
|
||||
ThingTranslateModule.forChild(),
|
||||
|
||||
@@ -28,6 +28,12 @@
|
||||
"toast": {
|
||||
"TITLE_COPIED": "In die Zwischenablage kopiert"
|
||||
},
|
||||
"about": {
|
||||
"VERSION_INFO": "{{name}} {{version}} basierend auf StApps {{stappsVersion}}"
|
||||
},
|
||||
"releaseNotes": {
|
||||
"TITLE_UPDATED": "Deine App wurde aktualisiert!"
|
||||
},
|
||||
"app": {
|
||||
"ui": {
|
||||
"CLOSE": "Schließen",
|
||||
|
||||
@@ -28,6 +28,12 @@
|
||||
"toast": {
|
||||
"TITLE_COPIED": "Copied to clipboard"
|
||||
},
|
||||
"about": {
|
||||
"VERSION_INFO": "{{name}} {{version}} based on StApps {{stappsVersion}}"
|
||||
},
|
||||
"releaseNotes": {
|
||||
"TITLE_UPDATED": "Your app was updated!"
|
||||
},
|
||||
"app": {
|
||||
"ui": {
|
||||
"CLOSE": "Close",
|
||||
|
||||
@@ -18,6 +18,7 @@ import {SCMap} from '../general/map.js';
|
||||
import {SCLanguageSetting, SCSetting, SCUserGroupSetting} from '../things/setting.js';
|
||||
import {SCAuthorizationProviderType} from './authorization.js';
|
||||
import {SCFeatureConfiguration} from './feature.js';
|
||||
import {SCISO8601Date} from '../general/time.js';
|
||||
|
||||
/**
|
||||
* An app configuration menu item
|
||||
@@ -136,6 +137,38 @@ export interface SCAppConfiguration {
|
||||
* URL where a web instance of the app is available
|
||||
*/
|
||||
url?: string;
|
||||
|
||||
/**
|
||||
* Version history in sequence of their publishing date.
|
||||
*
|
||||
* The items are ordered in descending version index order,
|
||||
* where the first item is always the most recent release.
|
||||
*/
|
||||
versionHistory?: SCAppVersionInfo[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Info about app version releases
|
||||
*/
|
||||
export interface SCAppVersionInfo {
|
||||
/**
|
||||
* Published date for each platform.
|
||||
*
|
||||
* Missing entries mean the version has not been published on that platform yet.
|
||||
*/
|
||||
published: Partial<Record<'web' | 'android' | 'ios', SCISO8601Date>>;
|
||||
/**
|
||||
* Version index that increments by one each version
|
||||
*/
|
||||
version: string;
|
||||
/**
|
||||
* Release notes of the version
|
||||
*/
|
||||
releaseNotes: string;
|
||||
/**
|
||||
* Translations
|
||||
*/
|
||||
translations: SCTranslations<{releaseNotes: string}>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user