diff --git a/ios/App/App/Info.plist b/ios/App/App/Info.plist index 00e212b3..7c0d7032 100644 --- a/ios/App/App/Info.plist +++ b/ios/App/App/Info.plist @@ -24,6 +24,10 @@ NSCameraUsageDescription Scanning Attendee badge QR codes + NSCalendarsWriteOnlyAccessUsageDescription + Add PyCon US sessions to your calendar + NSCalendarsUsageDescription + Add PyCon US sessions to your calendar UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/ios/App/Podfile b/ios/App/Podfile index 9f4a14ff..3737cdf1 100644 --- a/ios/App/Podfile +++ b/ios/App/Podfile @@ -24,6 +24,7 @@ def capacitor_pods pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share' pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen' pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar' + pod 'EbarooniCapacitorCalendar', :path => '../../node_modules/@ebarooni/capacitor-calendar' end target 'PyCon US' do diff --git a/package-lock.json b/package-lock.json index b7b11ed8..8d6fccce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@capacitor/splash-screen": "^7.0.0", "@capacitor/status-bar": "^7.0.0", "@ciag/ngx-pinch-zoom": "13.3.0", + "@ebarooni/capacitor-calendar": "^7.2.0", "@ionic/angular": "^8.4.3", "@ionic/storage-angular": "^4.0.0", "@json-editor/json-editor": "2.14.1", @@ -3567,6 +3568,18 @@ "node": ">=10.0.0" } }, + "node_modules/@ebarooni/capacitor-calendar": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@ebarooni/capacitor-calendar/-/capacitor-calendar-7.2.0.tgz", + "integrity": "sha512-V5Qr4n7Aoc9BIcS7ap0dC6QKiAC2HFnV1QBa2gdu7hMSOlYCNMQ38ar7HoeQ6QJtWt8/n2nipczCtAv3XANpOg==", + "license": "MIT", + "engines": { + "node": ">=20.12.2" + }, + "peerDependencies": { + "@capacitor/core": ">=7.0.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.20.1", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.1.tgz", diff --git a/package.json b/package.json index e5969bfc..72bdca2c 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "@capacitor/splash-screen": "^7.0.0", "@capacitor/status-bar": "^7.0.0", "@ciag/ngx-pinch-zoom": "13.3.0", + "@ebarooni/capacitor-calendar": "^7.2.0", "@ionic/angular": "^8.4.3", "@ionic/storage-angular": "^4.0.0", "@json-editor/json-editor": "2.14.1", diff --git a/src/app/pages/schedule-list/schedule-list.page.html b/src/app/pages/schedule-list/schedule-list.page.html index 448fc806..143edac2 100644 --- a/src/app/pages/schedule-list/schedule-list.page.html +++ b/src/app/pages/schedule-list/schedule-list.page.html @@ -18,6 +18,13 @@ +
+ + + Favorite all {{trackName | trackName : 'plural'}} + +
+ diff --git a/src/app/pages/schedule-list/schedule-list.page.ts b/src/app/pages/schedule-list/schedule-list.page.ts index 36ec736f..71f74d1b 100644 --- a/src/app/pages/schedule-list/schedule-list.page.ts +++ b/src/app/pages/schedule-list/schedule-list.page.ts @@ -2,7 +2,10 @@ import { Component, ChangeDetectorRef, ViewChild, OnInit } from '@angular/core'; import { ConferenceData } from '../../providers/conference-data'; import { ActivatedRoute } from '@angular/router'; import { Config, InfiniteScrollCustomEvent, LoadingController } from '@ionic/angular'; +import { InAppBrowser, DefaultWebViewOptions } from '@capacitor/inappbrowser'; import { LiveUpdateService } from '../../providers/live-update.service'; +import { UserData } from '../../providers/user-data'; +import { environment } from '../../../environments/environment'; const slugify = str => str @@ -39,10 +42,11 @@ export class ScheduleListPage implements OnInit { constructor( public confData: ConferenceData, public config: Config, - private changeDetection: ChangeDetectorRef, + private changeDetectorRef: ChangeDetectorRef, private loadingCtrl: LoadingController, private route: ActivatedRoute, public liveUpdateService: LiveUpdateService, + private userData: UserData, ) { } @@ -72,6 +76,13 @@ export class ScheduleListPage implements OnInit { this.reloadSessions(); } + async favoriteAll() { + const sessions = this.displaySessions?.filter(s => !s.hide && s.id) || []; + const ids = sessions.map(s => String(s.id)); + await this.userData.addFavorites(ids); + this.changeDetectorRef.detectChanges(); + } + organizeSessionsByDay() { if (!this.isOpenSpaceView) return; diff --git a/src/app/pages/session-detail/session-detail.html b/src/app/pages/session-detail/session-detail.html index ce266785..dc8b3d75 100644 --- a/src/app/pages/session-detail/session-detail.html +++ b/src/app/pages/session-detail/session-detail.html @@ -4,6 +4,9 @@ + + + diff --git a/src/app/pages/session-detail/session-detail.ts b/src/app/pages/session-detail/session-detail.ts index c206d567..7d42aa24 100644 --- a/src/app/pages/session-detail/session-detail.ts +++ b/src/app/pages/session-detail/session-detail.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core'; import { InAppBrowser, DefaultWebViewOptions } from '@capacitor/inappbrowser'; - +import { CapacitorCalendar } from '@ebarooni/capacitor-calendar'; import { ConferenceData } from '../../providers/conference-data'; import { ActivatedRoute } from '@angular/router'; import { UserData } from '../../providers/user-data'; @@ -60,6 +60,25 @@ export class SessionDetailPage { } } + async addToCalendar() { + if (!this.session) return; + + await CapacitorCalendar.requestWriteOnlyCalendarAccess(); + + const speakers = this.session.speakers?.map((s: any) => s.name).join(', ') || ''; + const description = speakers ? `Speakers: ${speakers}` : ''; + + await CapacitorCalendar.createEventWithPrompt({ + title: this.session.name, + location: this.session.location || '', + description, + startDate: new Date(this.session.startUtc).getTime(), + endDate: new Date(this.session.endUtc).getTime(), + isAllDay: false, + url: environment.baseUrl + '/2026/schedule/presentation/' + this.session.id + '/', + }); + } + onDescriptionClick(event: Event) { const target = event.target as HTMLElement; const anchor = target.closest('a') as HTMLAnchorElement; diff --git a/src/app/providers/conference-data.ts b/src/app/providers/conference-data.ts index 49c8a616..49666fca 100644 --- a/src/app/providers/conference-data.ts +++ b/src/app/providers/conference-data.ts @@ -111,6 +111,8 @@ export class ConferenceData { "speakerNames": [], "timeStart": start.toLocaleTimeString([], {timeZone: environment.timezone, hour: 'numeric', minute:'2-digit'}).toLowerCase(), "timeEnd": end.toLocaleString([], {timeZone: environment.timezone, hour: 'numeric', minute:'2-digit'}).toLowerCase(), + "startUtc": openSpace.start, + "endUtc": openSpace.end, "track": "Open Space", "tracks": ["open-space"], "id": openSpace.conf_key + 9000, @@ -241,6 +243,8 @@ export class ConferenceData { "speakerNames": slot.authors, "timeStart": start.toLocaleTimeString([], {timeZone: environment.timezone, hour: 'numeric', minute:'2-digit'}).toLowerCase(), "timeEnd": end.toLocaleString([], {timeZone: environment.timezone, hour: 'numeric', minute:'2-digit'}).toLowerCase(), + "startUtc": slot.start, + "endUtc": slot.end, "track": slot.kind.charAt(0).toUpperCase() + slot.kind.slice(1), "tracks": [slot.kind.charAt(0).toUpperCase() + slot.kind.slice(1)], "id": slot.conf_key, @@ -304,6 +308,8 @@ export class ConferenceData { "speakerNames": shared_session.authors, "timeStart": start.toLocaleTimeString([], {timeZone: environment.timezone, hour: 'numeric', minute:'2-digit'}).toLowerCase(), "timeEnd": end.toLocaleString([], {timeZone: environment.timezone, hour: 'numeric', minute:'2-digit'}).toLowerCase(), + "startUtc": slot.start, + "endUtc": slot.end, "trackSlug": slot.kind + 's', "track": slot.kind.charAt(0).toUpperCase() + slot.kind.slice(1), "tracks": [slot.kind.charAt(0).toUpperCase() + slot.kind.slice(1)], @@ -375,6 +381,8 @@ export class ConferenceData { "speakerNames": [], "timeStart": start.toLocaleTimeString([], {timeZone: environment.timezone, hour: 'numeric', minute:'2-digit'}).toLowerCase(), "timeEnd": end.toLocaleString([], {timeZone: environment.timezone, hour: 'numeric', minute:'2-digit'}).toLowerCase(), + "startUtc": slot.start, + "endUtc": slot.end, "track": trackName, "tracks": [trackName], "id": slot.kind + "-" + slot.room + "-" + slot.start, diff --git a/src/app/providers/user-data.ts b/src/app/providers/user-data.ts index c881f866..48fd3ab8 100644 --- a/src/app/providers/user-data.ts +++ b/src/app/providers/user-data.ts @@ -103,6 +103,24 @@ export class UserData { }); } + async addFavorites(sessionIds: string[]): Promise { + const data = await this.storage.get('favorite_sessions'); + this.favorites = (data === null) ? [] : data; + + const ids = sessionIds.map(String); + const newIds = ids.filter(id => this.favorites.indexOf(id) === -1); + if (newIds.length === 0) return; + + this.favorites.push(...newIds); + await this.storage.set('favorite_sessions', this.favorites); + this.isLoggedIn().then((loggedIn) => { + if (loggedIn) { + this.pycon.patchUserData({ favorites: this.favorites }); + } + }); + this.favoritesSubject.next(); + } + removeFavorite(sessionId: string): void { this.storage.get('favorite_sessions').then((data) => { this.favorites = (data === null)? [] : data;