1
0
forked from firka/firka

353 Commits

Author SHA1 Message Date
1ef757d10f merge upstream 2026-03-03 23:46:10 +01:00
444abb83c2 Remove fmb_dart subproject from vendor directory 2026-03-03 23:44:24 +01:00
e835dcf6b1 firka: fix app icon changing
Closes: #9
2026-03-03 23:26:02 +01:00
e61a19fbbf Merge branch 'dev' of https://git.firka.app/devbeni/firka into dev 2026-03-03 23:23:30 +01:00
a937b854cd Update localization files and enhance build script for native assets handling 2026-03-03 23:22:43 +01:00
6d8f17ac00 Update object version and set development team for RunnerTests configuration 2026-03-03 23:11:25 +01:00
78dd4239cc merge upstream 2026-03-03 23:10:56 +01:00
39c5ca357e firka: remove package: 'firka'
Closes: #5
2026-03-03 23:08:45 +01:00
8249dbf03e remove dependency on brotli 2026-03-03 23:05:40 +01:00
c4e30ee4a6 Refactor project settings to improve folder exception handling and update object version 2026-03-03 22:31:16 +01:00
1291d20e55 Update app group identifiers to unify naming convention across the project 2026-03-03 22:14:04 +01:00
fb8d57c0ee Refactor entitlements and project settings to unify app group identifiers and update team identifiers 2026-03-03 22:11:16 +01:00
e79de0326c bump version codes for firka and firka_wear 2026-03-03 21:43:20 +01:00
de335af3c1 add build script 2026-03-03 20:29:56 +01:00
4be0bcd813 bump version to 1.1.0 and version code to 1100 2026-03-03 20:28:31 +01:00
zypherift
b3f46d8e84 Merge branch 'dev' of https://git.firka.app/firka/firka into dev 2026-03-03 20:17:38 +01:00
zypherift
e97246ee55 more vibration 2026-03-03 20:17:36 +01:00
zypherift
159fb73919 codegen 2026-03-03 20:17:01 +01:00
7c35a5675c Revert "accidentaly deleted"
This reverts commit 4c9eca217e.
2026-03-03 20:14:53 +01:00
45adddb7f7 add nix devshell 2026-03-03 20:11:10 +01:00
zypherift
4aad2bb292 l10n 2026-03-03 20:05:53 +01:00
zypherift
4c9eca217e accidentaly deleted 2026-03-03 20:05:46 +01:00
zypherift
ec6700e1cb Merge branch 'dev' of https://git.firka.app/firka/firka into dev 2026-03-03 20:00:42 +01:00
zypherift
b55595108f add assets to simulation 2026-03-03 20:00:24 +01:00
zypherift
8b3ab4a3a9 add colorwheel asset for new
loginscreen
2026-03-03 20:00:08 +01:00
zypherift
046b7926c4 nightmare: add simulation from scratch for collision and movement 2026-03-03 19:59:49 +01:00
zypherift
5626466107 disable drag bc login is fullscreen 2026-03-03 19:55:15 +01:00
zypherift
6c674bd596 finish webview 2026-03-03 19:53:34 +01:00
zypherift
9465a2b2a7 begin changing webview 2026-03-03 19:53:05 +01:00
484d8cf4cb codegen: add lock files 2026-03-03 18:40:59 +01:00
863f9c8077 firka_wear: add codegen script 2026-03-03 18:15:52 +01:00
a8983074dd firka: show weighted avg when adding ghost grades 2026-03-03 18:09:35 +01:00
e031c18ecb firka: fix Live Activity registration with fallback on resume and delayed retry 2026-03-03 17:37:39 +01:00
ba075c3b14 firka: add a section for ghost grades 2026-03-03 17:34:44 +01:00
32936c2aa5 firka: extract firka_common package with shared widgets (Isar kept separate)
- Create firka_common package with core helpers (debug, json, icon), theme,
  and shared widgets (FirkaCard, FirkaShadow, GradeWidget, GradeSmallCard,
  ClassIconWidget, FirkaIconWidget, DelayedSpinnerWidget, CounterDigitWidget)
- Keep Isar models (GenericCacheModel, TimetableCacheModel, HomeworkCacheModel,
  DatedCacheEntry, util) in firka and firka_wear - not moved to firka_common
- Update firka and firka_wear to depend on firka_common for shared UI only
- Add configurable roundGrade thresholds for firka settings
- Add package param to FirkaIconWidget for app asset paths
2026-03-03 15:33:11 +01:00
zypherift
ad75a80805 change text 2026-03-03 15:30:02 +01:00
zypherift
0ce7db23de add new padding 2026-03-03 15:16:47 +01:00
zypherift
0317f47b88 change height to fullscreen 2026-03-03 15:14:53 +01:00
zypherift
5a2616bf71 l10n 2026-03-03 15:14:34 +01:00
zypherift
9753717a25 add webview file for later use 2026-03-03 15:14:22 +01:00
zypherift
cf4e27ad30 add route 2026-03-03 15:12:22 +01:00
d96c4b66bb firka: show ghost grades on the chart and in the grades list 2026-03-02 21:40:01 +01:00
68ddffd808 firka: add GradeChartWithInteraction helper, use on grades and subject screens 2026-03-02 21:21:23 +01:00
483c8de0c0 firka: add grade calculator bottom sheet from Figma 2026-03-02 21:16:29 +01:00
4850923305 firka: add grade/:subjectId settings modal 2026-03-02 20:56:11 +01:00
489a3a1d24 firka_wear: add rotary support 2026-03-02 20:34:02 +01:00
4b0bf5a22d firka_wear: add LessonCardSmall and dynamic lesson pages
Implements the small lesson card from Figma.
2026-03-02 20:25:13 +01:00
e07b0264b8 firka_wear: trim the next lesson too 2026-03-02 19:19:44 +01:00
32d8481217 firka_wear: put the "..." in the correct place 2026-03-02 19:18:22 +01:00
d0c3938510 firka_wear: extract home body to part, add vertical scrollable PageView with placeholders 2026-03-02 19:17:40 +01:00
636c2ea68d firka_wear: fix overflow if the lesson's room name or name is long 2026-03-02 18:22:55 +01:00
b986d8b660 firka_wear: centralize app state, add logging, remove unused routes, add Bloc for sync state 2026-03-02 15:48:11 +01:00
613db488b1 firka_wear: pass model number to phone when pairing 2026-03-02 15:07:51 +01:00
5d5c3c4c6f fix wearos pairing and syncing 2026-03-02 14:54:00 +01:00
9f36569d2a firka_wear: refactor lib folder structure to match firka 2026-03-01 15:13:51 +01:00
9fc73e3c5c firka_wear: use kreta_api for API models 2026-03-01 14:57:24 +01:00
bd53ba6c9b firka: use kreta_api for API models and types 2026-03-01 14:56:09 +01:00
8d95c71fae chore: add kreta_api shared package 2026-03-01 14:52:54 +01:00
befaa45cdf firka(android): use app icon for Wear sync foreground service notification 2026-03-01 14:39:27 +01:00
5570f73cb4 firka: extract startWearSyncServiceWithFreshCache for Wear OS 2026-03-01 14:31:23 +01:00
59b470a64c firka_wear: local sync store, 1h rule, remove KretaClient; data from phone only 2026-03-01 14:31:23 +01:00
4811519ced firka_wear: sync API models from firka (Lesson, Grade, Subject, generic) 2026-03-01 14:31:23 +01:00
3580dc2ef8 firka(android): Wear sync foreground service, getLocalizedString, no Android strings 2026-03-01 14:31:23 +01:00
eb1312398d firka: add Wear sync cache, payload, helper and background entrypoint 2026-03-01 14:31:23 +01:00
a70457528b firka(android): add Wear OS support toggle in settings 2026-03-01 14:31:23 +01:00
6bbdbadac4 firka: update l10n 2026-03-01 14:31:22 +01:00
911a1970e4 firka_wear: make it buildable again 2026-03-01 10:59:05 +01:00
65ab5caa69 firka: update l10n 2026-03-01 10:18:09 +01:00
a9624df915 firka: unify grade widget 2026-02-28 23:32:55 +01:00
52f6ebcfd3 firka: add grade summary bar with dropdown under subjects chart 2026-02-28 23:12:24 +01:00
635fdd5497 firka(android): lesson/room badge sizing, centered text, uniform room width 2026-02-28 22:31:46 +01:00
a11f118861 firka(android): timetable widget responsive layout, resize handling, centered lessons 2026-02-28 22:05:30 +01:00
5c205a9844 firka(android): widget refresh via broadcast + updateAll, Glance IO off main thread 2026-02-28 17:55:14 +01:00
f620bfe76c firka(android): fix widget refresh via GlanceAppWidget.updateAll 2026-02-28 16:04:29 +01:00
b99051dbfc firka: add Generate widget state for date to developer options 2026-02-28 15:51:05 +01:00
c58f34f499 firka: rewrite Android widget and fix refresh 2026-02-28 15:45:04 +01:00
1e7dceb995 firka: fix logout button to refresh app like login 2026-02-28 13:42:34 +01:00
022915378e firka: pop login sheet and navigate to home after OAuth 2026-02-28 13:26:16 +01:00
589e722310 firka: fix login webview not receiving touches 2026-02-28 12:59:51 +01:00
12cce27e9d firka: highlight current value on grade chart left axis 2026-02-28 12:06:18 +01:00
826312b503 firka: give chart higher gesture priority over page swipe 2026-02-28 11:18:24 +01:00
e071fc15d1 firka: stream home items in progressively, keep spinner until all fetch 2026-02-28 11:12:25 +01:00
94b819ffbd firka: add debug button to clear all cache 2026-02-28 11:04:06 +01:00
299a769f74 firka: migrate state management to Bloc
- Add flutter_bloc dependency
- Create ThemeCubit, SettingsCubit, ProfilePictureCubit, ReauthCubit, HomeRefreshCubit
- Replace UpdateNotifier/ValueNotifier with Bloc across app
- Remove update_notifier.dart and FirkaState globalUpdate listener
- Provide cubits via MultiBlocProvider at app root
2026-02-28 10:46:11 +01:00
4abf995fde firka: add haptic feedback to bottom nav icons 2026-02-28 10:04:04 +01:00
e261f73c30 switch to go_router 2026-02-28 09:03:01 +01:00
fc9907f33d firka: refactor file structure 2026-02-28 07:47:33 +01:00
2d2c2fbef9 chore: dart format + remove unused imports 2026-02-27 23:23:56 +01:00
Horváth Gergely
d5c3d02dfa Remove onAppOpened and related LiveActivity state
Remove the _lastActivityRecreation field and the onAppOpened(...) method from LiveActivityService, which previously handled ending/creating activities, refreshing push tokens, fetching the week's timetable and scheduling background fetch. Update home_screen to call LiveActivityService.checkAndUpdateTimetable(...) instead of onAppOpened, and tweak related log messages to clarify the behavior. This simplifies LiveActivity lifecycle on app resume and stops forced activity recreation/token refresh performed by the removed method.
2026-02-27 23:23:56 +01:00
Horváth Gergely
40a1e8f459 Improve Watch sync, token, and live activity handling
Multiple fixes and improvements for watch <> phone sync, token recovery, and live activity behavior:

- WatchSessionManager: add mergeApplicationContext to avoid clobbering app context, add thread-safe pending auth queue and flush, add sendMessageToWatch API, ensure message handling runs on main thread, add reply-timeout logic for language requests, support fire-and-forget messages, and improve enqueue/flush logic.
- WatchConnectivityManager & SettingsView: publish shared session state on force-logout/logout and improve account-switch/token handling; clear DataStore error and reset recovery state after token updates.
- DataStore & WatchL10n: add recovery-in-progress guard to avoid duplicate recovery runs, reset language version tracking on account switch, make WatchL10n.setLanguage main-thread safe.
- TokenManager & SharedKeychainManager: remove old keychain observer plumbing and instead publish shared session state when active token changes or is deleted.
- UI tweaks: reduce icon/text sizes and spacing in pairing view; only show sync button when paired; Settings logout now also publishes shared state.
- Watch sync wiring in Flutter: replace direct watch_connectivity usage with a MethodChannel-backed WatchSyncHelper.sendMessageToWatch and onWatchMessage callback; main and pairing UI updated accordingly.
- Kreta client: replace simple boolean mutex with a Completer-based mutex and timeout handling to avoid busy-waiting.
- LiveActivityService: throttle/avoid frequent activity recreation (cache last recreation time), skip placeholder creation when called from background, and minor cache-clearing adjustments.
- HomeScreen: add WidgetsBindingObserver to manage lifecycle, prevent prefetch while backgrounded, debounce prefetch, ensure LiveActivity registration runs once and refresh on resume after first prefetch.

These changes increase robustness of token sync and account switching, reduce race conditions and duplicate work, and avoid conflicts between WCSession and Flutter plugin delegates.
2026-02-27 23:23:56 +01:00
0c4bc4cd40 firka(android): fix splash screen 2026-02-27 23:23:56 +01:00
28fb054571 fix warnings, reformat files 2026-02-27 23:23:56 +01:00
b0c8f1f4b3 firka(android): optimize build time 2026-02-27 23:23:56 +01:00
2ea0549258 add codegen script 2026-02-27 23:23:56 +01:00
c0ea4fde7a update lib/l10n 2026-02-27 23:23:56 +01:00
d56422ba0d update pubspec and rebuild isar files 2026-02-27 23:23:56 +01:00
3fa00fa6b6 grafikon javítgatás? 2026-02-27 23:23:56 +01:00
ef14b9dc0e HW fix 2026-02-27 23:23:56 +01:00
Pearoo
9238869568 feat: add error page (+pubspec merge fix) 2026-02-27 23:23:56 +01:00
6f04f9e9b8 Tantárgy megjelenítése 2026-02-27 23:23:56 +01:00
b6c8bb3267 grade chart próbálkozgatás 2026-02-27 23:23:56 +01:00
5ef21222aa Hiba fix (-1-es óraszám) 2026-02-27 23:23:56 +01:00
c0a8c696a3 Összátlag 2026-02-27 23:23:56 +01:00
f4a7fe7923 showSubjectBottomSheetSettings belekezdés (?) 2026-02-27 23:23:56 +01:00
c2f3af7be1 Átlagszámláló segéd 2026-02-27 23:23:56 +01:00
3440ea2eef gradeDefault svg hozzáadása 2026-02-27 23:23:56 +01:00
34475f35b0 Update firka/lib/ui/phone/screens/home/home_screen.dart 2026-02-27 23:23:56 +01:00
091647dbe4 chart folytatás 2026-02-27 23:23:56 +01:00
36ca357392 chart megjelenítése 2026-02-27 23:23:56 +01:00
dbbc119fd5 Hiba fix: nincs középen az óraszám 2026-02-27 23:23:56 +01:00
448be1ae10 pici fix 2026-02-27 23:23:56 +01:00
23beb6e31f Extras menü átalakítása dizájn alapján 2026-02-27 23:23:56 +01:00
c122eb4ff9 asszem hiba fix 2026-02-27 23:23:56 +01:00
8568aa5678 Holnapi órák megjelenítése, amennyibe nem lesz dolgozat (Félkész) 2026-02-27 23:23:56 +01:00
12dea89cd4 HTML házinál 2026-02-27 23:23:56 +01:00
67965610ce Óraszámok megjelenítése 2026-02-27 23:23:56 +01:00
7cede065c3 Kéthetes órarend 2026-02-27 23:23:56 +01:00
ae4862a653 Holnapi dolgozat megjelenítése 2026-02-27 23:23:56 +01:00
30f493ddc4 Értékelésnél tantárgy megtekintése gomb 2026-02-27 23:23:56 +01:00
50eb96377e pici szerkesztés 2026-02-27 23:23:56 +01:00
61c795e362 Jegy dátum formázás 2026-02-27 23:23:56 +01:00
7c65a696aa Tantárgy dizájn alapján szerkesztés 2026-02-27 23:23:56 +01:00
a341833441 nyelv 2026-02-27 23:23:56 +01:00
8bf3cc72be Születésnap jelzés konfettivel 2026-02-27 23:23:56 +01:00
33028cdf38 tördelés fix 2026-02-27 23:23:56 +01:00
f01b9d4fd5 License rész javítás 2026-02-27 23:23:56 +01:00
41ff65d50b Dátum formázás javítása 2026-02-27 23:23:56 +01:00
ae9ef1603a added loading spinner, fade out, and matched the color theme to the login page 2026-02-27 23:23:56 +01:00
1d8d341f2d fordítás hozzáadása 2026-02-27 23:23:56 +01:00
633926bf2d Áthúzással jelzés, hogy kész a házi 2026-02-27 23:23:56 +01:00
a6344e42fa dátum formálása fix, showHomeworkBottomSheet hozzáadása 2026-02-27 23:23:56 +01:00
9cb2265a97 Adatbázisba mentés: kész házik 2026-02-27 23:23:56 +01:00
971a35b738 HomeworkDoneModelSchema hozzáadása 2026-02-27 23:23:56 +01:00
69ee75966d SVG hozzáadása 2026-02-27 23:23:56 +01:00
177bf3bf38 generált homework_cache_model_g.dart 2026-02-27 23:23:56 +01:00
5b794b199c Órarend szünet fix (2) 2026-02-27 23:23:56 +01:00
a000481cd9 Órarend fix: Szünetek megjelenítése 2026-02-27 23:23:56 +01:00
ae79a44df9 firka: make android buildable again 2026-02-27 23:23:56 +01:00
Horváth Gergely
70213e376c Refactor timetable day selection and parsing
Add robust date parsing and simplify lesson selection logic. Introduces ISO8601 formatters (with and without fractional seconds) and updates parseNextSchoolDayDate to try both variants, plus existing yyyy-MM-dd fallback. Adds LessonCandidate, startOfDay and nextSchoolDay helpers and builds a sorted candidate list (today, tomorrow, next school day) to pick the appropriate lesson set and correctly set isNextDay/isNextSchoolDay flags. Cleans up previous branching-heavy logic and improves handling of edge cases (lessons finished, next-school-day resolution).
2026-02-27 23:23:56 +01:00
Horváth Gergely
146124228a Improve watch UI and robust language sync
Several watch UI and sync improvements: clamp CountdownRing values and use clamped values for progress/color/display; add optional backgroundColor to FirkaCard and refactor HomeView to use lessonTitleWithStatus, lessonCardBackgroundColor and improved break time calculation and refresh handling (onChange of lastUpdated). TimetableView now shows status icons/colors via helper methods. DataStore now returns a localized "time_now" for recent updates. WatchSessionManager handles Flutter channel/unready states by serving shared language state when available and avoids empty language replies. Dart fixes: await initLang in settings, return null from WatchSyncHelper when uninitialized, and attempt to publish language to watch after initialization in main.
2026-02-27 23:23:56 +01:00
Horváth Gergely
9a99a6869a Use zip to iterate adjacent lessons
Replace index-based loops with zip(entry.lessons, entry.lessons.dropFirst()) in TimetableMediumView and TimetableLargeView to iterate adjacent lesson pairs. This improves readability and safety (avoids manual index arithmetic and potential out-of-bounds issues) while keeping the same break-detection logic.
2026-02-27 23:23:56 +01:00
Horváth Gergely
61953b68d2 Add shared session/language state & refresh leases
Introduce shared session and language state plus cross-device refresh leases to improve Watch/iPhone sync. Adds SharedSessionStateManager, SharedLanguageStateManager and RefreshLeaseManager (keychain-backed) and exposes accessGroup via SharedKeychainManager. Wire shared state into WatchSessionManager (publish/load language and session state, immediate token send via message + userInfo + applicationContext, reachability check, lease-related Flutter handlers) and into WatchConnectivityManager (parse versions, apply immediate token updates). Update DataStore and WatchL10n to reconcile shared session/language state, handle stale account switching, and request tokens from phone when needed. Add TokenManager watch-side lease wrapper for refresh coordination, UI tweaks for pairing/no-token messages and icons, small fixes (date parsing in widget provider, background refresh cadence to ~15 min, time-since localization keys and usage), and various helper utilities for parsing int64 and state versioning.
2026-02-27 23:23:56 +01:00
Horváth Gergely
69dde9281d Send language to Watch and handle locale changes
iOS: Update WatchSessionManager to send the current language via WCSession.default.updateApplicationContext with logging and error handling, in addition to existing transferUserInfo. Dart: await initLang during startup and add dispatcher.onLocaleChanged handler that re-initializes language when the app is in auto-language mode, logs locale changes, and triggers a global UI update.
2026-02-27 23:23:56 +01:00
Horváth Gergely
86c7641c60 Migrate token storage to shared Keychain
Add SharedKeychainManager and migrate token storage from the old iCloud Key-Value (KV) approach to a synchronized Keychain-backed solution. Replace iCloudTokenManager usages across WatchSessionManager, TokenManager, and WatchConnectivityManager, update saveToken API to syncToSharedKeychain, and add KV-store migration logic that moves existing KV entries into the shared Keychain and clears the old KV store. Update entitlements (add keychain-access-groups) for both Runner and the Watch app, add/remove files in the Xcode project, delete iCloudTokenManager.swift. Also include access-group resolution, logging, and compatibility observer methods in the new manager.
2026-02-27 23:23:56 +01:00
Horváth Gergely
71f1412164 Improve token recovery and iCloud probing
Track and surface classified token recovery failures and add a timed iCloud probe to accelerate recovery. TokenManager: introduce lastRecoveryFailure, clearLastRecoveryFailure(), and iCloudProbeTimeoutNs; add probeICloudTokenWithTimeout() and attempt an early iCloud-probe apply before refresh flow; record TokenError results from refresh attempts and abort recovery early on network errors; clear failure on successful actions; default lastRecoveryFailure to .noToken when all attempts fail. KretaAPIClient: clear failure on valid token/recovery success and throw a classified APIError when recovery produced a known TokenError. Overall: better diagnostics, faster iCloud-based short-circuiting, and more robust recovery error handling.
2026-02-27 23:23:56 +01:00
Horváth Gergely
7c344be550 Add robust Apple Watch token sync & force-logout
Improve Apple Watch sync resilience and token safety across iOS and Flutter. Key changes:

- Watch (watchOS): rate-limit phone token requests, handle "force_logout" via applicationContext/userInfo and perform local token deletion/cleanup. Added cooldown tracking.
- iPhone host (Swift): expose Flutter methods to check watch app installation, clear iCloud token, and send force-logout to the watch; refuse to forward expired tokens and avoid using iCloud fallback when Flutter reports reauth needed.
- Flutter (WatchSyncHelper/KretaClient): cache and check whether a paired Watch app is installed before touching iCloud/watch; reject expired access tokens (both incoming and outgoing) and prevent sending expired tokens to watch; added fresh-install cleanup to clear iCloud/local state once per install; added methods to notify watch of forced logout and to clear iCloud token.
- Initialization: run fresh-install cleanup before attempting iCloud recovery and skip recovery if cleanup ran.
- Login/settings/home UI: only attempt watch sync when a watch is installed; clear iCloud token on account removal (iOS); minor UX/timing and formatting cleanups.

These changes prevent propagation of expired tokens, reduce redundant phone/watch messaging, and provide a controlled force-logout flow for account removal or fresh installs.
2026-02-27 23:23:56 +01:00
Horváth Gergely
e620f3e564 Add force account-switch and iCloud recovery
Introduce forced account-switch handling and improve token selection/recovery logic.

- WatchConnectivityManager & ReauthRequiredView: parse sentAtMs robustly and compute a shouldForceAccountSwitch flag when an incoming token is for a different account; pass forceAccountSwitch to TokenManager.saveToken and include it in logs.
- TokenManager: add localTokenFromKeychainAndFile helper, prefer the active account when selecting a local token, and refine loadToken to prefer active-account tokens but fall back to the freshest available. saveToken now accepts forceAccountSwitch and will persist a token even if it switches accounts when requested. Adjust iCloud handling to skip iCloud token only if a local active-account token exists; improve diagnostic logging.
- WatchToken: enhance isNewer comparison to consider effectiveUpdatedAt and tokenVersion before falling back to expiryDate.
- HomeScreen (phone): import watch_sync_helper and run a secondary iCloud recovery pass on startup (with a short delay) to apply a fresher iCloud token to the app state if needed.

These changes aim to make cross-device token sync more robust, correctly handle account switches triggered from the watch, and recover stale auth state from iCloud on app startup.
2026-02-27 23:23:56 +01:00
B3ni
2719e6bf77 Update main.dart 2026-02-27 23:23:56 +01:00
Horváth Gergely
55de7d1645 Improve iCloud / Watch token sync & recovery
Add robust handling for token synchronization and recovery across iOS/watchOS and Flutter.

- iOS (WatchSessionManager.swift): queue token events until Flutter signals watchSyncReady, flush queued auth events and iCloud recovery notifications when ready, forward tokens with de-duplication logic, provide fallback to iCloud when Flutter isn't ready, and avoid sending duplicate events. Adds helper methods for token payloads and comparison.
- Token management (TokenManager.swift): track active studentIdNorm in UserDefaults to prefer a single account, persist active account on saves, prefer freshest token within preferred account, improve proactive refresh with configurable lead time and cooldown, skip unnecessary recoveries when a valid token exists, and add cooldown for phone recovery requests.
- iCloud logic (iCloudTokenManager.swift): avoid overwriting tokens across accounts using updatedAt comparisons and only ignore truly stale saves.
- Dart client (kreta_client.dart): skip recovery if local token still valid, clearer iCloud recovery retry flow, fallbacks, and clear reauth flag when usable token applied.
- Token model (token_model.dart & generated .g.dart): add tokenVersion and updatedAtMs fields, populate them on creation/from response, and update Isar schema + generated query helpers.
- Watch sync helper (watch_sync_helper.dart): include tokenVersion/updatedAt in outgoing payloads, resolve incoming/current versions more robustly, use updatedAt when deciding freshness, and invoke watchSyncReady on init so native side can flush queued events.
- App init (main.dart): run an iCloud recovery check on iOS during startup and only clear reauth flag when the chosen token is still in the future.

These changes improve cross-device token consistency, reduce spurious reauth prompts, and make proactive refresh/recovery less noisy and more efficient.
2026-02-27 23:23:56 +01:00
Horváth Gergely
0b78712e64 Token recovery and sync refactor; LiveActivity UI
Centralize and harden token recovery and synchronization across watch/phone components and update LiveActivity UI for iOS 18.

Highlights:
- Introduced a centralized recovery flow in TokenManager.recoverToken() with locking, multi-step recovery (local refresh, keychain/file/watch, iCloud retries) and refreshTokenInternal helper. Added proactive refresh and improved refresh logic to include tokenVersion/updatedAtMs metadata.
- Added tokenVersion and updatedAtMs to WatchToken and propagated these fields through iCloud, WatchConnectivity, WatchSessionManager, and ReauthRequiredView payloads. Many save/load paths now avoid unnecessary iCloud sync and ignore stale tokens based on version/timestamps.
- WatchConnectivityManager now filters stale incoming updates using sentAtMs and persists lastAppliedTokenUpdateMs to avoid regressions; payloads include updatedAtMs/tokenVersion when available.
- DataStore and KretaAPIClient use the centralized recovery API instead of ad-hoc retry logic or direct WatchConnectivity requests.
- iCloudTokenManager stores tokenVersion/updatedAtMs, ignores stale saves, and uses consistent timestamps.
- LiveActivityWidget and LiveActivity updated: iOS 18+ uses new adaptive/family-aware views (including a new SmallActivityView) while legacy behavior is preserved for iOS 16.2–17.x.

These changes aim to make token synchronization more reliable across devices and OS versions, reduce duplicate/conflicting writes, and provide a more adaptive live activity UI on newer iOS releases.
2026-02-27 23:23:56 +01:00
Horváth Gergely
6c67d22fb8 Add active account handling & token recovery
Introduce active account selection and robust token recovery across watch and phone code. Adds a new Dart helper (active_account_helper.dart) to resolve the active account/token and updates KretaClient, token refresh/grant flows, live activity, watch sync, main init and home UI to use pickActiveToken. On the watch side, implement refreshAllWithRecovery in DataStore and replace direct refreshAll calls with refreshAllWithRecovery everywhere (ContentView, BackgroundRefreshManager, WatchConnectivityManager, HomeView). Rewrite the token recovery logic to retry (with delays), prefer fresher iCloud/file/keychain tokens, attempt iPhone retrieval, and better distinguish transient network errors from permanent token invalidation. Also update TokenManager to choose the freshest local token between keychain and file, and add small sync/NULL-safety fixes and delay tweaks in home_screen to improve iCloud recovery behavior. These changes aim to avoid unnecessary reauth prompts, handle intermittent network issues, and support multi-account setups by always using the active token.
2026-02-27 23:23:56 +01:00
Horváth Gergely
5403d1324d Handle iCloud token recovery and notify Flutter
Add end-to-end handling for tokens recovered from iCloud: TokenManager now detects fresh iCloud tokens and posts a "TokenRecoveredFromiCloud" Notification on iOS; WatchSessionManager observes that notification and invokes the Flutter method "onTokenRecoveredFromiCloud". On the Dart side, WatchSyncHelper handles the new callback and runs checkAndRecoverFromiCloud (clearing the reauth flag when appropriate). HomeScreen now attempts proactive iCloud recovery on iOS during startup when the token is expired/expiring or reauth is required, retrying with incremental delays and updating the client/token cache on success. Also minor log text tweak in main.dart and necessary imports added to home_screen.dart.
2026-02-27 23:23:56 +01:00
Horváth Gergely
a071cafacd Add auto-refresh and duplicate-call guard
Watch app: add Combine import, scenePhase tracking, a periodic timer and a 10-minute freshness threshold to auto-refresh data when the app becomes active or data becomes stale. Move refresh logic into onChange/onReceive handlers and add shouldAutoRefresh to avoid unnecessary refreshes; removed an immediate proactive token refresh in ContentView. DataStore.refreshAll now early-returns if a refresh is already in progress and logs the skip to prevent duplicate concurrent refreshes.
2026-02-27 23:23:56 +01:00
Horváth Gergely
dbf0d18e5c Handle percentage grades and token recovery
Add support for percentage-style grades and normalize them to standard grade scale across the watch app and shared models; show raw percentage in grade badge UI while keeping normalized coloring. Update WidgetGrade/KretaGrade to include valueType, add isPercentageGrade, percentageToGrade, normalizedNumericValue, displayTypeWithWeight and displayGradeValue helpers, and use normalized values in DataStore and views. Improve HomeView refresh button to reflect background loading, disable during datastore loading, and auto-transition success/failure states when background sync finishes. Enhance token recovery logic in Dart: KretaClient attempts watch/iCloud recovery twice (with a short delay) before forcing reauth, and token_grant tries to recover a fresher token from iCloud via WatchSyncHelper on 401 responses before throwing token errors.
2026-02-27 23:23:56 +01:00
Horváth Gergely
12e3fa5bff Add iCloud token sync & improve recovery
Add iCloud-based token recovery and saving across iOS/watch components. Introduces WatchSyncHelper.checkAndRecoverFromiCloud and saveTokenToiCloud to fetch/save fresher tokens via a method channel, calls the check on app init and before proactive refresh, and attempts to save refreshed tokens to iCloud. Adds native handlers in WatchSessionManager (checkiCloudToken, saveTokeToniCloud) and updates DataStore token recovery to distinguish network errors vs permanently invalid tokens (avoid unnecessary reauth). Update logging messages accordingly. Also update ubiquity-kvstore identifiers in entitlements to use explicit group.app.firka.firkaa and bump iOS build/version (CURRENT_PROJECT_VERSION and CFBundleVersion) to 1068.
2026-02-27 23:23:56 +01:00
Horváth Gergely
72cfa9b5eb Add splash assets and watch token recovery
Add Android and iOS launch/splash assets and flutter_native_splash config, and update launch_background.xml/styles for Android 12 support and dark mode.

Implement robust Apple Watch token recovery and refresh behavior: add recovery state and async recovery flow to DataStore (iCloud check, API refresh, request from iPhone), show a recovering UI in ContentView, attempt proactive recoveries on startup, and add WatchToken/iCloudTokenManager model support. WatchConnectivityManager now attempts to refresh expired tokens before replying to iPhone requests. BackgroundRefreshManager scheduling was improved to choose intervals based on timetable and user settings with debug logs. SettingsView defaults to Auto (0) and adds the Auto picker option and localization strings.

Also update entitlements across watch/widget extensions to include com.apple.developer.ubiquity-kvstore-identifier, rename watch app icon entry, and add project.pbxproj references for new source files.
2026-02-27 23:23:56 +01:00
Horváth Gergely
51831e94e4 Enhance reauthentication process with token recovery from Apple Watch 2026-02-27 23:23:56 +01:00
Horváth Gergely
aa8b3d5e16 Improve watch sync error handling and reauthentication logic 2026-02-27 23:23:56 +01:00
Horváth Gergely
b83cbef7fc Fix typo in import statement
Corrects a misspelled 'mport SwiftUI' to 'import SwiftUI' in LessonCard.swift, restoring the proper SwiftUI import and preventing compilation errors.
2026-02-27 23:23:56 +01:00
Horváth Gergely
35e1e2c6ab Add Apple Watch app and watch sync support
Add a new Firka Watch app target with UI components, views and services: DataStore, BackgroundRefreshManager, WatchConnectivity/WatchSession integration, localization (WatchL10n), entitlements and assets. Move widget/shared models into ios/Shared and update Xcode project/schemes; add native Kreta API client and TokenManager for watch use. Implement watch-side caching, proactive token refresh, background scheduling, and pairing/pairing UI. Update Flutter side (watch_sync_helper, main, API/token helper changes) and tweak iOS .gitignore and project metadata to enable the watch integration and data sync between phone and watch.
2026-02-27 23:23:56 +01:00
Horváth Gergely
874f5d4297 Add Shortcuts intents, entities & localizations
Introduce App Shortcuts and AppIntents support: add multiple shortcut intents (next/closest lesson, today/tomorrow/closest timetable, recent grades, subject/overall averages), AppShortcuts provider, and ShortcutError. Add AppEntity types (Average, Grade, Lesson, Subject) and corresponding entity queries. Refactor Controls to use AppIntent-based navigation (OpenHome/OpenGrades/OpenTimetable) with localized labels/descriptions and remove duplicate SubjectEntity from AveragesIntent. Update TimetableProvider to use a lock-screen-friendly timeline refresh policy. Add localization resources (AppShortcuts.strings and expanded Localizable.strings) for en/de/hu and register new strings in project files; update Info.plist/project.pbxproj accordingly.
2026-02-27 23:23:56 +01:00
Horváth Gergely
71ef509021 Show active break in timetable widget
Add break detection and UI for active breaks in timetable widgets. Introduces a new localization key "break_between" and a BreakIndicatorRow view that displays a break marker and minutes left. TimetableMediumView and TimetableLargeView now detect when the current time falls between consecutive lessons, reduce the number of visible lessons accordingly, and inject a BreakIndicatorRow between lessons when in a break (minutes are calculated using ceil of time interval). Also clean up commented/debug lines in the iOS widget cache helper (widget.dart) for locale/theme and removed a TODO comment.
2026-02-27 23:23:56 +01:00
Horváth Gergely
16b7d2f70a Add next-school-day support to iOS widgets
Add support for showing the next school day in widgets. Changes include: new localization keys (next_school_day_timetable, no_lessons_ahead) and a formatShortDate helper; updated Hungarian hours abbreviation. Widget model updated (TimetableData) to include nextSchoolDay and nextSchoolDayDate. TimetableProvider now sets isNextSchoolDay/nextSchoolDayDate and returns an entry when a future school day is found. Views (inline, lock screen, small/medium/large) updated to display the next school day header/date and show no_lessons_ahead where appropriate. Flutter helpers (widget cache/IOS helper) extended to serialize nextSchoolDayLessons/nextSchoolDayDate and to search up to a week ahead for the next school day when tomorrow is empty.
2026-02-27 23:23:56 +01:00
Horváth Gergely
402067d624 Add logging, await token callback and widget refresh
Fixes async token handling and improves observability: await the callback in KretaClient to ensure proper async flow, add informative logs for token expiry/refresh and warn on empty 200/201 API responses. Enhance token refresh flow in token_grant with detailed info/warning/severe logs and exception logging for easier debugging. Update widget DB helper to force fresh fetches for timetables/grades when updating widget cache, avoid writing empty caches and add debug prints for counts and cache status. Wire widget refresh into LiveActivityService background fetch (with import and try/catch logging) so iOS widgets get refreshed during background processing.
2026-02-27 23:23:56 +01:00
Horváth Gergely
f76b5fbcca Add iOS Control widgets & timetable UI updates
Introduce iOS 18 Control widgets and improve timetable/widget UI and behavior.

- Add Home, Grades and Timetable Control widgets (AppIntents) that write a "controlNavigation" value into the app group (group.app.firka.firkaa) so the main app can open the requested page.
- Register control widgets in HomeWidgetsBundle for iOS 18+.
- Extend WidgetLocalization with short/abbrev strings (tomorrow_short, hours_abbrev, in_hours) and related localization keys.
- Enhance Lock Screen and Timetable widgets to handle next-day lessons, show hours when >60 minutes, and use localized hour/minute strings.
- Update TimetableProvider timeline generation to avoid producing per-minute entries far before the next lesson; if the next lesson is >60 minutes away, add only a single entry ~60 minutes before it.
- Adjust various widget views (Averages, Grades, Timetable) for more compact layout: spacing, font weights/sizes, compact modes, and added average color logic.
- Update project.pbxproj to add a file-system-synchronized exception for the HomeWidgetsExtension files in the Runner target.
- AppDelegate: read and remove "controlNavigation" from the shared app group and return it via the Flutter method channel as a pending deep link; fall back to existing pendingWidgetDeepLink.
- Flutter: map a received 'home' deep link to HomePage.home in the home screen navigation switch.

These changes add Control Center / Lock Screen launch shortcuts and refine widget presentation and timeline performance.
2026-02-27 23:23:56 +01:00
Horváth Gergely
d395529282 Add lock-screen/inline widgets & timeline updates
Introduce Lock Screen and Inline variants for Timetable, Grades and Averages widgets and register them in HomeWidgetsBundle. Add new views and widget configurations (accessoryInline, accessoryCircular, accessoryRectangular) under LockScreen/, plus grade badge and average display UI. Extend Localization with many new keys in Helpers/Localization.swift and add lock-screen descriptions in en/de/hu Localizable.strings. Update TimetableProvider timeline logic to generate more frequent (per-minute) update entries for lock-screen families, deduplicate timeline dates and ensure proper midnight transition. Adjust TimetableViews display logic to prefer next lesson when current is absent and fix displayed label selection. Apply project.pbxproj updates to file-exception/group entries and some build phase metadata.
2026-02-27 23:23:56 +01:00
Horváth Gergely
1038b49092 iOS: rename LiveActivity widget & update configs
Rename TimetableWidget to LiveActivityWidget and clean up HomeWidgetsExtension targets/files. Update entitlements (group id and formatting) and remove duplicate HomeWidgetsExtensionExtension entitlements file. Update Runner.xcodeproj: bump objectVersion, rename targets/products/references, add WidgetExtension.xcconfig, switch entitlements/infoplist paths, set live-activity related flags, adjust deployment targets/locales and MARKETING_VERSION/CURRENT_PROJECT_VERSION to use Flutter variables. Change app display name to "Firka Testing" and move CFBundleVersion in Info.plist. Also change default widget style to .liquidGlass for Averages, Grades and Timetable intents and fix null-safety when serializing subject/grade category fields in ios_widget_helper.dart.
2026-02-27 23:23:56 +01:00
Horváth Gergely
d06a3dee69 Improve iOS widgets: UI, timeline and JSON fixes
Multiple widget improvements and bug fixes:

- Models: WidgetGrade now exposes subjectNameWithWeight and teacherName for display.
- AveragesProvider: added isFiltered flag, improved subject filtering logic and propagated flag to entries.
- Averages views: show filtered count badge, use configurable max visible items, and respect isFiltered.
- Grades views: display subject name with weight, show teacher in grade rows (renamed flag to showTeacher) and preserve topic display.
- TimetableProvider: build unique transition times for timeline updates, sort entries, and determine current lesson using the provided date instead of Date().
- Timetable views: compute visibleLessons window around the current lesson based on entry.date and use isLessonActive checks against entry.date.
- iOS widget JSON helper: fix subject.category serialization structure, use grade.creationDate for recordDate, and use grade.teacher for teacherName.

These changes fix incorrect JSON exports, improve timeline accuracy, and enhance widget UX when subjects are filtered or when showing teacher/weight info.
2026-02-27 23:23:56 +01:00
Horváth Gergely
d617efec80 Add deep link handling for widgets; implement navigation from widget links 2026-02-27 23:23:56 +01:00
Horváth Gergely
beb4127ef8 Add iOS Home Widgets Extension with timetable, grades, and averages
Introduces a new HomeWidgetsExtension for iOS, including widgets for timetable, recent grades, and subject averages. Adds widget models, providers, intents, localization, and SwiftUI views. Updates project files and main app to support widget data sharing and communication. Also includes new Dart helper for widget data and updates to relevant Flutter screens.
2026-02-27 23:23:56 +01:00
Horváth Gergely
1f57281004 Add tokenExpired property and update warning display logic in live activity 2026-02-27 23:23:56 +01:00
Horváth Gergely
f866052905 Add user-specific bell delay settings; implement retrieval and storage in SharedPreferences 2026-02-27 23:23:56 +01:00
Horváth Gergely
deb013a30e Add morning notification settings; implement user-specific preferences for time and enabled state 2026-02-27 23:23:56 +01:00
Horváth Gergely
8a3f77d565 Implement token expiration handling and reauthentication UI; add reauth toast and update seasonal icon logic 2026-02-27 23:23:56 +01:00
Horváth Gergely
1ea46ede0a Refactor logging in live activity manager and backend client; streamline lesson data handling and improve break event detection logic 2026-02-27 23:23:56 +01:00
Horváth Gergely
79f59b6938 Refactor lesson filtering and add logic to handle break events in live activity manager 2026-02-27 23:23:56 +01:00
Horváth Gergely
e2b98125fc Enhance time formatting by adding language detection for compact time and seasonal break methods 2026-02-27 23:23:56 +01:00
Horváth Gergely
8c877e43db Add time formatting helper and update seasonal display logic 2026-02-27 23:23:56 +01:00
Horváth Gergely
02acf0e59b Add morning notification settings and debounce handling for Live Activities 2026-02-27 23:23:56 +01:00
Horváth Gergely
04870205f9 Refactor date calculations for start of the week in live activity service 2026-02-27 23:23:56 +01:00
Horváth Gergely
bcc95ed0af Reduce bellDelay debounce interval from 5 seconds to 3 seconds;
Enhance lesson fetching logic for improved notification scheduling
2026-02-27 23:23:56 +01:00
88fd491281 Update project settings and entitlements for improved configuration
- Bump object version in project.pbxproj
- Refactor TimetableWidget folder exceptions in project.pbxproj
- Add BGTaskSchedulerPermittedIdentifiers to Info.plist
- Update development team and product bundle identifiers in entitlements
- Mark subproject as dirty in l10n
2026-02-27 23:23:56 +01:00
Horváth Gergely
9a435235b6 - Fixed UI/UX bugs in the Live Activities
- Fixed the unhandled language get, when the Live Activity is turned off
- Fixed bellDelay bugs
2026-02-27 23:23:56 +01:00
Horváth Gergely
e583c77a7e - Added bellDelay support for Live Activities; both standard push notifications and Live Activities now use the bellDelay value (default: 0). IMPORTANT: Holidays and school break(spring,summer,autumn,winter) activities do not apply bellDelay.
- Implemented background fetch on iOS: the app now refreshes every 30 minutes in the background and sends timetable changes to the backend if any are detected.
2026-02-27 23:23:56 +01:00
Horváth Gergely
8dbcc1e2a9 - Fixed user switching so previous user data is now properly cleared.
- Fixed re-login behavior: after every new login, the Live Activities privacy notice is shown as intended.
- Made the Live Activity toggle user-specific to prevent settings from carrying over after switching accounts.
- Fixed user logout so the backend now correctly removes all related data.
- Fixed first-install behavior so the Live Activities privacy notice no longer appears on the public beta screen; it now only appears after reaching the home page.
2026-02-27 23:23:56 +01:00
Horváth Gergely
f631d52d5a - Fixed lesson language data; backend now passes the correct values.
- Updated handling of cancelled lessons: removed timer and now displaying the localized “Cancelled” text.
- Aligned the icon and lesson number in the Dynamic Island so they are now level with the label text.
2026-02-27 23:23:56 +01:00
Horváth Gergely
768d0904a8 Add data privacy consent dialog for Live Activity
A new data privacy consent dialog has been added to the Live Activity feature. Users must accept this dialog to use Live Activity. If they decline, all their data will be immediately deleted from the database, and cannot use the Live Activities feature. Additionally, users receive a detailed description explaining what data we store, for how long, and their GDPR rights.
2026-02-27 23:23:56 +01:00
Horváth Gergely
c21ff3e15f Added an example of the .env file. 2026-02-27 23:23:56 +01:00
0e08919209 feat: add logging for device registration and timetable updates 2025-11-19 15:58:53 +01:00
e651552dec fix: enable iOS platform and update build settings for arm64 architecture 2025-11-19 15:49:28 +01:00
8f683561b9 fix: update version number to 1.0.9+1030 2025-11-19 15:46:25 +01:00
519c9a4043 fix: update development team and bundle identifiers 2025-11-19 13:41:11 +01:00
Horváth Gergely
887d765f65 Live activity support added!
I have merged the latest repo myself and decided to publish my version to the repo.
2025-11-19 01:57:24 +01:00
Pearoo
34cf77f216 feat: add error page 2025-11-07 15:25:48 +01:00
7b382563ba Jenkinsfile: fdroid -> artifacts 2025-10-26 09:14:10 +01:00
667a8e0e4d refactor: use streams for fetching data 2025-10-26 09:08:05 +01:00
147dff3696 fix: payload cast 2025-10-12 16:53:56 +02:00
0fd36de4a3 Increment base build number
Since we rebased the commit count is smaller
2025-10-12 15:50:53 +02:00
d4fe91860a cast jwt payload to Map<String, dynamic> 2025-10-12 13:57:20 +02:00
45d0b298c4 settings/licenses: fix text & spinner style 2025-10-11 20:14:13 +02:00
c812b0721f update l10n module 2025-10-11 20:07:18 +02:00
b62140c0d0 Fordítás hozzáadása 2025-10-09 22:21:28 +02:00
f55cc65f07 License menü 2025-10-09 20:57:55 +02:00
3690cf0462 Fordítás, License menü 2025-10-09 20:57:23 +02:00
233f0c9ed0 Órarend kinézet javítás 2025-10-09 20:56:59 +02:00
6912e44b7e Órarenden igazítás 2025-10-08 20:50:13 +02:00
369febba91 Discord, PP átirányítás 2025-10-08 20:50:01 +02:00
2ca5e8c54b Helyettesítések elrejtése, Discord + PP átirányítás 2025-10-08 20:49:35 +02:00
dd515955ba Helyettesítések elrejtése 2025-10-08 20:48:31 +02:00
97d1f005b4 Amennyiben nem írt dolgozatot/szöveges értékelés stb, nem 0-val jelzi, hanem szöveges értékelést mutatja. 2025-10-07 22:06:37 +02:00
92e94f60c5 Időformátum szebbítése 2025-10-07 22:05:46 +02:00
ba53f30cce kicsi hiba fix 2025-10-07 20:48:03 +02:00
a34c8e23d3 Funkció hozzáadása: Dolgozat popup 2025-10-07 20:39:22 +02:00
ab1208999d Amennyiben nincs jegy 1 tantárgyból, jelzi dave svg-vel 2025-10-06 21:03:58 +02:00
2b51523eb1 Tantárgy név átvitele 2025-10-06 21:03:29 +02:00
9f3190495b Felesleges import törlése 2025-10-06 21:03:12 +02:00
14fdf00259 Felesleges import törlése + tantárgy név átvitel 2025-10-06 21:02:37 +02:00
815604d144 tt: localize "Tantárgy megtekintése" btn 2025-10-06 16:57:54 +02:00
8f499aac21 tt: implement view subject button 2025-10-06 16:56:08 +02:00
f24f340d63 + Tantárgy megtekintése gomb 2025-10-06 12:38:52 +02:00
ccf7443f26 Számonkérés megjelenítése az adott óra felugró ablakánál 2025-10-06 12:37:57 +02:00
2f0c6ca9df Üres napnál amennyiben pld tanítás nélküli munkanap, akkor jelzi a felhasználónak 2025-10-06 12:37:52 +02:00
ff73bafb0d #75 Feleslegesen nagy üres rész Jegyeknél fix 2025-10-06 12:37:40 +02:00
978df96aaf #78 Dolgozatnál hibás tördelés fix 2025-10-06 12:37:31 +02:00
07b8714c7e Amennyiben a felhasználónak nincsenek órái, azt a dizájn szerinti módon jelzi 2025-10-06 12:37:13 +02:00
8881f8c674 #77 Óra témája hibás tördelése fix 2025-10-06 12:37:06 +02:00
569147ae8a Tantárgyak feldolgozása 2025-09-29 21:44:15 +02:00
947e1d12cf Amennyiben nincs jegy a tantárgyból, ismeretlen a tanár is, tehát azt a szöveget kicseréltem 2025-09-29 21:43:59 +02:00
fd750ae6ad studyTask hozzáadása, SubjectAverage bővítése 2025-09-29 21:43:35 +02:00
f31b719605 SubjectAverage átírása, getLessons törlése 2025-09-29 21:43:05 +02:00
0bcdf35060 AllLessons törlése: nincs rá szükség 2025-09-29 21:43:17 +02:00
a0c1f3bb1f DKT API törlése, getSubjectAvg módosítása 2025-09-29 21:40:16 +02:00
6e35ca9d72 home/main: add homework entries 2025-09-30 09:54:51 +02:00
3b6e5d8213 add privacy policy button 2025-09-29 21:39:36 +02:00
894d897778 home,grades,tt: capitalize titles
some school administrators enter everything as lower case
2025-09-29 18:28:55 +02:00
a96be41d01 settings: add logout button 2025-09-29 12:47:15 +02:00
24876aebd6 lesson/sheet: add room name 2025-09-29 12:15:00 +02:00
e4aea80f0b android/gradle: Use /usr/bin/ as ANDROID_SDK_ROOT fallback 2025-09-27 20:56:10 +02:00
56cbaa6d16 message toast -> screen
the design has a screen for messages not a toast
2025-09-25 22:08:27 +02:00
850800864d adjust fonts 2025-09-25 21:47:23 +02:00
a92ea1dcf6 home/tt: Fix date formatting on the title bar 2025-09-25 21:33:20 +02:00
a9829b2163 model: translate field names to English 2025-09-25 21:30:52 +02:00
4f1c903384 Összes tantárgy megjelenítése 2025-09-25 15:57:47 +02:00
b045980bb4 lesson: fix popup height 2025-09-17 13:40:05 +02:00
352f9f223b fix: use proper font size 2025-09-17 13:32:46 +02:00
3d41113c0f fix(style): adjust font height 2025-09-17 13:32:24 +02:00
e616893ad2 debug: update throw exception
use a nested isar txn, which can trigger the error popup
2025-09-16 21:29:32 +02:00
8d934dd0f8 grade bottom sheet: fix size and padding 2025-09-16 21:25:59 +02:00
3e31804fe5 lesson: don't check the week if its empty 2025-09-16 21:23:56 +02:00
5f74a99f5a refactor: setting -> settings 2025-09-16 17:28:43 +02:00
6e16b9a37c tt: add haptic feedback to swiping between pages 2025-09-16 13:07:41 +02:00
ebcf49d957 home: fix styling in active lesson 2025-09-16 09:50:39 +02:00
aec2453a05 tt: fix last lesson getting covered by T H E S H A D O W 2025-09-16 09:40:11 +02:00
6c64ac4a35 added pfp for homescreen 2025-09-15 23:26:32 +02:00
ae06a61b05 fixes #59 2025-09-15 22:23:03 +02:00
d5c81d443e add haptic feedback 2025-09-15 21:36:52 +02:00
8b1b5cc4cf Update submodule 2025-09-15 19:09:15 +02:00
3d09187d6f home: remove logging 2025-09-15 19:02:14 +02:00
722068fdde Főoldalhoz hátramaradt tanórák száma fix 2025-09-15 18:51:22 +02:00
c471768598 Tanulo > Tanuló 2025-09-15 18:50:36 +02:00
2009418887 + jel kivétele: Nincs használva 2025-09-15 18:50:22 +02:00
94d3802e95 Több információ a beírt jegyeknél 2025-09-15 18:50:01 +02:00
4fe8af2e66 fix: use a 30 bit hash if the username is non-numeric 2025-09-15 15:09:29 +02:00
57122a4c3f Jenkins: update version code for aab 2025-09-15 12:43:18 +02:00
e5fac2609f Jenkins: move bundle compression to build_apk.sh 2025-09-15 12:25:49 +02:00
dd4ccf2736 wearos: fix pairing 2025-09-15 12:21:40 +02:00
Pearoo
1356fd3eb3 fix(timetable): clamp carousel to bottom
only tested on Pixel 9 Pro yet
2025-09-14 13:54:22 +02:00
Pearoo
fde6a47adc fix(timetable): no more unexplainable padding 2025-09-13 22:55:55 +02:00
Pearoo
c1edbe0971 feat(timetable): cache next & previous weeks 2025-09-13 20:14:04 +02:00
Pearoo
f04517749b chore: update cache retention 2025-09-13 20:13:09 +02:00
Pearoo
c36d656178 fix: next week resets to monday 2025-09-13 19:16:33 +02:00
8c4735ccb3 tt: fix event text color 2025-09-13 17:54:32 +02:00
a665478930 kreta_client: more verbose logging 2025-09-13 17:04:27 +02:00
033ab39c59 home_subpage: handle back events 2025-09-13 16:39:31 +02:00
6bf6b46119 move scaffold from grades & tt pages to subpage
fixes random white stuttering in the background
2025-09-13 14:14:27 +02:00
2e425dd757 tt: show breaks after last lesson is over 2025-09-13 14:07:25 +02:00
942ecc9db2 home/tt: truncate long lesson names and room names
closes #53
2025-09-13 13:59:33 +02:00
b4d87978e2 settings & home: use firka app bundle
fixes #61
2025-09-13 13:36:08 +02:00
e84ea8c383 home: only call setState on update if screen is mounted 2025-09-13 13:06:18 +02:00
5354c601eb settings: move postUpdate hook outside save function
fixes #62
2025-09-13 13:05:50 +02:00
9533fdaeec settings: add log file sharing 2025-09-13 12:05:04 +02:00
c79934f946 some apple things idk 2025-09-13 00:14:08 +02:00
d21b8955a7 removed support for ipad, macos, vision os 2025-09-12 23:15:26 +02:00
c95128a48e logging: censor debugPrint in release mode 2025-09-13 11:38:10 +02:00
308ff7f14c logging: delete old log files 2025-09-13 11:37:37 +02:00
69eae36ef8 logging: log to file 2025-09-13 11:32:39 +02:00
788f808d4e logger: censor sensitive information 2025-09-13 11:01:05 +02:00
c16c899e66 login: more verbose logging 2025-09-13 10:01:25 +02:00
6e2eeea8f0 use proper logging 2025-09-12 22:44:02 +02:00
191ee62be9 login: fix comparison 2025-09-12 18:44:28 +02:00
b51169ab3c token: add support for guardians 2025-09-12 18:12:54 +02:00
1902ace989 login: add a fallback error handler 2025-09-12 17:09:20 +02:00
d3e30689f0 firka_card: fix dark mode shadow
closes #39
2025-09-11 21:33:05 +02:00
c1aae7adb1 home: add bottom sheet for announcements 2025-09-11 19:34:01 +02:00
ee72056074 grade & home: add bottom sheet 2025-09-11 18:19:57 +02:00
4bd585d1dc grade: convert dates to local ones 2025-09-11 17:59:30 +02:00
4806ac073a refactor: extract showLessonBottomSheet out to a file 2025-09-11 17:25:12 +02:00
ac9a5cda91 home: add grades 2025-09-11 15:01:53 +02:00
60e0a1755b grades/{subjects}: fix text colors 2025-09-11 14:54:48 +02:00
9039356cfd fix color for light buttons in dark mode
closes #57
2025-09-11 14:16:59 +02:00
4327cae06a remove wear_login_screen.dart 2025-09-11 13:56:22 +02:00
c22d175afb home: check for mounting before preloading assets 2025-09-11 13:55:01 +02:00
6650b8aaef login: automatically switch to new account 2025-09-11 13:53:58 +02:00
e18f31a8b4 login: remove login hint from add account screen 2025-09-11 12:49:23 +02:00
caeca5e050 feat: account switching 2025-09-11 12:47:43 +02:00
e44684049f home: fix random bouncing
- switch pull_to_refresh_flutter3 from to smart_scroll
- decrease spring stiffness to 95

closes #47
2025-09-10 21:10:45 +02:00
359129c0dd tt: add animations to switching between pages
closes #45
2025-09-10 20:26:29 +02:00
e53df719c7 settings: add back button for rounding options 2025-09-10 10:38:10 +02:00
53c5ad4f14 tt: only show next break on current day
closes #50
2025-09-10 10:28:45 +02:00
dd59381b35 tt: increase padding when there is a test 2025-09-10 09:41:13 +02:00
5811ece08e tt: show tests on lessons when tests are hidden
closes #51
2025-09-10 07:48:57 +02:00
0701424b3c tt: show tests on day picker 2025-09-10 07:44:39 +02:00
93d3125013 settings: add padding to customization screen
closes #48
2025-09-09 16:47:38 +02:00
e96979d36f settings: add back button to developer options 2025-09-09 13:35:32 +02:00
9a17d9085c main: invalidate ui on platform brightness change 2025-09-09 13:27:42 +02:00
6c3ac6c09b settings: invalidate ui on theme & lang change 2025-09-09 13:26:16 +02:00
d1c34bd607 State -> FirkaState
Add a helper called FirkaState which registers
a listener to globalUpdate, so that we can
invalidate every widget that is active
2025-09-09 13:13:55 +02:00
869ba964ee date: use proper locale 2025-09-09 11:53:07 +02:00
4d45de372b login: add dark themed images 2025-09-09 11:33:36 +02:00
18adc04801 kreta_login: revert ua on iOS 2025-09-09 08:00:11 +02:00
c71dd3e680 revert: Change webview UA to Google Chrome on Android 2025-09-09 07:56:42 +02:00
e978459c19 kréta login update 2025-09-08 21:00:58 +02:00
5f7106d257 tt: fix lesson no for empty classes 2025-09-09 07:47:27 +02:00
1f2b8091f1 timetable: Fix lesson number icon
closes #49
2025-09-08 20:30:38 +02:00
932a89193e docs: Update CONTRIBUTING.md 2025-09-08 20:50:41 +02:00
048832589c Jenkinsfile: Bring back debug artifacts 2025-09-07 16:44:46 +02:00
f55ec7c525 tt: switch to monday if the week isn't the current one
closes #44
2025-09-07 16:43:37 +02:00
d3fce38235 tt: switch to monday if the week isn't the current one
closes #44
2025-09-07 16:30:28 +02:00
9183769ff4 tt: jump to current day
closes #42
2025-09-07 12:03:40 +02:00
4f4960280a tt: make padding more consistent 2025-09-07 11:26:05 +02:00
c85a428a7b fix: change "Hide breaks & empty lessons" to "Hide breaks"
closes #52
2025-09-07 11:22:22 +02:00
1738a38b4f home/grades/{subject}: fix text color 2025-09-07 11:17:49 +02:00
ddd7c5a9d6 tmp: remove lidl app icon
closes #46
2025-09-06 21:20:17 +02:00
a141f1822c fix SettingsItemsRadio
closes #41
2025-09-06 19:00:03 +02:00
41fab74f11 fix typo
closes #40
2025-09-06 18:47:58 +02:00
df1c5d9acf Jenkinsfile: build aab on release branch 2025-09-06 16:59:38 +02:00
a4b691f8be bump version string 2025-09-06 16:59:27 +02:00
ac82436751 settings: use runApp in lang settings as well 2025-09-06 10:46:44 +02:00
868d6f3665 fix: nav color when switching theme 2025-09-06 10:45:23 +02:00
bd965caa4c bottom_tt_icon.dart: add shadow to active icon 2025-09-06 10:28:48 +02:00
145eca557e android: build each abi in parallel 2025-09-06 08:54:55 +02:00
25e3ea95ba fix: reinit theme on platform brightness change 2025-09-06 08:47:35 +02:00
b6683158d9 bump version to 1.0.0+1017 2025-09-06 01:01:37 +02:00
3efc019322 update project configuration and framework references in Xcode project file 2025-09-04 21:42:00 +02:00
a980e4eff2 lesson: fix bottom sheet in dark mode 2025-09-06 08:24:23 +02:00
324d21ec07 tt: fix empty week in dark mode 2025-09-06 08:21:44 +02:00
1224d4801d fix: navbar theme on auto 2025-09-06 08:11:58 +02:00
51b8753875 extras: respect theming 2025-09-05 21:46:26 +02:00
b31e90b052 tt*: respect theming 2025-09-05 21:44:25 +02:00
2d5cc896b8 bottom_nav_icon.dart: respect theming 2025-09-05 21:41:10 +02:00
975c9c5a08 fix: native status bar theming 2025-09-05 21:38:10 +02:00
57d6c503f2 login: remove hardcoded colors 2025-09-05 21:28:07 +02:00
e3b71dbd4d dark mode toggle 2025-09-05 20:58:30 +02:00
476 changed files with 39540 additions and 19106 deletions

18
.gitmodules vendored
View File

@@ -1,18 +1,6 @@
[submodule "firka/vendor/isar_generator"]
path = firka/vendor/isar_generator
url = https://git.qwit.cloud/firka/isar_generator
[submodule "firka/vendor/isar"]
path = firka/vendor/isar
url = https://git.qwit.cloud/firka/isar
[submodule "firka/vendor/isar_flutter_libs"]
path = firka/vendor/isar_flutter_libs
url = https://git.qwit.cloud/firka/isar_flutter_libs
[submodule "firka/lib/l10n"]
path = firka/lib/l10n
url = https://github.com/QwIT-Development/firka-localization
[submodule "firka_wear/vendor/wear_plus"]
path = firka_wear/vendor/wear_plus
url = https://git.firka.app/firka/wear_plus
[submodule "firka/android/app/src/main/java/org/brotli"]
path = firka/android/app/src/main/java/org/brotli
url = https://git.firka.app/firka/org_brotli
[submodule "firka_wear/lib/l10n"]
path = firka_wear/lib/l10n
url = https://github.com/qwit-development/firka-localization

View File

@@ -4,7 +4,7 @@ A firka androidra való lebuildeléséhez kötelező a saját Flutter fork haszn
A Flutter telepítéséhez a dokumentáció [itt](https://docs.flutter.dev/get-started/install) található.
A Flutter zip letöltése helyett [a custom engine zip-et töltsd le](https://git.firka.app/firka/flutter/archive/main.zip)
A Flutter zip letöltése helyett a custom engine-t cloneold le ([https://git.firka.app/firka/flutter/](https://git.firka.app/firka/flutter/))
# Brotli

View File

@@ -5,7 +5,7 @@ and to make a release build you will have to use our custom
flutter engine.
The documentation for installing flutter can be found [here](https://docs.flutter.dev/get-started/install).
Instead of downloading the regular flutter zip, download it from [here](https://git.firka.app/firka/flutter/archive/main.zip).
Instead of downloading the regular flutter zip, clone it from ([https://git.firka.app/firka/flutter/](https://git.firka.app/firka/flutter/)).
# Brotli

108
Jenkinsfile vendored
View File

@@ -16,6 +16,7 @@ pipeline {
}
}
}
stage('Decrypt main keys') {
when {
branch 'main'
@@ -40,6 +41,7 @@ pipeline {
'''
}
}
stage('Clone submodules') {
steps {
script {
@@ -47,36 +49,13 @@ pipeline {
}
}
}
stage('Modify firka_bundle.dart') {
when {
branch 'main'
}
steps {
script {
sh '''#!/bin/sh
set -e
BUNDLE_FILE="firka/lib/helpers/firka_bundle.dart"
if [ -f "$BUNDLE_FILE" ]; then
echo "Modifying $BUNDLE_FILE"
sed -i 's/final bool _compressedBundle = false;/final bool _compressedBundle = Platform.isAndroid;/' "$BUNDLE_FILE"
echo "Modified _compressedBundle setting"
grep "_compressedBundle" "$BUNDLE_FILE" || echo "Warning: _compressedBundle line not found after modification"
else
echo "$BUNDLE_FILE not found"
exit 1
fi
'''
}
}
}
stage('Build firka') {
steps {
sh 'bash -c "./tools/linux/build_apk.sh ' + env.BRANCH_NAME + '"'
}
}
stage('Rename Release APKs') {
when {
branch 'main'
@@ -109,6 +88,7 @@ pipeline {
}
}
}
stage('Calculate Version Code') {
steps {
script {
@@ -118,7 +98,7 @@ pipeline {
# Calculate version code based on git commits (same logic as build script)
COMMIT_COUNT=$(git rev-list --count HEAD)
BASE_BUILD_NUMBER=$((1000 + COMMIT_COUNT))
BASE_BUILD_NUMBER=$((1300 + COMMIT_COUNT))
if [ "$BRANCH_NAME" = "main" ]; then
# For main branch, highest version code is BASE + 3000 (x64 build)
@@ -137,75 +117,43 @@ pipeline {
}
}
}
stage('Upload to F-Droid Debug') {
stage('Publish debug artifacts') {
when {
branch 'dev'
}
steps {
script {
withCredentials([usernamePassword(credentialsId: 'fdroid-ssh', usernameVariable: 'SSH_USER', passwordVariable: 'SSHPASS')]) {
sh '''
SOURCE_FILE="firka/build/app/outputs/flutter-apk/app-debug.apk"
REMOTE_PATH="/home/fdroid/firka-fdroid/repo/app.firka.naplo.debug.apk"
export SSHPASS
sshpass -e scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
"$SOURCE_FILE" "$SSH_USER@10.0.0.21:$REMOTE_PATH"
# Update version code in F-Droid metadata
sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
"$SSH_USER@10.0.0.21" \
"sed -i 's/^CurrentVersionCode: .*/CurrentVersionCode: $VERSION_CODE/' /home/fdroid/firka-fdroid/metadata/app.firka.naplo.debug.yml"
sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
"$SSH_USER@10.0.0.21" \
"cd /home/fdroid/firka-fdroid && /run/current-system/sw/bin/fdroid update"
'''
}
not {
branch 'main'
}
}
steps {
archiveArtifacts artifacts: 'firka/build/app/outputs/flutter-apk/app-debug.apk', fingerprint: true
}
}
stage('Upload to F-Droid Release') {
stage('Publish release AABs artifacts') {
when {
branch 'main'
}
steps {
script {
withCredentials([usernamePassword(credentialsId: 'fdroid-ssh', usernameVariable: 'SSH_USER', passwordVariable: 'SSHPASS')]) {
sh '''
# Use the renamed APK files
REMOTE_PATH="/home/fdroid/firka-fdroid/repo/"
export SSHPASS
# Loop over each APK file and upload it one by one
for SOURCE_FILE in firka/build/app/outputs/flutter-apk/app.firka.naplo_*.apk; do
if [ -f "$SOURCE_FILE" ]; then
echo "Uploading $SOURCE_FILE to $REMOTE_PATH"
sshpass -e scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
"$SOURCE_FILE" "$SSH_USER@10.0.0.21:$REMOTE_PATH"
else
echo "No APK files found to upload."
fi
done
# Update version code in F-Droid metadata for release
sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
"$SSH_USER@10.0.0.21" \
"sed -i 's/^CurrentVersionCode: .*/CurrentVersionCode: $VERSION_CODE/' /home/fdroid/firka-fdroid/metadata/app.firka.naplo.yml"
# Update F-Droid repository
sshpass -e ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
"$SSH_USER@10.0.0.21" \
"cd /home/fdroid/firka-fdroid && /run/current-system/sw/bin/fdroid update"
'''
}
}
archiveArtifacts artifacts: 'firka/build/app/outputs/bundle/release/*.aab', fingerprint: true
sh 'rm firka/build/app/outputs/bundle/release/*.aab'
}
}
stage('Publish release APKs artifacts') {
when {
branch 'main'
}
steps {
archiveArtifacts artifacts: 'firka/build/app/outputs/flutter-apk/app.firka.naplo_*.apk', fingerprint: true
sh 'rm firka/build/app/outputs/flutter-apk/app.firka.naplo_*.apk'
}
}
stage('Post Cleanup') {
steps {
script {
sh '''
rm firka/build/app/outputs/bundle/release/*.aab || true
rm firka/build/app/outputs/flutter-apk/app.firka.naplo_*.apk || true
rm firka/build/app/outputs/flutter-apk/app-debug.apk || true
rm version_code.txt || true

45
build.sh Normal file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -euo pipefail
# Build firka and/or firka_wear with version from pubspec + short git SHA.
# Usage: ./build.sh [firka|firka_wear|all]
# Default (no args) builds both.
ROOT="$(cd "$(dirname "$0")" && pwd)"
SHA=$(git -C "$ROOT" rev-parse --short HEAD)
build_app() {
local APP="$1"
local PUBSPEC="$ROOT/$APP/pubspec.yaml"
if [[ ! -f "$PUBSPEC" ]]; then
echo "Not found: $PUBSPEC" >&2
return 1
fi
local VERSION_LINE BUILD_NUMBER BASE_VERSION BUILD_NAME
VERSION_LINE=$(grep -E '^\s*version:\s*' "$PUBSPEC" | head -1)
BASE_VERSION=$(echo "$VERSION_LINE" | sed -E 's/^[[:space:]]*version:[[:space:]]*([^+]+).*/\1/' | tr -d ' ')
BUILD_NUMBER=""
if [[ "$VERSION_LINE" == *+* ]]; then
BUILD_NUMBER=$(echo "$VERSION_LINE" | sed -E 's/^[[:space:]]*version:[[:space:]]*[^+]+\+([0-9]+).*/\1/')
fi
BUILD_NAME="${BASE_VERSION}-${SHA}"
echo "Building $APP: version $BUILD_NAME (build number: ${BUILD_NUMBER:-none})"
cd "$ROOT/$APP"
local FLUTTER_ARGS=(build appbundle --build-name="$BUILD_NAME" --verbose)
[[ -n "${BUILD_NUMBER:-}" ]] && FLUTTER_ARGS+=(--build-number="$BUILD_NUMBER")
flutter "${FLUTTER_ARGS[@]}"
}
case "${1:-all}" in
firka) build_app firka ;;
firka_wear) build_app firka_wear ;;
all) build_app firka && build_app firka_wear ;;
*)
echo "Usage: $0 [firka|firka_wear|all]" >&2
exit 1
;;
esac

5
firka/.env.example Normal file
View File

@@ -0,0 +1,5 @@
# Backend Configuration
# Development: http://192.168.X.YYY:3000/api/v1
# Production: https://your-domain.com/api/v1
BACKEND_BASE_URL=http://192.168.X.YYY:3000/api/v1
BACKEND_API_KEY=development_api_key_12345

10
firka/.gitignore vendored
View File

@@ -12,6 +12,11 @@
.swiftpm/
migrate_working_dir/
# Environment variables
.env
.env.local
.env.*.local
# IntelliJ related
*.iml
*.ipr
@@ -44,4 +49,7 @@ app.*.map.json
/android/app/profile
/android/app/release
coverage
coverage
# Generated files
*.g.dart

View File

@@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "05db9689081f091050f01aed79f04dce0c750154"
revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2"
channel: "stable"
project_type: app
@@ -13,11 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: 05db9689081f091050f01aed79f04dce0c750154
base_revision: 05db9689081f091050f01aed79f04dce0c750154
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
- platform: ios
create_revision: 05db9689081f091050f01aed79f04dce0c750154
base_revision: 05db9689081f091050f01aed79f04dce0c750154
create_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
base_revision: adc901062556672b4138e18a4dc62a4be8f4b3c2
# User provided section

View File

@@ -1,36 +1,18 @@
import org.apache.commons.io.FileUtils
import java.io.FileInputStream
import java.security.MessageDigest
import java.util.Properties
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.concurrent.locks.ReentrantReadWriteLock
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
import java.util.zip.ZipOutputStream
import java.util.zip.ZipOutputStream.DEFLATED
import java.util.zip.ZipOutputStream.STORED
plugins {
id("com.android.application")
id("kotlin-android")
id("org.jetbrains.kotlin.plugin.compose") version "2.2.0"
id("org.jetbrains.kotlin.plugin.compose")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
fun loadProperties(file: File): Properties {
val properties = Properties()
FileInputStream(file).use { inputStream ->
properties.load(inputStream)
}
return properties
}
android {
namespace = "app.firka.naplo"
compileSdk = flutter.compileSdkVersion
ndkVersion = "27.0.12077973"
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
@@ -49,8 +31,8 @@ android {
applicationId = "app.firka.naplo"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 29
targetSdk = 36
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}
@@ -59,7 +41,7 @@ android {
val propsFile = File(secretsDir, "keystore.properties")
if (propsFile.exists()) {
val props = loadProperties(propsFile)
val props = Properties().apply { FileInputStream(propsFile).use { load(it) } }
val store = File(secretsDir, props["storeFile"].toString())
signingConfigs {
@@ -68,6 +50,10 @@ android {
storePassword = props["storePassword"] as String
keyPassword = props["keyPassword"] as String
keyAlias = props["keyAlias"] as String
// Use APK Signature Scheme v3 (and v4 for streaming verification). See:
// https://source.android.com/docs/security/features/apksigning/v3
enableV3Signing = true
enableV4Signing = true
}
}
}
@@ -89,798 +75,25 @@ android {
}
}
dependencies {
implementation("androidx.wear:wear-ongoing:1.0.0")
implementation("androidx.glance:glance-appwidget:1.1.1")
implementation("com.google.android.gms:play-services-wearable:18.1.0")
}
// Ensure .env exists before Flutter bundles assets (copy from .env.example if missing)
val envFile = file("${project.projectDir}/../../.env")
val envExampleFile = file("${project.projectDir}/../../.env.example")
tasks.register("ensureEnv") {
doLast {
if (!envFile.exists() && envExampleFile.exists()) {
envExampleFile.copyTo(envFile, overwrite = false)
println("Created .env from .env.example for asset bundling.")
}
}
}
tasks.matching { it.name.startsWith("compileFlutterBuild") }.configureEach {
dependsOn("ensureEnv")
}
flutter {
source = "../.."
}
tasks.register("transformAndResignDebugApk") {
group = "build"
description = "Transform and resign APK with debug key"
dependsOn("assembleDebug")
doLast {
if (System.getenv("TRANSFORM_APK") != null
&& System.getenv("TRANSFORM_APK") == "true") {
transformApks(true)
}
}
}
tasks.register("transformAndResignReleaseApk") {
group = "build"
description = "Transform and resign APK with release key"
dependsOn("assembleRelease")
doLast {
checkReleaseKey()
if (System.getenv("TRANSFORM_APK") != null
&& System.getenv("TRANSFORM_APK") == "true") {
transformApks(false)
}
}
}
tasks.register("transformAndResignReleaseBundle") {
group = "build"
description = "Transform and resign bundle with release key"
dependsOn("bundleRelease")
doLast {
if (System.getenv("TRANSFORM_AAB") != null
&& System.getenv("TRANSFORM_AAB") == "true") {
transformAppBundle()
}
}
}
afterEvaluate {
tasks.findByName("assembleDebug")?.finalizedBy("transformAndResignDebugApk")
tasks.findByName("assembleRelease")?.finalizedBy("transformAndResignReleaseApk")
tasks.findByName("bundleRelease")?.finalizedBy("transformAndResignReleaseBundle")
}
fun checkReleaseKey() {
val secretsDir = File(projectDir.absolutePath, "../../../secrets/")
val propsFile = File(secretsDir, "keystore.properties")
if (propsFile.exists()) {
val props = loadProperties(propsFile)
val store = File(secretsDir, props["storeFile"].toString())
println(
"Signing with:\n" +
"\t- store: ${store.name}\n" +
"\t- key: ${props["keyAlias"]}"
)
} else {
throw Exception("Release keystore not found!")
}
}
fun transformApks(debug: Boolean, i : Int = 0) {
try {
_transformApks(debug)
} catch (e: Exception) {
if (i < 5) {
e.printStackTrace()
println("Retrying: ${i + 1}")
transformApks(debug, i + 1)
} else {
throw e
}
}
}
fun _transformApks(debug: Boolean) {
println("Starting APK transformation process...")
val buildDir = project.buildDir
val apkDir = File(buildDir, "outputs/flutter-apk")
val apks = getApks(debug)
var c = 0
apks
.forEach { c++; transformAndSignApk(apkDir, it.nameWithoutExtension, debug) }
println("Transformed: $c apks")
}
fun transformAndSignApk(apkDir: File, name: String, debug: Boolean) {
val originalApk = File(apkDir, "$name.apk")
val transformedApk = File(apkDir, "$name-transformed.apk")
val finalApk = File(apkDir, "$name-resigned.apk")
val finalIdsig = File(apkDir, "$name-resigned.apk.idsig")
if (!originalApk.exists()) {
throw GradleException("Original APK not found at: ${originalApk.absolutePath}")
}
if (transformedApk.exists()) transformedApk.delete()
if (finalApk.exists()) finalApk.delete()
println("Original APK: ${originalApk.absolutePath}")
try {
println("Transforming APK...")
transformApk(originalApk, transformedApk, if (debug) { "6" } else {"Z"})
if (debug) {
println("Signing with debug key...")
signWithDebugKey(transformedApk, finalApk)
} else {
println("Signing with release key...")
signWithReleaseKey(transformedApk, finalApk)
}
if (finalApk.exists()) {
originalApk.delete()
finalIdsig.delete()
finalApk.renameTo(originalApk)
println("APK successfully transformed")
println("Final APK: ${originalApk.absolutePath}")
}
transformedApk.delete()
} catch (e: Exception) {
throw GradleException("Failed to transform and resign APK: ${e.message}", e)
}
}
fun transformApk(input: File, output: File, compressionLevel: String = "Z") {
val tempDir = File(project.buildDir, "tmp/apk-transform")
val cacheDir = File(project.buildDir, "cache")
val optipngCacheDir = File(cacheDir, "optipng")
val assetCompressionDir = File(cacheDir, "assets")
tempDir.deleteRecursively()
tempDir.mkdirs()
if (!optipngCacheDir.exists()) optipngCacheDir.mkdirs()
if (!assetCompressionDir.exists()) assetCompressionDir.mkdirs()
val brotli = findToolInPath("brotli")
?: throw Exception("Brotli not found in path")
val optipng = findToolInPath("optipng")
if (optipng == null || optipng.isEmpty()) {
println("Optipng was not found in PATH, optimizing images will be skipped.")
}
copy {
from(zipTree(input))
into(tempDir)
}
val metaInf = File(tempDir, "META-INF")
val metaInfFiles = metaInf.listFiles()
for (file in metaInfFiles!!) {
if (file.name.endsWith("MF") || file.name.endsWith("SF")
|| file.name.endsWith("RSA")) {
file.delete()
}
}
val arches = File(tempDir, "lib").listFiles()
val compressedLibs = mutableMapOf<String, String>()
for (arch in arches!!) {
val libFlutter = File(arch, "libflutter.so")
if (!libFlutter.exists()) continue
val compressedFlutter = File(arch, "libflutter-br.so")
compressedLibs["libflutter.so"] = libFlutter.sha256()
println("Compressing ${arch.name}/libflutter.so with brotli")
exec {
commandLine(
brotli,
"-$compressionLevel",
libFlutter.absolutePath,
"-o", compressedFlutter.absolutePath
)
}
libFlutter.delete()
val json = groovy.json.JsonBuilder(compressedLibs)
File(arch, "index.so").writeText(json.toString())
}
val topDirL = tempDir.absolutePath.length + 1
val zos = ZipOutputStream(output.outputStream())
val coreCount = Runtime.getRuntime().availableProcessors()
val flutterResources = tempDir.walkTopDown().filter{f -> f.absolutePath.contains("flutter_assets")}
val pngFiles = tempDir.walkTopDown().filter{f -> f.name.endsWith(".png")}
val assetIndex = mutableMapOf<String, String>()
val indexReadWriteLock = ReentrantReadWriteLock()
if (compressionLevel == "Z") {
if (optipng != null) {
val executor = Executors.newFixedThreadPool(coreCount)
val futures = mutableListOf<Future<*>>()
pngFiles.forEach { pngFile ->
val cacheFile = File(optipngCacheDir, pngFile.sha256())
if (cacheFile.exists()) {
cacheFile.copyTo(pngFile, true)
} else {
val future = executor.submit {
exec {
commandLine(
optipng,
"-zm", "9",
"-zw", "32k",
"-o9",
pngFile.absolutePath
)
}
pngFile.copyTo(cacheFile, true)
}
futures.add(future)
}
}
futures.forEach { it.get() }
executor.shutdown()
}
val executor = Executors.newFixedThreadPool(coreCount)
val futures = mutableListOf<Future<*>>()
val blacklist = listOf(
// "AssetManifest.bin",
"AssetManifest.json",
"FontManifest.json",
"isolate_snapshot_data",
"kernel_blob.bin",
"NativeAssetsManifest.json",
"NOTICES.Z",
"vm_snapshot_data",
"fonts",
"shaders"
)
flutterResources.forEach { f ->
val relName = f.absolutePath.substring(topDirL).replace("\\", "/")
if (f.isDirectory) return@forEach
val cacheFileRaw = File(assetCompressionDir, f.sha256()+".r")
val cacheFileGz = File(assetCompressionDir, f.sha256()+".gz")
val cacheFileBr = File(assetCompressionDir, f.sha256()+".br")
if (cacheFileRaw.exists() || cacheFileGz.exists() || cacheFileBr.exists()) {
if (cacheFileRaw.exists()) {
cacheFileRaw.copyTo(f, true)
indexReadWriteLock.writeLock().lock()
assetIndex[relName] = "r"
indexReadWriteLock.writeLock().unlock()
} else if (cacheFileGz.exists()) {
cacheFileGz.copyTo(f, true)
indexReadWriteLock.writeLock().lock()
assetIndex[relName] = "g"
indexReadWriteLock.writeLock().unlock()
} else {
cacheFileBr.copyTo(f, true)
indexReadWriteLock.writeLock().lock()
assetIndex[relName] = "b"
indexReadWriteLock.writeLock().unlock()
}
} else {
val future = executor.submit {
val brTmp = File(f.absolutePath + ".br.tmp")
val gzTmp = File(f.absolutePath + ".gz.tmp")
var blacklisted = false
for (f in blacklist) {
if (relName.contains(f)) {
blacklisted = true
break
}
}
if (!blacklisted) {
println("$relName: Testing with brotli")
exec {
commandLine(
brotli,
"-$compressionLevel",
f.absolutePath,
"-o", brTmp.absolutePath
)
}
println("$relName: Testing with gzip")
ant.invokeMethod(
"gzip", mapOf(
"src" to f.absolutePath,
"destfile" to gzTmp.absolutePath,
)
)
println("$brTmp: ${brTmp.length()}")
println("$gzTmp: ${gzTmp.length()}")
if (f.length() < gzTmp.length() && f.length() < brTmp.length()) {
println("$relName: Raw file wins")
f.copyTo(cacheFileRaw, true)
indexReadWriteLock.writeLock().lock()
assetIndex[relName] = "r"
indexReadWriteLock.writeLock().unlock()
} else {
if (brTmp.length() < gzTmp.length()) {
println("$relName: Brotli wins")
f.delete()
brTmp.copyTo(f, true)
brTmp.copyTo(cacheFileBr, true)
indexReadWriteLock.writeLock().lock()
assetIndex[relName] = "b"
indexReadWriteLock.writeLock().unlock()
} else {
println("$relName: Gzip wins")
f.delete()
gzTmp.copyTo(f, true)
gzTmp.copyTo(cacheFileGz, true)
indexReadWriteLock.writeLock().lock()
assetIndex[relName] = "g"
indexReadWriteLock.writeLock().unlock()
}
}
brTmp.delete()
gzTmp.delete()
}
}
futures.add(future)
}
}
futures.forEach { it.get() }
executor.shutdown()
}
tempDir.walkTopDown().forEach { f ->
if (f.absolutePath == tempDir.absolutePath) return@forEach
var relName = f.absolutePath.substring(topDirL).replace("\\", "/")
if (f.isDirectory && !relName.endsWith("/")) relName += "/"
if (compressionLevel == "Z") {
if (relName == "assets/flutter_assets/assets/firka.i") return@forEach
}
println(relName)
val compress = !relName.endsWith(".so") && !relName.endsWith(".arsc")
zos.setMethod(if (compress) { DEFLATED } else { STORED })
val entry = ZipEntry(relName)
if (!compress) {
entry.size = f.length()
entry.crc = FileUtils.checksumCRC32(f)
}
zos.putNextEntry(entry)
if (f.isFile) {
zos.write(f.readBytes())
}
zos.closeEntry()
}
if (compressionLevel == "Z") {
zos.setMethod(DEFLATED)
zos.putNextEntry(ZipEntry("assets/flutter_assets/assets/firka.i"))
val indexUncompressed = File(tempDir, "index.json")
indexReadWriteLock.readLock().lock()
val json = groovy.json.JsonBuilder(assetIndex)
indexReadWriteLock.readLock().unlock()
indexUncompressed.writeText(json.toString())
val indexCompressed = File(tempDir, "index.json.br")
exec {
commandLine(
brotli,
"-$compressionLevel",
indexUncompressed.absolutePath,
"-o", indexCompressed.absolutePath
)
}
zos.write(indexCompressed.readBytes())
indexUncompressed.delete()
indexCompressed.delete()
zos.closeEntry()
}
zos.close()
tempDir.deleteRecursively()
println("APK transformed successfully")
}
fun transformAppBundle() {
val buildDir = project.buildDir
val bundle = File(buildDir, "outputs/bundle/release/app-release.aab")
val bundleTmp = File(buildDir, "outputs/bundle/release/tmp.zip")
val apks = getApks(false)
val apkCount = apks.count { it.name.startsWith("app-") && it.name.endsWith("-release.apk") }
if (!bundle.exists()) {
throw Exception("Bundle not found at: $bundle")
}
if (apkCount < 3) {
throw Exception("Excepected 3 apks per abi but only found $apkCount")
}
val aabTempDir = File(project.buildDir, "tmp/aab-transform")
aabTempDir.deleteRecursively()
aabTempDir.mkdirs()
val apksUnzipped = File(project.buildDir, "tmp/apks-unzipped")
apksUnzipped.deleteRecursively()
val arm32TempDir = File(apksUnzipped, "armeabi-v7a")
arm32TempDir.mkdirs()
val arm64TempDir = File(apksUnzipped, "arm64-v8a")
arm64TempDir.mkdirs()
val x86TempDir = File(apksUnzipped, "x86_64")
x86TempDir.mkdirs()
copy {
from(zipTree(bundle))
into(aabTempDir)
}
copy {
from(zipTree(apks.first { it.name.contains("armeabi-v7a") }))
into(arm32TempDir)
}
copy {
from(zipTree(apks.first { it.name.contains("arm64-v8a") }))
into(arm64TempDir)
}
copy {
from(zipTree(apks.first { it.name.contains("x86_64") }))
into(x86TempDir)
}
val libs = File(aabTempDir, "base/lib").listFiles()!!
for (dstLibs in libs) {
println("Copying lib: ${dstLibs.name}")
val srcDir = File(apksUnzipped, dstLibs.name)
if (!srcDir.exists()) {
continue
}
val srcLibs = File(srcDir, "lib/${dstLibs.name}/")
dstLibs.listFiles()!!.forEach { it.delete() }
srcLibs.listFiles()!!.forEach { it.copyTo(File(dstLibs, it.name)) }
}
val zos = ZipOutputStream(bundleTmp.outputStream())
val bundleZip = ZipFile(bundle)
val bundleEntries = bundleZip.entries()
val brotli = findToolInPath("brotli")
?: throw Exception("Brotli not found in path")
val optipng = findToolInPath("optipng")
?: throw Exception("Optipng not found in path")
val indexReadWriteLock = ReentrantReadWriteLock()
val assetIndex = mutableMapOf<String, String>()
while (bundleEntries.hasMoreElements()) {
val entry = bundleEntries.nextElement()
/*
if (entry.name == "base/assets/flutter_assets/assets/firka.i") {
println("Patching: ${entry.name}")
zos.putNextEntry(ZipEntry("assets/flutter_assets/assets/firka.i"))
val indexUncompressed = File(aabTempDir, "index.json")
indexReadWriteLock.readLock().lock()
val json = groovy.json.JsonBuilder(assetIndex)
indexReadWriteLock.readLock().unlock()
indexUncompressed.writeText(json.toString())
val indexCompressed = File(aabTempDir, "index.json.br")
exec {
commandLine(
brotli,
"-Z",
indexUncompressed.absolutePath,
"-o", indexCompressed.absolutePath
)
}
zos.write(indexCompressed.readBytes())
indexUncompressed.delete()
indexCompressed.delete()
zos.closeEntry()
continue
}
if (entry.name.startsWith("base/lib")) {
println("Patching: ${entry.name}")
zos.putNextEntry(ZipEntry(entry.name))
zos.closeEntry()
continue
}
*/
println("Adding: ${entry.name}")
zos.putNextEntry(ZipEntry(entry.name))
if (!entry.isDirectory) {
val data = bundleZip.getInputStream(entry).readAllBytes()
zos.write(data)
}
zos.closeEntry()
}
bundleZip.close()
zos.close()
bundle.delete()
signBundle(bundleTmp, bundle)
bundleTmp.delete()
aabTempDir.deleteRecursively()
println("AAB transformed successfully")
}
fun File.sha256(): String {
val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(this.readBytes())
return digest.fold("") { str, it -> str + "%02x".format(it) }
}
fun getApks(debug: Boolean): List<File> {
val buildDir = project.buildDir
val apkDir = File(buildDir, "outputs/flutter-apk")
val apks = apkDir.listFiles()!!
val flavor = if (debug) { "debug" } else { "release" }
return apks
.filter { apk -> apk.name.startsWith("app-") && apk.name.endsWith("-$flavor.apk") }
.toList()
}
fun getDebugKeystorePath(): String {
val userHome = System.getProperty("user.home")
val debugKeystore = File(userHome, ".android/debug.keystore")
if (!debugKeystore.exists()) {
throw GradleException("Debug keystore not found at: ${debugKeystore.absolutePath}")
}
return debugKeystore.absolutePath
}
fun getDefaultAndroidSdkPath(): String? {
val os = System.getProperty("os.name").lowercase()
val userHome = System.getProperty("user.home")
return when {
os.contains("win") ->
"$userHome\\AppData\\Local\\Android\\Sdk"
os.contains("mac") ->
"$userHome/Library/Android/sdk"
os.contains("linux") ->
"$userHome/Android/Sdk"
else -> null
}
}
fun findToolInPath(toolName: String): String? {
val pathEnvironment = System.getenv("PATH")
val pathDirs = pathEnvironment.split(File.pathSeparator)
val executableNames = when {
System.getProperty("os.name").lowercase().contains("win") ->
listOf("$toolName.exe", toolName)
else ->
listOf(toolName)
}
for (pathDir in pathDirs) {
for (execName in executableNames) {
val possibleTool = File(pathDir, execName)
if (possibleTool.exists() && possibleTool.canExecute()) {
return possibleTool.absolutePath
}
}
}
return null
}
fun findToolInSdkPath(toolName: String): String? {
var androidHome : String? = System.getenv("ANDROID_HOME")
?: System.getenv("ANDROID_SDK_ROOT")
if (androidHome == null) androidHome = getDefaultAndroidSdkPath()
if (androidHome != null) {
val buildTools = File(androidHome, "build-tools")
if (buildTools.exists()) {
val latestVersion = buildTools.listFiles()
?.filter { it.isDirectory }
?.filter { it.name != "debian" }
?.maxByOrNull { it.name }
if (latestVersion != null) {
val toolExec = File(latestVersion, toolName)
if (toolExec.exists()) {
return toolExec.absolutePath
}
}
}
}
if (!toolName.contains(".exe")) {
val exeTool = findToolInSdkPath("$toolName.exe")
if (exeTool != null) return exeTool
}
if (!toolName.contains(".sh")) {
val shTool = findToolInSdkPath("$toolName.sh")
if (shTool != null) return shTool
}
if (!toolName.contains(".bat")) {
val batTool = findToolInSdkPath("$toolName.bat")
if (batTool != null) return batTool
}
return null
}
fun signWithDebugKey(input: File, output: File) {
val debugKeystore = getDebugKeystorePath()
val debugKeystorePassword = "android"
val debugKeyAlias = "androiddebugkey"
val debugKeyPassword = "android"
val zipAlign: String = findToolInSdkPath("zipalign")
?: throw Exception("Could not find zipalign in ANDROID_SDK")
val apksigner: String = findToolInSdkPath("apksigner")
?: throw Exception("Could not find zipalign in ANDROID_SDK")
exec {
commandLine(
zipAlign,
"-v", "4",
input.absolutePath,
output.absolutePath
)
}
exec {
commandLine(
apksigner, "sign",
"--ks", debugKeystore,
"--ks-pass", "pass:$debugKeystorePassword",
"--ks-key-alias", debugKeyAlias,
"--key-pass", "pass:$debugKeyPassword",
output.absolutePath
)
}
println("APK signed and aligned successfully")
}
fun signWithReleaseKey(input: File, output: File) {
val secretsDir = File(projectDir.absolutePath, "../../../secrets/")
val propsFile = File(secretsDir, "keystore.properties")
if (!propsFile.exists()) {
throw Exception("Release keystore not found!")
}
val props = loadProperties(propsFile)
val releaseKeystore = File(secretsDir, props["storeFile"].toString())
val releaseKeystorePassword = props["storePassword"] as String
val releaseKeyAlias = props["keyAlias"] as String
val releaseKeyPassword = props["keyPassword"] as String
val zipAlign: String = findToolInSdkPath("zipalign")
?: throw Exception("Could not find zipalign either in ANDROID_SDK")
val apksigner: String = findToolInSdkPath("apksigner")
?: throw Exception("Could not find zipalign either in ANDROID_SDK")
exec {
commandLine(
zipAlign,
"-v", "4",
input.absolutePath,
output.absolutePath
)
}
exec {
commandLine(
apksigner, "sign",
"--ks", releaseKeystore,
"--ks-pass", "pass:$releaseKeystorePassword",
"--ks-key-alias", releaseKeyAlias,
"--key-pass", "pass:$releaseKeyPassword",
output.absolutePath
)
}
println("APK signed and aligned successfully")
}
fun signBundle(input: File, output: File) {
val secretsDir = File(projectDir.absolutePath, "../../../secrets/")
val propsFile = File(secretsDir, "keystore.properties")
if (!propsFile.exists()) {
throw Exception("Release keystore not found!")
}
val props = loadProperties(propsFile)
val releaseKeystore = File(secretsDir, props["storeFile"].toString())
val releaseKeystorePassword = props["storePassword"] as String
val releaseKeyAlias = props["keyAlias"] as String
val releaseKeyPassword = props["keyPassword"] as String
// val zipAlign: String = findToolInSdkPath("zipalign")
// ?: throw Exception("Could not find zipalign in ANDROID_SDK")
val jarsigner: String = findToolInPath("jarsigner")
?: throw Exception("Could not find jarsigner in PATH")
/*
exec {
commandLine(
zipAlign,
"-v", "4",
input.absolutePath,
output.absolutePath
)
}
*/
input.copyTo(output, true)
exec {
// -keystore $KEYSTORE -storetype $STORETYPE -storepass $STOREPASS -digestalg SHA1 -sigalg SHA256withRSA application.zip $KEYALIAS
commandLine(
jarsigner,
"-verbose",
"-sigalg", "SHA256withRSA",
"-digestalg", "SHA-256",
"-keystore", releaseKeystore,
"-storepass", releaseKeystorePassword,
output.absolutePath,
releaseKeyAlias
)
}
println("AAB signed and aligned successfully")
}

View File

@@ -1,2 +1 @@
-keep class org.brotli.** { *; }
-keep class app.firka.naplo.glance.** { *; }

View File

@@ -5,11 +5,16 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<application
android:name=".AppMain"
android:icon="@mipmap/launcher_icon">
<service
android:name=".WearSyncForegroundService"
android:exported="false"
android:foregroundServiceType="dataSync" />
<activity
android:name=".MainActivity"

View File

@@ -1,99 +1,5 @@
package app.firka.naplo
import android.annotation.SuppressLint
import android.app.Application
import android.os.Build
import android.util.Log
import org.brotli.dec.BrotliInputStream
import org.json.JSONObject
import java.io.File
import java.io.FileOutputStream
import java.security.MessageDigest
import java.util.zip.ZipFile
class AppMain : Application() {
private fun File.sha256(): String {
if (!exists()) return "0000000000000000000000000000000000000000000000000000000000000000"
val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(this.readBytes())
return digest.fold("") { str, it -> str + "%02x".format(it) }
}
@SuppressLint("UnsafeDynamicallyLoadedCode")
override fun onCreate() {
super.onCreate()
var useUncompressedLibs = false
val abi = Build.SUPPORTED_ABIS[0]
val apks = File(applicationInfo.nativeLibraryDir, "../..").absoluteFile
.listFiles()!!
.filter { file -> file.name.endsWith(".apk") }
.toList()
var nativesApkN: ZipFile? = null
for (apk in apks) {
if (nativesApkN != null) break
val zip = ZipFile(apk)
val entries = zip.entries()
while (entries.hasMoreElements()) {
val entry = entries.nextElement()
if (entry.name.endsWith("$abi/index.so")) {
zip.close()
nativesApkN = ZipFile(apk)
break
}
if (entry.name.endsWith("$abi/libflutter.so")) {
useUncompressedLibs = true
break
}
}
zip.close()
}
if (useUncompressedLibs) {
return;
}
if (nativesApkN == null) {
throw Exception("Can't find native libraries")
}
val nativesApk: ZipFile = nativesApkN
val compressedLibsIndex = nativesApk.getInputStream(
nativesApk.getEntry("lib/$abi/index.so")
)
val compressedLibs = JSONObject(compressedLibsIndex.readBytes().toString(Charsets.UTF_8))
for (so in compressedLibs.keys()) {
val soFile = File(cacheDir, so)
if (soFile.sha256() == compressedLibs.getString(so)) {
System.load(soFile.absolutePath)
return
}
Log.d("AppMain", "Decompressing: $so")
val brInput = nativesApk.getInputStream(
nativesApk.getEntry("lib/$abi/${so.replace(".so", "-br.so")}")
)
val soOutput = FileOutputStream(soFile)
val brIn = BrotliInputStream(brInput)
brIn.copyTo(soOutput)
brInput.close()
soOutput.close()
System.load(soFile.absolutePath)
}
}
}
class AppMain : Application() {}

View File

@@ -1,19 +1,28 @@
package app.firka.naplo
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.glance.appwidget.updateAll
import app.firka.naplo.glance.TimetableWidget
import app.firka.naplo.glance.TimetableWidgetReceiver
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlin.system.exitProcess
class MainActivity : FlutterActivity() {
private val channel = "firka.app/main"
private val wearSyncChannel = "app.firka/wear_sync"
private fun forceIconUpdate() {
try {
@@ -30,6 +39,57 @@ class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, wearSyncChannel).setMethodCallHandler { call, result ->
when (call.method) {
"startWearSyncService" -> {
val args = call.arguments as? Map<*, *>
val cachePath = args?.get("cachePath") as? String
val appDirPath = args?.get("appDirPath") as? String
if (cachePath != null && appDirPath != null) {
val messenger = flutterEngine.dartExecutor.binaryMessenger
val ch = MethodChannel(messenger, wearSyncChannel)
ch.invokeMethod("getLocalizedString", "wearSyncNotificationTitle", object : MethodChannel.Result {
override fun success(titleResult: Any?) {
val title = titleResult as? String ?: "Syncing with watch"
ch.invokeMethod("getLocalizedString", "wearSyncNotificationText", object : MethodChannel.Result {
override fun success(textResult: Any?) {
val text = textResult as? String ?: ""
val intent = Intent(this@MainActivity, WearSyncForegroundService::class.java).apply {
action = WearSyncForegroundService.ACTION_START
putExtra(WearSyncForegroundService.EXTRA_CACHE_PATH, cachePath)
putExtra(WearSyncForegroundService.EXTRA_APP_DIR_PATH, appDirPath)
putExtra(WearSyncForegroundService.EXTRA_NOTIFICATION_TITLE, title)
putExtra(WearSyncForegroundService.EXTRA_NOTIFICATION_TEXT, text)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
result.success(null)
}
override fun error(code: String, msg: String?, details: Any?) { result.success(null) }
override fun notImplemented() { result.success(null) }
})
}
override fun error(code: String, msg: String?, details: Any?) { result.error(code, msg, details) }
override fun notImplemented() { result.notImplemented() }
})
} else {
result.error("INVALID_ARGS", "cachePath and appDirPath required", null)
}
}
"stopWearSyncService" -> {
val intent = Intent(this, WearSyncForegroundService::class.java).apply {
action = WearSyncForegroundService.ACTION_STOP
}
startService(intent)
result.success(null)
}
else -> result.notImplemented()
}
}
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channel).setMethodCallHandler {
call, result ->
when (call.method) {
@@ -97,7 +157,29 @@ class MainActivity : FlutterActivity() {
} catch (e: Exception) {
e.printStackTrace()
}
result.success(true)
result.success(true)
}
"refreshTimetableWidget" -> {
CoroutineScope(SupervisorJob() + Dispatchers.Default).launch {
try {
val appContext = context.applicationContext
val appWidgetManager = AppWidgetManager.getInstance(appContext)
val componentName = ComponentName(appContext, TimetableWidgetReceiver::class.java)
val ids = appWidgetManager.getAppWidgetIds(componentName)
if (ids.isNotEmpty()) {
val intent = Intent(appContext, TimetableWidgetReceiver::class.java).apply {
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
}
appContext.sendBroadcast(intent)
}
TimetableWidget().updateAll(appContext)
result.success(true)
} catch (e: Exception) {
result.error("refresh_failed", e.message, null)
}
}
}
else -> {
result.notImplemented()

View File

@@ -0,0 +1,221 @@
package app.firka.naplo
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.NotificationCompat
import java.io.ByteArrayInputStream
import java.io.ObjectInputStream
import com.google.android.gms.wearable.MessageClient
import com.google.android.gms.wearable.MessageEvent
import com.google.android.gms.wearable.Wearable
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
import io.flutter.embedding.engine.loader.FlutterLoader
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeout
import kotlinx.coroutines.delay
/**
* Foreground service that keeps the app able to respond to Wear OS sync requests.
* When the watch sends request_sync, starts a Dart background isolate to fetch data,
* then reads the cache file and sends sync_data to the watch.
*/
class WearSyncForegroundService : Service(), MessageClient.OnMessageReceivedListener {
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
private var cachePath: String? = null
private var appDirPath: String? = null
private val channelId = "firka_wear_sync"
private val notificationId = 4001
private var notificationTitle: String = "Syncing with watch"
private var notificationText: String = ""
override fun onCreate() {
super.onCreate()
createNotificationChannel()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
when (intent?.action) {
ACTION_START -> {
cachePath = intent.getStringExtra(EXTRA_CACHE_PATH)
appDirPath = intent.getStringExtra(EXTRA_APP_DIR_PATH)
notificationTitle = intent.getStringExtra(EXTRA_NOTIFICATION_TITLE) ?: "Syncing with watch"
notificationText = intent.getStringExtra(EXTRA_NOTIFICATION_TEXT) ?: ""
startForegroundWithNotification()
Wearable.getMessageClient(this@WearSyncForegroundService)
.addListener(this@WearSyncForegroundService)
}
ACTION_STOP -> {
stopForegroundService()
return START_NOT_STICKY
}
}
return START_STICKY
}
override fun onBind(intent: Intent?): IBinder? = null
override fun onDestroy() {
try {
Wearable.getMessageClient(this@WearSyncForegroundService)
.removeListener(this@WearSyncForegroundService)
.addOnCompleteListener { }
} catch (_: Exception) { }
scope.cancel()
super.onDestroy()
}
override fun onMessageReceived(messageEvent: MessageEvent) {
if (messageEvent.path != PATH_WATCH_CONNECTIVITY ||
!isRequestSyncPayload(messageEvent.data)
) return
val cPath = cachePath
val aPath = appDirPath
if (cPath == null || aPath == null) return
scope.launch {
runSyncInBackground(cPath, aPath)
}
}
/**
* watch_connectivity plugin sends with path "watch_connectivity" and serializes the message
* map with Java ObjectOutputStream. Parse payload and check for id == "request_sync".
*/
private fun isRequestSyncPayload(data: ByteArray?): Boolean {
if (data == null || data.isEmpty()) return false
return try {
ObjectInputStream(ByteArrayInputStream(data)).use { ois ->
val map = ois.readObject()
if (map is Map<*, *>) map["id"] == "request_sync" else false
}
} catch (_: Exception) {
false
}
}
private fun startForegroundWithNotification() {
val notification = buildNotification()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
startForeground(notificationId, notification, android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
} else {
@Suppress("DEPRECATION")
startForeground(notificationId, notification)
}
}
private fun buildNotification(): Notification {
val pendingIntent = PendingIntent.getActivity(
this,
0,
packageManager.getLaunchIntentForPackage(packageName),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
return NotificationCompat.Builder(this, channelId)
.setContentTitle(notificationTitle)
.setContentText(notificationText)
.setSmallIcon(R.drawable.ic_notification)
.setContentIntent(pendingIntent)
.setOngoing(true)
.build()
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
channelId,
"Wear sync",
NotificationManager.IMPORTANCE_LOW
).apply { setShowBadge(false) }
(getSystemService(NOTIFICATION_SERVICE) as NotificationManager)
.createNotificationChannel(channel)
}
}
private fun stopForegroundService() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
stopForeground(STOP_FOREGROUND_REMOVE)
} else {
@Suppress("DEPRECATION")
stopForeground(true)
}
stopSelf()
}
private suspend fun runSyncInBackground(cPath: String, aPath: String) = withContext(Dispatchers.Default) {
val flutterLoader = FlutterLoader()
if (!flutterLoader.initialized()) {
withContext(Dispatchers.Main) {
flutterLoader.startInitialization(applicationContext)
flutterLoader.ensureInitializationComplete(applicationContext, null)
}
}
val (engine, bgChannel) = withContext(Dispatchers.Main) {
val eng = FlutterEngine(applicationContext)
val entrypoint = DartExecutor.DartEntrypoint(
flutterLoader.findAppBundlePath(),
"package:firka/services/wear_sync_background.dart",
"wearSyncBackgroundEntrypoint"
)
eng.dartExecutor.executeDartEntrypoint(entrypoint)
val ch = MethodChannel(eng.dartExecutor.binaryMessenger, "app.firka/wear_sync_background")
Pair(eng, ch)
}
val completer = CompletableDeferred<Unit>()
delay(500)
withContext(Dispatchers.Main) {
bgChannel.invokeMethod("request_sync", mapOf(
"cachePath" to cPath,
"appDirPath" to aPath
), object : MethodChannel.Result {
override fun success(result: Any?) {
completer.complete(Unit)
}
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
Log.e(TAG, "request_sync error: $errorCode $errorMessage")
completer.complete(Unit)
}
override fun notImplemented() {
completer.complete(Unit)
}
})
}
try {
withTimeout(30_000) {
completer.await()
}
} catch (_: kotlinx.coroutines.TimeoutCancellationException) {
Log.w(TAG, "Wear sync isolate timed out")
}
withContext(Dispatchers.Main) {
engine.destroy()
}
}
companion object {
private const val TAG = "WearSyncService"
const val ACTION_START = "app.firka.naplo.WearSyncForegroundService.START"
const val ACTION_STOP = "app.firka.naplo.WearSyncForegroundService.STOP"
const val EXTRA_CACHE_PATH = "cachePath"
const val EXTRA_APP_DIR_PATH = "appDirPath"
const val EXTRA_NOTIFICATION_TITLE = "notificationTitle"
const val EXTRA_NOTIFICATION_TEXT = "notificationText"
private const val PATH_WATCH_CONNECTIVITY = "watch_connectivity"
}
}

View File

@@ -18,7 +18,7 @@ import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import app.firka.naplo.model.Colors
import app.firka.naplo.model.Lesson
import app.firka.naplo.glance.WidgetLesson
import java.time.format.DateTimeFormatterBuilder
val hhmm = DateTimeFormatterBuilder()
@@ -26,8 +26,12 @@ val hhmm = DateTimeFormatterBuilder()
.toFormatter()
@Composable
fun LessonCard(lesson: Lesson, colors: Colors,
modifier: GlanceModifier = GlanceModifier) {
fun LessonCard(
lesson: WidgetLesson,
colors: Colors,
modifier: GlanceModifier = GlanceModifier,
roomBadgeWidthDp: Float = 48f,
) {
Box(modifier =
modifier
.fillMaxWidth()
@@ -38,7 +42,7 @@ fun LessonCard(lesson: Lesson, colors: Colors,
var bgColor = colors.a15p
var fgColor = colors.textSecondary
if (lesson.substituteTeacher == null) {
if (lesson.substituteTeacher != null) {
bgColor = colors.warning15p
fgColor = colors.warningText
}
@@ -46,9 +50,21 @@ fun LessonCard(lesson: Lesson, colors: Colors,
Box(modifier = GlanceModifier.padding(12.dp)) {
Row {
val badgeStyle = TextStyle(
color = ColorProvider(colors.textSecondary, colors.textSecondary),
fontSize = 14.sp,
fontWeight = FontWeight.Bold
)
val badgePadding = GlanceModifier.padding(8.dp, 4.dp)
val lessonNumberBadgeModifier = GlanceModifier.cornerRadius(16.dp).width(24.dp)
val roomBadgeModifier = GlanceModifier.cornerRadius(16.dp).width(roomBadgeWidthDp.dp)
Row(modifier = GlanceModifier.width(226.dp), verticalAlignment = Alignment.CenterVertically) {
if (lesson.lessonNumber != null) {
Box(modifier = GlanceModifier.cornerRadius(16.dp).background(bgColor)) {
Box(
modifier = lessonNumberBadgeModifier.background(bgColor),
contentAlignment = Alignment.Center,
) {
Text(
lesson.lessonNumber.toString(),
style = TextStyle(
@@ -56,7 +72,7 @@ fun LessonCard(lesson: Lesson, colors: Colors,
fontSize = 14.sp,
fontWeight = FontWeight.Bold
),
modifier = GlanceModifier.padding(8.dp, 4.dp),
modifier = GlanceModifier.padding(4.dp, 4.dp),
)
}
Spacer(modifier = GlanceModifier.width(4.dp))
@@ -72,8 +88,6 @@ fun LessonCard(lesson: Lesson, colors: Colors,
)
}
// Spacer(modifier = GlanceModifier.width(10.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
lesson.start.format(hhmm),
@@ -84,24 +98,15 @@ fun LessonCard(lesson: Lesson, colors: Colors,
),
)
Spacer(modifier = GlanceModifier.width(8.dp))
Box(modifier = GlanceModifier.cornerRadius(16.dp).background(colors.a15p)) {
var roomName = "N/A";
if (lesson.roomName != null) {
roomName = lesson.roomName!!;
}
if (roomName.length < 2) {
roomName = " $roomName"
}
val roomName = (lesson.roomName ?: "N/A").take(5)
Box(
modifier = roomBadgeModifier.background(colors.a15p),
contentAlignment = Alignment.Center,
) {
Text(
roomName,
style = TextStyle(
color = ColorProvider(colors.textSecondary, colors.textSecondary),
fontSize = 14.sp,
fontWeight = FontWeight.Bold
),
modifier = GlanceModifier.padding(8.dp, 4.dp),
style = badgeStyle,
modifier = GlanceModifier.padding(4.dp, 4.dp),
)
}
}

View File

@@ -9,8 +9,10 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.LocalSize
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.provideContent
import androidx.glance.appwidget.SizeMode
import androidx.glance.background
import androidx.glance.color.ColorProvider
import androidx.glance.currentState
@@ -26,7 +28,9 @@ import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import app.firka.naplo.model.Colors
import app.firka.naplo.model.Lesson
import app.firka.naplo.glance.WidgetLesson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.File
import java.time.LocalDate
@@ -37,20 +41,51 @@ class TimetableWidget : GlanceAppWidget() {
override val stateDefinition: GlanceStateDefinition<*>?
get() = HomeWidgetGlanceStateDefinition()
override val sizeMode: SizeMode
get() = SizeMode.Exact
override suspend fun provideGlance(context: Context, id: GlanceId) {
val data = withContext(Dispatchers.IO) {
loadWidgetData(context)
}
provideContent {
GlanceContent(context, currentState())
GlanceContent(context, currentState(), data)
}
}
@Composable
private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) {
private fun loadWidgetData(context: Context): WidgetData? {
val appFlutter = File(context.applicationContext.dataDir, "app_flutter")
val widgetStateFile = File(appFlutter, "widget_state.json")
if (!widgetStateFile.exists()) return null
val widgetState = JSONObject(widgetStateFile.readText(Charsets.UTF_8))
val colors = Colors(widgetState)
val tt = widgetState.getJSONArray("timetable")
val lessons = mutableListOf<WidgetLesson>()
for (i in 0..<tt.length()) {
lessons.add(WidgetLesson(tt.getJSONObject(i)))
}
val displayDateStr = widgetState.optString("displayDate", "")
val targetDate = if (displayDateStr.isNotEmpty()) {
try {
LocalDate.parse(displayDateStr)
} catch (_: Exception) {
LocalDate.now()
}
} else {
LocalDate.now()
}
val start = LocalDateTime.of(targetDate.year, targetDate.month, targetDate.dayOfMonth, 0, 0)
val end = start.plusHours(23)
val filtered = lessons.filter { it.start.isAfter(start) && it.end.isBefore(end) }
val headerText = if (displayDateStr.isNotEmpty()) displayDateStr else "Mai órarend"
return WidgetData(colors, headerText, filtered)
}
if (!widgetStateFile.exists()) {
Box(modifier =
GlanceModifier
@Composable
private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState, data: WidgetData?) {
if (data == null) {
Box(
modifier = GlanceModifier
.background(Color(0xFFFAFFF0))
.padding(16.dp)
.fillMaxSize(),
@@ -65,47 +100,67 @@ class TimetableWidget : GlanceAppWidget() {
)
)
}
return
}
val widgetState = JSONObject(widgetStateFile.readText(Charsets.UTF_8))
val colors = Colors(widgetState)
val tt = widgetState.getJSONArray("timetable")
var lessons = mutableListOf<Lesson>()
for (i in 0..<tt.length()) {
lessons.add(Lesson(tt.getJSONObject(i)))
val size = LocalSize.current
val lessonRowHeightDp = 52f
val scale = lessonRowHeightDp / 52f
val headerHeightDp = 20f * scale
val verticalPaddingDp = 32f * scale
val spacerDp = 4f * scale
val paddingDp = 16f * scale
val availableHeightDp = size.height.value - verticalPaddingDp - headerHeightDp - spacerDp
val maxVisibleLessons = (availableHeightDp / lessonRowHeightDp).toInt().coerceAtLeast(0)
val maxLessons = (maxVisibleLessons.coerceAtMost(16) / 2 * 2).coerceAtLeast(1)
val displayLessons = data.lessons.take(maxLessons)
val lessonChunks = displayLessons.chunked(2)
val showDate = maxLessons > 1
val maxRoomNameLen = displayLessons.maxOfOrNull { (it.roomName ?: "N/A").take(5).length } ?: 0
val roomBadgeWidthDp = if (maxRoomNameLen <= 3) 28f else 48f
val dateSectionHeight = if (showDate) headerHeightDp + spacerDp else 0f
val lessonListHeight = when (val n = displayLessons.size) {
0 -> 0f
else -> n * lessonRowHeightDp + (n - 1) * spacerDp
}
val remainingHeight = (size.height.value - 2 * paddingDp - dateSectionHeight - lessonListHeight).coerceAtLeast(0f)
val verticalPaddingAroundLessons = remainingHeight / 2f
val now = LocalDate.now()
val start = LocalDateTime.of(now.year, now.month, now.dayOfMonth, 0, 0)
val end = start.plusHours(23)
lessons = lessons.filter { lesson -> lesson.start.isAfter(start) && lesson.end.isBefore(end) }.toMutableList()
Box(modifier =
GlanceModifier
.background(colors.background)
.padding(16.dp)
Box(
modifier = GlanceModifier
.background(data.colors.background)
.padding(paddingDp.dp)
.fillMaxSize()
) {
Column {
Text(
"Mai órarend",
style = TextStyle(
color = ColorProvider(colors.textSecondary, colors.textSecondary),
fontSize = 12.sp,
fontWeight = FontWeight.Medium
if (showDate) {
Text(
data.headerText,
style = TextStyle(
color = ColorProvider(data.colors.textSecondary, data.colors.textSecondary),
fontSize = 12.sp,
fontWeight = FontWeight.Medium
)
)
)
Spacer(modifier = GlanceModifier.height(4.dp))
for (lesson in lessons) {
LessonCard(lesson, colors)
Spacer(modifier = GlanceModifier.height(4.dp))
Spacer(modifier = GlanceModifier.height(spacerDp.dp))
}
Spacer(modifier = GlanceModifier.height(verticalPaddingAroundLessons.dp))
for (chunk in lessonChunks) {
Column {
for (lesson in chunk) {
LessonCard(lesson, data.colors, roomBadgeWidthDp = roomBadgeWidthDp)
Spacer(modifier = GlanceModifier.height(spacerDp.dp))
}
}
}
Spacer(modifier = GlanceModifier.height(verticalPaddingAroundLessons.dp))
}
}
}
}
}
private data class WidgetData(
val colors: Colors,
val headerText: String,
val lessons: List<WidgetLesson>,
)

View File

@@ -1,7 +1,25 @@
package app.firka.naplo.glance
import android.appwidget.AppWidgetManager
import android.content.Context
import android.os.Bundle
import HomeWidgetGlanceWidgetReceiver
import androidx.glance.appwidget.GlanceAppWidgetManager
import kotlinx.coroutines.runBlocking
class TimetableWidgetReceiver : HomeWidgetGlanceWidgetReceiver<TimetableWidget>() {
override val glanceAppWidget = TimetableWidget()
override fun onAppWidgetOptionsChanged(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
newOptions: Bundle,
) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
runBlocking {
val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(appWidgetId)
glanceAppWidget.update(context, glanceId)
}
}
}

View File

@@ -0,0 +1,23 @@
package app.firka.naplo.glance
import app.firka.naplo.getIntOrNull
import app.firka.naplo.getStringOrNull
import org.json.JSONObject
import java.time.LocalDateTime
import java.time.format.DateTimeFormatterBuilder
class WidgetLesson(data: JSONObject) {
val formatter = DateTimeFormatterBuilder()
.appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS")
.optionalStart()
.appendLiteral('Z')
.optionalEnd()
.toFormatter()
val name: String = data.getString("Nev")
val start: LocalDateTime = LocalDateTime.parse(data.getString("KezdetIdopont"), formatter)
val end: LocalDateTime = LocalDateTime.parse(data.getString("VegIdopont"), formatter)
val lessonNumber: Int? = data.getIntOrNull("Oraszam")
val roomName: String? = data.getStringOrNull("TeremNeve")
val substituteTeacher: String? = data.getStringOrNull("HelyettesTanarNeve")
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 B

View File

@@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
<item>
<bitmap android:gravity="fill" android:src="@drawable/background"/>
</item>
<item>
<bitmap android:gravity="center" android:src="@drawable/splash"/>
</item>
</layer-list>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#7ca120</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
<item name="android:windowSplashScreenIconBackgroundColor">#7ca120</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -5,6 +5,10 @@
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground">#7ca120</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
<item name="android:windowSplashScreenIconBackgroundColor">#7ca120</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@@ -5,6 +5,10 @@
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
<item name="android:forceDarkAllowed">false</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your

View File

@@ -1,7 +1,9 @@
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/glance_default_loading_layout"
android:minWidth="300dp"
android:minHeight="100dp"
android:minWidth="250dp"
android:minHeight="93dp"
android:minResizeWidth="180dp"
android:minResizeHeight="93dp"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="10000">
android:updatePeriodMillis="1800000">
</appwidget-provider>

View File

@@ -22,8 +22,8 @@ subprojects {
if (plugins.hasPlugin("com.android.application") || plugins.hasPlugin("com.android.library")) {
val androidExtension = extensions.getByName("android") as BaseExtension
androidExtension.apply {
compileSdkVersion(35)
buildToolsVersion = "35.0.0"
compileSdkVersion(36)
buildToolsVersion = "36.1.0"
}
}
if (hasProperty("android")) {
@@ -40,9 +40,6 @@ subprojects {
project.layout.buildDirectory.value(newSubprojectBuildDir)
}
subprojects {
project.evaluationDependsOn(":app")
}
tasks.register<Delete>("clean") {
delete(rootProject.layout.buildDirectory)

View File

@@ -1,3 +1,13 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
# Disabled for faster config and incremental builds; re-enable if any dependency needs support-library
android.enableJetifier=false
# Build performance (cold and warm)
org.gradle.caching=true
org.gradle.parallel=true
# Configuration cache disabled: Flutter/AGP/Kotlin plugin not fully compatible (KotlinBaseApiPlugin / ProjectServices)
# org.gradle.configuration-cache=true
org.gradle.daemon=true
# Better Kotlin incremental compilation (warm builds)
kotlin.incremental.useClasspathSnapshot=true

View File

@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip

View File

@@ -18,8 +18,9 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.7.0" apply false
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false
}
include(":app")

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.44866 0.51265C7.82192 0.400678 8.21619 0.377515 8.59999 0.44501C8.98379 0.512504 9.3465 0.668787 9.65917 0.901383C9.97183 1.13398 10.2258 1.43645 10.4008 1.78464C10.5758 2.13284 10.6669 2.51712 10.667 2.90682V15.0935C10.6669 15.4832 10.5758 15.8675 10.4008 16.2157C10.2258 16.5639 9.97183 16.8663 9.65917 17.0989C9.3465 17.3315 8.98379 17.4878 8.59999 17.5553C8.21619 17.6228 7.82192 17.5996 7.44866 17.4876L2.44866 15.9876C1.93374 15.8332 1.48233 15.5169 1.16139 15.0856C0.840454 14.6543 0.66708 14.1311 0.666992 13.5935V4.40682C0.66708 3.86923 0.840454 3.34599 1.16139 2.91472C1.48233 2.48345 1.93374 2.16712 2.44866 2.01265L7.44866 0.51265ZM11.5003 2.33348C11.5003 2.11247 11.5881 1.90051 11.7444 1.74423C11.9007 1.58795 12.1126 1.50015 12.3337 1.50015H14.8337C15.4967 1.50015 16.1326 1.76354 16.6014 2.23238C17.0703 2.70122 17.3337 3.33711 17.3337 4.00015V4.83348C17.3337 5.0545 17.2459 5.26646 17.0896 5.42274C16.9333 5.57902 16.7213 5.66682 16.5003 5.66682C16.2793 5.66682 16.0673 5.57902 15.9111 5.42274C15.7548 5.26646 15.667 5.0545 15.667 4.83348V4.00015C15.667 3.77914 15.5792 3.56717 15.4229 3.41089C15.2666 3.25461 15.0547 3.16682 14.8337 3.16682H12.3337C12.1126 3.16682 11.9007 3.07902 11.7444 2.92274C11.5881 2.76646 11.5003 2.5545 11.5003 2.33348ZM16.5003 12.3335C16.7213 12.3335 16.9333 12.4213 17.0896 12.5776C17.2459 12.7338 17.3337 12.9458 17.3337 13.1668V14.0001C17.3337 14.6632 17.0703 15.2991 16.6014 15.7679C16.1326 16.2368 15.4967 16.5001 14.8337 16.5001H12.3337C12.1126 16.5001 11.9007 16.4124 11.7444 16.2561C11.5881 16.0998 11.5003 15.8878 11.5003 15.6668C11.5003 15.4458 11.5881 15.2338 11.7444 15.0776C11.9007 14.9213 12.1126 14.8335 12.3337 14.8335H14.8337C15.0547 14.8335 15.2666 14.7457 15.4229 14.5894C15.5792 14.4331 15.667 14.2212 15.667 14.0001V13.1668C15.667 12.9458 15.7548 12.7338 15.9111 12.5776C16.0673 12.4213 16.2793 12.3335 16.5003 12.3335ZM6.50033 8.16682C6.27931 8.16682 6.06735 8.25461 5.91107 8.41089C5.75479 8.56717 5.66699 8.77914 5.66699 9.00015C5.66699 9.22116 5.75479 9.43312 5.91107 9.58941C6.06735 9.74569 6.27931 9.83348 6.50033 9.83348H6.50116C6.72217 9.83348 6.93413 9.74569 7.09041 9.58941C7.2467 9.43312 7.33449 9.22116 7.33449 9.00015C7.33449 8.77914 7.2467 8.56717 7.09041 8.41089C6.93413 8.25461 6.72217 8.16682 6.50116 8.16682H6.50033Z" fill="#A7DC22"/>
<path d="M12.334 8.99967H16.5007M16.5007 8.99967L14.834 7.33301M16.5007 8.99967L14.834 10.6663" stroke="#A7DC22" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,3 @@
<svg width="17" height="18" viewBox="0 0 17 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.6372 4.23604C15.047 1.69217 12.2211 0 9 0C4.02944 0 0 4.02944 0 9C0 13.9706 4.02944 18 9 18C12.2211 18 15.047 16.3078 16.6372 13.764C16.2218 12.2466 16 10.6492 16 9C16 7.35081 16.2218 5.75343 16.6372 4.23604Z" fill="#000000" />
</svg>

After

Width:  |  Height:  |  Size: 384 B

View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M0 8C0 3.58172 3.58172 0 8 0V0C12.4183 0 16 3.58172 16 8V8C16 12.4183 12.4183 16 8 16H2C0.895431 16 0 15.1046 0 14V8Z" fill="#000000"/>
</svg>

After

Width:  |  Height:  |  Size: 248 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,6 @@
<svg width="108" height="48" viewBox="0 0 108 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="108" height="48" rx="12" fill="#F3FBDE"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M30.6555 15.6657C30.968 15.3533 31.3919 15.1777 31.8338 15.1777C32.2758 15.1777 32.6996 15.3533 33.0122 15.6657L34.3338 16.9874C34.6463 17.2999 34.8218 17.7238 34.8218 18.1657C34.8218 18.6077 34.6463 19.0315 34.3338 19.344L33.0122 20.6657L29.3338 16.9874L30.6555 15.6657ZM28.1555 18.1657L23.9888 22.3324C23.6762 22.6449 23.5006 23.0687 23.5005 23.5107V24.8324C23.5005 25.2744 23.6761 25.6983 23.9886 26.0109C24.3012 26.3235 24.7251 26.499 25.1672 26.499H26.4888C26.9308 26.499 27.3547 26.3233 27.6672 26.0107L31.8338 21.844L28.1555 18.1657Z" fill="#A7DC22"/>
<path d="M21.0005 25.666H20.1672C19.7251 25.666 19.3012 25.8416 18.9886 26.1542C18.6761 26.4667 18.5005 26.8907 18.5005 27.3327C18.5005 27.7747 18.6761 28.1986 18.9886 28.5112C19.3012 28.8238 19.7251 28.9993 20.1672 28.9993H31.8338C32.2758 28.9993 32.6998 29.1749 33.0123 29.4875C33.3249 29.8001 33.5005 30.224 33.5005 30.666C33.5005 31.108 33.3249 31.532 33.0123 31.8445C32.6998 32.1571 32.2758 32.3327 31.8338 32.3327H28.5005" stroke="#A7DC22" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M47.648 29.5V19.996H44.208V18.3H52.944V19.996H49.504V29.5H47.648ZM56.7006 29.692C55.922 29.692 55.2286 29.516 54.6206 29.164C54.0233 28.8013 53.5486 28.3053 53.1966 27.676C52.8553 27.0467 52.6846 26.3213 52.6846 25.5C52.6846 24.6787 52.8606 23.9533 53.2126 23.324C53.5646 22.6947 54.0446 22.204 54.6526 21.852C55.2713 21.4893 55.9753 21.308 56.7646 21.308C57.4793 21.308 58.1246 21.4947 58.7006 21.868C59.2766 22.2307 59.73 22.7587 60.0606 23.452C60.402 24.1453 60.5726 24.972 60.5726 25.932H54.2846L54.5246 25.708C54.5246 26.1987 54.6313 26.6253 54.8446 26.988C55.058 27.34 55.3406 27.612 55.6926 27.804C56.0446 27.996 56.434 28.092 56.8606 28.092C57.3513 28.092 57.7566 27.9853 58.0766 27.772C58.3966 27.548 58.6473 27.26 58.8286 26.908L60.4126 27.58C60.1886 28.0067 59.9006 28.38 59.5486 28.7C59.2073 29.02 58.7966 29.2653 58.3166 29.436C57.8473 29.6067 57.3086 29.692 56.7006 29.692ZM54.6366 24.78L54.3806 24.556H58.8926L58.6526 24.78C58.6526 24.3427 58.5566 23.9853 58.3646 23.708C58.1726 23.42 57.9273 23.2067 57.6286 23.068C57.3406 22.9187 57.0366 22.844 56.7166 22.844C56.3966 22.844 56.0766 22.9187 55.7566 23.068C55.4366 23.2067 55.17 23.42 54.9566 23.708C54.7433 23.9853 54.6366 24.3427 54.6366 24.78ZM55.5326 20.46L57.3886 18.3H59.4686L57.4046 20.46H55.5326ZM61.9764 29.5V21.5H63.6564L63.7364 22.572C63.9817 22.156 64.2964 21.8413 64.6804 21.628C65.0644 21.4147 65.5017 21.308 65.9924 21.308C66.6324 21.308 67.1764 21.452 67.6244 21.74C68.0724 22.028 68.3977 22.4653 68.6004 23.052C68.835 22.4867 69.1657 22.0547 69.5924 21.756C70.019 21.4573 70.5204 21.308 71.0964 21.308C72.0244 21.308 72.739 21.6067 73.2404 22.204C73.7417 22.7907 73.987 23.6973 73.9764 24.924V29.5H72.2004V25.404C72.2004 24.764 72.131 24.2733 71.9924 23.932C71.8537 23.58 71.667 23.3347 71.4324 23.196C71.1977 23.0573 70.9257 22.988 70.6164 22.988C70.0617 22.9773 69.6297 23.1747 69.3204 23.58C69.0217 23.9853 68.8724 24.5667 68.8724 25.324V29.5H67.0804V25.404C67.0804 24.764 67.011 24.2733 66.8724 23.932C66.7444 23.58 66.563 23.3347 66.3284 23.196C66.0937 23.0573 65.8217 22.988 65.5124 22.988C64.9577 22.9773 64.5257 23.1747 64.2164 23.58C63.9177 23.9853 63.7684 24.5667 63.7684 25.324V29.5H61.9764ZM80.6845 29.5L80.6045 27.996V25.388C80.6045 24.844 80.5458 24.3907 80.4285 24.028C80.3218 23.6547 80.1405 23.372 79.8845 23.18C79.6392 22.9773 79.3085 22.876 78.8925 22.876C78.5085 22.876 78.1725 22.956 77.8845 23.116C77.5965 23.276 77.3512 23.5267 77.1485 23.868L75.5805 23.292C75.7512 22.94 75.9752 22.6147 76.2525 22.316C76.5405 22.0067 76.8978 21.7613 77.3245 21.58C77.7618 21.3987 78.2845 21.308 78.8925 21.308C79.6712 21.308 80.3218 21.4627 80.8445 21.772C81.3672 22.0707 81.7512 22.5027 81.9965 23.068C82.2525 23.6333 82.3805 24.316 82.3805 25.116L82.3325 29.5H80.6845ZM78.3805 29.692C77.4205 29.692 76.6738 29.4787 76.1405 29.052C75.6178 28.6253 75.3565 28.0227 75.3565 27.244C75.3565 26.412 75.6338 25.7773 76.1885 25.34C76.7538 24.9027 77.5378 24.684 78.5405 24.684H80.6845V26.06H79.1165C78.4018 26.06 77.9005 26.1613 77.6125 26.364C77.3245 26.556 77.1805 26.8333 77.1805 27.196C77.1805 27.5053 77.3032 27.7507 77.5485 27.932C77.8045 28.1027 78.1565 28.188 78.6045 28.188C79.0098 28.188 79.3618 28.0973 79.6605 27.916C79.9592 27.7347 80.1885 27.4947 80.3485 27.196C80.5192 26.8973 80.6045 26.5613 80.6045 26.188H81.1325C81.1325 27.276 80.9138 28.1347 80.4765 28.764C80.0392 29.3827 79.3405 29.692 78.3805 29.692ZM77.5965 20.46L79.4525 18.3H81.5325L79.4685 20.46H77.5965ZM85.829 27.26L84.741 26.028L88.901 21.5H91.061L85.829 27.26ZM84.117 29.5V18.3H85.909V29.5H84.117ZM89.205 29.5L86.389 25.356L87.557 24.108L91.333 29.5H89.205Z" fill="#394C0A"/>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,5 @@
<svg width="108" height="48" viewBox="0 0 108 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="108" height="48" rx="12" fill="#F3FBDE"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M26.8026 15.9055C26.7524 15.8382 26.6891 15.7817 26.6166 15.7394C26.544 15.6971 26.4637 15.6699 26.3804 15.6593C26.2971 15.6487 26.2125 15.655 26.1317 15.6778C26.0509 15.7007 25.9755 15.7396 25.9101 15.7922C24.3129 17.0742 23.2594 18.9127 22.9609 20.9388C22.4141 20.5413 21.9344 20.0589 21.5401 19.5097C21.4866 19.4349 21.4173 19.3729 21.3371 19.328C21.2569 19.2831 21.1678 19.2564 21.0761 19.2499C20.9845 19.2434 20.8925 19.2572 20.8067 19.2902C20.721 19.3233 20.6436 19.3749 20.5801 19.4413C19.4795 20.5926 18.774 22.0644 18.5658 23.6435C18.3576 25.2225 18.6575 26.8268 19.4221 28.2241C20.1866 29.6213 21.3761 30.7388 22.8182 31.4148C24.2604 32.0908 25.8803 32.2902 27.4433 31.984C29.0063 31.6778 30.4312 30.882 31.5117 29.7118C32.5922 28.5416 33.2721 27.0579 33.453 25.4755C33.6338 23.893 33.3062 22.2941 32.5176 20.9104C31.729 19.5266 30.5204 18.4298 29.0667 17.7788C28.1732 17.3443 27.3967 16.7019 26.8026 15.9055ZM29.1251 25.8755C29.1248 26.3275 29.0264 26.7741 28.8367 27.1844C28.6471 27.5948 28.3707 27.959 28.0266 28.2522C27.6825 28.5453 27.2789 28.7603 26.8437 28.8823C26.4084 29.0044 25.9519 29.0305 25.5056 28.959C25.0592 28.8875 24.6337 28.7199 24.2584 28.468C23.8831 28.2161 23.5669 27.8857 23.3317 27.4998C23.0964 27.1138 22.9477 26.6814 22.8958 26.2323C22.8438 25.7833 22.8899 25.3283 23.0309 24.8988C23.5542 25.2863 24.1559 25.5738 24.8084 25.7322C24.986 24.5915 25.5528 23.5475 26.4126 22.7772C27.1634 22.8772 27.8524 23.2465 28.3513 23.8164C28.8503 24.3863 29.1252 25.118 29.1251 25.8755Z" fill="#A7DC22"/>
<path d="M49.008 29.692C48.4213 29.692 47.8827 29.6227 47.392 29.484C46.912 29.3453 46.4853 29.1533 46.112 28.908C45.7493 28.6627 45.4453 28.3907 45.2 28.092C44.9653 27.7827 44.8 27.4627 44.704 27.132L46.528 26.572C46.6667 26.9667 46.9387 27.3133 47.344 27.612C47.7493 27.9107 48.2507 28.0653 48.848 28.076C49.5413 28.076 50.0907 27.932 50.496 27.644C50.9013 27.356 51.104 26.9773 51.104 26.508C51.104 26.0813 50.9333 25.7347 50.592 25.468C50.2507 25.1907 49.792 24.9773 49.216 24.828L47.84 24.476C47.3173 24.3373 46.8427 24.1347 46.416 23.868C46 23.6013 45.6693 23.2653 45.424 22.86C45.1893 22.4547 45.072 21.9747 45.072 21.42C45.072 20.3747 45.4133 19.564 46.096 18.988C46.7787 18.4013 47.7547 18.108 49.024 18.108C49.7387 18.108 50.3627 18.22 50.896 18.444C51.44 18.6573 51.888 18.956 52.24 19.34C52.592 19.7133 52.8533 20.14 53.024 20.62L51.232 21.196C51.072 20.7693 50.7947 20.4173 50.4 20.14C50.0053 19.8627 49.5147 19.724 48.928 19.724C48.32 19.724 47.84 19.868 47.488 20.156C47.1467 20.444 46.976 20.844 46.976 21.356C46.976 21.772 47.1093 22.0973 47.376 22.332C47.6533 22.556 48.0267 22.7267 48.496 22.844L49.872 23.18C50.8747 23.4253 51.6533 23.8467 52.208 24.444C52.7627 25.0413 53.04 25.7027 53.04 26.428C53.04 27.068 52.8853 27.6333 52.576 28.124C52.2667 28.6147 51.808 28.9987 51.2 29.276C50.6027 29.5533 49.872 29.692 49.008 29.692ZM57.9416 29.692C57.099 29.692 56.4536 29.484 56.0056 29.068C55.5683 28.6413 55.3496 28.0333 55.3496 27.244V19.004H57.1256V26.908C57.1256 27.2813 57.211 27.564 57.3816 27.756C57.563 27.948 57.8243 28.044 58.1656 28.044C58.2723 28.044 58.3896 28.0227 58.5176 27.98C58.6456 27.9373 58.7896 27.8573 58.9496 27.74L59.6056 29.1C59.3283 29.292 59.051 29.436 58.7736 29.532C58.4963 29.6387 58.219 29.692 57.9416 29.692ZM54.0216 23.036V21.5H59.2856V23.036H54.0216ZM62.2229 25.244C62.2229 24.38 62.3882 23.6707 62.7189 23.116C63.0495 22.5613 63.4762 22.1507 63.9989 21.884C64.5322 21.6067 65.0869 21.468 65.6629 21.468V23.18C65.1722 23.18 64.7082 23.2493 64.2709 23.388C63.8442 23.516 63.4975 23.7293 63.2309 24.028C62.9642 24.3267 62.8309 24.7213 62.8309 25.212L62.2229 25.244ZM61.0389 29.5V21.5H62.8309V29.5H61.0389ZM70.4194 29.692C69.6407 29.692 68.9474 29.516 68.3394 29.164C67.742 28.8013 67.2674 28.3053 66.9154 27.676C66.574 27.0467 66.4034 26.3213 66.4034 25.5C66.4034 24.6787 66.5794 23.9533 66.9314 23.324C67.2834 22.6947 67.7634 22.204 68.3714 21.852C68.99 21.4893 69.694 21.308 70.4834 21.308C71.198 21.308 71.8434 21.4947 72.4194 21.868C72.9954 22.2307 73.4487 22.7587 73.7794 23.452C74.1207 24.1453 74.2914 24.972 74.2914 25.932H68.0034L68.2434 25.708C68.2434 26.1987 68.35 26.6253 68.5634 26.988C68.7767 27.34 69.0594 27.612 69.4114 27.804C69.7634 27.996 70.1527 28.092 70.5794 28.092C71.07 28.092 71.4754 27.9853 71.7954 27.772C72.1154 27.548 72.366 27.26 72.5474 26.908L74.1314 27.58C73.9074 28.0067 73.6194 28.38 73.2674 28.7C72.926 29.02 72.5154 29.2653 72.0354 29.436C71.566 29.6067 71.0274 29.692 70.4194 29.692ZM68.3554 24.78L68.0994 24.556H72.6114L72.3714 24.78C72.3714 24.3427 72.2754 23.9853 72.0834 23.708C71.8914 23.42 71.646 23.2067 71.3474 23.068C71.0594 22.9187 70.7554 22.844 70.4354 22.844C70.1154 22.844 69.7954 22.9187 69.4754 23.068C69.1554 23.2067 68.8887 23.42 68.6754 23.708C68.462 23.9853 68.3554 24.3427 68.3554 24.78ZM80.5439 29.5L80.4639 27.996V25.388C80.4639 24.844 80.4052 24.3907 80.2879 24.028C80.1812 23.6547 79.9999 23.372 79.7439 23.18C79.4985 22.9773 79.1679 22.876 78.7519 22.876C78.3679 22.876 78.0319 22.956 77.7439 23.116C77.4559 23.276 77.2105 23.5267 77.0079 23.868L75.4399 23.292C75.6105 22.94 75.8345 22.6147 76.1119 22.316C76.3999 22.0067 76.7572 21.7613 77.1839 21.58C77.6212 21.3987 78.1439 21.308 78.7519 21.308C79.5305 21.308 80.1812 21.4627 80.7039 21.772C81.2265 22.0707 81.6105 22.5027 81.8559 23.068C82.1119 23.6333 82.2399 24.316 82.2399 25.116L82.1919 29.5H80.5439ZM78.2399 29.692C77.2799 29.692 76.5332 29.4787 75.9999 29.052C75.4772 28.6253 75.2159 28.0227 75.2159 27.244C75.2159 26.412 75.4932 25.7773 76.0479 25.34C76.6132 24.9027 77.3972 24.684 78.3999 24.684H80.5439V26.06H78.9759C78.2612 26.06 77.7599 26.1613 77.4719 26.364C77.1839 26.556 77.0399 26.8333 77.0399 27.196C77.0399 27.5053 77.1625 27.7507 77.4079 27.932C77.6639 28.1027 78.0159 28.188 78.4639 28.188C78.8692 28.188 79.2212 28.0973 79.5199 27.916C79.8185 27.7347 80.0479 27.4947 80.2079 27.196C80.3785 26.8973 80.4639 26.5613 80.4639 26.188H80.9919C80.9919 27.276 80.7732 28.1347 80.3359 28.764C79.8985 29.3827 79.1999 29.692 78.2399 29.692ZM85.7665 27.26L84.6785 26.028L88.8385 21.5H90.9985L85.7665 27.26ZM84.0545 29.5V18.3H85.8465V29.5H84.0545ZM89.1425 29.5L86.3265 25.356L87.4945 24.108L91.2705 29.5H89.1425Z" fill="#394C0A"/>
</svg>

After

Width:  |  Height:  |  Size: 6.3 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 329 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1,4 @@
<svg width="23" height="18" viewBox="0 0 23 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.8252 0L3.09442 6.29374L2.5175 3.93359C3.26925 3.93359 3.88114 4.15212 4.35317 4.58919C4.8252 5.02625 5.06122 5.6294 5.06122 6.39864C5.06122 7.15039 4.81646 7.76228 4.32695 8.23431C3.85492 8.68886 3.26051 8.91613 2.54372 8.91613C1.80945 8.91613 1.19756 8.68886 0.708046 8.23431C0.236015 7.76228 0 7.15039 0 6.39864C0 6.17136 0.0174826 5.95283 0.0524478 5.74304C0.0874131 5.51576 0.157344 5.25352 0.262239 4.95632C0.367135 4.65912 0.515737 4.26576 0.708046 3.77624L2.22903 0H4.8252ZM11.014 0L9.28327 6.29374L8.70634 3.93359C9.45809 3.93359 10.07 4.15212 10.542 4.58919C11.014 5.02625 11.2501 5.6294 11.2501 6.39864C11.2501 7.15039 11.0053 7.76228 10.5158 8.23431C10.0438 8.68886 9.44935 8.91613 8.73256 8.91613C7.99829 8.91613 7.3864 8.68886 6.89689 8.23431C6.42486 7.76228 6.18884 7.15039 6.18884 6.39864C6.18884 6.17136 6.20633 5.95283 6.24129 5.74304C6.27626 5.51576 6.34619 5.25352 6.45108 4.95632C6.55598 4.65912 6.70458 4.26576 6.89689 3.77624L8.41788 0H11.014Z" fill="#A0D025"/>
<path d="M17.6748 17.832L19.4056 11.5383L19.9825 13.8984C19.2308 13.8984 18.6189 13.6799 18.1468 13.2428C17.6748 12.8058 17.4388 12.2026 17.4388 11.4334C17.4388 10.6816 17.6835 10.0698 18.1731 9.59772C18.6451 9.14317 19.2395 8.9159 19.9563 8.9159C20.6905 8.9159 21.3024 9.14317 21.792 9.59772C22.264 10.0698 22.5 10.6816 22.5 11.4334C22.5 11.6607 22.4825 11.8792 22.4476 12.089C22.4126 12.3163 22.3427 12.5785 22.2378 12.8757C22.1329 13.1729 21.9843 13.5663 21.792 14.0558L20.271 17.832H17.6748ZM11.486 17.832L13.2167 11.5383L13.7937 13.8984C13.0419 13.8984 12.43 13.6799 11.958 13.2428C11.486 12.8058 11.2499 12.2026 11.2499 11.4334C11.2499 10.6816 11.4947 10.0698 11.9842 9.59772C12.4562 9.14317 13.0506 8.9159 13.7674 8.9159C14.5017 8.9159 15.1136 9.14317 15.6031 9.59772C16.0751 10.0698 16.3112 10.6816 16.3112 11.4334C16.3112 11.6607 16.2937 11.8792 16.2587 12.089C16.2237 12.3163 16.1538 12.5785 16.0489 12.8757C15.944 13.1729 15.7954 13.5663 15.6031 14.0558L14.0821 17.832H11.486Z" fill="#A0D025"/>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21.2187 15.3754C21.5637 14.9441 22.1937 14.8741 22.6249 15.2191C23.0562 15.5641 23.1262 16.1941 22.7812 16.6254L18.7812 21.6254C18.6036 21.8474 18.3394 21.9826 18.0556 21.9984C17.7716 22.0142 17.494 21.9086 17.2929 21.7074L15.2929 19.7074C14.9024 19.3169 14.9024 18.6839 15.2929 18.2934C15.6834 17.9028 16.3164 17.9028 16.707 18.2934L17.9159 19.5023L21.2187 15.3754Z" fill="#A0D025"/>
<path d="M10.7998 3.65137C11.146 3.39172 11.5673 3.25098 12 3.25098C12.4327 3.25098 12.854 3.39172 13.2002 3.65137L20.2002 8.90137C20.4484 9.08763 20.6503 9.32886 20.7891 9.60645C20.9279 9.88415 21 10.1905 21 10.501V12.4565C21 12.9796 20.4396 13.3258 19.9346 13.1894C19.4773 13.066 18.9964 13 18.5 13C16.5152 13 14.776 14.0513 13.8089 15.6274C13.6713 15.8515 13.4337 16.001 13.1708 16.001H13H11V19.001C11 19.5314 10.7891 20.04 10.4141 20.415C10.039 20.7901 9.53043 21.001 9 21.001H5C4.46957 21.001 3.96101 20.7901 3.58594 20.415C3.21087 20.04 3 19.5314 3 19.001V10.501C3 10.1905 3.07208 9.88416 3.21094 9.60645C3.34973 9.32885 3.55156 9.08763 3.7998 8.90137L10.7998 3.65137Z" fill="#A0D025"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -0,0 +1,369 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- https://github.com/stifolder/kretainsult/blob/master/src/assets/dirtywords.xml -->
<!-- Köszönjük a kretainsult.online eredeti alkotójának a kategorizálást! -->
<DirtyWords>
<Word type="m">Aberált</Word>
<Word type="m">Aberrált</Word>
<Word type="f">Abortuszmaradék</Word>
<Word type="m">Abszolút hülye</Word>
<Word type="m">Agyalágyult</Word>
<Word type="m">Agyatlan</Word>
<Word type="m">Agybatetovált</Word>
<Word type="m">Ágybavizelős</Word>
<Word type="f">Agyfasz</Word>
<Word type="f">Agyhalott</Word>
<Word type="m">Agyonkúrt</Word>
<Word type="m">Agyonvert</Word>
<Word type="m">Agyrákos</Word>
<Word type="m">AIDS-es</Word>
<Word type="m">Alapvetően fasz</Word>
<Word type="f">Animalsex-mániás</Word>
<Word type="f">Antibarom</Word>
<Word type="m">Aprófaszú</Word>
<Word type="m">Arcbarakott</Word>
<Word type="m">Aszaltfaszú</Word>
<Word type="m">Aszott</Word>
<Word type="m">Átbaszott</Word>
<Word type="f">Azt a kurva de fasz</Word>
<Word type="m">Balatonberényben napvilágot látott</Word>
<Word type="f">Balfasz</Word>
<Word type="f">Balfészek</Word>
<Word type="f">Baromfifasz</Word>
<Word type="f">Basz-o-matic</Word>
<Word type="m">Baszhatatlan</Word>
<Word type="m">Basznivaló</Word>
<Word type="m">Bebaszott</Word>
<Word type="m">Befosi</Word>
<Word type="m">Békapicsa</Word>
<Word type="m">Bélböfi</Word>
<Word type="m">Beleiből kiforgatott</Word>
<Word type="f">Bélszél</Word>
<Word type="m">Bronz térdű</Word>
<Word type="f">Brunya</Word>
<Word type="m">Büdös szájú</Word>
<Word type="m">Büdösszájú</Word>
<Word type="m">Búvalbaszott</Word>
<Word type="f">Buzeráns</Word>
<Word type="m">Buzernyák</Word>
<Word type="f">Buzi</Word>
<Word type="f">Buzikurva</Word>
<Word type="f">Cafat</Word>
<Word type="f">Cafka</Word>
<Word type="f">Céda</Word>
<Word type="m">Cérnafaszú</Word>
<Word type="f">Cottonfej</Word>
<Word type="m">Csempe szobában felneveltetett</Word>
<Word type="m">Cseszett</Word>
<Word type="f">Csibefasz</Word>
<Word type="f">Csipszar</Word>
<Word type="m">Csirkefaszú</Word>
<Word type="f">Csitri</Word>
<Word type="f">Csöcs</Word>
<Word type="f">Csöcsfej</Word>
<Word type="f">Csöppszar</Word>
<Word type="m">Csőszkunyhóban elrejtett</Word>
<Word type="m">Csupaszfarkú</Word>
<Word type="f">Cuncipunci</Word>
<Word type="m">Deformáltfaszú</Word>
<Word type="m">Dekorált pofájú</Word>
<Word type="m">Döbbenetesen segg</Word>
<Word type="m">Dobseggű</Word>
<Word type="m">Dughatatlan</Word>
<Word type="m">Dunyhavalagú</Word>
<Word type="m">Duplafaszú</Word>
<Word type="f">Ebfasz</Word>
<Word type="m">Egyszerűen fasz</Word>
<Word type="m">Elbaszott</Word>
<Word type="m">Eleve hülye</Word>
<Word type="m">Extrahülye</Word>
<Word type="m">Fafogú rézfűrésszel megsebzett</Word>
<Word type="m">Fantasztikusan segg</Word>
<Word type="f">Fasszopó</Word>
<Word type="m">Fasz</Word>
<Word type="m">Fasz-emulátor</Word>
<Word type="m">Faszagyú</Word>
<Word type="f">Faszarc</Word>
<Word type="f">Faszfej</Word>
<Word type="f">Faszfészek</Word>
<Word type="f">Faszkalap</Word>
<Word type="f">Faszkarika</Word>
<Word type="m">Faszkedvelő</Word>
<Word type="f">Faszkópé</Word>
<Word type="f">Faszogány</Word>
<Word type="f">Faszpörgettyű</Word>
<Word type="f">Faszsapka</Word>
<Word type="m">Faszszagú</Word>
<Word type="f">Faszszopó</Word>
<Word type="m">Fasztalan</Word>
<Word type="f">Fasztarisznya</Word>
<Word type="f">Fasztengely</Word>
<Word type="f">Fasztolvaj</Word>
<Word type="f">Faszváladék</Word>
<Word type="f">Faszverő</Word>
<Word type="m">Félrebaszott</Word>
<Word type="m">Félrefingott</Word>
<Word type="m">Félreszart</Word>
<Word type="f">Félribanc</Word>
<Word type="f">Fing</Word>
<Word type="m">Fölcsinált</Word>
<Word type="m">Fölfingott</Word>
<Word type="f">Fos</Word>
<Word type="f">Foskemence</Word>
<Word type="f">Fospisztoly</Word>
<Word type="f">Fospumpa</Word>
<Word type="f">Fostalicska</Word>
<Word type="f">Fütyi</Word>
<Word type="m">Fütyinyalogató</Word>
<Word type="f">Fütykös</Word>
<Word type="f">Geci</Word>
<Word type="m">Gecinyelő</Word>
<Word type="m">Geciszaró</Word>
<Word type="m">Geciszívó</Word>
<Word type="f">Genny</Word>
<Word type="m">Gennyesszájú</Word>
<Word type="f">Gennygóc</Word>
<Word type="f">Genyac</Word>
<Word type="f">Genyó</Word>
<Word type="f">Gólyafos</Word>
<Word type="m">Görbefaszú</Word>
<Word type="m">Gyennyszopó</Word>
<Word type="f">Gyíkfing</Word>
<Word type="f">Hájpacni</Word>
<Word type="f">Hatalmas nagy fasz</Word>
<Word type="m">Hátbabaszott</Word>
<Word type="f">Házikurva</Word>
<Word type="m">Hererákos</Word>
<Word type="m">Hígagyú</Word>
<Word type="m">Hihetetlenül fasz</Word>
<Word type="f">Hikomat</Word>
<Word type="f">Hímnőstény</Word>
<Word type="f">Hímringyó</Word>
<Word type="m">Hiperstrici</Word>
<Word type="m">Hitler-imádó</Word>
<Word type="m">Hitlerista</Word>
<Word type="f">Hivatásos balfasz</Word>
<Word type="m">Hú de segg</Word>
<Word type="m">Hugyagyú</Word>
<Word type="m">Hugyos</Word>
<Word type="f">Hugytócsa</Word>
<Word type="m">Hüje</Word>
<Word type="m">Hüle</Word>
<Word type="m">Hülye</Word>
<Word type="m">Hülyécske</Word>
<Word type="f">Hülyegyerek</Word>
<Word type="f">Inkubátor-szökevény</Word>
<Word type="f">Integrált barom</Word>
<Word type="m">Ionizált faszú</Word>
<Word type="f">IQ bajnok</Word>
<Word type="f">IQ fighter</Word>
<Word type="m">IQ hiányos</Word>
<Word type="m">Irdatlanul köcsög</Word>
<Word type="m">Íveltfaszú</Word>
<Word type="m">Jajj de barom</Word>
<Word type="m">Jókora fasz</Word>
<Word type="f">Kaka</Word>
<Word type="f">Kakamatyi</Word>
<Word type="f">Kaki</Word>
<Word type="f">Kaksi</Word>
<Word type="m">Kecskebaszó</Word>
<Word type="m">Kellően fasz</Word>
<Word type="m">Képlékeny faszú</Word>
<Word type="f">Keresve sem található fasz</Word>
<Word type="m">Kétfaszú</Word>
<Word type="m">Kétszer agyonbaszott</Word>
<Word type="m">Ki-bebaszott</Word>
<Word type="m">Kibaszott</Word>
<Word type="m">Kifingott</Word>
<Word type="m">Kiherélt</Word>
<Word type="m">Kikakkantott</Word>
<Word type="m">Kikészült</Word>
<Word type="m">Kimagaslóan fasz</Word>
<Word type="m">Kimondhatatlan pöcs</Word>
<Word type="f">Kis szaros</Word>
<Word type="f">Kisfütyi</Word>
<Word type="m">Klotyószagú</Word>
<Word type="m">Ködmönbe bújtatott</Word>
<Word type="m">Kojak-faszú</Word>
<Word type="m">Kopárfaszú</Word>
<Word type="m">Korlátolt gecizésű</Word>
<Word type="f">Kotonszökevény</Word>
<Word type="m">Középszar</Word>
<Word type="f">Kretén</Word>
<Word type="f">Kuki</Word>
<Word type="f">Kula</Word>
<Word type="m">Kunkorított faszú</Word>
<Word type="f">Kurva</Word>
<Word type="m">Kurvaanyjú</Word>
<Word type="f">Kurvapecér</Word>
<Word type="f">Kutyakaki</Word>
<Word type="f">Kutyapina</Word>
<Word type="f">Kutyaszar</Word>
<Word type="m">Lankadtfaszú</Word>
<Word type="m">Lebaszirgált</Word>
<Word type="m">Lebaszott</Word>
<Word type="m">Lecseszett</Word>
<Word type="m">Leírhatatlanul segg</Word>
<Word type="m">Lemenstruált</Word>
<Word type="m">Leokádott</Word>
<Word type="f">Lepkefing</Word>
<Word type="f">Leprafészek</Word>
<Word type="m">Leszart</Word>
<Word type="m">Leszbikus</Word>
<Word type="f">Lőcs</Word>
<Word type="f">Lőcsgéza</Word>
<Word type="f">Lófasz</Word>
<Word type="m">Lógócsöcsű</Word>
<Word type="f">Lóhugy</Word>
<Word type="f">Lotyó</Word>
<Word type="m">Lucskos</Word>
<Word type="f">Lugnya</Word>
<Word type="m">Lyukasbelű</Word>
<Word type="m">Lyukasfaszú</Word>
<Word type="m">Lyukát vakaró</Word>
<Word type="m">Lyuktalanított</Word>
<Word type="f">Mamutsegg</Word>
<Word type="f">Maszturbációs görcs</Word>
<Word type="f">Maszturbagép</Word>
<Word type="m">Maszturbáltatott</Word>
<Word type="m">Megfingatott</Word>
<Word type="m">Megkettyintett</Word>
<Word type="m">Megkúrt</Word>
<Word type="m">Megszopatott</Word>
<Word type="m">Mesterséges faszú</Word>
<Word type="f">Méteres kékeres</Word>
<Word type="m">Mikrotökű</Word>
<Word type="m">Mocskos</Word>
<Word type="f">Mojfing</Word>
<Word type="m">Műfaszú</Word>
<Word type="f">Muff</Word>
<Word type="f">Multifasz</Word>
<Word type="m">Műtöttpofájú</Word>
<Word type="m">Náci</Word>
<Word type="m">Nagyfejű</Word>
<Word type="f">Nikotinpatkány</Word>
<Word type="m">Nimfomániás</Word>
<Word type="f">Nuna</Word>
<Word type="f">Nunci</Word>
<Word type="f">Nuncóka</Word>
<Word type="f">Nyalábfasz</Word>
<Word type="f">Nyelestojás</Word>
<Word type="f">Nyúlszar</Word>
<Word type="f">Oltári nagy fasz</Word>
<Word type="m">Ondónyelő</Word>
<Word type="m">Orbitálisan hülye</Word>
<Word type="m">Ordenálé</Word>
<Word type="m">Összebaszott</Word>
<Word type="f">Ötcsillagos fasz</Word>
<Word type="m">Óvszerezett</Word>
<Word type="f">Pénisz</Word>
<Word type="m">Peremesfaszú</Word>
<Word type="f">Picsa</Word>
<Word type="f">Picsafej</Word>
<Word type="m">Picsameresztő</Word>
<Word type="m">Picsánnyalt</Word>
<Word type="m">Picsánrugott</Word>
<Word type="m">Picsányi</Word>
<Word type="m">Pikkelypáncélt hordó</Word>
<Word type="f">Pina</Word>
<Word type="f">Pisa</Word>
<Word type="m">Pisaszagú</Word>
<Word type="m">Pisis</Word>
<Word type="f">Pöcs</Word>
<Word type="f">Pöcsfej</Word>
<Word type="m">Porbafingó</Word>
<Word type="f">Pornóbuzi</Word>
<Word type="m">Pornómániás</Word>
<Word type="m">Pudvás</Word>
<Word type="m">Pudváslikú</Word>
<Word type="m">Puhafaszú</Word>
<Word type="f">Punci</Word>
<Word type="f">Puncimókus</Word>
<Word type="m">Puncis</Word>
<Word type="f">Punciutáló</Word>
<Word type="f">Puncivirág</Word>
<Word type="f">Qki</Word>
<Word type="f">Qrva</Word>
<Word type="f">Qtyaszar</Word>
<Word type="m">Rabló</Word>
<Word type="m">Rágcsáltfaszú</Word>
<Word type="f">Redva</Word>
<Word type="m">Rendkívül fasz</Word>
<Word type="m">Repedtsarkú</Word>
<Word type="m">Rétó-román</Word>
<Word type="m">Rézhasú</Word>
<Word type="f">Ribanc</Word>
<Word type="f">Riherongy</Word>
<Word type="m">Ritka fogú</Word>
<Word type="m">Rivalizáló</Word>
<Word type="f">Rőfös fasz</Word>
<Word type="m">Rojtospicsájú</Word>
<Word type="m">Rongyospinájú</Word>
<Word type="m">Roppant hülye</Word>
<Word type="f">Rossz kurva</Word>
<Word type="m">Saját nemével kefélő</Word>
<Word type="f">Segg</Word>
<Word type="f">Seggarc</Word>
<Word type="f">Seggdugó</Word>
<Word type="f">Seggfej</Word>
<Word type="f">Seggnyaló</Word>
<Word type="f">Seggszőr</Word>
<Word type="f">Seggtorlasz</Word>
<Word type="m">Sikoltozásokba öltöztetett</Word>
<Word type="f">Strici</Word>
<Word type="m">Suttyó</Word>
<Word type="m">Sutyerák</Word>
<Word type="m">Szálkafaszú</Word>
<Word type="f">Szar</Word>
<Word type="f">Szaralak</Word>
<Word type="f">Szárazfing</Word>
<Word type="f">Szarbojler</Word>
<Word type="f">Szarcsimbók</Word>
<Word type="m">Szarevő</Word>
<Word type="m">Szarfaszú</Word>
<Word type="f">Szarházi</Word>
<Word type="f">Szarjankó</Word>
<Word type="m">Szarnivaló</Word>
<Word type="m">Szarosvalagú</Word>
<Word type="m">Szarrá vágott</Word>
<Word type="f">Szarrágó</Word>
<Word type="m">Szarszagú</Word>
<Word type="m">Szarszájú</Word>
<Word type="f">Szartragacs</Word>
<Word type="f">Szarzsák</Word>
<Word type="f">Szégyencsicska</Word>
<Word type="m">Szifiliszes</Word>
<Word type="f">Szivattyús kurva</Word>
<Word type="m">Szófosó</Word>
<Word type="m">Szokatlanul fasz</Word>
<Word type="f">Szop-o-matic</Word>
<Word type="f">Szopógép</Word>
<Word type="f">Szopógörcs</Word>
<Word type="f">Szopós kurva</Word>
<Word type="m">Szopottfarkú</Word>
<Word type="m">Szűklyukú</Word>
<Word type="m">Szultán udvarát megjárt</Word>
<Word type="f">Szúnyogfaszni</Word>
<Word type="f">Szuperbuzi</Word>
<Word type="f">Szuperkurva</Word>
<Word type="m">Szűzhártya-repedéses</Word>
<Word type="f">Szűzkurva</Word>
<Word type="f">Szűzpicsa</Word>
<Word type="f">Szűzpunci</Word>
<Word type="m">Tetves</Word>
<Word type="f">Tikfos</Word>
<Word type="f">Tikszar</Word>
<Word type="m">Tompatökű</Word>
<Word type="m">Törpefaszú</Word>
<Word type="m">Toszatlan</Word>
<Word type="m">Toszott</Word>
<Word type="m">Totálisan hülye</Word>
<Word type="m">Tyű de picsa</Word>
<Word type="m">Tyúkfasznyi</Word>
<Word type="f">Tyúkszar</Word>
<Word type="f">Vadfasz</Word>
<Word type="f">Valag</Word>
<Word type="f">Valagváladék</Word>
<Word type="f">Végbélféreg</Word>
<Word type="f">Xar</Word>
<Word type="m">Zsugorított faszú</Word>
</DirtyWords>

21
firka/codegen-lock.yaml Normal file
View File

@@ -0,0 +1,21 @@
icons:
"flutter_launcher_icons.yaml": "c600507ca0df7cebd0f708124842512a14ed3d597b779176200d6ba25b1335b1"
"pubspec.yaml": "c84752e36ab6218d2ac824c17ffb0edb5ac4f809ab87784c9c607a0d9f525abb"
"assets/images/logos/colored_logo.webp": "4b4fa99d144fe6694aa4487ba1b26aeecafae41e3c877836cd7da28d61a77983"
"assets/images/logos/monochrome_logo.png": "188d2b0a64c70323b09bcee721663d6698fb557066f20ddaec97bba6869c1c6c"
"assets/images/logos/colored_logo_without_mustache.png": "d11cff9f38985885873bfdd2d84e61f8fab03803eada94d4caac1545ef3685f3"
"assets/images/logos/colored_logo_only_mustache.png": "bad6220c11bdfb1dfe04e5173bd2ebedd3999689d4b3a68fc63dc520c96dd33b"
l10n:
"l10n.yml": "a57bc304cac4a2b0235593586f17f400a5165d67fc9aadeaa11893cfa36ee082"
"lib/l10n/app_de.arb": "9cd5913be1e3bc3ed6c088ef448d5ce2924a6290b7dd6006d1af624c5e9a2503"
"lib/l10n/app_hu.arb": "17077ec76b68ed03796a264b99e4dba9e6ddd532e27a92d8fb237ea6f211f757"
"lib/l10n/app_en.arb": "cbad6dd2485a983e399cce97371c19089b9110d30536488c14a7ea709c7b6ead"
isar:
"lib/data/models/app_settings_model.dart": "5eb5af345f1347f104257f0999763650fe2673f9da1754bd12d3f756fe5c9723"
"lib/data/models/generic_cache_model.dart": "79151d0467fb5d40c532eaaa08ad7c7e24a34304199280fbf49cf6e5adcce6bc"
"lib/data/models/homework_cache_model.dart": "45789970b27d5790cdc54c292ef2f5feaa5f4e293b8a8862fd676d5eb3e25d29"
"lib/data/models/timetable_cache_model.dart": "b972bf51e399f8d20d4f9ad660082d4cc4a9798df9ac9d6ec9ef6ac640205572"
"lib/data/models/token_model.dart": "8c957cd07e473827d78fd8fd4fb6c1336b636a69c25c93618e1e7f94b7cf0683"
splash:
"flutter_native_splash.yaml": "0fd4a85d6f950d97298e99916927649940ffcfdadfc136ceee126fed0dbaa9f2"
"assets/images/logos/splash.png": "88fbebc3d686cb9095bcce362029b69978b1b14270e465e91d962b1425db1152"

View File

@@ -0,0 +1,21 @@
flutter_native_splash:
color: "#7ca120"
image: assets/images/logos/splash.png
# Keep image centered instead of fill-scaled (stops icon looking zoomed/cropped)
android_gravity: center
# Dark mode - same color as light mode for consistency
color_dark: "#7ca120"
image_dark: assets/images/logos/splash.png
# Android 12+ uses 960×960 image with logo in 640px circle (generated by codegen) to avoid cropping
android_12:
image: assets/images/logos/splash_android12.png
color: "#7ca120"
color_dark: "#7ca120"
image_dark: assets/images/logos/splash_android12.png
icon_background_color: "#7ca120"
icon_background_color_dark: "#7ca120"
ios: true
web: false

View File

@@ -1,42 +0,0 @@
import 'package:firka/helpers/db/models/generic_cache_model.dart';
import 'package:firka/helpers/db/models/timetable_cache_model.dart';
import 'package:firka/helpers/db/models/token_model.dart';
import 'package:firka/main.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:isar/isar.dart';
import 'package:path_provider/path_provider.dart';
import 'test_helpers.dart';
Future<void> main() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
await resetAppData();
setApiUrls();
group('main', () {
testWidgets('InitializationScreen -> HomeScreen', (tester) async {
final dir = await getApplicationDocumentsDirectory();
var isar = await Isar.open(
[TokenModelSchema, GenericCacheModelSchema, TimetableCacheModelSchema],
inspector: true,
directory: dir.path,
);
isarInit = isar;
await isar.writeTxn(() async {
await isar.tokenModels.put(TokenModel());
});
await tester.pumpWidget(InitializationScreen());
await waitUntil(Duration(minutes: 2), tester, () async {
var ele = find.byKey(const Key('homeScreen'));
return ele.allCandidates.isNotEmpty;
});
});
});
}

View File

@@ -1,24 +0,0 @@
import 'package:firka/main.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'test_helpers.dart';
Future<void> main() async {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
await resetAppData();
setApiUrls();
group('main', () {
testWidgets('InitializationScreen -> LoginScreen', (tester) async {
await tester.pumpWidget(InitializationScreen());
await waitUntil(Duration(minutes: 2), tester, () async {
var ele = find.byKey(const Key('loginScreen'));
return ele.allCandidates.isNotEmpty;
});
});
});
}

View File

@@ -1,42 +0,0 @@
import 'package:firka/helpers/api/consts.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path_provider/path_provider.dart';
Future<bool> isWear() async {
const platform = MethodChannel('firka.app/main');
return await platform.invokeMethod("isWear");
}
Future<bool> isPhone() async {
return !(await isWear());
}
Future<void> resetAppData() async {
final isarDir = await getApplicationDocumentsDirectory();
if (await isarDir.exists()) await isarDir.delete(recursive: true);
}
void setApiUrls() {
KretaEndpoints.kretaBase = "localhost:8060";
KretaEndpoints.kretaIdp = "http://localhost:8060";
KretaEndpoints.kretaLoginUrl =
"${KretaEndpoints.kretaIdp}/Account/Login?ReturnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fprompt%3Dlogin%26nonce%3DwylCrqT4oN6PPgQn2yQB0euKei9nJeZ6_ffJ-VpSKZU%26response_type%3Dcode%26code_challenge_method%3DS256%26scope%3Dopenid%2520email%2520offline_access%2520kreta-ellenorzo-webapi.public%2520kreta-eugyintezes-webapi.public%2520kreta-fileservice-webapi.public%2520kreta-mobile-global-webapi.public%2520kreta-dkt-webapi.public%2520kreta-ier-webapi.public%26code_challenge%3DHByZRRnPGb-Ko_wTI7ibIba1HQ6lor0ws4bcgReuYSQ%26redirect_uri%3Dhttps%253A%252F%252Fmobil.e-kreta.hu%252Fellenorzo-student%252Fprod%252Foauthredirect%26client_id%3Dkreta-ellenorzo-student-mobile-ios%26state%3Dkreta_student_mobile%26suppressed_prompt%3Dlogin";
KretaEndpoints.tokenGrantUrl = "${KretaEndpoints.kretaIdp}/connect/token";
}
Future<void> waitUntil(Duration timeout, WidgetTester tester,
Future<bool> Function() callback) async {
var now = DateTime.now();
while (
now.difference(DateTime.now()).inMilliseconds < timeout.inMilliseconds) {
await tester.pump(Duration(milliseconds: 100));
if (await callback()) {
return;
}
}
throw Exception("waitUntil timed out");
}

View File

@@ -33,4 +33,7 @@ Runner/GeneratedPluginRegistrant.*
!default.pbxuser
!default.perspectivev3
/.DerivedData
/.DerivedData
# Developer-specific configuration
.dev_config

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "Icon-1024.png",
"idiom" : "universal",
"platform" : "watchos",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,64 @@
import SwiftUI
struct CountdownRing: View {
let totalMinutes: Int
let remainingMinutes: Int
let label: String
var size: CGFloat = 80
var lineWidth: CGFloat = 8
var displayOffset: Int = 0 // Add to displayed minutes (e.g., +1)
private var clampedRemainingMinutes: Int {
guard totalMinutes > 0 else { return 0 }
return max(0, min(remainingMinutes, totalMinutes))
}
var progress: Double {
guard totalMinutes > 0 else { return 0 }
return Double(totalMinutes - clampedRemainingMinutes) / Double(totalMinutes)
}
var displayedMinutes: Int {
max(0, remainingMinutes + displayOffset)
}
var ringColor: Color {
if clampedRemainingMinutes < 5 { return .red }
if clampedRemainingMinutes < 10 { return .yellow }
return .green
}
var body: some View {
ZStack {
Circle()
.stroke(Color(white: 0.2), lineWidth: lineWidth)
Circle()
.trim(from: 0, to: progress)
.stroke(ringColor, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round))
.rotationEffect(.degrees(-90))
.animation(.easeInOut, value: progress)
VStack(spacing: 1) {
Text("\(displayedMinutes)")
.font(size > 60 ? .title2 : .headline)
.fontWeight(.bold)
Text(label)
.font(.caption2)
.foregroundColor(.secondary)
}
}
.frame(width: size, height: size)
}
}
#Preview {
VStack(spacing: 20) {
CountdownRing(totalMinutes: 45, remainingMinutes: 30, label: "min")
CountdownRing(totalMinutes: 45, remainingMinutes: 8, label: "min")
CountdownRing(totalMinutes: 45, remainingMinutes: 3, label: "min")
}
.padding()
}

View File

@@ -0,0 +1,42 @@
import SwiftUI
struct FirkaCard<Content: View>: View {
let content: Content
var isHighlighted: Bool = false
var backgroundColor: Color? = nil
init(
isHighlighted: Bool = false,
backgroundColor: Color? = nil,
@ViewBuilder content: () -> Content
) {
self.isHighlighted = isHighlighted
self.backgroundColor = backgroundColor
self.content = content()
}
var body: some View {
content
.padding(12)
.background(
backgroundColor ??
(isHighlighted ? Color.green.opacity(0.2) : Color(white: 0.12))
)
.cornerRadius(12)
}
}
#Preview {
VStack(spacing: 12) {
FirkaCard {
Text("Normal Card")
.foregroundColor(.primary)
}
FirkaCard(isHighlighted: true) {
Text("Highlighted Card")
.foregroundColor(.primary)
}
}
.padding()
}

View File

@@ -0,0 +1,39 @@
import SwiftUI
struct GradeBadge: View {
let grade: Int
var size: CGFloat = 24
var color: Color {
switch grade {
case 5: return .green
case 4: return .blue
case 3: return .yellow
case 2: return .orange
default: return .red
}
}
var body: some View {
ZStack {
Circle()
.fill(color)
.frame(width: size, height: size)
Text("\(grade)")
.font(.system(size: size * 0.5, weight: .bold))
.foregroundColor(.white)
}
}
}
#Preview {
HStack(spacing: 12) {
GradeBadge(grade: 5)
GradeBadge(grade: 4)
GradeBadge(grade: 3)
GradeBadge(grade: 2)
GradeBadge(grade: 1)
}
.padding()
}

View File

@@ -0,0 +1,46 @@
import SwiftUI
struct GradeRow: View {
let grade: WidgetGrade
var body: some View {
HStack(alignment: .center, spacing: 8) {
Text(grade.displayValue)
.font(.caption)
.fontWeight(.semibold)
.foregroundColor(.white)
.frame(width: 24, height: 24)
.background(
Circle()
.fill(grade.gradeColor)
)
VStack(alignment: .leading, spacing: 2) {
if let topic = grade.topic {
Text(topic)
.font(.caption2)
.foregroundColor(.primary)
.lineLimit(2)
}
HStack(spacing: 4) {
Text(grade.type.name)
.font(.system(size: 10))
.foregroundColor(.secondary)
if let weight = grade.weightPercentage, weight != 100 {
Text("(\(weight)%)")
.font(.system(size: 10))
.foregroundColor(.secondary)
}
}
}
Spacer()
}
.padding(.horizontal, 8)
.padding(.vertical, 6)
.background(Color(white: 0.15))
.cornerRadius(6)
}
}

View File

@@ -0,0 +1,146 @@
import SwiftUI
struct LessonCard: View {
let lesson: WidgetLesson
let isActive: Bool
let colors: WidgetColors?
var backgroundColor: Color {
if let colors = colors {
return colors.cardColor
}
return Color(white: 0.15)
}
var textPrimaryColor: Color {
if let colors = colors {
return colors.textPrimaryColor
}
return .primary
}
var textSecondaryColor: Color {
if let colors = colors {
return colors.textSecondaryColor
}
return .secondary
}
var textTertiaryColor: Color {
if let colors = colors {
return colors.textTertiaryColor
}
return .secondary.opacity(0.7)
}
var body: some View {
VStack(alignment: .leading, spacing: 6) {
HStack(alignment: .top, spacing: 8) {
if let number = lesson.lessonNumber {
Text("\(number)")
.font(.headline)
.fontWeight(.semibold)
.foregroundColor(isActive ? .white : textPrimaryColor)
.frame(width: 28, height: 28)
.background(
Circle()
.fill(isActive ? Color.green : Color.clear)
)
}
VStack(alignment: .leading, spacing: 2) {
Text(lesson.displayName)
.font(.headline)
.fontWeight(.semibold)
.foregroundColor(lesson.isCancelled ? .red :
lesson.isSubstitution ? .orange : textPrimaryColor)
.strikethrough(lesson.isCancelled, color: .red)
.lineLimit(1)
Text(lesson.timeString)
.font(.caption2)
.foregroundColor(lesson.isCancelled ? .red.opacity(0.8) :
lesson.isSubstitution ? .orange.opacity(0.8) : textSecondaryColor)
}
Spacer()
}
if let room = lesson.roomName {
HStack(spacing: 4) {
Image(systemName: "door.right.hand.closed")
.font(.caption2)
Text(room)
.font(.caption2)
}
.foregroundColor(lesson.isCancelled ? .red.opacity(0.7) :
lesson.isSubstitution ? .orange.opacity(0.7) : textSecondaryColor)
.lineLimit(1)
}
if let teacher = lesson.teacher {
Text(teacher)
.font(.caption2)
.foregroundColor(lesson.isCancelled ? .red.opacity(0.7) :
lesson.isSubstitution ? .orange.opacity(0.7) : textTertiaryColor)
.lineLimit(1)
}
}
.padding(10)
.frame(maxWidth: .infinity, alignment: .leading)
.background(backgroundColor)
.cornerRadius(10)
.overlay(
RoundedRectangle(cornerRadius: 10)
.stroke(
isActive ? Color.green : Color.clear,
lineWidth: isActive ? 2 : 0
)
)
}
}
#Preview {
VStack(spacing: 12) {
LessonCard(
lesson: WidgetLesson(
uid: "1",
date: "2026-02-01",
start: Date(),
end: Date().addingTimeInterval(3600),
name: "Matematika",
lessonNumber: 3,
teacher: "Nagy János",
substituteTeacher: nil,
subject: WidgetSubject(uid: "math", name: "Matematika", category: nil, sortIndex: 1, teacherName: "Nagy János"),
theme: nil,
roomName: "201",
isCancelled: false,
isSubstitution: false
),
isActive: true,
colors: nil
)
LessonCard(
lesson: WidgetLesson(
uid: "2",
date: "2026-02-01",
start: Date().addingTimeInterval(7200),
end: Date().addingTimeInterval(10800),
name: "Angol",
lessonNumber: 4,
teacher: "Kovács Éva",
substituteTeacher: nil,
subject: WidgetSubject(uid: "eng", name: "Angol", category: nil, sortIndex: 2, teacherName: "Kovács Éva"),
theme: nil,
roomName: "105",
isCancelled: false,
isSubstitution: false
),
isActive: false,
colors: nil
)
}
.padding()
}

Some files were not shown because too many files have changed in this diff Show More