Compare commits

...

1 Commits

Author SHA1 Message Date
Rainer Killinger
1214b31cfc refactor: overhaul minimal-deployment compose file
fix: changeset crashes because it uses internal prettier version

refactor: add asdf tool versioning file

fix: iOS build resources

fix: backend tests break every year

refactor: update some backend unit tests

feat: add direnv for nix

feat: update nix flake to not rely on buildFHSUserEnv

feat: enable checkJs by default

feat: custom ion-icon package

feat: custom ion-icon package

feat: custom ion-icon element

feat: custom ion-icon element

feat: custom ion-icon element

feat: custom ion icon element

fix: e2e tests
2024-04-16 13:19:01 +02:00
69 changed files with 312 additions and 781 deletions

1
.gitignore vendored
View File

@@ -102,6 +102,7 @@ typings/
# ignore lib
lib
dist
# ignore docs
docs

View File

@@ -11,6 +11,7 @@ log.txt
*.sublime-workspace
.vscode/*
npm-debug.log*
.browser-data
# This file is sometimes created automatically, even though
# we actually use the capacitor.config.ts

View File

@@ -21,10 +21,10 @@ as usual.
The modified `ion-icon` comes with a few extra features:
- `[fill]` controls the fill color of the icon.
- `[weight]` controls the font weight of the icon.
- `[size]` controls the font size of the icon.
- `[grade]` controls the font grade of the icon.
- `[style.--fill]` controls the fill color of the icon.
- `[style.--weight]` controls the font weight of the icon.
- `[style.--size]` controls the font size of the icon.
- `[style.--grade]` controls the font grade of the icon.
All of these attributes are animated as described
[here](https://developers.google.com/fonts/docs/material_symbols).
@@ -50,11 +50,10 @@ the config file.
Icon font minification is done automatically, but requires you to
follow a few simple rules:
1. Use the tagged template literal for referencing icon names in
TypeScript files and code
1. Use the Proxy object to reference icon names in TypeScript files and code
```ts
SCIcon`icon_name`;
SCIcon.icon_name;
```
2. When using `ion-icon` in HTML, reference either icons that went through

View File

@@ -16,7 +16,7 @@
describe('favorites', function () {
beforeEach(() => {
cy.interceptSearch({
extends: {query: 'test'},
extends: {query: 'a'},
fixture: 'search/generic',
alias: 'search',
});
@@ -29,7 +29,7 @@ describe('favorites', function () {
it('should add a favorite', function () {
cy.visit('/search');
cy.patchSearchPage();
cy.get('ion-searchbar').type('test');
cy.get('ion-searchbar').type('a');
let text!: string;
cy.get('stapps-data-list-item')
.first()
@@ -40,9 +40,7 @@ describe('favorites', function () {
text = it;
});
cy.get('stapps-favorite-button').click();
cy.get('stapps-favorite-button > ion-button > ion-icon')
.invoke('attr', 'ng-reflect-fill')
.should('eq', 'true');
cy.get('stapps-favorite-button > ion-button > ion-icon').should('have.class', 'selected');
});
cy.visit('/favorites');
cy.get('stapps-data-list-item').within(() => {

View File

@@ -96,6 +96,7 @@
"form-data": "4.0.0",
"geojson": "0.5.0",
"ionic-appauth": "0.9.0",
"ionicons": "7.2.1",
"jsonpath-plus": "6.0.1",
"leaflet": "1.9.4",
"leaflet.markercluster": "1.5.3",

View File

@@ -25,7 +25,6 @@ import moment from 'moment';
import 'moment/min/locales';
import {LoggerModule, NGXLogger, NgxLoggerLevel} from 'ngx-logger';
import SwiperCore, {FreeMode, Navigation} from 'swiper';
import {environment} from '../environments/environment';
import {AppRoutingModule} from './app-routing.module';
import {AppComponent} from './app.component';
@@ -62,12 +61,12 @@ import {RoutingStackService} from './util/routing-stack.service';
import {SCLanguageCode, SCSettingValue} from '@openstapps/core';
import {DefaultAuthService} from './modules/auth/default-auth.service';
import {PAIAAuthService} from './modules/auth/paia/paia-auth.service';
import {IonIconModule} from './util/ion-icon/ion-icon.module';
import {NavigationModule} from './modules/menu/navigation/navigation.module';
import {browserFactory, SimpleBrowser} from './util/browser.factory';
import {getDateFnsLocale} from './translation/dfns-locale';
import {setDefaultOptions} from 'date-fns';
import {DateFnsConfigurationService} from 'ngx-date-fns';
import {SCIcon} from './util/ion-icon/icon';
import {Capacitor} from '@capacitor/core';
import {SplashScreen} from '@capacitor/splash-screen';
@@ -155,8 +154,9 @@ export function createTranslateLoader(http: HttpClient) {
DashboardModule,
DataModule,
HebisModule,
IonicModule.forRoot(),
IonIconModule,
IonicModule.forRoot({
backButtonIcon: SCIcon.arrow_back,
}),
JobModule,
FavoritesModule,
LibraryModule,

View File

@@ -29,7 +29,12 @@
<ion-card-header>
<ion-card-title>
{{ license.name }}
<ion-icon [size]="16" [weight]="300" class="supertext-icon" name="open_in_browser"></ion-icon>
<ion-icon
[size]="16"
[style.--weight]="300"
class="supertext-icon"
name="open_in_browser"
></ion-icon>
</ion-card-title>
@if (license.authors || license.publisher) {
<ion-card-subtitle> {{ license.authors || license.publisher }} </ion-card-subtitle>

View File

@@ -29,7 +29,6 @@ import {ScrollingModule} from '@angular/cdk/scrolling';
import {AboutLicenseModalComponent} from './about-license-modal.component';
import {AboutChangelogComponent} from './about-changelog.component';
import {UtilModule} from '../../util/util.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
const settingsRoutes: Routes = [
{path: 'about', component: AboutPageComponent},
@@ -53,9 +52,8 @@ const settingsRoutes: Routes = [
],
imports: [
CommonModule,
IonIconModule,
FormsModule,
IonicModule.forRoot(),
IonicModule,
TranslateModule.forChild(),
ThingTranslateModule.forChild(),
RouterModule.forChild(settingsRoutes),

View File

@@ -35,7 +35,6 @@ import {AssessmentsProvider} from './assessments.provider';
import {AssessmentsSimpleDataListComponent} from './list/assessments-simple-data-list.component';
import {ProtectedRoutes} from '../auth/protected.routes';
import {AssessmentsTreeListComponent} from './list/assessments-tree-list.component';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
import {UtilModule} from '../../util/util.module';
const routes: ProtectedRoutes = [
@@ -69,7 +68,6 @@ const routes: ProtectedRoutes = [
imports: [
CommonModule,
FormsModule,
IonIconModule,
IonicModule,
RouterModule.forChild(routes),
TranslateModule,

View File

@@ -67,7 +67,7 @@
<div class="horizontal-flex">
<ion-button fill="clear" (click)="export()">
{{ 'share' | translate }}
<ion-icon slot="end" md="share" ios="ios_share"></ion-icon>
<ion-icon slot="end" name="share"></ion-icon>
</ion-button>
@if (isWeb) {
<ion-button fill="outline" (click)="download()">

View File

@@ -25,15 +25,13 @@ import {FormsModule} from '@angular/forms';
import {CommonModule} from '@angular/common';
import {MomentModule} from 'ngx-moment';
import {UtilModule} from '../../util/util.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
@NgModule({
declarations: [AddEventReviewModalComponent],
imports: [
IonicModule.forRoot(),
IonicModule,
TranslateModule.forChild(),
ThingTranslateModule.forChild(),
IonIconModule,
FormsModule,
CommonModule,
MomentModule,

View File

@@ -23,7 +23,6 @@ import {DataModule} from '../data/data.module';
import {SettingsProvider} from '../settings/settings.provider';
import {CatalogComponent} from './catalog.component';
import {UtilModule} from '../../util/util.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
const catalogRoutes: Routes = [
{path: 'catalog', component: CatalogComponent},
@@ -36,11 +35,10 @@ const catalogRoutes: Routes = [
@NgModule({
declarations: [CatalogComponent],
imports: [
IonicModule.forRoot(),
IonicModule,
FormsModule,
TranslateModule.forChild(),
RouterModule.forChild(catalogRoutes),
IonIconModule,
CommonModule,
MomentModule,
DataModule,

View File

@@ -20,7 +20,7 @@
</ion-header>
<div #schedule class="schedule">
<a [routerLink]="['/schedule/week-overview']">
<ion-icon [size]="36" [weight]="300" name="calendar_month"></ion-icon>
<ion-icon [size]="36" [style.--weight]="300" name="calendar_month"></ion-icon>
<ion-label [innerHTML]="'schedule.recurring' | translate"></ion-label>
</a>
<!-- Avoid structural directives here, they might interfere with the collapse animation -->

View File

@@ -30,7 +30,6 @@ import {MensaSectionContentComponent} from './sections/mensa-section/mensa-secti
import {FavoritesSectionComponent} from './sections/favorites-section/favorites-section.component';
import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {UtilModule} from '../../util/util.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
import {NewsModule} from '../news/news.module';
import {JobSectionComponent} from './sections/jobs-section/job-section.component';
import {JobModule} from '../jobs/jobs.module';
@@ -56,8 +55,7 @@ const catalogRoutes: Routes = [
JobSectionComponent,
],
imports: [
IonicModule.forRoot(),
IonIconModule,
IonicModule,
FormsModule,
TranslateModule.forChild(),
RouterModule.forChild(catalogRoutes),

View File

@@ -60,7 +60,7 @@ export class AddEventActionChipComponent {
/**
* Current state of icon fill
*/
iconFill: boolean;
iconFill: 1 | 0;
/**
* Label

View File

@@ -24,31 +24,31 @@ export enum AddEventStates {
export const AddEventStatesMap = {
[AddEventStates.ADDED_ALL]: {
icon: SCIcon`event_available`,
fill: true,
icon: SCIcon.event_available,
fill: 1,
label: 'data.chips.add_events.ADDED_ALL',
disabled: false,
color: 'success',
},
[AddEventStates.ADDED_SOME]: {
icon: SCIcon`event`,
fill: true,
icon: SCIcon.event,
fill: 1,
label: 'data.chips.add_events.ADDED_SOME',
disabled: false,
color: 'success',
},
[AddEventStates.REMOVED_ALL]: {
icon: SCIcon`calendar_today`,
fill: false,
icon: SCIcon.calendar_today,
fill: 0,
label: 'data.chips.add_events.REMOVED_ALL',
disabled: false,
color: 'primary',
},
[AddEventStates.UNAVAILABLE]: {
icon: SCIcon`event_busy`,
fill: false,
icon: SCIcon.event_busy,
fill: 0,
label: 'data.chips.add_events.UNAVAILABLE',
disabled: true,
color: 'dark',
},
};
} as const;

View File

@@ -22,7 +22,7 @@
[color]="color"
[outline]="true"
>
<ion-icon [name]="icon" [fill]="iconFill"></ion-icon>
<ion-icon [name]="icon" [style.--fill]="iconFill"></ion-icon>
<ion-label>{{ label | translate }}</ion-label>
<stapps-edit-modal #editModal (save)="selection.save()">
<ng-template>

View File

@@ -16,35 +16,35 @@ import {SCThingType} from '@openstapps/core';
import {SCIcon} from '../../util/ion-icon/icon';
export const DataIcons: Record<SCThingType, string> = {
'academic event': SCIcon`school`,
'assessment': SCIcon`fact_check`,
'article': SCIcon`article`,
'book': SCIcon`book`,
'building': SCIcon`location_city`,
'certification': SCIcon`contract`,
'catalog': SCIcon`inventory_2`,
'contact point': SCIcon`contact_page`,
'course of study': SCIcon`school`,
'date series': SCIcon`event`,
'dish': SCIcon`lunch_dining`,
'favorite': SCIcon`favorite`,
'floor': SCIcon`foundation`,
'id card': SCIcon`badge`,
'message': SCIcon`newspaper`,
'organization': SCIcon`business_center`,
'periodical': SCIcon`feed`,
'person': SCIcon`person`,
'point of interest': SCIcon`pin_drop`,
'publication event': SCIcon`campaign`,
'room': SCIcon`meeting_room`,
'semester': SCIcon`date_range`,
'setting': SCIcon`settings`,
'sport course': SCIcon`sports_soccer`,
'study module': SCIcon`view_module`,
'ticket': SCIcon`confirmation_number`,
'todo': SCIcon`task`,
'tour': SCIcon`tour`,
'video': SCIcon`movie`,
'diff': SCIcon`difference`,
'job posting': SCIcon`work`,
'academic event': SCIcon.school,
'assessment': SCIcon.fact_check,
'article': SCIcon.article,
'book': SCIcon.book,
'building': SCIcon.location_city,
'certification': SCIcon.contract,
'catalog': SCIcon.inventory_2,
'contact point': SCIcon.contact_page,
'course of study': SCIcon.school,
'date series': SCIcon.event,
'dish': SCIcon.lunch_dining,
'favorite': SCIcon.favorite,
'floor': SCIcon.foundation,
'id card': SCIcon.badge,
'message': SCIcon.newspaper,
'organization': SCIcon.business_center,
'periodical': SCIcon.feed,
'person': SCIcon.person,
'point of interest': SCIcon.pin_drop,
'publication event': SCIcon.campaign,
'room': SCIcon.meeting_room,
'semester': SCIcon.date_range,
'setting': SCIcon.settings,
'sport course': SCIcon.sports_soccer,
'study module': SCIcon.view_module,
'ticket': SCIcon.confirmation_number,
'todo': SCIcon.task,
'tour': SCIcon.tour,
'video': SCIcon.movie,
'diff': SCIcon.difference,
'job posting': SCIcon.work,
};

View File

@@ -24,7 +24,6 @@ import {MarkdownModule} from 'ngx-markdown';
import {MomentModule} from 'ngx-moment';
import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {SimpleBrowser, browserFactory} from '../../util/browser.factory';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
import {RoutingStackService} from '../../util/routing-stack.service';
import {UtilModule} from '../../util/util.module';
import {CalendarService} from '../calendar/calendar.service';
@@ -188,11 +187,10 @@ import {ShareButtonComponent} from './elements/share-button.component';
DataRoutingModule,
FormsModule,
HttpClientModule,
IonicModule.forRoot(),
IonicModule,
LeafletModule,
MarkdownModule.forRoot(),
MenuModule,
IonIconModule,
MomentModule.forRoot({
relativeTimeThresholdOptions: {
m: 59,

View File

@@ -16,7 +16,7 @@
<ion-button (click)="toggle($event)" color="medium" size="small" fill="clear">
<ion-icon
slot="icon-only"
[fill]="(isFavorite$ | async) ?? false"
[style.--fill]="(isFavorite$ | async) ? 1 : 0"
[class.selected]="isFavorite$ | async"
name="grade"
></ion-icon>

View File

@@ -1,4 +1,4 @@
<ion-button size="small" fill="clear" (click)="share() && toast.present()">
<ion-icon [size]="24" slot="icon-only" name="share" ios="ios_share"></ion-icon>
<ion-icon [size]="24" slot="icon-only" name="share"></ion-icon>
</ion-button>
<ion-toast [message]="'toast.TITLE_COPIED' | translate" #toast [duration]="2000"></ion-toast>

View File

@@ -16,10 +16,11 @@
import {Component, ElementRef, HostListener, Input, OnChanges, OnInit, ViewChild} from '@angular/core';
import {SCThings} from '@openstapps/core';
import {SCIcon} from '../../../util/ion-icon/icon';
import {MaterialSymbol} from 'material-symbols';
const AccordionButtonState = {
collapsed: SCIcon`expand_more`,
expanded: SCIcon`expand_less`,
collapsed: SCIcon.expand_more,
expanded: SCIcon.expand_less,
};
@Component({
@@ -35,7 +36,7 @@ export class TitleCardComponent implements OnInit, OnChanges {
@ViewChild('accordionTextArea') accordionTextArea: ElementRef;
buttonState = AccordionButtonState.collapsed;
buttonState: MaterialSymbol = AccordionButtonState.collapsed;
buttonShown = true;

View File

@@ -14,7 +14,7 @@
-->
@if (isInCalendar | async) {
<ion-chip outline="true" color="success" (click)="removeFromCalendar()">
<ion-icon name="event_available" [fill]="true"></ion-icon>
<ion-icon name="event_available" [style.--fill]="1"></ion-icon>
<ion-label>{{ 'chips.addEvent.addedToEvents' | translate }}</ion-label>
</ion-chip>
} @else {

View File

@@ -22,7 +22,6 @@ import {MenuModule} from '../menu/menu.module';
import {TranslateModule} from '@ngx-translate/core';
import {DataModule} from '../data/data.module';
import {UtilModule} from '../../util/util.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
const favoritesRoutes: Routes = [
{
@@ -40,7 +39,6 @@ const favoritesRoutes: Routes = [
MenuModule,
TranslateModule,
DataModule,
IonIconModule,
UtilModule,
],
declarations: [FavoritesPageComponent],

View File

@@ -21,7 +21,6 @@ import {RouterModule, Routes} from '@angular/router';
import {TranslateModule} from '@ngx-translate/core';
import {MarkdownModule} from 'ngx-markdown';
import {UtilModule} from '../../util/util.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
const feedbackRoutes: Routes = [
{
@@ -35,7 +34,6 @@ const feedbackRoutes: Routes = [
CommonModule,
FormsModule,
IonicModule,
IonIconModule,
RouterModule.forChild(feedbackRoutes),
TranslateModule,
MarkdownModule,

View File

@@ -27,7 +27,6 @@ import {HebisDetailComponent} from './hebis-detail.component';
import {Observable, of} from 'rxjs';
import {StorageProvider} from '../../storage/storage.provider';
import {IonicModule} from '@ionic/angular';
import {IonIconModule} from '../../../util/ion-icon/ion-icon.module';
import {LoggerModule, NgxLoggerLevel} from 'ngx-logger';
const translations: any = {data: {detail: {TITLE: 'Foo'}}};
@@ -69,7 +68,6 @@ describe('HebisDetailComponent', () => {
HebisRoutingModule,
HebisModule,
IonicModule,
IonIconModule,
TranslateModule.forRoot({
loader: {provide: TranslateLoader, useClass: TranslateFakeLoader},
}),

View File

@@ -34,7 +34,6 @@ import {HebisRoutingModule} from './hebis-routing.module';
import {DataModule} from '../data/data.module';
import {DaiaAvailabilityComponent} from './daia-availability/daia-availability.component';
import {UtilModule} from '../../util/util.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
import {DaiaHoldingComponent} from './daia-availability/daia-holding.component';
/**
@@ -53,9 +52,8 @@ import {DaiaHoldingComponent} from './daia-availability/daia-holding.component';
DataModule,
FormsModule,
HebisRoutingModule,
IonIconModule,
HttpClientModule,
IonicModule.forRoot(),
IonicModule,
MarkdownModule.forRoot(),
MenuModule,
MomentModule.forRoot({

View File

@@ -5,7 +5,6 @@ import {TranslateModule} from '@ngx-translate/core';
import {MomentModule} from 'ngx-moment';
import {DataModule} from '../data/data.module';
import {UtilModule} from '../../util/util.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
import {ConfigProvider} from '../config/config.provider';
import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {RouterModule, Routes} from '@angular/router';
@@ -16,11 +15,10 @@ const jobsRoutes: Routes = [{path: 'jobs', component: JobsPageComponent}];
@NgModule({
declarations: [JobsPageComponent],
imports: [
IonicModule.forRoot(),
IonicModule,
ThingTranslateModule.forChild(),
TranslateModule.forChild(),
RouterModule.forChild(jobsRoutes),
IonIconModule,
CommonModule,
MomentModule,
DataModule,

View File

@@ -32,7 +32,6 @@ import {MomentModule} from 'ngx-moment';
import {FeeItemComponent} from './account/elements/fee-item/fee-item.component';
import {DataModule} from '../data/data.module';
import {UtilModule} from '../../util/util.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
const routes: ProtectedRoutes | Routes = [
{
@@ -72,7 +71,6 @@ const routes: ProtectedRoutes | Routes = [
CommonModule,
FormsModule,
IonicModule,
IonIconModule,
RouterModule.forChild(routes),
TranslateModule,
MomentModule,

View File

@@ -32,7 +32,6 @@ import {MapPageComponent} from './page/map-page.component';
import {MapListModalComponent} from './page/map-list-modal.component';
import {NgModule} from '@angular/core';
import {UtilModule} from '../../util/util.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
import {GeoNavigationDirective} from './geo-navigation.directive';
/**
@@ -59,9 +58,8 @@ const mapRoutes: Routes = [
exports: [],
imports: [
CommonModule,
IonicModule.forRoot(),
IonicModule,
LeafletModule,
IonIconModule,
LeafletMarkerClusterModule,
RouterModule.forChild(mapRoutes),
TranslateModule.forChild(),

View File

@@ -52,10 +52,10 @@ export class MapProvider {
icon: divIcon({
className: className,
html: `<span
name="${SCIcon`location_on`}"
name="${SCIcon.location_on}"
class="material-symbols-rounded map-location-pin"
style="font-size: ${iconSize}px;"
>${SCIcon`location_on`}</span>`,
>${SCIcon.location_on}</span>`,
iconSize: [iconSize, iconSize],
iconAnchor: [iconSize / 2, iconSize],
}),
@@ -75,10 +75,10 @@ export class MapProvider {
html:
position.heading === undefined
? `<span
name="${SCIcon`person_pin_circle`}"
name="${SCIcon.person_pin_circle}"
class="material-symbols-rounded map-location-pin"
style="font-size: ${iconSize}px; color: var(--ion-color-primary);"
>${SCIcon`person_pin_circle`}</span>`
>${SCIcon.person_pin_circle}</span>`
: `<span
class="material-symbols-rounded map-location-pin"
style="
@@ -87,7 +87,7 @@ export class MapProvider {
font-size: ${iconSize}px;
color: var(--ion-color-primary);
"
>${SCIcon`navigation`}</span>`,
>${SCIcon.navigation}</span>`,
iconSize: [iconSize, iconSize],
}),
zIndexOffset: 1000,

View File

@@ -52,7 +52,7 @@ describe('ContextMenuComponent', async () => {
],
imports: [
FormsModule,
IonicModule.forRoot(),
IonicModule,
TranslateModule.forRoot(),
CommonModule,
SettingsModule,

View File

@@ -22,7 +22,6 @@ import {TranslateModule} from '@ngx-translate/core';
import {SettingsModule} from '../settings/settings.module';
import {ContextMenuComponent} from './context/context-menu.component';
import {ContextMenuService} from './context/context-menu.service';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
/**
* Menu module
@@ -32,9 +31,8 @@ import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
exports: [ContextMenuComponent],
imports: [
CommonModule,
IonIconModule,
FormsModule,
IonicModule.forRoot(),
IonicModule,
RouterModule,
SettingsModule,
TranslateModule.forChild(),

View File

@@ -18,14 +18,13 @@ import {NavigationComponent} from './navigation.component';
import {TabsComponent} from './tabs.component';
import {CommonModule} from '@angular/common';
import {IonicModule} from '@ionic/angular';
import {IonIconModule} from '../../../util/ion-icon/ion-icon.module';
import {TranslateModule} from '@ngx-translate/core';
import {RouterModule} from '@angular/router';
import {OfflineNoticeComponent} from './offline-notice.component';
@NgModule({
declarations: [RootLinkDirective, NavigationComponent, TabsComponent, OfflineNoticeComponent],
imports: [CommonModule, IonicModule, IonIconModule, TranslateModule, RouterModule],
imports: [CommonModule, IonicModule, TranslateModule, RouterModule],
exports: [TabsComponent, RootLinkDirective, NavigationComponent],
})
export class NavigationModule {}

View File

@@ -77,9 +77,7 @@ ion-router-outlet {
}
.link-active > * {
color: var(--ion-color-primary);
--fill: 1;
::ng-deep stapps-icon {
--fill: 1;
}
color: var(--ion-color-primary);
}

View File

@@ -13,13 +13,13 @@
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<ion-button class="offline-button" color="warning">
<ion-icon slot="start" [size]="16" [weight]="800" name="cloud_off"></ion-icon>
<ion-icon slot="start" [size]="16" [style.--weight]="800" name="cloud_off"></ion-icon>
<ion-label>{{ 'app.errors.OFFLINE' | translate }}</ion-label>
</ion-button>
<ion-button class="error-button" color="danger" (click)="retry()">
<ion-icon #spinIcon slot="start" [size]="16" [weight]="800" name="refresh"></ion-icon>
<ion-icon #spinIcon slot="start" [size]="16" [style.--weight]="800" name="refresh"></ion-icon>
<ion-label>{{ 'app.errors.CONNECTION_ERROR' | translate }}</ion-label>
</ion-button>
<ion-button class="close" fill="clear" color="light" (click)="offlineProvider.dismissError()"
><ion-icon [size]="16" [weight]="800" name="close" slot="icon-only"></ion-icon
><ion-icon [size]="16" [style.--weight]="800" name="close" slot="icon-only"></ion-icon
></ion-button>

View File

@@ -60,6 +60,6 @@
}
}
.tab-selected ::ng-deep stapps-icon {
.tab-selected ion-icon {
--fill: 1;
}

View File

@@ -16,7 +16,7 @@
@if (displayValue) {
<ion-chip [class.active]="active" (click)="emitToggle(value)">
@if (active) {
<ion-icon class="ion-color" name="check_circle" [fill]="true"></ion-icon>
<ion-icon class="ion-color" name="check_circle" [style.--fill]="1"></ion-icon>
}
<ion-label>{{ displayValue }}</ion-label>
</ion-chip>

View File

@@ -24,11 +24,10 @@ import {SettingsProvider} from '../settings/settings.provider';
import {NewsItemComponent} from './item/news-item.component';
import {NewsPageComponent} from './page/news-page.component';
import {SkeletonNewsItemComponent} from './item/skeleton-news-item.component';
import {ChipFilterComponent} from '../data/chips/filter/chip-filter.component';
import {ChipFilterComponent} from './elements/news-filter-settings/chip-filter.component';
import {SettingsModule} from '../settings/settings.module';
import {NewsSettingsFilterComponent} from './elements/news-filter-settings/news-settings-filter.component';
import {UtilModule} from '../../util/util.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
const newsRoutes: Routes = [{path: 'news', component: NewsPageComponent}];
@@ -44,11 +43,10 @@ const newsRoutes: Routes = [{path: 'news', component: NewsPageComponent}];
NewsSettingsFilterComponent,
],
imports: [
IonicModule.forRoot(),
IonicModule,
ThingTranslateModule.forChild(),
TranslateModule.forChild(),
RouterModule.forChild(newsRoutes),
IonIconModule,
CommonModule,
MomentModule,
DataModule,

View File

@@ -19,7 +19,7 @@
<div>
<div class="log-in">
{{ 'profile.userInfo.logInPrompt' | translate | sentencecase }}
<ion-icon name="person" [fill]="true"></ion-icon>
<ion-icon name="person" [style.--fill]="1"></ion-icon>
</div>
</div>
}

View File

@@ -21,7 +21,6 @@ import {ProfilePageComponent} from './page/profile-page.component';
import {TranslateModule} from '@ngx-translate/core';
import {SwiperModule} from 'swiper/angular';
import {UtilModule} from '../../util/util.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
import {ProfilePageSectionComponent} from './page/profile-page-section.component';
import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {DataModule} from '../data/data.module';
@@ -41,7 +40,6 @@ const routes: Routes = [
imports: [
CommonModule,
FormsModule,
IonIconModule,
IonicModule,
RouterModule.forChild(routes),
TranslateModule,

View File

@@ -35,7 +35,6 @@ import {ScheduleDayComponent} from './page/grid/schedule-day.component';
import {ThingTranslateModule} from '../../translation/thing-translate.module';
import {InfiniteSwiperComponent} from './page/grid/infinite-swiper.component';
import {CalendarComponent} from './page/components/calendar.component';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
import {ChooseEventsPageComponent} from './page/choose-events-page.component';
const settingsRoutes: Routes = [
@@ -69,8 +68,7 @@ const settingsRoutes: Routes = [
CommonModule,
DataModule,
FormsModule,
IonicModule.forRoot(),
IonIconModule,
IonicModule,
MomentModule,
RouterModule.forChild(settingsRoutes),
SwiperModule,

View File

@@ -33,7 +33,6 @@ import {CalendarService} from '../calendar/calendar.service';
import {CalendarModule} from '../calendar/calendar.module';
import {BackgroundModule} from '../background/background.module';
import {UtilModule} from '../../util/util.module';
import {IonIconModule} from '../../util/ion-icon/ion-icon.module';
const settingsRoutes: Routes = [{path: 'settings', component: SettingsPageComponent}];
@@ -52,9 +51,8 @@ const settingsRoutes: Routes = [{path: 'settings', component: SettingsPageCompon
CommonModule,
FormsModule,
CalendarModule,
IonIconModule,
BackgroundModule,
IonicModule.forRoot(),
IonicModule,
TranslateModule.forChild(),
ThingTranslateModule.forChild(),
RouterModule.forChild(settingsRoutes),

View File

@@ -29,7 +29,6 @@ import {
import {ThingTranslateDefaultParser, ThingTranslateParser} from './thing-translate.parser';
import {ThingTranslatePipe} from './thing-translate.pipe';
import {ThingTranslateService} from './thing-translate.service';
import {IonIconModule} from '../util/ion-icon/ion-icon.module';
import {TranslateSimplePipe} from './translate-simple.pipe';
import {PropertyNameTranslatePipe} from './property-name-translate.pipe';
@@ -38,7 +37,6 @@ export interface ThingTranslateModuleConfig {
}
@NgModule({
imports: [IonIconModule],
declarations: [
ArrayJoinPipe,
DurationLocalizedPipe,
@@ -56,7 +54,6 @@ export interface ThingTranslateModuleConfig {
IsNumericPipe,
],
exports: [
IonIconModule,
ArrayJoinPipe,
DurationLocalizedPipe,
NumberLocalizedPipe,

View File

@@ -61,7 +61,6 @@ export class InternetConnectionService {
private doRetry(error: unknown, retryCount: number): ObservableInput<unknown> {
return race(
this.offline$.pipe(
tap(it => console.log(it)),
filter(it => !it),
take(1),
delay(Math.min(retryCount ** 4 + 100, 10_000)),

View File

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

View File

@@ -14,17 +14,24 @@
*/
/**
*
* @param {string} tag
*/
export function matchTagProperties(tag: string) {
export function matchTagProperties(tag) {
return new RegExp(`(?<=<${tag})[\\s\\S]*?(?=>\\s*<\\/${tag}\\s*>)`, 'g');
}
/**
*
* @param {string[]} properties
*/
export function matchPropertyContent(properties: string[]) {
export function matchPropertyContent(properties) {
const names = properties.join('|');
return new RegExp(`((?<=(${names})=")[\\w-]+(?="))|((?<=\\[(${names})]="')[\\w-]+(?='"))`, 'g');
}
/**
* @param {string} objectName
*/
export function matchPropertyAccess(objectName) {
return new RegExp(`(?<=${objectName}\\s*\\.\\s*)\\w+`, 'g');
}

View File

@@ -14,7 +14,7 @@
*/
/* eslint-disable unicorn/no-null */
import {matchPropertyContent, matchTagProperties} from './icon-match';
import {matchPropertyContent, matchTagProperties, matchPropertyAccess} from './icon-match.mjs';
describe('matchTagProperties', function () {
const regex = matchTagProperties('test');
@@ -59,3 +59,30 @@ describe('matchPropertyContent', function () {
expect(`no="content" [no]="'content'"`.match(regex)).toEqual(null);
});
});
describe('matchPropertyAccess', function () {
const property = '0_20a_boninAo0_';
const object = 'test';
const regex = matchPropertyAccess(object);
it('should match property access', function () {
expect(`${object}.${property}`.match(regex)).toEqual([property]);
});
it('should respect whitespace', function () {
expect(`${object}. ${property}`.match(regex)).toEqual([property]);
expect(`${object} .${property}`.match(regex)).toEqual([property]);
expect(`${object} . ${property}`.match(regex)).toEqual([property]);
expect(`${object} \n . \n ${property}`.match(regex)).toEqual([property]);
});
it('should not include invalid trailing stuff', function () {
expect(`${object}.${property}!`.match(regex)).toEqual([property]);
expect(`${object}.${property}.`.match(regex)).toEqual([property]);
expect(`${object}.${property}-`.match(regex)).toEqual([property]);
expect(`${object}.${property}]`.match(regex)).toEqual([property]);
expect(`${object}.${property}}`.match(regex)).toEqual([property]);
expect(`${object}.${property};`.match(regex)).toEqual([property]);
expect(`${object}.${property}:`.match(regex)).toEqual([property]);
});
});

View File

@@ -1,49 +0,0 @@
/*
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {ChangeDetectionStrategy, Component, HostBinding, Input} from '@angular/core';
@Component({
selector: 'stapps-icon',
templateUrl: 'icon.html',
styleUrls: ['icon.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class IconComponent {
@HostBinding('style.--size')
@Input()
size?: number;
@HostBinding('style.--weight')
@Input()
weight?: number;
@HostBinding('style.--grade')
@Input()
grade?: number;
@Input()
fill: boolean;
@HostBinding('innerHtml')
@Input()
name: string;
@HostBinding('style.--fill') get fillStyle(): number | undefined {
return this.fill ? 1 : undefined;
}
@HostBinding('class.material-symbols-rounded') hostClass = true;
}

View File

@@ -1,15 +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/>.
-->
<ng-content></ng-content>

View File

@@ -1,31 +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/>.
*/
:host {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
font-size: inherit;
font-variation-settings:
'wght' var(--weight),
'GRAD' var(--grade),
'FILL' var(--fill);
transition: all 0.2s ease-in-out;
}

View File

@@ -12,10 +12,8 @@
* 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 {MaterialSymbol} from 'material-symbols';
/**
* A noop function to aid parsing icon names
*/
export function SCIcon(strings: TemplateStringsArray, ..._keys: string[]): string {
return strings.join('');
}
export const SCIcon = new Proxy({} as {[key in MaterialSymbol]: key}, {
get: (_target, property: string) => property as MaterialSymbol,
});

View File

@@ -1,66 +0,0 @@
/*
* Copyright (C) 2023 StApps
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation, version 3.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>.
*/
import {
DestroyRef,
Directive,
ElementRef,
Host,
inject,
OnInit,
Optional,
Self,
ViewContainerRef,
} from '@angular/core';
import {SCIcon} from './icon';
import {IconReplacer} from './replace-util';
import {TranslateService} from '@ngx-translate/core';
import {IonBackButton} from '@ionic/angular';
import {TitleCasePipe} from '@angular/common';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
@Directive({
selector: 'ion-back-button',
})
export class IonBackButtonDirective extends IconReplacer implements OnInit {
destroy$ = inject(DestroyRef);
constructor(
element: ElementRef,
viewContainerRef: ViewContainerRef,
@Host() @Self() @Optional() private ionBackButton: IonBackButton,
private translateService: TranslateService,
private titleCasePipe: TitleCasePipe,
) {
super(element, viewContainerRef, 'shadow');
}
replace() {
this.replaceIcon(this.host.querySelector('.button-inner'), {
md: SCIcon`arrow_back`,
ios: SCIcon`arrow_back_ios`,
size: 24,
});
}
async ngOnInit() {
await super.ngOnInit();
this.translateService
.stream('back')
.pipe(takeUntilDestroyed(this.destroy$))
.subscribe((value: string) => {
this.ionBackButton.text = this.titleCasePipe.transform(value);
});
}
}

View File

@@ -1,46 +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 {Directive, ElementRef, ViewContainerRef} from '@angular/core';
import {SCIcon} from './icon';
import {IconReplacer} from './replace-util';
@Directive({
selector: 'ion-breadcrumb',
})
export class IonBreadcrumbDirective extends IconReplacer {
constructor(element: ElementRef, viewContainerRef: ViewContainerRef) {
super(element, viewContainerRef, 'shadow');
}
replace() {
this.replaceIcon(
this.host.querySelector('span[part="separator"]'),
{
name: SCIcon`arrow_forward_ios`,
size: 16,
style: `color: var(--ion-color-tint);`,
},
'-separator',
);
this.replaceIcon(
this.host.querySelector('button[part="collapsed-indicator"]'),
{
name: SCIcon`more_horiz`,
size: 24,
},
'-collapsed',
);
}
}

View File

@@ -0,0 +1,126 @@
import {MaterialSymbol} from 'material-symbols';
import {SCIcon} from './icon';
import {
arrowBackOutline,
caretDownSharp,
chevronBackOutline,
chevronExpand,
chevronForwardOutline,
close,
closeCircle,
closeSharp,
ellipsisHorizontal,
menu,
menuSharp,
searchOutline,
searchSharp,
} from 'ionicons/icons';
const styles = new CSSStyleSheet();
styles.replaceSync(/* css */ `
:host {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1em;
height: 1em;
font-size: inherit;
font-variation-settings: 'wght' var(--weight, 400), 'GRAD' var(--grade, 1), 'FILL' var(--fill, 0);
transition: font-variation-settings 0.2s ease-in-out;
direction: ltr;
font-family: 'Material Symbols Rounded';
font-feature-settings: 'liga';
font-style: normal;
line-height: 1;
text-transform: none;
text-rendering: optimizelegibility;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
`);
export class IonIconCustomElement extends HTMLElement {
static observedAttributes = ['name', 'size', 'color'];
set name(value: MaterialSymbol) {
this.shadowRoot!.textContent =
document.querySelector('ion-app.ios') && iosAlias.has(value) ? iosAlias.get(value)! : value;
}
set icon(value: string) {
if (iconMap.has(value)) {
this.name = iconMap.get(value)!;
} else {
this.style.setProperty('color', 'red');
// @ts-expect-error not assignable
this.name = '??';
console.error(
`The icon "${value}" is missing from the icon map (did Ionic change something?)`,
'\n\n',
new Error('...')!.stack,
);
}
}
set size(value: string) {
switch (value) {
case 'small': {
this.style.setProperty('font-size', '1.125rem');
break;
}
case 'large': {
this.style.setProperty('font-size', '2rem');
break;
}
default: {
this.style.setProperty('font-size', Number.isNaN(Number(value)) ? value : `${value}px`);
}
}
}
set color(value: string) {
this.style.setProperty('color', `var(--ion-color-${value}`);
}
constructor() {
super();
const shadow = this.attachShadow({mode: 'open'});
shadow.adoptedStyleSheets = [styles];
}
}
const iosAlias = new Map<MaterialSymbol, MaterialSymbol>([
[SCIcon.arrow_back, SCIcon.arrow_back_ios],
[SCIcon.arrow_forward, SCIcon.arrow_forward_ios],
[SCIcon.share, SCIcon.ios_share],
]);
/**
* Maps hardcoded icon SVGs to our icon names.
* Thanks, Ionic.
*/
const iconMap = new Map<string, MaterialSymbol>([
[arrowBackOutline, SCIcon.arrow_back],
[chevronBackOutline, SCIcon.arrow_back],
[chevronForwardOutline, SCIcon.arrow_forward],
[menu, SCIcon.menu],
[menuSharp, SCIcon.menu],
[searchOutline, SCIcon.search],
[searchSharp, SCIcon.search],
[chevronExpand, SCIcon.expand_more],
[caretDownSharp, SCIcon.expand_more],
[close, SCIcon.close],
[closeSharp, SCIcon.close],
[closeCircle, SCIcon.cancel],
[ellipsisHorizontal, SCIcon.more_horiz],
['chevron-down-outline', SCIcon.expand_more],
['arrow_back', SCIcon.arrow_back],
]);

View File

@@ -1,132 +1,14 @@
/*
* 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 {
ComponentRef,
Directive,
ElementRef,
Host,
Input,
OnChanges,
OnDestroy,
OnInit,
Optional,
Self,
ViewContainerRef,
} from '@angular/core';
import {IconComponent} from './icon.component';
import {IonIcon} from '@ionic/angular';
// eslint-disable-next-line @typescript-eslint/no-empty-function
const noop = () => {};
const noopProperty = {
set: noop,
get: noop,
};
import {Directive, Input} from '@angular/core';
import {MaterialSymbol} from 'material-symbols';
@Directive({
selector: 'ion-icon',
standalone: true,
})
export class IonIconDirective implements OnInit, OnDestroy, OnChanges {
@Input() name: string;
export class IonIconDirective {
@Input({required: true}) name: MaterialSymbol;
@Input() md: string;
@Input() size: 'large' | 'small' | number | string;
@Input() ios: string;
@Input() fill = false;
@Input() weight: number;
@Input() size: number | 'small' | 'large';
@Input() grade: number;
@Input() style: string;
private mutationObserver: MutationObserver;
iconComponent?: ComponentRef<IconComponent>;
private static get mode(): 'md' | 'ios' {
return document.querySelector(':root')?.getAttribute('mode') as 'md' | 'ios';
}
constructor(
private element: ElementRef,
private viewContainerRef: ViewContainerRef,
@Host() @Self() @Optional() private ionIcon: IonIcon,
) {}
ngOnInit() {
this.iconComponent = this.viewContainerRef.createComponent(IconComponent, {});
this.element.nativeElement.insertBefore(
this.iconComponent.location.nativeElement,
this.element.nativeElement.firstChild,
);
this.mutationObserver = new MutationObserver(() => {
const inner = this.element.nativeElement.shadowRoot.querySelector('.icon-inner');
if (!inner) return;
inner.insertBefore(document.createElement('slot'), inner.firstChild);
});
this.mutationObserver.observe(this.element.nativeElement.shadowRoot, {
childList: true,
});
this.ngOnChanges();
// this will effectively completely disable the ion-icon component
for (const name of ['src', 'name', 'icon', 'md', 'ios']) {
this.disableProperty(name);
}
}
ngOnDestroy() {
this.mutationObserver.disconnect();
}
ngOnChanges() {
if (!this.iconComponent) return;
for (const key of ['name', 'weight', 'fill', 'size', 'grade'] as Array<
keyof IconComponent & keyof IonIconDirective
>) {
// @ts-expect-error type mismatch
this.iconComponent.instance[key] = this[key];
}
for (const mode of ['md', 'ios'] as Array<'md' | 'ios'>) {
if (this[mode] && IonIconDirective.mode === mode) {
this.iconComponent.instance.name = this[mode];
}
}
if (this.size) {
this.element.nativeElement.style.cssText = `font-size: ${this.size}px;`;
}
if (this.style) {
this.element.nativeElement.style.cssText += this.style;
}
}
disableProperty(name: string) {
Object.defineProperty(
Object.getPrototypeOf((this.ionIcon as unknown as {el: HTMLElement}).el),
name,
noopProperty,
);
}
@Input() color: 'primary' | 'secondary' | 'light' | 'medium' | 'dark' | string;
}

View File

@@ -1,45 +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 {NgModule} from '@angular/core';
import {IconComponent} from './icon.component';
import {IonIconDirective} from './ion-icon.directive';
import {IonBackButtonDirective} from './ion-back-button.directive';
import {IonSearchbarDirective} from './ion-searchbar.directive';
import {IonBreadcrumbDirective} from './ion-breadcrumb.directive';
import {IonReorderDirective} from './ion-reorder.directive';
import {TranslateModule, TranslateService} from '@ngx-translate/core';
import {CommonModule, TitleCasePipe} from '@angular/common';
@NgModule({
imports: [TranslateModule, CommonModule],
declarations: [
IconComponent,
IonIconDirective,
IonBackButtonDirective,
IonSearchbarDirective,
IonBreadcrumbDirective,
IonReorderDirective,
],
exports: [
IonIconDirective,
IonReorderDirective,
IonBackButtonDirective,
IonSearchbarDirective,
IonBreadcrumbDirective,
],
providers: [TranslateService, TitleCasePipe],
})
export class IonIconModule {}

View File

@@ -1,33 +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 {Directive, ElementRef, ViewContainerRef} from '@angular/core';
import {SCIcon} from './icon';
import {IconReplacer} from './replace-util';
@Directive({
selector: 'ion-reorder',
})
export class IonReorderDirective extends IconReplacer {
constructor(element: ElementRef, viewContainerRef: ViewContainerRef) {
super(element, viewContainerRef, 'shadow');
}
replace() {
this.replaceIcon(this.host, {
name: SCIcon`reorder`,
size: 24,
});
}
}

View File

@@ -1,37 +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 {Directive, ElementRef, ViewContainerRef} from '@angular/core';
import {SCIcon} from './icon';
import {IconReplacer} from './replace-util';
@Directive({
selector: 'ion-searchbar',
})
export class IonSearchbarDirective extends IconReplacer {
constructor(element: ElementRef, viewContainerRef: ViewContainerRef) {
super(element, viewContainerRef, 'light');
}
replace() {
this.replaceIcon(this.host.querySelector('.searchbar-input-container'), {
name: SCIcon`search`,
size: 24,
});
this.replaceIcon(this.host.querySelector('.searchbar-clear-button'), {
name: SCIcon`close`,
size: 24,
});
}
}

View File

@@ -1,124 +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 {ComponentRef, Directive, ElementRef, OnDestroy, OnInit, ViewContainerRef} from '@angular/core';
import {IonIcon} from '@ionic/angular';
import {IonIconDirective} from './ion-icon.directive';
import {waitForElement} from './shadow-attacher';
export type IconData = Omit<
Partial<IonIconDirective>,
'ngOnChanges' | 'ngOnInit' | 'viewContainerRef' | 'ngOnDestroy' | 'element' | 'ionIcon' | 'disableProperty'
>;
/**
* A utility class to replace ion-icons in other ionic components.
*/
@Directive()
export abstract class IconReplacer implements OnInit, OnDestroy {
private mutationObserver: MutationObserver;
protected slotName = 'sc-icon';
protected maxAttempts = 10;
protected retryAfterMs = 10;
/**
* The host element
*
* This will be either element.nativeElement.shadowRoot or element.nativeElement
* depending on the iconDomLocation
*/
protected get host() {
return this.iconDomLocation === 'shadow'
? this.element.nativeElement.shadowRoot
: this.element.nativeElement;
}
/**
* @param element The host element
* @param viewContainerRef The view container ref
* @param iconDomLocation If the icon is placed inside the shadow dom or not
* @protected
*/
protected constructor(
private readonly element: ElementRef,
private readonly viewContainerRef: ViewContainerRef,
private readonly iconDomLocation: 'shadow' | 'light',
) {}
/**
* Replace the icons here
*/
abstract replace(): void;
async ngOnInit() {
if (this.host) {
this.attachObserver();
} else {
console.warn("Shadow root didn't exist for ion icon replacer, retrying...");
await waitForElement(() => this.host);
this.replace();
}
}
private attachObserver() {
this.mutationObserver = new MutationObserver(() => this.replace());
this.mutationObserver.observe(this.host, {
childList: true,
});
}
replaceIcon(parent: HTMLElement | null, iconData: IconData, slotName = '') {
if (!parent) return;
const icon = parent.querySelector('ion-icon');
if (!icon) return;
const scIcon = this.createIcon(iconData);
// @ts-expect-error can be spread
scIcon.location.nativeElement.classList.add(...icon.classList);
if (this.iconDomLocation === 'shadow') {
// shadow dom needs to utilize slotting, to put it outside the shadow dom
// otherwise it won't receive any css data
const slot = document.createElement('slot');
slot.name = this.slotName + slotName;
icon.replaceWith(slot);
scIcon.location.nativeElement.slot = this.slotName + slotName;
this.element.nativeElement.append(scIcon.location.nativeElement);
} else {
icon.replaceWith(scIcon.location.nativeElement);
}
}
private createIcon(iconData: IconData): ComponentRef<IonIcon> {
const ionIcon = this.viewContainerRef.createComponent(IonIcon, {});
const iconDirective = new IonIconDirective(ionIcon.location, this.viewContainerRef, ionIcon.instance);
for (const key in iconData) {
// @ts-expect-error type mismatch
iconDirective[key] = iconData[key];
}
iconDirective.ngOnInit();
return ionIcon;
}
ngOnDestroy() {
this.mutationObserver?.disconnect();
}
}

View File

@@ -62,7 +62,7 @@ export const profilePageSections: SCSection[] = [
links: [
{
name: 'Favorites',
icon: SCIcon`grade`,
icon: SCIcon.grade,
link: ['/favorites'],
translations: {
de: {
@@ -73,7 +73,7 @@ export const profilePageSections: SCSection[] = [
},
{
name: 'Schedule',
icon: SCIcon`calendar_today`,
icon: SCIcon.calendar_today,
link: ['/schedule'],
translations: {
de: {
@@ -84,7 +84,7 @@ export const profilePageSections: SCSection[] = [
},
{
name: 'Course Catalog',
icon: SCIcon`inventory_2`,
icon: SCIcon.inventory_2,
link: ['/catalog'],
translations: {
de: {
@@ -95,7 +95,7 @@ export const profilePageSections: SCSection[] = [
},
{
name: 'Settings',
icon: SCIcon`settings`,
icon: SCIcon.settings,
link: ['/settings'],
translations: {
de: {
@@ -106,7 +106,7 @@ export const profilePageSections: SCSection[] = [
},
{
name: 'Feedback',
icon: SCIcon`rate_review`,
icon: SCIcon.rate_review,
link: ['/feedback'],
translations: {
de: {
@@ -117,7 +117,7 @@ export const profilePageSections: SCSection[] = [
},
{
name: 'About',
icon: SCIcon`info`,
icon: SCIcon.info,
link: ['/about'],
translations: {
de: {
@@ -140,7 +140,7 @@ export const profilePageSections: SCSection[] = [
links: [
{
name: 'Assessments',
icon: SCIcon`fact_check`,
icon: SCIcon.fact_check,
link: ['/assessments'],
needsAuth: true,
translations: {
@@ -164,7 +164,7 @@ export const profilePageSections: SCSection[] = [
links: [
{
name: 'Library Catalog',
icon: SCIcon`local_library`,
icon: SCIcon.local_library,
link: ['/hebis-search'],
translations: {
de: {
@@ -175,7 +175,7 @@ export const profilePageSections: SCSection[] = [
},
{
name: 'Library Account',
icon: SCIcon`badge`,
icon: SCIcon.badge,
needsAuth: true,
link: ['/library-account/profile'],
translations: {
@@ -187,7 +187,7 @@ export const profilePageSections: SCSection[] = [
},
{
name: 'Orders & Reservations',
icon: SCIcon`collections_bookmark`,
icon: SCIcon.collections_bookmark,
needsAuth: true,
link: ['/library-account/holds'],
translations: {
@@ -199,7 +199,7 @@ export const profilePageSections: SCSection[] = [
},
{
name: 'Checked out items',
icon: SCIcon`library_books`,
icon: SCIcon.library_books,
needsAuth: true,
link: ['/library-account/checked-out'],
translations: {
@@ -211,7 +211,7 @@ export const profilePageSections: SCSection[] = [
},
{
name: 'Fines',
icon: SCIcon`request_page`,
icon: SCIcon.request_page,
needsAuth: true,
link: ['/library-account/fines'],
translations: {

View File

@@ -36,13 +36,6 @@
/* StApps */
stapps-icon {
--size-unit: 1px;
--weight: 400;
--grade: 0;
--fill: 0;
}
.map-location-pin {
font-variation-settings: 'FILL' 1;
color: var(--ion-color-tertiary);

View File

@@ -12,10 +12,14 @@
* 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 {enableProdMode} from '@angular/core';
// override the ion-icon before everything else
customElements.define('ion-icon', IonIconCustomElement);
import {platformBrowser} from '@angular/platform-browser';
import {AppModule} from './app/app.module';
import {environment} from './environments/environment';
import {enableProdMode} from '@angular/core';
import {AppModule} from './app/app.module';
import {IonIconCustomElement} from './app/util/ion-icon/ion-icon-custom-element';
if (environment.production) {
enableProdMode();

View File

@@ -33,9 +33,14 @@ app-root ion-searchbar[class*='sc-ion-searchbar-'] {
width: $icon-size;
height: $icon-size;
font-size: $icon-size;
color: var(--ion-color-medium-shade);
}
ion-icon.searchbar-clear-icon {
font-size: $icon-size;
}
input.searchbar-input {
padding-top: var(--spacing-xs);
padding-bottom: var(--spacing-xs);

View File

@@ -68,6 +68,7 @@ app-root ion-toolbar.in-toolbar {
ion-back-button {
--icon-margin-end: var(--spacing-xs);
--icon-margin-start: 0;
height: 42px; // this prevents the back button to become a .x px value
}

View File

@@ -30,12 +30,12 @@
"devDependencies": {
"@changesets/changelog-git": "0.1.14",
"@changesets/cli": "2.26.1",
"prettier": "3.1.1",
"cobertura-merge": "1.0.4",
"deepmerge": "4.3.1",
"dotenv-cli": "7.2.1",
"glob": "10.3.10",
"junit-report-merger": "6.0.3",
"prettier": "3.1.1",
"syncpack": "12.3.0",
"turbo": "1.10.16",
"turbo-ignore": "1.10.16",

44
pnpm-lock.yaml generated
View File

@@ -830,6 +830,9 @@ importers:
ionic-appauth:
specifier: 0.9.0
version: 0.9.0(rxjs@7.8.1)
ionicons:
specifier: 7.2.1
version: 7.2.1
jsonpath-plus:
specifier: 6.0.1
version: 6.0.1
@@ -4546,7 +4549,7 @@ packages:
object-assign: 4.1.1
open: 8.4.0
proxy-middleware: 0.15.0
send: 1.0.0-beta.2
send: 0.18.0
serve-index: 1.9.1
transitivePeerDependencies:
- supports-color
@@ -6528,12 +6531,6 @@ packages:
hasBin: true
dev: false
/@stencil/core@4.5.0:
resolution: {integrity: sha512-XRbHdb9t4SQzCCbF9qsh0dexvnlArEzCDJl19BJzxzazVBM398SeJUKCBh4p91AZIWveN0gHuZSIGMhLWR7qSA==}
engines: {node: '>=16.0.0', npm: '>=7.10.0'}
hasBin: true
dev: false
/@swc/helpers@0.4.14:
resolution: {integrity: sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==}
dependencies:
@@ -10089,17 +10086,6 @@ packages:
dependencies:
ms: 2.0.0
/debug@3.1.0:
resolution: {integrity: sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==}
peerDependencies:
supports-color: '*'
peerDependenciesMeta:
supports-color:
optional: true
dependencies:
ms: 2.0.0
dev: true
/debug@3.2.7(supports-color@5.5.0):
resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
peerDependencies:
@@ -12872,7 +12858,7 @@ packages:
/ionicons@7.2.1:
resolution: {integrity: sha512-2pvCM7DGVEtbbj48PJzQrCADCQrqjU1nUYX9l9PyEWz3ZfdnLdAouqwPxLdl8tbaF9cE7OZRSlyQD7oLOLnGoQ==}
dependencies:
'@stencil/core': 4.5.0
'@stencil/core': 4.12.6
dev: false
/ionicons@7.2.2:
@@ -17306,26 +17292,6 @@ packages:
transitivePeerDependencies:
- supports-color
/send@1.0.0-beta.2:
resolution: {integrity: sha512-k1yHu/FNK745PULKdsGpQ+bVSXYNwSk+bWnYzbxGZbt5obZc0JKDVANsCRuJD1X/EG15JtP9eZpwxkhUxIYEcg==}
engines: {node: '>= 0.10'}
dependencies:
debug: 3.1.0
destroy: 1.2.0
encodeurl: 1.0.2
escape-html: 1.0.3
etag: 1.8.1
fresh: 0.5.2
http-errors: 2.0.0
mime-types: 2.1.35
ms: 2.1.3
on-finished: 2.4.1
range-parser: 1.2.1
statuses: 2.0.1
transitivePeerDependencies:
- supports-color
dev: true
/serialize-javascript@6.0.0:
resolution: {integrity: sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==}
dependencies: