feat: calendar plugin

This commit is contained in:
Thea Schöbl
2022-01-31 15:57:38 +00:00
committed by Rainer Killinger
parent 080e6fa3e8
commit a57c3029df
54 changed files with 2880 additions and 70 deletions

View File

@@ -0,0 +1,21 @@
/*
* 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 Licens 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/>.
*/
export const CALENDAR_SYNC_SETTINGS_KEY = 'calendarSettings';
export const CALENDAR_SYNC_ENABLED_KEY = 'sync';
export const CALENDAR_NOTIFICATIONS_ENABLED_KEY = 'notifications';
export type CALENDAR_SYNC_KEYS =
| typeof CALENDAR_SYNC_ENABLED_KEY
| typeof CALENDAR_NOTIFICATIONS_ENABLED_KEY;

View File

@@ -0,0 +1,209 @@
/*
* Copyright (C) 2021 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 {Component, OnInit} from '@angular/core';
import {AddEventReviewModalComponent} from '../../calendar/add-event-review-modal.component';
import {ModalController} from '@ionic/angular';
import {ScheduleProvider} from '../../calendar/schedule.provider';
import {map} from 'lodash-es';
import {Directory, Encoding, Filesystem} from '@capacitor/filesystem';
import {Share} from '@capacitor/share';
import {Device} from '@capacitor/device';
import {Dialog} from '@capacitor/dialog';
import {SCUuid} from '@openstapps/core';
import {TranslateService} from '@ngx-translate/core';
import {StorageProvider} from '../../storage/storage.provider';
import {ScheduleSyncService} from '../../background/schedule/schedule-sync.service';
import {CalendarService} from '../../calendar/calendar.service';
import {getNativeCalendarExport} from '../../calendar/ical/ical';
import {ThingTranslateService} from '../../../translation/thing-translate.service';
import {
CALENDAR_NOTIFICATIONS_ENABLED_KEY,
CALENDAR_SYNC_ENABLED_KEY,
CALENDAR_SYNC_KEYS,
CALENDAR_SYNC_SETTINGS_KEY,
} from './calendar-sync-settings-keys';
@Component({
selector: 'calendar-sync-settings',
templateUrl: 'calendar-sync-settings.html',
styleUrls: ['calendar-sync-settings.scss'],
})
export class CalendarSyncSettingsComponent implements OnInit {
isWeb = true;
syncEnabled = false;
notificationsEnabled = false;
constructor(
readonly modalController: ModalController,
readonly scheduleProvider: ScheduleProvider,
readonly translator: TranslateService,
readonly thingTranslator: ThingTranslateService,
readonly storageProvider: StorageProvider,
readonly scheduleSyncService: ScheduleSyncService,
readonly calendarService: CalendarService,
) {}
ngOnInit() {
Device.getInfo().then(it => {
this.isWeb = it.platform === 'web';
});
this.getSetting(CALENDAR_SYNC_ENABLED_KEY).then(
it => (this.syncEnabled = it),
);
this.getSetting(CALENDAR_NOTIFICATIONS_ENABLED_KEY).then(
it => (this.notificationsEnabled = it),
);
}
async getSetting(key: CALENDAR_SYNC_KEYS) {
return (await this.storageProvider.get(
`${CALENDAR_SYNC_SETTINGS_KEY}.${key}`,
)) as boolean;
}
async syncCalendar(sync: boolean) {
this.syncEnabled = sync;
if (sync) {
const uuids = this.scheduleProvider.partialEvents$.value.map(
it => it.uid,
);
const dateSeries = (await this.scheduleProvider.getDateSeries(uuids))
.dates;
await this.calendarService.syncEvents(
getNativeCalendarExport(dateSeries, this.thingTranslator.translator),
);
} else {
await this.calendarService.purge();
}
}
async setSetting(settings: Partial<Record<CALENDAR_SYNC_KEYS, boolean>>) {
await Promise.all(
map(settings, (setting, key) =>
this.storageProvider.put(
`${CALENDAR_SYNC_SETTINGS_KEY}.${key}`,
setting,
),
),
);
return this.scheduleSyncService.enable();
}
async export() {
const uuids = this.scheduleProvider.partialEvents$.value.map(it => it.uid);
const dateSeries = (await this.scheduleProvider.getDateSeries(uuids)).dates;
const modal = await this.modalController.create({
component: AddEventReviewModalComponent,
swipeToClose: true,
cssClass: 'add-modal',
componentProps: {
dismissAction: () => {
modal.dismiss();
},
dateSeries: dateSeries,
},
});
await modal.present();
await modal.onWillDismiss();
}
async restore(event: Event) {
// @ts-expect-error files do actually exist
const file = event.target?.files[0] as File;
const uuids = JSON.parse(await file.text()) as SCUuid[] | unknown;
if (!Array.isArray(uuids) || uuids.some(it => typeof it !== 'string')) {
return Dialog.alert({
title: this.translator.instant(
'settings.calendar.export.dialogs.restore.rejectFile.title',
),
message: this.translator.instant(
'settings.calendar.export.dialogs.restore.rejectFile.message',
),
});
}
const dateSeries = await this.scheduleProvider.restore(uuids);
return dateSeries
? Dialog.confirm({
title: this.translator.instant(
'settings.calendar.export.dialogs.restore.success.title',
),
message: this.translator.instant(
'settings.calendar.export.dialogs.restore.success.message',
),
})
: Dialog.alert({
title: this.translator.instant(
'settings.calendar.export.dialogs.restore.error.title',
),
message: this.translator.instant(
'settings.calendar.export.dialogs.restore.error.message',
),
});
}
translateWithDefault(key: string, defaultValue: string) {
const out = this.translator.instant(key);
return out === key ? defaultValue : out;
}
async backup() {
const uuids = JSON.stringify(
this.scheduleProvider.partialEvents$.value.map(it => it.uid),
);
const fileName = `${this.translator.instant(
'settings.calendar.export.fileName',
)}.json`;
const info = await Device.getInfo();
if (info.platform === 'web') {
const blob = new Blob([uuids], {type: 'application/json'});
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName;
a.click();
window.URL.revokeObjectURL(url);
} else {
const result = await Filesystem.writeFile({
path: fileName,
data: uuids,
encoding: Encoding.UTF8,
directory: Directory.Cache,
});
await Share.share({
title: this.translator.instant(
'settings.calendar.export.dialogs.backup.save.title',
),
text: this.translator.instant(
'settings.calendar.export.dialogs.backup.save.message',
),
url: result.uri,
dialogTitle: this.translator.instant(
'settings.calendar.export.dialogs.backup.save.title',
),
});
}
}
}

View File

@@ -0,0 +1,120 @@
<!--
~ Copyright (C) 2021 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 Licens for
~ more details.
~
~ You should have received a copy of the GNU General Public License along with
~ this program. If not, see <https://www.gnu.org/licenses/>.
-->
<ion-card>
<ion-card-header>
<ion-card-subtitle>{{
'settings.calendar.title' | translate
}}</ion-card-subtitle>
</ion-card-header>
<ion-card-content>
<ion-list lines="none">
<ion-item-group>
<ion-item-divider>
<ion-label>{{
'settings.calendar.sync.title' | translate
}}</ion-label>
</ion-item-divider>
<ion-item>
<ion-toggle
[disabled]="isWeb"
[checked]="syncEnabled"
(ionChange)="
setSetting({
sync: $event.detail.checked
});
syncCalendar($event.detail.checked)
"
>
</ion-toggle>
<ion-label>{{
'settings.calendar.sync.syncWithCalendar' | translate
}}</ion-label>
</ion-item>
<ion-item>
<ion-toggle
[disabled]="isWeb"
[checked]="notificationsEnabled"
(ionChange)="
setSetting({
notifications: $event.detail.checked
})
"
>
</ion-toggle>
<ion-label>{{
'settings.calendar.sync.eventNotifications' | translate
}}</ion-label>
</ion-item>
<ion-item>
<ion-button
[disabled]="isWeb || !syncEnabled"
fill="clear"
(click)="syncCalendar(true)"
>
<ion-label>Sync Now</ion-label>
<ion-icon slot="end" name="sync"></ion-icon>
</ion-button>
</ion-item>
<ion-item *ngIf="isWeb">
<ion-label color="medium"
><i>{{
'settings.calendar.sync.unavailableWeb' | translate
}}</i></ion-label
>
</ion-item>
</ion-item-group>
<ion-item-group>
<ion-item-divider>
<ion-label>{{
'settings.calendar.export.title' | translate
}}</ion-label>
</ion-item-divider>
<ion-item>
<ion-button fill="clear" (click)="export()">
<ion-label>{{
'settings.calendar.export.exportEvents' | translate
}}</ion-label>
<ion-icon slot="end" name="download"></ion-icon>
</ion-button>
</ion-item>
<ion-item>
<ion-button fill="clear" (click)="backup()">
<ion-label>{{
'settings.calendar.export.backup' | translate
}}</ion-label>
<ion-icon slot="end" name="save"></ion-icon>
</ion-button>
<ion-button fill="clear" (click)="restoreInput.click()">
<ion-label>{{
'settings.calendar.export.restore' | translate
}}</ion-label>
<ion-icon slot="end" name="refresh"></ion-icon>
</ion-button>
<!--suppress CheckEmptyScriptTag -->
<input
class="ion-hide"
type="file"
accept="application/json"
#restoreInput
(change)="restore($event)"
/>
</ion-item>
</ion-item-group>
</ion-list>
</ion-card-content>
</ion-card>

View File

@@ -16,9 +16,11 @@
{{
'categories[0]'
| thingTranslate
: settingsCache[categoryKey]?.settings[
objectKeys(settingsCache[categoryKey]?.settings)[0]
]
: $any(
settingsCache[categoryKey]?.settings[
objectKeys(settingsCache[categoryKey]?.settings)[0]
]
)
| titlecase
}}
</h5>
@@ -31,6 +33,9 @@
></stapps-settings-item>
</div>
</ion-list>
<calendar-sync-settings></calendar-sync-settings>
<ion-button
color="medium"
expand="block"

View File

@@ -25,6 +25,14 @@ import {SettingsItemComponent} from './item/settings-item.component';
import {SettingsPageComponent} from './page/settings-page.component';
import {SettingTranslatePipe} from './setting-translate.pipe';
import {SettingsProvider} from './settings.provider';
import {CalendarSyncSettingsComponent} from './page/calendar-sync-settings.component';
import {ScheduleProvider} from '../calendar/schedule.provider';
import {FileOpener} from '@ionic-native/file-opener/ngx';
import {ThingTranslatePipe} from '../../translation/thing-translate.pipe';
import {ScheduleSyncService} from '../background/schedule/schedule-sync.service';
import {CalendarService} from '../calendar/calendar.service';
import {CalendarModule} from '../calendar/calendar.module';
import {BackgroundModule} from '../background/background.module';
const settingsRoutes: Routes = [
{path: 'settings', component: SettingsPageComponent},
@@ -38,16 +46,27 @@ const settingsRoutes: Routes = [
SettingsPageComponent,
SettingsItemComponent,
SettingTranslatePipe,
CalendarSyncSettingsComponent,
],
exports: [SettingsItemComponent, SettingTranslatePipe],
imports: [
CommonModule,
FormsModule,
CalendarModule,
BackgroundModule,
IonicModule.forRoot(),
TranslateModule.forChild(),
ThingTranslateModule.forChild(),
RouterModule.forChild(settingsRoutes),
],
providers: [ConfigProvider, SettingsProvider],
providers: [
ScheduleSyncService,
ConfigProvider,
SettingsProvider,
CalendarService,
ScheduleProvider,
FileOpener,
ThingTranslatePipe,
],
})
export class SettingsModule {}

View File

@@ -26,11 +26,13 @@ import {
SettingValuesContainer,
STORAGE_KEY_SETTING_VALUES,
} from './settings.provider';
import {ScheduleSyncService} from '../background/schedule/schedule-sync.service';
describe('SettingsProvider', () => {
let configProviderSpy: jasmine.SpyObj<ConfigProvider>;
let settingsProvider: SettingsProvider;
let storageProviderSpy: jasmine.SpyObj<StorageProvider>;
let scheduleSyncServiceSpy: jasmine.SpyObj<ScheduleSyncService>;
beforeEach(async () => {
const storageProviderMethodSpy = jasmine.createSpyObj('StorageProvider', [
@@ -42,6 +44,10 @@ describe('SettingsProvider', () => {
const configProviderMethodSpy = jasmine.createSpyObj('ConfigProvider', [
'getValue',
]);
scheduleSyncServiceSpy = jasmine.createSpyObj('ScheduleSyncService', [
'getDifferences',
'postDifferencesNotification',
]);
TestBed.configureTestingModule({
imports: [],
@@ -55,6 +61,10 @@ describe('SettingsProvider', () => {
provide: ConfigProvider,
useValue: configProviderMethodSpy,
},
{
provide: ScheduleSyncService,
useValue: scheduleSyncServiceSpy,
},
],
});
configProviderSpy = TestBed.get(ConfigProvider);