Showing preview only (4,043K chars total). Download the full file or copy to clipboard to get everything.
Repository: PapillonApp/Papillon
Branch: dev
Commit: 638fc1659495
Files: 517
Total size: 3.7 MB
Directory structure:
gitextract_hxvgag7x/
├── .github/
│ ├── CONTRIBUTING.md
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug.yml
│ │ ├── config.yml
│ │ └── feature.yml
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── bot/
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── issue/
│ │ │ ├── index.ts
│ │ │ ├── labeler.ts
│ │ │ └── message.ts
│ │ └── tsconfig.json
│ └── workflows/
│ ├── build-android.yml
│ ├── merge.yml
│ └── release.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── CODEOWNERS
├── LICENSE
├── README.md
├── app/
│ ├── (features)/
│ │ ├── (cards)/
│ │ │ ├── cards.tsx
│ │ │ ├── qrcode.tsx
│ │ │ └── specific.tsx
│ │ ├── (news)/
│ │ │ └── specific.tsx
│ │ ├── attendance.tsx
│ │ └── soon.tsx
│ ├── (modals)/
│ │ ├── address.tsx
│ │ ├── course.tsx
│ │ ├── grade.tsx
│ │ ├── news.tsx
│ │ ├── notifications.tsx
│ │ ├── profile.tsx
│ │ ├── task.tsx
│ │ ├── wallpaper.tsx
│ │ └── wrapped/
│ │ ├── _layout.tsx
│ │ ├── index.tsx
│ │ └── stories/
│ │ ├── consent.tsx
│ │ └── welcome.tsx
│ ├── (new)/
│ │ ├── _layout.tsx
│ │ └── event.tsx
│ ├── (onboarding)/
│ │ ├── _layout.tsx
│ │ ├── ageSelection.tsx
│ │ ├── components/
│ │ │ ├── LoginView.tsx
│ │ │ ├── OnboardingSelector.tsx
│ │ │ ├── OnboardingWebView.tsx
│ │ │ └── ageSelection/
│ │ │ └── illustrations/
│ │ │ ├── highSchool.tsx
│ │ │ ├── middleSchool.tsx
│ │ │ ├── parents.tsx
│ │ │ ├── supSchool.tsx
│ │ │ └── teacher.tsx
│ │ ├── restaurants/
│ │ │ ├── _layout.tsx
│ │ │ ├── alise.tsx
│ │ │ ├── ard.tsx
│ │ │ ├── izly.tsx
│ │ │ ├── method.tsx
│ │ │ ├── turboself.tsx
│ │ │ └── turboselfHost.tsx
│ │ ├── serviceSelection.tsx
│ │ ├── services/
│ │ │ ├── appscho/
│ │ │ │ ├── _layout.tsx
│ │ │ │ ├── credentials.tsx
│ │ │ │ ├── list.tsx
│ │ │ │ └── webview.tsx
│ │ │ ├── ed/
│ │ │ │ ├── _layout.tsx
│ │ │ │ └── credentials.tsx
│ │ │ ├── lannion/
│ │ │ │ ├── _layout.tsx
│ │ │ │ └── credentials.tsx
│ │ │ ├── multi/
│ │ │ │ ├── _layout.tsx
│ │ │ │ └── credentials.tsx
│ │ │ ├── pronote/
│ │ │ │ ├── 2fa.tsx
│ │ │ │ ├── _layout.tsx
│ │ │ │ ├── browser.tsx
│ │ │ │ ├── locate.tsx
│ │ │ │ ├── qrcode.tsx
│ │ │ │ ├── select.tsx
│ │ │ │ └── url.tsx
│ │ │ └── skolengo/
│ │ │ ├── _layout.tsx
│ │ │ ├── locate.tsx
│ │ │ └── webview.tsx
│ │ ├── utils/
│ │ │ ├── constants.tsx
│ │ │ └── fetchSchools.ts
│ │ └── welcome.tsx
│ ├── (settings)/
│ │ ├── _layout.tsx
│ │ ├── about.tsx
│ │ ├── accounts.tsx
│ │ ├── cards.tsx
│ │ ├── contributors.tsx
│ │ ├── edit_subject.tsx
│ │ ├── language.tsx
│ │ ├── magic.tsx
│ │ ├── personalization.tsx
│ │ ├── services.tsx
│ │ ├── settings.tsx
│ │ ├── subject_personalization.tsx
│ │ ├── tabs.tsx
│ │ └── transport.tsx
│ ├── (tabs)/
│ │ ├── _layout.tsx
│ │ ├── calendar/
│ │ │ ├── _layout.tsx
│ │ │ ├── components/
│ │ │ │ ├── CalendarDay.tsx
│ │ │ │ ├── CalendarHeader.tsx
│ │ │ │ └── EmptyCalendar.tsx
│ │ │ ├── event/
│ │ │ │ └── [id].tsx
│ │ │ ├── hooks/
│ │ │ │ ├── useCalendarState.ts
│ │ │ │ └── useTimetableData.ts
│ │ │ ├── icals.tsx
│ │ │ └── index.tsx
│ │ ├── grades/
│ │ │ ├── _layout.tsx
│ │ │ ├── atoms/
│ │ │ │ ├── Averages.tsx
│ │ │ │ ├── FeaturesMap.tsx
│ │ │ │ └── Subject.tsx
│ │ │ ├── features/
│ │ │ │ └── ScodocUES.tsx
│ │ │ ├── hooks/
│ │ │ │ └── useGradeInfluence.ts
│ │ │ ├── index.tsx
│ │ │ ├── modals/
│ │ │ │ ├── AboutAverages.tsx
│ │ │ │ └── SubjectInfo.tsx
│ │ │ └── utils/
│ │ │ └── graph.ts
│ │ ├── index/
│ │ │ ├── _layout.tsx
│ │ │ ├── atoms/
│ │ │ │ ├── HomeHeader.tsx
│ │ │ │ ├── HomeTopBar.tsx
│ │ │ │ ├── UserProfile.tsx
│ │ │ │ ├── Wallpaper.tsx
│ │ │ │ └── WrappedBanner.tsx
│ │ │ ├── components/
│ │ │ │ ├── HomeHeaderButton.ios.tsx
│ │ │ │ ├── HomeHeaderButton.tsx
│ │ │ │ ├── HomeTopBarButton.ios.tsx
│ │ │ │ ├── HomeTopBarButton.tsx
│ │ │ │ └── HomeWidget.tsx
│ │ │ ├── hooks/
│ │ │ │ ├── useHomeData.ts
│ │ │ │ ├── useHomeHeaderData.ts
│ │ │ │ ├── useTimetableWidgetData.ts
│ │ │ │ └── useUserProfileData.ts
│ │ │ ├── index.old.tsx
│ │ │ ├── index.tsx
│ │ │ └── widgets/
│ │ │ ├── Grades.tsx
│ │ │ └── timetable.tsx
│ │ ├── news/
│ │ │ ├── _layout.tsx
│ │ │ └── index.tsx
│ │ └── tasks/
│ │ ├── _layout.tsx
│ │ ├── atoms/
│ │ │ ├── DateHeader.tsx
│ │ │ ├── EmptyState.tsx
│ │ │ └── TasksSummary.tsx
│ │ ├── components/
│ │ │ ├── TaskItem.tsx
│ │ │ ├── TasksHeader.tsx
│ │ │ ├── TasksList.tsx
│ │ │ └── WeekPicker.tsx
│ │ ├── hooks/
│ │ │ ├── useHomeworkData.ts
│ │ │ ├── useMagicPrediction.ts
│ │ │ ├── useTaskFilters.ts
│ │ │ └── useWeekSelection.ts
│ │ └── index.tsx
│ ├── _layout.tsx
│ ├── alert.tsx
│ ├── changelog.tsx
│ ├── consent.tsx
│ ├── demo.tsx
│ └── devmode.tsx
├── app.config.ts
├── assets/
│ ├── app.icon/
│ │ └── icon.json
│ └── lotties/
│ ├── alise.json
│ ├── ard.json
│ ├── connexion.json
│ ├── izly.json
│ ├── link.json
│ ├── location.json
│ ├── onboarding.json
│ ├── qr-code.json
│ ├── school-services.json
│ ├── search.json
│ ├── self.json
│ ├── turboself.json
│ └── uni-services.json
├── babel.config.js
├── components/
│ ├── ActivityIndicator.tsx
│ ├── AndroidHeaderBackground.tsx
│ ├── AppColorsSelector.tsx
│ ├── AppProviders.tsx
│ ├── DevModeNotice.tsx
│ ├── FakeSplash.tsx
│ ├── Log/
│ │ └── LogIcon.tsx
│ ├── ModalOverhead.tsx
│ ├── RootNavigator.tsx
│ ├── SettingsHeader.tsx
│ ├── Transit.tsx
│ ├── UnderConstructionNotice.tsx
│ ├── onboarding/
│ │ ├── OnboardingBackButton.tsx
│ │ ├── OnboardingInput.tsx
│ │ ├── OnboardingScrollingFlatList.tsx
│ │ └── OnboardingWebview.tsx
│ └── router/
│ └── BottomTabs.tsx
├── constants/
│ ├── AvailableTransportServices.ts
│ ├── LayoutScreenOptions.ts
│ └── UnicodeEmojis.ts
├── crowdin.yml
├── database/
│ ├── DatabaseProvider.tsx
│ ├── index.ts
│ ├── mappers/
│ │ ├── attendance.ts
│ │ ├── balances.ts
│ │ ├── canteen.ts
│ │ ├── chats.ts
│ │ ├── course.ts
│ │ ├── grade.ts
│ │ ├── kids.ts
│ │ └── subject.ts
│ ├── models/
│ │ ├── Attendance.ts
│ │ ├── Balance.ts
│ │ ├── CanteenHistory.ts
│ │ ├── CanteenMenu.ts
│ │ ├── Chat.ts
│ │ ├── Event.ts
│ │ ├── Grades.ts
│ │ ├── Homework.ts
│ │ ├── Ical.ts
│ │ ├── Kid.ts
│ │ ├── News.ts
│ │ ├── Subject.ts
│ │ └── Timetable.ts
│ ├── schema.ts
│ ├── useAttendance.ts
│ ├── useBalance.ts
│ ├── useCanteen.ts
│ ├── useChat.ts
│ ├── useEvents.ts
│ ├── useEventsById.ts
│ ├── useGrades.ts
│ ├── useHomework.ts
│ ├── useIcals.ts
│ ├── useKids.ts
│ ├── useNews.ts
│ ├── usePeriodsCache.tsx
│ ├── useSubject.ts
│ ├── useTimetable.ts
│ └── utils/
│ ├── initialization.ts
│ └── safeTransaction.ts
├── eslint.config.mjs
├── hooks/
│ └── useAppInitialization.ts
├── ios/
│ └── Papillon/
│ ├── AppDelegate.swift
│ ├── Images.xcassets/
│ │ ├── AppIcon.appiconset/
│ │ │ └── Contents.json
│ │ ├── Contents.json
│ │ ├── SplashScreenBackground.colorset/
│ │ │ └── Contents.json
│ │ └── SplashScreenLegacy.imageset/
│ │ └── Contents.json
│ ├── Info.plist
│ ├── Papillon-Bridging-Header.h
│ ├── Papillon.entitlements
│ ├── PrivacyInfo.xcprivacy
│ ├── SplashScreen.storyboard
│ └── Supporting/
│ └── Expo.plist
├── locales/
│ ├── af.json
│ ├── ar.json
│ ├── bg.json
│ ├── bn.json
│ ├── br.json
│ ├── cs.json
│ ├── da.json
│ ├── de.json
│ ├── el.json
│ ├── en.json
│ ├── es.json
│ ├── et.json
│ ├── fa.json
│ ├── fi.json
│ ├── fr.json
│ ├── he.json
│ ├── hi.json
│ ├── hr.json
│ ├── hu.json
│ ├── id.json
│ ├── it.json
│ ├── ja.json
│ ├── ko.json
│ ├── ms.json
│ ├── nl.json
│ ├── no.json
│ ├── pl.json
│ ├── pt.json
│ ├── ro.json
│ ├── ru.json
│ ├── sk.json
│ ├── sq.json
│ ├── sv.json
│ ├── sw.json
│ ├── th.json
│ ├── tr.json
│ ├── uk.json
│ ├── ur.json
│ └── vi.json
├── metro.config.js
├── package.json
├── patches/
│ ├── countly-sdk-react-native-bridge+25.4.0.patch
│ └── react-native-fast-tflite+1.6.1.patch
├── scripts/
│ └── generateEmojiList.sh
├── services/
│ ├── alise/
│ │ ├── balance.ts
│ │ ├── history.ts
│ │ ├── index.ts
│ │ └── refresh.ts
│ ├── appscho/
│ │ ├── index.ts
│ │ ├── news.ts
│ │ ├── refresh.ts
│ │ └── timetable.ts
│ ├── ard/
│ │ ├── balance.ts
│ │ ├── history.ts
│ │ ├── index.ts
│ │ └── refresh.ts
│ ├── ecoledirecte/
│ │ ├── attendance.ts
│ │ ├── balance.ts
│ │ ├── chat.ts
│ │ ├── grades.ts
│ │ ├── homework.ts
│ │ ├── index.ts
│ │ ├── news.ts
│ │ ├── qrcode.ts
│ │ ├── refresh.ts
│ │ └── timetable.ts
│ ├── errors/
│ │ └── AuthenticationError.ts
│ ├── izly/
│ │ ├── balances.ts
│ │ ├── history.ts
│ │ ├── index.ts
│ │ ├── qrcode.ts
│ │ └── refresh.ts
│ ├── lannion/
│ │ ├── attendance.ts
│ │ ├── grades.ts
│ │ ├── index.ts
│ │ └── module/
│ │ ├── api.ts
│ │ ├── client.ts
│ │ ├── index.ts
│ │ └── types.ts
│ ├── local/
│ │ ├── .ical.ts.swp
│ │ ├── event-converter.ts
│ │ ├── event-filter.ts
│ │ ├── ical-database.ts
│ │ ├── ical-utils.ts
│ │ ├── ical.ts
│ │ └── parsers/
│ │ ├── ade-parser.ts
│ │ ├── hyperplanning-parser.ts
│ │ ├── ical-event-parser.ts
│ │ └── schools/
│ │ └── univrennes1_parser.ts
│ ├── multi/
│ │ ├── index.ts
│ │ ├── news.ts
│ │ ├── refresh.ts
│ │ └── timetable.ts
│ ├── pronote/
│ │ ├── attendance.ts
│ │ ├── canteen.ts
│ │ ├── chat.ts
│ │ ├── grades.ts
│ │ ├── homework.ts
│ │ ├── index.ts
│ │ ├── news.ts
│ │ ├── refresh.ts
│ │ └── timetable.ts
│ ├── shared/
│ │ ├── attachment.ts
│ │ ├── attendance.ts
│ │ ├── balance.ts
│ │ ├── canteen.ts
│ │ ├── chat.ts
│ │ ├── grade.ts
│ │ ├── homework.ts
│ │ ├── index.ts
│ │ ├── kid.ts
│ │ ├── news.ts
│ │ ├── timetable.ts
│ │ └── types.ts
│ ├── skolengo/
│ │ ├── attendance.ts
│ │ ├── chat.ts
│ │ ├── grades.ts
│ │ ├── homework.ts
│ │ ├── index.ts
│ │ ├── kid.ts
│ │ ├── news.ts
│ │ ├── refresh.ts
│ │ └── timetable.ts
│ ├── transit/
│ │ ├── fetcher/
│ │ │ ├── Fetcher.ts
│ │ │ └── endpoints.ts
│ │ ├── index.ts
│ │ └── models/
│ │ ├── Alerts.ts
│ │ ├── ArrivalSchedule.ts
│ │ ├── DepartureLegs.ts
│ │ ├── Itineraries.ts
│ │ ├── ItinerariesPlanDetails.ts
│ │ ├── Location.ts
│ │ ├── MatchedSubstring.ts
│ │ ├── Period.ts
│ │ ├── Place.ts
│ │ ├── PlaceDetails.ts
│ │ ├── PlaceSuggestion.ts
│ │ ├── PlanDetails.ts
│ │ ├── PlanResult.ts
│ │ ├── Route.ts
│ │ ├── RouteLegs.ts
│ │ ├── Station.ts
│ │ ├── Stop.ts
│ │ ├── StopSchedule.ts
│ │ ├── Suggestions.ts
│ │ ├── TransitDepartures.ts
│ │ └── Vehicle.ts
│ └── turboself/
│ ├── balance.ts
│ ├── booking.ts
│ ├── history.ts
│ ├── index.ts
│ ├── qrcode.ts
│ └── refresh.ts
├── stores/
│ ├── account/
│ │ ├── index.ts
│ │ └── types.ts
│ ├── flags/
│ │ └── index.ts
│ ├── global/
│ │ ├── index.ts
│ │ └── serializer.ts
│ ├── logs/
│ │ ├── index.ts
│ │ └── types.ts
│ ├── magic/
│ │ ├── index.ts
│ │ └── types.ts
│ └── settings/
│ ├── index.ts
│ └── types.ts
├── stubs/
│ └── appscho/
│ ├── index.d.ts
│ ├── index.js
│ └── package.json
├── tsconfig.json
├── ui/
│ ├── components/
│ │ ├── ActionMenu.tsx
│ │ ├── ActivityIndicator.tsx
│ │ ├── AlertProvider.tsx
│ │ ├── AnimatedNumber.tsx
│ │ ├── AnimatedPressable.tsx
│ │ ├── Avatar.tsx
│ │ ├── Button.tsx
│ │ ├── Calendar.tsx
│ │ ├── ChipButton.tsx
│ │ ├── CircularProgress.tsx
│ │ ├── CompactGrade.tsx
│ │ ├── CompactTask.tsx
│ │ ├── ContainedNumber.tsx
│ │ ├── Course.tsx
│ │ ├── Dynamic.tsx
│ │ ├── EmptyItem.tsx
│ │ ├── ErrorBoundary.tsx
│ │ ├── Grade.tsx
│ │ ├── Icon.tsx
│ │ ├── Item.tsx
│ │ ├── List.tsx
│ │ ├── NativeHeader.tsx
│ │ ├── Pattern/
│ │ │ ├── CrossPattern.tsx
│ │ │ └── Pattern.tsx
│ │ ├── Search.tsx
│ │ ├── SectionHeader.tsx
│ │ ├── SkeletonView.tsx
│ │ ├── Stack.tsx
│ │ ├── Subject.tsx
│ │ ├── TabFlatList.tsx
│ │ ├── TabHeader.tsx
│ │ ├── TabHeaderTitle.tsx
│ │ ├── TableFlatList.tsx
│ │ ├── Task.tsx
│ │ ├── Typography.tsx
│ │ └── ViewContainer.tsx
│ ├── hooks/
│ │ └── useKeyboardHeight.ts
│ ├── native/
│ │ └── NativeSwitch.tsx
│ ├── new/
│ │ ├── Button.tsx
│ │ ├── Divider.tsx
│ │ ├── List.tsx
│ │ ├── ListTouchableContext.ts
│ │ ├── RippleEffect.tsx
│ │ ├── TextInput.tsx
│ │ ├── Typography.tsx
│ │ └── symbols/
│ │ └── PapillonLogo.tsx
│ ├── package.json
│ └── utils/
│ ├── Animation.ts
│ ├── Corners.ts
│ ├── Duration.ts
│ ├── IsLiquidGlass.ts
│ └── Transition.ts
└── utils/
├── adjustColor.ts
├── attachments/
│ └── helper.ts
├── chats/
│ ├── colors.ts
│ └── initials.ts
├── colorCheck.ts
├── colors.ts
├── endpoints.ts
├── format/
│ ├── formatSchoolName.ts
│ └── html.ts
├── generateId.ts
├── github/
│ └── contributors.ts
├── grades/
│ ├── algorithms/
│ │ ├── helpers.ts
│ │ ├── median.ts
│ │ ├── subject.ts
│ │ ├── time.ts
│ │ └── weighted.ts
│ └── helper/
│ └── period.ts
├── i18n.ts
├── logger/
│ ├── consent.ts
│ └── logger.ts
├── magic/
│ ├── ModelManager.ts
│ ├── prediction.ts
│ ├── regex/
│ │ └── homeworks.json
│ └── updater/
│ ├── extract.ts
│ ├── fileUtils.ts
│ ├── index.ts
│ ├── integrity.ts
│ ├── manifest.ts
│ ├── network.ts
│ ├── semver.ts
│ └── types.ts
├── native/
│ ├── AnimatedNavigator.ts
│ ├── georeverse.ts
│ └── position.ts
├── news/
│ ├── cleanUpHTMLNews.ts
│ └── getAttachmentIcon.ts
├── notification/
│ └── reminder/
│ └── helper.ts
├── pronote/
│ ├── fetcher.ts
│ └── name.ts
├── restaurant/
│ └── detect-price.ts
├── services/
│ ├── helper.ts
│ └── periods.ts
├── subjects/
│ ├── colors.ts
│ ├── emoji.ts
│ ├── lesson_formats.json
│ ├── name.ts
│ └── utils.ts
├── theme/
│ ├── AndroidBackButton.tsx
│ ├── ScreenOptions.tsx
│ └── Theme.ts
├── transport.ts
└── uuid/
└── uuid.ts
================================================
FILE CONTENTS
================================================
================================================
FILE: .github/CONTRIBUTING.md
================================================
# Règles de contribution
## 🔐 Signaler une vulnérabilité
Nous prenons la sécurité **très au sérieux**. Si tu découvres une **vulnérabilité** dans **Papillon**, merci de suivre notre [**politique de sécurité**](https://github.com/PapillonApp/Papillon/.github/blob/main/SECURITY.md) : **n’ouvre pas d’issue publique** et signale-la directement à l’adresse suivante : <mark style="color:$danger;">**support@papillon.bzh**</mark>.
## 📤 Soumettre une Pull Request
Nous serions ravis d’intégrer tes modifications à Papillon. Cependant, avant de fusionner avec la branche principale, merci de respecter les règles ci-dessous. En cas de non-respect, ta Pull Request sera considérée comme **invalide** et ne sera pas traitée tant que les corrections nécessaires n’auront pas été apportées.
* [x] Tu ne dois pas soumettre plusieurs fonctionnalités ou corrections de bugs dans une même Pull Request. Chaque modification doit rester isolée afin de faciliter son traitement et, si nécessaire, son éventuel retour.
* [x] Si ta Pull Request concerne des changements majeurs, merci d'ouvrir une Issue pour discuter avec les mainteneurs de la stratégie à adopter pour ne pas faire de gros travaux pour rien.
* [x] Ta Pull Request doit respecter les conventions [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) et [Conventional Branch](https://conventional-branch.github.io/), ainsi que le style de code de l’application.
* [x] Si ta Pull Request modifie une partie documentée, comme la structure, l’architecture ou autre, assure-toi d’avoir mis à jour la documentation en conséquence.
* [x] J'ai testé mes changements sur iOS et Android et l'application compile correctement.
* [x] J'utilise un langage informel (tutoiement).
#### ❓ Comment vérifier le Lint ?
Par défaut, en effectuant la commande ci-dessous, ESLint essayera de résoudre automatiquement les problèmes, s'il n'y arrive pas, tu dois les corriger manuellement.
```bash
$ npm run lint
```
## 📥 Ouvrir une issue
Avant d’ouvrir une issue, assure-toi d’utiliser la **dernière version** de **Papillon**, teste si le problème persiste après mise à jour, et vérifie qu’une issue similaire n’a pas **déjà** été ouverte. Une issue bien écrite facilite son traitement et est toujours plus agréable pour nous à lire, afin que le traitement se passe au mieux, voici quelques conseils :
1. **Elle porte un nom explicite**, qui permet d’identifier **immédiatement** son **sujet principal**.
2. **Aucune issue semblable n’existe déjà** : il est inutile d’en créer plusieurs pour le même problème, **cela ne fait que ralentir son traitement**. Si tu es concerné par une issue existante, **réagis** simplement avec un👍
3. **Elle contient une description détaillée**, si c’est une fonctionnalité, elle est **clairement expliquée**, idéalement accompagnée d’une **capture d’écran** ou d’un **design Figma,** s’il s’agit d’un bug, la description précise **le comportement actuel**, **le comportement attendu**, ainsi que **les étapes pour le reproduire**.
4. Si tu rencontres **le même problème** ou souhaite **la même fonctionnalité**, privilégie les **réactions** aux commentaires.
5. **Compléte le modèle fourni lors de la création de ton issue**, il a été rédigé pour t'aider à **structurer ta demande, ne rien oublier d’important et gagner du temps**.
================================================
FILE: .github/FUNDING.yml
================================================
ko_fi: thepapillonapp
================================================
FILE: .github/ISSUE_TEMPLATE/bug.yml
================================================
name: 🐛 Signaler un bug
description: Signaler des bugs nous permet d'améliorer Papillon !
title: "[Bug]: "
body:
- type: textarea
attributes:
label: Description du bug
description: Plus il y a de détails, plus vite nous pourrons trouver le bug !
placeholder: La connexion à mon établissement ne fonctionne pas, j'ai un chargement infini lors de la connexion
validations:
required: true
- type: textarea
attributes:
label: Étapes à reproduire
description: Comment pouvons-nous reproduire le bug ?
placeholder: |
1. Ouvrir l'application
2. Se connecter à l'établissement
3. Observer le comportement
validations:
required: true
- type: textarea
attributes:
label: Comportement attendu
description: Ce que Papillon devrait faire
placeholder: Que la connexion à mon établissement fonctionne et qu'il n'y ait pas de chargement infini
validations:
required: true
- type: input
attributes:
label: Appareil
description: Sur quel appareil tu as rencontré ce bug ?
placeholder: iPhone 13, Samsung Galaxy S23...
validations:
required: true
- type: input
attributes:
label: Version du système d'exploitation
description: Paramètres (du Téléphone) -> À propos (Android)/Général (Apple)
placeholder: iOS 18, Android 15...
validations:
required: true
- type: dropdown
attributes:
label: Papillon testé depuis
description: Tu as testé/installé Papillon depuis...
options:
- Play/App Store (version stable)
- Play Store/TestFlight (version bêta)
validations:
required: true
- type: input
attributes:
label: Version utilisée
description: Paramètres (de Papillon) -> Version affichée en bas de la page
placeholder: "8.0.0"
validations:
required: true
- type: dropdown
attributes:
label: Service scolaire/cantine
description: 🎒 = Service scolaire et 🍽️ = Service de cantine
options:
- 🎒🦋 Pronote
- 🎒🟦 ÉcoleDirecte
- 🎒🟡 Skolengo
- 🎒🏫 Universités et autres (à préciser dans la description du bug)
- 🍽️🟢 Alise
- 🍽️🔴 Turboself
- 🍽️🟣 ARD
- 🍽️🔵 Izly
validations:
required: true
- type: textarea
attributes:
label: "Capture(s) d'écran / vidéo"
description: Cela permettra une résolution encore plus rapide du bug
placeholder: Il faut cliquer sur l'icône 📎 pour pouvoir importer une/des photo(s)/vidéo(s)
validations:
required: false
================================================
FILE: .github/ISSUE_TEMPLATE/config.yml
================================================
blank_issues_enabled: false
contact_links:
- name: Une faille de sécurité ?
url: https://github.com/PapillonApp/Papillon/security/advisories/new
about: Nous prenons la sécurité très au sérieux. Si vous découvrez une vulnérabilité, veuillez remplir ce formulaire.
- name: Une question ? Un problème ?
url: https://discord.gg/xn3NstgjuT
about: Rejoignez nous sur Discord et obtenez de l’aide rapidement !
================================================
FILE: .github/ISSUE_TEMPLATE/feature.yml
================================================
name: ✨ Amélioration/Fonctionnalité
description: Comment pouvons-nous améliorer Papillon ?
title: "[Feature]: "
body:
- type: textarea
attributes:
label: Description de la fonctionnalité
description: Fais place à ta créativité en détaillant ce que tu veux
placeholder: Ça serait cool une intégration d'un simulateur permettant de savoir si on aura le bac/brevet
validations:
required: true
- type: textarea
attributes:
label: Davantage de détails ?
description: Une image pour montrer à quoi ça ressemblerait ? Ce n'est pas obligatoire
placeholder: Ah, même si j'ai des idées, je ne suis pas designer 😂
validations:
required: false
================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================

# Règles de contribution
> [!CAUTION]
> Afin de garantir une application stable et pérenne dans le temps, nous t'invitons à vérifier que tu as bien respecté les règles de contribution. Sans cela, ta Pull Request ne pourra pas être examinée.
- [ ] Cette Pull Request porte sur une seule fonctionnalité ou un seul correctif.
- [ ] Cette Pull Request n'est pas faite essentiellement avec de l'IA.
- [ ] Pour tout changement majeur, j’ai créé une issue afin d’échanger avec les mainteneurs de Papillon sur la meilleure façon de l’intégrer.
- [ ] Ma Pull Request respecte les conventions Conventional Commits et Conventional Branch ainsi que les conventions de codage de l'application.
- [ ] J’ai testé mes modifications sur iOS et Android, et l’application fonctionne correctement.
- [ ] J’emploie un langage informel, clair et concis dans mes messages.
- [ ] J’ai documenté mes changements de manière appropriée, soit dans la description de la Pull Request, soit dans le GitBook.
- [ ] J’ai ajouté les traductions nécessaires dans au moins un fichier de langue.
# Résumé des changements
> [!NOTE]
> Une description détaillée des changements apportés dans cette Pull Request permet un traitement plus efficace et rapide.
# Capture(s) d'écran
> [!NOTE]
> Si tes changements concernent l'interface utilisateur, inclue des captures d'écran pour illustrer tes modifications.
# Informations supplémentaires
> [!NOTE]
> Numéro des issues concernées par cette Pull Request, détail sur le fonctionnement ou les choix techniques effectués, ainsi que toute autre information pertinente.
================================================
FILE: .github/bot/package.json
================================================
{
"name": "bot",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "tsc"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"@actions/core": "^2.0.1",
"@actions/github": "^6.0.1",
"@octokit/rest": "^22.0.1",
"@types/node": "^25.0.0",
"typescript": "^5.9.3"
}
}
================================================
FILE: .github/bot/src/issue/index.ts
================================================
import * as github from '@actions/github';
import * as core from '@actions/core';
import { getLabelsFromTitle } from './labeler';
import { postWelcomeMessage } from './message';
async function run() {
try {
const context = github.context;
const token = process.env.GITHUB_TOKEN;
if (!token) {
throw new Error("GITHUB_TOKEN is missing");
}
const octokit = github.getOctokit(token);
if (!context.payload.issue) {
core.info("Not an issue event, skipping.");
return;
}
const { title, number, user } = context.payload.issue;
const { owner, repo } = context.repo;
core.info(`Processing Issue #${number}: ${title}`);
const labelsToAdd = getLabelsFromTitle(title);
if (context.payload.action === 'opened') {
labelsToAdd.push('status: needs triage');
}
if (labelsToAdd.length > 0) {
await octokit.rest.issues.addLabels({
owner,
repo,
issue_number: number,
labels: labelsToAdd,
});
core.info(`Added labels: ${labelsToAdd.join(', ')}`);
}
if (context.payload.action === 'opened') {
await postWelcomeMessage(octokit, owner, repo, number, user.login);
}
} catch (error) {
if (error instanceof Error) core.setFailed(error.message);
}
}
run();
================================================
FILE: .github/bot/src/issue/labeler.ts
================================================
export function getLabelsFromTitle(title: string): string[] {
const labels: string[] = [];
const lowerTitle = title.toLowerCase();
if (lowerTitle.startsWith('[bug]')) {
labels.push('type: bug');
} else if (lowerTitle.startsWith('[feature]')) {
labels.push('type: enhancement');
}
return labels;
}
================================================
FILE: .github/bot/src/issue/message.ts
================================================
import { Octokit } from '@octokit/rest';
export async function postWelcomeMessage(
octokit: any,
owner: string,
repo: string,
issueNumber: number,
author: string
) {
const message = `
# 🦋 Merci pour ta contribution sur Papillon
Merci pour l'intérêt que tu portes au projet **Papillon** !
Nous espérons te revoir très bientôt avec une nouvelle issue !
À très vite sur **Papillon** 🦋
`.trim();
const comments = await octokit.rest.issues.listComments({
owner,
repo,
issue_number: issueNumber,
});
const botComment = comments.data.find((comment: any) =>
comment.body && comment.body.includes("Merci pour ta contribution sur Papillon")
);
if (!botComment) {
await octokit.rest.issues.createComment({
owner,
repo,
issue_number: issueNumber,
body: message,
});
}
}
================================================
FILE: .github/bot/tsconfig.json
================================================
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"]
}
================================================
FILE: .github/workflows/build-android.yml
================================================
name: Build Android
on:
push:
branches:
- main
jobs:
build:
name: Build & Upload to Play Store
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Install dependencies
run: npm install --legacy-peer-deps
- name: Cache Expo prebuild
id: prebuild-cache
uses: actions/cache@v4
with:
path: android/
key: ${{ runner.os }}-expo-prebuild-${{ hashFiles('app.json', 'package-lock.json') }}
restore-keys: ${{ runner.os }}-expo-prebuild-
- name: Create secrets.json
run: |
echo '{
"APP_KEY": "${{ secrets.COUNTLY_APP_KEY }}",
"SALT": "${{ secrets.COUNTLY_SALT }}",
"SERVER_URL": "${{ secrets.COUNTLY_SERVER_URL }}"
}' > secrets.json
- name: Prebuild Android
if: steps.prebuild-cache.outputs.cache-hit != 'true'
run: npx expo prebuild --platform android --clean
- name: Decode keystore
run: echo "${{ secrets.KEYSTORE }}" | base64 --decode > android/app/release.keystore
- name: Calculate version code
run: |
VERSION_CODE=$(node -e "
const version = require('./package.json').version;
const cleanVersion = parseInt(version.replace(/\./g, ''));
const runNumber = parseInt(process.env.GITHUB_RUN_NUMBER || '0');
console.log(cleanVersion * 100000 + runNumber);
")
echo "VERSION_CODE=$VERSION_CODE" >> $GITHUB_ENV
- name: Configure signing
run: |
sed -i '/signingConfigs {/a\ release {\n storeFile file('\''release.keystore'\'')\n storePassword '\''${{ secrets.KEYSTORE_PASSWORD }}'\''\n keyAlias '\''${{ secrets.KEY_ALIAS }}'\''\n keyPassword '\''${{ secrets.KEY_PASSWORD }}'\''\n }' android/app/build.gradle
sed -i 's/signingConfig signingConfigs.debug/signingConfig signingConfigs.release/' android/app/build.gradle
sed -i "s/versionCode .*/versionCode ${{ env.VERSION_CODE }}/" android/app/build.gradle
- name: Configure Gradle
run: |
chmod +x android/gradlew
echo "org.gradle.jvmargs=-Xmx6144m -XX:MaxMetaspaceSize=1024m -XX:+HeapDumpOnOutOfMemoryError" >> android/gradle.properties
echo "org.gradle.parallel=true" >> android/gradle.properties
echo "org.gradle.caching=true" >> android/gradle.properties
echo "org.gradle.configuration-cache=true" >> android/gradle.properties
echo "android.enable16KbPageSizes=true" >> android/gradle.properties
- name: Build AAB
run: cd android && ./gradlew bundleRelease --no-daemon
- name: Upload AAB artifact
uses: actions/upload-artifact@v4
with:
name: app-release.aab
path: android/app/build/outputs/bundle/release/*.aab
- name: Upload to Play Store
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_KEY }}
packageName: xyz.getpapillon.app
releaseFiles: android/app/build/outputs/bundle/release/*.aab
track: internal
inAppUpdatePriority: 3
status: draft
================================================
FILE: .github/workflows/merge.yml
================================================
name: Merge dev into main
on:
workflow_dispatch:
jobs:
merge:
name: Merge
runs-on: ubuntu-latest
steps:
- name: Generate app token
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token }}
fetch-depth: 0
- name: Configure git
run: |
git config user.name "papillon-release[bot]"
git config user.email "papillon-release[bot]@users.noreply.github.com"
- name: Merge dev into main
run: |
git checkout main
git merge origin/dev --no-edit
git push origin main
================================================
FILE: .github/workflows/release.yml
================================================
name: Release
on:
workflow_dispatch:
inputs:
version:
description: "Numéro de version (x.x.x)"
required: true
type: string
jobs:
release:
name: Merge & Release
runs-on: ubuntu-latest
steps:
- name: Validate version format
run: |
if ! echo "${{ inputs.version }}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "::error::Version invalide '${{ inputs.version }}'. Format attendu : x.x.x (ex: 7.2.0)"
exit 1
fi
- name: Generate app token
id: app-token
uses: actions/create-github-app-token@v3
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ steps.app-token.outputs.token }}
fetch-depth: 0
- name: Configure git
run: |
git config user.name "papillon-release[bot]"
git config user.email "papillon-release[bot]@users.noreply.github.com"
- name: Merge dev into main
run: |
git checkout main
git merge origin/dev --no-edit
git push origin main
- name: Create release
uses: softprops/action-gh-release@v2
with:
token: ${{ steps.app-token.outputs.token }}
tag_name: v${{ inputs.version }}
name: v${{ inputs.version }}
target_commitish: main
make_latest: true
body: |
<a href="https://github.com/PapillonApp/Papillon"><picture><source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/8bffdf1a-6e18-4545-874d-94c3978fb1c3"><source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/cc4a4903-fbf6-45ab-bb91-467351c89803"><img alt="Papillon" src="https://github.com/user-attachments/assets/cc4a4903-fbf6-45ab-bb91-467351c89803"></picture></a>
**Notes de mise à jour :** https://papillon.bzh/release-notes/${{ inputs.version }}
================================================
FILE: .gitignore
================================================
# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# dependencies
node_modules/
# Expo
.expo/
dist/
web-build/
expo-env.d.ts
.idea/
# Native
.kotlin/
*.orig.*
*.jks
*.p8
*.p12
*.key
*.mobileprovision
# Metro
.metro-health-check*
# debug
npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env*.local
# typescript
*.tsbuildinfo
ios/
!ios/Papillon
!ios/Papillon.xcodeproj/xcshareddata/xcschemes
ios/Podfile
android/**
android/*
!android/app/
!android/app/src/
!android/app/src/main/
!android/app/src/main/AndroidManifest.xml
bun.lock
yarn.lock
secrets.json
.vscode/
.idea/
================================================
FILE: .prettierignore
================================================
# Dependencies
node_modules/
bun.lock
package-lock.json
yarn.lock
# Build outputs
dist/
build/
.expo/
android/build/
ios/build/
# Generated files
*.log
.DS_Store
.vscode/
.idea/
# Platform specific
android/
ios/Pods/
ios/Papillon.xcworkspace/
ios/Papillon.xcodeproj/
# Patches
patches/
================================================
FILE: .prettierrc.json
================================================
{
"semi": true,
"trailingComma": "es5",
"singleQuote": false,
"printWidth": 80,
"tabWidth": 2,
"useTabs": false,
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "avoid",
"endOfLine": "lf"
}
================================================
FILE: CODEOWNERS
================================================
services/* @raphckrman
database/* @raphckrman
stores/* @raphckrman
.github/* @ryzenixx
utils/magic/* @tryon-dev
ui/* @ecnivtwelve
app/* @ecnivtwelve
components/* @ecnivtwelve
utils/* @ecnivtwelve
ui/components/Pattern/* @godetremy
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
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, either version 3 of the License, or
(at your option) any later version.
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/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
================================================
FILE: README.md
================================================
<p align="center">
<a href="https://github.com/PapillonApp/Papillon"><picture><source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/8bffdf1a-6e18-4545-874d-94c3978fb1c3"><source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/cc4a4903-fbf6-45ab-bb91-467351c89803"><img alt="Papillon" src="https://github.com/user-attachments/assets/cc4a4903-fbf6-45ab-bb91-467351c89803"></picture></a>
</p>
<p align="center">
L'application libre et open source <b>ultime</b> pour gérer toute ta vie scolaire sans compromis.
</p>
<p align="center">
<a href="https://papillon.bzh/download"><picture><source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/58713fdf-1768-4093-9aba-cf09d01e296b"><source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/5ce75010-f06f-446f-bee5-94745bf8ab15"><img alt="Télécharger" src="https://github.com/user-attachments/assets/5ce75010-f06f-446f-bee5-94745bf8ab15"></picture></a> <a href="https://papillon.bzh/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/43883003-54bd-4ad6-8660-4a23b1b6fe2b"><source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/f435b124-3d2c-491d-9b59-e5b2ed90d024"><img alt="Site web" src="https://github.com/user-attachments/assets/f435b124-3d2c-491d-9b59-e5b2ed90d024"></picture></a> <a href="https://docs.papillon.bzh/"><picture><source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/fe722cf6-45e9-4cb4-a958-b7e21eb5cf0a"><source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/2545f95a-d0e3-49e6-b628-0f16b009cf97"><img alt="Documentation" src="https://github.com/user-attachments/assets/2545f95a-d0e3-49e6-b628-0f16b009cf97"></picture></a>
</p>
#
<br />
<p align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/53902bcf-6f5b-40ec-84a6-490183e731bc">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/18e38796-0258-401f-a026-9ddaa69bdf01">
<img alt="Interface intuitive" src="https://github.com/user-attachments/assets/18e38796-0258-401f-a026-9ddaa69bdf01">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/b336b00d-801a-4180-91d7-46cabe3277dd">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/d66a0dcd-4b4a-4316-9cac-043fe4a93ef7">
<img alt="Données précises" src="https://github.com/user-attachments/assets/d66a0dcd-4b4a-4316-9cac-043fe4a93ef7">
</picture>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/user-attachments/assets/ceecdfce-0b3f-493d-933d-fc22f21966c8">
<source media="(prefers-color-scheme: light)" srcset="https://github.com/user-attachments/assets/8cae7eed-bcdd-4763-84a5-3cb6d9a142ba">
<img alt="Affichage intelligent" src="https://github.com/user-attachments/assets/8cae7eed-bcdd-4763-84a5-3cb6d9a142ba">
</picture>
</p>
#
<br />
<p align="center">
<img alt="Bienvenue !" src="https://github.com/user-attachments/assets/b9b5be6f-49cb-4327-a4dc-1c4325cff113" />
<p align="center">
<b>Si tu souhaites contribuer à Papillon, tu es au bon endroit !</b><br/>
Retrouve les resources importantes pour t'aider à commencer dans l'univers Papillon ci-dessous.
</p>
</p>
<p align="center">
<a href="https://docs.papillon.bzh/developper/compile"><img alt="Compiler" src="https://github.com/user-attachments/assets/3555367e-3813-4edc-9d60-998b2c1a9f79" /></a>
<a href="https://docs.papillon.bzh/ui"><img alt="Papillon UI" src="https://github.com/user-attachments/assets/75176fa7-4745-4c70-8464-569f61e3ac2b" /></a>
<a href="https://discord.gg/wVKWBRTbfh"><img alt="Community" src="https://github.com/user-attachments/assets/bac194cc-9183-4167-9bef-5787c9929b95" /></a>
</p>
#
<br />
<p align="center">
<img alt="L'équipe" src="https://github.com/user-attachments/assets/cf7f8edf-016f-4c03-bb78-5c54b4690986" />
<p align="center">
<table><tr>
<td>
<p align="center">
<a href="https://github.com/ecnivtwelve"><img alt="" src="https://github.com/user-attachments/assets/73e73e9c-b771-47ef-8d87-adf634fda95a" /></a>
<p align="center">Vince</p>
</p>
</td>
<td>
<p align="center">
<a href="https://github.com/tryon-dev"><img alt="" src="https://github.com/user-attachments/assets/7ab04024-2cdc-47e7-91e9-ba234efb1d3d" /></a>
<p align="center">Lucas</p>
</p>
</td>
<td>
<p align="center">
<a href="https://github.com/godetremy"><img alt="" src="https://github.com/user-attachments/assets/64ba327b-9095-4972-b8a8-d2e81819b327" /></a>
<p align="center">Rémy</p>
</p>
</td>
<td>
<p align="center">
<a href="https://github.com/tom-things"><img alt="" src="https://github.com/user-attachments/assets/79437727-c6a7-443c-b20a-3b83e6d1d46e" /></a>
<p align="center">Tom</p>
</p>
</td>
<td>
<p align="center">
<a href="https://github.com/ryzenixx"><img alt="" src="https://github.com/user-attachments/assets/5e61c4e6-48a9-41b9-a859-5cce653494f8" /></a>
<p align="center">Mael</p>
</p>
</td>
<td>
<p align="center">
<a href="https://github.com/raphckrman"><img width="250" height="250" alt="" src="https://github.com/user-attachments/assets/70c8c6c7-484d-4dd3-8679-9cbb0fcaf993" />
</a>
<p align="center">Raphaël</p>
</p>
</td>
</tr></table>
</p>
</p>
================================================
FILE: app/(features)/(cards)/cards.tsx
================================================
import { getManager } from "@/services/shared";
import { Balance } from "@/services/shared/balance";
import { useAccountStore } from "@/stores/account";
import { Services } from "@/stores/account/types";
import Button from "@/ui/new/Button";
import ChipButton from "@/ui/components/ChipButton";
import { Dynamic } from "@/ui/components/Dynamic";
import { EmptyItem } from "@/ui/components/EmptyItem";
import Icon from "@/ui/components/Icon";
import { NativeHeaderPressable, NativeHeaderSide, NativeHeaderTitle } from "@/ui/components/NativeHeader";
import Stack from "@/ui/components/Stack";
import TabHeader from "@/ui/components/TabHeader";
import TabHeaderTitle from "@/ui/components/TabHeaderTitle";
import Typography from "@/ui/components/Typography";
import { PapillonAppearIn, PapillonAppearOut } from "@/ui/utils/Transition";
import { getServiceBackground, getServiceLogo, getServiceName } from "@/utils/services/helper";
import { Papicons, Plus } from "@getpapillon/papicons";
import { LinearGradient } from "expo-linear-gradient";
import { router, useFocusEffect } from "expo-router";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Image, Platform, Pressable, View } from "react-native";
import { ScrollView } from "react-native-gesture-handler";
export default function QRCodeAndCardsPage() {
const [wallets, setWallets] = useState<Balance[]>([]);
const accounts = useAccountStore((state) => state.accounts);
const lastUsedAccount = useAccountStore((state) => state.lastUsedAccount);
const account = accounts.find((a) => a.id === lastUsedAccount);
async function fetchWallets() {
const manager = getManager()
const balances = await manager.getCanteenBalances()
const result: Balance[] = []
for (const balance of balances) {
result.push(balance)
}
setWallets(result);
}
useEffect(() => {
setWallets([])
fetchWallets();
}, [accounts])
const { t } = useTranslation();
const [headerHeight, setHeaderHeight] = useState(0);
return (
<>
<TabHeader
showAndroidBackButton
modal
onHeightChanged={setHeaderHeight}
title={
<TabHeaderTitle
chevron={false}
leading={t("Profile_QRCards")}
subtitle={t("Profile_QRCards_Subtitle", { count: wallets.length })}
/>
}
trailing={Platform.OS === "ios" && (
<ChipButton
single
icon="cross"
onPress={() => {
router.dismiss();
}}
/>
)}
/>
<ScrollView
contentInsetAdjustmentBehavior="automatic" style={{ flex: 1, paddingTop: headerHeight - 16 }} contentContainerStyle={{ padding: 20, gap: 16 }}
>
{wallets.map((c, i) => {
return (
<Dynamic
animated
key={c.createdByAccount + c.label}
entering={PapillonAppearIn}
exiting={PapillonAppearOut}
>
<Card
key={c.createdByAccount + c.label}
index={i}
wallet={c}
service={account?.services.find(service => service.id === c.createdByAccount)?.serviceId ?? Services.TURBOSELF}
totalCards={wallets.length}
/>
</Dynamic>
);
})}
{wallets.length === 0 && (
<Dynamic
animated
entering={PapillonAppearIn}
exiting={PapillonAppearOut}
>
<EmptyItem
icon="Card"
title={t("Settings_Cards_None_Title")}
description={t("Settings_Cards_None_Description")}
margin={0}
/>
</Dynamic>
)}
<Dynamic animated>
<Button
fullWidth
label="Ajouter"
leading={<Plus color="#FFF" />}
onPress={() => {
router.navigate({
pathname: "/(onboarding)/restaurants/method"
});
}}
/>
</Dynamic>
</ScrollView >
</>
);
}
export function Card({
index = 0,
wallet,
service,
disabled,
}: {
index: number;
wallet: Balance;
service: Services;
disabled?: boolean;
inSpecificView?: boolean;
totalCards?: number;
}) {
const [pressed, setPressed] = useState(false);
return (
<Pressable
onPress={() => {
if (!disabled) {
router.push({
pathname: "/(features)/(cards)/specific",
params: { serviceName: getServiceName(service), service: service, wallet: JSON.stringify(wallet) }
});
}
}}
onPressIn={() => setPressed(true)}
onPressOut={() => setPressed(false)}
style={{
width: "100%",
minHeight: 210,
borderRadius: 20,
overflow: "hidden",
marginTop: index === 0 ? 0 : -140,
zIndex: 100 + index,
top: 0,
left: 0,
right: 0,
}}
disabled={disabled}
>
<Image
source={getServiceBackground(service)}
style={{
position: "absolute",
bottom: 0,
right: 0,
left: 0,
width: "100%",
height: '100%',
}}
resizeMode="cover"
/>
<LinearGradient
colors={["#00000080", "transparent"]}
locations={[0, 0.87]}
start={{ x: 0.5, y: 0 }}
end={{ x: 0.5, y: 1 }}
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
/>
{pressed && (
<View style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0, 0, 0, 0.2)",
}} />
)}
<View style={{ padding: 15, flex: 1 }}>
<Stack
direction="horizontal"
style={{ justifyContent: "space-between" }}
hAlign="center"
>
<Stack direction="horizontal" hAlign="center" gap={8}>
<Image
style={{
width: 32,
height: 32,
borderRadius: 10,
borderWidth: 1,
borderColor: "#0000001F",
}}
source={getServiceLogo(service)}
resizeMode="cover"
/>
<Typography variant="title" color={"#FFFFFF"}>{getServiceName(service)}</Typography>
</Stack>
<Stack gap={0} direction="vertical">
<Typography variant="caption" align="right" color={"#FFFFFF" + 90} style={{ width: "100%", lineHeight: 0 }}>
{wallet.label}
</Typography>
<Typography variant="title" align="right" color={"#FFFFFF"} style={{ width: "100%", lineHeight: 0 }}>
{(wallet.amount / 100).toFixed(2)} {wallet.currency}
</Typography>
</Stack>
</Stack>
</View>
</Pressable>
)
}
================================================
FILE: app/(features)/(cards)/qrcode.tsx
================================================
import Barcode, { Format } from "@aramir/react-native-barcode";
import { Phone } from "@getpapillon/papicons";
import { BlurView } from "expo-blur";
import { router, useLocalSearchParams } from "expo-router";
import React from "react";
import { useTranslation } from "react-i18next";
import { Dimensions, Platform } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import QRCode from "react-native-qrcode-svg";
import Reanimated, {
FlipInEasyX,
runOnJS,
useSharedValue,
withSpring,
ZoomInDown,
} from "react-native-reanimated";
import OnboardingBackButton from "@/components/onboarding/OnboardingBackButton";
import { Services } from "@/stores/account/types";
import Stack from "@/ui/components/Stack";
import Typography from "@/ui/components/Typography";
export default function QRCodePage() {
const search = useLocalSearchParams();
const qr = String(search.qrcode);
const type = String(search.type || "QR");
const service = Number(search.service || Services.TURBOSELF);
const { t } = useTranslation();
const translationY = useSharedValue(0);
const opacity = useSharedValue(1);
const scale = useSharedValue(1);
const finalTranslation = Dimensions.get("window").height / 2;
const panGesture = Gesture.Pan()
.onUpdate((e) => {
translationY.value = e.translationY < 0 ? e.translationY / 10 : e.translationY;
if (e.translationY < 0) { return; }
opacity.value = 1 - Math.min(Math.abs(e.translationY) / 300, 0.7);
scale.value = 1 - Math.min(Math.abs(e.translationY) / 600, 0.4);
})
.onEnd((e) => {
if (e.translationY > 150) {
translationY.value = withSpring(finalTranslation, { damping: 150, stiffness: 1500 });
opacity.value = withSpring(0, { damping: 150, stiffness: 1500 });
scale.value = withSpring(0.6, { damping: 150, stiffness: 1500 });
setTimeout(() => {
runOnJS(router.back)();
}, 200);
return;
}
translationY.value = withSpring(0, { damping: 150, stiffness: 1500 });
opacity.value = withSpring(1, { damping: 150, stiffness: 1500 });
scale.value = withSpring(1, { damping: 150, stiffness: 1500 });
});
return (
<GestureDetector
gesture={panGesture}
>
<BlurView style={{ flex: 1, backgroundColor: Platform.OS === "ios" ? undefined : "#000" }}
tint={"dark"}
>
<Reanimated.View
entering={ZoomInDown.springify()}
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
gap: 20,
transform: [{ translateY: translationY }, { scale: scale }],
opacity: opacity,
padding: 20,
}}
>
<Reanimated.View
style={{
aspectRatio: 1,
width: "100%",
backgroundColor: "#FFF",
position: "relative",
justifyContent: "center",
alignItems: "center",
shadowRadius: 20,
shadowColor: "#000",
shadowOpacity: 0.3,
borderRadius: 25,
}}
entering={FlipInEasyX.springify().delay(100)}
>
{type === "QR" ? (
<QRCode
value={qr}
size={Dimensions.get("window").width * 0.8}
backgroundColor={"transparent"}
color={"#000"}
/>
) : (
<Barcode
value={qr}
format={type as Format}
background={"transparent"}
/>
)}
</Reanimated.View>
<Stack
style={{ width: 240 }}
hAlign="center"
>
<Phone fill={"#FFFFFF"} />
<Typography variant="body2"
align="center"
color="#FFFFFF"
>{t("Profile_Cards_Scan_Orientation")}</Typography>
</Stack>
</Reanimated.View>
<OnboardingBackButton icon={"Cross"}
position={"right"}
/>
</BlurView>
</GestureDetector>
);
}
================================================
FILE: app/(features)/(cards)/specific.tsx
================================================
import { useCallback, useEffect, useState, useMemo } from "react";
import { Platform, ScrollView, View } from "react-native";
import { useHeaderHeight } from "@react-navigation/elements";
import { useTheme } from "@react-navigation/native";
import { LinearGradient } from "expo-linear-gradient";
import { router, useLocalSearchParams } from "expo-router";
import { Switch } from "react-native-gesture-handler";
import adjust from "@/utils/adjustColor";
import { getCodeType, getServiceColor } from "@/utils/services/helper";
import { warn } from "@/utils/logger/logger";
import { getWeekNumberFromDate } from "@/database/useHomework";
import { getManager } from "@/services/shared";
import { Balance } from "@/services/shared/balance";
import { BookingDay, CanteenHistoryItem, CanteenKind } from "@/services/shared/canteen";
import ContainedNumber from "@/ui/components/ContainedNumber";
import Icon from "@/ui/components/Icon";
import { NativeHeaderPressable, NativeHeaderSide, NativeHeaderTitle } from "@/ui/components/NativeHeader";
import Stack from "@/ui/components/Stack";
import Typography from "@/ui/components/Typography";
import AnimatedPressable from "@/ui/components/AnimatedPressable";
import List from "@/ui/components/List";
import Item, { Trailing } from "@/ui/components/Item";
import Calendar from "@/ui/components/Calendar";
import { Card } from "./cards";
import { Calendar as CalendarIcon, ChevronDown, Clock, Papicons, QrCode } from "@getpapillon/papicons";
import { useAlert } from "@/ui/components/AlertProvider";
import { Services } from "@/stores/account/types";
import { useTranslation } from "react-i18next";
import { Capabilities } from "@/services/shared/types";
import i18n from "@/utils/i18n";
import TabHeader from "@/ui/components/TabHeader";
import TabHeaderTitle from "@/ui/components/TabHeaderTitle";
import ChipButton from "@/ui/components/ChipButton";
import ActivityIndicator from "@/ui/components/ActivityIndicator";
import NativeSwitch from "@/ui/native/NativeSwitch";
export default function QRCodeAndCardsPage() {
const alert = useAlert();
const search = useLocalSearchParams();
const serviceName = String(search.serviceName);
const service = Number(search.service) as Services;
const wallet = JSON.parse(String(search.wallet)) as Balance;
const theme = useTheme();
const { colors } = theme;
const manager = getManager();
const hasBookingCapacity = manager?.clientHasCapatibility(Capabilities.CANTEEN_BOOKINGS, wallet.createdByAccount)
const [history, setHistory] = useState<CanteenHistoryItem[]>([]);
const [qrcode, setQR] = useState("");
const [accountKind, setAccountKind] = useState<CanteenKind>(CanteenKind.ARGENT)
const [date, setDate] = useState(new Date());
const [weekNumber, setWeekNumber] = useState(getWeekNumberFromDate(date));
const [bookingWeek, setBookingWeek] = useState<BookingDay[]>([]);
const [showDatePicker, setShowDatePicker] = useState(false);
const bookingDay = useMemo(() => {
const target = new Date(date);
target.setUTCHours(0, 0, 0, 0);
return bookingWeek.find(day => {
const dayDate = new Date(day.date);
dayDate.setUTCHours(0, 0, 0, 0);
return dayDate.getTime() === target.getTime();
});
}, [date, bookingWeek]);
const fetchQRCode = useCallback(async () => {
try {
const { data } = await manager.getCanteenQRCodes(wallet.createdByAccount);
setQR(data);
} catch (error) {
warn(String(error));
}
}, [manager, wallet.createdByAccount]);
const [loadingHistory, setLoadingHistory] = useState(false);
const fetchHistory = useCallback(async () => {
setLoadingHistory(true);
const history = await manager.getCanteenTransactionsHistory(wallet.createdByAccount);
setHistory(history);
setLoadingHistory(false);
}, [manager, wallet.createdByAccount]);
const fetchBookingWeek = useCallback(async () => {
const bookings = await manager.getCanteenBookingWeek(weekNumber, wallet.createdByAccount);
setBookingWeek(bookings);
}, [manager, weekNumber, wallet.createdByAccount]);
const fetchKind = useCallback(async () => {
const kind = await manager.getCanteenKind(wallet.createdByAccount);
setAccountKind(kind);
}, [manager, weekNumber, wallet.createdByAccount]);
useEffect(() => {
fetchQRCode();
fetchHistory();
fetchKind();
}, [fetchQRCode, fetchHistory]);
useEffect(() => {
fetchBookingWeek();
}, [fetchBookingWeek]);
const handleDateChange = useCallback(
(newDate: Date) => {
setDate(newDate);
const newWeek = getWeekNumberFromDate(newDate);
if (newWeek !== weekNumber) setWeekNumber(newWeek);
if (Platform.OS === "ios") setShowDatePicker(false);
},
[weekNumber]
);
const serviceColor = useMemo(() => adjust(getServiceColor(service), -0.1), [service]);
const handleToggle = async (index: number) => {
if (!bookingDay) return;
const updatedBookingDay = { ...bookingDay };
const item = updatedBookingDay.available[index];
const previous = item.booked;
item.booked = !previous;
setBookingWeek(prev =>
prev.map(day => (day.date === bookingDay.date ? updatedBookingDay : day))
);
try {
await manager.setMealAsBooked(item);
} catch (error) {
alert.showAlert({
title: "Erreur lors de la réservation",
description: "Une erreur est survenue lors de la réservation de ton repas, il n'a donc pas été réservé.",
icon: "AlertTriangle",
color: "#D60046",
technical: String(error),
withoutNavbar: false
});
item.booked = previous;
setBookingWeek(prev =>
prev.map(day => (day.date === bookingDay.date ? updatedBookingDay : day))
);
}
};
const { t } = useTranslation();
const [headerHeight, setHeaderHeight] = useState(0);
return (
<>
<Calendar
key={`calendar-${date.toISOString()}`}
date={date}
onDateChange={handleDateChange}
showDatePicker={showDatePicker}
setShowDatePicker={setShowDatePicker}
/>
<LinearGradient
colors={[getServiceColor(service) + 40, colors.background, colors.background, colors.background]}
locations={[0, 0.87]}
start={{ x: 0.5, y: 0 }}
end={{ x: 0.5, y: 0.6 }}
style={{ position: "absolute", top: 0, left: 0, right: 0, bottom: 0 }}
/>
<TabHeader
modal
onHeightChanged={setHeaderHeight}
title={
<TabHeaderTitle
chevron={false}
leading={serviceName}
subtitle={(wallet.amount / 100).toFixed(2) + " " + wallet.currency}
/>
}
trailing={
<ChipButton
single
icon="cross"
onPress={() => {
router.dismiss();
}}
/>
}
/>
<ScrollView contentInsetAdjustmentBehavior="automatic" contentContainerStyle={{ paddingTop: headerHeight - 12 }}>
<View style={{ padding: 15, flex: 1, gap: 20 }}>
<Card index={0} service={service} wallet={wallet} disabled inSpecificView />
{qrcode && (
<AnimatedPressable
onPress={() => router.push({ pathname: "/(features)/(cards)/qrcode", params: { qrcode, type: getCodeType(service), service } })}
style={{
width: "100%",
backgroundColor: colors.background,
paddingVertical: 18,
borderRadius: 25,
borderWidth: 1,
borderColor: colors.border,
}}
>
<Stack direction="horizontal" hAlign="center" vAlign="center" gap={10}>
<QrCode color={getServiceColor(service)} />
<Typography variant="h6">Afficher le QR-Code</Typography>
</Stack>
</AnimatedPressable>
)}
<Stack card direction="horizontal" width="100%">
<Stack
width="50%"
vAlign="center"
hAlign="center"
style={{ borderRightWidth: 1, borderRightColor: colors.border }}
padding={12}
>
<Icon papicon opacity={0.5}>
<Papicons name="PiggyBank" />
</Icon>
<Typography color="secondary">Solde</Typography>
<ContainedNumber color={serviceColor}>
{(wallet.amount / 100).toFixed(2)} {wallet.currency}
</ContainedNumber>
</Stack>
<Stack width="50%" vAlign="center" hAlign="center" padding={12}>
<Icon papicon opacity={0.5}>
<Papicons name="Cutlery" />
</Icon>
<Typography color="secondary">Repas restants</Typography>
<ContainedNumber color={serviceColor}>{wallet.lunchPrice > 0 ? String(Math.floor(wallet.amount / wallet.lunchPrice)) : "Indéterminé"}</ContainedNumber>
</Stack>
</Stack>
{hasBookingCapacity && (
<View>
<AnimatedPressable onPress={() => setShowDatePicker(prev => !prev)}>
<Stack hAlign="center" vAlign="center" style={{ padding: 20 }}>
<Stack direction="horizontal" gap={5}>
<Typography color="secondary">Réserver mon repas</Typography>
</Stack>
<Stack direction="horizontal" gap={5} hAlign="center" vAlign="center">
<Typography color="secondary">
{date.toLocaleDateString(i18n.language, { weekday: "long", day: "numeric", month: "long" })}
</Typography>
<ChevronDown opacity={0.5} size={18} />
</Stack>
{bookingDay ? (
<List style={{ marginTop: 10 }}>
{bookingDay.available.map((item, index) => (
<Item key={item.label}>
<Typography>
Borne {item.label}
</Typography>
<Trailing>
<NativeSwitch
disabled={accountKind === CanteenKind.FORFAIT ? false : !item.canBook || (wallet.lunchRemaining < 1 && wallet.lunchPrice !== 0)}
value={item.booked}
onValueChange={() => handleToggle(index)}
/>
</Trailing>
</Item>
))}
</List>
) : (
<Stack hAlign="center" vAlign="center" margin={16} gap={16}>
<View style={{ alignItems: "center" }}>
<Icon papicon opacity={0.5} size={32} style={{ marginBottom: 3 }}>
<Papicons name="Card" />
</Icon>
<Typography variant="h4" color="text" align="center">
{t("Profile_Cards_No_Reservation")}
</Typography>
<Typography variant="body2" color="secondary" align="center">
{t("Profile_Cards_No_Available_Reservation")}
</Typography>
</View>
</Stack>
)}
</Stack>
</AnimatedPressable>
</View>
)}
{loadingHistory && history.length === 0 && (
<Stack padding={20} hAlign="center" vAlign="center" gap={4}>
<ActivityIndicator size={42} />
<Typography align="center" variant="title" color="text" style={{ marginTop: 8 }}>
{t("Profile_Cards_Loading_History")}
</Typography>
<Typography align="center" variant="body2" color="secondary">
{t("Profile_Cards_Loading_History_Description")}
</Typography>
</Stack>
)}
{history.length > 0 && (
<View style={{ display: "flex", gap: 13.5 }}>
<Stack direction="horizontal" style={{ flex: 1, borderRightWidth: 1, borderRightColor: colors.border }} gap={5}>
<Icon papicon opacity={0.5}>
<Clock />
</Icon>
<Typography color="secondary">{t("Profile_Cards_History")}</Typography>
</Stack>
<List>
{history.slice(0, 10).map((c, index) =>
<Item key={`${c.label}-${c.date.getTime()}-${index}`}>
<Trailing>
<ContainedNumber color={adjust(c.amount < 0 ? "#C50000" : "#42C500", -0.1)}>
{c.amount > 0 ? "+" : ""}{(c.amount / 100).toFixed(2)} {c.currency}
</ContainedNumber>
</Trailing>
<Typography>{c.label}</Typography>
<Stack direction="horizontal" hAlign="center">
<Typography color="secondary">{c.date.toLocaleDateString(i18n.language, { day: "2-digit", month: "2-digit", year: "numeric" })}</Typography>
<View style={{ height: 4, width: 4, borderRadius: 2, backgroundColor: colors.text + 80 }} />
<Typography color="secondary">
{c.date.toLocaleTimeString(i18n.language, { hour: "2-digit", minute: "2-digit", hour12: false })}
</Typography>
</Stack>
</Item>)}
</List>
</View>
)}
</View>
</ScrollView>
</>
);
}
================================================
FILE: app/(features)/(news)/specific.tsx
================================================
import { getManager } from "@/services/shared";
import { News } from "@/services/shared/news";
import { useAccountStore } from "@/stores/account";
import { Services } from "@/stores/account/types";
import Stack from "@/ui/components/Stack";
import TypographyLegacy from "@/ui/components/Typography";
import { useLocalSearchParams, useNavigation } from "expo-router"
import { useEffect, useState } from "react";
import { Linking, Platform, ScrollView, StyleSheet, View } from "react-native";
import { Attachment, News as SkolengoNews } from "skolengojs"
import * as Papicons from '@getpapillon/papicons';
import { VARIANTS } from "@/ui/components/Typography";
import HTMLView from 'react-native-htmlview';
import * as WebBrowser from 'expo-web-browser';
import { useTheme } from "@react-navigation/native";
import { NativeHeaderPressable, NativeHeaderSide } from "@/ui/components/NativeHeader";
import { MenuView } from "@react-native-menu/menu";
import Icon from "@/ui/components/Icon";
import { t } from "i18next";
import ListLegacy from "@/ui/components/List";
import Item from "@/ui/components/Item";
import ActionMenu from "@/ui/components/ActionMenu";
import Typography from "@/ui/new/Typography";
import List from "@/ui/new/List";
export default function NewsPage() {
const search = useLocalSearchParams();
const news = JSON.parse(String(search.news)) as News
const navigation = useNavigation()
useEffect(() => {
const acknowledgeNews = async () => {
if (!news.acknowledged) {
const manager = getManager();
const store = useAccountStore.getState()
const account = store.accounts.find(account => account.id === store.lastUsedAccount)
const service = account?.services.find(service => service.id === news.createdByAccount)
if (service?.serviceId === Services.SKOLENGO) {
const attachment = new Attachment("", "", "")
const ref = new SkolengoNews(news.id, news.createdAt, news.title ?? "", news.content, news.content, { id: "", name: "" }, "", attachment)
news.ref = ref
}
await manager.setNewsAsDone(news);
}
};
acknowledgeNews();
}, [])
const { colors } = useTheme();
const styles = {
"papillon": {
background: colors.background,
foreground: colors.text,
html: StyleSheet.create({
...VARIANTS,
p: {
...VARIANTS.body1,
},
div: {
...VARIANTS.body1,
},
a: {
color: colors.primary,
textDecorationLine: 'underline',
},
})
},
"reading": {
background: "#ffe1a9ff",
foreground: "#634000ff",
html: StyleSheet.create({
...VARIANTS,
p: {
fontFamily: "serif_medium"
},
div: {
fontFamily: "serif_medium"
},
h1: {
fontFamily: "serif_bold"
},
a: {
color: "#a26900ff",
textDecorationLine: 'underline',
fontFamily: "serif_medium"
},
})
}
};
const [style, setStyle] = useState("papillon");
useEffect(() => {
navigation.setOptions({
headerTitle: news.title ?? "News",
headerTitleStyle: {
color: styles[style].foreground,
fontFamily: styles[style].html.p.fontFamily
},
headerLargeTitleStyle: {
color: styles[style].foreground,
fontFamily: styles[style].html.h1.fontFamily
}
})
}, [styles, style])
// Replace all color properties in currentHTMLStyle with foreground
const foreground = styles[style].foreground;
const currentHTMLStyle = (() => {
const htmlStyle = { ...styles[style].html };
Object.keys(htmlStyle).forEach(key => {
htmlStyle[key] = { ...htmlStyle[key], color: foreground };
});
return htmlStyle;
})();
const themes = [
{
title: t("News_Theme_Papillon_Title"),
description: t("News_Theme_Papillon_Description"),
value: "papillon"
},
{
title: t("News_Theme_Reading_Title"),
description: t("News_Theme_Reading_Description"),
value: "reading"
}
]
return (
<>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={{ flex: 1, backgroundColor: styles[style].background }}
>
{news.attachments.length > 0 && (
<View style={{ padding: 16, width: "100%" }}>
<ListLegacy>
{news.attachments.map((attachment, i) => (
<Item key={i}
onPress={() => {
WebBrowser.openBrowserAsync(
attachment.url,
{
controlsColor: colors.primary,
dismissButtonStyle: 'done'
}
)
}}
>
<Icon papicon>
{attachment.type === 0 ? (
<Papicons.Link />
) : (
<Papicons.Paper />
)}
</Icon>
<TypographyLegacy nowrap variant="title">{attachment.name}</TypographyLegacy>
<TypographyLegacy nowrap variant="caption">{attachment.url}</TypographyLegacy>
</Item>
))}
</ListLegacy>
</View>
)}
<List>
<List.Item>
<Typography>
Cette news contient un sondage
</Typography>
</List.Item>
</List>
<HTMLView
value={news.content}
stylesheet={currentHTMLStyle}
style={{
padding: 16,
paddingTop: 0
}}
/>
</ScrollView>
<NativeHeaderSide side="Right">
<ActionMenu
actions={
themes.map(theme => ({
id: theme.value,
title: theme.title,
subtitle: theme.description,
state: theme.value === style ? 'on' : 'off'
}))
}
onPressAction={({ nativeEvent }) => {
const selectedTheme = themes.find(theme => theme.value === nativeEvent.event);
if (selectedTheme) {
setStyle(selectedTheme.value);
}
}}
>
<NativeHeaderPressable>
<Icon papicon>
<Papicons.Palette />
</Icon>
</NativeHeaderPressable>
</ActionMenu>
</NativeHeaderSide>
</>
)
}
================================================
FILE: app/(features)/attendance.tsx
================================================
import Icon from "@/ui/components/Icon";
import { NativeHeaderHighlight, NativeHeaderPressable, NativeHeaderSide, NativeHeaderTitle } from "@/ui/components/NativeHeader";
import { router, useLocalSearchParams } from "expo-router";
import { Platform, ScrollView, View } from "react-native";
import { Papicons } from "@getpapillon/papicons"
import { useTheme } from "@react-navigation/native";
import { Dynamic } from "@/ui/components/Dynamic";
import { MenuView } from "@react-native-menu/menu";
import { Period } from "@/services/shared/grade";
import { getPeriodName, getPeriodNumber, isPeriodWithNumber } from "@/utils/services/periods";
import { useMemo, useState } from "react";
import { Attendance } from "@/services/shared/attendance";
import Stack from "@/ui/components/Stack";
import { useHeaderHeight } from "@react-navigation/elements";
import AnimatedNumber from "@/ui/components/AnimatedNumber";
import adjust from "@/utils/adjustColor";
import { error } from "@/utils/logger/logger";
import { getManager } from "@/services/shared";
import { t } from "i18next";
import i18n from "@/utils/i18n";
import ActionMenu from "@/ui/components/ActionMenu";
import AndroidBackButton from "@/utils/theme/AndroidBackButton";
import TabHeader from "@/ui/components/TabHeader";
import TabHeaderTitle from "@/ui/components/TabHeaderTitle";
import List from "@/ui/new/List";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Typography from "@/ui/new/Typography";
import { formatDate, formatDistanceToNow, formatDistanceToNowStrict } from "date-fns";
import * as DateLocale from 'date-fns/locale';
const formatEventTime = (durationData: number, detailed: boolean) => {
if(detailed) {
return durationData >= 60
? t("Attendance_Duration_HoursMinutes_Detailed", { hours: Math.floor(durationData / 60), minutes: lz(durationData % 60) })
: t("Attendance_Duration_Minutes", { value: durationData })
}
return durationData >= 60
? t("Attendance_Duration_HoursMinutes_Compact", { hours: Math.floor(durationData / 60), minutes: lz(durationData % 60) })
: t("Attendance_Duration_Minutes", { value: durationData })
}
export default function AttendanceView() {
try {
const theme = useTheme()
const { colors } = theme;
const header = useHeaderHeight();
const search = useLocalSearchParams();
const currentPeriod = JSON.parse(String(search.currentPeriod)) as Period;
const periods = JSON.parse(String(search.periods)) as Period[];
const attendancesFromSearch = JSON.parse(String(search.attendances)) as Attendance[];
const [attendances, setAttendances] = useState<Attendance[]>(attendancesFromSearch);
const [period, setPeriod] = useState<Period>(currentPeriod);
const { missedTime, missedTimeUnjustified, unjustifiedAbsenceCount, unjustifiedDelayCount, absenceCount, delayCount } = useMemo(() => {
let missed = 0;
let unjustified = 0;
let unjustifiedAbs = 0;
let unjustifiedDelays = 0;
let Abs = 0
let Delays = 0
for (const attendance of attendances) {
for (const absence of attendance.absences) {
Abs += 1;
missed += absence.timeMissed;
if (!absence.justified) {
unjustified += absence.timeMissed;
unjustifiedAbs += 1;
}
}
for (const delay of attendance.delays) {
Delays += 1;
if (!delay.justified) {
unjustifiedDelays += 1;
unjustified += delay.duration
}
missed += delay.duration
}
}
return { missedTime: missed, missedTimeUnjustified: unjustified, unjustifiedAbsenceCount: unjustifiedAbs, unjustifiedDelayCount: unjustifiedDelays, absenceCount: Abs, delayCount: Delays };
}, [period, attendances]);
const [headerHeight, setHeaderHeight] = useState(0);
const insets = useSafeAreaInsets();
const dangerColor = adjust("#C50000", theme.dark ? 0.4 : -0.1);
const dangerBg = adjust("#C50000", theme.dark ? -0.65 : 0.85);
const successColor = adjust("#00C851", theme.dark ? 0.3 : -0.1);
const successBg = adjust("#00C851", theme.dark ? -0.75 : 0.85);
return (
<>
<TabHeader
showAndroidBackButton
modal={Platform.OS !== "android"}
onHeightChanged={setHeaderHeight}
title={
<ActionMenu
key={String(period?.id ?? "")}
onPressAction={async ({ nativeEvent }) => {
const actionId = nativeEvent.event;
if (actionId.startsWith("period:")) {
const selectedPeriodId = actionId.replace("period:", "");
const selectedPeriod: Period | undefined = periods.find(item => item.id === selectedPeriodId)
if (!selectedPeriod) {
error(t("Attendance_InvalidPeriod"))
}
const manager = getManager()
const attendancesFetched = await manager.getAttendanceForPeriod(selectedPeriod.name)
setAttendances(attendancesFetched)
setPeriod(selectedPeriod)
}
}}
actions={
periods.map((item) => ({
id: "period:" + item.id,
title: (getPeriodName(item.name || "") + " " + (isPeriodWithNumber(item.name || "") ? getPeriodNumber(item.name || "0") : "")).trim(),
subtitle: `${new Date(item.start).toLocaleDateString(i18n.language, {
month: "short",
year: "numeric",
})} - ${new Date(item.end).toLocaleDateString(i18n.language, {
month: "short",
year: "numeric",
})}`,
state: String(period?.id ?? "") === String(item.id ?? "") ? "on" : "off",
image: Platform.select({
ios: (getPeriodNumber(item.name || "0")) + ".calendar"
}),
imageColor: colors.text,
}))}
>
<TabHeaderTitle
chevron={true}
leading={getPeriodName(period?.name ?? "")}
number={getPeriodNumber(period?.name ?? "")}
/>
</ActionMenu>
}
/>
<List
contentContainerStyle={{
padding: 16,
paddingTop: headerHeight,
paddingBottom: insets.bottom + 16,
}}
>
{attendances.some(attendance => attendance.absences.length == 0) && attendances.some(attendance => attendance.delays.length == 0) ? (
<List.Item>
<List.Leading>
<Icon>
<Papicons name="Ghost" />
</Icon>
</List.Leading>
<Typography variant="title">
{t("Attendance_NoEvent_Title")}
</Typography>
<Typography color="textSecondary">
{t("Attendance_NoEvent_Description")}
</Typography>
</List.Item>
) : (
<List.Section>
{missedTimeUnjustified > 0 ? (
<List.Item
style={{
backgroundColor: dangerBg,
}}
>
<List.Leading>
<Icon fill={dangerColor}>
<Papicons name="AlertTriangle" />
</Icon>
</List.Leading>
<Typography variant="title" color={dangerColor}>
{t("Attendance_Hours_Unjustified_Value", { duration: formatEventTime(missedTimeUnjustified, true) })}
</Typography>
<Typography color="textSecondary" color={dangerColor}>
{t("Attendance_Unjustified_Description")}
</Typography>
</List.Item>
) : (
<List.Item
style={{
backgroundColor: successBg,
}}
>
<List.Leading>
<Icon fill={successColor}>
<Papicons name="Check" />
</Icon>
</List.Leading>
<Typography variant="title" color={successColor}>
{t("Attendance_NoUnjustified_Title")}
</Typography>
<Typography color="textSecondary" color={successColor}>
{t("Attendance_NoUnjustified_Description")}
</Typography>
</List.Item>
)}
<List.Item>
<Typography variant="action">
{t("Attendance_Hours_Missed")}
</Typography>
<List.Trailing>
<Typography variant="title" weight="bold" color={missedTimeUnjustified > 0 ? dangerColor : "textSecondary"}>
{formatEventTime(missedTime, true)}
</Typography>
</List.Trailing>
</List.Item>
</List.Section>
)}
{attendances.some(attendance => attendance.absences.length > 0) && (
<List.Section>
<List.SectionTitle>
<Icon opacity={0.5} size={20}>
<Papicons name="UserCross" />
</Icon>
<Typography variant="body1" weight="semibold" color="textSecondary" style={{ flex: 1 }}>
{t("Attendance_Missing")}
</Typography>
<Typography variant="title" weight="medium" color={"textSecondary"}>
{absenceCount}
</Typography>
</List.SectionTitle>
{attendances.map((attendance, index) =>
attendance.absences.map((absence, absenceIndex) => {
const fromDate = new Date(absence.from);
const dateString = formatDistanceToNowStrict(fromDate, {
locale: DateLocale[i18n.language as keyof typeof DateLocale] || DateLocale.enUS,
addSuffix: true
})
const dayString = formatDate(fromDate, "eeee d MMMM", {
locale: DateLocale[i18n.language as keyof typeof DateLocale] || DateLocale.enUS,
})
return (
<List.Item>
<Typography variant="title">
{absence.reason || t("Attendance_NoReason")}
</Typography>
<Typography color="textSecondary" numberOfLines={1}>
{dateString} · {dayString}
</Typography>
<List.Trailing>
<AttendanceTimer evt={absence} />
</List.Trailing>
</List.Item>
)
})
)}
</List.Section>
)}
{attendances.some(attendance => attendance.delays.length > 0) && (
<List.Section>
<List.SectionTitle>
<Icon opacity={0.5} size={20}>
<Papicons name="Clock" />
</Icon>
<Typography variant="body1" weight="semibold" color="textSecondary" style={{ flex: 1 }}>
{t("Attendance_Delays")}
</Typography>
<Typography variant="title" weight="medium" color={"textSecondary"}>
{delayCount}
</Typography>
</List.SectionTitle>
{attendances.map((attendance, index) =>
attendance.delays.map((delay, absenceIndex) => {
const fromDate = new Date(delay.givenAt);
const date = fromDate.getTime();
const dateString = formatDistanceToNowStrict(date, {
locale: DateLocale[i18n.language as keyof typeof DateLocale] || DateLocale.enUS,
addSuffix: true
})
const dayFormatted = formatDate(date, "eeee d MMMM", {
locale: DateLocale[i18n.language as keyof typeof DateLocale] || DateLocale.enUS,
})
return (
<List.Item>
<Typography variant="title">
{delay.reason || t("Attendance_NoReason")}
</Typography>
<Typography color="textSecondary">
{dateString} · {dayFormatted}
</Typography>
<List.Trailing>
<AttendanceTimer evt={delay} />
</List.Trailing>
</List.Item>
)
})
)}
</List.Section>
)}
</List>
</>
)
} catch (err) {
error(err.toString());
return null;
}
}
const lz = (num: number) => num.toString().padStart(2, "0");
const AttendanceTimer = ({ evt }: { evt: any }) => {
const theme = useTheme();
const { colors } = theme;
const dangerColor = adjust("#C50000", theme.dark ? 0.4 : -0.1);
const dangerBg = dangerColor + "30";
const durationData = evt.timeMissed || evt.duration || 0;
const durationText = formatEventTime(durationData);
return (
<Stack direction="horizontal" hAlign="center" gap={8}>
{!evt.justified && (
<Icon papicon fill={dangerColor}>
<Papicons name={"Minus"} />
</Icon>
)}
<View style={{ padding: 6, paddingHorizontal: evt.justified ? 3 : 12, backgroundColor: evt.justified ? "transparent" : dangerBg, borderRadius: 25, overflow: "hidden" }}>
<Typography variant="title" color={evt.justified ? colors.text : dangerColor}>{durationText}</Typography>
</View>
</Stack>
)
}
================================================
FILE: app/(features)/soon.tsx
================================================
import Icon from "@/ui/components/Icon";
import Stack from "@/ui/components/Stack";
import Typography from "@/ui/components/Typography";
import { Papicons } from "@getpapillon/papicons";
import React from "react";
import { Linking, View } from "react-native";
export default function Soon() {
return (
<View
style={{
padding: 20,
paddingBottom: 0,
}}
>
<Stack
padding={20}
gap={10}
vAlign="center"
hAlign="center"
>
<Icon size={42}>
<Papicons name="clock" color="#29947A" />
</Icon>
<Typography variant="h2" align="center">
Promis, ça arrive (vraiment) bientôt !
</Typography>
<Typography variant="body1" color="secondary" align="center">
L'onglet est toujours en cours de développement. Il arrivera prochainement dans une version future de Papillon.
</Typography>
<Typography variant="body1" color="primary" align="center" onPress={() => {
Linking.openURL("https://www.instagram.com/thepapillonapp/");
}} style={{
textDecorationLine: "underline",
}}>
Et pour rester au courant, tu peux nous suivre sur les réseaux sociaux !
</Typography>
</Stack>
</View>
);
}
================================================
FILE: app/(modals)/address.tsx
================================================
import { Papicons } from "@getpapillon/papicons";
import { useTheme } from "@react-navigation/native";
import * as Linking from "expo-linking";
import * as Location from "expo-location";
import * as React from "react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
ActivityIndicator,
Dimensions,
KeyboardAvoidingView,
Platform,
ScrollView,
TouchableNativeFeedback,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Transit from "@/services/transit";
import { PlaceSuggestion } from "@/services/transit/models/PlaceSuggestion";
import { Stop } from "@/services/transit/models/Stop";
import { TransportAddress } from "@/stores/account/types";
import Button from "@/ui/components/Button";
import Item, { Leading, Trailing } from "@/ui/components/Item";
import List from "@/ui/components/List";
import Search from "@/ui/components/Search";
import Typography from "@/ui/components/Typography";
import { useHeaderHeight } from "@react-navigation/elements";
import AndroidBackButton, { AndroidBackButtonStyles } from "@/utils/theme/AndroidBackButton";
import Icon from "@/ui/components/Icon";
export interface AddressModalProps {
canUseCurrentLocation: boolean;
onCancel: () => void;
onConfirm: (address: TransportAddress) => void;
}
interface AddressItemProps {
icon: string;
firstLine: string;
secondLine: string;
convertFunction: () => Promise<TransportAddress>;
save: (addess: TransportAddress) => void;
lineLimit?: number;
}
const AddressItem = ({
icon,
firstLine,
secondLine,
convertFunction,
save,
lineLimit = 1,
}: AddressItemProps) => {
const theme = useTheme();
const [savingAddress, setSavingAddress] = useState<boolean>(false);
const saveAddress = async () => {
setSavingAddress(true);
const address = await convertFunction();
save(address);
setSavingAddress(false);
};
return (
<Item onPress={saveAddress} disablePadding={true} isLast={true}>
<Leading>
<Papicons name={icon} fill={theme.colors.text} />
</Leading>
{savingAddress && (
<Trailing>
<ActivityIndicator />
</Trailing>
)}
<Typography variant={"title"} numberOfLines={1}>
{firstLine}
</Typography>
<Typography color={"secondary"} variant={"body2"} numberOfLines={lineLimit}>
{secondLine}
</Typography>
</Item>
);
};
export const AddressModal = ({
canUseCurrentLocation,
onCancel,
onConfirm,
}: AddressModalProps) => {
const theme = useTheme();
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const transit = new Transit();
const [status, requestPermission] = Location.useForegroundPermissions();
const [searchTerm, setSearchTerm] = useState("");
const [searchPlaceResults, setSearchPlaceResults] = useState<
PlaceSuggestion[]
>([]);
const [searchStopResults, setSearchStopResults] = useState<Stop[]>([]);
let timeout: number | null = null;
const search = async () => {
const location = await Location.getCurrentPositionAsync();
const res = await transit.suggestions(
location.coords.latitude,
location.coords.longitude,
searchTerm
);
setSearchPlaceResults(res.suggestions.places);
setSearchStopResults(res.suggestions.stops);
};
const currentLocationToTransportAddress =
async (): Promise<TransportAddress> => {
return new Promise(resolve => {
resolve({
firstTitle: "current_location",
secondTitle: "current_location",
address: "current_location",
latitude: -1,
longitude: -1,
});
});
};
const placeToTransportAddress = async (
place: PlaceSuggestion
): Promise<TransportAddress> => {
const details = await transit.locationDetails(place.place_id);
return {
firstTitle: place.structured_formatting.main_text,
secondTitle: place.structured_formatting.secondary_text,
address: details.placeDetails.result.formatted_address,
latitude: details.placeDetails.result.geometry.location.lat,
longitude: details.placeDetails.result.geometry.location.lng,
};
};
const stopToTransportAddress = async (
stop: Stop
): Promise<TransportAddress> => {
return new Promise(resolve => {
resolve({
firstTitle: stop.stop_name,
secondTitle: stop.city_name,
address: `${stop.stop_name}, ${stop.city_name}`,
latitude: stop.stop_lat,
longitude: stop.stop_lon,
});
});
};
useEffect(() => {
if (timeout !== null) {
clearTimeout(timeout);
}
if (searchTerm.length === 0) {
setSearchPlaceResults([]);
setSearchStopResults([]);
return;
}
timeout = setTimeout(() => search(), 200);
}, [searchTerm]);
const finalHeaderHeight = Platform.select({
android: insets.top,
default: 0
});
return (
<View
style={{
flex: 1,
paddingTop: 0 + finalHeaderHeight,
paddingBottom: insets.bottom,
backgroundColor: theme.colors.background,
}}
>
<View
style={{
marginTop: 14,
top: finalHeaderHeight,
position: "absolute",
zIndex: 9999,
left: 14,
right: 14,
flexDirection: "row",
}}
>
{Platform.OS === "android" && (
<TouchableNativeFeedback
onPress={onCancel}
useForeground
>
<View style={AndroidBackButtonStyles.container}>
<Icon size={26}>
<Papicons name="arrowleft" />
</Icon>
</View>
</TouchableNativeFeedback>
)}
<Search
placeholder={t("Settings_Transport_Search_Address_Placeholder")}
color="#E8901C"
onTextChange={setSearchTerm}
style={{
width: Dimensions.get("window").width - (14 * 2) - (Platform.OS === "android" ? 52 : 0),
}}
/>
</View>
{status === null || status?.granted ? (
<KeyboardAvoidingView
style={{
height: "100%",
}}
behavior={"height"}
keyboardVerticalOffset={70}
>
<ScrollView
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
contentContainerStyle={{
paddingTop: 56 + 14,
paddingHorizontal: 16,
paddingBottom: 56 + 14,
gap: 8,
}}
>
{searchTerm.length === 0 && canUseCurrentLocation && (
<List>
<AddressItem
icon={"MapPin"}
firstLine={t("Settings_Transport_Current_Position")}
secondLine={t(
"Settings_Transport_Current_Position_Description"
)}
convertFunction={currentLocationToTransportAddress}
save={onConfirm}
lineLimit={2}
/>
</List>
)}
{searchPlaceResults.length > 0 && (
<>
<Typography variant={"h6"} color={"secondary"}>
{t("Settings_Transport_Place")}
</Typography>
<List>
{searchPlaceResults.map((item: PlaceSuggestion) => (
<AddressItem
key={item.place_id}
icon={"MapPin"}
firstLine={item.structured_formatting.main_text}
secondLine={item.structured_formatting.secondary_text}
convertFunction={() => placeToTransportAddress(item)}
save={onConfirm}
/>
))}
</List>
</>
)}
{searchStopResults.length > 0 && (
<>
<Typography variant={"h6"} color={"secondary"}>
{t("Settings_Transport_Stops")}
</Typography>
<List>
{searchStopResults.map((item: Stop) => (
<AddressItem
key={item.raw_stop_id}
icon={"Bus"}
firstLine={item.stop_name}
secondLine={item.city_name}
convertFunction={() => stopToTransportAddress(item)}
save={onConfirm}
/>
))}
</List>
</>
)}
</ScrollView>
<View
style={{
padding: 16,
marginTop: "auto",
}}
>
<Button
title={t("Cancel")}
variant={"ghost"}
inline={true}
onPress={onCancel}
/>
</View>
</KeyboardAvoidingView>
) : (
<View
style={{
height: "100%",
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 16,
}}
>
<Papicons
name={"Ghost"}
size={64}
style={{ marginBottom: 16 }}
fill={theme.colors.text}
/>
<Typography variant={"h3"} style={{ textAlign: "center" }}>
{t("Settings_Transport_Localisation_Needed")}
</Typography>
<Typography
variant={"body1"}
color={"secondary"}
style={{
textAlign: "center",
marginBottom: 20,
}}
>
{t("Settings_Transport_Localisation_Needed_Description")}
</Typography>
<Button
title={t("Settings_Transport_Localisation_Request")}
variant={"light"}
color={"blue"}
onPress={() => {
if (status?.canAskAgain) {
requestPermission();
} else {
Linking.openSettings();
}
}}
/>
</View>
)}
</View>
);
};
================================================
FILE: app/(modals)/course.tsx
================================================
import { Papicons } from '@getpapillon/papicons';
import { useRoute, useTheme } from "@react-navigation/native";
import { formatDistanceStrict, formatDistanceToNow } from 'date-fns'
import * as DateLocale from 'date-fns/locale';
import i18n, { t } from "i18next";
import React from "react";
import { Platform } from 'react-native';
import LinearGradient from "react-native-linear-gradient";
import ModalOverhead from "@/components/ModalOverhead";
import { Course as SharedCourse } from "@/services/shared/timetable";
import Icon from "@/ui/components/Icon";
import List from "@/ui/new/List";
import Typography from "@/ui/new/Typography";
import { getSubjectName } from '@/utils/subjects/name';
import { getStatusText } from "../(tabs)/calendar/components/CalendarDay";
import { useSafeAreaInsets } from 'react-native-safe-area-context';
interface SubjectInfo {
name: string;
originalName: string;
emoji: string;
color: string;
}
interface GradesModalProps {
course: SharedCourse;
subjectInfo: SubjectInfo;
}
export default function CourseModal() {
const { params } = useRoute();
const { colors } = useTheme();
if (!params) {
return null;
}
const { course, subjectInfo } = params as GradesModalProps;
const item = course;
const startTime = Math.floor(course.from.getTime() / 1000);
const endTime = Math.floor(course.to.getTime() / 1000);
const insets = useSafeAreaInsets();
const finalHeaderHeight = Platform.select({
android: insets.top + 32,
default: 0
});
return (
<>
{Platform.OS !== 'android' && (
<LinearGradient
colors={[subjectInfo.color, colors.background]}
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: 500,
width: "100%",
zIndex: -9,
opacity: 0.6
}}
/>
)}
<List
ListHeaderComponent={
<ModalOverhead
subject={getSubjectName(item.subject)}
title={item.customStatus || getStatusText(item.status)}
color={Platform.OS === 'ios' ? subjectInfo.color : colors.primary}
emoji={subjectInfo.emoji}
subjectVariant="h3"
date={new Date(startTime * 1000)}
dateFormat={{
day: "numeric",
month: "long",
year: "numeric",
hour: "numeric",
minute: "numeric"
}}
style={{
marginBottom: 24,
marginTop: 24,
paddingTop: finalHeaderHeight
}}
/>
}
style={{ backgroundColor: "transparent" }}
contentContainerStyle={{ padding: 16 }}
>
{getStatusText(course.status) ? (
<List.Section>
<List.Item>
<List.Leading>
<Icon>
<Papicons name="Info" />
</Icon>
</List.Leading>
<Typography variant="title">
{getStatusText(course.status)}
</Typography>
</List.Item>
</List.Section>
) : null}
<List.Section>
<List.SectionTitle>
<List.Label>{t("Modal_Course_Time")}</List.Label>
</List.SectionTitle>
<List.Item>
<List.Leading>
<Icon>
<Papicons name="Logout" />
</Icon>
</List.Leading>
<Typography variant="title">
{t("Modal_Course_Start")}
</Typography>
<Typography variant="body1" color="textSecondary">
{formatDistanceToNow(startTime * 1000, {
locale: DateLocale[i18n.language as keyof typeof DateLocale] || DateLocale.enUS,
addSuffix: true
})}
</Typography>
<List.Trailing>
<Typography variant="title">
{new Date(startTime * 1000).toLocaleString(undefined, {
hour: "numeric",
minute: "numeric"
})}
</Typography>
</List.Trailing>
</List.Item>
<List.Item>
<List.Leading>
<Icon>
<Papicons name="Login" />
</Icon>
</List.Leading>
<Typography variant="title">
{t("Modal_Course_End")}
</Typography>
<List.Trailing>
<Typography variant="title">
{new Date(endTime * 1000).toLocaleString(undefined, {
hour: "numeric",
minute: "numeric"
})}
</Typography>
</List.Trailing>
</List.Item>
</List.Section>
<List.Section>
<List.SectionTitle>
<List.Label>{t("Modal_Course_Details")}</List.Label>
</List.SectionTitle>
<List.Item>
<List.Leading>
<Icon>
<Papicons name="User" />
</Icon>
</List.Leading>
<Typography variant="title">
{t("Modal_Course_Teacher")}
</Typography>
<Typography variant="body1" color="textSecondary">
{item.teacher}
</Typography>
</List.Item>
<List.Item>
<List.Leading>
<Icon>
<Papicons name="MapPin" />
</Icon>
</List.Leading>
<Typography variant="title">
{t("Modal_Course_Room")}
</Typography>
<Typography variant="body1" color="textSecondary">
{item.room || t("No_Course_Room")}
</Typography>
</List.Item>
<List.Item>
<List.Leading>
<Icon>
<Papicons name="Clock" />
</Icon>
</List.Leading>
<Typography variant="title">
{t("Modal_Course_Duration")}
</Typography>
<Typography variant="body1" color="textSecondary">
{formatDistanceStrict(startTime * 1000, endTime * 1000, {
locale: DateLocale[i18n.language as keyof typeof DateLocale] || DateLocale.enUS
})}
</Typography>
</List.Item>
</List.Section>
</List>
</>
)
}
================================================
FILE: app/(modals)/grade.tsx
================================================
import { Papicons } from '@getpapillon/papicons';
import { useRoute, useTheme } from "@react-navigation/native";
import { t } from "i18next";
import React from "react";
import { Platform, View } from "react-native";
import LinearGradient from "react-native-linear-gradient";
import ModalOverhead, { ModalOverHeadScore } from '@/components/ModalOverhead';
import { Grade as SharedGrade } from "@/services/shared/grade";
import ContainedNumber from "@/ui/components/ContainedNumber";
import Icon from "@/ui/components/Icon";
import Stack from "@/ui/components/Stack";
import TableFlatList from "@/ui/components/TableFlatList";
import TypographyLegacy from "@/ui/components/Typography";
import adjust from '@/utils/adjustColor';
import { colorCheck } from '@/utils/colorCheck';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import List from '@/ui/new/List';
import Typography from '@/ui/new/Typography';
interface SubjectInfo {
name: string;
originalName: string;
emoji: string;
color: string;
}
interface GradesModalProps {
grade: SharedGrade;
subjectInfo: SubjectInfo;
avgInfluence: number;
avgClass: number;
}
interface GradeBadgeProps {
icon: string;
label: string;
color: string;
theme: any;
is_outlined?: boolean;
}
const GradeBadge = ({ icon, label, color, theme, is_outlined = false }: GradeBadgeProps) => {
const backgroundColor = is_outlined ? "transparent" : adjust(color, theme.dark ? 0.3 : -0.3);
const textColor = is_outlined ? color : (colorCheck("#FFFFFF", [backgroundColor]) ? "#FFFFFF" : "#000000");
const borderStyle = is_outlined ? { borderWidth: 1, borderColor: color } : undefined;
return (
<Stack direction="horizontal" gap={8} backgroundColor={backgroundColor} vAlign="center" hAlign="center" padding={[12, 6]} radius={32} style={borderStyle}>
<Papicons size={20} name={icon} color={textColor} />
<TypographyLegacy color={textColor} variant='body2'>
{label}
</TypographyLegacy>
</Stack>
);
};
export default function GradesModal() {
const { params } = useRoute();
const theme = useTheme();
const colors = theme.colors;
if (!params) {
return null;
}
const { grade, subjectInfo, avgInfluence = 0, avgClass = 0 } = params as GradesModalProps;
const insets = useSafeAreaInsets();
const finalHeaderHeight = Platform.select({
android: insets.top + 32,
default: 0
});
return (
<>
{Platform.OS !== 'android' && (
<LinearGradient
colors={[subjectInfo.color, colors.background]}
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: 300,
width: "100%",
zIndex: -9,
opacity: 0.4
}}
/>
)}
<List
ListHeaderComponent={
<View
style={{
alignItems: "center",
justifyContent: "center",
gap: 16,
marginVertical: 20,
paddingTop: finalHeaderHeight
}}
>
<ModalOverhead
color={Platform.OS === 'ios' ? subjectInfo.color : colors.primary}
emoji={subjectInfo.emoji}
subject={subjectInfo.name}
title={grade.description}
date={new Date(grade.givenAt)}
overhead={
<ModalOverHeadScore
color={Platform.OS === 'ios' ? subjectInfo.color : colors.primary}
score={grade.studentScore?.disabled ? String(grade.studentScore?.status) : String(grade.studentScore?.value.toFixed(2))}
outOf={grade.outOf?.value}
/>
}
/>
{grade.studentScore?.value === grade.maxScore?.value && !grade.studentScore?.disabled &&
<GradeBadge
icon="crown"
label={t("Modal_Grades_BestGrade")}
color={subjectInfo.color}
theme={theme}
is_outlined={false}
/>
}
{grade.optional &&
<GradeBadge
icon="info"
label={t("Modal_Grades_OptionalGrade")}
color={subjectInfo.color}
theme={theme}
is_outlined={true}
/>
}
{grade.bonus &&
<GradeBadge
icon="info"
label={t("Modal_Grades_BonusGrade")}
color={subjectInfo.color}
theme={theme}
is_outlined={true}
/>
}
<Stack
card
direction="horizontal"
width={"100%"}
style={{ marginTop: 8 }}
>
<Stack
width={"50%"}
vAlign="center"
hAlign="center"
style={{ borderRightWidth: 1, borderRightColor: colors.border }}
padding={12}
>
<Icon papicon opacity={0.5}>
<Papicons name={"Coefficient"} />
</Icon>
<TypographyLegacy color="secondary">
{t("Grades_Coefficient")}
</TypographyLegacy>
<ContainedNumber color={Platform.OS === 'android' ? theme.colors.tint : adjust(subjectInfo.color, theme.dark ? 0.3 : -0.3)}>
x{(grade.coefficient ?? 1).toFixed(2)}
</ContainedNumber>
</Stack>
<Stack
width={"50%"}
vAlign="center"
hAlign="center"
padding={12}
>
<Icon papicon opacity={0.5}>
<Papicons name={"Apple"} />
</Icon>
<TypographyLegacy color="secondary">
{t("Grades_Avg_Group_Short")}
</TypographyLegacy>
<ContainedNumber color={Platform.OS === 'android' ? theme.colors.tint : adjust(subjectInfo.color, theme.dark ? 0.3 : -0.3)} denominator={"/" + grade.outOf?.value}>
{grade.averageScore?.value.toFixed(2)}
</ContainedNumber>
</Stack>
</Stack>
</View>
}
style={{ backgroundColor: "transparent" }}
contentContainerStyle={{ padding: 16,
paddingBottom: 16 + insets.bottom }}
>
<List.Section>
<List.SectionTitle>
<List.Label>{t("Grades_Details_Title")}</List.Label>
</List.SectionTitle>
{grade.studentScore && grade.outOf && grade.outOf.value !== 20 ? (
<List.Item>
<List.Leading>
<Icon>
<Papicons name={"Star"} />
</Icon>
</List.Leading>
<Typography variant="title">
{t("Grades_NormalizedGrade_Title")}
</Typography>
<Typography variant="body1" color="textSecondary">
{t("Grades_NormalizedGrade_Description")}
</Typography>
<List.Trailing>
<ContainedNumber
color={subjectInfo.color}
denominator={"/20"}
>
{((grade.studentScore.value / grade.outOf.value) * 20).toFixed(2)}
</ContainedNumber>
</List.Trailing>
</List.Item>
) : null}
<List.Item>
<List.Leading>
<Icon>
<Papicons name={"Plus"} />
</Icon>
</List.Leading>
<Typography variant="title">
{t("Grades_HighestGrade_Title")}
</Typography>
<Typography variant="body1" color="textSecondary">
{t("Grades_HighestGrade_Description")}
</Typography>
<List.Trailing>
<ContainedNumber
color={Platform.OS === 'android' ? theme.colors.tint : adjust(subjectInfo.color, theme.dark ? 0.3 : -0.3)}
denominator={"/" + grade.outOf?.value}
>
{grade.maxScore?.value.toFixed(2)}
</ContainedNumber>
</List.Trailing>
</List.Item>
<List.Item>
<List.Leading>
<Icon>
<Papicons name={"Minus"} />
</Icon>
</List.Leading>
<Typography variant="title">
{t("Grades_LowestGrade_Title")}
</Typography>
<Typography variant="body1" color="textSecondary">
{t("Grades_LowestGrade_Description")}
</Typography>
<List.Trailing>
<ContainedNumber
color={Platform.OS === 'android' ? theme.colors.tint : adjust(subjectInfo.color, theme.dark ? 0.3 : -0.3)}
denominator={"/" + grade.outOf?.value}
>
{grade.minScore?.value.toFixed(2)}
</ContainedNumber>
</List.Trailing>
</List.Item>
</List.Section>
<List.Section>
<List.SectionTitle>
<List.Label>{t("Grades_Influence_Title")}</List.Label>
</List.SectionTitle>
<List.Item>
<List.Leading>
<Icon>
<Papicons name={"Grades"} />
</Icon>
</List.Leading>
<Typography variant="title">
{t("Grades_Avg_All_Title")}
</Typography>
<List.Trailing>
<ContainedNumber
color={avgInfluence === 0 ? "#757575" : avgInfluence >= 0 ? "#2e8900" : "#990000"}
denominator="pts"
>
{avgInfluence >= 0 ? `+${avgInfluence.toFixed(2)}` : avgInfluence.toFixed(2)}
</ContainedNumber>
</List.Trailing>
</List.Item>
<List.Item>
<List.Leading>
<Icon>
<Papicons name={"Apple"} />
</Icon>
</List.Leading>
<Typography variant="title">
{t("Grades_Avg_Group_Title")}
</Typography>
<List.Trailing>
<ContainedNumber
color={avgClass === 0 ? "#757575" : avgClass >= 0 ? "#2e8900" : "#990000"}
denominator="pts"
>
{avgClass >= 0 ? `+${avgClass.toFixed(2)}` : avgClass.toFixed(2)}
</ContainedNumber>
</List.Trailing>
</List.Item>
</List.Section>
</List>
</>
)
}
================================================
FILE: app/(modals)/news.tsx
================================================
import { getManager } from "@/services/shared";
import { News } from "@/services/shared/news";
import { useAccountStore } from "@/stores/account";
import { Services } from "@/stores/account/types";
import Stack from "@/ui/components/Stack";
import TypographyLegacy from "@/ui/components/Typography";
import { useLocalSearchParams, useNavigation, useRouter } from "expo-router"
import { useEffect, useState } from "react";
import { Linking, Platform, ScrollView, StyleSheet, View } from "react-native";
import { Attachment, News as SkolengoNews } from "skolengojs"
import { VARIANTS } from "@/ui/components/Typography";
import HTMLView from 'react-native-htmlview';
import * as WebBrowser from 'expo-web-browser';
import { useTheme } from "@react-navigation/native";
import { NativeHeaderPressable, NativeHeaderSide } from "@/ui/components/NativeHeader";
import { MenuView } from "@react-native-menu/menu";
import Icon from "@/ui/components/Icon";
import { t } from "i18next";
import ListLegacy from "@/ui/components/List";
import Item, { Leading } from "@/ui/components/Item";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { cleanHtmlForArticle } from "@/utils/news/cleanUpHTMLNews";
import { news } from "pawnote";
import Avatar from "@/ui/components/Avatar";
import { getInitials } from "@/utils/chats/initials";
import { HeaderBackButton } from "@react-navigation/elements";
import { runsIOS26 } from "@/ui/utils/IsLiquidGlass";
import { IconNames, Papicons } from "@getpapillon/papicons";
import { getAttachmentIcon } from "@/utils/news/getAttachmentIcon";
import List from "@/ui/new/List";
import Typography from "@/ui/new/Typography";
const NewsModal = () => {
const search = useLocalSearchParams();
const news = JSON.parse(String(search.news)) as News
const insets = useSafeAreaInsets();
const navigation = useNavigation()
const router = useRouter()
useEffect(() => {
const acknowledgeNews = async () => {
if (!news.acknowledged) {
const manager = getManager();
const store = useAccountStore.getState()
const account = store.accounts.find(account => account.id === store.lastUsedAccount)
const service = account?.services.find(service => service.id === news.createdByAccount)
if (service?.serviceId === Services.SKOLENGO) {
const attachment = new Attachment("", "", "")
const ref = new SkolengoNews(news.id, news.createdAt, news.title ?? "", news.content, news.content, { id: "", name: "" }, "", attachment)
news.ref = ref
}
await manager.setNewsAsDone(news);
}
};
acknowledgeNews();
}, [])
const { colors } = useTheme();
const stylesheet = StyleSheet.create({
...VARIANTS,
p: {
...VARIANTS.body1,
color: colors.text,
},
div: {
...VARIANTS.body1,
color: colors.text,
},
a: {
color: colors.primary,
textDecorationLine: 'underline'
},
ul: {
...VARIANTS.body1,
paddingHorizontal: 4,
color: colors.text,
},
});
const [HTMLCleanupEnabled, setHTMLCleanupEnabled] = useState(true)
const cleanedContent = HTMLCleanupEnabled ? cleanHtmlForArticle(news.content) : news.content
return (
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={{ flex: 1 }}
contentContainerStyle={{
paddingHorizontal: 20,
paddingTop: 20,
paddingBottom: 20 + insets.bottom,
gap: 24
}}
>
{
Platform.OS === 'ios' && (
<NativeHeaderSide side="Left">
<HeaderBackButton
tintColor={runsIOS26 ? colors.text : colors.primary}
onPress={() => router.back()}
style={{
marginLeft: runsIOS26 ? 3 : -32,
}}
/>
</NativeHeaderSide>
)
}
<Stack gap={10}>
<Stack padding={[10, 4]} radius={200} backgroundColor={colors.text + "16"}>
<TypographyLegacy variant="body2">
{news.category}
</TypographyLegacy>
</Stack>
<TypographyLegacy variant="h3">
{news.title}
</TypographyLegacy>
<Stack direction="horizontal" hAlign="center">
<Stack direction="horizontal" gap={8} inline flex hAlign="center">
<Avatar initials={getInitials(news.author)} size={28} />
<TypographyLegacy nowrap variant="body2">
{news.author}
</TypographyLegacy>
</Stack>
<TypographyLegacy nowrap variant="body2" color="secondary">
{new Date(news.createdAt).toLocaleDateString(undefined, {
day: '2-digit',
month: 'short',
year: 'numeric'
})}
</TypographyLegacy>
</Stack>
</Stack>
{news.question && (
<List scrollEnabled={false}>
<List.Item>
<List.Leading>
<Icon>
<Papicons name="pie" />
</Icon>
</List.Leading>
<Typography variant="title">
Cette actualité contient un sondage
</Typography>
<Typography variant="body1" color="textSecondary">
PRONOTE ne nous permet pas d'afficher les sondages pour le moment.
</Typography>
</List.Item>
</List>
)}
<HTMLView
value={cleanedContent}
stylesheet={stylesheet}
style={{
gap: 12
}}
paragraphBreak=""
bullet=" • "
/>
{news.attachments.length > 0 && (
<ListLegacy>
{news.attachments.map((attachment, index) => (
<Item key={index} onPress={() => Linking.openURL(attachment.url)}>
<Leading>
<Icon size={28}>
<Papicons name={getAttachmentIcon(attachment)} />
</Icon>
</Leading>
<TypographyLegacy variant="title">
{attachment.name}
</TypographyLegacy>
<TypographyLegacy variant="body1" nowrap color="secondary">
{attachment.url}
</TypographyLegacy>
</Item>
))}
</ListLegacy>
)}
<Stack gap={0} style={{ opacity: 0.4 }}>
<TypographyLegacy variant="caption">
Si cette actualité ne s'affiche pas correctement,
</TypographyLegacy>
<TypographyLegacy variant="caption" style={{
textDecorationLine: 'underline'
}} onPress={() => setHTMLCleanupEnabled(!HTMLCleanupEnabled)}>
{HTMLCleanupEnabled ? "désactiver" : "activer"} le formattage automatique
</TypographyLegacy>
</Stack>
</ScrollView >
);
};
export default NewsModal;
================================================
FILE: app/(modals)/notifications.tsx
================================================
import { Papicons } from "@getpapillon/papicons";
import { useTheme } from "@react-navigation/native";
import { useTranslation } from "react-i18next";
import Stack from "@/ui/components/Stack";
import Typography from "@/ui/components/Typography";
export default function NotificationsModal() {
const { colors } = useTheme();
const { t } = useTranslation();
return (
<Stack
hAlign={"center"}
vAlign={"top"}
paddingTop={150}
paddingHorizontal={20}
paddingBottom={20}
style={{
width: "100%",
height: "100%",
backgroundColor: colors.background
}}
>
<Papicons name={"Clock"} size={80} style={{ marginBottom: 10 }} opacity={0.5} color={colors.text} />
<Typography variant="h2" align={"center"}>
{t("Feature_Soon")}
</Typography>
<Typography variant={"caption"} align={"center"} color={"secondary"}>
{t("Feature_Soon_Notification")}
</Typography>
</Stack>
)
}
================================================
FILE: app/(modals)/profile.tsx
================================================
import { Papicons } from "@getpapillon/papicons";
import { MenuView, NativeActionEvent } from "@react-native-menu/menu";
import { useHeaderHeight } from "@react-navigation/elements";
import { useTheme } from "@react-navigation/native";
import * as ImagePicker from "expo-image-picker"
import { router } from "expo-router";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Alert,
KeyboardAvoidingView,
Platform,
ScrollView,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import OnboardingInput from "@/components/onboarding/OnboardingInput";
import { useAccountStore } from "@/stores/account";
import Avatar from "@/ui/components/Avatar";
import Button from "@/ui/components/Button";
import Icon from "@/ui/components/Icon";
import { NativeHeaderPressable, NativeHeaderSide } from "@/ui/components/NativeHeader";
import Typography from "@/ui/components/Typography";
import { getInitials } from "@/utils/chats/initials";
import ActionMenu from "@/ui/components/ActionMenu";
export default function CustomProfileScreen() {
const { t } = useTranslation();
const store = useAccountStore.getState();
const accounts = useAccountStore((state) => state.accounts);
const lastUsedAccount = useAccountStore((state) => state.lastUsedAccount);
const account = accounts.find((a) => a.id === lastUsedAccount);
const [firstName, setFirstName] = useState<string>(account?.firstName ?? "");
const [lastName, setLastName] = useState<string>(account?.lastName ?? "");
const [profilePictureUrl, setProfilePictureUrl] = useState<string | null>(account?.customisation?.profilePicture ? `data:image/png;base64,${account.customisation.profilePicture}` : null);
useEffect(() => {
if (account) {
setFirstName(account.firstName);
setLastName(account.lastName);
setProfilePictureUrl(account.customisation?.profilePicture ? `data:image/png;base64,${account.customisation.profilePicture}` : null);
}
}, [account]);
const insets = useSafeAreaInsets()
const updateProfilePictureFromLibrary = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ['images', 'videos'],
allowsEditing: true,
aspect: [4, 3],
quality: 1,
base64: true
});
if (!result.canceled) {
const b64 = result.assets[0].base64 ?? "";
store.setAccountProfilePicture(lastUsedAccount, b64);
}
}
const updateProfilePictureFromService = async () => {
Alert.alert(
t("Feature_Soon"),
"Cette fonctionnalité n'est pas encore disponible, mais elle le sera dans une prochaine mise à jour.",
[{ text: "OK" }]
);
}
const { colors } = useTheme();
const height = useHeaderHeight();
return (
<KeyboardAvoidingView
behavior={"position"}
keyboardVerticalOffset={-insets.top * 3.2}
style={{ flex: 1 }}
>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
style={{ height: "100%" }}
>
<View style={{ paddingHorizontal: 50, alignItems: "center", gap: 15, paddingTop: 20 }}>
<Avatar
size={117}
initials={getInitials(`${firstName} ${lastName}`)}
imageUrl={profilePictureUrl || undefined}
/>
<ActionMenu
actions={[
{
id: 'photo_library',
title: t("Button_Change_ProfilePicture_FromLibrary"),
papicon: 'gallery',
image: Platform.select({
ios: 'photo',
android: 'ic_menu_gallery',
}),
imageColor: colors.text
},
{
id: 'from_service',
title: t("Button_Change_ProfilePicture_FromService"),
papicon: 'crown',
image: Platform.select({
ios: 'square.and.arrow.down',
android: 'ic_menu_save',
}),
imageColor: colors.text
},
{
id: 'remove_photo',
title: t("Button_Change_ProfilePicture_Remove"),
attributes: { destructive: true },
papicon: 'trash',
image: Platform.select({
ios: 'trash',
android: 'ic_menu_delete',
}),
imageColor: "#FF0000"
}
]}
onPressAction={(e: NativeActionEvent) => {
switch (e.nativeEvent.event) {
case 'photo_library':
updateProfilePictureFromLibrary();
break;
case 'from_service':
updateProfilePictureFromService();
break;
case 'remove_photo':
store.setAccountProfilePicture(lastUsedAccount, "");
break;
}
}}
>
<Button
inline
size="small"
icon={<Papicons name="Camera" />}
title={t("Button_Change_ProfilePicture")}
/>
</ActionMenu>
</View>
<View style={{ paddingHorizontal: 20, paddingTop: 30, gap: 15 }}>
<View style={{ gap: 10 }}>
<Typography color="secondary">Prénom</Typography>
<OnboardingInput
placeholder={"Prénom"}
text={firstName}
setText={setFirstName}
icon={"Font"}
inputProps={{}}
/>
<Typography color="secondary">Nom</Typography>
<OnboardingInput
placeholder={"Nom"}
text={lastName}
setText={setLastName}
icon={"Bold"}
inputProps={{}}
/>
</View>
</View>
<NativeHeaderSide side="Left" key={`${firstName}-${lastName}`}>
<NativeHeaderPressable
onPressIn={() => {
useAccountStore.getState().setAccountName(lastUsedAccount, firstName, lastName);
router.back();
}}
>
<Icon papicon size={26}>
<Papicons name="ArrowLeft" />
</Icon>
</NativeHeaderPressable>
</NativeHeaderSide>
</ScrollView>
</ KeyboardAvoidingView >
);
}
================================================
FILE: app/(modals)/task.tsx
================================================
import { Papicons } from "@getpapillon/papicons";
import { useRoute, useTheme } from "@react-navigation/native";
import { LinearGradient } from "expo-linear-gradient";
import * as WebBrowser from "expo-web-browser";
import { t } from "i18next";
import React, { useState } from "react";
import ModalOverhead from "@/components/ModalOverhead";
import Homework from "@/database/models/Homework";
import { updateHomeworkIsDone } from "@/database/useHomework";
import { getManager } from "@/services/shared";
import AnimatedPressable from "@/ui/components/AnimatedPressable";
import Icon from "@/ui/components/Icon";
import Stack from "@/ui/components/Stack";
import TableFlatList from "@/ui/components/TableFlatList";
import { formatHTML } from "@/utils/format/html";
import { generateId } from "@/utils/generateId";
import { getAttachmentIcon } from "@/utils/news/getAttachmentIcon";
import { getSubjectColor } from "@/utils/subjects/colors";
import { getSubjectEmoji } from "@/utils/subjects/emoji";
import { getSubjectName } from "@/utils/subjects/name";
import { Platform } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import List from "@/ui/new/List";
import Typography from "@/ui/new/Typography";
const Task = () => {
const { params } = useRoute();
const theme = useTheme();
const colors = theme.colors;
const { task } = params as { task: Homework };
const subjectInfo = {
color: getSubjectColor(task.subject),
emoji: getSubjectEmoji(task.subject),
name: getSubjectName(task.subject)
}
const [isDone, setIsDone] = useState(task.isDone);
const setAsDone = async (done: boolean) => {
const manager = getManager();
await manager.setHomeworkCompletion(task, done);
const id = generateId(
task.subject +
task.content +
task.createdByAccount +
new Date(task.dueDate).toDateString()
);
updateHomeworkIsDone(id, done);
setIsDone(done);
}
const insets = useSafeAreaInsets();
const finalHeaderHeight = Platform.select({
android: insets.top + 32,
default: 0
});
return (
<>
{Platform.OS !== 'android' && (
<LinearGradient
colors={[subjectInfo.color, colors.background]}
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: 300,
width: "100%",
zIndex: -9,
opacity: 0.4
}}
/>
)}
<List
ListHeaderComponent={
<ModalOverhead
emoji={subjectInfo.emoji}
subject={subjectInfo.name}
subjectVariant="header"
color={Platform.OS === 'ios' ? subjectInfo.color : colors.primary}
date={new Date(task.dueDate)}
style={{
marginVertical: 24,
paddingTop: finalHeaderHeight,
}}
/>
}
style={{
backgroundColor: "transparent"
}}
contentContainerStyle={{
padding: 16
}}
>
<List.Section>
<List.SectionTitle>
<List.Label>{t("Modal_Task_Status")}</List.Label>
</List.SectionTitle>
<List.Item>
<List.Leading>
<AnimatedPressable onPress={() => setAsDone(!isDone)}>
<Stack
backgroundColor={isDone ? (Platform.OS === 'ios' ? subjectInfo.color : theme.colors.primary) : theme.colors.card}
card
radius={100}
width={28}
height={28}
vAlign="center"
hAlign="center"
>
{isDone &&
<Papicons name="check" size={22} color="white" />
}
</Stack>
</AnimatedPressable>
</List.Leading>
<Typography variant="title">
{isDone ? t("Task_Done") : t("Task_Undone")}
</Typography>
</List.Item>
</List.Section>
<List.Section>
<List.SectionTitle>
<List.Label>{t("Modal_Task_Description")}</List.Label>
</List.SectionTitle>
<List.Item>
<Typography>
{formatHTML(task.content)}
</Typography>
</List.Item>
</List.Section>
<List.Section>
<List.SectionTitle>
<List.Label>{t("Modal_Task_Attachments")}</List.Label>
</List.SectionTitle>
{task.attachments.map((attachment) => (
<List.Item onPress={() => WebBrowser.openBrowserAsync(attachment.url, {
presentationStyle: "formSheet"
})}>
<List.Leading>
<Icon>
<Papicons name={getAttachmentIcon(attachment)} />
</Icon>
</List.Leading>
<Typography variant="title" numberOfLines={1}>
{attachment.name || attachment.url}
</Typography>
<Typography variant="body1" color="textSecondary" numberOfLines={1}>
{attachment.url}
</Typography>
</List.Item>
))}
</List.Section>
</List>
</>
);
};
export default Task;
================================================
FILE: app/(modals)/wallpaper.tsx
================================================
import { useAccountStore } from "@/stores/account"
import { useSettingsStore } from "@/stores/settings"
import { Wallpaper } from "@/stores/settings/types"
import AnimatedPressable from "@/ui/components/AnimatedPressable"
import Stack from "@/ui/components/Stack"
import Typography from "@/ui/components/Typography"
import { useTheme } from "@react-navigation/native"
import React, { useEffect, useState } from "react"
import { FlatList, Image, Platform, RefreshControl, View } from "react-native"
import { File, Directory, Paths } from 'expo-file-system';
import ActivityIndicator from "@/components/ActivityIndicator"
import { NativeHeaderPressable, NativeHeaderSide } from "@/ui/components/NativeHeader"
import Icon from "@/ui/components/Icon"
import { router } from "expo-router";
import { Papicons } from "@getpapillon/papicons"
import { t } from "i18next";
import * as ImagePicker from 'expo-image-picker';
import ActionMenu from "@/ui/components/ActionMenu"
const COLLECTIONS_SOURCE = "https://raw.githubusercontent.com/PapillonApp/datasets/refs/heads/main/wallpapers/index.json";
interface Collection {
name: string;
icon?: string;
link?: string;
images: Wallpaper[];
}
const WallpaperModal = () => {
const { colors } = useTheme()
const [collections, setCollections] = useState<Collection[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const fetchCollections = async () => {
try {
setLoading(true);
const response = await fetch(COLLECTIONS_SOURCE);
const data = await response.json();
setCollections(data);
} catch (error) {
setError(error as string);
} finally {
setLoading(false);
}
}
useEffect(() => {
fetchCollections();
}, []);
const [currentlyDownloading, setCurrentlyDownloading] = useState<string[]>([]);
const settingsStore = useSettingsStore(state => state.personalization);
const mutateProperty = useSettingsStore(state => state.mutateProperty);
const currentWallpaper = settingsStore.wallpaper;
const selectedId = currentWallpaper?.id;
const hasCustomWallpaper = selectedId?.startsWith("custom:") ?? false;
const flatListRef = React.useRef<FlatList>(null);
useEffect(() => {
if (collections.length > 0 && currentWallpaper) {
const collectionIndex = collections.findIndex((collection) => collection.images.find((image) => image.id === currentWallpaper.id));
if (collectionIndex !== -1) {
setTimeout(() => {
flatListRef.current?.scrollToIndex({ index: collectionIndex, animated: true });
}, 500);
}
}
}, [collections, currentWallpaper]);
const wallpaperDirectory = new Directory(Paths.document, "wallpapers");
const downloadAndSelect = (wallpaper: Wallpaper) => {
const fileName = `${wallpaper.id}.jpg`;
const wallpaperFile = new File(wallpaperDirectory, fileName);
if (wallpaperFile.exists) {
mutateProperty("personalization", {
wallpaper: {
id: wallpaper.id,
path: {
directory: wallpaperDirectory.name,
name: wallpaperFile.name
}
}
})
return;
}
setCurrentlyDownloading((prev) => [...prev, wallpaper.id]);
if (!wallpaperDirectory.exists) {
wallpaperDirectory.create();
}
File.downloadFileAsync(wallpaper.url!, wallpaperFile).then((result) => {
mutateProperty("personalization", {
wallpaper: {
id: wallpaper.id,
path: {
directory: wallpaperDirectory.name,
name: result.name
}
}
})
}).finally(() => {
setCurrentlyDownloading((prev) => prev.filter((id) => id !== wallpaper.id));
})
}
const uploadCustomWallpaper = () => {
try {
ImagePicker.launchImageLibraryAsync({
allowsEditing: true,
aspect: [4, 3],
quality: 1,
}).then((result) => {
if (result.canceled) return;
const asset = result.assets[0];
const sourceFile = new File(asset.uri);
if (!wallpaperDirectory.exists) {
wallpaperDirectory.create();
}
const newFileName = `custom:${Date.now()}.jpg`;
const destFile = new File(wallpaperDirectory, newFileName);
sourceFile.copy(destFile);
mutateProperty("personalization", {
wallpaper: {
id: `custom:${Date.now()}`,
path: {
directory: wallpaperDirectory.name,
name: destFile.name
}
}
})
})
} catch (error) {
console.log(error);
}
}
return (
<>
<FlatList
ref={flatListRef}
data={collections}
style={{
flex: 1,
}}
contentContainerStyle={{
gap: 16,
paddingTop: Platform.OS === 'android' ? 20 : 72
}}
renderItem={({ item, index }) => (
<View>
<Stack direction="horizontal" alignItems="center" gap={8} padding={[16, 10]}>
{item.icon &&
<Image
source={{ uri: item.icon }}
style={{
width: 24,
height: 24,
borderRadius: 6
}}
/>
}
<Typography style={{ flex: 1 }} variant="body1" color="text">{item.name}</Typography>
{item.images.find((image) => image.id === currentWallpaper?.id) && item.images.find((image) => image.id === currentWallpaper?.id)?.credit &&
<Typography variant="caption" color="secondary">{item.images.find((image) => image.id === currentWallpaper?.id)?.credit}</Typography>
}
</Stack>
<FlatList
data={item.images}
horizontal
style={{
width: "100%",
paddingHorizontal: 12
}}
contentContainerStyle={{
gap: 6,
paddingRight: 12
}}
showsHorizontalScrollIndicator={false}
renderItem={({ item }) => <WallpaperImage item={item} onPress={() => downloadAndSelect(item)} selectedId={currentWallpaper?.id} isDownloading={currentlyDownloading.includes(item.id)} />}
getItemLayout={(data, index) => (
{ length: 160 + 6, offset: (160 + 6) * index, index }
)}
initialScrollIndex={item.images.findIndex((image) => image.id === currentWallpaper?.id) !== -1 ? item.images.findIndex((image) => image.id === currentWallpaper?.id) : undefined}
/>
</View>
)}
refreshControl={
<RefreshControl
refreshing={loading}
onRefresh={fetchCollections}
progressViewOffset={72}
/>
}
/>
<NativeHeaderSide side="Left" key={currentWallpaper?.id + ":" + "upload:" + (hasCustomWallpaper ? "true" : "false")}>
{Platform.OS === 'android' ? (
<NativeHeaderPressable onPress={() => router.back()}>
<Icon size={28}>
<Papicons name="Cross" />
</Icon>
</NativeHeaderPressable>
) : (
<NativeHeaderPressable onPress={() => uploadCustomWallpaper()}>
<Icon size={28} fill={hasCustomWallpaper ? colors.primary : undefined}>
<Papicons name="Gallery" />
</Icon>
</NativeHeaderPressable>
)}
</NativeHeaderSide>
<NativeHeaderSide side="Right" key={currentWallpaper?.id + ":" + wallpaperDirectory.exists}>
{Platform.OS === 'android' && (
<NativeHeaderPressable onPress={() => uploadCustomWallpaper()}>
<Icon size={28} fill={hasCustomWallpaper ? colors.primary : undefined}>
<Papicons name="Gallery" />
</Icon>
</NativeHeaderPressable>
)}
<ActionMenu
actions={[
{
id: "background:clear",
title: t("Modal_Wallpaper_Clear"),
imageColor: "#FF0000",
image: Platform.select({
ios: "trash.fill"
}),
attributes: { "destructive": true, "disabled": !currentWallpaper }
},
{
id: "background:downloads",
title: t("Modal_Wallpaper_Downloads"),
imageColor: colors.text,
image: Platform.select({
ios: "square.and.arrow.down"
}),
displayInline: false,
subactions: [
{
title: t("Modal_Wallpaper_Downloads_Size"),
subtitle: (wallpaperDirectory.info().size / (1024 * 1024)).toFixed(2) + " MB"
},
{
id: "downloads:clear",
title: t("Modal_Wallpaper_ClearDownloads"),
imageColor: "#FF0000",
image: Platform.select({
ios: "trash.fill"
}),
attributes: { "destructive": true, "disabled": !wallpaperDirectory.exists }
}
]
},
]}
placement="below"
onPressAction={({ nativeEvent }) => {
const action = nativeEvent.event;
if (action === "downloads:clear") {
wallpaperDirectory.delete();
mutateProperty("personalization", {
wallpaper: undefined
})
}
if (action === "background:clear") {
mutateProperty("personalization", {
wallpaper: undefined
})
}
}}
>
<NativeHeaderPressable>
<Icon size={28}>
<Papicons name="Gears" />
</Icon>
</NativeHeaderPressable>
</ActionMenu>
</NativeHeaderSide>
</>
)
}
const WallpaperImage = ({ item, onPress, selectedId, isDownloading }: { item: WallpaperCollection, onPress: () => void, selectedId: string, isDownloading: boolean }) => {
const [imageLoaded, setImageLoaded] = useState(false);
const { colors } = useTheme();
return (
<AnimatedPressable
onPress={onPress}
>
<View
style={{
width: 160,
height: 100,
padding: 2,
borderRadius: 16,
borderCurve: "continuous",
borderWidth: selectedId === item.id ? 2 : 0,
borderColor: selectedId === item.id ? colors.primary : "transparent"
}}
key={item.id}
>
{
(!imageLoaded || isDownloading) &&
<View
style={{
position: "absolute",
top: 2,
left: 2,
width: "100%",
height: "100%",
justifyContent: "center",
alignItems: "center",
zIndex: 1,
borderRadius: 12,
backgroundColor: "rgba(0, 0, 0, 0.5)"
}}
>
<ActivityIndicator color="#ffffff" />
</View>
}
<Image
source={{ uri: item.thumbnail || item.url }}
style={{ width: "100%", height: "100%", borderRadius: 12 }}
onLoad={() => setImageLoaded(true)}
/>
</View>
</AnimatedPressable>
);
};
export default WallpaperModal
================================================
FILE: app/(modals)/wrapped/_layout.tsx
================================================
import { Stack } from "expo-router";
import React from "react";
import { screenOptions } from "@/utils/theme/ScreenOptions";
export default function Layout() {
return (
<Stack screenOptions={screenOptions}>
<Stack.Screen
name="index"
options={{
headerShown: false,
}}
/>
</Stack>
);
}
================================================
FILE: app/(modals)/wrapped/index.tsx
================================================
import React, { useEffect, useRef, useState } from 'react';
import { Dimensions, FlatList, Pressable, StatusBar, View } from 'react-native';
import { useVideoPlayer, VideoView, VideoPlayer } from 'expo-video';
import { useEvent } from "expo";
import AnimatedPressable from '@/ui/components/AnimatedPressable';
import { LiquidGlassView } from '@sbaiahmed1/react-native-blur';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import { Papicons } from '@getpapillon/papicons';
import { useNavigation } from 'expo-router';
import Reanimated, { FadeIn, FadeInDown, FadeInLeft, FadeInRight, FadeInUp, FadeOut, FadeOutUp, LinearTransition, ZoomIn, ZoomOut } from 'react-native-reanimated';
import Typography from '@/ui/components/Typography';
import { Cooking, Warning } from '@/app/(modals)/wrapped/stories/consent';
import { Welcome } from '@/app/(modals)/wrapped/stories/welcome';
const WrappedView = () => {
const insets = useSafeAreaInsets();
const navigation = useNavigation();
const [aboutToExit, setAboutToExit] = useState(false);
const [currentIndex, setCurrentIndex] = useState(0);
const mainBackground = useVideoPlayer({
assetId: require('@/assets/video/wrapped.mp4'),
}, player => {
player.loop = true;
player.play();
});
const altBackground = useVideoPlayer({
assetId: require('@/assets/video/wrapped_alt.mp4'),
}, player => {
player.loop = true;
player.play();
});
const redBackground = useVideoPlayer({
assetId: require('@/assets/video/wrapped_red.mp4'),
}, player => {
player.loop = true;
player.play();
});
const slides = [Welcome, Warning, Cooking];
const sliderRef = useRef<FlatList>(null);
return (
<View style={{ flex: 1, backgroundColor: 'black' }}>
{!aboutToExit && (
<>
{(currentIndex == 0 || currentIndex == 1 || currentIndex == 2) && (
<>
<StatusBar barStyle={"light-content"} />
<WrappedBackgroundVideo player={mainBackground} />
</>
)}
</>
)}
<LiquidGlassView
style={{
position: 'absolute',
top: insets.top + 2,
right: 16,
zIndex: 100,
borderRadius: 120,
}}
glassType="clear"
isInteractive={true}
glassOpacity={0.6}
glassTintColor={"#000"}
>
<Pressable
style={{
padding: 10
}}
onPress={() => {
setTimeout(() => {
setAboutToExit(true);
}, 0);
setTimeout(() => {
navigation.goBack();
}, 50);
}}
>
<Papicons name="cross" size={24} color='white' />
</Pressable>
</LiquidGlassView>
<View
style={{
position: 'absolute',
top: 0,
bottom: 0,
left: 0,
padding: 16,
gap: 8,
zIndex: 100,
justifyContent: 'center',
alignItems: 'center',
flexDirection: 'column',
}}
>
{slides.map((_, index) => (
<Reanimated.View
layout={LinearTransition.springify().duration(300)}
key={index}
style={{
width: index === currentIndex ? 8 : 6,
height: index === currentIndex ? 42 : 6,
borderRadius: 5,
backgroundColor: index === currentIndex ? '#FFF' : '#FFFFFF95',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 0,
},
shadowOpacity: index === currentIndex ? 0.3 : 1,
shadowRadius: 5,
}}
/>
))}
</View>
<FlatList
removeClippedSubviews={true}
windowSize={1}
data={slides}
renderItem={({ item: Item, index }) => (
<Item isCurrent={index === currentIndex} sliderRef={sliderRef} />
)}
keyExtractor={(item, index) => index.toString()}
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 10,
}}
showsVerticalScrollIndicator={false}
showsHorizontalScrollIndicator={false}
scrollEventThrottle={16}
snapToInterval={Dimensions.get('screen').height}
decelerationRate="fast"
onScroll={e => {
const index = Math.round(e.nativeEvent.contentOffset.y / Dimensions.get('screen').height);
setCurrentIndex(index);
}}
ref={sliderRef}
/>
</View>
);
};
const WrappedBackgroundVideo = ({ player }: { player: VideoPlayer }) => {
const { isPlaying, oldIsPlaying } = useEvent(player, 'playingChange', { isPlaying: player.playing });
useEffect(() => {
if (!isPlaying) {
player.play();
}
}, [isPlaying]);
return (
<Reanimated.View
style={{
width: '100%',
height: '100%',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
zIndex: 1,
}}
entering={FadeIn.duration(300)}
exiting={FadeOut.duration(300)}
>
<VideoView
player={player}
style={{
width: '100%',
height: '100%',
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
}}
contentFit="cover"
nativeControls={false}
/>
</Reanimated.View>
);
};
export default WrappedView;
================================================
FILE: app/(modals)/wrapped/stories/consent.tsx
================================================
import { Papicons } from '@getpapillon/papicons';
import { useTheme } from '@react-navigation/native';
import { LiquidGlassView } from '@sbaiahmed1/react-native-blur';
import React, { memo, useCallback, useState } from 'react';
import { Dimensions, FlatList, StyleSheet, Switch, View } from 'react-native';
import Reanimated, { FadeInDown, FadeOut, FadeOutUp } from 'react-native-reanimated';
import Stack from '@/ui/components/Stack';
import Typography from "@/ui/components/Typography";
import adjust from '@/utils/adjustColor';
import NativeSwitch from '@/ui/native/NativeSwitch';
type ConsentItem = {
title: string;
icon: string;
enabled: boolean;
};
const ConsentButton = memo(({
item,
index,
borderColor,
onToggle
}: {
item: ConsentItem;
index: number;
borderColor: string;
onToggle: (index: number) => void;
}) => (
<Reanimated.View
entering={FadeInDown.springify().duration(400).delay(index * 100 + 500)}
exiting={FadeOutUp.springify().duration(400).delay(index * 100 + 500)}
>
<LiquidGlassView
glassTintColor='white'
glassOpacity={0.2}
glassType='clear'
isInteractive
style={{
justifyContent: "space-between",
borderRadius: 20,
borderCurve: "circular",
borderWidth: 1,
borderColor,
width: 300
}}>
<Stack
direction='horizontal'
padding={15}
>
<Stack direction='horizontal' style={{ alignItems: "center", flex: 1 }} gap={10}>
<Papicons name={item.icon} color={ICON_COLOR} />
<Typography variant='title' color={ICON_COLOR}>{item.title}</Typography>
</Stack>
<NativeSwitch value={item.enabled} onValueChange={() => onToggle(index)} trackColor={TRACK_COLOR} />
</Stack>
</LiquidGlassView>
</Reanimated.View>
));
ConsentButton.displayName = 'ConsentButton';
const INITIAL_ITEMS: ConsentItem[] = [
{ title: "Mes notes", icon: "Pie", enabled: true },
{ title: "Absences et retards", icon: "Chair", enabled: true },
{ title: "Emploi du temps", icon: "Calendar", enabled: true },
{ title: "Tâches", icon: "Tasks", enabled: true }
];
const ICON_COLOR = '#31424A';
const TRACK_COLOR = { true: "#C50000" };
export const Warning = ({ isCurrent }: { isCurrent: boolean, sliderRef: React.RefObject<FlatList> }) => {
const { colors } = useTheme();
const [consentItems, setConsentItems] = useState(INITIAL_ITEMS);
const toggleConsent = useCallback((index: number) => {
setConsentItems(prev => prev.map((item, i) =>
i === index ? { ...item, enabled: !item.enabled } : item
));
}, []);
const renderItem = useCallback(({ item, index }: { item: ConsentItem; index: number }) => (
<ConsentButton item={item} index={index} borderColor={colors.border} onToggle={toggleConsent} />
), [colors.border, toggleConsent]);
return (
<View style={{ width: "100%", height: Dimensions.get('screen').height, justifyContent: 'center', alignItems: 'center' }}>
{isCurrent && (
<View
style={{
flex: 1,
alignItems: "center",
alignContent: "center",
justifyContent: "center"
}}>
<Reanimated.View
entering={FadeInDown.springify().dampingRatio(0.5).duration(1800).delay(200)}
exiting={FadeOut.duration(800)}
>
<Typography variant="h4" align='center' color={adjust(colors.background, 0.1)} style={styles.title}>
Ton Yearbook contient un récap de toutes tes statistiques liées à ta vie d'étudiant, choisis ce que tu souhaites afficher avant de commencer :
</Typography>
<FlatList
data={consentItems}
renderItem={renderItem}
removeClippedSubviews
style={styles.listContent}
/>
</Reanimated.View>
</View>
)}
</View>
);
};
export const Cooking = ({ isCurrent }: { isCurrent: boolean, sliderRef: React.RefObject<FlatList> }) => {
const { colors } = useTheme();
return (
<View style={{ width: "100%", height: Dimensions.get('screen').height, justifyContent: 'center', alignItems: 'center' }}>
{isCurrent && (
<Reanimated.View
entering={FadeInDown.springify().dampingRatio(0.5).duration(1800).delay(200)}
exiting={FadeOut.duration(800)}
>
<Typography variant="h4" align='center' color='white' style={styles.title}>
Ton Yearbook contient un récap de toutes tes statistiques liées à ta vie d'étudiant, choisis ce que tu souhaites afficher avant de commencer :
</Typography>
</Reanimated.View>
)}
</View>
);
};
const styles = StyleSheet.create({
title: {
width: 350,
marginBottom: 40
},
listContent: {
gap: 20,
alignItems: "center"
}
})
================================================
FILE: app/(modals)/wrapped/stories/welcome.tsx
================================================
import Stack from '@/ui/components/Stack';
import Typography from '@/ui/components/Typography';
import { Papicons } from '@getpapillon/papicons';
import React from 'react';
import { Dimensions, FlatList, View } from 'react-native';
import Reanimated, { FadeOut, ZoomIn } from 'react-native-reanimated';
export const Welcome = ({ isCurrent }: { isCurrent: boolean, sliderRef: React.RefObject<FlatList> }) => {
return (
<View style={{ width: "100%", height: Dimensions.get('screen').height, justifyContent: 'center', alignItems: 'center' }}>
{isCurrent && (
<>
<Reanimated.Image
entering={ZoomIn.delay(100).springify().duration(800).dampingRatio(0.5)}
exiting={FadeOut.duration(300)}
// eslint-disable-next-line @typescript-eslint/no-require-imports
source={require('@/assets/images/monYearbook.png')}
style={{
height: 180,
width: 280,
overflow: 'visible',
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 2,
},
shadowOpacity: 0.2,
shadowRadius: 10,
}}
resizeMode="contain"
/>
<Reanimated.View
entering={ZoomIn.delay(400).springify().duration(800).dampingRatio(0.5)}
exiting={FadeOut.duration(300)}
style={{
position: "absolute",
bottom: 70
}}
>
<Stack direction='horizontal' hAlign='center' gap={5}>
<Papicons name='ArrowUp' color='white' />
<Typography variant='h4' color='white'>Swipe pour le révéler</Typography>
</Stack>
</Reanimated.View>
</>
)}
</View>
);
};
================================================
FILE: app/(new)/_layout.tsx
================================================
import { Stack } from "expo-router";
import React from "react";
import { useTranslation } from "react-i18next";
import { screenOptions } from "@/utils/theme/ScreenOptions";
export default function Layout() {
const { t } = useTranslation();
const newScreenOptions = React.useMemo(() => ({
...screenOptions,
headerShown: true,
headerLargeTitle: false
}), []);
return (
<Stack screenOptions={newScreenOptions}>
<Stack.Screen
name="event"
options={{
headerTitle: t("Tab_New_Event"),
}}
/>
</Stack>
);
}
================================================
FILE: app/(new)/event.tsx
================================================
import DateTimePicker, { DateTimePickerAndroid } from '@react-native-community/datetimepicker';
import { useTheme } from "@react-navigation/native";
import * as Localization from "expo-localization";
import { useRouter } from "expo-router";
import { CalendarDays, Check, Clock4Icon, MapPinIcon, TypeIcon, User2Icon, X } from "lucide-react-native";
import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import React, { Platform, Pressable, ScrollView, StyleSheet, TextInput } from "react-native";
import { useDatabase } from "@/database/DatabaseProvider";
import Icon from "@/ui/components/Icon";
import Item, { Trailing } from "@/ui/components/Item";
import List from "@/ui/components/List";
import { NativeHeaderPressable, NativeHeaderSide } from "@/ui/components/NativeHeader";
import Stack from "@/ui/components/Stack";
import Typography from "@/ui/components/Typography";
export default function NewEventScreen() {
const router = useRouter();
const { colors } = useTheme();
const { t } = useTranslation();
const database = useDatabase();
const [canSave, setCanSave] = useState(false);
const [inputTitle, setInputTitle] = useState("");
const [inputLocation, setInputLocation] = useState("");
const [inputOrganizer, setInputOrganizer] = useState("");
const [inputStartDate, setInputStartDate] = useState(new Date());
const inHour = new Date();
inHour.setHours(new Date().getHours() + 1, 0, 0, 0); // Set to one hour later
const [inputEndDate, setInputEndDate] = useState(inHour);
const checkCanSave = () => {
const titleValid = inputTitle.length > 0;
const locationValid = inputLocation.length > 0;
const organizerValid = inputOrganizer.length > 0;
const startDateValid = inputStartDate instanceof Date && !isNaN(inputStartDate.getTime());
const endDateValid = inputEndDate instanceof Date && !isNaN(inputEndDate.getTime());
const endDateAfterStartDate = inputEndDate.getTime() > inputStartDate.getTime();
setCanSave(titleValid && locationValid && organizerValid && startDateValid && endDateValid && endDateAfterStartDate);
};
useEffect(() => {
checkCanSave();
}, [inputTitle, inputLocation, inputOrganizer, inputStartDate, inputEndDate]);
// Helper to create an event without linking to subject
async function createEvent(eventData: {
title: string;
start: number;
end: number;
color?: string;
room?: string;
teacher?: string;
status?: string;
canceled?: boolean;
}) {
await database.write(async () => {
await database.get<Event>('events').create((ev: Event) => {
ev.title = eventData.title;
ev.start = eventData.start;
ev.end = eventData.end;
if (eventData.color) {ev.color = eventData.color;}
if (eventData.room) {ev.room = eventData.room;}
if (eventData.teacher) {ev.teacher = eventData.teacher;}
if (eventData.status) {ev.status = eventData.status;}
if (typeof eventData.canceled === 'boolean') {ev.canceled = eventData.canceled;}
});
});
}
const saveEvent = useCallback(async () => {
if (!canSave) {return;}
// Create event data
const eventData = {
title: inputTitle,
start: inputStartDate.getTime(),
end: inputEndDate.getTime(),
color: "#888888", // Default color, can be changed later
room: inputLocation,
teacher: inputOrganizer,
status: null, // Default status
canceled: false // Default not canceled
};
createEvent(eventData)
.then(() => {
router.back();
})
.catch((error) => {
console.error("Error creating event:", error);
alert(t("Error_CreatingEvent"));
});
}, [canSave, inputTitle, inputLocation, inputOrganizer, inputStartDate, inputEndDate, router, t, database]);
return (
<>
<NativeHeaderSide side="Left">
<NativeHeaderPressable onPress={() => { router.back() }}>
<Icon>
<X />
</Icon>
</NativeHeaderPressable>
</NativeHeaderSide>
<NativeHeaderSide
side="Right"
// @ts-expect-error TypeScript doesn't recognize the `key` prop on NativeHeaderSide
key={"saveIcon_save=" + canSave + inputTitle + inputLocation + inputOrganizer + inputStartDate.getTime() + inputEndDate.getTime()}
>
<NativeHeaderPressable disabled={!canSave} onPress={() => { saveEvent() }}>
<Check style={{ opacity: canSave ? 1 : 0.5 }} color={canSave ? colors.primary : colors.text} />
</NativeHeaderPressable>
</NativeHeaderSide>
<ScrollView
contentInsetAdjustmentBehavior="automatic"
contentContainerStyle={styles.containerContent}
style={styles.container}
>
<List>
<Item>
<Icon>
<TypeIcon opacity={inputTitle.length > 0 ? 1 : 0.5} />
</Icon>
<TextInput
placeholder={t("Form_Title")}
value={inputTitle}
onChangeText={setInputTitle}
style={{ flex: 1, paddingVertical: 8, fontSize: 16, fontFamily: "medium" }}
/>
</Item>
<Item>
<Icon>
<MapPinIcon opacity={inputLocation.length > 0 ? 1 : 0.5} />
</Icon>
<TextInput
placeholder={t("Form_Location")}
value={inputLocation}
onChangeText={setInputLocation}
style={{ flex: 1, paddingVertical: 8, fontSize: 16, fontFamily: "medium" }}
/>
</Item>
</List>
<List>
<Item>
<Icon>
<User2Icon opacity={inputOrganizer.length > 0 ? 1 : 0.5} />
</Icon>
<TextInput
placeholder={t("Form_Organizer")}
autoCorrect={false}
autoComplete={"off"}
value={inputOrganizer}
onChangeText={text => {
setInputOrganizer(text);
}}
style={{ flex: 1, paddingVertical: 8, fontSize: 16, fontFamily: "medium" }}
/>
</Item>
</List>
<List>
<Item>
<Icon>
<CalendarDays />
</Icon>
<Typography variant="body1" style={{ flex: 1, paddingVertical: 6 }}>
{t("Form_Start")}
</Typography>
<Trailing>
{Platform.OS === "ios" ? (
<DateTimePicker
value={inputStartDate}
mode="datetime"
accentColor={colors.primary}
locale={Localization.getLocales()[0].languageTag}
display="compact"
onChange={(event, date) => {
if (date) {
// When changing the start date, update the date part of end date to match, but keep the time part
setInputStartDate(date);
setInputEndDate(prevEnd => {
const newEnd = new Date(date);
newEnd.setHours(prevEnd.getHours(), prevEnd.getMinutes(), 0, 0);
// If new end is before or equal to new start, add 1 hour
if (newEnd.getTime() <= date.getTime()) {
newEnd.setTime(date.getTime() + 60 * 60 * 1000);
}
return newEnd;
});
}
}}
/>
) : (
<Stack direction="horizontal" gap={8}>
<Pressable
onPress={() => {
DateTimePickerAndroid.open({
value: inputStartDate,
mode: "date",
design: "material",
locale: Localization.getLocales()[0].languageTag,
onChange: (event, date) => {
if (date) {
setInputStartDate(date);
}
}
});
}}
>
<Typography variant="h5" color="primary">
{inputStartDate.toLocaleDateString(Localization.getLocales()[0].languageTag, {
day: "2-digit",
month: "short",
year: "numeric"
})}{" "}
</Typography>
</Pressable>
<Pressable
onPress={() => {
DateTimePickerAndroid.open({
value: inputStartDate,
mode: "time",
design: "material",
locale: Localization.getLocales()[0].languageTag,
onChange: (event, date) => {
if (date) {
setInputStartDate(date);
}
}
});
}}
>
<Typography variant="h5" color="primary">
{inputStartDate.toLocaleTimeString(Localization.getLocales()[0].languageTag, {
hour: "2-digit",
minute: "2-digit"
})}{" "}
</Typography>
</Pressable>
</Stack>
)}
</Trailing>
</Item>
<Item>
<Icon>
<Clock4Icon />
</Icon>
<Typography variant="body1" style={{ flex: 1, paddingVertical: 6 }}>
{t("Form_End")}
</Typography>
<Trailing>
{Platform.OS === "ios" ? (
<DateTimePicker
value={inputEndDate}
mode="time"
accentColor={colors.primary}
locale={Localization.getLocales()[0].languageTag}
display="compact"
onChange={(event, date) => {
if (date) {
// Only update the time part of end date, keep the date part
setInputEndDate(prevEnd => {
const newEnd = new Date(prevEnd);
newEnd.setHours(date.getHours(), date.getMinutes(), 0, 0);
// If new end is before or equal to start, move to next day
if (newEnd.getTime() <= inputStartDate.getTime()) {
newEnd.setDate(newEnd.getDate() + 1);
}
return newEnd;
});
}
}}
/>
) : (
<Stack direction="horizontal" gap={8}>
<Pressable
onPress={() => {
DateTimePickerAndroid.open({
value: inputEndDate,
mode: "time",
design: "material",
locale: Localization.getLocales()[0].languageTag,
onChange: (event, date) => {
if (date) {
setInputEndDate(date);
}
}
});
}}
>
<Typography variant="h5" color="primary">
{inputEndDate.toLocaleTimeString(Localization.getLocales()[0].languageTag, {
hour: "2-digit",
minute: "2-digit"
})}{" "}
</Typography>
</Pressable>
</Stack>
)}
</Trailing>
</Item>
</List>
</ScrollView>
</>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16
},
containerContent: {
justifyContent: "center",
alignItems: "center",
}
});
================================================
FILE: app/(onboarding)/_layout.tsx
================================================
import React from 'react';
import { Platform, StatusBar, View } from 'react-native';
import { Stack } from "expo-router";
import { screenOptions } from "@/utils/theme/ScreenOptions";
import AndroidHeaderBackground, { AndroidHeaderProps } from '@/components/AndroidHeaderBackground';
import { t } from 'i18next';
export default function OnboardingLayout() {
const newScreenOptions = React.useMemo(() => ({
...screenOptions,
headerShown: true,
...AndroidHeaderProps,
headerTransparent: true,
headerBackButtonDisplayMode: "minimal",
headerLargeTitle: false,
}), []);
return (
<View style={{ flex: 1, backgroundColor: Platform.OS === "ios" ? "black" : undefined }}>
<Stack>
<Stack.Screen
name="welcome"
options={{ ...newScreenOptions, title: "", headerLeft: () => null, headerShown: false, headerBackground: null }}
/>
<Stack.Screen
name="ageSelection"
options={{ ...newScreenOptions, title: t("ONBOARDING_HEADER_ABOUTYOU") }}
/>
<Stack.Screen
name="serviceSelection"
options={{ ...newScreenOptions, title: t("ONBOARDING_HEADER_SCHOOLSERVICE") }}
/>
<Stack.Screen
name="restaurants"
options={{ headerShown: false, title: t("ONBOARDING_RESTAURANTS") }}
/>
<Stack.Screen
name="services/pronote"
options={{ headerShown: false, title: "", presentation: "modal" }}
/>
<Stack.Screen
name="services/ed"
options={{ headerShown: false, title: "", presentation: "modal" }}
/>
<Stack.Screen
name="services/skolengo"
options={{ headerShown: false, title: "", presentation: "modal" }}
/>
<Stack.Screen
name="services/lannion"
options={{ headerShown: false, title: "", presentation: "modal" }}
/>
<Stack.Screen
name="services/multi"
options={{ headerShown: false, title: "", presentation: "modal" }}
/>
</Stack>
</View>
);
}
================================================
FILE: app/(onboarding)/ageSelection.tsx
================================================
import { useHeaderHeight } from "@react-navigation/elements";
import { useTheme } from "@react-navigation/native";
import { useNavigation } from "expo-router";
import React, { useState } from "react";
import { FlatList, StatusBar, View } from "react-native";
import { useTranslation } from "react-i18next";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Stack from "@/ui/components/Stack";
import Button from "@/ui/new/Button";
import Divider from "@/ui/new/Divider";
import Typography from "@/ui/new/Typography";
import HighSchoolIllustration from "./components/ageSelection/illustrations/highSchool";
import MiddleSchoolIllustration from "./components/ageSelection/illustrations/middleSchool";
import ParentsIllustration from "./components/ageSelection/illustrations/parents";
import SupSchoolIllustration from "./components/ageSelection/illustrations/supSchool";
import TeacherIllustration from "./components/ageSelection/illustrations/teacher";
import OnboardingSelector from "./components/OnboardingSelector";
const LEVELS = [
{
key: "middle-school",
labelKey: "ONBOARDING_LEVEL_MIDDLE_SCHOOL",
color: "#008CFF",
icon: MiddleSchoolIllustration,
gitextract_hxvgag7x/
├── .github/
│ ├── CONTRIBUTING.md
│ ├── FUNDING.yml
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug.yml
│ │ ├── config.yml
│ │ └── feature.yml
│ ├── PULL_REQUEST_TEMPLATE.md
│ ├── bot/
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── issue/
│ │ │ ├── index.ts
│ │ │ ├── labeler.ts
│ │ │ └── message.ts
│ │ └── tsconfig.json
│ └── workflows/
│ ├── build-android.yml
│ ├── merge.yml
│ └── release.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── CODEOWNERS
├── LICENSE
├── README.md
├── app/
│ ├── (features)/
│ │ ├── (cards)/
│ │ │ ├── cards.tsx
│ │ │ ├── qrcode.tsx
│ │ │ └── specific.tsx
│ │ ├── (news)/
│ │ │ └── specific.tsx
│ │ ├── attendance.tsx
│ │ └── soon.tsx
│ ├── (modals)/
│ │ ├── address.tsx
│ │ ├── course.tsx
│ │ ├── grade.tsx
│ │ ├── news.tsx
│ │ ├── notifications.tsx
│ │ ├── profile.tsx
│ │ ├── task.tsx
│ │ ├── wallpaper.tsx
│ │ └── wrapped/
│ │ ├── _layout.tsx
│ │ ├── index.tsx
│ │ └── stories/
│ │ ├── consent.tsx
│ │ └── welcome.tsx
│ ├── (new)/
│ │ ├── _layout.tsx
│ │ └── event.tsx
│ ├── (onboarding)/
│ │ ├── _layout.tsx
│ │ ├── ageSelection.tsx
│ │ ├── components/
│ │ │ ├── LoginView.tsx
│ │ │ ├── OnboardingSelector.tsx
│ │ │ ├── OnboardingWebView.tsx
│ │ │ └── ageSelection/
│ │ │ └── illustrations/
│ │ │ ├── highSchool.tsx
│ │ │ ├── middleSchool.tsx
│ │ │ ├── parents.tsx
│ │ │ ├── supSchool.tsx
│ │ │ └── teacher.tsx
│ │ ├── restaurants/
│ │ │ ├── _layout.tsx
│ │ │ ├── alise.tsx
│ │ │ ├── ard.tsx
│ │ │ ├── izly.tsx
│ │ │ ├── method.tsx
│ │ │ ├── turboself.tsx
│ │ │ └── turboselfHost.tsx
│ │ ├── serviceSelection.tsx
│ │ ├── services/
│ │ │ ├── appscho/
│ │ │ │ ├── _layout.tsx
│ │ │ │ ├── credentials.tsx
│ │ │ │ ├── list.tsx
│ │ │ │ └── webview.tsx
│ │ │ ├── ed/
│ │ │ │ ├── _layout.tsx
│ │ │ │ └── credentials.tsx
│ │ │ ├── lannion/
│ │ │ │ ├── _layout.tsx
│ │ │ │ └── credentials.tsx
│ │ │ ├── multi/
│ │ │ │ ├── _layout.tsx
│ │ │ │ └── credentials.tsx
│ │ │ ├── pronote/
│ │ │ │ ├── 2fa.tsx
│ │ │ │ ├── _layout.tsx
│ │ │ │ ├── browser.tsx
│ │ │ │ ├── locate.tsx
│ │ │ │ ├── qrcode.tsx
│ │ │ │ ├── select.tsx
│ │ │ │ └── url.tsx
│ │ │ └── skolengo/
│ │ │ ├── _layout.tsx
│ │ │ ├── locate.tsx
│ │ │ └── webview.tsx
│ │ ├── utils/
│ │ │ ├── constants.tsx
│ │ │ └── fetchSchools.ts
│ │ └── welcome.tsx
│ ├── (settings)/
│ │ ├── _layout.tsx
│ │ ├── about.tsx
│ │ ├── accounts.tsx
│ │ ├── cards.tsx
│ │ ├── contributors.tsx
│ │ ├── edit_subject.tsx
│ │ ├── language.tsx
│ │ ├── magic.tsx
│ │ ├── personalization.tsx
│ │ ├── services.tsx
│ │ ├── settings.tsx
│ │ ├── subject_personalization.tsx
│ │ ├── tabs.tsx
│ │ └── transport.tsx
│ ├── (tabs)/
│ │ ├── _layout.tsx
│ │ ├── calendar/
│ │ │ ├── _layout.tsx
│ │ │ ├── components/
│ │ │ │ ├── CalendarDay.tsx
│ │ │ │ ├── CalendarHeader.tsx
│ │ │ │ └── EmptyCalendar.tsx
│ │ │ ├── event/
│ │ │ │ └── [id].tsx
│ │ │ ├── hooks/
│ │ │ │ ├── useCalendarState.ts
│ │ │ │ └── useTimetableData.ts
│ │ │ ├── icals.tsx
│ │ │ └── index.tsx
│ │ ├── grades/
│ │ │ ├── _layout.tsx
│ │ │ ├── atoms/
│ │ │ │ ├── Averages.tsx
│ │ │ │ ├── FeaturesMap.tsx
│ │ │ │ └── Subject.tsx
│ │ │ ├── features/
│ │ │ │ └── ScodocUES.tsx
│ │ │ ├── hooks/
│ │ │ │ └── useGradeInfluence.ts
│ │ │ ├── index.tsx
│ │ │ ├── modals/
│ │ │ │ ├── AboutAverages.tsx
│ │ │ │ └── SubjectInfo.tsx
│ │ │ └── utils/
│ │ │ └── graph.ts
│ │ ├── index/
│ │ │ ├── _layout.tsx
│ │ │ ├── atoms/
│ │ │ │ ├── HomeHeader.tsx
│ │ │ │ ├── HomeTopBar.tsx
│ │ │ │ ├── UserProfile.tsx
│ │ │ │ ├── Wallpaper.tsx
│ │ │ │ └── WrappedBanner.tsx
│ │ │ ├── components/
│ │ │ │ ├── HomeHeaderButton.ios.tsx
│ │ │ │ ├── HomeHeaderButton.tsx
│ │ │ │ ├── HomeTopBarButton.ios.tsx
│ │ │ │ ├── HomeTopBarButton.tsx
│ │ │ │ └── HomeWidget.tsx
│ │ │ ├── hooks/
│ │ │ │ ├── useHomeData.ts
│ │ │ │ ├── useHomeHeaderData.ts
│ │ │ │ ├── useTimetableWidgetData.ts
│ │ │ │ └── useUserProfileData.ts
│ │ │ ├── index.old.tsx
│ │ │ ├── index.tsx
│ │ │ └── widgets/
│ │ │ ├── Grades.tsx
│ │ │ └── timetable.tsx
│ │ ├── news/
│ │ │ ├── _layout.tsx
│ │ │ └── index.tsx
│ │ └── tasks/
│ │ ├── _layout.tsx
│ │ ├── atoms/
│ │ │ ├── DateHeader.tsx
│ │ │ ├── EmptyState.tsx
│ │ │ └── TasksSummary.tsx
│ │ ├── components/
│ │ │ ├── TaskItem.tsx
│ │ │ ├── TasksHeader.tsx
│ │ │ ├── TasksList.tsx
│ │ │ └── WeekPicker.tsx
│ │ ├── hooks/
│ │ │ ├── useHomeworkData.ts
│ │ │ ├── useMagicPrediction.ts
│ │ │ ├── useTaskFilters.ts
│ │ │ └── useWeekSelection.ts
│ │ └── index.tsx
│ ├── _layout.tsx
│ ├── alert.tsx
│ ├── changelog.tsx
│ ├── consent.tsx
│ ├── demo.tsx
│ └── devmode.tsx
├── app.config.ts
├── assets/
│ ├── app.icon/
│ │ └── icon.json
│ └── lotties/
│ ├── alise.json
│ ├── ard.json
│ ├── connexion.json
│ ├── izly.json
│ ├── link.json
│ ├── location.json
│ ├── onboarding.json
│ ├── qr-code.json
│ ├── school-services.json
│ ├── search.json
│ ├── self.json
│ ├── turboself.json
│ └── uni-services.json
├── babel.config.js
├── components/
│ ├── ActivityIndicator.tsx
│ ├── AndroidHeaderBackground.tsx
│ ├── AppColorsSelector.tsx
│ ├── AppProviders.tsx
│ ├── DevModeNotice.tsx
│ ├── FakeSplash.tsx
│ ├── Log/
│ │ └── LogIcon.tsx
│ ├── ModalOverhead.tsx
│ ├── RootNavigator.tsx
│ ├── SettingsHeader.tsx
│ ├── Transit.tsx
│ ├── UnderConstructionNotice.tsx
│ ├── onboarding/
│ │ ├── OnboardingBackButton.tsx
│ │ ├── OnboardingInput.tsx
│ │ ├── OnboardingScrollingFlatList.tsx
│ │ └── OnboardingWebview.tsx
│ └── router/
│ └── BottomTabs.tsx
├── constants/
│ ├── AvailableTransportServices.ts
│ ├── LayoutScreenOptions.ts
│ └── UnicodeEmojis.ts
├── crowdin.yml
├── database/
│ ├── DatabaseProvider.tsx
│ ├── index.ts
│ ├── mappers/
│ │ ├── attendance.ts
│ │ ├── balances.ts
│ │ ├── canteen.ts
│ │ ├── chats.ts
│ │ ├── course.ts
│ │ ├── grade.ts
│ │ ├── kids.ts
│ │ └── subject.ts
│ ├── models/
│ │ ├── Attendance.ts
│ │ ├── Balance.ts
│ │ ├── CanteenHistory.ts
│ │ ├── CanteenMenu.ts
│ │ ├── Chat.ts
│ │ ├── Event.ts
│ │ ├── Grades.ts
│ │ ├── Homework.ts
│ │ ├── Ical.ts
│ │ ├── Kid.ts
│ │ ├── News.ts
│ │ ├── Subject.ts
│ │ └── Timetable.ts
│ ├── schema.ts
│ ├── useAttendance.ts
│ ├── useBalance.ts
│ ├── useCanteen.ts
│ ├── useChat.ts
│ ├── useEvents.ts
│ ├── useEventsById.ts
│ ├── useGrades.ts
│ ├── useHomework.ts
│ ├── useIcals.ts
│ ├── useKids.ts
│ ├── useNews.ts
│ ├── usePeriodsCache.tsx
│ ├── useSubject.ts
│ ├── useTimetable.ts
│ └── utils/
│ ├── initialization.ts
│ └── safeTransaction.ts
├── eslint.config.mjs
├── hooks/
│ └── useAppInitialization.ts
├── ios/
│ └── Papillon/
│ ├── AppDelegate.swift
│ ├── Images.xcassets/
│ │ ├── AppIcon.appiconset/
│ │ │ └── Contents.json
│ │ ├── Contents.json
│ │ ├── SplashScreenBackground.colorset/
│ │ │ └── Contents.json
│ │ └── SplashScreenLegacy.imageset/
│ │ └── Contents.json
│ ├── Info.plist
│ ├── Papillon-Bridging-Header.h
│ ├── Papillon.entitlements
│ ├── PrivacyInfo.xcprivacy
│ ├── SplashScreen.storyboard
│ └── Supporting/
│ └── Expo.plist
├── locales/
│ ├── af.json
│ ├── ar.json
│ ├── bg.json
│ ├── bn.json
│ ├── br.json
│ ├── cs.json
│ ├── da.json
│ ├── de.json
│ ├── el.json
│ ├── en.json
│ ├── es.json
│ ├── et.json
│ ├── fa.json
│ ├── fi.json
│ ├── fr.json
│ ├── he.json
│ ├── hi.json
│ ├── hr.json
│ ├── hu.json
│ ├── id.json
│ ├── it.json
│ ├── ja.json
│ ├── ko.json
│ ├── ms.json
│ ├── nl.json
│ ├── no.json
│ ├── pl.json
│ ├── pt.json
│ ├── ro.json
│ ├── ru.json
│ ├── sk.json
│ ├── sq.json
│ ├── sv.json
│ ├── sw.json
│ ├── th.json
│ ├── tr.json
│ ├── uk.json
│ ├── ur.json
│ └── vi.json
├── metro.config.js
├── package.json
├── patches/
│ ├── countly-sdk-react-native-bridge+25.4.0.patch
│ └── react-native-fast-tflite+1.6.1.patch
├── scripts/
│ └── generateEmojiList.sh
├── services/
│ ├── alise/
│ │ ├── balance.ts
│ │ ├── history.ts
│ │ ├── index.ts
│ │ └── refresh.ts
│ ├── appscho/
│ │ ├── index.ts
│ │ ├── news.ts
│ │ ├── refresh.ts
│ │ └── timetable.ts
│ ├── ard/
│ │ ├── balance.ts
│ │ ├── history.ts
│ │ ├── index.ts
│ │ └── refresh.ts
│ ├── ecoledirecte/
│ │ ├── attendance.ts
│ │ ├── balance.ts
│ │ ├── chat.ts
│ │ ├── grades.ts
│ │ ├── homework.ts
│ │ ├── index.ts
│ │ ├── news.ts
│ │ ├── qrcode.ts
│ │ ├── refresh.ts
│ │ └── timetable.ts
│ ├── errors/
│ │ └── AuthenticationError.ts
│ ├── izly/
│ │ ├── balances.ts
│ │ ├── history.ts
│ │ ├── index.ts
│ │ ├── qrcode.ts
│ │ └── refresh.ts
│ ├── lannion/
│ │ ├── attendance.ts
│ │ ├── grades.ts
│ │ ├── index.ts
│ │ └── module/
│ │ ├── api.ts
│ │ ├── client.ts
│ │ ├── index.ts
│ │ └── types.ts
│ ├── local/
│ │ ├── .ical.ts.swp
│ │ ├── event-converter.ts
│ │ ├── event-filter.ts
│ │ ├── ical-database.ts
│ │ ├── ical-utils.ts
│ │ ├── ical.ts
│ │ └── parsers/
│ │ ├── ade-parser.ts
│ │ ├── hyperplanning-parser.ts
│ │ ├── ical-event-parser.ts
│ │ └── schools/
│ │ └── univrennes1_parser.ts
│ ├── multi/
│ │ ├── index.ts
│ │ ├── news.ts
│ │ ├── refresh.ts
│ │ └── timetable.ts
│ ├── pronote/
│ │ ├── attendance.ts
│ │ ├── canteen.ts
│ │ ├── chat.ts
│ │ ├── grades.ts
│ │ ├── homework.ts
│ │ ├── index.ts
│ │ ├── news.ts
│ │ ├── refresh.ts
│ │ └── timetable.ts
│ ├── shared/
│ │ ├── attachment.ts
│ │ ├── attendance.ts
│ │ ├── balance.ts
│ │ ├── canteen.ts
│ │ ├── chat.ts
│ │ ├── grade.ts
│ │ ├── homework.ts
│ │ ├── index.ts
│ │ ├── kid.ts
│ │ ├── news.ts
│ │ ├── timetable.ts
│ │ └── types.ts
│ ├── skolengo/
│ │ ├── attendance.ts
│ │ ├── chat.ts
│ │ ├── grades.ts
│ │ ├── homework.ts
│ │ ├── index.ts
│ │ ├── kid.ts
│ │ ├── news.ts
│ │ ├── refresh.ts
│ │ └── timetable.ts
│ ├── transit/
│ │ ├── fetcher/
│ │ │ ├── Fetcher.ts
│ │ │ └── endpoints.ts
│ │ ├── index.ts
│ │ └── models/
│ │ ├── Alerts.ts
│ │ ├── ArrivalSchedule.ts
│ │ ├── DepartureLegs.ts
│ │ ├── Itineraries.ts
│ │ ├── ItinerariesPlanDetails.ts
│ │ ├── Location.ts
│ │ ├── MatchedSubstring.ts
│ │ ├── Period.ts
│ │ ├── Place.ts
│ │ ├── PlaceDetails.ts
│ │ ├── PlaceSuggestion.ts
│ │ ├── PlanDetails.ts
│ │ ├── PlanResult.ts
│ │ ├── Route.ts
│ │ ├── RouteLegs.ts
│ │ ├── Station.ts
│ │ ├── Stop.ts
│ │ ├── StopSchedule.ts
│ │ ├── Suggestions.ts
│ │ ├── TransitDepartures.ts
│ │ └── Vehicle.ts
│ └── turboself/
│ ├── balance.ts
│ ├── booking.ts
│ ├── history.ts
│ ├── index.ts
│ ├── qrcode.ts
│ └── refresh.ts
├── stores/
│ ├── account/
│ │ ├── index.ts
│ │ └── types.ts
│ ├── flags/
│ │ └── index.ts
│ ├── global/
│ │ ├── index.ts
│ │ └── serializer.ts
│ ├── logs/
│ │ ├── index.ts
│ │ └── types.ts
│ ├── magic/
│ │ ├── index.ts
│ │ └── types.ts
│ └── settings/
│ ├── index.ts
│ └── types.ts
├── stubs/
│ └── appscho/
│ ├── index.d.ts
│ ├── index.js
│ └── package.json
├── tsconfig.json
├── ui/
│ ├── components/
│ │ ├── ActionMenu.tsx
│ │ ├── ActivityIndicator.tsx
│ │ ├── AlertProvider.tsx
│ │ ├── AnimatedNumber.tsx
│ │ ├── AnimatedPressable.tsx
│ │ ├── Avatar.tsx
│ │ ├── Button.tsx
│ │ ├── Calendar.tsx
│ │ ├── ChipButton.tsx
│ │ ├── CircularProgress.tsx
│ │ ├── CompactGrade.tsx
│ │ ├── CompactTask.tsx
│ │ ├── ContainedNumber.tsx
│ │ ├── Course.tsx
│ │ ├── Dynamic.tsx
│ │ ├── EmptyItem.tsx
│ │ ├── ErrorBoundary.tsx
│ │ ├── Grade.tsx
│ │ ├── Icon.tsx
│ │ ├── Item.tsx
│ │ ├── List.tsx
│ │ ├── NativeHeader.tsx
│ │ ├── Pattern/
│ │ │ ├── CrossPattern.tsx
│ │ │ └── Pattern.tsx
│ │ ├── Search.tsx
│ │ ├── SectionHeader.tsx
│ │ ├── SkeletonView.tsx
│ │ ├── Stack.tsx
│ │ ├── Subject.tsx
│ │ ├── TabFlatList.tsx
│ │ ├── TabHeader.tsx
│ │ ├── TabHeaderTitle.tsx
│ │ ├── TableFlatList.tsx
│ │ ├── Task.tsx
│ │ ├── Typography.tsx
│ │ └── ViewContainer.tsx
│ ├── hooks/
│ │ └── useKeyboardHeight.ts
│ ├── native/
│ │ └── NativeSwitch.tsx
│ ├── new/
│ │ ├── Button.tsx
│ │ ├── Divider.tsx
│ │ ├── List.tsx
│ │ ├── ListTouchableContext.ts
│ │ ├── RippleEffect.tsx
│ │ ├── TextInput.tsx
│ │ ├── Typography.tsx
│ │ └── symbols/
│ │ └── PapillonLogo.tsx
│ ├── package.json
│ └── utils/
│ ├── Animation.ts
│ ├── Corners.ts
│ ├── Duration.ts
│ ├── IsLiquidGlass.ts
│ └── Transition.ts
└── utils/
├── adjustColor.ts
├── attachments/
│ └── helper.ts
├── chats/
│ ├── colors.ts
│ └── initials.ts
├── colorCheck.ts
├── colors.ts
├── endpoints.ts
├── format/
│ ├── formatSchoolName.ts
│ └── html.ts
├── generateId.ts
├── github/
│ └── contributors.ts
├── grades/
│ ├── algorithms/
│ │ ├── helpers.ts
│ │ ├── median.ts
│ │ ├── subject.ts
│ │ ├── time.ts
│ │ └── weighted.ts
│ └── helper/
│ └── period.ts
├── i18n.ts
├── logger/
│ ├── consent.ts
│ └── logger.ts
├── magic/
│ ├── ModelManager.ts
│ ├── prediction.ts
│ ├── regex/
│ │ └── homeworks.json
│ └── updater/
│ ├── extract.ts
│ ├── fileUtils.ts
│ ├── index.ts
│ ├── integrity.ts
│ ├── manifest.ts
│ ├── network.ts
│ ├── semver.ts
│ └── types.ts
├── native/
│ ├── AnimatedNavigator.ts
│ ├── georeverse.ts
│ └── position.ts
├── news/
│ ├── cleanUpHTMLNews.ts
│ └── getAttachmentIcon.ts
├── notification/
│ └── reminder/
│ └── helper.ts
├── pronote/
│ ├── fetcher.ts
│ └── name.ts
├── restaurant/
│ └── detect-price.ts
├── services/
│ ├── helper.ts
│ └── periods.ts
├── subjects/
│ ├── colors.ts
│ ├── emoji.ts
│ ├── lesson_formats.json
│ ├── name.ts
│ └── utils.ts
├── theme/
│ ├── AndroidBackButton.tsx
│ ├── ScreenOptions.tsx
│ └── Theme.ts
├── transport.ts
└── uuid/
└── uuid.ts
SYMBOL INDEX (980 symbols across 350 files)
FILE: .github/bot/src/issue/index.ts
function run (line 6) | async function run() {
FILE: .github/bot/src/issue/labeler.ts
function getLabelsFromTitle (line 1) | function getLabelsFromTitle(title: string): string[] {
FILE: .github/bot/src/issue/message.ts
function postWelcomeMessage (line 3) | async function postWelcomeMessage(
FILE: app/(features)/(cards)/cards.tsx
function QRCodeAndCardsPage (line 25) | function QRCodeAndCardsPage() {
function Card (line 129) | function Card({
FILE: app/(features)/(cards)/qrcode.tsx
function QRCodePage (line 23) | function QRCodePage() {
FILE: app/(features)/(cards)/specific.tsx
function QRCodeAndCardsPage (line 41) | function QRCodeAndCardsPage() {
FILE: app/(features)/(news)/specific.tsx
function NewsPage (line 28) | function NewsPage() {
FILE: app/(features)/attendance.tsx
function AttendanceView (line 43) | function AttendanceView() {
FILE: app/(features)/soon.tsx
function Soon (line 8) | function Soon() {
FILE: app/(modals)/address.tsx
type AddressModalProps (line 32) | interface AddressModalProps {
type AddressItemProps (line 38) | interface AddressItemProps {
FILE: app/(modals)/course.tsx
type SubjectInfo (line 20) | interface SubjectInfo {
type GradesModalProps (line 27) | interface GradesModalProps {
function CourseModal (line 32) | function CourseModal() {
FILE: app/(modals)/grade.tsx
type SubjectInfo (line 21) | interface SubjectInfo {
type GradesModalProps (line 28) | interface GradesModalProps {
type GradeBadgeProps (line 35) | interface GradeBadgeProps {
function GradesModal (line 58) | function GradesModal() {
FILE: app/(modals)/notifications.tsx
function NotificationsModal (line 8) | function NotificationsModal() {
FILE: app/(modals)/profile.tsx
function CustomProfileScreen (line 28) | function CustomProfileScreen() {
FILE: app/(modals)/wallpaper.tsx
constant COLLECTIONS_SOURCE (line 21) | const COLLECTIONS_SOURCE = "https://raw.githubusercontent.com/PapillonAp...
type Collection (line 23) | interface Collection {
FILE: app/(modals)/wrapped/_layout.tsx
function Layout (line 6) | function Layout() {
FILE: app/(modals)/wrapped/stories/consent.tsx
type ConsentItem (line 13) | type ConsentItem = {
constant INITIAL_ITEMS (line 63) | const INITIAL_ITEMS: ConsentItem[] = [
constant ICON_COLOR (line 70) | const ICON_COLOR = '#31424A';
constant TRACK_COLOR (line 71) | const TRACK_COLOR = { true: "#C50000" };
FILE: app/(new)/_layout.tsx
function Layout (line 7) | function Layout() {
FILE: app/(new)/event.tsx
function NewEventScreen (line 18) | function NewEventScreen() {
FILE: app/(onboarding)/_layout.tsx
function OnboardingLayout (line 9) | function OnboardingLayout() {
FILE: app/(onboarding)/ageSelection.tsx
constant LEVELS (line 21) | const LEVELS = [
function AgeSelection (line 62) | function AgeSelection() {
FILE: app/(onboarding)/components/LoginView.tsx
type LoginViewProps (line 13) | interface LoginViewProps {
function LoginView (line 34) | function LoginView({
FILE: app/(onboarding)/components/OnboardingSelector.tsx
function OnboardingSelector (line 19) | function OnboardingSelector({ item, selected, setSelected }) {
FILE: app/(onboarding)/components/OnboardingWebView.tsx
function OnboardingWebView (line 13) | function OnboardingWebView({ webViewRef, ...props }: React.ComponentProp...
FILE: app/(onboarding)/restaurants/_layout.tsx
function OnboardingLayout (line 8) | function OnboardingLayout() {
FILE: app/(onboarding)/restaurants/alise.tsx
constant ANIMATION_DURATION (line 23) | const ANIMATION_DURATION = 100;
function AliseLoginWithCredentials (line 25) | function AliseLoginWithCredentials() {
FILE: app/(onboarding)/restaurants/ard.tsx
constant ANIMATION_DURATION (line 29) | const ANIMATION_DURATION = 100;
function TurboSelfLoginWithCredentials (line 31) | function TurboSelfLoginWithCredentials() {
FILE: app/(onboarding)/restaurants/izly.tsx
constant ANIMATION_DURATION (line 36) | const ANIMATION_DURATION = 100;
function TurboSelfLoginWithCredentials (line 38) | function TurboSelfLoginWithCredentials() {
FILE: app/(onboarding)/restaurants/method.tsx
function ServiceSelection (line 23) | function ServiceSelection() {
FILE: app/(onboarding)/restaurants/turboself.tsx
constant ANIMATION_DURATION (line 27) | const ANIMATION_DURATION = 100;
function TurboSelfLoginWithCredentials (line 29) | function TurboSelfLoginWithCredentials() {
FILE: app/(onboarding)/restaurants/turboselfHost.tsx
constant INITIAL_HEIGHT (line 30) | const INITIAL_HEIGHT = 570;
constant COLLAPSED_HEIGHT (line 31) | const COLLAPSED_HEIGHT = 270;
function TurboSelfSelectHost (line 76) | function TurboSelfSelectHost() {
FILE: app/(onboarding)/serviceSelection.tsx
function ServiceSelection (line 22) | function ServiceSelection() {
FILE: app/(onboarding)/services/appscho/_layout.tsx
function OnboardingLayout (line 8) | function OnboardingLayout() {
FILE: app/(onboarding)/services/appscho/credentials.tsx
constant ANIMATION_DURATION (line 29) | const ANIMATION_DURATION = 100;
function AppSchoCredentials (line 31) | function AppSchoCredentials() {
FILE: app/(onboarding)/services/appscho/list.tsx
constant UNIVERSITY_LOGOS (line 17) | const UNIVERSITY_LOGOS: { [key: string]: any } = {
function AppschoInstancesList (line 64) | function AppschoInstancesList() {
FILE: app/(onboarding)/services/appscho/webview.tsx
function AppschoWebView (line 11) | function AppschoWebView() {
FILE: app/(onboarding)/services/ed/_layout.tsx
function OnboardingLayout (line 9) | function OnboardingLayout() {
FILE: app/(onboarding)/services/ed/credentials.tsx
constant ANIMATION_DURATION (line 39) | const ANIMATION_DURATION = 170;
function EDLoginWithCredentials (line 42) | function EDLoginWithCredentials() {
FILE: app/(onboarding)/services/lannion/_layout.tsx
function OnboardingLayout (line 9) | function OnboardingLayout() {
FILE: app/(onboarding)/services/lannion/credentials.tsx
constant ANIMATION_DURATION (line 32) | const ANIMATION_DURATION = 170;
function LannionCredentials (line 38) | function LannionCredentials() {
FILE: app/(onboarding)/services/multi/_layout.tsx
function OnboardingLayout (line 10) | function OnboardingLayout() {
FILE: app/(onboarding)/services/multi/credentials.tsx
constant ANIMATION_DURATION (line 32) | const ANIMATION_DURATION = 170;
function MultiLoginWithCredentials (line 34) | function MultiLoginWithCredentials() {
FILE: app/(onboarding)/services/pronote/2fa.tsx
function Pronote2FAModal (line 21) | function Pronote2FAModal({ doubleAuthSession, doubleAuthError, setChalle...
FILE: app/(onboarding)/services/pronote/_layout.tsx
function OnboardingLayout (line 9) | function OnboardingLayout() {
FILE: app/(onboarding)/services/pronote/browser.tsx
function PronoteENTLogin (line 26) | function PronoteENTLogin() {
FILE: app/(onboarding)/services/pronote/locate.tsx
type School (line 27) | interface School {
function PronoteLoginMethod (line 68) | function PronoteLoginMethod() {
FILE: app/(onboarding)/services/pronote/qrcode.tsx
function PronoteLoginWithQR (line 24) | function PronoteLoginWithQR() {
FILE: app/(onboarding)/services/pronote/select.tsx
type School (line 19) | interface School {
function PronoteLoginSelectEtab (line 58) | function PronoteLoginSelectEtab() {
FILE: app/(onboarding)/services/pronote/url.tsx
function PronoteLoginURL (line 44) | function PronoteLoginURL() {
FILE: app/(onboarding)/services/skolengo/_layout.tsx
function OnboardingLayout (line 9) | function OnboardingLayout() {
FILE: app/(onboarding)/services/skolengo/locate.tsx
type School (line 20) | interface School {
function PronoteLoginMethod (line 61) | function PronoteLoginMethod() {
FILE: app/(onboarding)/services/skolengo/webview.tsx
function WebViewScreen (line 15) | function WebViewScreen() {
FILE: app/(onboarding)/utils/constants.tsx
type SupportedService (line 11) | interface SupportedService {
function GetSupportedServices (line 25) | function GetSupportedServices(redirect: (path: { pathname: string, optio...
type SupportedUniversity (line 111) | interface SupportedUniversity {
function GetSupportedUniversities (line 120) | function GetSupportedUniversities(redirect: (path: { pathname: string, o...
type LoginMethod (line 207) | interface LoginMethod {
function GetLoginMethods (line 215) | function GetLoginMethods(redirect: (path: { pathname: RelativePathString...
type SupportedRestaurant (line 258) | interface SupportedRestaurant {
function GetSupportedRestaurants (line 267) | function GetSupportedRestaurants(redirect: (path: { pathname: string }) ...
FILE: app/(onboarding)/utils/fetchSchools.ts
type School (line 10) | interface School {
function fetchSchools (line 17) | async function fetchSchools(service: Services, alert: ReturnType<typeof ...
type SchoolItem (line 94) | type SchoolItem = {
FILE: app/(onboarding)/welcome.tsx
function Welcome (line 18) | function Welcome() {
FILE: app/(settings)/_layout.tsx
function Layout (line 10) | function Layout() {
FILE: app/(settings)/about.tsx
function SettingsAbout (line 106) | function SettingsAbout() {
FILE: app/(settings)/accounts.tsx
function AccountsView (line 20) | function AccountsView() {
FILE: app/(settings)/cards.tsx
function CardView (line 22) | function CardView() {
FILE: app/(settings)/contributors.tsx
constant TEAM_LOGINS (line 13) | const TEAM_LOGINS = [
function SettingsContributors (line 22) | function SettingsContributors() {
FILE: app/(settings)/edit_subject.tsx
function EmojiPicker (line 75) | function EmojiPicker({
function EditSubject (line 265) | function EditSubject() {
FILE: app/(settings)/magic.tsx
function getMagicURL (line 18) | function getMagicURL(): string {
function SettingsMagic (line 22) | function SettingsMagic() {
FILE: app/(settings)/services.tsx
function SettingsServices (line 12) | function SettingsServices() {
FILE: app/(settings)/settings.tsx
function SettingsIndex (line 32) | function SettingsIndex() {
FILE: app/(settings)/subject_personalization.tsx
function SubjectPersonalization (line 19) | function SubjectPersonalization() {
FILE: app/(settings)/transport.tsx
function TransportView (line 18) | function TransportView() {
FILE: app/(tabs)/_layout.tsx
constant IS_IOS_WITH_PADDING (line 8) | const IS_IOS_WITH_PADDING = runsIOS26;
constant IS_ANDROID (line 9) | const IS_ANDROID = Platform.OS === 'android';
constant TAB_LABEL_STYLE (line 11) | const TAB_LABEL_STYLE = {
function TabLayout (line 16) | function TabLayout() {
FILE: app/(tabs)/calendar/_layout.tsx
function Layout (line 8) | function Layout() {
FILE: app/(tabs)/calendar/components/CalendarDay.tsx
type CalendarDayProps (line 16) | interface CalendarDayProps {
function shallowEqual (line 35) | function shallowEqual(objA: any, objB: any) {
function getStatusText (line 224) | function getStatusText(status?: CourseStatus): string {
FILE: app/(tabs)/calendar/components/CalendarHeader.tsx
type CalendarHeaderProps (line 12) | interface CalendarHeaderProps {
FILE: app/(tabs)/calendar/event/[id].tsx
function TabOneScreen (line 15) | function TabOneScreen() {
FILE: app/(tabs)/calendar/hooks/useCalendarState.ts
constant INITIAL_INDEX (line 6) | const INITIAL_INDEX = 10000;
function useCalendarState (line 8) | function useCalendarState() {
FILE: app/(tabs)/calendar/hooks/useTimetableData.ts
function useTimetableData (line 8) | function useTimetableData(weekNumber: number, currentDate: Date = new Da...
FILE: app/(tabs)/calendar/icals.tsx
function TabOneScreen (line 20) | function TabOneScreen() {
FILE: app/(tabs)/calendar/index.tsx
function TabOneScreen (line 15) | function TabOneScreen() {
function getStatusText (line 117) | function getStatusText(status?: CourseStatus): string {
FILE: app/(tabs)/grades/_layout.tsx
function Layout (line 9) | function Layout() {
FILE: app/(tabs)/grades/features/ScodocUES.tsx
type AveragedElement (line 17) | interface AveragedElement {
type UEMoyenne (line 23) | interface UEMoyenne {
type ECTSInfo (line 33) | interface ECTSInfo {
type UE (line 38) | interface UE {
type UEMap (line 54) | type UEMap = Record<string, UE>;
FILE: app/(tabs)/grades/modals/AboutAverages.tsx
function AboutAverages (line 7) | function AboutAverages() {
FILE: app/(tabs)/grades/utils/graph.ts
type GraphPoint (line 2) | interface GraphPoint {
type AverageHistoryItem (line 9) | interface AverageHistoryItem {
FILE: app/(tabs)/index/_layout.tsx
function Layout (line 7) | function Layout() {
FILE: app/(tabs)/index/atoms/WrappedBanner.tsx
constant BANNER_HEIGHT (line 20) | const BANNER_HEIGHT = 130;
FILE: app/(tabs)/index/components/HomeHeaderButton.ios.tsx
type HomeHeaderButtonItem (line 9) | interface HomeHeaderButtonItem {
type HomeHeaderButtonProps (line 17) | interface HomeHeaderButtonProps {
FILE: app/(tabs)/index/components/HomeHeaderButton.tsx
type HomeHeaderButtonItem (line 11) | interface HomeHeaderButtonItem {
type HomeHeaderButtonProps (line 19) | interface HomeHeaderButtonProps {
FILE: app/(tabs)/index/components/HomeTopBarButton.ios.tsx
type HomeTopBarButtonProps (line 7) | interface HomeTopBarButtonProps {
FILE: app/(tabs)/index/components/HomeTopBarButton.tsx
type HomeTopBarButtonProps (line 9) | interface HomeTopBarButtonProps {
FILE: app/(tabs)/index/components/HomeWidget.tsx
type HomeWidgetItem (line 14) | interface HomeWidgetItem {
type HomeWidgetProps (line 25) | interface HomeWidgetProps {
FILE: app/(tabs)/index/hooks/useHomeData.ts
constant HOME_SYNC_TTL_MS (line 16) | const HOME_SYNC_TTL_MS = 5 * 60 * 1000;
FILE: app/(tabs)/index/index.old.tsx
function setHomeworkAsDone (line 170) | async function setHomeworkAsDone(homework: Homework) {
FILE: app/(tabs)/index/widgets/Grades.tsx
constant PERIODS_TTL_MS (line 10) | const PERIODS_TTL_MS = 5 * 60 * 1000;
constant GRADES_TTL_MS (line 11) | const GRADES_TTL_MS = 5 * 60 * 1000;
type GradesWidgetProps (line 35) | type GradesWidgetProps = {
FILE: app/(tabs)/news/_layout.tsx
function Layout (line 7) | function Layout() {
FILE: app/(tabs)/news/index.tsx
function cleanContent (line 203) | function cleanContent(html: string): string {
function truncateString (line 209) | function truncateString(str: string, maxLength: number): string {
FILE: app/(tabs)/tasks/_layout.tsx
function Layout (line 7) | function Layout() {
FILE: app/(tabs)/tasks/atoms/DateHeader.tsx
type DateHeaderProps (line 16) | interface DateHeaderProps {
FILE: app/(tabs)/tasks/atoms/EmptyState.tsx
type EmptyStateProps (line 10) | interface EmptyStateProps {
FILE: app/(tabs)/tasks/atoms/TasksSummary.tsx
type TasksSummaryProps (line 12) | interface TasksSummaryProps {
FILE: app/(tabs)/tasks/components/TaskItem.tsx
type TaskItemProps (line 14) | interface TaskItemProps {
FILE: app/(tabs)/tasks/components/TasksHeader.tsx
type SortMethod (line 12) | type SortMethod = 'date' | 'subject' | 'done';
type TasksHeaderProps (line 14) | interface TasksHeaderProps {
FILE: app/(tabs)/tasks/components/TasksList.tsx
type HomeworkSection (line 20) | interface HomeworkSection {
type TasksListProps (line 27) | interface TasksListProps {
FILE: app/(tabs)/tasks/components/WeekPicker.tsx
type WeekPickerProps (line 12) | interface WeekPickerProps {
FILE: app/(tabs)/tasks/hooks/useHomeworkData.ts
type Service (line 17) | type Service = { id: string };
FILE: app/(tabs)/tasks/hooks/useTaskFilters.ts
type SortMethod (line 6) | type SortMethod = 'date' | 'subject' | 'done';
type HomeworkSection (line 18) | interface HomeworkSection {
FILE: app/_layout.tsx
function RootLayout (line 16) | function RootLayout() {
FILE: app/alert.tsx
function AlertModal (line 16) | function AlertModal() {
FILE: app/changelog.tsx
function ChangelogScreen (line 7) | function ChangelogScreen() {
FILE: app/consent.tsx
function ConsentScreen (line 19) | function ConsentScreen() {
FILE: app/demo.tsx
function TabOneScreen (line 21) | function TabOneScreen() {
type SectionTitleProps (line 308) | interface SectionTitleProps {
FILE: app/devmode.tsx
function Devmode (line 28) | function Devmode() {
FILE: components/ActivityIndicator.tsx
type ActivityIndicatorProps (line 13) | interface ActivityIndicatorProps {
FILE: components/AndroidHeaderBackground.tsx
function AndroidHeaderBackground (line 4) | function AndroidHeaderBackground() {
FILE: components/AppColorsSelector.tsx
type ColorSelectorProps (line 15) | interface ColorSelectorProps {
type AppColorsSelectorProps (line 24) | interface AppColorsSelectorProps {
FILE: components/AppProviders.tsx
type AppProvidersProps (line 14) | interface AppProvidersProps {
function AppProviders (line 18) | function AppProviders({ children }: AppProvidersProps) {
FILE: components/DevModeNotice.tsx
function DevModeNotice (line 10) | function DevModeNotice() {
FILE: components/Log/LogIcon.tsx
constant ICON_COLORS (line 7) | const ICON_COLORS: Record<LogType, string> = {
constant ICON_COMPONENTS (line 14) | const ICON_COMPONENTS: Record<LogType, React.ComponentType<{ color: stri...
FILE: components/RootNavigator.tsx
function RootNavigator (line 21) | function RootNavigator() {
FILE: components/SettingsHeader.tsx
type SettingsHeaderProps (line 9) | interface SettingsHeaderProps {
function SettingsHeader (line 24) | function SettingsHeader({
FILE: components/Transit.tsx
type TransitProps (line 47) | interface TransitProps {
FILE: components/UnderConstructionNotice.tsx
function UnderConstructionNotice (line 9) | function UnderConstructionNotice() {
FILE: constants/LayoutScreenOptions.ts
constant FONT_CONFIG (line 6) | const FONT_CONFIG = {
constant STACK_SCREEN_OPTIONS (line 20) | const STACK_SCREEN_OPTIONS = {
constant ALERT_SCREEN_OPTIONS (line 24) | const ALERT_SCREEN_OPTIONS = {
constant DEVMODE_SCREEN_OPTIONS (line 34) | const DEVMODE_SCREEN_OPTIONS = {
constant DEMO_SCREEN_OPTIONS (line 39) | const DEMO_SCREEN_OPTIONS = {
constant CONSENT_SCREEN_OPTIONS (line 44) | const CONSENT_SCREEN_OPTIONS = {
constant CHANGELOG_SCREEN_OPTIONS (line 53) | const CHANGELOG_SCREEN_OPTIONS = {
constant AI_SCREEN_OPTIONS (line 58) | const AI_SCREEN_OPTIONS = {
FILE: database/DatabaseProvider.tsx
function ClearDatabaseForAccount (line 30) | async function ClearDatabaseForAccount(accountId: string) {
function removeAllDuplicates (line 66) | async function removeAllDuplicates() {
function findTableDuplicates (line 124) | async function findTableDuplicates(db: Database, tableName: string, keyF...
FILE: database/mappers/attendance.ts
function mapDelaysToShared (line 4) | function mapDelaysToShared(delays: Delay[], parent: Attendance): SharedD...
function mapAbsencesToShared (line 16) | function mapAbsencesToShared(absences: Absence[], parent: Attendance): S...
function mapPunishmentsToShared (line 28) | function mapPunishmentsToShared(punishments: Punishment[]): SharedPunish...
function mapObservationsToShared (line 49) | function mapObservationsToShared(observations: Observation[]): SharedObs...
FILE: database/mappers/balances.ts
function mapBalancesToShared (line 5) | function mapBalancesToShared(balance: Balance): SharedBalance {
FILE: database/mappers/canteen.ts
function mapCanteenMenuToShared (line 5) | function mapCanteenMenuToShared(menu: CanteenMenu): SharedCanteenMenu {
function mapCanteenTransactionToShared (line 15) | function mapCanteenTransactionToShared(transaction: CanteenHistoryItem):...
FILE: database/mappers/chats.ts
function mapChatsToShared (line 7) | function mapChatsToShared(data: Chat[]): SharedChat[] {
function mapRecipientsToShared (line 19) | function mapRecipientsToShared(data: Recipient[]): SharedRecipient[] {
function mapMessagesToShared (line 27) | function mapMessagesToShared(data: Message[]): SharedMessage[] {
FILE: database/mappers/course.ts
function mapCourseToShared (line 5) | function mapCourseToShared(course: Course): SharedCourse {
FILE: database/mappers/grade.ts
function mapPeriodToShared (line 6) | function mapPeriodToShared(period: Period): SharedPeriod {
function mapGradeToShared (line 18) | function mapGradeToShared(grade: Grade): SharedGrade {
function mapPeriodGradesToShared (line 40) | function mapPeriodGradesToShared(data: PeriodGrades): SharedPeriodGrades {
FILE: database/mappers/kids.ts
function mapKidsToShared (line 5) | function mapKidsToShared(kid: Kid): SharedKid {
FILE: database/mappers/subject.ts
function mapSubjectToShared (line 5) | function mapSubjectToShared(subject: Subject): SharedSubject {
FILE: database/models/Attendance.ts
class Attendance (line 8) | class Attendance extends Model {
class Delay (line 29) | class Delay extends Model {
class Observation (line 45) | class Observation extends Model {
class Absence (line 62) | class Absence extends Model {
class Punishment (line 78) | class Punishment extends Model {
method homeworkDocuments (line 100) | get homeworkDocuments(): Attachment[] {
method reasonDocuments (line 104) | get reasonDocuments(): Attachment[] {
FILE: database/models/Balance.ts
class Balance (line 6) | class Balance extends Model {
FILE: database/models/CanteenHistory.ts
class CanteenHistoryItem (line 6) | class CanteenHistoryItem extends Model {
FILE: database/models/CanteenMenu.ts
class CanteenMenu (line 8) | class CanteenMenu extends Model {
method lunch (line 17) | get lunch(): Meal {
method dinner (line 21) | get dinner(): Meal {
FILE: database/models/Chat.ts
class Chat (line 6) | class Chat extends Model {
class Recipient (line 24) | class Recipient extends Model {
class Message (line 38) | class Message extends Model {
FILE: database/models/Event.ts
class Event (line 6) | class Event extends Model {
FILE: database/models/Grades.ts
class Period (line 10) | class Period extends Model {
class Grade (line 26) | class Grade extends Model {
method outOf (line 51) | get outOf(): GradeScore {
method studentScore (line 55) | get studentScore(): GradeScore {
method averageScore (line 59) | get averageScore(): GradeScore {
method minScore (line 63) | get minScore(): GradeScore {
method maxScore (line 67) | get maxScore(): GradeScore {
class PeriodGrades (line 72) | class PeriodGrades extends Model {
method studentOverall (line 89) | get studentOverall(): GradeScore {
method classAverage (line 93) | get classAverage(): GradeScore {
FILE: database/models/Homework.ts
class Homework (line 6) | class Homework extends Model {
FILE: database/models/Ical.ts
class Ical (line 6) | class Ical extends Model {
FILE: database/models/Kid.ts
class Kid (line 6) | class Kid extends Model {
FILE: database/models/News.ts
class News (line 6) | class News extends Model {
FILE: database/models/Subject.ts
class Subject (line 10) | class Subject extends Model {
method studentAverage (line 30) | get studentAverage(): GradeScore {
method classAverage (line 34) | get classAverage(): GradeScore {
method maximum (line 38) | get maximum(): GradeScore {
method minimum (line 42) | get minimum(): GradeScore {
method outOf (line 46) | get outOf(): GradeScore {
FILE: database/models/Timetable.ts
class Course (line 6) | class Course extends Model {
FILE: database/useAttendance.ts
function addAttendanceToDatabase (line 12) | async function addAttendanceToDatabase(attendances: SharedAttendance[], ...
function getAttendanceFromCache (line 165) | async function getAttendanceFromCache(period: string): Promise<SharedAtt...
FILE: database/useBalance.ts
function removeBalanceFromDatabase (line 12) | async function removeBalanceFromDatabase(serviceId: string) {
function addBalancesToDatabase (line 29) | async function addBalancesToDatabase(balances: SharedBalance[]) {
function getBalancesFromCache (line 54) | async function getBalancesFromCache(): Promise<SharedBalance[]> {
FILE: database/useCanteen.ts
function addCanteenMenuToDatabase (line 14) | async function addCanteenMenuToDatabase(menus: SharedCanteenMenu[]) {
function getCanteenMenuFromCache (line 57) | async function getCanteenMenuFromCache(startDate: Date): Promise<SharedC...
function addCanteenTransactionToDatabase (line 76) | async function addCanteenTransactionToDatabase(transactions: SharedCante...
function getCanteenTransactionsFromCache (line 122) | async function getCanteenTransactionsFromCache(): Promise<SharedCanteenH...
function getWeekRangeForDate (line 140) | function getWeekRangeForDate(date: Date) {
FILE: database/useChat.ts
function addChatsToDatabase (line 12) | async function addChatsToDatabase(chats: SharedChat[]) {
function addRecipientsToDatabase (line 36) | async function addRecipientsToDatabase(chat: SharedChat, recipients: Sha...
function addMessagesToDatabase (line 66) | async function addMessagesToDatabase(chat: SharedChat, messages: SharedM...
function getChatsFromCache (line 99) | async function getChatsFromCache(): Promise<SharedChat[]> {
function getRecipientsFromCache (line 110) | async function getRecipientsFromCache(chat: SharedChat): Promise<SharedR...
function getMessagesFromCache (line 124) | async function getMessagesFromCache(chat: SharedChat): Promise<SharedMes...
FILE: database/useEvents.ts
function useEventsForDay (line 7) | function useEventsForDay(date: Date, refresh = 0) {
FILE: database/useEventsById.ts
function useEventById (line 7) | function useEventById(id: string | number | undefined) {
FILE: database/useGrades.ts
function addPeriodsToDatabase (line 12) | async function addPeriodsToDatabase(periods: SharedPeriod[]) {
function getPeriodsFromCache (line 40) | async function getPeriodsFromCache(): Promise<SharedPeriod[]> {
function addGradesToDatabase (line 58) | async function addGradesToDatabase(grades: SharedGrade[], subject: strin...
function addPeriodGradesToDatabase (line 93) | async function addPeriodGradesToDatabase(item: SharedPeriodGrades, perio...
function getGradePeriodsFromCache (line 125) | async function getGradePeriodsFromCache(period: string): Promise<SharedP...
FILE: database/useHomework.ts
function mapHomeworkToShared (line 13) | function mapHomeworkToShared(homework: Homework): SharedHomework {
function useHomeworkForWeek (line 30) | function useHomeworkForWeek(weekNumber: number, refresh = 0) {
function getHomeworksFromCache (line 45) | async function getHomeworksFromCache(
function addHomeworkToDatabase (line 65) | async function addHomeworkToDatabase(homeworks: SharedHomework[]) {
function updateHomeworkIsDone (line 165) | async function updateHomeworkIsDone(
function getDateRangeOfWeek (line 196) | function getDateRangeOfWeek(
function parseJsonArray (line 213) | function parseJsonArray(s: string): unknown[] {
function getWeekNumberFromDate (line 222) | function getWeekNumberFromDate(date: Date): number {
FILE: database/useIcals.ts
function useIcals (line 7) | function useIcals(refresh = 0) {
function useAddIcal (line 20) | function useAddIcal() {
function useRemoveIcal (line 35) | function useRemoveIcal() {
function useUpdateIcalParsing (line 45) | function useUpdateIcalParsing() {
FILE: database/useKids.ts
function addKidToDatabase (line 11) | async function addKidToDatabase(kids: SharedKid[]) {
function getKidsFromCache (line 34) | async function getKidsFromCache(): Promise<SharedKid[]> {
FILE: database/useNews.ts
function useNews (line 14) | function useNews(refresh = 0) {
function addNewsToDatabase (line 34) | async function addNewsToDatabase(news: SharedNews[]) {
function getNewsFromCache (line 100) | async function getNewsFromCache(): Promise<SharedNews[]> {
function mapNewsToShared (line 118) | function mapNewsToShared(news: News): SharedNews {
FILE: database/usePeriodsCache.tsx
function usePeriods (line 7) | function usePeriods() {
FILE: database/useSubject.ts
function addSubjectsToDatabase (line 11) | async function addSubjectsToDatabase(
FILE: database/useTimetable.ts
function useTimetable (line 15) | function useTimetable(refresh = 0, weekNumber: number | number[] = 0, da...
function addCourseDayToDatabase (line 46) | async function addCourseDayToDatabase(courses: SharedCourseDay[]) {
function getCoursesFromCache (line 145) | async function getCoursesFromCache(weeks: number[], year: number): Promi...
FILE: database/utils/initialization.ts
class DatabaseInitializer (line 7) | class DatabaseInitializer {
method getInstance (line 11) | static getInstance(): DatabaseInitializer {
method initializeDatabase (line 18) | async initializeDatabase(): Promise<void> {
method forceResetDatabaseQueue (line 44) | private async forceResetDatabaseQueue(db: Database): Promise<void> {
method testInitialDatabaseHealth (line 73) | private async testInitialDatabaseHealth(db: Database): Promise<boolean> {
method attemptDatabaseRecovery (line 97) | private async attemptDatabaseRecovery(db: Database): Promise<void> {
method isReady (line 128) | isReady(): boolean {
function initializeDatabaseOnStartup (line 133) | async function initializeDatabaseOnStartup(): Promise<void> {
FILE: database/utils/safeTransaction.ts
function safeWrite (line 5) | async function safeWrite<T>(
function safeRead (line 29) | async function safeRead<T>(
function batchOperations (line 54) | function batchOperations<T>(
function executeBatchedOperations (line 65) | async function executeBatchedOperations<T>(
FILE: hooks/useAppInitialization.ts
constant APP_KEY (line 28) | const APP_KEY = secrets.APP_KEY;
constant SALT (line 29) | const SALT = secrets.SALT;
constant SERVER_URL (line 30) | const SERVER_URL = secrets.SERVER_URL ?? "https://analytics.papillon.bzh";
function useAppInitialization (line 32) | function useAppInitialization() {
FILE: services/alise/balance.ts
function fetchAliseBalance (line 7) | async function fetchAliseBalance(session: Client, accountId: string): Pr...
FILE: services/alise/history.ts
function fetchAliseHistory (line 4) | async function fetchAliseHistory(session: Client, accountId: string): Pr...
FILE: services/alise/index.ts
class Alise (line 11) | class Alise implements SchoolServicePlugin {
method constructor (line 24) | constructor(accountId: string) {
method refreshAccount (line 30) | async refreshAccount(credentials: Auth): Promise<Alise> {
method getCanteenBalances (line 41) | async getCanteenBalances(): Promise<Balance[]> {
method getCanteenTransactionsHistory (line 52) | async getCanteenTransactionsHistory(): Promise<CanteenHistoryItem[]> {
FILE: services/alise/refresh.ts
function refreshAliseAccount (line 4) | async function refreshAliseAccount(accountId: string, credentials: Auth)...
FILE: services/appscho/index.ts
class Appscho (line 11) | class Appscho implements SchoolServicePlugin {
method constructor (line 18) | constructor(public accountId: string) {}
method refreshAccount (line 20) | async refreshAccount(credentials: Auth): Promise<Appscho> {
method getWeeklyTimetable (line 34) | async getWeeklyTimetable(weekNumber: number, date: Date, forceRefresh?...
method getNews (line 43) | async getNews(): Promise<News[]> {
FILE: services/appscho/news.ts
function fetchAppschoNews (line 5) | async function fetchAppschoNews(_session: User, accountId: string, insta...
FILE: services/appscho/refresh.ts
function refreshAppSchoAccount (line 5) | async function refreshAppSchoAccount(
FILE: services/appscho/timetable.ts
function parseAppschoDate (line 6) | function parseAppschoDate(dateStr: string): Date {
function fetchAppschoTimetable (line 11) | async function fetchAppschoTimetable(
function mapAppschoCourses (line 53) | function mapAppschoCourses(lessons: Lesson[], accountId: string): Course...
FILE: services/ard/balance.ts
function fetchArdBalance (line 5) | async function fetchArdBalance(
FILE: services/ard/history.ts
function fetchARDHistory (line 5) | async function fetchARDHistory(
FILE: services/ard/index.ts
class ARD (line 11) | class ARD implements SchoolServicePlugin {
method constructor (line 18) | constructor(public accountId: string) {}
method initCapabilities (line 20) | private async initCapabilities() {
method refreshAccount (line 27) | async refreshAccount(credentials: Auth): Promise<ARD> {
method getCanteenBalances (line 36) | async getCanteenBalances(): Promise<Balance[]> {
method getCanteenTransactionsHistory (line 45) | async getCanteenTransactionsHistory(): Promise<CanteenHistoryItem[]> {
FILE: services/ard/refresh.ts
function refreshArdAccount (line 5) | async function refreshArdAccount(
FILE: services/ecoledirecte/attendance.ts
function fetchEDAttendance (line 8) | async function fetchEDAttendance(session: Client, accountId: string): Pr...
function mapEcoleDirecteAbsences (line 33) | function mapEcoleDirecteAbsences(data: SchoolLifeAttendanceItem[], accou...
function mapEcoleDirecteDelays (line 50) | function mapEcoleDirecteDelays(data: SchoolLifeAttendanceItem[], account...
function mapEcoleDirectePunishments (line 66) | function mapEcoleDirectePunishments(data: SchoolLifeConductItem[], accou...
function mapStringToDates (line 88) | function mapStringToDates(str: string): { start: Date, end: Date } {
function mapStringToDuration (line 150) | function mapStringToDuration(str: string): number | undefined {
FILE: services/ecoledirecte/balance.ts
type EDBalanceElement (line 5) | interface EDBalanceElement {
type EDBalanceResponse (line 14) | interface EDBalanceResponse {
function fetchEDBalances (line 43) | async function fetchEDBalances(session: Session): Promise<Balance[]> {
FILE: services/ecoledirecte/chat.ts
function fetchEDChats (line 8) | async function fetchEDChats(session: Session, account: Account, accountI...
function fetchEDChatMessage (line 28) | async function fetchEDChatMessage(session: Session, account: Account, ac...
FILE: services/ecoledirecte/grades.ts
function fetchEDGradePeriods (line 9) | async function fetchEDGradePeriods(session: Client, accountId: string): ...
function fetchEDGrades (line 24) | async function fetchEDGrades(session: Client, accountId: string, period:...
function parseGradeValue (line 100) | function parseGradeValue(value: string): GradeScore {
FILE: services/ecoledirecte/homework.ts
function fetchEDHomeworks (line 6) | async function fetchEDHomeworks(
function setEDHomeworkAsDone (line 37) | async function setEDHomeworkAsDone(session: Client, homework: Homework, ...
FILE: services/ecoledirecte/index.ts
class EcoleDirecte (line 19) | class EcoleDirecte implements SchoolServicePlugin {
method constructor (line 33) | constructor(public accountId: string) {}
method refreshAccount (line 35) | async refreshAccount(credentials: Auth): Promise<EcoleDirecte> {
method getHomeworks (line 44) | async getHomeworks(weekNumber: number): Promise<Homework[]> {
method getNews (line 52) | async getNews(): Promise<News[]> {
method getGradesForPeriod (line 60) | async getGradesForPeriod(period: Period): Promise<PeriodGrades> {
method getGradesPeriods (line 68) | async getGradesPeriods(): Promise<Period[]> {
method getAttendanceForPeriod (line 76) | async getAttendanceForPeriod(): Promise<Attendance> {
method getWeeklyTimetable (line 84) | async getWeeklyTimetable(weekNumber: number, date: Date): Promise<Cour...
method setHomeworkCompletion (line 92) | async setHomeworkCompletion(homework: Homework, state?: boolean): Prom...
FILE: services/ecoledirecte/news.ts
function fetchEDNews (line 7) | async function fetchEDNews(session: Client, accountId: string): Promise<...
FILE: services/ecoledirecte/qrcode.ts
function fetchEDQRCode (line 4) | function fetchEDQRCode(account: Account): QRCode {
FILE: services/ecoledirecte/refresh.ts
function refreshEDAccount (line 6) | async function refreshEDAccount(accountId: string, credentials: Auth): P...
FILE: services/ecoledirecte/timetable.ts
function fetchEDTimetable (line 8) | async function fetchEDTimetable(session: Client, accountId: string, week...
function mapEcoleDirecteCourses (line 36) | function mapEcoleDirecteCourses(data: TimetableCourse[], accountId: stri...
function mapCourseKind (line 52) | function mapCourseKind(kind: TimetableCourseType): CourseType {
FILE: services/errors/AuthenticationError.ts
class AuthenticationError (line 3) | class AuthenticationError extends Error {
method constructor (line 6) | constructor(message: string, service: ServiceAccount){
FILE: services/izly/balances.ts
function fetchIzlyBalances (line 4) | async function fetchIzlyBalances(accountId: string, session: Identificat...
FILE: services/izly/history.ts
function fetchIzlyHistory (line 4) | async function fetchIzlyHistory(accountId: string, session: Identificati...
FILE: services/izly/index.ts
class Izly (line 12) | class Izly implements SchoolServicePlugin {
method constructor (line 19) | constructor(public accountId: string) {}
method refreshAccount (line 21) | async refreshAccount(credentials: Auth): Promise<Izly> {
method getCanteenBalances (line 30) | async getCanteenBalances(): Promise<Balance[]> {
method getCanteenTransactionsHistory (line 38) | async getCanteenTransactionsHistory(): Promise<CanteenHistoryItem[]> {
method getCanteenQRCodes (line 46) | async getCanteenQRCodes(): Promise<QRCode> {
FILE: services/izly/qrcode.ts
function fetchIzlyQRCode (line 4) | function fetchIzlyQRCode(accountId: string, session: Identification): QR...
FILE: services/izly/refresh.ts
function refreshIzlyAccount (line 5) | async function refreshIzlyAccount(accountId: string, credentials: Auth):...
FILE: services/lannion/attendance.ts
function fetchLannionAttendance (line 7) | async function fetchLannionAttendance(session: LannionClient, accountId:...
function processAbsences (line 51) | function processAbsences(releve: LannionReleve, accountId: string): Abse...
FILE: services/lannion/grades.ts
function safeParseFloat (line 8) | function safeParseFloat(value: string | null | undefined): number {
function createScore (line 19) | function createScore(value: number, disabled: boolean = false, outOf?: n...
function getGradeScore (line 27) | function getGradeScore(note: LannionNote | undefined, key: 'value' | 'mo...
function getSubjectAverageScore (line 39) | function getSubjectAverageScore(ressource: LannionRessource, key: 'value...
function getSubjectRankScore (line 45) | function getSubjectRankScore(ressource: LannionRessource): Score | undef...
function createEvaluationGrade (line 56) | function createEvaluationGrade(
function processRessourceGrades (line 82) | function processRessourceGrades(ressource: LannionRessource, subjectId: ...
function processUEGrades (line 92) | function processUEGrades(ressource: LannionRessource, subjectId: string,...
function processSubjectData (line 126) | function processSubjectData(
function fetchLannionGrades (line 185) | async function fetchLannionGrades(session: LannionClient, accountId: str...
FILE: services/lannion/index.ts
class Lannion (line 12) | class Lannion implements SchoolServicePlugin {
method constructor (line 21) | constructor(public accountId: string) {}
method refreshAccount (line 23) | async refreshAccount(credentials: Auth): Promise<Lannion> {
method getSemestres (line 100) | async getSemestres() {
method getInitialData (line 109) | async getInitialData() {
method getAllReleves (line 118) | async getAllReleves() {
method getGradesPeriods (line 127) | async getGradesPeriods(): Promise<Period[]> {
method getGradesForPeriod (line 131) | async getGradesForPeriod(period: Period): Promise<PeriodGrades> {
method getAttendancePeriods (line 139) | async getAttendancePeriods(): Promise<Period[]> {
method getAttendanceForPeriod (line 143) | async getAttendanceForPeriod(period: string): Promise<Attendance> {
FILE: services/lannion/module/api.ts
constant SERVICE_URL (line 8) | const SERVICE_URL = 'https://notes9.iutlan.univ-rennes1.fr/services/data...
class LannionAPI (line 10) | class LannionAPI {
method constructor (line 11) | constructor(private client: LannionClient) {}
method request (line 13) | private async request<T>(query: string, params?: Record<string, string...
method getInitialData (line 67) | async getInitialData(): Promise<ApiResponse<InitialData>> {
method getSemestres (line 71) | async getSemestres(): Promise<ApiResponse<Semestre[]>> {
method getReleveEtudiant (line 79) | async getReleveEtudiant(semestreId: string | number): Promise<ApiRespo...
method getAllReleves (line 83) | async getAllReleves(): Promise<ApiResponse<Releve[]>> {
FILE: services/lannion/module/client.ts
constant CAS_CONFIG (line 7) | const CAS_CONFIG = {
class LannionClient (line 14) | class LannionClient {
method constructor (line 17) | constructor() { /* empty */ }
method authenticate (line 19) | async authenticate(username: string, password: string): Promise<void> {
method extractExecutionToken (line 94) | private extractExecutionToken(html: string): string {
method getSession (line 102) | getSession(): LannionSession {
method setSession (line 109) | setSession(phpSessionId: string) {
method validateSession (line 113) | async validateSession(): Promise<{ isValid: boolean; error?: string }> {
method clearSessionAndCookies (line 118) | async clearSessionAndCookies() {
function authenticateWithCredentials (line 123) | async function authenticateWithCredentials(username: string, password: s...
FILE: services/lannion/module/types.ts
type LannionSession (line 1) | interface LannionSession {
type ECTS (line 6) | interface ECTS {
type Groupe (line 10) | interface Groupe {
type AbsencesSemestre (line 28) | interface AbsencesSemestre {
type Decision (line 34) | interface Decision {
type DecisionUE (line 40) | interface DecisionUE {
type AutorisationInscription (line 49) | interface AutorisationInscription {
type LannionSemestre (line 54) | interface LannionSemestre {
type LannionReleve (line 63) | interface LannionReleve {
type LannionUE (line 78) | interface LannionUE {
type LannionRessource (line 91) | interface LannionRessource {
type LannionSAE (line 96) | interface LannionSAE {
type LannionEvaluation (line 101) | interface LannionEvaluation {
type LannionAbsence (line 114) | interface LannionAbsence {
type Competence (line 123) | interface Competence {
type Niveau (line 131) | interface Niveau {
type DecisionRCUE (line 138) | interface DecisionRCUE {
type DecisionAnnee (line 143) | interface DecisionAnnee {
type NotesSemestre (line 150) | interface NotesSemestre {
type Rang (line 157) | interface Rang {
type SemestreInfo (line 163) | interface SemestreInfo {
type Semestre (line 189) | interface Semestre {
type Releve (line 198) | interface Releve {
type Etudiant (line 204) | interface Etudiant {
type Formation (line 238) | interface Formation {
type InitialData (line 245) | interface InitialData {
type ApiResponse (line 251) | interface ApiResponse<T> {
FILE: services/local/event-converter.ts
type ConversionContext (line 8) | interface ConversionContext {
type ParsedEventData (line 18) | interface ParsedEventData {
constant DEFAULT_EVENT_DATA (line 26) | const DEFAULT_EVENT_DATA: ParsedEventData = {
constant SCHOOL_PARSERS (line 32) | const SCHOOL_PARSERS = {
function parseEventData (line 36) | function parseEventData(event: ICalEvent, isADE: boolean, isHyperplannin...
function calculateEventEndTime (line 82) | function calculateEventEndTime(event: ICalEvent): Date {
function convertICalEventToSharedCourse (line 91) | function convertICalEventToSharedCourse(
function convertMultipleEvents (line 124) | function convertMultipleEvents(
FILE: services/local/event-filter.ts
function filterEventsByDateRange (line 3) | function filterEventsByDateRange(
function filterEventsByWeek (line 18) | function filterEventsByWeek(events: ICalEvent[], weekStart: Date, weekEn...
FILE: services/local/ical-database.ts
function getAllIcals (line 6) | async function getAllIcals(): Promise<Ical[]> {
function updateIcalUrl (line 11) | async function updateIcalUrl(ical: Ical, newUrl: string): Promise<void> {
function updateIcalProvider (line 20) | async function updateIcalProvider(ical: Ical, provider: string): Promise...
function enhanceIcalIfNeeded (line 29) | async function enhanceIcalIfNeeded(ical: Ical): Promise<void> {
function updateProviderIfUnknown (line 36) | async function updateProviderIfUnknown(ical: Ical, detectedProvider: str...
FILE: services/local/ical-utils.ts
constant SCHOOL_LIST (line 1) | const SCHOOL_LIST = [
function isValidUrl (line 8) | function isValidUrl(url: string): boolean {
function normalizeUrl (line 17) | function normalizeUrl(url: string): string {
function detectProvider (line 24) | function detectProvider(prodId?: string, url?: string): { isADE: boolean...
function isADEProvider (line 35) | function isADEProvider(provider?: string): boolean {
function isHyperplanningProvider (line 39) | function isHyperplanningProvider(provider?: string): boolean {
FILE: services/local/ical.ts
type ICalEvent (line 9) | interface ICalEvent {
type ParsedICalData (line 20) | interface ParsedICalData {
function fetchAndParseICal (line 31) | async function fetchAndParseICal(url: string): Promise<ParsedICalData> {
function processIcalData (line 57) | async function processIcalData(ical: any): Promise<{ parsedData: ParsedI...
function getICalEventsForWeek (line 69) | async function getICalEventsForWeek(weekStart: Date, weekEnd: Date): Pro...
FILE: services/local/parsers/ade-parser.ts
type ParsedDescription (line 1) | interface ParsedDescription {
function parseADEDescription (line 8) | function parseADEDescription(
function enhanceADEUrl (line 95) | function enhanceADEUrl(url: string): string {
FILE: services/local/parsers/hyperplanning-parser.ts
type ParsedDescription (line 1) | interface ParsedDescription {
function parseHyperplanningDescription (line 10) | function parseHyperplanningDescription(description: string): ParsedDescr...
function isHyperplanningDescription (line 91) | function isHyperplanningDescription(description: string): boolean {
FILE: services/local/parsers/ical-event-parser.ts
type ParsedCalendarMetadata (line 5) | interface ParsedCalendarMetadata {
function parseCalendarMetadata (line 10) | function parseCalendarMetadata(comp: ICAL.Component): ParsedCalendarMeta...
function parseICalEvents (line 17) | function parseICalEvents(comp: ICAL.Component): ICalEvent[] {
function parseICalString (line 39) | function parseICalString(icalString: string): { events: ICalEvent[]; met...
FILE: services/local/parsers/schools/univrennes1_parser.ts
function parseCourseLabel (line 14) | function parseCourseLabel(label) {
function parseUR1Ical (line 52) | function parseUR1Ical(
FILE: services/multi/index.ts
class Multi (line 13) | class Multi implements SchoolServicePlugin {
method constructor (line 24) | constructor(public accountId: string) {}
method refreshAccount (line 26) | async refreshAccount(credentials: Auth): Promise<Multi> {
method getNews (line 35) | async getNews(): Promise<News[]> {
method getWeeklyTimetable (line 42) | async getWeeklyTimetable(weekNumber: number, date: Date): Promise<Cour...
FILE: services/multi/news.ts
function fetchMultiNews (line 7) | async function fetchMultiNews(
FILE: services/multi/refresh.ts
function refreshMultiSession (line 7) | async function refreshMultiSession(
FILE: services/multi/timetable.ts
function fetchMultiTimetable (line 12) | async function fetchMultiTimetable(
function mapMultiCourse (line 38) | function mapMultiCourse(data: EventResponse[], accountId: string): Cours...
FILE: services/pronote/attendance.ts
function fetchPronoteAttendance (line 22) | async function fetchPronoteAttendance(session: SessionHandle, accountId:...
function fetchPronoteAttendancePeriods (line 58) | async function fetchPronoteAttendancePeriods(session: SessionHandle, acc...
function mapObservations (line 77) | function mapObservations(observations: NotebookObservation[]): Observati...
function mapDelays (line 93) | function mapDelays(delays: NotebookDelay[], accountId: string): Delay[] {
function mapAbsences (line 108) | function mapAbsences(absences: NotebookAbsence[], accountId: string): Ab...
function mapPunishments (line 125) | function mapPunishments(punishments: NotebookPunishment[], accountId: st...
FILE: services/pronote/canteen.ts
function fetchPronoteCanteenMenu (line 6) | async function fetchPronoteCanteenMenu(
function mapCanteenMenu (line 27) | function mapCanteenMenu(menu: Menu): { lunch: Meal; dinner: Meal } {
function mapMeal (line 34) | function mapMeal(meal: PawnoteMeal | undefined): Meal {
function mapFood (line 45) | function mapFood(meal: PawnoteFood[]): Food[] {
FILE: services/pronote/chat.ts
function fetchPronoteChats (line 14) | async function fetchPronoteChats(
function fetchPronoteChatRecipients (line 34) | async function fetchPronoteChatRecipients(
function fetchPronoteChatMessages (line 67) | async function fetchPronoteChatMessages(
function sendPronoteMessageInChat (line 109) | async function sendPronoteMessageInChat(
function fetchPronoteRecipients (line 134) | async function fetchPronoteRecipients(
function createPronoteMail (line 169) | async function createPronoteMail(session: SessionHandle, accountId: stri...
function sharedToPronoteRecipient (line 181) | function sharedToPronoteRecipient(recipients: Recipient[]): NewDiscussio...
FILE: services/pronote/grades.ts
function fetchPronoteGrades (line 14) | async function fetchPronoteGrades(session: SessionHandle, accountId: str...
function fetchPronoteGradePeriods (line 45) | async function fetchPronoteGradePeriods(session: SessionHandle, accountI...
function mapSubjectGrades (line 65) | function mapSubjectGrades(grades: GradesOverview, accountId: string): Su...
function mapGradeValueToScore (line 119) | function mapGradeValueToScore(grade: GradeValue | undefined): GradeScore {
FILE: services/pronote/homework.ts
function fetchPronoteHomeworks (line 13) | async function fetchPronoteHomeworks(session: SessionHandle, accountId: ...
function setPronoteHomeworkAsDone (line 45) | async function setPronoteHomeworkAsDone(session: SessionHandle, homework...
FILE: services/pronote/index.ts
class Pronote (line 28) | class Pronote implements SchoolServicePlugin {
method constructor (line 36) | constructor(public accountId: string) {}
method checkTokenValidty (line 38) | private async checkTokenValidty(): Promise<boolean> {
method refreshAccount (line 48) | async refreshAccount(credentials: Auth): Promise<Pronote> {
method getHomeworks (line 73) | async getHomeworks(weekNumber: number): Promise<Homework[]> {
method getNews (line 83) | async getNews(): Promise<News[]> {
method getGradesForPeriod (line 93) | async getGradesForPeriod(period: Period): Promise<PeriodGrades> {
method getGradesPeriods (line 103) | async getGradesPeriods(): Promise<Period[]> {
method getAttendanceForPeriod (line 113) | async getAttendanceForPeriod(period: string): Promise<Attendance> {
method getAttendancePeriods (line 123) | async getAttendancePeriods(): Promise<Period[]> {
method getWeeklyCanteenMenu (line 133) | async getWeeklyCanteenMenu(startDate: Date): Promise<CanteenMenu[]> {
method getWeeklyTimetable (line 143) | async getWeeklyTimetable(weekNumber: number, date: Date): Promise<Cour...
method getCourseResources (line 153) | async getCourseResources(course: Course): Promise<CourseResource[]> {
method getChats (line 163) | async getChats(): Promise<Chat[]> {
method getChatRecipients (line 173) | async getChatRecipients(chat: Chat): Promise<Recipient[]> {
method getChatMessages (line 183) | async getChatMessages(chat: Chat): Promise<Message[]> {
method getRecipientsAvailableForNewChat (line 193) | async getRecipientsAvailableForNewChat(): Promise<Recipient[]> {
method sendMessageInChat (line 203) | async sendMessageInChat(chat: Chat, content: string): Promise<void> {
method setNewsAsAcknowledged (line 213) | async setNewsAsAcknowledged(news: News): Promise<News> {
method setHomeworkCompletion (line 223) | async setHomeworkCompletion(homework: Homework, state?: boolean): Prom...
method createMail (line 232) | async createMail(subject: string, content: string, recipients: Recipie...
FILE: services/pronote/news.ts
function fetchPronoteNews (line 12) | async function fetchPronoteNews(session: SessionHandle, accountId: strin...
function setPronoteNewsAsAcknowledged (line 40) | async function setPronoteNewsAsAcknowledged(
FILE: services/pronote/refresh.ts
function refreshPronoteAccount (line 12) | async function refreshPronoteAccount(
FILE: services/pronote/timetable.ts
function fetchPronoteWeekTimetable (line 16) | async function fetchPronoteWeekTimetable(
function fetchPronoteCourseResources (line 106) | async function fetchPronoteCourseResources(
FILE: services/shared/attachment.ts
type Attachment (line 10) | interface Attachment extends GenericInterface {
type AttachmentType (line 16) | enum AttachmentType {
FILE: services/shared/attendance.ts
type Attendance (line 4) | interface Attendance extends GenericInterface {
type Delay (line 11) | interface Delay extends GenericInterface {
type ObservationType (line 19) | enum ObservationType {
type Observation (line 26) | interface Observation {
type Absence (line 36) | interface Absence extends GenericInterface {
type Punishment (line 45) | interface Punishment {
FILE: services/shared/balance.ts
type Balance (line 3) | interface Balance extends GenericInterface {
FILE: services/shared/canteen.ts
type CanteenMenu (line 6) | interface CanteenMenu extends GenericInterface {
type Meal (line 12) | interface Meal {
type Food (line 21) | interface Food {
type CanteenHistoryItem (line 26) | interface CanteenHistoryItem extends GenericInterface {
type QRCode (line 33) | interface QRCode extends GenericInterface {
type QRType (line 38) | enum QRType {
type BookingDay (line 43) | interface BookingDay {
type Booking (line 48) | interface Booking extends GenericInterface {
type CanteenKind (line 56) | enum CanteenKind {
FILE: services/shared/chat.ts
type Chat (line 7) | interface Chat extends GenericInterface {
type Recipient (line 16) | interface Recipient {
type Message (line 23) | interface Message {
FILE: services/shared/grade.ts
type PeriodGrades (line 4) | interface PeriodGrades extends GenericInterface {
type Subject (line 15) | interface Subject {
type Grade (line 28) | interface Grade extends GenericInterface {
type GradeScore (line 47) | interface GradeScore {
type Period (line 54) | interface Period extends GenericInterface {
FILE: services/shared/homework.ts
type Homework (line 19) | interface Homework extends GenericInterface{
type ReturnFormat (line 33) | enum ReturnFormat {
FILE: services/shared/index.ts
class AccountManager (line 67) | class AccountManager {
method constructor (line 70) | constructor(readonly account: Account) {}
method removeService (line 72) | removeService(id: string): void {
method getAccount (line 76) | getAccount(): Account {
method refreshAllAccounts (line 80) | async refreshAllAccounts(): Promise<boolean> {
method getCanteenKind (line 116) | async getCanteenKind(clientId: string): Promise<CanteenKind> {
method getKids (line 128) | async getKids(): Promise<Kid[]> {
method getHomeworks (line 142) | async getHomeworks(weekNumber: number): Promise<Homework[]> {
method getNews (line 157) | async getNews(): Promise<News[]> {
method getGradesForPeriod (line 171) | async getGradesForPeriod(
method getGradesPeriods (line 193) | async getGradesPeriods(): Promise<Period[]> {
method getAttendanceForPeriod (line 208) | async getAttendanceForPeriod(period: string): Promise<Attendance[]> {
method getAttendancePeriods (line 230) | async getAttendancePeriods(): Promise<Period[]> {
method getWeeklyCanteenMenu (line 245) | async getWeeklyCanteenMenu(startDate: Date): Promise<CanteenMenu[]> {
method getChats (line 262) | async getChats(): Promise<Chat[]> {
method getChatRecipients (line 276) | async getChatRecipients(chat: Chat): Promise<Recipient[]> {
method getChatMessages (line 292) | async getChatMessages(chat: Chat): Promise<Message[]> {
method getRecipientsAvailableForNewChat (line 308) | async getRecipientsAvailableForNewChat(): Promise<Recipient[]> {
method getWeeklyTimetable (line 319) | async getWeeklyTimetable(weekNumber: number, date: Date): Promise<Cour...
method getCourseResources (line 336) | async getCourseResources(course: Course): Promise<CourseResource[]> {
method sendMessageInChat (line 347) | async sendMessageInChat(chat: Chat, content: string): Promise<void> {
method setNewsAsDone (line 359) | async setNewsAsDone(news: News): Promise<News> {
method setHomeworkCompletion (line 370) | async setHomeworkCompletion(
method createMail (line 384) | async createMail(
method getCanteenBalances (line 404) | async getCanteenBalances(): Promise<Balance[]> {
method getCanteenTransactionsHistory (line 419) | async getCanteenTransactionsHistory(
method getCanteenQRCodes (line 439) | async getCanteenQRCodes(clientId: string): Promise<QRCode> {
method getCanteenBookingWeek (line 453) | async getCanteenBookingWeek(
method setMealAsBooked (line 470) | async setMealAsBooked(meal: Booking, booked?: boolean): Promise<Bookin...
method clientHasCapatibility (line 481) | clientHasCapatibility(capatibility: Capabilities, clientId: string): b...
method getAvailableClients (line 489) | getAvailableClients(capability: Capabilities): SchoolServicePlugin[] {
method handleHasInternet (line 495) | private async handleHasInternet<T>(
method fetchData (line 521) | private async fetchData<T>(
method getServicePluginForAccount (line 587) | private getServicePluginForAccount(
FILE: services/shared/kid.ts
type Kid (line 5) | interface Kid extends GenericInterface {
FILE: services/shared/news.ts
type News (line 19) | interface News extends GenericInterface {
FILE: services/shared/timetable.ts
type CourseDay (line 4) | interface CourseDay {
type Course (line 9) | interface Course extends GenericInterface {
type CourseResource (line 26) | interface CourseResource {
type CourseType (line 33) | enum CourseType {
type CourseStatus (line 40) | enum CourseStatus {
FILE: services/shared/types.ts
type SchoolServicePlugin (line 46) | interface SchoolServicePlugin {
type Capabilities (line 106) | enum Capabilities {
type GenericInterface (line 130) | interface GenericInterface {
type FetchOptions (line 136) | type FetchOptions<T> = {
FILE: services/skolengo/attendance.ts
function fetchSkolengoAttendance (line 5) | async function fetchSkolengoAttendance(session: Skolengo, accountId: str...
function mapSkolengoDelays (line 30) | function mapSkolengoDelays(data: AttendanceItem[], accountId: string, ki...
function mapSkolengoAbsences (line 42) | function mapSkolengoAbsences(data: AttendanceItem[], accountId: string, ...
function durationToMinutes (line 55) | function durationToMinutes(timestamp1: number, timestamp2: number): numb...
FILE: services/skolengo/chat.ts
function fetchSkolengoChats (line 7) | async function fetchSkolengoChats(session: Skolengo, accountId: string):...
function fetchSkolengoChatRecipients (line 24) | async function fetchSkolengoChatRecipients(chat: Chat): Promise<Recipien...
function fetchSkolengoChatMessages (line 35) | async function fetchSkolengoChatMessages(chat: Chat): Promise<Message[]> {
function fetchSkolengoAvailableRecipients (line 51) | async function fetchSkolengoAvailableRecipients(session: Skolengo): Prom...
function createSkolengoMail (line 60) | async function createSkolengoMail(session: Skolengo, accountId: string, ...
function sharedToSkolengoRecipient (line 73) | function sharedToSkolengoRecipient(recipients: Recipient[]): Recipients[] {
FILE: services/skolengo/grades.ts
function fetchSkolengoGradesForPeriod (line 7) | async function fetchSkolengoGradesForPeriod(session: Skolengo, accountId...
function fetchSkolengoGradePeriods (line 36) | async function fetchSkolengoGradePeriods(session: Skolengo, accountId: s...
function mapSkolengoGrades (line 68) | function mapSkolengoGrades(grades: SkolengoGrade[], accountId: string, k...
function mapSkolengoSubjects (line 83) | function mapSkolengoSubjects(subjects: SkolengoSubjects[], accountId: st...
FILE: services/skolengo/homework.ts
function fetchSkolengoHomeworks (line 9) | async function fetchSkolengoHomeworks(session: Skolengo, accountId: stri...
function setSkolengoHomeworkAsDone (line 54) | async function setSkolengoHomeworkAsDone(accountId: string, homework: Ho...
FILE: services/skolengo/index.ts
class Skolengo (line 23) | class Skolengo implements SchoolServicePlugin {
method constructor (line 30) | constructor(public accountId: string){}
method refreshAccount (line 32) | async refreshAccount(credentials: Auth): Promise<Skolengo> {
method getKids (line 58) | getKids(): Kid[] {
method getHomeworks (line 66) | async getHomeworks(weekNumber: number): Promise<Homework[]> {
method getNews (line 74) | async getNews(): Promise<News[]> {
method getGradesForPeriod (line 82) | async getGradesForPeriod(period: Period, kid?: Kid): Promise<PeriodGra...
method getGradesPeriods (line 94) | async getGradesPeriods(): Promise<Period[]> {
method getAttendanceForPeriod (line 102) | async getAttendanceForPeriod(): Promise<Attendance> {
method getWeeklyTimetable (line 110) | async getWeeklyTimetable(weekNumber: number, date: Date): Promise<Cour...
method getChats (line 118) | async getChats(): Promise<Chat[]> {
method getChatRecipients (line 126) | async getChatRecipients(chat: Chat): Promise<Recipient[]> {
method getChatMessages (line 134) | async getChatMessages(chat: Chat): Promise<Message[]> {
method setHomeworkCompletion (line 142) | async setHomeworkCompletion(homework: Homework, state?: boolean): Prom...
method createMail (line 146) | async createMail(subject: string, content: string, recipients: Recipie...
FILE: services/skolengo/kid.ts
function fetchSkolengoKids (line 5) | function fetchSkolengoKids(session: Skolengo, accountId: string): Kid[] {
FILE: services/skolengo/news.ts
function fetchSkolengoNews (line 6) | async function fetchSkolengoNews(session: Skolengo, accountId: string): ...
FILE: services/skolengo/refresh.ts
function refreshSkolengoAccount (line 7) | async function refreshSkolengoAccount(
FILE: services/skolengo/timetable.ts
function fetchSkolengoTimetable (line 7) | async function fetchSkolengoTimetable(session: Skolengo, accountId: stri...
function mapSkolengoCourse (line 29) | function mapSkolengoCourse(data: Lesson[], accountId: string, kidName?: ...
FILE: services/transit/fetcher/Fetcher.ts
class Fetcher (line 1) | class Fetcher {
method get (line 2) | public async get<T>(url: string, options: RequestInit = {
FILE: services/transit/fetcher/endpoints.ts
constant BGTFS_BASE_URL (line 1) | const BGTFS_BASE_URL = 'https://bgtfs.transitapp.com/v3/public';
constant API_BASE_URL (line 2) | const API_BASE_URL = "https://transitapp.com/en/trip/api";
FILE: services/transit/index.ts
class Transit (line 12) | class Transit {
method plan (line 15) | public async plan(
method suggestions (line 48) | public async suggestions(
method locationDetails (line 60) | public async locationDetails(
FILE: services/transit/models/Alerts.ts
type Alerts (line 3) | interface Alerts {
FILE: services/transit/models/ArrivalSchedule.ts
type ArrivalSchedule (line 1) | interface ArrivalSchedule {
FILE: services/transit/models/DepartureLegs.ts
type DepartureLegs (line 3) | interface DepartureLegs {
FILE: services/transit/models/Itineraries.ts
type Itineraries (line 4) | interface Itineraries {
FILE: services/transit/models/ItinerariesPlanDetails.ts
type ItinerariesPlanDetails (line 1) | interface ItinerariesPlanDetails {
FILE: services/transit/models/Location.ts
type Location (line 1) | interface Location {
FILE: services/transit/models/MatchedSubstring.ts
type MatchedSubstring (line 1) | interface MatchedSubstring {
FILE: services/transit/models/Period.ts
type Period (line 1) | interface Period {
FILE: services/transit/models/Place.ts
type Place (line 3) | interface Place {
FILE: services/transit/models/PlaceDetails.ts
type PlaceDetails (line 3) | interface PlaceDetails {
FILE: services/transit/models/PlaceSuggestion.ts
type PlaceSuggestion (line 3) | interface PlaceSuggestion {
FILE: services/transit/models/PlanDetails.ts
type PlanDetails (line 4) | interface PlanDetails {
FILE: services/transit/models/PlanResult.ts
type PlanResult (line 3) | interface PlanResult {
FILE: services/transit/models/Route.ts
type Route (line 5) | interface Route {
FILE: services/transit/models/RouteLegs.ts
type BaseRouteLegs (line 4) | interface BaseRouteLegs {
type WalkRouteLegs (line 13) | interface WalkRouteLegs extends BaseRouteLegs {
type TransitRouteLegs (line 22) | interface TransitRouteLegs extends BaseRouteLegs {
type RouteLegs (line 29) | type RouteLegs = WalkRouteLegs | TransitRouteLegs;
FILE: services/transit/models/Station.ts
type Station (line 1) | interface Station {
FILE: services/transit/models/Stop.ts
type Stop (line 3) | interface Stop {
FILE: services/transit/models/StopSchedule.ts
type StopSchedule (line 1) | interface StopSchedule {
FILE: services/transit/models/Suggestions.ts
type Suggestions (line 4) | interface Suggestions {
FILE: services/transit/models/TransitDepartures.ts
type TransitDepartures (line 1) | interface TransitDepartures {
FILE: services/transit/models/Vehicle.ts
type Vehicle (line 1) | interface Vehicle {
FILE: services/turboself/balance.ts
function fetchTurboSelfBalance (line 5) | async function fetchTurboSelfBalance(session: Client, accountId: string)...
FILE: services/turboself/booking.ts
function fetchTurboSelfBookingsWeek (line 7) | async function fetchTurboSelfBookingsWeek(session: Client, accountId: st...
function mapBookings (line 15) | function mapBookings(data: TurboBooking[], accountId: string): BookingDa...
function setTurboSelfMealBookState (line 38) | async function setTurboSelfMealBookState(meal: Booking, booked?: boolean...
FILE: services/turboself/history.ts
function fetchTurboSelfHistory (line 5) | async function fetchTurboSelfHistory(session: Client, accountId: string)...
FILE: services/turboself/index.ts
class TurboSelf (line 15) | class TurboSelf implements SchoolServicePlugin {
method constructor (line 28) | constructor(public accountId: string) {}
method refreshAccount (line 30) | async refreshAccount(credentials: Auth): Promise<TurboSelf> {
method getCanteenKind (line 39) | getCanteenKind(): CanteenKind {
method getCanteenBalances (line 51) | async getCanteenBalances(): Promise<Balance[]> {
method getCanteenTransactionsHistory (line 59) | async getCanteenTransactionsHistory(): Promise<CanteenHistoryItem[]> {
method getCanteenQRCodes (line 67) | async getCanteenQRCodes(): Promise<QRCode> {
method getCanteenBookingWeek (line 75) | async getCanteenBookingWeek(weekNumber: number): Promise<BookingDay[]> {
method setMealAsBooked (line 83) | async setMealAsBooked(meal: Booking, booked?: boolean): Promise<Bookin...
FILE: services/turboself/qrcode.ts
function fetchTurboSelfQRCode (line 5) | function fetchTurboSelfQRCode(session: Client, accountId: string): QRCode {
FILE: services/turboself/refresh.ts
function refreshTurboSelfAccount (line 5) | async function refreshTurboSelfAccount(accountId: string, credentials: A...
FILE: stores/account/types.ts
type AccountsStorage (line 10) | interface AccountsStorage {
type Account (line 49) | interface Account {
type CustomisationStorage (line 62) | interface CustomisationStorage {
type TransportAddress (line 67) | interface TransportAddress {
type TransportStorage (line 75) | interface TransportStorage {
type ServiceAccount (line 93) | interface ServiceAccount {
type Auth (line 110) | interface Auth {
type Services (line 117) | enum Services {
FILE: stores/global/serializer.ts
type SerializableClass (line 5) | interface SerializableClass {
class UniversalClassSerializer (line 10) | class UniversalClassSerializer {
method serialize (line 11) | static serialize(obj: any): any {
method deserialize (line 42) | static deserialize(obj: any, classRegistry?: Map<string, any>): any {
method deserializeClass (line 62) | private static deserializeClass(obj: SerializableClass, classRegistry?...
method findConstructor (line 72) | private static findConstructor(className: string, classRegistry?: Map<...
method createInstance (line 89) | private static createInstance(Constructor: any, data: Record<string, a...
method isSerializableClass (line 99) | static isSerializableClass(obj: any): obj is SerializableClass {
FILE: stores/logs/types.ts
type LogsStorage (line 1) | interface LogsStorage {
type Log (line 6) | interface Log {
type LogType (line 13) | enum LogType {
FILE: stores/magic/types.ts
type MagicStorage (line 1) | interface MagicStorage {
type Item (line 8) | interface Item {
FILE: stores/settings/index.ts
constant DEFAULT_MATERIAL_YOU_ENABLED (line 11) | const DEFAULT_MATERIAL_YOU_ENABLED =
FILE: stores/settings/types.ts
type SettingsStorage (line 3) | interface SettingsStorage {
type SettingsState (line 11) | interface SettingsState {
type Path (line 15) | interface Path {
type Wallpaper (line 20) | interface Wallpaper {
type Personalization (line 28) | interface Personalization {
FILE: stubs/appscho/index.d.ts
type Instance (line 1) | type Instance = {
type User (line 7) | type User = {
type Lesson (line 16) | type Lesson = {
type NewsFeed (line 26) | type NewsFeed = {
constant INSTANCES (line 35) | const INSTANCES: Instance[];
FILE: stubs/appscho/index.js
constant INSTANCES (line 3) | const INSTANCES = [];
function loginWithCredentials (line 5) | async function loginWithCredentials() {
function loginWithOAuth (line 9) | async function loginWithOAuth() {
function refreshOAuthTokenWithUser (line 13) | async function refreshOAuthTokenWithUser() {
function getPlanning (line 17) | async function getPlanning() {
function getNewsFeed (line 21) | async function getNewsFeed() {
function getCASURL (line 25) | function getCASURL() {
FILE: ui/components/ActionMenu.tsx
function MenuItem (line 36) | function MenuItem({
function ActionMenu (line 109) | function ActionMenu({
FILE: ui/components/ActivityIndicator.tsx
type ActivityIndicatorProps (line 15) | interface ActivityIndicatorProps {
FILE: ui/components/AlertProvider.tsx
type Alert (line 38) | type Alert = {
type AlertContextType (line 55) | type AlertContextType = {
FILE: ui/components/AnimatedNumber.tsx
type AnimatedNumberProps (line 5) | interface AnimatedNumberProps extends TypographyProps {
function AnimatedNumber (line 15) | function AnimatedNumber({
FILE: ui/components/AnimatedPressable.tsx
constant IS_ANDROID (line 16) | const IS_ANDROID = Platform.OS === "android";
constant SPRING_IN_CONFIG (line 17) | const SPRING_IN_CONFIG = { duration: 30 };
constant SPRING_OUT_CONFIG (line 18) | const SPRING_OUT_CONFIG = { duration: 200 };
type AnimatedPressableProps (line 20) | type AnimatedPressableProps = PressableProps & {
function AnimatedPressable (line 27) | function AnimatedPressable({
FILE: ui/components/Avatar.tsx
type AvatarProps (line 18) | interface AvatarProps extends ViewProps {
FILE: ui/components/Button.tsx
type Variant (line 18) | type Variant = 'primary' | 'outline' | 'light' | 'ghost' | 'service';
type Color (line 19) | type Color = 'primary' | 'text' | 'light' | 'danger' | 'cherry' | 'black...
type Size (line 20) | type Size = 'small' | 'medium' | 'large';
type Alignment (line 21) | type Alignment = 'start' | 'center' | 'end';
type ButtonProps (line 23) | interface ButtonProps extends PressableProps {
FILE: ui/components/Calendar.tsx
type CalendarProps (line 13) | interface CalendarProps {
type CalendarRef (line 19) | interface CalendarRef {
FILE: ui/components/CircularProgress.tsx
type CircularProgressProps (line 12) | type CircularProgressProps = {
FILE: ui/components/CompactGrade.tsx
type CompactGradeProps (line 15) | interface CompactGradeProps {
FILE: ui/components/CompactTask.tsx
function CompactTask (line 12) | function CompactTask({ fromCache, setHomeworkAsDone, ref, subject, color...
FILE: ui/components/ContainedNumber.tsx
type ContainedNumberProps (line 6) | interface ContainedNumberProps {
FILE: ui/components/Course.tsx
type Variant (line 19) | type Variant = "primary" | "separator";
type CourseProps (line 21) | interface CourseProps {
FILE: ui/components/Dynamic.tsx
type DynamicProps (line 7) | type DynamicProps = {
constant APPEAR_IN (line 19) | const APPEAR_IN = PapillonAppearIn;
constant APPEAR_OUT (line 20) | const APPEAR_OUT = PapillonAppearOut;
constant ANIMATED_LAYOUT (line 23) | const ANIMATED_LAYOUT = Animation(LinearTransition, "smooth");
constant BASE_STYLE (line 26) | const BASE_STYLE = { flexDirection: "row" as const };
FILE: ui/components/ErrorBoundary.tsx
type Props (line 6) | interface Props {
type State (line 11) | interface State {
class ErrorBoundary (line 15) | class ErrorBoundary extends React.Component<Props, State> {
method constructor (line 16) | constructor(props: Props) {
method getDerivedStateFromError (line 21) | static getDerivedStateFromError(_: Error): State {
method componentDidCatch (line 25) | componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
method render (line 29) | render() {
FILE: ui/components/Grade.tsx
type GradeProps (line 13) | interface GradeProps {
FILE: ui/components/Icon.tsx
type IconProps (line 8) | interface IconProps extends ViewProps {
constant COLORED_ICON_STYLE (line 19) | const COLORED_ICON_STYLE = Object.freeze({
constant EMPTY_STYLE (line 26) | const EMPTY_STYLE = Object.freeze({});
constant WHITE_COLOR (line 30) | const WHITE_COLOR = "#ffffff";
FILE: ui/components/Item.tsx
constant LAYOUT_ANIMATION (line 10) | const LAYOUT_ANIMATION = Animation(LinearTransition, "list");
constant LEADING_TYPE (line 12) | const LEADING_TYPE = Symbol("Leading");
constant TRAILING_TYPE (line 13) | const TRAILING_TYPE = Symbol("Trailing");
constant LEADING_STYLE (line 16) | const LEADING_STYLE = Object.freeze({ layout: LAYOUT_ANIMATION });
constant TRAILING_STYLE (line 17) | const TRAILING_STYLE = Object.freeze({ layout: LAYOUT_ANIMATION });
function Leading (line 19) | function Leading({ children, ...rest }: PressableProps) {
function Trailing (line 27) | function Trailing({ children, ...rest }: PressableProps) {
type ListProps (line 44) | interface ListProps extends PressableProps {
constant DEFAULT_CONTAINER_STYLE (line 52) | const DEFAULT_CONTAINER_STYLE = Object.freeze({
constant DEFAULT_CONTENT_STYLE (line 61) | const DEFAULT_CONTENT_STYLE = Object.freeze({
function areEqual (line 68) | function areEqual(prev: ListProps, next: ListProps) {
FILE: ui/components/List.tsx
type ListProps (line 10) | interface ListProps extends ViewProps {
constant LAYOUT_ANIMATION (line 25) | const LAYOUT_ANIMATION = Object.freeze(Animation(LinearTransition, "list...
constant BORDER_BOTTOM_WIDTH (line 26) | const BORDER_BOTTOM_WIDTH = 0.5;
constant OPACITY_HEX (line 27) | const OPACITY_HEX = "25";
constant ESTIMATED_ITEM_SIZE (line 28) | const ESTIMATED_ITEM_SIZE = 48;
constant EMPTY_ARRAY (line 29) | const EMPTY_ARRAY = Object.freeze([]);
constant VIRTUALIZATION_THRESHOLD (line 31) | const VIRTUALIZATION_THRESHOLD = 20;
constant MEMOIZATION_THRESHOLD (line 32) | const MEMOIZATION_THRESHOLD = 10;
constant BASE_CONTAINER_STYLE (line 34) | const BASE_CONTAINER_STYLE: ViewStyle = Object.freeze({
constant BASE_ITEM_STYLE (line 45) | const BASE_ITEM_STYLE: ViewStyle = Object.freeze({
constant DEFAULT_PADDING (line 50) | const DEFAULT_PADDING: ViewStyle = Object.freeze({
type ChildMeta (line 88) | interface ChildMeta {
FILE: ui/components/NativeHeader.tsx
constant PRESSABLE_STYLE_CACHE (line 60) | const PRESSABLE_STYLE_CACHE = new WeakMap();
constant COLOR_CACHE (line 72) | const COLOR_CACHE = new Map();
constant DEFAULT_COLOR (line 83) | const DEFAULT_COLOR = "29947A";
constant DEFAULT_BACKGROUND_COLOR (line 84) | const DEFAULT_BACKGROUND_COLOR = getBackgroundColor(DEFAULT_COLOR);
type NativeSideProps (line 86) | interface NativeSideProps extends ViewProps {
type NativeHeaderTitleProps (line 125) | interface NativeHeaderTitleProps extends ViewProps {
type NativeHeaderHighlightProps (line 256) | interface NativeHeaderHighlightProps extends ViewProps {
FILE: ui/components/Pattern/Pattern.tsx
type AvailablePatterns (line 7) | enum AvailablePatterns {
type PatternProps (line 11) | interface PatternProps extends ViewProps{
FILE: ui/components/Search.tsx
type SearchProps (line 13) | interface SearchProps {
FILE: ui/components/SectionHeader.tsx
type SectionHeaderProps (line 6) | interface SectionHeaderProps {
function SectionHeader (line 12) | function SectionHeader({ title, leading, trailing }: SectionHeaderProps) {
FILE: ui/components/SkeletonView.tsx
type SkeletonViewProps (line 7) | interface SkeletonViewProps extends ViewProps {
FILE: ui/components/Stack.tsx
type Direction (line 9) | type Direction = "vertical" | "horizontal";
type Alignment (line 10) | type Alignment = "start" | "center" | "end";
type StackProps (line 12) | interface StackProps extends ViewProps {
constant ALIGN_ITEMS_MAP (line 35) | const ALIGN_ITEMS_MAP: Record<Alignment, FlexAlignType> = {
constant JUSTIFY_CONTENT_MAP (line 41) | const JUSTIFY_CONTENT_MAP: Record<Alignment, ViewStyle["justifyContent"]...
constant STYLE_CACHE (line 48) | const STYLE_CACHE = new Map<string, StyleProp<ViewStyle>>();
constant MAX_CACHE_SIZE (line 51) | const MAX_CACHE_SIZE = 100;
constant COMMON_STYLES (line 64) | const COMMON_STYLES = StyleSheet.create({
FILE: ui/components/Subject.tsx
type SubjectProps (line 12) | interface SubjectProps {
FILE: ui/components/TabFlatList.tsx
type TabFlatListProps (line 14) | interface TabFlatListProps extends FlatListProps<any> {
FILE: ui/components/TabHeader.tsx
type TabHeaderProps (line 14) | interface TabHeaderProps {
FILE: ui/components/TabHeaderTitle.tsx
type TabHeaderTitleProps (line 17) | interface TabHeaderTitleProps {
FILE: ui/components/TableFlatList.tsx
type SectionItem (line 15) | interface SectionItem {
type Section (line 37) | interface Section {
type TableFlatListProps (line 45) | interface TableFlatListProps extends FlatListProps<SectionItem> {
FILE: ui/components/Task.tsx
type TaskProps (line 23) | interface TaskProps {
function formatDistanceDay (line 53) | function formatDistanceDay(date: Date): string {
FILE: ui/components/Typography.tsx
constant FONT_FAMILIES (line 7) | const FONT_FAMILIES = {
constant VARIANTS (line 14) | const VARIANTS = StyleSheet.create({
constant ALIGNMENT_STYLES (line 89) | const ALIGNMENT_STYLES = StyleSheet.create({
constant WEIGHT_STYLES (line 96) | const WEIGHT_STYLES = StyleSheet.create({
constant STATIC_COLORS (line 103) | const STATIC_COLORS = {
constant FLEX_ALIGN_MAP (line 108) | const FLEX_ALIGN_MAP = {
constant ITALIC_STYLE (line 115) | const ITALIC_STYLE = { transform: [{ skewX: "-13deg" }] };
type Variant (line 117) | type Variant = keyof typeof VARIANTS;
type Color (line 118) | type Color = "primary" | "text" | "secondary" | "light" | "danger";
type Alignment (line 119) | type Alignment = keyof typeof ALIGNMENT_STYLES;
type TypographyProps (line 121) | interface TypographyProps extends TextProps {
FILE: ui/components/ViewContainer.tsx
function ViewContainer (line 6) | function ViewContainer({ children }: Readonly<{ children: React.ReactNod...
FILE: ui/native/NativeSwitch.tsx
type NativeSwitchProps (line 4) | type NativeSwitchProps = {
FILE: ui/new/Button.tsx
function Button (line 8) | function Button({ label, onPress, disabled = false, variant = "primary",...
FILE: ui/new/Divider.tsx
function Divider (line 5) | function Divider({ height = 1, ghost = false, color }: { height?: number...
FILE: ui/new/List.tsx
type MarkerProps (line 12) | type MarkerProps = {
type ListItemProps (line 16) | type ListItemProps = MarkerProps & {
type ListViewProps (line 26) | type ListViewProps = MarkerProps & {
type ListRuntimeItemContext (line 31) | type ListRuntimeItemContext = {
FILE: ui/new/ListTouchableContext.ts
type ListTouchableBlockPressController (line 3) | type ListTouchableBlockPressController = {
FILE: ui/new/RippleEffect.tsx
type RippleProps (line 13) | interface RippleProps {
FILE: ui/new/Typography.tsx
constant WEIGHTS (line 5) | const WEIGHTS = ["light", "regular", "medium", "semibold", "bold"];
constant VARIANTS (line 7) | const VARIANTS: Record<string, Record<string, string | number>> = {
constant ALIGNMENTS (line 71) | const ALIGNMENTS: Record<string, string> = {
function Typography (line 77) | function Typography({ variant = "body1", color = "textPrimary", align = ...
FILE: ui/utils/Animation.ts
constant SPRING_CONFIG (line 3) | const SPRING_CONFIG = { mass: 1, damping: 20, stiffness: 300 };
type AnimationStyle (line 5) | type AnimationStyle = "default" | "spring" | "list" | "smooth" | "fade";
FILE: ui/utils/Duration.ts
function formatDuration (line 1) | function formatDuration(seconds: number): string {
FILE: utils/adjustColor.ts
function adjust (line 3) | function adjust(hex: string, percent: number) {
FILE: utils/chats/colors.ts
function getProfileColorByName (line 3) | function getProfileColorByName (name: string): string {
FILE: utils/chats/initials.ts
function getInitials (line 1) | function getInitials(name: string): string {
FILE: utils/colorCheck.ts
type RgbComponent (line 1) | type RgbComponent = number;
type HexColor (line 2) | type HexColor = string;
type ContrastCheckResult (line 4) | interface ContrastCheckResult {
FILE: utils/colors.ts
type Colors (line 3) | enum Colors {
FILE: utils/endpoints.ts
constant MAGIC_URL (line 2) | const MAGIC_URL =
FILE: utils/format/formatSchoolName.ts
function formatSchoolName (line 1) | function formatSchoolName(input: string): string {
FILE: utils/generateId.ts
function generateId (line 1) | function generateId(str?: string): string {
FILE: utils/github/contributors.ts
type Contributor (line 2) | interface Contributor {
function getContributors (line 9) | async function getContributors (): Promise<Contributor[]> {
FILE: utils/grades/algorithms/helpers.ts
type ScoreProperty (line 3) | type ScoreProperty = "studentScore" | "averageScore" | "minScore" | "max...
FILE: utils/grades/helper/period.ts
function getCurrentPeriod (line 4) | function getCurrentPeriod(periods: Period[]): Period {
FILE: utils/logger/consent.ts
type ConsentStatus (line 4) | interface ConsentStatus {
type ConsentLevels (line 21) | interface ConsentLevels {
FILE: utils/logger/logger.ts
function getIsoDate (line 11) | function getIsoDate(): string {
function getMessage (line 15) | function getMessage(type: number, date: string, from: string, message: s...
function obtainFunctionName (line 23) | function obtainFunctionName(from?: string): string {
function saveLog (line 43) | function saveLog(date: string, message: string, type: LogType, from?: st...
function log (line 50) | function log(message: string, from?: string): void {
function error (line 58) | function error(message: string, from?: string): void {
function warn (line 67) | function warn(message: string, from?: string): void {
function info (line 75) | function info(message: string, from?: string): void {
FILE: utils/magic/ModelManager.ts
type ModelPrediction (line 12) | type ModelPrediction = {
function removeAccents (line 18) | function removeAccents(text: string): string {
function applyKerasFilters (line 22) | function applyKerasFilters(text: string, filters?: string): string {
function compactSpaces (line 34) | function compactSpaces(text: string): string {
function normalizeText (line 38) | function normalizeText(text: string, config: any): string {
function getMagicURL (line 55) | function getMagicURL(): string {
class ModelManager (line 59) | class ModelManager {
method getInstance (line 74) | static getInstance(): ModelManager {
method performPreventiveCleanup (line 81) | async performPreventiveCleanup(): Promise<void> {
method safeInit (line 103) | async safeInit(): Promise<void> {
method _performSafeInit (line 121) | private async _performSafeInit(): Promise<void> {
method resetInitializationState (line 145) | resetInitializationState(): void {
method init (line 151) | async init(): Promise<{ source: string; success: boolean; error?: stri...
method refresh (line 185) | async refresh(): Promise<{
method reset (line 249) | async reset(): Promise<{ success: boolean; error?: string }> {
method getStatus (line 283) | getStatus(): {
method tryLoadFromActivePtr (line 333) | private async tryLoadFromActivePtr(): Promise<string | null> {
method loadFromDirectory (line 363) | async loadFromDirectory(dirUri: string): Promise<void> {
method tokenize (line 448) | tokenize(text: string, verbose: boolean = false): number[] {
method processQueue (line 489) | private async processQueue(): Promise<void> {
method queuePrediction (line 510) | private async queuePrediction<T>(task: () => Promise<T>): Promise<T> {
method predict (line 525) | async predict(
method predictInternal (line 532) | private async predictInternal(
FILE: utils/magic/prediction.ts
function isModelPrediction (line 16) | function isModelPrediction(object: unknown): object is ModelPrediction {
function predictHomework (line 27) | async function predictHomework(label: string, magicEnabled: boolean = tr...
FILE: utils/magic/updater/extract.ts
function base64ToBytes (line 8) | function base64ToBytes(b64: string): Uint8Array {
function bytesToBase64 (line 14) | function bytesToBase64(u8: Uint8Array): string {
function listAllFiles (line 20) | async function listAllFiles(dirUri: string, relPrefix = ""): Promise<str...
function normalizeStagingLayout (line 37) | async function normalizeStagingLayout(stagingDir: string) {
function extractMagicToStaging (line 75) | async function extractMagicToStaging(
function validateExtractedTree (line 110) | async function validateExtractedTree(stagingDir: string) {
FILE: utils/magic/updater/fileUtils.ts
function ensureDir (line 3) | async function ensureDir(uri: string) {
function ensureParentDir (line 10) | async function ensureParentDir(path: string) {
function readJSON (line 18) | async function readJSON<T>(uri: string): Promise<T> {
function writeJSON (line 24) | async function writeJSON(uri: string, data: unknown) {
function withLock (line 30) | async function withLock<T>(
FILE: utils/magic/updater/index.ts
constant MODELS_ROOT (line 14) | const MODELS_ROOT = new Directory(Paths.document, "papillon-models");
function getCurrentPtr (line 16) | async function getCurrentPtr(): Promise<CurrentPtr | null> {
function setCurrentPtr (line 41) | async function setCurrentPtr(ptr: CurrentPtr) {
function smokeTestModel (line 48) | async function smokeTestModel(dirUri: string) {
function checkAndUpdateModel (line 64) | async function checkAndUpdateModel(
function getActivePaths (line 181) | function getActivePaths(ptr: CurrentPtr) {
FILE: utils/magic/updater/integrity.ts
function fileSha256Hex (line 4) | async function fileSha256Hex(uri: string): Promise<string> {
function verifySize (line 20) | async function verifySize(uri: string, expected: number) {
FILE: utils/magic/updater/manifest.ts
function fetchManifest (line 5) | async function fetchManifest(
function compareModels (line 39) | function compareModels(a: ApiModel, b: ApiModel): number {
function validateManifest (line 58) | function validateManifest(m: ApiModel): void {
FILE: utils/magic/updater/network.ts
function isInternetReachable (line 5) | async function isInternetReachable(): Promise<boolean> {
function fetchJsonWithRetry (line 10) | async function fetchJsonWithRetry<T>(
function fetchLatestManifest (line 30) | async function fetchLatestManifest(url: string) {
FILE: utils/magic/updater/semver.ts
type SemverTuple (line 1) | type SemverTuple = [number, number, number];
function parse (line 3) | function parse(v: string): SemverTuple {
function cmp (line 15) | function cmp(a: string, b: string): number {
function satisfies (line 25) | function satisfies(version: string, constraint: string): boolean {
function satisfiesAll (line 52) | function satisfiesAll(version: string, constraints: string[]): boolean {
FILE: utils/magic/updater/types.ts
type CurrentPtr (line 1) | type CurrentPtr = { name: string; version: string; dir: string };
type ApiModel (line 3) | type ApiModel = {
type ApiResponse (line 13) | type ApiResponse = { model: ApiModel };
FILE: utils/native/georeverse.ts
function GeographicReverse (line 3) | async function GeographicReverse(lat: number, lon: number): Promise<GeoI...
function GeographicQuerying (line 50) | async function GeographicQuerying(q: string, retry = 3): Promise<GeoInfo> {
function GeographicSearchCities (line 91) | async function GeographicSearchCities(q: string, retry = 3): Promise<Geo...
type GeoInfo (line 131) | interface GeoInfo {
type GeoSearchCityInfo (line 138) | interface GeoSearchCityInfo {
FILE: utils/native/position.ts
type CurrentPosition (line 3) | interface CurrentPosition {
FILE: utils/notification/reminder/helper.ts
type ScheduledNotification (line 4) | type ScheduledNotification = {
function requestNotificationPermissions (line 22) | async function requestNotificationPermissions(): Promise<boolean> {
function scheduleNotification (line 42) | async function scheduleNotification(
function scheduleNotificationAtDate (line 50) | async function scheduleNotificationAtDate(
function scheduleDailyNotification (line 62) | async function scheduleDailyNotification(
function getAllScheduledNotifications (line 75) | async function getAllScheduledNotifications(): Promise<Notifications.Not...
function cancelNotification (line 79) | async function cancelNotification(id: string): Promise<void> {
function cancelAllNotifications (line 83) | async function cancelAllNotifications(): Promise<void> {
FILE: utils/pronote/fetcher.ts
method headers (line 22) | get headers() {
FILE: utils/pronote/name.ts
function GetIdentityFromPronoteUsername (line 1) | function GetIdentityFromPronoteUsername(str: string): { firstName: strin...
FILE: utils/restaurant/detect-price.ts
function detectMealPrice (line 3) | function detectMealPrice (history: CanteenHistoryItem[]): number | null {
FILE: utils/services/helper.ts
function getServiceName (line 5) | function getServiceName(service: Services): string {
function getServiceLogo (line 24) | function getServiceLogo(service: Services): ImageSourcePropType {
function getServiceBackground (line 47) | function getServiceBackground(service: Services): ImageSourcePropType {
function getServiceColor (line 65) | function getServiceColor(service: Services): string {
function getCodeType (line 80) | function getCodeType(service: Services): string {
function isSelfModuleEnabledED (line 89) | function isSelfModuleEnabledED(additionals?: Record<string, any>): boole...
FILE: utils/subjects/colors.ts
function getSubjectColor (line 5) | function getSubjectColor(subject: string): string {
function getRandomColor (line 25) | function getRandomColor(ignoredColors?: string[]) {
FILE: utils/subjects/emoji.ts
function getSubjectEmoji (line 5) | function getSubjectEmoji(subject: string): string {
FILE: utils/subjects/name.ts
function getSubjectName (line 6) | function getSubjectName(subject: string): string {
FILE: utils/subjects/utils.ts
function normalizeSubjectNameInternal (line 35) | function normalizeSubjectNameInternal(subject: string): string {
function normalizeSubjectName (line 48) | function normalizeSubjectName(subject: string): string {
function cleanSubjectName (line 58) | function cleanSubjectName(subject: string): string {
function getSubjectFormat (line 74) | function getSubjectFormat(subject: string) {
FILE: utils/theme/Theme.ts
constant FALLBACK_COLORS (line 8) | const FALLBACK_COLORS = {
function getThemeColors (line 30) | function getThemeColors(useMaterialYou: boolean) {
function createDefaultTheme (line 56) | function createDefaultTheme(useMaterialYou: boolean, primaryColor: strin...
function createDarkTheme (line 74) | function createDarkTheme(useMaterialYou: boolean, primaryColor: string) {
FILE: utils/uuid/uuid.ts
function uuid (line 1) | function uuid (): string {
Condensed preview — 517 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (4,261K chars).
[
{
"path": ".github/CONTRIBUTING.md",
"chars": 3316,
"preview": "# Règles de contribution\n\n## 🔐 Signaler une vulnérabilité\n\nNous prenons la sécurité **très au sérieux**. Si tu découvres"
},
{
"path": ".github/FUNDING.yml",
"chars": 22,
"preview": "ko_fi: thepapillonapp\n"
},
{
"path": ".github/ISSUE_TEMPLATE/bug.yml",
"chars": 2623,
"preview": "name: 🐛 Signaler un bug\ndescription: Signaler des bugs nous permet d'améliorer Papillon !\ntitle: \"[Bug]: \"\n\nbody:\n - ty"
},
{
"path": ".github/ISSUE_TEMPLATE/config.yml",
"chars": 424,
"preview": "blank_issues_enabled: false\n\ncontact_links:\n - name: Une faille de sécurité ?\n url: https://github.com/PapillonApp/P"
},
{
"path": ".github/ISSUE_TEMPLATE/feature.yml",
"chars": 702,
"preview": "name: ✨ Amélioration/Fonctionnalité\ndescription: Comment pouvons-nous améliorer Papillon ?\ntitle: \"[Feature]: \"\n\nbody:\n "
},
{
"path": ".github/PULL_REQUEST_TEMPLATE.md",
"chars": 1688,
"preview": "\n\n# Règles de"
},
{
"path": ".github/bot/package.json",
"chars": 381,
"preview": "{\n \"name\": \"bot\",\n \"version\": \"1.0.0\",\n \"description\": \"\",\n \"main\": \"index.js\",\n \"scripts\": {\n \"build\": \"tsc\"\n "
},
{
"path": ".github/bot/src/issue/index.ts",
"chars": 1305,
"preview": "import * as github from '@actions/github';\nimport * as core from '@actions/core';\nimport { getLabelsFromTitle } from './"
},
{
"path": ".github/bot/src/issue/labeler.ts",
"chars": 324,
"preview": "export function getLabelsFromTitle(title: string): string[] {\n const labels: string[] = [];\n \n const lowerTitle = tit"
},
{
"path": ".github/bot/src/issue/message.ts",
"chars": 837,
"preview": "import { Octokit } from '@octokit/rest';\n\nexport async function postWelcomeMessage(\n octokit: any,\n owner: string,\n r"
},
{
"path": ".github/bot/tsconfig.json",
"chars": 276,
"preview": "{\n \"compilerOptions\": {\n \"target\": \"ES2022\",\n \"module\": \"commonjs\",\n \"outDir\": \"./dist\",\n \"rootDir\": \"./src"
},
{
"path": ".github/workflows/build-android.yml",
"chars": 3632,
"preview": "name: Build Android\n\non:\n push:\n branches:\n - main\n\njobs:\n build:\n name: Build & Upload to Play Store\n r"
},
{
"path": ".github/workflows/merge.yml",
"chars": 824,
"preview": "name: Merge dev into main\n\non:\n workflow_dispatch:\n\njobs:\n merge:\n name: Merge\n runs-on: ubuntu-latest\n\n step"
},
{
"path": ".github/workflows/release.yml",
"chars": 2085,
"preview": "name: Release\n\non:\n workflow_dispatch:\n inputs:\n version:\n description: \"Numéro de version (x.x.x)\"\n "
},
{
"path": ".gitignore",
"chars": 651,
"preview": "# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files\n\n# dependencies\nnode_modules"
},
{
"path": ".prettierignore",
"chars": 290,
"preview": "# Dependencies\nnode_modules/\nbun.lock\npackage-lock.json\nyarn.lock\n\n# Build outputs\ndist/\nbuild/\n.expo/\nandroid/build/\nio"
},
{
"path": ".prettierrc.json",
"chars": 227,
"preview": "{\n \"semi\": true,\n \"trailingComma\": \"es5\",\n \"singleQuote\": false,\n \"printWidth\": 80,\n \"tabWidth\": 2,\n \"useTabs\": fa"
},
{
"path": "CODEOWNERS",
"chars": 235,
"preview": "services/* @raphckrman\ndatabase/* @raphckrman\nstores/* @raphckrman\n\n.github/* @ryzenixx\n\nutils/magic/* @tryon-dev\n\nui/* "
},
{
"path": "LICENSE",
"chars": 35149,
"preview": " GNU GENERAL PUBLIC LICENSE\n Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
},
{
"path": "README.md",
"chars": 5588,
"preview": "<p align=\"center\">\n <a href=\"https://github.com/PapillonApp/Papillon\"><picture><source media=\"(prefers-color-scheme: da"
},
{
"path": "app/(features)/(cards)/cards.tsx",
"chars": 7080,
"preview": "import { getManager } from \"@/services/shared\";\nimport { Balance } from \"@/services/shared/balance\";\nimport { useAccount"
},
{
"path": "app/(features)/(cards)/qrcode.tsx",
"chars": 4160,
"preview": "import Barcode, { Format } from \"@aramir/react-native-barcode\";\nimport { Phone } from \"@getpapillon/papicons\";\nimport { "
},
{
"path": "app/(features)/(cards)/specific.tsx",
"chars": 13705,
"preview": "import { useCallback, useEffect, useState, useMemo } from \"react\";\nimport { Platform, ScrollView, View } from \"react-nat"
},
{
"path": "app/(features)/(news)/specific.tsx",
"chars": 6529,
"preview": "import { getManager } from \"@/services/shared\";\nimport { News } from \"@/services/shared/news\";\nimport { useAccountStore "
},
{
"path": "app/(features)/attendance.tsx",
"chars": 14059,
"preview": "import Icon from \"@/ui/components/Icon\";\nimport { NativeHeaderHighlight, NativeHeaderPressable, NativeHeaderSide, Native"
},
{
"path": "app/(features)/soon.tsx",
"chars": 1295,
"preview": "import Icon from \"@/ui/components/Icon\";\nimport Stack from \"@/ui/components/Stack\";\nimport Typography from \"@/ui/compone"
},
{
"path": "app/(modals)/address.tsx",
"chars": 10305,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { useTheme } from \"@react-navigation/native\";\nimport * as Linki"
},
{
"path": "app/(modals)/course.tsx",
"chars": 6419,
"preview": "import { Papicons } from '@getpapillon/papicons';\nimport { useRoute, useTheme } from \"@react-navigation/native\";\nimport "
},
{
"path": "app/(modals)/grade.tsx",
"chars": 10646,
"preview": "import { Papicons } from '@getpapillon/papicons';\nimport { useRoute, useTheme } from \"@react-navigation/native\";\nimport "
},
{
"path": "app/(modals)/news.tsx",
"chars": 6810,
"preview": "import { getManager } from \"@/services/shared\";\nimport { News } from \"@/services/shared/news\";\nimport { useAccountStore "
},
{
"path": "app/(modals)/notifications.tsx",
"chars": 987,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { useTheme } from \"@react-navigation/native\";\nimport { useTrans"
},
{
"path": "app/(modals)/profile.tsx",
"chars": 6425,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { MenuView, NativeActionEvent } from \"@react-native-menu/menu\";"
},
{
"path": "app/(modals)/task.tsx",
"chars": 5292,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { useRoute, useTheme } from \"@react-navigation/native\";\nimport "
},
{
"path": "app/(modals)/wallpaper.tsx",
"chars": 11466,
"preview": "import { useAccountStore } from \"@/stores/account\"\nimport { useSettingsStore } from \"@/stores/settings\"\nimport { Wallpap"
},
{
"path": "app/(modals)/wrapped/_layout.tsx",
"chars": 345,
"preview": "import { Stack } from \"expo-router\";\nimport React from \"react\";\n\nimport { screenOptions } from \"@/utils/theme/ScreenOpti"
},
{
"path": "app/(modals)/wrapped/index.tsx",
"chars": 5576,
"preview": "import React, { useEffect, useRef, useState } from 'react';\nimport { Dimensions, FlatList, Pressable, StatusBar, View } "
},
{
"path": "app/(modals)/wrapped/stories/consent.tsx",
"chars": 4863,
"preview": "import { Papicons } from '@getpapillon/papicons';\nimport { useTheme } from '@react-navigation/native';\nimport { LiquidGl"
},
{
"path": "app/(modals)/wrapped/stories/welcome.tsx",
"chars": 1818,
"preview": "import Stack from '@/ui/components/Stack';\nimport Typography from '@/ui/components/Typography';\nimport { Papicons } from"
},
{
"path": "app/(new)/_layout.tsx",
"chars": 579,
"preview": "import { Stack } from \"expo-router\";\nimport React from \"react\";\nimport { useTranslation } from \"react-i18next\";\n\nimport "
},
{
"path": "app/(new)/event.tsx",
"chars": 12192,
"preview": "import DateTimePicker, { DateTimePickerAndroid } from '@react-native-community/datetimepicker';\nimport { useTheme } from"
},
{
"path": "app/(onboarding)/_layout.tsx",
"chars": 2087,
"preview": "import React from 'react';\nimport { Platform, StatusBar, View } from 'react-native';\nimport { Stack } from \"expo-router\""
},
{
"path": "app/(onboarding)/ageSelection.tsx",
"chars": 3648,
"preview": "import { useHeaderHeight } from \"@react-navigation/elements\";\nimport { useTheme } from \"@react-navigation/native\";\nimpor"
},
{
"path": "app/(onboarding)/components/LoginView.tsx",
"chars": 5167,
"preview": "import { useTheme } from '@react-navigation/native';\nimport React from 'react';\nimport { Alert, Image, View } from 'reac"
},
{
"path": "app/(onboarding)/components/OnboardingSelector.tsx",
"chars": 2777,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { useTheme } from \"@react-navigation/native\";\nimport { LinearGr"
},
{
"path": "app/(onboarding)/components/OnboardingWebView.tsx",
"chars": 4081,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { useTheme } from \"@react-navigation/native\";\nimport React from"
},
{
"path": "app/(onboarding)/components/ageSelection/illustrations/highSchool.tsx",
"chars": 16454,
"preview": "import * as React from \"react\";\nimport type { SvgProps } from \"react-native-svg\";\nimport Svg, { Path } from \"react-nativ"
},
{
"path": "app/(onboarding)/components/ageSelection/illustrations/middleSchool.tsx",
"chars": 4070,
"preview": "import * as React from \"react\";\nimport type { SvgProps } from \"react-native-svg\";\nimport Svg, { Path } from \"react-nativ"
},
{
"path": "app/(onboarding)/components/ageSelection/illustrations/parents.tsx",
"chars": 8979,
"preview": "import * as React from \"react\";\nimport type { SvgProps } from \"react-native-svg\";\nimport Svg, { Path } from \"react-nativ"
},
{
"path": "app/(onboarding)/components/ageSelection/illustrations/supSchool.tsx",
"chars": 2390,
"preview": "import * as React from \"react\";\nimport type { SvgProps } from \"react-native-svg\";\nimport Svg, { Path } from \"react-nativ"
},
{
"path": "app/(onboarding)/components/ageSelection/illustrations/teacher.tsx",
"chars": 13419,
"preview": "import * as React from \"react\";\nimport type { SvgProps } from \"react-native-svg\";\nimport Svg, { Path } from \"react-nativ"
},
{
"path": "app/(onboarding)/restaurants/_layout.tsx",
"chars": 1426,
"preview": "import React from 'react';\nimport { useTranslation } from \"react-i18next\";\n\nimport { Stack } from 'expo-router';\nimport "
},
{
"path": "app/(onboarding)/restaurants/alise.tsx",
"chars": 7219,
"preview": "import { router, useLocalSearchParams } from \"expo-router\";\nimport LottieView from \"lottie-react-native\";\nimport React, "
},
{
"path": "app/(onboarding)/restaurants/ard.tsx",
"chars": 9835,
"preview": "/* eslint-disable @typescript-eslint/no-require-imports */\nimport { useTheme } from \"@react-navigation/native\";\nimport {"
},
{
"path": "app/(onboarding)/restaurants/izly.tsx",
"chars": 6565,
"preview": "/* eslint-disable @typescript-eslint/no-require-imports */\nimport { useTheme } from \"@react-navigation/native\";\nimport {"
},
{
"path": "app/(onboarding)/restaurants/method.tsx",
"chars": 4360,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { useHeaderHeight } from \"@react-navigation/elements\";\nimport {"
},
{
"path": "app/(onboarding)/restaurants/turboself.tsx",
"chars": 5546,
"preview": "/* eslint-disable @typescript-eslint/no-require-imports */\nimport { useTheme } from \"@react-navigation/native\";\nimport {"
},
{
"path": "app/(onboarding)/restaurants/turboselfHost.tsx",
"chars": 8674,
"preview": "import React, { useCallback, useEffect, useMemo, useState } from 'react';\nimport { StyleSheet, Pressable, TextInput, Key"
},
{
"path": "app/(onboarding)/serviceSelection.tsx",
"chars": 5467,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { useHeaderHeight } from \"@react-navigation/elements\";\nimport {"
},
{
"path": "app/(onboarding)/services/appscho/_layout.tsx",
"chars": 850,
"preview": "import React from 'react';\n\nimport { Stack } from 'expo-router';\nimport { screenOptions } from \"@/utils/theme/ScreenOpti"
},
{
"path": "app/(onboarding)/services/appscho/credentials.tsx",
"chars": 8308,
"preview": "import { router, useLocalSearchParams } from \"expo-router\";\nimport LottieView from \"lottie-react-native\";\nimport React, "
},
{
"path": "app/(onboarding)/services/appscho/list.tsx",
"chars": 5998,
"preview": "import { Image, View } from \"react-native\";\nimport { useTheme } from \"@react-navigation/native\";\nimport React, { useMemo"
},
{
"path": "app/(onboarding)/services/appscho/webview.tsx",
"chars": 3085,
"preview": "import OnboardingWebview from \"@/components/onboarding/OnboardingWebview\";\nimport { router, useLocalSearchParams } from "
},
{
"path": "app/(onboarding)/services/ed/_layout.tsx",
"chars": 793,
"preview": "import React from 'react';\nimport { useTranslation } from \"react-i18next\";\n\nimport { Stack } from 'expo-router';\nimport "
},
{
"path": "app/(onboarding)/services/ed/credentials.tsx",
"chars": 8411,
"preview": "\nimport { Client, DoubleAuthQuestions, DoubleAuthResult, Require2FA } from \"@blockshub/blocksdirecte\";\nimport { useTheme"
},
{
"path": "app/(onboarding)/services/lannion/_layout.tsx",
"chars": 793,
"preview": "import React from 'react';\nimport { useTranslation } from \"react-i18next\";\n\nimport { Stack } from 'expo-router';\nimport "
},
{
"path": "app/(onboarding)/services/lannion/credentials.tsx",
"chars": 6409,
"preview": "import { useTheme } from \"@react-navigation/native\";\nimport { router, useNavigation } from \"expo-router\";\nimport React, "
},
{
"path": "app/(onboarding)/services/multi/_layout.tsx",
"chars": 911,
"preview": "import React from 'react';\n\nimport { Stack } from 'expo-router';\nimport { screenOptions } from \"@/utils/theme/ScreenOpti"
},
{
"path": "app/(onboarding)/services/multi/credentials.tsx",
"chars": 5235,
"preview": "\nimport { useTheme } from \"@react-navigation/native\";\nimport { authWithCredentials } from 'esup-multi.js';\nimport { rout"
},
{
"path": "app/(onboarding)/services/pronote/2fa.tsx",
"chars": 8329,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { useTheme } from \"@react-navigation/native\";\nimport * as Devic"
},
{
"path": "app/(onboarding)/services/pronote/_layout.tsx",
"chars": 1505,
"preview": "import React from 'react';\nimport { Platform, PlatformColor, View } from 'react-native';\nimport { useTranslation } from "
},
{
"path": "app/(onboarding)/services/pronote/browser.tsx",
"chars": 13675,
"preview": "import { useRoute, useTheme } from \"@react-navigation/native\";\nimport { router, useNavigation } from \"expo-router\";\nimpo"
},
{
"path": "app/(onboarding)/services/pronote/locate.tsx",
"chars": 5781,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { useHeaderHeight } from \"@react-navigation/elements\";\nimport {"
},
{
"path": "app/(onboarding)/services/pronote/qrcode.tsx",
"chars": 13120,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport MaskedView from \"@react-native-masked-view/masked-view\";\nimport"
},
{
"path": "app/(onboarding)/services/pronote/select.tsx",
"chars": 4327,
"preview": "import { useHeaderHeight } from \"@react-navigation/elements\";\nimport { useRoute, useTheme } from \"@react-navigation/nati"
},
{
"path": "app/(onboarding)/services/pronote/url.tsx",
"chars": 2193,
"preview": "import { useHeaderHeight } from \"@react-navigation/elements\";\nimport { useNavigation } from \"expo-router\";\nimport React,"
},
{
"path": "app/(onboarding)/services/skolengo/_layout.tsx",
"chars": 931,
"preview": "import React from 'react';\nimport { useTranslation } from \"react-i18next\";\n\nimport { Stack } from 'expo-router';\nimport "
},
{
"path": "app/(onboarding)/services/skolengo/locate.tsx",
"chars": 4617,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { useHeaderHeight } from \"@react-navigation/elements\";\nimport {"
},
{
"path": "app/(onboarding)/services/skolengo/webview.tsx",
"chars": 4213,
"preview": "import * as Linking from \"expo-linking\";\nimport { router, useLocalSearchParams, useNavigation } from \"expo-router\";\nimpo"
},
{
"path": "app/(onboarding)/utils/constants.tsx",
"chars": 9250,
"preview": "/* eslint-disable @typescript-eslint/no-require-imports */\nimport { Papicons } from '@getpapillon/papicons';\nimport { us"
},
{
"path": "app/(onboarding)/utils/fetchSchools.ts",
"chars": 2613,
"preview": "import { geolocation } from \"pawnote\";\nimport { School as SkolengoSkool,SearchSchools } from \"skolengojs\";\nimport { t } "
},
{
"path": "app/(onboarding)/welcome.tsx",
"chars": 5118,
"preview": "import { useFocusEffect, useIsFocused, useTheme } from \"@react-navigation/native\";\nimport { useRouter } from \"expo-route"
},
{
"path": "app/(settings)/_layout.tsx",
"chars": 4201,
"preview": "import { Stack } from \"expo-router\";\nimport React from \"react\";\nimport { Platform, StatusBar } from \"react-native\";\n\nimp"
},
{
"path": "app/(settings)/about.tsx",
"chars": 8745,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { useTheme } from \"@react-navigation/native\";\nimport { useRoute"
},
{
"path": "app/(settings)/accounts.tsx",
"chars": 7306,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { MenuView, NativeActionEvent } from \"@react-native-menu/menu\";"
},
{
"path": "app/(settings)/cards.tsx",
"chars": 6748,
"preview": "/* eslint-disable @typescript-eslint/no-require-imports */\nimport { Papicons } from \"@getpapillon/papicons\";\nimport { us"
},
{
"path": "app/(settings)/contributors.tsx",
"chars": 2655,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport React, { useEffect, useMemo, useState } from \"react\";\nimport { "
},
{
"path": "app/(settings)/edit_subject.tsx",
"chars": 14733,
"preview": "import Stack from \"@/ui/components/Stack\";\nimport AnimatedPressable from \"@/ui/components/AnimatedPressable\";\nimport { P"
},
{
"path": "app/(settings)/language.tsx",
"chars": 2461,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { useTheme } from \"@react-navigation/native\";\nimport React from"
},
{
"path": "app/(settings)/magic.tsx",
"chars": 7303,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { useTheme } from \"@react-navigation/native\";\nimport { t } from"
},
{
"path": "app/(settings)/personalization.tsx",
"chars": 10060,
"preview": "import { Alert, Platform, ScrollView, Switch } from \"react-native\";\nimport Stack from \"@/ui/components/Stack\";\nimport { "
},
{
"path": "app/(settings)/services.tsx",
"chars": 1708,
"preview": "import { UserX2Icon } from \"lucide-react-native\";\nimport React from \"react\";\nimport { ScrollView } from \"react-native\";\n"
},
{
"path": "app/(settings)/settings.tsx",
"chars": 13008,
"preview": "import { Papicons } from '@getpapillon/papicons';\nimport { useTheme } from \"@react-navigation/native\";\nimport { LinearGr"
},
{
"path": "app/(settings)/subject_personalization.tsx",
"chars": 4281,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { useTheme } from \"@react-navigation/native\";\nimport { router }"
},
{
"path": "app/(settings)/tabs.tsx",
"chars": 2256,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport React from \"react\";\nimport { useTranslation } from \"react-i18ne"
},
{
"path": "app/(settings)/transport.tsx",
"chars": 6868,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { useTheme } from \"@react-navigation/native\";\nimport * as React"
},
{
"path": "app/(tabs)/_layout.tsx",
"chars": 2534,
"preview": "import { runsIOS26 } from '@/ui/utils/IsLiquidGlass';\nimport { useTheme } from '@react-navigation/native';\nimport { Nati"
},
{
"path": "app/(tabs)/calendar/_layout.tsx",
"chars": 935,
"preview": "import { Stack } from \"expo-router\";\nimport React from \"react\";\nimport { useTranslation } from \"react-i18next\";\n\nimport "
},
{
"path": "app/(tabs)/calendar/components/CalendarDay.tsx",
"chars": 7633,
"preview": "import { useNavigation } from \"expo-router\";\nimport { t } from \"i18next\";\nimport React, { useMemo, useRef } from \"react\""
},
{
"path": "app/(tabs)/calendar/components/CalendarHeader.tsx",
"chars": 2882,
"preview": "import React from 'react';\nimport { Platform } from 'react-native';\nimport { useTheme } from '@react-navigation/native';"
},
{
"path": "app/(tabs)/calendar/components/EmptyCalendar.tsx",
"chars": 866,
"preview": "import React, { memo } from 'react';\nimport { t } from \"i18next\";\nimport { Papicons } from '@getpapillon/papicons';\nimpo"
},
{
"path": "app/(tabs)/calendar/event/[id].tsx",
"chars": 3763,
"preview": "import { MenuView } from '@react-native-menu/menu';\nimport { useTheme } from \"@react-navigation/native\";\nimport { useLoc"
},
{
"path": "app/(tabs)/calendar/hooks/useCalendarState.ts",
"chars": 3470,
"preview": "import { useState, useRef, useEffect, useCallback } from 'react';\nimport { Dimensions, FlatList } from 'react-native';\ni"
},
{
"path": "app/(tabs)/calendar/hooks/useTimetableData.ts",
"chars": 3940,
"preview": "import { useCallback, useEffect, useMemo,useRef, useState } from 'react';\n\nimport { useTimetable } from '@/database/useT"
},
{
"path": "app/(tabs)/calendar/icals.tsx",
"chars": 6557,
"preview": "import { t } from \"i18next\";\nimport { Calendar, Link2Icon, TypeIcon, Brain } from \"lucide-react-native\";\nimport React, {"
},
{
"path": "app/(tabs)/calendar/index.tsx",
"chars": 4021,
"preview": "import { useTheme } from \"@react-navigation/native\";\nimport { t } from \"i18next\";\nimport React, { useCallback, useRef, u"
},
{
"path": "app/(tabs)/grades/_layout.tsx",
"chars": 1129,
"preview": "import { Stack } from \"expo-router\";\nimport React from \"react\";\nimport { useTranslation } from \"react-i18next\";\n\nimport "
},
{
"path": "app/(tabs)/grades/atoms/Averages.tsx",
"chars": 12242,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { MenuView } from \"@react-native-menu/menu\";\nimport { useTheme "
},
{
"path": "app/(tabs)/grades/atoms/FeaturesMap.tsx",
"chars": 632,
"preview": "import React from 'react';\nimport { View } from 'react-native';\n\nimport ScodocUES from '../features/ScodocUES';\n\nconst f"
},
{
"path": "app/(tabs)/grades/atoms/Subject.tsx",
"chars": 7095,
"preview": "import { Papicons } from '@getpapillon/papicons';\nimport { useTheme } from '@react-navigation/native';\nimport { useNavig"
},
{
"path": "app/(tabs)/grades/features/ScodocUES.tsx",
"chars": 13381,
"preview": "import { Papicons } from '@getpapillon/papicons';\nimport React from 'react';\n\nimport Icon from '@/ui/components/Icon';\ni"
},
{
"path": "app/(tabs)/grades/hooks/useGradeInfluence.ts",
"chars": 4573,
"preview": "import { useMemo, useCallback } from 'react';\nimport { Grade, Subject } from \"@/services/shared/grade\";\n\nimport { getSub"
},
{
"path": "app/(tabs)/grades/index.tsx",
"chars": 20294,
"preview": "import { Papicons } from '@getpapillon/papicons';\nimport { LegendList } from '@legendapp/list';\nimport { MenuView } from"
},
{
"path": "app/(tabs)/grades/modals/AboutAverages.tsx",
"chars": 2575,
"preview": "import Stack from \"@/ui/components/Stack\";\nimport Typography from \"@/ui/components/Typography\";\nimport { useTheme } from"
},
{
"path": "app/(tabs)/grades/modals/SubjectInfo.tsx",
"chars": 5849,
"preview": "import ModalOverhead, { ModalOverHeadScore } from \"@/components/ModalOverhead\";\nimport Subject from \"@/database/models/S"
},
{
"path": "app/(tabs)/grades/utils/graph.ts",
"chars": 3711,
"preview": "\nexport interface GraphPoint {\n value: number;\n date: Date;\n originalValue?: number;\n originalDate?: Date;\n}\n\nexport"
},
{
"path": "app/(tabs)/index/_layout.tsx",
"chars": 536,
"preview": "import { Stack } from \"expo-router\";\nimport React from \"react\";\nimport { useTranslation } from \"react-i18next\";\n\nimport "
},
{
"path": "app/(tabs)/index/atoms/HomeHeader.tsx",
"chars": 6909,
"preview": "import { LiquidGlassContainer } from '@sbaiahmed1/react-native-blur';\nimport { router } from 'expo-router';\nimport * as "
},
{
"path": "app/(tabs)/index/atoms/HomeTopBar.tsx",
"chars": 1841,
"preview": "import { ProgressiveBlurView } from '@sbaiahmed1/react-native-blur';\nimport { useRouter } from 'expo-router';\nimport Rea"
},
{
"path": "app/(tabs)/index/atoms/UserProfile.tsx",
"chars": 5216,
"preview": "import { Papicons } from '@getpapillon/papicons';\nimport { MenuView } from '@react-native-menu/menu';\nimport { useTheme "
},
{
"path": "app/(tabs)/index/atoms/Wallpaper.tsx",
"chars": 2098,
"preview": "import MaskedView from '@react-native-masked-view/masked-view';\nimport { File, Paths } from 'expo-file-system';\nimport R"
},
{
"path": "app/(tabs)/index/atoms/WrappedBanner.tsx",
"chars": 4709,
"preview": "import { useFocusEffect } from \"@react-navigation/native\";\nimport { useEvent } from \"expo\";\nimport { useNavigation } fro"
},
{
"path": "app/(tabs)/index/components/HomeHeaderButton.ios.tsx",
"chars": 1798,
"preview": "import { Papicons } from '@getpapillon/papicons';\nimport { useTheme } from '@react-navigation/native';\nimport { LiquidGl"
},
{
"path": "app/(tabs)/index/components/HomeHeaderButton.tsx",
"chars": 2024,
"preview": "import React from 'react';\nimport { Pressable, StyleSheet, View } from 'react-native';\nimport { LiquidGlassView } from '"
},
{
"path": "app/(tabs)/index/components/HomeTopBarButton.ios.tsx",
"chars": 966,
"preview": "import React from 'react';\nimport { Pressable, View } from 'react-native';\nimport { LiquidGlassView } from '@sbaiahmed1/"
},
{
"path": "app/(tabs)/index/components/HomeTopBarButton.tsx",
"chars": 1050,
"preview": "import React from 'react';\nimport { Pressable, TouchableNativeFeedback, View } from 'react-native';\nimport { LiquidGlass"
},
{
"path": "app/(tabs)/index/components/HomeWidget.tsx",
"chars": 2749,
"preview": "import React from 'react';\nimport Stack from '@/ui/components/Stack';\nimport Icon from '@/ui/components/Icon';\nimport { "
},
{
"path": "app/(tabs)/index/hooks/useHomeData.ts",
"chars": 5050,
"preview": "import { router } from 'expo-router';\nimport { instance } from 'pawnote';\nimport { useCallback, useEffect } from 'react'"
},
{
"path": "app/(tabs)/index/hooks/useHomeHeaderData.ts",
"chars": 2579,
"preview": "import { useState, useEffect, useMemo, useRef } from 'react';\nimport { getChatsFromCache } from '@/database/useChat';\nim"
},
{
"path": "app/(tabs)/index/hooks/useTimetableWidgetData.ts",
"chars": 1929,
"preview": "import { useState, useEffect, useMemo } from \"react\";\nimport { useAccountStore } from \"@/stores/account\";\nimport { getWe"
},
{
"path": "app/(tabs)/index/hooks/useUserProfileData.ts",
"chars": 1357,
"preview": "import { useMemo } from 'react';\n\nimport { useAccountStore } from '@/stores/account';\nimport { getInitials } from '@/uti"
},
{
"path": "app/(tabs)/index/index.old.tsx",
"chars": 25696,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { useHeaderHeight } from \"@react-navigation/elements\";\nimport {"
},
{
"path": "app/(tabs)/index/index.tsx",
"chars": 3897,
"preview": "import { Papicons } from '@getpapillon/papicons';\nimport { useIsFocused } from '@react-navigation/native';\nimport { useR"
},
{
"path": "app/(tabs)/index/widgets/Grades.tsx",
"chars": 5894,
"preview": "import React, { useCallback, useEffect, useMemo, useState } from \"react\";\nimport { View } from \"react-native\";\n\nimport {"
},
{
"path": "app/(tabs)/index/widgets/timetable.tsx",
"chars": 2506,
"preview": "import { useNavigation } from \"expo-router\";\nimport { t } from \"i18next\";\nimport React from 'react';\nimport { FlatList }"
},
{
"path": "app/(tabs)/news/_layout.tsx",
"chars": 466,
"preview": "import { Stack } from \"expo-router\";\nimport React from \"react\";\nimport { useTranslation } from \"react-i18next\";\n\nimport "
},
{
"path": "app/(tabs)/news/index.tsx",
"chars": 7612,
"preview": "import { useNews } from '@/database/useNews'\nimport { getManager, subscribeManagerUpdate } from '@/services/shared'\nimpo"
},
{
"path": "app/(tabs)/tasks/_layout.tsx",
"chars": 502,
"preview": "import { Stack } from \"expo-router\";\nimport React from \"react\";\nimport { useTranslation } from \"react-i18next\";\n\nimport "
},
{
"path": "app/(tabs)/tasks/atoms/DateHeader.tsx",
"chars": 1940,
"preview": "import React, { memo } from 'react';\nimport { Pressable, TouchableOpacity } from 'react-native';\nimport Reanimated, {\n "
},
{
"path": "app/(tabs)/tasks/atoms/EmptyState.tsx",
"chars": 1295,
"preview": "import { Dynamic } from \"@/ui/components/Dynamic\";\nimport Icon from \"@/ui/components/Icon\";\nimport Stack from \"@/ui/comp"
},
{
"path": "app/(tabs)/tasks/atoms/TasksSummary.tsx",
"chars": 2119,
"preview": "import React from 'react';\nimport Reanimated, { LinearTransition } from 'react-native-reanimated';\nimport { useTheme } f"
},
{
"path": "app/(tabs)/tasks/components/TaskItem.tsx",
"chars": 1883,
"preview": "import React, { memo, useMemo } from 'react';\nimport { StyleSheet } from 'react-native';\nimport Reanimated from 'react-n"
},
{
"path": "app/(tabs)/tasks/components/TasksHeader.tsx",
"chars": 3771,
"preview": "import { useTheme } from '@react-navigation/native';\nimport { t } from 'i18next';\nimport React, { useMemo } from 'react'"
},
{
"path": "app/(tabs)/tasks/components/TasksList.tsx",
"chars": 4358,
"preview": "import React, { useCallback } from 'react';\nimport { Platform, RefreshControl, SectionList, StyleSheet } from 'react-nat"
},
{
"path": "app/(tabs)/tasks/components/WeekPicker.tsx",
"chars": 4924,
"preview": "import { useTheme } from '@react-navigation/native';\nimport { BlurView } from 'expo-blur';\nimport React, { useCallback, "
},
{
"path": "app/(tabs)/tasks/hooks/useHomeworkData.ts",
"chars": 3920,
"preview": "import { useState, useCallback, useEffect, useMemo } from 'react';\nimport { useAccountStore } from \"@/stores/account\";\ni"
},
{
"path": "app/(tabs)/tasks/hooks/useMagicPrediction.ts",
"chars": 793,
"preview": "import { useState, useEffect } from 'react';\nimport { useSettingsStore } from \"@/stores/settings\";\nimport { predictHomew"
},
{
"path": "app/(tabs)/tasks/hooks/useTaskFilters.ts",
"chars": 3687,
"preview": "import { useState, useMemo, useCallback } from 'react';\nimport { t } from 'i18next';\nimport { Homework } from \"@/service"
},
{
"path": "app/(tabs)/tasks/hooks/useWeekSelection.ts",
"chars": 730,
"preview": "import { useState, useCallback } from 'react';\nimport { getWeekNumberFromDate } from \"@/database/useHomework\";\n\nexport c"
},
{
"path": "app/(tabs)/tasks/index.tsx",
"chars": 2471,
"preview": "import React, { useState } from 'react';\nimport { Platform, StyleSheet, View } from 'react-native';\n\nimport TasksHeader "
},
{
"path": "app/_layout.tsx",
"chars": 688,
"preview": "\nimport 'react-native-reanimated';\nimport \"@/utils/i18n\";\n\nimport { Buffer } from 'buffer';\nimport React from 'react';\n\n"
},
{
"path": "app/alert.tsx",
"chars": 5413,
"preview": "import { useTheme } from \"@react-navigation/native\";\nimport { useLocalSearchParams, useRouter } from \"expo-router\";\nimpo"
},
{
"path": "app/changelog.tsx",
"chars": 3768,
"preview": "import Typography from \"@/ui/components/Typography\";\nimport React from \"react\";\nimport { View, ScrollView, Image } from "
},
{
"path": "app/consent.tsx",
"chars": 7330,
"preview": "import React, { useEffect, useMemo, useState } from \"react\";\nimport { Image, Linking, ScrollView, StyleSheet, View } fro"
},
{
"path": "app/demo.tsx",
"chars": 9876,
"preview": "import { useTheme } from \"@react-navigation/native\";\nimport { Hamburger } from \"lucide-react-native\";\nimport React, { Sc"
},
{
"path": "app/devmode.tsx",
"chars": 14784,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { useTheme } from \"@react-navigation/native\";\nimport { router }"
},
{
"path": "app.config.ts",
"chars": 4365,
"preview": "import PackageJSON from \"./package.json\" with { type: 'json' };\n\nconst androidPreVersion = PackageJSON.version.replaceAl"
},
{
"path": "assets/app.icon/icon.json",
"chars": 1976,
"preview": "{\n \"fill\" : {\n \"linear-gradient\" : [\n \"display-p3:0.20392,0.69804,0.57647,1.00000\",\n \"display-p3:0.19216,0"
},
{
"path": "assets/lotties/alise.json",
"chars": 23786,
"preview": "{\"assets\":[{\"h\":129,\"id\":\"0\",\"p\":\"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQo"
},
{
"path": "assets/lotties/ard.json",
"chars": 32248,
"preview": "{\"assets\":[{\"h\":129,\"id\":\"0\",\"p\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIEAAACBCAYAAADnoNlQAAAACXBIWXMAACxLAAAs"
},
{
"path": "assets/lotties/connexion.json",
"chars": 55861,
"preview": "{\"assets\":[{\"h\":96,\"id\":\"0\",\"p\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIEAAABgCAYAAAA6uBF3AAAAAXNSR0IArs4c6QAAC"
},
{
"path": "assets/lotties/izly.json",
"chars": 25280,
"preview": "{\"assets\":[{\"h\":129,\"id\":\"0\",\"p\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIEAAACBCAYAAADnoNlQAAAACXBIWXMAACxLAAAs"
},
{
"path": "assets/lotties/link.json",
"chars": 40577,
"preview": "{\"assets\":[{\"h\":346,\"id\":\"0\",\"p\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAdEAAAFaCAYAAABBtGeAAAAAAXNSR0IArs4c6QAA"
},
{
"path": "assets/lotties/location.json",
"chars": 37999,
"preview": "{\"assets\":[{\"h\":96,\"id\":\"0\",\"p\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIEAAABgCAYAAAA6uBF3AAAAAXNSR0IArs4c6QAAC"
},
{
"path": "assets/lotties/onboarding.json",
"chars": 802174,
"preview": "{\n \"v\": \"5.7.5\",\n \"fr\": 100,\n \"ip\": 0,\n \"op\": 400,\n \"w\": 315,\n \"h\": 283,\n \"nm\": \"Comp 1\",\n \"ddd\": 0,\n \"metadata"
},
{
"path": "assets/lotties/qr-code.json",
"chars": 29053,
"preview": "{\"assets\":[{\"h\":96,\"id\":\"0\",\"p\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIEAAABgCAYAAAA6uBF3AAAAAXNSR0IArs4c6QAAC"
},
{
"path": "assets/lotties/school-services.json",
"chars": 35040,
"preview": "{\"assets\":[{\"h\":84,\"id\":\"0\",\"p\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFQAAABUCAYAAAAcaxDBAAAACXBIWXMAABYlAAAWJ"
},
{
"path": "assets/lotties/search.json",
"chars": 29049,
"preview": "{\"assets\":[{\"h\":96,\"id\":\"0\",\"p\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIEAAABgCAYAAAA6uBF3AAAAAXNSR0IArs4c6QAAC"
},
{
"path": "assets/lotties/self.json",
"chars": 14089,
"preview": "{\"assets\":[{\"id\":\"7\",\"layers\":[{\"ind\":6,\"ty\":4,\"ks\":{},\"ip\":0,\"op\":61,\"st\":0,\"shapes\":[{\"ty\":\"rc\",\"p\":{\"a\":0,\"k\":[58,61]"
},
{
"path": "assets/lotties/turboself.json",
"chars": 29087,
"preview": "{\"assets\":[{\"h\":237,\"id\":\"0\",\"p\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAO0AAADtCAYAAABTTfKPAAAQAElEQVR4AexdCZwU"
},
{
"path": "assets/lotties/uni-services.json",
"chars": 50262,
"preview": "{\"assets\":[{\"h\":112,\"id\":\"0\",\"p\":\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAHAAAABwCAYAAADG4PRLAAAACXBIWXMAAAsTAAAL"
},
{
"path": "babel.config.js",
"chars": 168,
"preview": "module.exports = function (api) {\n api.cache(true);\n return {\n presets: ['babel-preset-expo'],\n plugins: [\n "
},
{
"path": "components/ActivityIndicator.tsx",
"chars": 1734,
"preview": "import React, { useEffect } from 'react';\nimport { ViewStyle } from 'react-native';\nimport Animated, {\n useAnimatedStyl"
},
{
"path": "components/AndroidHeaderBackground.tsx",
"chars": 503,
"preview": "import { useTheme } from \"@react-navigation/native\";\nimport { Platform, PlatformColor, View } from \"react-native\";\n\nexpo"
},
{
"path": "components/AppColorsSelector.tsx",
"chars": 4736,
"preview": "import React, { useState, useEffect, useMemo, useCallback } from \"react\";\nimport { FlatList, View } from \"react-native\";"
},
{
"path": "components/AppProviders.tsx",
"chars": 2598,
"preview": "import { ThemeProvider } from '@react-navigation/native';\nimport * as SystemUI from 'expo-system-ui';\nimport React, { us"
},
{
"path": "components/DevModeNotice.tsx",
"chars": 1272,
"preview": "import { useTheme } from \"@react-navigation/native\";\r\nimport { Code } from \"lucide-react-native\";\r\nimport React, { memo "
},
{
"path": "components/FakeSplash.tsx",
"chars": 1486,
"preview": "import { SplashScreen } from \"expo-router\";\nimport { VideoSource } from 'expo-video';\nimport React from \"react\";\nimport "
},
{
"path": "components/Log/LogIcon.tsx",
"chars": 998,
"preview": "import { AlertCircle, Info, TriangleAlert } from 'lucide-react-native';\r\nimport React from 'react';\r\nimport { View } fro"
},
{
"path": "components/ModalOverhead.tsx",
"chars": 2969,
"preview": "import Icon from \"@/ui/components/Icon\";\nimport Stack from \"@/ui/components/Stack\";\nimport Typography, { Variant } from "
},
{
"path": "components/RootNavigator.tsx",
"chars": 7441,
"preview": "import { useTheme } from '@react-navigation/native';\nimport { Stack } from 'expo-router';\nimport { t } from 'i18next';\ni"
},
{
"path": "components/SettingsHeader.tsx",
"chars": 3042,
"preview": "import { Papicons } from \"@getpapillon/papicons\"\r\nimport { useTheme } from \"@react-navigation/native\"\r\nimport { Image, I"
},
{
"path": "components/Transit.tsx",
"chars": 13299,
"preview": "import { Cross, Papicons } from \"@getpapillon/papicons\";\nimport { useTheme } from \"@react-navigation/native\";\nimport * a"
},
{
"path": "components/UnderConstructionNotice.tsx",
"chars": 1211,
"preview": "import { AlertTriangle } from \"lucide-react-native\";\r\nimport React, { memo } from \"react\";\r\nimport { useTranslation } fr"
},
{
"path": "components/onboarding/OnboardingBackButton.tsx",
"chars": 914,
"preview": "import { useRouter } from \"expo-router\";\nimport { Papicons } from \"@getpapillon/papicons\";\nimport AnimatedPressable from"
},
{
"path": "components/onboarding/OnboardingInput.tsx",
"chars": 1798,
"preview": "import { Papicons } from \"@getpapillon/papicons\";\nimport { useTheme } from \"@react-navigation/native\";\nimport React from"
},
{
"path": "components/onboarding/OnboardingScrollingFlatList.tsx",
"chars": 4855,
"preview": "import { useFocusEffect } from \"expo-router\";\nimport LottieView from \"lottie-react-native\";\nimport React from \"react\";\ni"
},
{
"path": "components/onboarding/OnboardingWebview.tsx",
"chars": 5294,
"preview": "import { useTheme } from \"@react-navigation/native\";\nimport { t } from \"i18next\";\nimport React, { useEffect } from \"reac"
},
{
"path": "components/router/BottomTabs.tsx",
"chars": 555,
"preview": "import {\n createNativeBottomTabNavigator,\n NativeBottomTabNavigationEventMap,\n NativeBottomTabNavigationOptions,\n} fr"
},
{
"path": "constants/AvailableTransportServices.ts",
"chars": 1990,
"preview": "import { Platform } from \"react-native\";\n\nimport { TransportAddress } from \"@/stores/account/types\";\n\nexport const Avail"
},
{
"path": "constants/LayoutScreenOptions.ts",
"chars": 1966,
"preview": "import { Platform } from 'react-native';\nimport \"@/utils/i18n\";\nimport { t } from 'i18next';\nimport { runsIOS26 } from '"
},
{
"path": "constants/UnicodeEmojis.ts",
"chars": 35838,
"preview": "export const UnicodeEmojis = {\n\tsmileys_and_emotion: { \n\t\ticon: \"Emoji\",\n\t\temojis: [0x1F600, 0x1F603, 0x1F604, 0x1F601, "
},
{
"path": "crowdin.yml",
"chars": 86,
"preview": "files:\n - source: /locales/fr.json\n translation: /locales/%two_letters_code%.json\n"
},
{
"path": "database/DatabaseProvider.tsx",
"chars": 4937,
"preview": "import { Database, Q } from \"@nozbe/watermelondb\";\nimport React, { createContext, useContext } from 'react';\n\nimport { e"
},
{
"path": "database/index.ts",
"chars": 1287,
"preview": "import { Database } from '@nozbe/watermelondb';\nimport SQLiteAdapter from '@nozbe/watermelondb/adapters/sqlite';\n\nimport"
},
{
"path": "database/mappers/attendance.ts",
"chars": 2045,
"preview": "import { Absence, Attendance, Delay, Observation, Punishment } from \"@/database/models/Attendance\";\nimport { Absence as "
},
{
"path": "database/mappers/balances.ts",
"chars": 438,
"preview": "import { Balance as SharedBalance } from \"@/services/shared/balance\";\n\nimport { Balance } from \"../models/Balance\";\n\nexp"
},
{
"path": "database/mappers/canteen.ts",
"chars": 794,
"preview": "import CanteenMenu from \"@/database/models/CanteenMenu\";\nimport { CanteenMenu as SharedCanteenMenu, CanteenHistoryItem a"
},
{
"path": "database/mappers/chats.ts",
"chars": 1108,
"preview": "import { Attachment } from \"@/services/shared/attachment\";\nimport { Chat as SharedChat, Message as SharedMessage,Recipie"
},
{
"path": "database/mappers/course.ts",
"chars": 677,
"preview": "import { Course as SharedCourse } from \"@/services/shared/timetable\";\n\nimport Course from \"../models/Timetable\";\n\nexport"
},
{
"path": "database/mappers/grade.ts",
"chars": 1591,
"preview": "import { mapSubjectToShared } from \"@/database/mappers/subject\";\nimport { Grade, Period, PeriodGrades } from \"@/database"
}
]
// ... and 317 more files (download for full content)
About this extraction
This page contains the full source code of the PapillonApp/Papillon GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 517 files (3.7 MB), approximately 984.7k tokens, and a symbol index with 980 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.