351 Commits

Author SHA1 Message Date
MineTec 32be67426c Merge remote-tracking branch 'origin/develop' into develop 2026-07-26 13:49:05 +02:00
MineTec b957189fd3 implemented absence report module with form validation, prefill support, and API integration 2026-07-26 11:45:48 +02:00
MineTec 624c5512a6 fixed failing test 2026-07-23 14:22:32 +02:00
MineTec 3493cf8617 fixed error on pdf viewer transition 2026-07-23 14:16:33 +02:00
MineTec 75080a2c49 implemented shared items view for Talk chats with tabbed categories and on-scroll pagination 2026-07-19 10:58:02 +02:00
MineTec f3cd7896d9 implemented backward pagination for chat history with on-scroll prefetching 2026-07-17 20:14:47 +02:00
Marianum 101e7c909c bump version to 1.4.0+59 2026-07-17 15:19:26 +02:00
Marianum 429f1e6f96 fix build against updated dependencies: re-add DioExceptionType.transformTimeout case (restored in dio 5.10.0) and rename SwiftReceiveSharingIntentPlugin to ReceiveSharingIntentPlugin (renamed in receive_sharing_intent 1.9.0); refresh Podfile.lock and SPM Package.resolved 2026-07-17 15:19:10 +02:00
Marianum a0c55a811c completed iOS push notification setup: created the NotificationServiceExtension Xcode target programmatically (via xcodeproj), embedded it in Runner, and added its xcconfig base configs; fixed Automatic Signing by switching the NSE↔Runner shared keychain from an app-group to a team-prefixed access group across entitlements, Dart and Swift; fixed the analyzer break from dio 5.9.2 dropping DioExceptionType.transformTimeout 2026-07-17 15:05:34 +02:00
MineTec e625216a90 improved push notification settings with health indicators and optimized notification dismissal 2026-07-16 22:40:33 +02:00
MineTec 8128cede21 refactored teacher display in lesson details to better handle substitutions and duplicates 2026-07-16 21:44:08 +02:00
MineTec e5f7cf0176 updated the ProseMirror table view to conditionally hide the mode toggle toolbar when content fits within the available width, ensuring the toggle is only visible for overflowing tables. 2026-07-16 21:43:52 +02:00
MineTec e349d667d4 implemented a 1-minute in-memory cache for the EmergencyNoticeClient to prevent redundant network requests on app resume 2026-07-16 10:47:51 +02:00
MineTec 7c1f5c06df implemented a backend-independent emergency notice system 2026-07-15 20:30:20 +02:00
MineTec e8c6ac1c65 further improved accessibility across the application and add tooltips to action buttons 2026-07-15 19:29:40 +02:00
MineTec 478f0ff20b implemented comprehensive accessibility (A11y) support across the app 2026-07-13 23:41:47 +02:00
MineTec 8274dd46cd refactored HTTP error handling and streamlined UI components by centralizing shared logic and removing redundant parameters 2026-07-13 22:58:02 +02:00
MineTec d7536ea5d0 remove dead endpoint staging mode (obsoleted by demo mode) 2026-07-13 22:32:38 +02:00
MineTec 398b147c76 simplify: dedupe boilerplate and remove dead code across all layers 2026-07-13 22:22:39 +02:00
MineTec 15791423ea added fileFetchedAt to TickerPageResponse and updated the Ticker UI to use it as the data currency timestamp for proxied files 2026-07-13 00:32:27 +02:00
MineTec 2f5a6b4ce0 split file_viewer into focused sub-widgets 2026-07-13 00:01:07 +02:00
MineTec 53bc6d5360 share weekKey extension and log splash precache failures 2026-07-12 23:56:47 +02:00
MineTec f50359b4eb use AppProgressIndicator for plain loading spinners 2026-07-12 23:45:55 +02:00
MineTec 9994a1f3fa extract SettingsDropdownTile for settings dropdowns 2026-07-12 23:42:56 +02:00
MineTec 4aa31a2e44 extract SettingsCheckboxTile for settings toggles 2026-07-12 23:41:12 +02:00
MineTec dfce3e7b5c add PromptDialog helper for text input dialogs 2026-07-12 23:36:46 +02:00
MineTec 564a334cdc route avatar crop and profile view through AppRoutes 2026-07-12 23:32:48 +02:00
MineTec db329c7299 adopt MarianumConnectQuery base in remaining queries 2026-07-12 23:28:51 +02:00
MineTec 0a2ff5c3fb add MarianumConnectQuery base and adopt it in list queries 2026-07-12 23:24:21 +02:00
MineTec 9b5198c6db add ticker page bloc and avatar disk cache 2026-07-12 23:18:53 +02:00
MineTec 94794ff092 remove unused dead code files 2026-07-12 23:17:47 +02:00
MineTec fe2b3c43b2 implemented custom subject colors for the timetable and unified the color palette system 2026-07-12 21:30:08 +02:00
MineTec a7111844b1 bugfixes, better background operation robustness and platform-specific error handling 2026-07-12 19:17:49 +02:00
MineTec babc347b18 implemented global user search integration for Talk chats with role-coded badges and direct chat creation logic 2026-07-12 19:05:27 +02:00
MineTec 44e45c9b78 implemented full interactive poll support in Talk, including creation, voting, and closing functionality 2026-07-12 17:54:30 +02:00
MineTec 3f44e9302f implemented dynamic quick reactions by surfacing emojis from message content in the chat options dialog 2026-07-12 16:42:30 +02:00
MineTec 59501d3b45 implemented enlarged emoji rendering for chat messages containing up to three emojis 2026-07-12 16:26:22 +02:00
MineTec 91a6216f66 migrated custom timetable events to Marianum-Connect and refactored push notification handling 2026-07-12 14:31:48 +02:00
MineTec c444ed54a5 migrated Marianum Message module to the Marianum-Connect API and implemented push notification deep linking 2026-07-11 17:37:50 +02:00
MineTec ff23199345 downgrade AGP and Gradle versions, update Ticker icon, and upgrade dependencies 2026-07-11 15:32:09 +02:00
MineTec 9545d2a946 upgraded Android Gradle Plugin to 9.2.1 and Gradle wrapper to 9.4.1 2026-07-10 19:16:07 +02:00
MineTec 37608e59b3 implemented Ticker page selection persistence and enhanced ProseMirror table display modes 2026-07-10 19:15:52 +02:00
MineTec 0d01f6b631 fixed push-registration not activated on app-login 2026-07-10 15:35:56 +02:00
MineTec b0d2e7a34b fixed inconsistent placement of appbar title on iOS 2026-07-09 12:04:20 +02:00
MineTec cedeb06569 implemented the Ticker module with a native ProseMirror document renderer and integrated API support for structured content, navigation trees, and proxied files 2026-07-09 00:51:08 +02:00
MineTec 1114291313 Add Fastlane Play Store integration with automated screenshots
Fastlane supply config (android/fastlane) with de-DE metadata structure,
integration_test-driven screenshot automation (screenshot_test.dart,
test_driver, tool/screenshots.sh) using stable keys on the login form,
refreshed materials/screenshots (phone + 7-inch) and app banner/symbol.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 23:43:10 +02:00
MineTec 9b74c9fd81 implemented a comprehensive client-side demo mode triggered by a "demo@" username prefix, serving curated fixture data for all app modules 2026-07-07 20:24:16 +02:00
MineTec 2668b111f1 updated login branding and the about dialog with updated slogan. 2026-07-07 19:21:11 +02:00
MineTec a19b67eb84 refactored and condensed technical documentation and comments across the codebase to improve readability 2026-07-06 23:17:06 +02:00
MineTec c48f5ef215 migrated feedback submission from the legacy mhsl.eu endpoint to the MarianumConnect API 2026-07-06 22:36:11 +02:00
MineTec ddbb3fbc19 implemented a new telemetry heartbeat and client-side error reporting system for MarianumConnect; replaced legacy mhsl.eu user index updates with a POST me/telemetry heartbeat using stable, securely-stored device identifiers; integrated global Flutter and platform error handlers to report uncaught exceptions with session-based deduplication and rate limiting. 2026-07-06 22:11:23 +02:00
MineTec 2be5051d12 migrated the breaker system to MarianumConnect and refactored the API response to a rule-based model with support for version-specific blocks using maxBuild. 2026-07-06 21:15:39 +02:00
MineTec 38a271929c implemented a guided notification permission flow triggered on the first Talk visit 2026-07-06 20:01:45 +02:00
MineTec 3be0113f93 fixed bug on leaving talk chat, spamming console 2026-07-06 15:55:27 +02:00
MineTec 2b59f11ea5 fixed z-index issue with background-loading indicator being behind talk-messages in chats 2026-07-06 15:51:08 +02:00
MineTec 124b1a5177 added support for simultan downloads in files and talk, support for background downloads, enhanced loading in files with retry 2026-07-06 15:43:17 +02:00
MineTec 614ab159af updated notification subject parsing to support newline separators for sender and message, while maintaining a fallback for legacy colon-delimited formats. 2026-07-05 23:57:48 +02:00
MineTec 5952c6619f fixed a race condition in long-poll initialization by adding a token validation guard to prevent stale chat sessions from hijacking the current state during navigation or app resumes. 2026-07-05 23:55:01 +02:00
MineTec dab208877f improved reliability and error handling for background notification actions by migrating to a top-level entry point for stable AOT callback resolution; implemented failure notifications to preserve undelivered reply text and surface technical errors; resolved a race condition that could resurrect dismissed notifications during silent re-renders; updated the Android manifest with the required broadcast receiver for notification actions; and refactored internal OCS methods to return detailed success/failure records for improved logging and field debugging. 2026-07-05 23:43:58 +02:00
MineTec 483fea62ba implemented dual Nextcloud push registration with separate general and talk apptypes to ensure reliable Talk notification delivery; introduced stacked MessagingStyle notifications for chat threads with support for circular conversation avatars and disk caching 2026-07-05 22:48:04 +02:00
MineTec 35e144799e Bumped Gradle to 8.14 and fixed build warnings 2026-07-05 00:17:20 +02:00
MineTec b14e8c8ea2 improved timetable date range calculation to ensure the current week remains accessible during holiday gaps or when persisted bounds are stale, and updated holiday labels to prioritize long names over short names in the calendar view. 2026-07-04 23:54:50 +02:00
MineTec 74a2ddd17f implemented an E2E-encrypted Nextcloud push-v2 notification system with support for RSA decryption and signature verification; introduced an iOS Notification Service Extension and native AppDelegate handlers for Talk actions (inline reply and mark-as-read); replaced the legacy notification registration with a new lifecycle managing app passwords and secure keypair storage; added background message handling with tray synchronization and a test notification utility in the settings. 2026-07-04 22:50:18 +02:00
MineTec 32f7c311bc fixed pending share race error on warm app start 2026-07-02 15:28:05 +02:00
MineTec 4bc7ffd37a implemented post-login splash screen with Lottie animations and integrated background data prefetching during the login transition 2026-06-29 12:00:26 +02:00
MineTec 13f4f79829 removed broad media access permissions from the manifest to comply with Google Play policies, and bumped the app version to 1.2.2+57. 2026-06-23 18:41:18 +02:00
MineTec a76b09af26 updated pods and SPM dependencies (Firebase 12.15, PhoneNumberKit)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 12:13:00 +02:00
Marianum 4fd09204ec iOS build 2026-06-23 12:06:22 +02:00
MineTec f554c57d8d updated version to 1.2.1+56 2026-06-22 22:44:54 +02:00
MineTec baa26a6e79 implemented a comprehensive Nextcloud file sharing system with support for user, group, and public link shares with gating based on server-side permissions; added sharing management interfaces including a share sheet; updated the file list with visual badges for incoming shares and improved OCS API response handling. 2026-06-02 21:42:08 +02:00
MineTec b6d06dd3b4 implemented foreign timetable support for students, teachers, rooms, and classes, including a searchable element picker with favorites support, introduced a capabilities system for feature gating, refactored the timetable UI into a reusable TimetableCalendarView component, and redesigned the chat input field with a unified emoji picker and integrated attachment actions. 2026-05-31 21:29:16 +02:00
MineTec 6e12da08c0 implemented a customizable chat background system with support for patterns, solid colors, and gallery images; added a dedicated settings page with live preview and adjustable blur/dim effects, updated the image cropper to support flexible aspect ratios for wallpapers, and integrated file cleanup logic during account logout. 2026-05-31 19:20:18 +02:00
MineTec 5ebf5bccdb implemented avatar management for user profiles and chat rooms, including 1:1 cropping, integrated OCS and Spreed avatar APIs, added cache invalidation logic, and updated the account settings view to display user info and profile pictures. 2026-05-31 18:42:30 +02:00
MineTec f966cf302b implemented favorite and leave actions for chat/rooms info view 2026-05-30 14:05:00 +02:00
MineTec 582432dbb9 implemented support for viewing large group profile pictures 2026-05-30 13:57:26 +02:00
MineTec ece0669f7d implemented a central haptic feedback system with configurable levels (off, reduced, full), added a Haptics facade providing semantic feedback methods, integrated haptic cues across navigation, settings toggles, and async action results, and updated version to 1.1.0+54 2026-05-30 13:54:19 +02:00
MineTec 01b4b44010 migrated holidays module to MarianumConnect API, replaced local Holiday model. 2026-05-24 17:49:25 +02:00
MineTec 93b9929f8f migrated timetable integration from WebUntis to the MarianumConnect API, implementing a Dio-based client with bearer token authentication, background session validation, and auto-refresh logic. 2026-05-23 17:32:42 +02:00
MineTec 2858f910c9 implemented DST-safe date arithmetic with new addDays and subtractDays extensions, updated timetable state to reset view and scroll boundaries on initialization to prevent stale views, added hard caps to calendar navigation, and updated version to 1.0.3+52 2026-05-22 15:08:30 +02:00
MineTec f185b3273a updated version to 1.0.2+50 2026-05-17 11:55:40 +02:00
MineTec 831ea56869 Merge pull request 'updated app icons and splash screen assets, updated sort logic' (#100) from develop-logo into develop
Reviewed-on: #100
2026-05-17 09:54:59 +00:00
MineTec 215911cf29 refactored room and file sorting to use direct comparators instead of temporary sort strings, removed obsolete 'sort' properties from API models, and improved file list sorting with case-insensitive name comparisons and null-safe date handling 2026-05-17 00:27:17 +02:00
MineTec e5873f73b9 updated app icons and splash screen assets across Android, iOS, and Web platforms, added a new brand SVG asset, refined Android adaptive icon scaling with insets, and updated iOS asset catalog configurations and launch screen dimensions 2026-05-16 14:33:22 +02:00
MineTec 582eff8750 implemented current schoolyear API and dynamic timetable scroll boundaries, added handling for out-of-range errors to narrow accessible dates, optimized holiday region rendering by collapsing overlaps, and refined holiday tile UI 2026-05-14 15:07:48 +02:00
MineTec 2cb8321d07 implemented recurrence exception (EXDATE) support for custom events, refactored timetable break and holiday generation logic, and refined RRule editor UI/theming and tile layouts 2026-05-14 12:58:29 +02:00
MineTec 194d8d1857 moved flutter_native_splash from dev_dependencies to dependencies 2026-05-13 20:47:10 +02:00
MineTec 22e9c43f78 updated version to 1.0.1+49 2026-05-13 20:39:47 +02:00
MineTec 4c04d00323 improved app rating UI logic by showing a disabled state during availability check instead of hiding the component 2026-05-13 20:39:05 +02:00
MineTec 0fd42439e2 improved unknown file preview handling with probe failure fallbacks and switched to an explicit TabController in the share view to prevent build-time layout issues 2026-05-13 20:28:30 +02:00
MineTec d970cfbe0c centered file leading icons in share folder picker 2026-05-13 20:14:29 +02:00
MineTec 91ab109ec5 corrected spelling of Notendurchschnittsrechner in app modules and grade averages view 2026-05-13 20:09:46 +02:00
MineTec d9fcd9f624 implemented file thumbnails and enhanced file type icons, added reusable FileLeading widget, and updated search to support previews 2026-05-13 20:05:54 +02:00
MineTec 092f9b622b implemented Nextcloud file previews for unknown file types using fileId and has-preview flags, updated file models, and refined manual refresh logic. 2026-05-13 19:44:26 +02:00
MineTec 843686358f overhauled feedback dialog UI, implemented async action buttons for submission and image picking, and added a custom image preview widget 2026-05-13 19:07:06 +02:00
MineTec cfcb901adb implemented confirmation dialog for resetting module settings 2026-05-13 19:00:32 +02:00
MineTec ba5d9e0e4e integrated link sharing and clipboard options directly into QR view and simplified sharing flow by removing intermediate selection dialog 2026-05-13 18:57:56 +02:00
MineTec e8707b36f1 updated forward icon in message options and added scale limits to profile picture viewer 2026-05-13 18:50:45 +02:00
MineTec d0ba7c0fd6 refactored direct chat logic into a shared utility, implemented direct message shortcuts in the participant list and message reactions, and added reaction visibility checks in the message options dialog 2026-05-13 18:46:34 +02:00
MineTec a09817a975 added verbose error body in case of errors 2026-05-13 18:37:50 +02:00
MineTec 37dbb7b374 implemented keyboard-aware back navigation and refined message sending logic to prevent phantom drafts and handle mid-send navigation 2026-05-13 18:37:14 +02:00
MineTec 6c7d217463 stabilized LoadableStateConsumer widget hierarchy to prevent scroll resets, added pull-to-refresh configuration, and disabled it in chat view 2026-05-13 18:22:25 +02:00
Marianum 58fb843f3d fixed iOS widget layout 2026-05-13 16:09:43 +02:00
Marianum d8a5ccfe80 iOS widget enhancements 2026-05-12 16:46:56 +02:00
Marianum 1ae3f7bb83 finalized iOS setup 2026-05-12 15:47:32 +02:00
MineTec 8c76f2d816 removed now indicator from android and ios widgets 2026-05-11 13:55:16 +02:00
MineTec c46f14f6a6 updated kotlin gradle plugin version to 2.2.20 2026-05-10 20:27:50 +02:00
MineTec b2b00d321e Merge pull request 'implemented chat long-polling and optimistic updates, centralized notification management, optimized avatar caching, cleanup and bugfixes' (#99) from develop-notifications into develop
Reviewed-on: #99
2026-05-10 15:02:12 +00:00
MineTec 1a11b9ac60 refactored internal documentation and simplified comments across chat BLoCs, file viewer, and navigation components 2026-05-10 17:01:50 +02:00
MineTec a0bc46f522 optimized avatar and linkify performance, refined navigation to preserve popups, implemented read marker caching, and added file size limits for saving, minor timetable details changes 2026-05-10 16:40:39 +02:00
MineTec 1458d8ce49 implemented chat long-polling and optimistic updates, centralized notification management, optimized avatar caching 2026-05-10 15:47:55 +02:00
MineTec 6ae396e605 Merge pull request 'general search, talk enhancements, overhauled fileviewer' (#98) from develop-search into develop
Reviewed-on: #98
2026-05-09 22:55:11 +00:00
MineTec ed2badfd35 fixed chat bubble link styling and gesture handling, and added android package visibility for common schemes 2026-05-10 00:54:13 +02:00
MineTec 1ff57b29f9 overhauled file viewer with video, audio, text, and SVG support, added media player and line-numbered text views, and fixed search controller recursion 2026-05-10 00:33:09 +02:00
MineTec c50a850ac9 reordered files app bar actions by moving search icon 2026-05-09 23:43:29 +02:00
MineTec 15833f3685 implemented disposal guard in files search controller to safely handle async listener notifications 2026-05-09 23:40:04 +02:00
MineTec bf28a678c9 implemented background prefetching for files root, added 24-hour caching for root directory listing, and enabled cache renewal for manual refreshes 2026-05-09 23:39:06 +02:00
MineTec 14090b96f4 implemented file search with local cache and server-side support, added result highlighting, and integrated search delegate into files page 2026-05-09 23:20:11 +02:00
MineTec 8e6b1877cc implemented search for marianum messages with name and date filtering 2026-05-09 22:35:20 +02:00
MineTec 9accb488f2 added delete confirmation dialog for chat messages and refined deletion logic flow 2026-05-09 22:32:45 +02:00
MineTec 79a6d9a594 filtered deleted messages from search and chat view, refactored chat bubble styling for deleted comments, and updated tests 2026-05-09 22:28:26 +02:00
MineTec 7d02e70459 implemented short relative date formatting for chat and added unit tests 2026-05-09 22:23:25 +02:00
MineTec 4c190de479 implemented in-chat search with text highlighting, added search navigation UI, and integrated scrollable list for message jumping 2026-05-09 22:21:36 +02:00
MineTec b36d1e02f5 Merge pull request 'added native features like homescreen-widgets and share intents' (#97) from develop-native into develop
Reviewed-on: #97
2026-05-09 19:35:32 +00:00
MineTec 53b290ab49 ensured timetable visibility on widget navigation by resetting root navigator 2026-05-09 21:31:39 +02:00
MineTec b422430994 implemented message forwarding and direct chat creation from group members, and added specialized share picker for forwarded content 2026-05-09 20:39:19 +02:00
MineTec 151678f0fe implemented internal file sharing and saving, added server-side file references, refactored share pickers for unified flows, and updated UI branding labels 2026-05-09 20:18:52 +02:00
MineTec cb2c38aaa1 implemented native share intent support for android and ios with chat and folder pickers 2026-05-09 19:42:51 +02:00
MineTec 00664c66a8 added base homescreen-widget setup, working on Android, iOS in progress 2026-05-09 18:01:05 +02:00
MineTec 0ff5eb7bc9 Merge pull request 'claude refactor' (#95) from develop-refactor into develop
Reviewed-on: #95
2026-05-08 19:49:43 +00:00
MineTec 3b8da1d3d6 dart format 2026-05-08 20:12:40 +02:00
MineTec 9e139b5704 refactored data providers with centralized cache resolution, unified UI using custom dialogs and bottom sheets, and enhanced network error handling for Dio and TLS errors 2026-05-08 20:01:45 +02:00
MineTec c62a14645a refactored broad range of the application, split files, modularized calendar and file views, centralized bottom sheets and clipboard handling, and implemented unit test coverage 2026-05-08 19:05:16 +02:00
MineTec 3b1b0d0c19 fixed lesson merging mutation, improved overlap detection, and implemented priority-based lane assignment with tablet support 2026-05-07 13:27:40 +02:00
MineTec c32e64fe74 improved yOfDateTime precision and period-based calculation in workweek calendar 2026-05-07 09:51:13 +02:00
MineTec 710e88d744 refactored chat data fetching to support separate cache and network callbacks 2026-05-07 09:46:30 +02:00
MineTec 517e515ac1 centered login view and updated layout constraints 2026-05-07 09:11:09 +02:00
MineTec e8f0c4383c added camera support and enabled gallery selection on ios 2026-05-06 23:09:44 +02:00
MineTec b8cac73e74 updated timetable UI with event status and enhanced appointment tile rendering 2026-05-06 22:53:24 +02:00
MineTec 95ef29fb09 implemented dynamic module settings and configurable bottom bar, added all-day event support to timetable, and overhauled marianum dates UI with month grouping and search 2026-05-06 22:37:41 +02:00
MineTec 86d12884fc custom login implementation, period-based timetable layout with overlap handling, enhanced error dialogs, and unified bottom sheets 2026-05-06 20:42:09 +02:00
MineTec 50d2941e52 refactored lesson details, centralized logout logic, and added resume re-fetch 2026-05-06 16:27:45 +02:00
MineTec 71506aab2d Merge remote-tracking branch 'origin/develop-refactor' into develop-refactor 2026-05-06 11:59:17 +02:00
MineTec 4e1272aba9 claude refactorings, flutter best practices, platform dependent changes, general cleanup 2026-05-06 11:59:01 +02:00
MineTec 72ebe6f7e7 claude refactorings, flutter best practices, platform dependent changes, general cleanup 2026-05-06 11:58:50 +02:00
MineTec 4b1d4379a0 loading state and error handling refactor 2026-05-06 10:11:45 +02:00
MineTec 2c376afd91 removed stray character in settings.gradle 2026-05-05 22:38:07 +02:00
MineTec 54ba04a7bd wait for account data population and set initial AccountBloc status 2026-05-05 22:08:10 +02:00
MineTec 9b5a70b285 api and storage restructure 2026-05-05 22:00:07 +02:00
MineTec 4f796dac2e folder restructuring 2026-05-05 21:44:23 +02:00
MineTec db9c3386f1 better loading indicators for timetables, talk and files 2026-05-05 21:07:48 +02:00
MineTec bee5c02a4f marianum appointments 2026-05-05 16:05:07 +02:00
MineTec e8faa77e70 refactored timetable 2026-05-05 13:49:45 +02:00
MineTec 551c1bf1fa claude refactor 2026-05-04 13:54:39 +02:00
MineTec 9973f12733 Merge pull request 'added opacity to past custom events' (#94) from develop-customEventOpacity into develop
Reviewed-on: #94
2026-05-04 10:20:05 +00:00
MineTec 278fed52f1 fixed indention 2026-05-04 12:19:43 +02:00
Pupsi f89ac87c51 added opacity to past custom events 2026-05-04 09:45:24 +02:00
MineTec 0b8380eff0 removed toolchain vendor restriction 2026-05-04 09:04:54 +02:00
Pupsi 4ef3a167ea fixed dart errors in notificationService 2026-05-04 08:51:16 +02:00
MineTec c26cefc073 moved native splash dependency to runtime dependencies 2026-04-04 00:38:25 +02:00
MineTec e78ef0df49 toolchain migration, agp upgrade, flutter upgrade, dependency upgrades 2026-04-04 00:30:24 +02:00
MineTec 0b6c80e84c fixed hanging chat when it contains any 'call' 2026-04-04 00:11:44 +02:00
Pupsi a52817231e fixed list view breaking layout 2026-02-01 17:16:42 +01:00
Pupsi f6933b6529 Merge pull request 'develop-polls' (#93) from develop-polls into develop
Reviewed-on: #93
Reviewed-by: Elias Müller <elias@elias-mueller.com>
2026-02-01 14:56:23 +00:00
Pupsi e4243e53ac resolved pr issues 2026-02-01 15:55:43 +01:00
Pupsi 0aead45191 Merge remote-tracking branch 'origin/develop' into develop-polls
# Conflicts:
#	devtools_options.yaml
2026-02-01 15:37:26 +01:00
Pupsi dacefd321b updated poll list design 2026-02-01 15:35:40 +01:00
Pupsi 92a9a7358e changed link to directly open the chat 2026-02-01 15:20:01 +01:00
Pupsi 174e6ac0b7 fixed finished polls causing errors, made poll list dense 2026-02-01 15:07:48 +01:00
MineTec c9eaed782a update grade averages UI and enable devtools extensions 2026-02-01 15:06:49 +01:00
Pupsi 567184bcf9 filtered system messages for poll votes 2026-02-01 13:56:39 +01:00
Pupsi 541d6ef164 fixed issues with null values in votes map 2026-02-01 13:32:18 +01:00
Pupsi 3469d02033 changed poll dialog to only show results 2026-02-01 03:23:36 +01:00
Pupsi 699aec8ab5 Merge branch 'develop' into develop-polls 2026-02-01 00:06:51 +01:00
Pupsi 7a3a022ecd Merge remote-tracking branch 'origin/develop' into develop 2026-01-31 23:41:05 +01:00
Pupsi 9d8a99df7c added screen_brightness 2026-01-31 23:33:31 +01:00
Pupsi a47e52e8e7 added screen_brightness 2026-01-31 23:31:53 +01:00
MineTec a1fd21de04 Merge pull request 'develop-fileDownload' (#92) from develop-fileDownload into develop
Reviewed-on: #92
Reviewed-by: Elias Müller <elias@elias-mueller.com>
2026-01-31 22:15:50 +00:00
Pupsi b3d8586c04 Merge remote-tracking branch 'origin/develop' into develop-fileDownload
# Conflicts:
#	pubspec.yaml
2026-01-31 22:59:46 +01:00
Pupsi 0409c5463f combined actions to popup menu 2026-01-31 22:55:49 +01:00
MineTec 7a3b69fade update dependencies and bump version to 0.1.7+45
- Bump version to `0.1.7+45` and update SDK constraint to `>=3.8.0 <4.0.0`.
- Update numerous dependencies
2026-01-31 22:40:50 +01:00
Pupsi df275c0108 added file saver 2026-01-31 22:17:31 +01:00
MineTec 0525453d48 migrate launcher icons configuration to standalone file 2026-01-31 21:09:56 +01:00
MineTec 4e8b2f34f9 bumped version 2026-01-26 17:13:58 +01:00
MineTec bc6a069c90 fixed voice-message preventing chat loading 2026-01-26 16:44:39 +01:00
MineTec bfa0b0f5c0 feat: add devtools extensions and fix poll dialog UI/UX
- Enabled `provider` and `shared_preferences` extensions in `devtools_options.yaml`.
- Added logging for message object data on chat bubble tap.
- Fixed layout issues in poll dialog by wrapping `LoadingSpinner` in a `Column` and changing `ListView` to a `Column` in `pollOptionsList.dart`.
- Updated poll submission button to wait for the poll state to load before allowing interaction.
2026-01-18 10:28:17 +01:00
MineTec 274b77f705 Merge branch 'develop' into develop-polls 2026-01-17 23:22:52 +01:00
Marianum c7ea80bea9 updated xCode settings 2025-12-10 14:37:36 +01:00
MineTec 1c787fdc4d feat: Bump version to 0.1.5+43 2025-12-03 17:23:11 +01:00
MineTec a689cf4fef Merge remote-tracking branch 'origin/develop' into develop 2025-12-03 17:12:40 +01:00
MineTec 8f92ab06d9 fix webuntis server URL and certificate 2025-12-03 17:12:33 +01:00
Pupsi b68bec9ebd WIP: add option to vote on polls 2025-10-10 11:39:57 +02:00
Pupsi 81f65750b7 added functionality to show own votes in polls 2025-10-10 02:01:43 +02:00
Pupsi 7b7ab2e82e Merge pull request 'develop-fix-geolocation' (#91) from develop-fix-geolocation into develop
Reviewed-on: #91
Reviewed-by: Elias Müller <elias@elias-mueller.com>
2025-10-01 17:08:16 +00:00
Pupsi a460d2b296 changed naming of variable at participants sorting 2025-10-01 19:03:43 +02:00
Pupsi 33dd6c4c69 fixed wrong sorting in participants view 2025-10-01 16:15:09 +02:00
Pupsi 06c27d6b50 fixed geo-location breaking chats and white text on white background 2025-10-01 16:06:02 +02:00
MineTec 7dea44d1e8 Merge remote-tracking branch 'origin/develop' into develop 2025-09-10 20:23:20 +02:00
MineTec 5f27956035 fix webuntis auth retry not working correctly 2025-09-10 20:23:14 +02:00
MineTec 32799f648c working ios build update 2025-09-09 13:58:00 +02:00
MineTec 859b85ab2c Revert "Upgraded Gradle and Android Gradle Plugin"
This reverts commit bd1101c348.
2025-09-06 18:51:00 +02:00
MineTec bd1101c348 Upgraded Gradle and Android Gradle Plugin
Upgraded Gradle from 8.9 to 8.13 and the Android Gradle Plugin from 8.7.3 to 8.13.0.
2025-09-06 17:35:41 +02:00
MineTec f29c84d05c fixed timetable subject name requirement 2025-09-06 17:03:01 +02:00
MineTec 7dbd6038f3 prevent common "change" tiles in timetable, more robust parsing 2025-09-06 16:47:47 +02:00
MineTec 877633f4de Merge pull request 'develop-groupedParticipants' (#89) from develop-groupedParticipants into develop
Reviewed-on: #89
Reviewed-by: Elias Müller <elias@elias-mueller.com>
2025-09-06 14:12:10 +00:00
Pupsi 22d3d18a17 changed naming for participants 2025-09-06 16:09:47 +02:00
Pupsi d65e61c297 Merge branch 'develop' into develop-groupedParticipants 2025-09-06 13:58:08 +00:00
MineTec 467b0e0dd8 Merge remote-tracking branch 'origin/develop' into develop 2025-09-06 15:57:19 +02:00
Pupsi 25a6ef37fa Merge branch 'develop' into develop-groupedParticipants 2025-09-06 13:57:10 +00:00
MineTec 34763ace4a Merge remote-tracking branch 'origin/develop' into develop 2025-09-06 15:57:08 +02:00
MineTec 590a70c623 Merge remote-tracking branch 'origin/develop' into develop 2025-09-06 15:56:51 +02:00
MineTec 9b58412ca7 fixed missing no reaction text
fixed loading indicator being delayed on file download
2025-09-06 15:56:46 +02:00
MineTec a6c16e41c2 fixed missing no reaction text
fixed loading indicator being delayed on file download
2025-09-06 15:56:28 +02:00
Pupsi 117434a5e3 Merge branch 'develop' into develop-groupedParticipants 2025-09-06 13:54:40 +00:00
Pupsi e4582eaac5 removed commented code 2025-09-06 15:46:55 +02:00
Pupsi 430d5b8dc7 Merge remote-tracking branch 'origin/develop' into develop-groupedParticipants
# Conflicts:
#	pubspec.yaml
2025-09-06 15:36:12 +02:00
MineTec 9177c30d6e fixed display dimensions of messages with files 2025-09-06 15:33:14 +02:00
MineTec 344f8f6d2c Merge branch 'develop' into develop-fileMessagesWithText 2025-09-06 15:13:15 +02:00
MineTec 46971a8d46 upgraded syncfusion dependencies 2025-09-06 14:51:14 +02:00
MineTec f330ef3f56 updated project dependencies and sdk. Comptaible with Flutter 3.35.3 2025-09-06 14:47:08 +02:00
MineTec 85f9988453 renamed timetable in ui 2025-09-06 14:12:13 +02:00
Pupsi f3de0bc165 centered file preview, made text copyable 2025-06-24 15:09:37 +02:00
Pupsi 421ee9179d grouped the participants list 2025-06-24 14:18:14 +02:00
MineTec 0a66858d93 Merge pull request 'sorted participants list alphabetically' (#87) from develop-sortedParticipants into develop
Reviewed-on: #87
Reviewed-by: Elias Müller <elias@elias-mueller.com>
2025-06-24 10:47:07 +00:00
MineTec 49428680de Merge branch 'develop' into develop-sortedParticipants 2025-06-24 10:46:58 +00:00
Pupsi 0c676dc3d6 made perticipants list stateless 2025-06-23 17:54:12 +02:00
Pupsi c702b610c5 removed logging 2025-06-23 11:17:59 +02:00
Pupsi 5938c6b3c3 fixed chat search 2025-06-23 11:16:39 +02:00
Pupsi 8000475c1f aligned text to the left 2025-06-16 16:07:04 +02:00
Pupsi da772f17cc changed file messages to show their text or their file name 2025-06-10 21:35:12 +02:00
Pupsi c44b0464a4 sorted participants list alphabetically 2025-06-10 20:19:44 +02:00
MineTec 9d0cf8e313 bumped version 2025-04-16 13:16:49 +02:00
MineTec f0009dad88 renamed files back to localized string 2025-04-16 13:15:31 +02:00
MineTec 905206f242 added missing implementation of "note to self", disabled breakers in debug environments 2025-03-11 16:22:02 +01:00
MineTec 769fbc1b6a added timetable color substitute teachers 2025-02-14 19:31:46 +01:00
MineTec 8daf57bcee #75 pinned timetable to german timezone 2025-02-13 22:28:45 +01:00
MineTec 33d488946a updated android configuration files 2025-02-09 20:42:47 +01:00
MineTec 41a5e021c5 Merge pull request 'made app modules movable in their order' (#84) from develop-reorderableAppModules into develop
Reviewed-on: #84
Reviewed-by: Pupsi <larslukasneuhaus@gmx.de>
2025-02-09 17:36:20 +00:00
MineTec 8f58893553 Merge branch 'develop' into develop-reorderableAppModules 2025-02-09 17:36:12 +00:00
MineTec 626d3d5564 added minimum message drag distance for chat reply 2025-02-09 15:16:02 +01:00
MineTec d833cdb733 made app modules movable in their order 2025-02-09 15:06:14 +01:00
MineTec 8868914a76 restructured settings and devtools 2025-02-08 23:21:20 +01:00
MineTec 70e6f82b10 updated chat images loading animation 2025-02-08 22:53:14 +01:00
MineTec 6651613331 fixed files cache not working correctly 2025-02-08 22:35:20 +01:00
MineTec 9ad0f624de Merge remote-tracking branch 'origin/develop' into develop 2025-02-08 21:40:49 +01:00
MineTec 82c143f847 bumped version 2025-02-08 21:40:39 +01:00
MineTec 1fdf731b81 Merge pull request 'added option for timetable naming modes' (#83) from feature-timetableNamingSetting into develop
Reviewed-on: #83
Reviewed-by: Pupsi <larslukasneuhaus@gmx.de>
2025-01-24 22:27:06 +00:00
MineTec 2d3ccd25b4 Merge branch 'develop' into feature-timetableNamingSetting 2025-01-24 22:26:52 +00:00
MineTec 385ee806d6 updated feedback info text 2025-01-24 13:53:12 +01:00
MineTec 92aef41031 updated room plan image 2025-01-24 11:54:35 +01:00
MineTec 65b29ec4b8 added option for timetable naming modes 2025-01-24 11:50:14 +01:00
MineTec 9f51d68531 updated build runner tasks 2025-01-24 11:02:03 +01:00
MineTec 5bc4ba6332 replaced version wildcard with version range 2025-01-23 23:39:01 +01:00
MineTec e9739ac2d5 updated app badger 2025-01-23 22:41:22 +01:00
MineTec 4d3a33dd9b updated project 2025-01-23 11:20:08 +01:00
MineTec ddeeaeaeac Merge pull request 'develop-bloc-holidays' (#72) from develop-bloc-holidays into develop
Reviewed-on: #72
Reviewed-by: Pupsi <larslukasneuhaus@gmx.de>
2024-06-23 21:00:45 +00:00
MineTec c8e31b896b added eof linebreak 2024-06-23 22:59:26 +02:00
MineTec c443a1d567 fixed disclaimer not showing on first visit 2024-06-23 20:31:43 +02:00
MineTec 08ef784f57 Merge remote-tracking branch 'origin/develop' into develop-bloc-holidays 2024-06-22 16:47:15 +02:00
MineTec 999e30ab3a added le-r10-certificate and removed/added some logging 2024-06-22 16:45:43 +02:00
MineTec c4c882a77d updated app screenshots 2024-06-14 22:20:54 +02:00
MineTec fe93a94fc6 bloc for holidays 2024-06-12 15:53:13 +02:00
MineTec a33c4ddac5 wip: fixed state not updating correctly 2024-05-27 22:28:42 +02:00
MineTec 634fe41e78 wip: bloc for holidays 2024-05-14 14:54:01 +02:00
MineTec 328c4f410c removed unused code and files 2024-05-12 20:45:00 +02:00
MineTec 43471fcf3d Merge pull request 'develop-bloc' (#68) from develop-bloc into develop
Reviewed-on: #68
Reviewed-by: Pupsi <larslukasneuhaus@gmx.de>
2024-05-12 13:12:22 +00:00
MineTec e78e2b8cd4 Merge remote-tracking branch 'origin/develop' into develop-bloc 2024-05-12 15:11:00 +02:00
MineTec a57f42d4ed resolved pr comments 2024-05-12 15:07:57 +02:00
MineTec 8ff993bf19 removed old marianum message structure 2024-05-12 14:44:12 +02:00
MineTec 8968e53e5c removed test page 2024-05-12 14:38:06 +02:00
Pupsi 430f2fe603 Merge pull request 'develop-replyToMessages' (#67) from develop-replyToMessages into develop
Reviewed-on: #67
Reviewed-by: Pupsi <larslukasneuhaus@gmx.de>
2024-05-12 12:36:14 +00:00
Pupsi d00592c3e7 Merge branch 'develop' into develop-replyToMessages 2024-05-12 12:35:52 +00:00
Pupsi 3fb48b5bcf added option to reply on long press (and better possibilities to play with chat bubble drag) 2024-05-12 14:30:41 +02:00
MineTec 69fc98ad45 automatic updating of last timestamp for bloc cache 2024-05-12 14:27:16 +02:00
MineTec 76a1f65083 Merge remote-tracking branch 'origin/develop' into develop-bloc 2024-05-12 14:01:57 +02:00
Pupsi ed35e91d59 Merge pull request 'updated localstore collection to global constant' (#69) from develop-fixLogoutCache into develop
Reviewed-on: #69
Reviewed-by: Pupsi <larslukasneuhaus@gmx.de>
2024-05-12 11:27:17 +00:00
MineTec ebbb70dc96 added timestamp to bloc cache, showing age in offline mode 2024-05-12 02:39:35 +02:00
MineTec 3281b134e0 moved message pdf view 2024-05-12 00:36:24 +02:00
MineTec 2056be23cd added minimum duration of loading animation 2024-05-12 00:31:23 +02:00
MineTec e57a1a915e implemented marianum message dataloader 2024-05-11 19:29:12 +02:00
MineTec 181682a424 moved reload actions out of error context 2024-05-11 17:52:53 +02:00
MineTec 9fa711e460 loadable error screen, reload actions, autoreload 2024-05-11 14:20:00 +02:00
MineTec 1f30e2d97f not replyable messages are no longer swipable 2024-05-10 22:29:40 +02:00
MineTec 0f84257eba saving message references in draft 2024-05-10 22:27:24 +02:00
MineTec b171fef348 repository and data provider concept 2024-05-07 22:15:56 +02:00
MineTec 81f5b662b7 added support for rich object messages 2024-05-07 11:01:46 +02:00
MineTec 7a393bf630 refactored answer references and added animation 2024-05-07 10:51:46 +02:00
MineTec fc72391a75 fixed bug when changing accounts 2024-05-07 09:01:30 +02:00
MineTec 6ad8203b6a implemented new loadable state concept 2024-05-05 22:58:40 +02:00
MineTec f58a2ec8cd revamp on bloc approach 2024-05-05 15:48:26 +02:00
Pupsi 91ef689d2a replies now get displayed 2024-05-01 17:37:03 +02:00
MineTec ee6af2bc07 Merge remote-tracking branch 'origin/develop-bloc' into develop-bloc
# Conflicts:
#	lib/state/widgets/components/error_bar.dart
2024-04-24 19:40:42 +02:00
MineTec 2db3b29359 added content position animation when error bar is displayed 2024-04-24 19:39:59 +02:00
MineTec 107a5bdbf8 added content position animation when error bar is displayed 2024-04-24 19:23:32 +02:00
MineTec 3802e4a5b9 fixed missing animation on error bar 2024-04-24 18:45:10 +02:00
MineTec 04e8ce9c0a loadable state is now detecting device connection status on failure 2024-04-23 22:40:18 +02:00
MineTec 450c26b187 WIP state management loadable errorbar 2024-04-23 14:48:12 +02:00
MineTec 7129c0dee8 wip basics for bloc based state management 2024-04-22 23:02:03 +02:00
Pupsi ae6b6511d7 parent message gets shown in chat bubble 2024-04-19 18:55:07 +02:00
Pupsi 232a767312 Merge remote-tracking branch 'origin/develop' into develop-replyToMessages 2024-04-19 16:23:59 +02:00
MineTec f5407d2477 updated localstore collection to global constant 2024-04-16 14:39:20 +02:00
MineTec 19aca8f97f updated version 2024-04-11 20:55:38 +02:00
Pupsi c4a7533315 added option to react to messages 2024-04-11 18:22:55 +02:00
MineTec d067ee702a Merge pull request 'feedback dialog is now scrollable' (#64) from develop-scrollableFeedbackWidget into develop
Reviewed-on: #64
Reviewed-by: Elias Müller <elias@elias-mueller.com>
2024-04-10 10:14:31 +00:00
Pupsi dfd9459922 feedback dialog is now scrollable 2024-04-10 12:10:57 +02:00
MineTec 51373fe7a2 Merge pull request 'double tap on messages to show options' (#63) from develop-doubleTapMessages into develop
Reviewed-on: #63
Reviewed-by: Elias Müller <elias@elias-mueller.com>
2024-04-09 18:37:28 +00:00
Pupsi cafa1248d5 resolved issue with merge 2024-04-09 19:46:00 +02:00
Pupsi ed710b3c2e Merge branch 'develop' into develop-doubleTapMessages
# Conflicts:
#	lib/view/pages/talk/components/chatBubble.dart
2024-04-09 19:42:51 +02:00
Pupsi 8dd3ca9eae double tap on messages to show options 2024-04-09 19:34:45 +02:00
Pupsi 07ffa374fe Merge pull request '#53 fixed misplaced profile pictures' (#62) from develop-fixSwappedProfilePictures into develop
Reviewed-on: #62
Reviewed-by: Pupsi <larslukasneuhaus@gmx.de>
2024-04-09 17:22:08 +00:00
MineTec 0302c10fcd #53 fixed misplaced profile pictures 2024-04-09 18:53:56 +02:00
MineTec 92361ec455 Merge pull request 'added upload with multiple files' (#61) from develop-uploadMultipleFiles into develop
Reviewed-on: #61
2024-04-09 08:21:54 +00:00
Pupsi cf4dea566e solved pr comments; picking multiple Images from Gallery is now possible 2024-04-08 22:39:28 +02:00
Pupsi d8c72a5d28 solved most pr comments and a bug 2024-04-07 16:48:38 +02:00
Pupsi 9803b06af1 merged to origin/devlop 2024-04-07 15:58:51 +02:00
Pupsi 8131ccae1e solved some pr comments 2024-04-07 15:49:43 +02:00
Pupsi e901f139d6 solved some pr comments 2024-04-06 13:34:52 +02:00
MineTec 02dd659d1d fixed typos 2024-04-06 11:40:48 +02:00
Pupsi 277b3366f9 added upload with multiple files 2024-04-05 18:19:49 +02:00
Pupsi b4defb9eda added upload with multiple files 2024-04-05 18:16:12 +02:00
MineTec 4c7f53e309 updated project style guidelines 2024-04-03 19:18:17 +02:00
MineTec 27618f4404 added min/ max-scale to fileViewer 2024-04-02 19:16:49 +02:00
MineTec 8bf5e5a06a disabled gestureNavigation on bottomNavigation 2024-04-02 19:12:28 +02:00
MineTec bc60362984 Merge pull request 'added option to choose more emoji reactions' (#55) from develop-moreEmojiReactions into develop
Reviewed-on: #55
2024-04-02 16:51:32 +00:00
Pupsi 74c7c16877 changed a standard emoji to eyes looking around 2024-04-02 18:30:13 +02:00
Pupsi 0a577b5f48 Merge branch 'develop' into develop-moreEmojiReactions 2024-04-02 16:26:33 +00:00
Pupsi 57ddee2dc9 removed search option and action bar 2024-04-02 18:24:12 +02:00
Pupsi 8ae8c97b41 made emojis in emoji picker bigger 2024-04-02 18:10:54 +02:00
MineTec 883db8d7ef Merge pull request 'develop-unfocusTextField' (#56) from develop-unfocusTextField into develop
Reviewed-on: #56
2024-04-02 14:55:35 +00:00
Pupsi 6450b292a7 resolved pr comments 2024-04-02 16:45:43 +02:00
Pupsi 65d15ffd4e removed commented code 2024-04-02 15:51:06 +02:00
Pupsi 07401d0864 resolved pr comments and fixed a bug 2024-04-02 15:47:55 +02:00
Pupsi 21411e1517 added unfocus at textfields for custon events and feedback 2024-04-02 15:03:37 +02:00
Pupsi 339d402422 added unfocus when tapping outside textfield 2024-04-02 14:41:36 +02:00
Pupsi c54a42aa43 added unfocus when tapping on background image 2024-04-02 14:34:35 +02:00
Pupsi 42574583ed added unfocus when tapping on background image 2024-04-02 14:32:37 +02:00
Pupsi 9aa3f7c058 added option to choose more emoji reactions 2024-04-02 14:11:48 +02:00
MineTec 1b407562df Merge pull request 'old cache gets deleted' (#52) from develop-removeOldCache into develop
Reviewed-on: #52
2024-04-02 07:34:05 +00:00
MineTec f86c97dcf0 Merge branch 'develop' into develop-removeOldCache 2024-04-02 07:33:13 +00:00
MineTec 6b4d6a1c8c Merge pull request 'develop-biggerFeedbackWidget' (#51) from develop-biggerFeedbackWidget into develop
Reviewed-on: #51
2024-04-02 07:32:51 +00:00
Pupsi 20dfe2c2e6 Merge branch 'develop' into develop-removeOldCache 2024-04-01 12:20:49 +00:00
Pupsi 6c2c305f1c old cache gets deleted 2024-04-01 14:18:23 +02:00
Pupsi dbbf6f6c5c Merge branch 'develop' into develop-biggerFeedbackWidget 2024-04-01 10:57:52 +00:00
Pupsi 07b32f1e32 added option to attach image to feedback 2024-04-01 12:57:27 +02:00
Pupsi ea329d8d64 added option to attach image to feedback 2024-03-31 21:02:47 +02:00
Pupsi f45848331e added bigger feedback dialog widget 2024-03-31 15:35:12 +02:00
MineTec a1ad6aa582 updated version and dependencies 2024-03-31 14:37:08 +02:00
MineTec 3e3e2579f0 Merge pull request 'added connected double lessons with own setting' (#49) from develop-connectedDoubleLessons into develop
Reviewed-on: #49
2024-03-31 10:50:58 +00:00
Pupsi 681b5e42c3 removed spaces 2024-03-31 12:31:53 +02:00
Pupsi a0c025b58b code fixes, added isSameOrAfter for DateTime objects, added check for teacher room and status 2024-03-30 23:01:37 +01:00
Pupsi 20d7b16ede Merge branch 'develop' into develop-connectedDoubleLessons 2024-03-30 22:05:54 +01:00
Pupsi afdc02f2a4 added connected double lessons with own setting 2024-03-30 18:26:33 +01:00
1126 changed files with 67274 additions and 12464 deletions
+20 -8
View File
@@ -48,14 +48,14 @@ lib/generated_plugin_registrant.dart
#pubspec.lock
# Android related
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/key.properties
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
materials/screenshots/android/**/gradle-wrapper.jar
materials/screenshots/android/.gradle
materials/screenshots/android/captures/
materials/screenshots/android/gradlew
materials/screenshots/android/gradlew.bat
materials/screenshots/android/key.properties
materials/screenshots/android/local.properties
materials/screenshots/android/**/GeneratedPluginRegistrant.java
# iOS/XCode related
**/ios/**/*.mode1v3
@@ -350,3 +350,15 @@ hs_err_pid*
*.idea*
**/.DS_store
# Fastlane (Play-Store-Upload)
android/fastlane/report.xml
android/fastlane/Preview.html
android/fastlane/play-service-account.json
android/fastlane/README.md.bak
# lokale Screenshots außerhalb der Fastlane-Metadaten
/screenshots/
# Fastlane-Bild-PNGs sind generierte Kopien; kanonisch/versioniert liegen die
# Screenshots unter materials/screenshots/. Nur die leeren Ordner (.gitkeep)
# bleiben eingecheckt, damit supply die Struktur vorfindet.
android/fastlane/metadata/android/**/images/**/*.png
+77
View File
@@ -0,0 +1,77 @@
# MarianumMobile Client
Flutter-App für die Schul-Community: Webuntis-Stundenplan, Nextcloud Talk + Files, Custom MHSL-Backend (Breaker, Custom Events, Push).
## Stack
- **Flutter** (Dart >= 3.8)
- **State:** `flutter_bloc` + `hydrated_bloc` (persistente BLoCs pro Modul)
- **Navigation:** `persistent_bottom_nav_bar_v2` mit zentraler `AppRoutes`-Klasse als Single Entry Point
- **HTTP:** `dio`, lokales Caching via `localstore` (Generic `RequestCache<T>`)
- **Calendar:** `syncfusion_flutter_calendar`
- **Datum/Zeit:** `jiffy` wird **nur** über die Extensions in `lib/extensions/date_time.dart` verwendet
- **Code-Gen:** `freezed`, `json_serializable`
## Ordnerstruktur
```
lib/
├── api/ HTTP-Layer pro Backend (mhsl/, marianumcloud/, webuntis/, holidays/)
├── state/app/modules/ BLoC pro Feature-Modul (timetable, chat, chat_list, files, ...)
├── state/app/infrastructure LoadableState<T>, DataLoader, geteilte BLoC-Bausteine
├── view/ Screens
│ ├── login/ Login-Flow
│ └── pages/ ein Verzeichnis pro Modul (timetable, files, talk, ...)
├── widget/ Geteilte UI-Komponenten (Dialoge, Buttons, Sheets)
├── extensions/ DateTime-, Text-, TimeOfDay-Extensions
├── routing/ AppRoutes (Single Navigation Entry)
├── theming/ Light/Dark Theme
├── storage/ Freezed Settings-Modelle (HydratedBloc-persistent)
├── notification/ Firebase + flutter_local_notifications
└── utils/ Helper (clipboard_helper, debouncer, download_manager, ...)
```
## Konventionen
**Navigation:** Ausschließlich über `AppRoutes.openX(context, ...)`. Direkte `Navigator.push(...)` für volle Pages sind nicht erlaubt `Navigator.pop` für Sheets/Dialogs bleibt am Call-Site.
**Dialoge:**
- Info/Fehler: `InfoDialog.show(context, body, copyable: true, title: '...')` aus `lib/widget/info_dialog.dart`.
- Bestätigung: `ConfirmDialog(...).asDialog(context)` aus `lib/widget/confirm_dialog.dart`. Async-Bestätigung nutzt `onConfirmAsync` (zeigt Spinner und Inline-Fehler über `AsyncDialogAction`).
- **Kein** inline `AlertDialog`/`SimpleDialog` mehr.
**Bottom-Sheets:** Detail-Sheets gehen über `showDetailsBottomSheet(context, header: ..., children: (ctx) => [...])` aus `lib/widget/details_bottom_sheet.dart`. Header ist optional.
**Async-Actions:** Statt manuelles Spinner+Try/Catch die `AsyncActionButton`-Familie aus `lib/widget/async_action_button.dart` (`AsyncActionButton`, `AsyncTextButton`, `AsyncIconButton`, `AsyncFab`, `AsyncListTile`, `AsyncDialogAction`, `runWithErrorDialog`). Fehler-Mapping läuft über `errorBuilder` oder zentral über `errorToUserMessage` aus `lib/api/errors/error_mapper.dart`.
**Clipboard:** Über `copyToClipboard(context, text)` aus `lib/utils/clipboard_helper.dart`. Zeigt automatisch SnackBar.
**Datum/Zeit-Formatierung:** Über die Extensions in `lib/extensions/date_time.dart`:
`dt.formatHm()`, `dt.formatDate()`, `dt.formatDateTime()`, `dt.formatDateShort()`, `dt.formatRelative()`, `start.timeRangeTo(end)`. **Kein** direktes `Jiffy.parseFromDateTime(...).format(pattern: '...')` im View-Code.
**Settings:** Pro Feature ein Freezed-Modell unter `lib/storage/`, persistiert via HydratedBloc.
## Build / Run
```bash
flutter pub get
dart run build_runner build --delete-conflicting-outputs # nach Änderungen an Freezed/JSON-Modellen
flutter run # Debug auf angeschlossenem Device
flutter analyze # statische Analyse, muss 0 Issues melden
flutter test # Tests (siehe test/)
```
## Backend-Integrationen
| Backend | Pfad | Zweck |
|---------------------------|-----------------------|----------------------------------------|
| Webuntis | `lib/api/webuntis/` | Stundenplan, Klassen, Räume, Lehrer |
| Nextcloud (Talk + WebDAV) | `lib/api/marianumcloud/` | Chats, Datei-Verwaltung |
| Custom MHSL-Server | `lib/api/mhsl/` | Breaker, Custom Events, Notify, Noten |
| Holiday-Calendar | `lib/api/holidays/` | Ferien |
`nextcloud`-Paket ist auf einen Custom-Fork gepinnt (siehe `pubspec.yaml` `dependency_overrides`).
## Tests
`test/` deckt aktuell nur Kern-Funktionen ab (DateTime-Extensions, AsyncActionController, LessonResolver). Beim Hinzufügen neuer pure-function-Helper bitte Test mit dazu.
+83 -28
View File
@@ -1,33 +1,88 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
# Static analysis configuration for the Flutter project.
# https://dart.dev/guides/language/analysis-options
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# Base ruleset: flutter_lints (recommended Flutter defaults).
# Additional lints below catch real bugs and enforce consistent style.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at
# https://dart-lang.github.io/linter/lints/index.html.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
file_names: false
prefer_relative_imports: true
unnecessary_lambdas: true
prefer_single_quotes: true
analyzer:
language:
strict-casts: true
strict-raw-types: true
errors:
invalid_annotation_target: ignore
todo: ignore
exclude:
- "**/*.g.dart"
- "**/*.freezed.dart"
- "lib/firebase_options.dart"
- "build/**"
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
linter:
rules:
# === Project conventions ===
prefer_relative_imports: true
prefer_single_quotes: true
eol_at_end_of_file: true
omit_local_variable_types: true
avoid_multiple_declarations_per_line: true
# === Bug catchers ===
always_declare_return_types: true
avoid_empty_else: true
avoid_slow_async_io: true
avoid_type_to_string: true
avoid_void_async: true
await_only_futures: true
cancel_subscriptions: true
cast_nullable_to_non_nullable: true
close_sinks: true
empty_catches: true
hash_and_equals: true
no_adjacent_strings_in_list: true
no_duplicate_case_values: true
test_types_in_equals: true
throw_in_finally: true
unawaited_futures: true
unnecessary_statements: true
unrelated_type_equality_checks: true
use_build_context_synchronously: true
valid_regexps: true
# === Flutter widget hygiene ===
avoid_unnecessary_containers: true
sized_box_for_whitespace: true
sort_child_properties_last: true
use_colored_box: true
use_decorated_box: true
use_full_hex_values_for_flutter_colors: true
use_key_in_widget_constructors: true
# === Code clarity ===
directives_ordering: true
library_prefixes: true
no_leading_underscores_for_local_identifiers: true
prefer_conditional_assignment: true
prefer_if_elements_to_conditional_expressions: true
prefer_if_null_operators: true
prefer_initializing_formals: true
prefer_interpolation_to_compose_strings: true
prefer_is_empty: true
prefer_is_not_empty: true
prefer_is_not_operator: true
prefer_iterable_whereType: true
prefer_null_aware_operators: true
prefer_spread_collections: true
prefer_void_to_null: true
unnecessary_await_in_return: true
unnecessary_brace_in_string_interps: true
unnecessary_lambdas: true
unnecessary_null_aware_assignments: true
unnecessary_null_checks: true
unnecessary_parenthesis: true
unnecessary_string_interpolations: true
use_super_parameters: true
# === File naming ===
file_names: true
+8 -9
View File
@@ -25,15 +25,16 @@ if (flutterVersionName == null) {
android {
namespace "eu.mhsl.marianum.mobile.client"
compileSdk flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
ndkVersion "28.2.13676358"
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
coreLibraryDesugaringEnabled true
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = '17'
}
sourceSets {
@@ -41,11 +42,8 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "eu.mhsl.marianum.mobile.client"
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 19
minSdkVersion 26
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
@@ -66,5 +64,6 @@ flutter {
}
dependencies {
implementation 'com.android.support:multidex:1.0.3'
implementation 'com.android.support:multidex:2.0.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
}
View File
@@ -4,4 +4,10 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
<!-- Allow cleartext HTTP in debug builds so developers can point the
Marianum-Connect custom endpoint at a local backend (e.g.
http://10.0.2.2:8080 from the Android emulator). Release builds
keep the default cleartext block. -->
<application android:usesCleartextTraffic="true" />
</manifest>
+116 -7
View File
@@ -1,8 +1,11 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:label="Marianum Fulda"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
android:icon="@mipmap/ic_launcher"
android:fullBackupContent="@xml/backup_rules"
android:dataExtractionRules="@xml/data_extraction_rules">
<activity
android:name=".MainActivity"
android:exported="true"
@@ -23,23 +26,129 @@
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
<data android:mimeType="application/*" />
<data android:mimeType="text/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
<data android:mimeType="video/*" />
<data android:mimeType="application/*" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility?hl=en and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<!-- Fallback small icon + accent for anything FCM ever renders itself
(e.g. plain notification messages). Launcher mipmaps are unusable
as small icons: Android only draws their alpha silhouette, which
shows up as a solid circle in the status bar. -->
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_stat_notification" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/notification_accent" />
<!-- flutter_local_notifications liefert Notification-Action-Taps (Talk:
Antworten / Gelesen) als expliziten Broadcast an diesen Receiver.
Das Plugin deklariert ihn NICHT selbst — ohne diesen Eintrag kommt
der Tap nirgends an und der Background-Handler startet nie. -->
<receiver
android:name="com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver"
android:exported="false" />
<!-- background_downloader runs large/long downloads in a foreground
service (WorkManager) so they survive the app being backgrounded and
beat the default 9-minute WorkManager timeout. On API 34+ the
dataSync foreground service type must be declared explicitly here;
see the FOREGROUND_SERVICE_DATA_SYNC permission below. -->
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
tools:node="merge" />
<!-- Receiver classes live at the package root (NOT under .widgets) because
the home_widget Flutter plugin resolves them as <app-package>.<name>. -->
<receiver
android:name="eu.mhsl.marianum.mobile.client.TimetableDayWidget"
android:label="@string/widget_day_label"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/timetable_day_widget_info" />
</receiver>
<receiver
android:name="eu.mhsl.marianum.mobile.client.TimetableWeekWidget"
android:label="@string/widget_week_label"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/timetable_week_widget_info" />
</receiver>
</application>
<!-- Required so url_launcher / can_launch can actually see browsers,
mail clients and dialers under Android 11+ package-visibility rules
(otherwise UrlLauncher logs "component name for ... is null" and
link taps in Talk silently do nothing). The PROCESS_TEXT intent is
needed by io.flutter.plugin.text.ProcessTextPlugin (selection
menu).
See https://developer.android.com/training/package-visibility -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="https"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="http"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="mailto"/>
</intent>
<intent>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="tel"/>
</intent>
</queries>
<uses-permission android:name="android.permission.INTERNET"/>
<!-- Android 13+ runtime notification permission. Requested at login via
FirebaseMessaging.requestPermission(); without this declaration the
locally rendered push notifications are silently dropped. -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- background_downloader foreground service for large/long downloads that
must keep running while the app is backgrounded. DATA_SYNC is the
required type declaration on Android 14+ (API 34+). -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
<!-- Workmanager periodic widget refresh needs to reschedule after device
reboot, otherwise the widget freezes until the user opens the app. -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<!-- Aus open_filex gemergt: die App öffnet nur selbst heruntergeladene Dateien
über dessen FileProvider (content://-URIs) und braucht keinen Zugriff auf
geteilte Medien. Entfernt wegen Google-Play "Photo and Video Permissions". -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:node="remove"/>
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" tools:node="remove"/>
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" tools:node="remove"/>
</manifest>
@@ -1,5 +1,42 @@
package eu.mhsl.marianum.mobile.client
import android.content.Intent
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterActivity()
class MainActivity : FlutterActivity() {
private val widgetChannel = "eu.mhsl.marianum.widget"
/// Last seen widget tap target. Cleared by Dart via `consumePendingNavigation`
/// so the same intent isn't replayed on every resume.
private var pendingTimetableTap: Boolean = false
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
widgetChannel
).setMethodCallHandler { call, result ->
when (call.method) {
"consumePendingNavigation" -> {
val pending = pendingTimetableTap
pendingTimetableTap = false
result.success(pending)
}
else -> result.notImplemented()
}
}
consumeIntentData(intent)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
consumeIntentData(intent)
}
private fun consumeIntentData(intent: Intent?) {
if (intent?.getBooleanExtra("widget_open_timetable", false) == true) {
pendingTimetableTap = true
}
}
}
@@ -0,0 +1,37 @@
package eu.mhsl.marianum.mobile.client
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.os.Bundle
import eu.mhsl.marianum.mobile.client.widgets.WidgetRenderer
/**
* Lives at the package root (not under `widgets/`) because the home_widget
* Flutter plugin resolves the receiver class as `<app-package>.<androidName>`.
*/
class TimetableDayWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
) {
for (id in appWidgetIds) {
val options = appWidgetManager.getAppWidgetOptions(id)
val views = WidgetRenderer.buildDay(context, context.packageName, options)
appWidgetManager.updateAppWidget(id, views)
}
}
override fun onAppWidgetOptionsChanged(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
newOptions: Bundle,
) {
// Re-render on resize, otherwise the tiles stay at install-time size
// and either clip or leave dead space.
val views = WidgetRenderer.buildDay(context, context.packageName, newOptions)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
@@ -0,0 +1,31 @@
package eu.mhsl.marianum.mobile.client
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.os.Bundle
import eu.mhsl.marianum.mobile.client.widgets.WidgetRenderer
class TimetableWeekWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
) {
for (id in appWidgetIds) {
val options = appWidgetManager.getAppWidgetOptions(id)
val views = WidgetRenderer.buildWeek(context, context.packageName, options)
appWidgetManager.updateAppWidget(id, views)
}
}
override fun onAppWidgetOptionsChanged(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
newOptions: Bundle,
) {
val views = WidgetRenderer.buildWeek(context, context.packageName, newOptions)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
@@ -0,0 +1,157 @@
package eu.mhsl.marianum.mobile.client.widgets
import org.json.JSONException
import org.json.JSONObject
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import java.util.TimeZone
// Mirror of lib/widget_data/widget_data.dart — JSON keys + enum names
// must stay in sync.
enum class WidgetLessonStatus {
REGULAR, ONGOING, PAST, CANCELLED, IRREGULAR, TEACHER_CHANGED, EVENT;
companion object {
fun fromWire(raw: String?): WidgetLessonStatus = when (raw) {
"regular" -> REGULAR
"ongoing" -> ONGOING
"past" -> PAST
"cancelled" -> CANCELLED
"irregular" -> IRREGULAR
"teacherChanged" -> TEACHER_CHANGED
"event" -> EVENT
else -> REGULAR
}
}
}
data class WidgetLesson(
val start: Date,
val end: Date,
val subjectShort: String,
val subjectLong: String?,
val room: String?,
val teacher: String?,
val originalTeacher: String?,
val status: WidgetLessonStatus,
val customColor: String?,
val siblingCount: Int,
)
data class WidgetPeriod(
val name: String,
val startMinutes: Int,
val endMinutes: Int,
val virtualStartMinutes: Int,
val virtualEndMinutes: Int,
)
data class WidgetTimetableData(
val fetchedAt: Date,
val anchorDate: Date,
val lessons: List<WidgetLesson>,
val periods: List<WidgetPeriod>,
val isHoliday: Boolean,
val holidayName: String?,
)
object WidgetDataParser {
private val isoFormat: SimpleDateFormat
get() = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.ROOT).apply {
timeZone = TimeZone.getDefault()
}
/// Dart's toIso8601String() ships microseconds (6 digits) when non-zero;
/// SimpleDateFormat only parses 3 → strip the extra digits. Local time
/// without Z is the default, so removeSuffix("Z") makes the parser
/// tolerate both shapes.
private fun parseDate(raw: String?): Date? {
if (raw.isNullOrEmpty()) return null
val cleaned = raw
.replace(Regex("([.,]\\d{3})\\d+"), "$1")
.removeSuffix("Z")
return try {
isoFormat.parse(cleaned)
} catch (_: Exception) {
null
}
}
fun parse(json: String?): WidgetTimetableData? {
if (json.isNullOrEmpty()) return null
return try {
val root = JSONObject(json)
val lessonsArray = root.optJSONArray("lessons")
val lessons = mutableListOf<WidgetLesson>()
if (lessonsArray != null) {
for (i in 0 until lessonsArray.length()) {
val obj = lessonsArray.optJSONObject(i) ?: continue
val start = parseDate(obj.stringOrNull("start")) ?: continue
val end = parseDate(obj.stringOrNull("end")) ?: continue
lessons += WidgetLesson(
start = start,
end = end,
subjectShort = obj.stringOrNull("subjectShort") ?: "",
subjectLong = obj.stringOrNull("subjectLong"),
room = obj.stringOrNull("room"),
teacher = obj.stringOrNull("teacher"),
originalTeacher = obj.stringOrNull("originalTeacher"),
status = WidgetLessonStatus.fromWire(obj.stringOrNull("status")),
customColor = obj.stringOrNull("customColor"),
siblingCount = obj.optInt("siblingCount", 0),
)
}
}
val periodsArray = root.optJSONArray("periods")
val periods = mutableListOf<WidgetPeriod>()
if (periodsArray != null) {
for (i in 0 until periodsArray.length()) {
val obj = periodsArray.optJSONObject(i) ?: continue
periods += WidgetPeriod(
name = obj.stringOrNull("name") ?: "",
startMinutes = obj.optInt("startMinutes", 0),
endMinutes = obj.optInt("endMinutes", 0),
virtualStartMinutes = obj.optInt("virtualStartMinutes", 0),
virtualEndMinutes = obj.optInt("virtualEndMinutes", 0),
)
}
}
WidgetTimetableData(
fetchedAt = parseDate(root.stringOrNull("fetchedAt")) ?: Date(),
anchorDate = parseDate(root.stringOrNull("anchorDate")) ?: Date(),
lessons = lessons,
periods = periods,
isHoliday = root.optBoolean("isHoliday", false),
holidayName = root.stringOrNull("holidayName"),
)
} catch (_: JSONException) {
null
}
}
private fun JSONObject.stringOrNull(key: String): String? {
if (!has(key) || isNull(key)) return null
val raw = optString(key, "")
return if (raw.isEmpty()) null else raw
}
}
object WidgetDateUtils {
fun startOfDay(date: Date): Date {
val cal = Calendar.getInstance().apply { time = date }
cal.set(Calendar.HOUR_OF_DAY, 0)
cal.set(Calendar.MINUTE, 0)
cal.set(Calendar.SECOND, 0)
cal.set(Calendar.MILLISECOND, 0)
return cal.time
}
fun isSameDay(a: Date, b: Date): Boolean {
val ca = Calendar.getInstance().apply { time = a }
val cb = Calendar.getInstance().apply { time = b }
return ca.get(Calendar.YEAR) == cb.get(Calendar.YEAR) &&
ca.get(Calendar.DAY_OF_YEAR) == cb.get(Calendar.DAY_OF_YEAR)
}
}
@@ -0,0 +1,743 @@
package eu.mhsl.marianum.mobile.client.widgets
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Configuration
import android.os.Bundle
import android.util.TypedValue
import android.view.View
import android.widget.RemoteViews
import eu.mhsl.marianum.mobile.client.MainActivity
import eu.mhsl.marianum.mobile.client.R
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.Locale
import kotlin.math.max
/**
* Renders the day and week widgets as a time-grid: lesson blocks absolutely
* positioned on a vertical axis, mirroring the in-app Syncfusion calendar.
* Per-hour dp is computed from the widget bundle so the grid scales with
* resize, clamped to [MIN_HOUR_HEIGHT_DP, MAX_HOUR_HEIGHT_DP].
*/
object WidgetRenderer {
private const val FALLBACK_VIRTUAL_MINUTES = 11 * 60
private const val DAY_CHROME_DP = 40
private const val WEEK_CHROME_DP = 64
private const val MIN_HOUR_HEIGHT_DP = 18
private const val MAX_HOUR_HEIGHT_DP = 72
private const val MIN_BLOCK_HEIGHT_DP = 16
private const val LESSON_GAP_DP = 1.5f
// Below SHOW_ROOM_MIN: subject only. Below SHOW_TEACHER_SEPARATE_MIN:
// subject + room. Above: subject + room + teacher stacked.
private const val BLOCK_SHOW_ROOM_MIN_DP = 18
private const val BLOCK_SHOW_TEACHER_SEPARATE_MIN_DP = 30
/// Below this column width autoSize can't fit subject + room — drop
/// room/teacher entirely on the week-widget.
private const val WEEK_COLUMN_TIGHT_DP = 45
private val timeFormat = SimpleDateFormat("HH:mm", Locale.GERMAN)
private val dateShort = SimpleDateFormat("dd.MM.", Locale.GERMAN)
private val weekdayShort = SimpleDateFormat("EE", Locale.GERMAN)
private val dateTimeShort = SimpleDateFormat("dd.MM. HH:mm", Locale.GERMAN)
/// Hex values mirror LightAppTheme / DarkAppTheme tokens so the widget
/// matches the app's branding rather than the generic system look.
private data class WidgetPalette(
val background: Int,
val textPrimary: Int,
val textSecondary: Int,
val divider: Int,
val breakBlock: Int,
val watermarkAlpha: Float,
)
private val lightPalette = WidgetPalette(
background = 0xFFFCF7F5.toInt(),
textPrimary = 0xFF1A1A1A.toInt(),
textSecondary = 0xFF555555.toInt(),
divider = 0x22000000,
breakBlock = 0x0C000000,
watermarkAlpha = 0.014f,
)
private val darkPalette = WidgetPalette(
background = 0xFF1F1716.toInt(),
textPrimary = 0xFFF1F1F1.toInt(),
textSecondary = 0xFFB0B0B0.toInt(),
divider = 0x33FFFFFF,
breakBlock = 0x14FFFFFF,
watermarkAlpha = 0.025f,
)
private fun resolvePalette(context: Context, themeMode: String?): WidgetPalette {
val isDark = when (themeMode) {
"light" -> false
"dark" -> true
else -> {
val uiMode = context.resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK
uiMode == Configuration.UI_MODE_NIGHT_YES
}
}
return if (isDark) darkPalette else lightPalette
}
fun buildDay(
context: Context,
packageName: String,
options: Bundle? = null,
): RemoteViews {
val prefs = sharedPrefs(context)
val palette = resolvePalette(context, prefs.getString(KEY_THEME_MODE, "system"))
if (!prefs.getBoolean(KEY_LOGGED_IN, false)) {
return buildPlaceholder(
context,
packageName,
context.getString(R.string.widget_login_required),
palette,
)
}
val data = WidgetDataParser.parse(prefs.getString(KEY_DAY_DATA, null))
?: return buildPlaceholder(
context,
packageName,
context.getString(R.string.widget_loading),
palette,
)
val totalVirtualMin = data.periods.lastOrNull()?.virtualEndMinutes
?: FALLBACK_VIRTUAL_MINUTES
val hourHeightDp = resolveHourHeight(options, DAY_CHROME_DP, totalVirtualMin)
val views = RemoteViews(packageName, R.layout.widget_day)
applyChrome(views, palette, options)
views.setTextColor(R.id.widget_day_title, palette.textPrimary)
views.setTextColor(R.id.widget_day_subtitle, palette.textSecondary)
views.setTextColor(R.id.widget_day_empty, palette.textSecondary)
views.setTextViewText(
R.id.widget_day_title,
"${dayLabel(context, data.anchorDate)} · ${dateShort.format(data.anchorDate)}",
)
views.setTextViewText(
R.id.widget_day_subtitle,
context.getString(R.string.widget_status_label, freshnessLabel(context, data.fetchedAt)),
)
views.removeAllViews(R.id.widget_day_time_labels)
views.removeAllViews(R.id.widget_day_grid)
if (data.isHoliday) {
views.setViewVisibility(R.id.widget_day_empty, View.VISIBLE)
views.setTextViewText(
R.id.widget_day_empty,
data.holidayName ?: context.getString(R.string.widget_holiday),
)
} else if (data.lessons.isEmpty()) {
views.setViewVisibility(R.id.widget_day_empty, View.VISIBLE)
views.setTextViewText(
R.id.widget_day_empty,
context.getString(R.string.widget_no_lessons),
)
} else {
views.setViewVisibility(R.id.widget_day_empty, View.GONE)
populateGridLines(packageName, views, R.id.widget_day_time_labels, hourHeightDp, palette, data.periods)
populateTimeLabels(packageName, views, R.id.widget_day_time_labels, hourHeightDp, palette, data.periods)
populateGridLines(packageName, views, R.id.widget_day_grid, hourHeightDp, palette, data.periods)
populateBreakBlocks(packageName, views, R.id.widget_day_grid, hourHeightDp, palette, data.periods)
for (lesson in data.lessons) {
addLessonBlock(
context = context,
packageName = packageName,
parent = views,
containerId = R.id.widget_day_grid,
lesson = lesson,
hourHeightDp = hourHeightDp,
periods = data.periods,
subjectOnly = false,
horizontalPaddingDp = 7,
)
}
}
views.setOnClickPendingIntent(R.id.widget_root, openAppIntent(context))
return views
}
fun buildWeek(
context: Context,
packageName: String,
options: Bundle? = null,
): RemoteViews {
val prefs = sharedPrefs(context)
val palette = resolvePalette(context, prefs.getString(KEY_THEME_MODE, "system"))
if (!prefs.getBoolean(KEY_LOGGED_IN, false)) {
return buildPlaceholder(
context,
packageName,
context.getString(R.string.widget_login_required),
palette,
)
}
val data = WidgetDataParser.parse(prefs.getString(KEY_WEEK_DATA, null))
?: return buildPlaceholder(
context,
packageName,
context.getString(R.string.widget_loading),
palette,
)
val totalVirtualMin = data.periods.lastOrNull()?.virtualEndMinutes
?: FALLBACK_VIRTUAL_MINUTES
val hourHeightDp = resolveHourHeight(options, WEEK_CHROME_DP, totalVirtualMin)
val views = RemoteViews(packageName, R.layout.widget_week)
applyChrome(views, palette, options)
views.setTextColor(R.id.widget_week_title, palette.textPrimary)
views.setTextColor(R.id.widget_week_subtitle, palette.textSecondary)
val cal = Calendar.getInstance().apply { time = data.anchorDate }
val weekNumber = cal.get(Calendar.WEEK_OF_YEAR)
val end = Calendar.getInstance().apply {
time = data.anchorDate
add(Calendar.DAY_OF_YEAR, 4)
}.time
val kwPrefix = context.getString(R.string.widget_calendar_week_prefix)
views.setTextViewText(
R.id.widget_week_title,
"$kwPrefix $weekNumber · ${dateShort.format(data.anchorDate)}${dateShort.format(end)}",
)
views.setTextViewText(
R.id.widget_week_subtitle,
context.getString(R.string.widget_status_label, freshnessLabel(context, data.fetchedAt)),
)
val headerIds = listOf(
R.id.widget_week_header_mon,
R.id.widget_week_header_tue,
R.id.widget_week_header_wed,
R.id.widget_week_header_thu,
R.id.widget_week_header_fri,
)
val columnIds = listOf(
R.id.widget_week_col_mon,
R.id.widget_week_col_tue,
R.id.widget_week_col_wed,
R.id.widget_week_col_thu,
R.id.widget_week_col_fri,
)
views.removeAllViews(R.id.widget_week_time_labels)
populateGridLines(packageName, views, R.id.widget_week_time_labels, hourHeightDp, palette, data.periods)
populateTimeLabels(packageName, views, R.id.widget_week_time_labels, hourHeightDp, palette, data.periods)
val (weekWidthDp, _) = widgetSizeDp(options)
// Time-label column is 28dp wide; the rest is split across 5 days
// plus thin dividers (negligible). Drop room/teacher only on the
// very narrowest week widgets — autoSize handles the in-between
// sizes.
val dayColumnWidthDp = (weekWidthDp - 28f - 20f) / 5f
val subjectOnly = dayColumnWidthDp < WEEK_COLUMN_TIGHT_DP
for ((index, columnId) in columnIds.withIndex()) {
views.removeAllViews(headerIds[index])
views.removeAllViews(columnId)
val day = Calendar.getInstance().apply {
time = data.anchorDate
add(Calendar.DAY_OF_YEAR, index)
}.time
val header = RemoteViews(packageName, R.layout.widget_week_day_header)
header.setTextColor(R.id.widget_week_day_header_weekday, palette.textPrimary)
header.setTextColor(R.id.widget_week_day_header_date, palette.textSecondary)
header.setTextViewText(R.id.widget_week_day_header_weekday, weekdayShort.format(day))
header.setTextViewText(R.id.widget_week_day_header_date, dateShort.format(day))
views.addView(headerIds[index], header)
populateGridLines(packageName, views, columnId, hourHeightDp, palette, data.periods)
populateBreakBlocks(packageName, views, columnId, hourHeightDp, palette, data.periods)
for (lesson in data.lessons.filter { WidgetDateUtils.isSameDay(it.start, day) }) {
addLessonBlock(
context = context,
packageName = packageName,
parent = views,
containerId = columnId,
lesson = lesson,
hourHeightDp = hourHeightDp,
periods = data.periods,
subjectOnly = subjectOnly,
horizontalPaddingDp = 3,
)
}
}
views.setOnClickPendingIntent(R.id.widget_root, openAppIntent(context))
return views
}
/// Pulls the launcher-reported widget size out of the AppWidget options
/// bundle. The grid now spans `totalVirtualMin` minutes (lessons +
/// preserved big breaks), so we divide by that instead of a fixed hour
/// count to keep tiles readable across different timetables.
private fun resolveHourHeight(
options: Bundle?,
chromeDp: Int,
totalVirtualMin: Int,
): Float {
val virtualHours = (totalVirtualMin / 60.0f).coerceAtLeast(1f)
val rawHeightDp = options?.let {
max(
it.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, 0),
it.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, 0),
)
} ?: 0
if (rawHeightDp <= 0) return 32f
val gridHeightDp = (rawHeightDp - chromeDp)
.coerceAtLeast((MIN_HOUR_HEIGHT_DP * virtualHours).toInt())
return (gridHeightDp.toFloat() / virtualHours)
.coerceIn(MIN_HOUR_HEIGHT_DP.toFloat(), MAX_HOUR_HEIGHT_DP.toFloat())
}
/// Real wall-clock minute → position on the virtual axis. Inside a
/// period: linear. In a gap: linear across the virtual gap (zero for
/// squeezed small breaks, real width for big breaks).
private fun realMinutesToVirtual(
realMin: Int,
periods: List<WidgetPeriod>,
): Float {
if (periods.isEmpty()) return realMin.toFloat()
for (period in periods) {
if (realMin in period.startMinutes..period.endMinutes) {
return period.virtualStartMinutes + (realMin - period.startMinutes).toFloat()
}
}
val first = periods.first()
if (realMin < first.startMinutes) {
return (realMin - first.startMinutes + first.virtualStartMinutes).toFloat()
}
val last = periods.last()
if (realMin > last.endMinutes) {
return last.virtualEndMinutes + (realMin - last.endMinutes).toFloat()
}
var prev = first
for (i in 1 until periods.size) {
val curr = periods[i]
if (realMin in (prev.endMinutes + 1) until curr.startMinutes) {
val gap = curr.startMinutes - prev.endMinutes
val virtualGap = curr.virtualStartMinutes - prev.virtualEndMinutes
return if (gap > 0) {
prev.virtualEndMinutes +
(realMin - prev.endMinutes).toFloat() * virtualGap / gap
} else {
curr.virtualStartMinutes.toFloat()
}
}
prev = curr
}
return 0f
}
/// Below this per-hour height the two-line label collapses to a single
/// period number — time + number overlap otherwise.
private const val TIME_LABEL_COMPACT_THRESHOLD_DP = 26f
private fun populateTimeLabels(
packageName: String,
parent: RemoteViews,
containerId: Int,
hourHeightDp: Float,
palette: WidgetPalette,
periods: List<WidgetPeriod>,
) {
val compact = hourHeightDp < TIME_LABEL_COMPACT_THRESHOLD_DP
for (period in periods) {
val label = RemoteViews(packageName, R.layout.widget_time_label)
label.setTextViewText(R.id.widget_time_label_number, "${period.name}.")
label.setTextViewText(R.id.widget_time_label_time, formatHm(period.startMinutes))
if (compact) {
label.setViewVisibility(R.id.widget_time_label_time, View.GONE)
label.setTextViewTextSize(
R.id.widget_time_label_number,
TypedValue.COMPLEX_UNIT_SP,
9f,
)
label.setTextColor(R.id.widget_time_label_number, palette.textPrimary)
} else {
label.setViewVisibility(R.id.widget_time_label_time, View.VISIBLE)
label.setTextViewTextSize(
R.id.widget_time_label_number,
TypedValue.COMPLEX_UNIT_SP,
7f,
)
label.setTextColor(R.id.widget_time_label_number, palette.textSecondary)
}
label.setTextColor(R.id.widget_time_label_time, palette.textPrimary)
val topDp = period.virtualStartMinutes * hourHeightDp / 60.0f
label.setViewLayoutMargin(
R.id.widget_time_label_root,
RemoteViews.MARGIN_TOP,
topDp,
TypedValue.COMPLEX_UNIT_DIP,
)
parent.addView(containerId, label)
}
}
private fun populateGridLines(
packageName: String,
parent: RemoteViews,
containerId: Int,
hourHeightDp: Float,
palette: WidgetPalette,
periods: List<WidgetPeriod>,
) {
// Lines at every period start + end, deduped by virtual minute so
// adjacent periods share a line and big-break boundaries get both
// upper and lower edges.
val drawn = mutableSetOf<Int>()
for (period in periods) {
for (virtualMin in listOf(period.virtualStartMinutes, period.virtualEndMinutes)) {
if (!drawn.add(virtualMin)) continue
val line = RemoteViews(packageName, R.layout.widget_grid_line)
line.setInt(R.id.widget_grid_line_root, "setBackgroundColor", palette.divider)
val topDp = virtualMin * hourHeightDp / 60.0f
line.setViewLayoutMargin(
R.id.widget_grid_line_root,
RemoteViews.MARGIN_TOP,
topDp,
TypedValue.COMPLEX_UNIT_DIP,
)
parent.addView(containerId, line)
}
}
}
/// Faint translucent block in any virtual gap between two periods —
/// only big breaks (Hofpause, Mittagspause) survive the mapper's
/// small-break collapse.
private fun populateBreakBlocks(
packageName: String,
parent: RemoteViews,
containerId: Int,
hourHeightDp: Float,
palette: WidgetPalette,
periods: List<WidgetPeriod>,
) {
for (i in 0 until periods.size - 1) {
val curr = periods[i]
val next = periods[i + 1]
val virtualGap = next.virtualStartMinutes - curr.virtualEndMinutes
if (virtualGap <= 0) continue
val block = RemoteViews(packageName, R.layout.widget_break_block)
val topDp = curr.virtualEndMinutes * hourHeightDp / 60.0f
val heightDp = virtualGap * hourHeightDp / 60.0f
block.setViewLayoutMargin(
R.id.widget_break_block_root,
RemoteViews.MARGIN_TOP,
topDp,
TypedValue.COMPLEX_UNIT_DIP,
)
block.setViewLayoutHeight(
R.id.widget_break_block_root,
heightDp,
TypedValue.COMPLEX_UNIT_DIP,
)
block.setInt(
R.id.widget_break_block_root,
"setBackgroundColor",
palette.breakBlock,
)
parent.addView(containerId, block)
}
}
private fun formatHm(minutesSinceMidnight: Int): String {
val h = minutesSinceMidnight / 60
val m = minutesSinceMidnight % 60
return "%02d:%02d".format(h, m)
}
/// Overrides the chrome XML's `@color/widget_*` defaults when the user
/// pins a fixed light/dark theme, and resizes the watermark M to match
/// the current widget bounds.
private fun applyChrome(views: RemoteViews, palette: WidgetPalette, options: Bundle?) {
views.setInt(R.id.widget_root, "setBackgroundColor", palette.background)
views.setInt(R.id.widget_watermark, "setColorFilter", palette.textPrimary)
views.setFloat(R.id.widget_watermark, "setAlpha", palette.watermarkAlpha)
val (widthDp, heightDp) = widgetSizeDp(options)
// Sized to the longer edge so the M scales with widget resize.
// Negative end/bottom margin lets a sliver tuck behind the edge.
val markSize = (max(widthDp, heightDp) * 0.8f).coerceIn(160f, 400f)
val offsetEnd = -(markSize * 0.18f)
val offsetBottom = -(markSize * 0.18f)
views.setViewLayoutWidth(
R.id.widget_watermark,
markSize,
TypedValue.COMPLEX_UNIT_DIP,
)
views.setViewLayoutHeight(
R.id.widget_watermark,
markSize,
TypedValue.COMPLEX_UNIT_DIP,
)
views.setViewLayoutMargin(
R.id.widget_watermark,
RemoteViews.MARGIN_END,
offsetEnd,
TypedValue.COMPLEX_UNIT_DIP,
)
views.setViewLayoutMargin(
R.id.widget_watermark,
RemoteViews.MARGIN_BOTTOM,
offsetBottom,
TypedValue.COMPLEX_UNIT_DIP,
)
}
private fun widgetSizeDp(options: Bundle?): Pair<Int, Int> {
if (options == null) return Pair(220, 220)
val width = max(
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH, 0),
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH, 0),
).coerceAtLeast(140)
val height = max(
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT, 0),
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT, 0),
).coerceAtLeast(140)
return Pair(width, height)
}
private fun addLessonBlock(
context: Context,
packageName: String,
parent: RemoteViews,
containerId: Int,
lesson: WidgetLesson,
hourHeightDp: Float,
periods: List<WidgetPeriod>,
subjectOnly: Boolean,
horizontalPaddingDp: Int,
) {
val cal = Calendar.getInstance()
cal.time = lesson.start
val startMinutes = cal.get(Calendar.HOUR_OF_DAY) * 60 + cal.get(Calendar.MINUTE)
cal.time = lesson.end
val endMinutes = cal.get(Calendar.HOUR_OF_DAY) * 60 + cal.get(Calendar.MINUTE)
val durationMinutes = (endMinutes - startMinutes).coerceAtLeast(15)
val virtualStart = realMinutesToVirtual(startMinutes, periods)
val virtualEnd = realMinutesToVirtual(startMinutes + durationMinutes, periods)
if (virtualEnd <= virtualStart) return
// Half the gap above + half below so the grid line under the tile
// stays visible.
val topDp = virtualStart * hourHeightDp / 60.0f + LESSON_GAP_DP / 2f
val heightDp = ((virtualEnd - virtualStart) * hourHeightDp / 60.0f - LESSON_GAP_DP)
.coerceAtLeast(MIN_BLOCK_HEIGHT_DP.toFloat())
val block = RemoteViews(packageName, R.layout.widget_lesson_block)
block.setViewLayoutMargin(
R.id.widget_lesson_block_root,
RemoteViews.MARGIN_TOP,
topDp,
TypedValue.COMPLEX_UNIT_DIP,
)
block.setViewLayoutHeight(
R.id.widget_lesson_block_root,
heightDp,
TypedValue.COMPLEX_UNIT_DIP,
)
block.setInt(
R.id.widget_lesson_block_root,
"setBackgroundResource",
statusDrawable(lesson),
)
val density = context.resources.displayMetrics.density
val padXPx = (horizontalPaddingDp * density).toInt()
val padYPx = (3 * density).toInt()
block.setViewPadding(
R.id.widget_lesson_block_root,
padXPx, padYPx, padXPx, padYPx,
)
block.setTextViewText(R.id.widget_lesson_subject, subjectLabel(lesson))
// Separate fixed-size badge so the +N hint stays readable when
// autoSize shrinks the subject on narrow tiles.
if (lesson.siblingCount > 0) {
block.setTextViewText(
R.id.widget_lesson_sibling_badge,
"+${lesson.siblingCount}",
)
block.setViewVisibility(R.id.widget_lesson_sibling_badge, View.VISIBLE)
} else {
block.setViewVisibility(R.id.widget_lesson_sibling_badge, View.GONE)
}
val room = roomLabel(lesson)
val teacher = teacherLabel(lesson)
val noSecondaryContent = room.isNullOrEmpty() && teacher.isNullOrEmpty()
val hideSecondary = subjectOnly ||
heightDp < BLOCK_SHOW_ROOM_MIN_DP ||
noSecondaryContent
block.setViewVisibility(
R.id.widget_lesson_secondary_stack,
if (hideSecondary) View.GONE else View.VISIBLE,
)
// Custom-events have no room/teacher → let the subject wrap to 2 lines
// so long titles don't autoshrink to nothing.
block.setInt(
R.id.widget_lesson_subject,
"setMaxLines",
if (noSecondaryContent) 2 else 1,
)
when {
hideSecondary -> {
applyOptionalText(block, R.id.widget_lesson_room, null)
applyOptionalText(block, R.id.widget_lesson_teacher, null)
}
heightDp < BLOCK_SHOW_TEACHER_SEPARATE_MIN_DP -> {
applyOptionalText(block, R.id.widget_lesson_room, room)
applyOptionalText(block, R.id.widget_lesson_teacher, null)
}
else -> {
applyOptionalText(block, R.id.widget_lesson_room, room)
applyOptionalText(block, R.id.widget_lesson_teacher, teacher)
}
}
parent.addView(containerId, block)
}
private fun applyOptionalText(views: RemoteViews, viewId: Int, text: String?) {
if (text.isNullOrEmpty()) {
views.setViewVisibility(viewId, View.GONE)
} else {
views.setTextViewText(viewId, text)
views.setViewVisibility(viewId, View.VISIBLE)
}
}
/// Custom-events use the user-picked palette (orange/red/green/blue,
/// mirroring CustomTimetableColors).
private fun statusDrawable(lesson: WidgetLesson): Int {
if (lesson.status == WidgetLessonStatus.EVENT && lesson.customColor != null) {
return when (lesson.customColor) {
"orange" -> R.drawable.widget_lesson_block_event_orange
"red" -> R.drawable.widget_lesson_block_event_red
"green" -> R.drawable.widget_lesson_block_event_green
"blue" -> R.drawable.widget_lesson_block_event_blue
else -> R.drawable.widget_lesson_block_event_orange
}
}
return when (lesson.status) {
WidgetLessonStatus.CANCELLED -> R.drawable.widget_lesson_block_cancelled
WidgetLessonStatus.IRREGULAR -> R.drawable.widget_lesson_block_irregular
WidgetLessonStatus.TEACHER_CHANGED -> R.drawable.widget_lesson_block_teacher_changed
WidgetLessonStatus.PAST -> R.drawable.widget_lesson_block_past
WidgetLessonStatus.EVENT -> R.drawable.widget_lesson_block_event_orange
// ONGOING collapses into REGULAR — widgets only refresh every
// ~30min so "the current lesson" is stale most of the time and
// the visual highlight would mislead more than help.
WidgetLessonStatus.ONGOING,
WidgetLessonStatus.REGULAR -> R.drawable.widget_lesson_block_regular
}
}
private fun subjectLabel(lesson: WidgetLesson): String {
return lesson.subjectShort.ifEmpty { lesson.subjectLong ?: "" }
}
private fun roomLabel(lesson: WidgetLesson): String? = lesson.room
private fun teacherLabel(lesson: WidgetLesson): String? =
lesson.teacher ?: lesson.originalTeacher
private fun dayLabel(context: Context, anchor: Date): String {
val today = WidgetDateUtils.startOfDay(Date())
val tomorrow = Calendar.getInstance().apply {
time = today
add(Calendar.DAY_OF_YEAR, 1)
}.time
val anchorStart = WidgetDateUtils.startOfDay(anchor)
return when {
anchorStart == today -> context.getString(R.string.widget_today)
anchorStart == tomorrow -> context.getString(R.string.widget_tomorrow)
else -> SimpleDateFormat("EEEE", Locale.GERMAN).format(anchor)
}
}
private fun freshnessLabel(context: Context, fetchedAt: Date): String {
val today = WidgetDateUtils.startOfDay(Date())
val fetchedDay = WidgetDateUtils.startOfDay(fetchedAt)
val yesterday = Calendar.getInstance().apply {
time = today
add(Calendar.DAY_OF_YEAR, -1)
}.time
val yesterdayPrefix = context.getString(R.string.widget_yesterday_prefix)
return when (fetchedDay) {
today -> timeFormat.format(fetchedAt)
yesterday -> "$yesterdayPrefix ${timeFormat.format(fetchedAt)}"
else -> dateTimeShort.format(fetchedAt)
}
}
private fun buildPlaceholder(
context: Context,
packageName: String,
message: String,
palette: WidgetPalette,
): RemoteViews {
val views = RemoteViews(packageName, R.layout.widget_placeholder)
applyChrome(views, palette, null)
views.setTextColor(R.id.widget_placeholder_title, palette.textPrimary)
views.setTextColor(R.id.widget_placeholder_message, palette.textSecondary)
views.setTextViewText(
R.id.widget_placeholder_title,
context.getString(R.string.widget_placeholder_title),
)
views.setTextViewText(R.id.widget_placeholder_message, message)
views.setOnClickPendingIntent(
R.id.widget_placeholder_message,
openAppIntent(context),
)
return views
}
private fun openAppIntent(context: Context): PendingIntent {
// ACTION_MAIN + LAUNCHER mirrors a launcher tap; the boolean extra
// is consumed by Dart via WidgetNavigation to route to the timetable.
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP
action = Intent.ACTION_MAIN
addCategory(Intent.CATEGORY_LAUNCHER)
putExtra("widget_open_timetable", true)
}
return PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
)
}
private fun sharedPrefs(context: Context): SharedPreferences {
return context.getSharedPreferences(
"HomeWidgetPreferences",
Context.MODE_PRIVATE,
)
}
const val KEY_DAY_DATA = "widget_data_day_v1"
const val KEY_WEEK_DATA = "widget_data_week_v1"
const val KEY_LOGGED_IN = "widget_data_logged_in_v1"
const val KEY_THEME_MODE = "widget_setting_theme_mode_v1"
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 454 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 B

After

Width:  |  Height:  |  Size: 69 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 1021 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Plain rounded rectangle. We deliberately avoid ?attr/appWidgetRadius
because RemoteViews inflation does not resolve custom theme attributes
reliably across launchers. Hard-coded 20dp matches the system home-screen
radius closely on stock Android. -->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="20dp" />
<solid android:color="@color/widget_background" />
</shape>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 B

After

Width:  |  Height:  |  Size: 69 B

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Status-bar small icon: monochrome Marianum M, ported from
marianum_m_watermark.xml (same path + nested transforms). Android renders
only the alpha channel of small icons, so the fill is hard white and never
tinted at runtime. 24dp with the source's non-square viewport — the system
fits it into the icon bounds.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="70.000168"
android:viewportHeight="82.227348">
<group
android:scaleX="0.26458333"
android:scaleY="0.26458334"
android:translateX="107.44411"
android:translateY="-80.482198">
<group
android:translateX="-749.41293"
android:translateY="290.52252">
<group
android:scaleX="0.13333333"
android:scaleY="-0.13333333"
android:translateX="0"
android:translateY="632.14667">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M 3499.67,4594.33 c -16.43,-106.19 -29.71,-199.97 -43.79,-293.49 86.83,-19 138.5,-27.61 223.38,-43.82 63.81,-12.18 175.24,-20.4 179.64,-83.23 6.46,-92.69 -124.69,-55.41 -188.38,-43.81 -84.33,15.36 -159.13,28.84 -232.2,43.81 -13.68,-60.72 -26.83,-118.68 -39.43,-179.61 -36.76,-178.32 -73.67,-368.16 -105.11,-551.97 18.09,25.66 30.84,42.72 43.8,65.7 66.7,118.26 140.39,245.04 227.76,354.83 33.49,42.05 76.86,94.81 118.31,113.91 98.42,45.36 166.68,-22.2 170.87,-118.28 3.68,-85.28 -23.09,-181.17 -35.08,-275.99 -12.4,-98.19 -22.89,-194.93 -35.03,-275.98 72.44,102.69 147.93,269.64 240.95,381.12 27.51,33 73.55,80.61 118.27,87.62 218.76,34.33 126.58,-312.17 127.05,-473.13 0.4,-144.9 44.01,-255.37 175.21,-271.59 43.02,-5.31 105.84,11.16 112.7,-26.34 8.67,-47.38 -78.15,-60.52 -125.84,-61.28 -291.34,-4.51 -322.06,262.33 -311.01,573.88 -19.85,-18.57 -35.71,-47.53 -52.57,-74.47 -97.59,-155.88 -203.95,-327.22 -297.92,-503.79 -25.93,-48.79 -53.68,-114.7 -135.8,-83.23 -17.27,6.63 -48.25,44.39 -52.56,56.96 -19.58,57.19 1.55,137.42 8.76,205.89 21.54,203.72 57.81,389.09 78.87,587.01 -26.3,0.51 -43.93,-30.07 -56.96,-48.2 -46.9,-65.27 -86.02,-140.76 -127.04,-214.64 -52.84,-95.15 -108.23,-192.84 -157.71,-293.52 -75.25,-153.09 -188.6,-501.89 -242.12,-678.81 -8.67,-28.67 -17.7,-58.08 -26.3,-87.64 -7.48,-25.72 -10.39,-57.68 -35.05,-74.46 -100.02,18.93 -89.71,104.89 -70.09,205.9 47.35,243.43 170.89,706.45 211.48,946.04 -72.97,-70.97 -153.99,-207.41 -289.14,-236.55 -136.47,-29.44 -217.95,47.68 -271.6,122.66 -17.14,23.96 -41.43,49.54 -26.29,78.84 83.96,35.51 113.37,-65.2 197.15,-74.47 22.65,-2.5 54.56,2.4 74.46,8.78 132.4,42.34 237.57,218.76 297.87,346.07 74.16,156.45 125.32,330.5 148.95,490.64 -65.71,11.4 -142.96,22.15 -219.25,36.52 -109.8,20.72 -158.81,10.75 -201.29,59.86 9.15,41.95 41.41,60.8 70.1,83.24 126.26,-16.84 252.45,-33.77 372.36,-56.97 20.43,89.25 51.98,218.51 74.45,311.05 40.53,26.02 88.88,-8.43 105.17,-35.06 z" />
</group>
</group>
</group>
</vector>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Direct port of /home/elias/Bilder/marianum_m_white.svg.
- viewport matches the source SVG (70mm × 82.2mm).
- Transforms recreate the SVG's three nested matrices: outer scale+offset,
inner translate, and a final scale+y-flip that pulls the path data into
the visible coordinate space.
- Tinted at runtime via setColorFilter so light/dark themes can share one
drawable.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="70dp"
android:height="82dp"
android:viewportWidth="70.000168"
android:viewportHeight="82.227348">
<group
android:scaleX="0.26458333"
android:scaleY="0.26458334"
android:translateX="107.44411"
android:translateY="-80.482198">
<group
android:translateX="-749.41293"
android:translateY="290.52252">
<group
android:scaleX="0.13333333"
android:scaleY="-0.13333333"
android:translateX="0"
android:translateY="632.14667">
<path
android:fillColor="#FFFFFF"
android:pathData="M 3499.67,4594.33 c -16.43,-106.19 -29.71,-199.97 -43.79,-293.49 86.83,-19 138.5,-27.61 223.38,-43.82 63.81,-12.18 175.24,-20.4 179.64,-83.23 6.46,-92.69 -124.69,-55.41 -188.38,-43.81 -84.33,15.36 -159.13,28.84 -232.2,43.81 -13.68,-60.72 -26.83,-118.68 -39.43,-179.61 -36.76,-178.32 -73.67,-368.16 -105.11,-551.97 18.09,25.66 30.84,42.72 43.8,65.7 66.7,118.26 140.39,245.04 227.76,354.83 33.49,42.05 76.86,94.81 118.31,113.91 98.42,45.36 166.68,-22.2 170.87,-118.28 3.68,-85.28 -23.09,-181.17 -35.08,-275.99 -12.4,-98.19 -22.89,-194.93 -35.03,-275.98 72.44,102.69 147.93,269.64 240.95,381.12 27.51,33 73.55,80.61 118.27,87.62 218.76,34.33 126.58,-312.17 127.05,-473.13 0.4,-144.9 44.01,-255.37 175.21,-271.59 43.02,-5.31 105.84,11.16 112.7,-26.34 8.67,-47.38 -78.15,-60.52 -125.84,-61.28 -291.34,-4.51 -322.06,262.33 -311.01,573.88 -19.85,-18.57 -35.71,-47.53 -52.57,-74.47 -97.59,-155.88 -203.95,-327.22 -297.92,-503.79 -25.93,-48.79 -53.68,-114.7 -135.8,-83.23 -17.27,6.63 -48.25,44.39 -52.56,56.96 -19.58,57.19 1.55,137.42 8.76,205.89 21.54,203.72 57.81,389.09 78.87,587.01 -26.3,0.51 -43.93,-30.07 -56.96,-48.2 -46.9,-65.27 -86.02,-140.76 -127.04,-214.64 -52.84,-95.15 -108.23,-192.84 -157.71,-293.52 -75.25,-153.09 -188.6,-501.89 -242.12,-678.81 -8.67,-28.67 -17.7,-58.08 -26.3,-87.64 -7.48,-25.72 -10.39,-57.68 -35.05,-74.46 -100.02,18.93 -89.71,104.89 -70.09,205.9 47.35,243.43 170.89,706.45 211.48,946.04 -72.97,-70.97 -153.99,-207.41 -289.14,-236.55 -136.47,-29.44 -217.95,47.68 -271.6,122.66 -17.14,23.96 -41.43,49.54 -26.29,78.84 83.96,35.51 113.37,-65.2 197.15,-74.47 22.65,-2.5 54.56,2.4 74.46,8.78 132.4,42.34 237.57,218.76 297.87,346.07 74.16,156.45 125.32,330.5 148.95,490.64 -65.71,11.4 -142.96,22.15 -219.25,36.52 -109.8,20.72 -158.81,10.75 -201.29,59.86 9.15,41.95 41.41,60.8 70.1,83.24 126.26,-16.84 252.45,-33.77 372.36,-56.97 20.43,89.25 51.98,218.51 74.45,311.05 40.53,26.02 88.88,-8.43 105.17,-35.06 z" />
</group>
</group>
</group>
</vector>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/widget_divider" />
</shape>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<corners android:radius="6dp" />
<solid android:color="@color/widget_lesson_cancelled" />
<stroke android:width="1.5dp" android:color="#C8FF0000" />
</shape>
</item>
<item
android:left="3dp"
android:top="3dp"
android:right="3dp"
android:bottom="3dp"
android:drawable="@drawable/widget_lesson_cancelled_x" />
</layer-list>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="6dp" />
<solid android:color="#FF2196F3" />
</shape>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="6dp" />
<solid android:color="#FF4CAF50" />
</shape>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="6dp" />
<solid android:color="#FFEF6C00" />
</shape>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="6dp" />
<solid android:color="#FF993333" />
</shape>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="6dp" />
<solid android:color="@color/widget_lesson_irregular" />
</shape>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="6dp" />
<solid android:color="@color/widget_lesson_ongoing" />
</shape>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="6dp" />
<solid android:color="@color/widget_lesson_past" />
</shape>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="6dp" />
<solid android:color="@color/widget_lesson_regular" />
</shape>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="6dp" />
<solid android:color="@color/widget_lesson_teacher_changed" />
</shape>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="100dp"
android:height="100dp"
android:viewportWidth="100"
android:viewportHeight="100">
<path
android:strokeColor="#C8FF0000"
android:strokeWidth="5"
android:pathData="M0,0 L100,100 M100,0 L0,100" />
</vector>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget_break_block_root"
android:layout_width="match_parent"
android:layout_height="14dp"
android:layout_marginTop="0dp"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp" />
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- FrameLayout root so the Marianum watermark can sit at bottom|end behind
the actual widget content without disturbing the LinearLayout flow. -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/widget_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/app_widget_background">
<!-- Bottom-leading so the mark looks like it's peeking out of the lower
left corner. Width/height/start+bottom margins are overridden in the
renderer so the mark scales with the widget size. -->
<ImageView
android:id="@+id/widget_watermark"
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_gravity="bottom|end"
android:scaleType="fitCenter"
android:importantForAccessibility="no"
android:contentDescription="@null"
android:alpha="0.025"
android:src="@drawable/marianum_m_watermark" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/widget_day_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="@color/widget_text_primary"
android:singleLine="true"
android:ellipsize="end"
tools:text="Heute · 08.05." />
<TextView
android:id="@+id/widget_day_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="10sp"
android:textColor="@color/widget_text_secondary"
tools:text="Stand: 14:32" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="6dp"
android:orientation="horizontal">
<FrameLayout
android:id="@+id/widget_day_time_labels"
android:layout_width="32dp"
android:layout_height="match_parent" />
<FrameLayout
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/widget_divider" />
<FrameLayout
android:id="@+id/widget_day_grid"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
<TextView
android:id="@+id/widget_day_empty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:textSize="13sp"
android:textColor="@color/widget_text_secondary"
android:gravity="center"
android:padding="12dp"
tools:text="Keine Stunden" />
</LinearLayout>
</FrameLayout>
@@ -0,0 +1,185 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/widget_background">
<ImageView
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_gravity="bottom|end"
android:layout_marginEnd="-28dp"
android:layout_marginBottom="-28dp"
android:scaleType="fitCenter"
android:importantForAccessibility="no"
android:contentDescription="@null"
android:alpha="0.018"
android:src="@drawable/marianum_m_watermark" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="@color/widget_text_primary"
android:singleLine="true"
android:text="Heute · 06.05." />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="10sp"
android:textColor="@color/widget_text_secondary"
android:text="Stand: 07:50" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="horizontal"
android:layout_marginTop="6dp">
<FrameLayout
android:layout_width="32dp"
android:layout_height="match_parent">
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
android:orientation="vertical" android:gravity="end" android:paddingEnd="3dp"
android:layout_marginTop="22dp">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textSize="11sp" android:textStyle="bold"
android:textColor="@color/widget_text_primary" android:text="1." />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textSize="7sp" android:textColor="@color/widget_text_secondary" android:text="07:55" />
</LinearLayout>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
android:orientation="vertical" android:gravity="end" android:paddingEnd="3dp"
android:layout_marginTop="50dp">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textSize="11sp" android:textStyle="bold"
android:textColor="@color/widget_text_primary" android:text="2." />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textSize="7sp" android:textColor="@color/widget_text_secondary" android:text="08:40" />
</LinearLayout>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
android:orientation="vertical" android:gravity="end" android:paddingEnd="3dp"
android:layout_marginTop="79dp">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textSize="11sp" android:textStyle="bold"
android:textColor="@color/widget_text_primary" android:text="3." />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textSize="7sp" android:textColor="@color/widget_text_secondary" android:text="09:30" />
</LinearLayout>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
android:orientation="vertical" android:gravity="end" android:paddingEnd="3dp"
android:layout_marginTop="115dp">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textSize="11sp" android:textStyle="bold"
android:textColor="@color/widget_text_primary" android:text="4." />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textSize="7sp" android:textColor="@color/widget_text_secondary" android:text="10:35" />
</LinearLayout>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
android:orientation="vertical" android:gravity="end" android:paddingEnd="3dp"
android:layout_marginTop="142dp">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textSize="11sp" android:textStyle="bold"
android:textColor="@color/widget_text_primary" android:text="5." />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textSize="7sp" android:textColor="@color/widget_text_secondary" android:text="11:25" />
</LinearLayout>
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
android:orientation="vertical" android:gravity="end" android:paddingEnd="3dp"
android:layout_marginTop="169dp">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textSize="11sp" android:textStyle="bold"
android:textColor="@color/widget_text_primary" android:text="6." />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textSize="7sp" android:textColor="@color/widget_text_secondary" android:text="12:15" />
</LinearLayout>
</FrameLayout>
<FrameLayout
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1">
<FrameLayout android:layout_width="match_parent" android:layout_height="1dp" android:layout_marginTop="29dp" android:background="@color/widget_divider" />
<FrameLayout android:layout_width="match_parent" android:layout_height="1dp" android:layout_marginTop="56dp" android:background="@color/widget_divider" />
<FrameLayout android:layout_width="match_parent" android:layout_height="1dp" android:layout_marginTop="83dp" android:background="@color/widget_divider" />
<FrameLayout android:layout_width="match_parent" android:layout_height="1dp" android:layout_marginTop="115dp" android:background="@color/widget_divider" />
<FrameLayout android:layout_width="match_parent" android:layout_height="1dp" android:layout_marginTop="143dp" android:background="@color/widget_divider" />
<FrameLayout android:layout_width="match_parent" android:layout_height="1dp" android:layout_marginTop="170dp" android:background="@color/widget_divider" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="22dp"
android:layout_marginTop="29dp"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:padding="3dp"
android:background="@drawable/widget_lesson_block_regular">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textSize="11sp" android:textStyle="bold"
android:textColor="@android:color/white" android:text="MA-LK" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="55dp"
android:layout_marginTop="83dp"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:padding="3dp"
android:background="@drawable/widget_lesson_block_regular">
<LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textSize="11sp" android:textStyle="bold"
android:textColor="@android:color/white" android:text="DE-GK" />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textSize="9sp" android:textColor="#CCFFFFFF" android:text="B11" />
</LinearLayout>
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="22dp"
android:layout_marginTop="143dp"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:padding="3dp"
android:background="@drawable/widget_lesson_block_cancelled">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textSize="11sp" android:textStyle="bold"
android:textColor="@android:color/white" android:text="BIO" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="22dp"
android:layout_marginTop="170dp"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:padding="3dp"
android:background="@drawable/widget_lesson_block_teacher_changed">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
android:textSize="11sp" android:textStyle="bold"
android:textColor="@android:color/white" android:text="GE" />
</FrameLayout>
</FrameLayout>
</LinearLayout>
</LinearLayout>
</FrameLayout>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget_grid_line_root"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="0dp"
android:background="@drawable/widget_grid_line" />
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/widget_lesson_block_root"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:layout_marginTop="0dp"
android:paddingStart="7dp"
android:paddingEnd="7dp"
android:paddingTop="3dp"
android:paddingBottom="3dp"
android:background="@drawable/widget_lesson_block_regular">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="horizontal"
android:gravity="top"
android:baselineAligned="false">
<TextView
android:id="@+id/widget_lesson_subject"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textStyle="bold"
android:textColor="@android:color/white"
android:maxLines="1"
android:ellipsize="end"
android:gravity="top|start"
android:includeFontPadding="false"
android:autoSizeTextType="uniform"
android:autoSizeMinTextSize="5sp"
android:autoSizeMaxTextSize="11sp"
android:autoSizeStepGranularity="1sp"
tools:text="MA-LK" />
<LinearLayout
android:id="@+id/widget_lesson_secondary_stack"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="top|end"
android:layout_marginStart="4dp">
<TextView
android:id="@+id/widget_lesson_room"
android:layout_width="wrap_content"
android:layout_height="12dp"
android:textColor="#CCFFFFFF"
android:maxLines="1"
android:ellipsize="end"
android:gravity="end|top"
android:includeFontPadding="false"
android:autoSizeTextType="uniform"
android:autoSizeMinTextSize="5sp"
android:autoSizeMaxTextSize="11sp"
android:autoSizeStepGranularity="1sp"
tools:text="A12" />
<TextView
android:id="@+id/widget_lesson_teacher"
android:layout_width="wrap_content"
android:layout_height="12dp"
android:textColor="#B3FFFFFF"
android:maxLines="1"
android:ellipsize="end"
android:gravity="end|top"
android:includeFontPadding="false"
android:autoSizeTextType="uniform"
android:autoSizeMinTextSize="5sp"
android:autoSizeMaxTextSize="11sp"
android:autoSizeStepGranularity="1sp"
tools:text="Müller" />
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/widget_lesson_sibling_badge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|start"
android:textStyle="bold"
android:textSize="12sp"
android:textColor="@android:color/white"
android:maxLines="1"
android:includeFontPadding="false"
android:visibility="gone"
tools:text="+1" />
</FrameLayout>
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/widget_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/app_widget_background">
<ImageView
android:id="@+id/widget_watermark"
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_gravity="bottom|end"
android:scaleType="fitCenter"
android:importantForAccessibility="no"
android:contentDescription="@null"
android:alpha="0.025"
android:src="@drawable/marianum_m_watermark" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:gravity="center"
android:padding="16dp">
<TextView
android:id="@+id/widget_placeholder_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="@color/widget_text_primary"
tools:text="Marianum Vertretungsplan" />
<TextView
android:id="@+id/widget_placeholder_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="12sp"
android:layout_marginTop="6dp"
android:textColor="@color/widget_text_secondary"
tools:text="Bitte einloggen, um den Stundenplan zu laden" />
</LinearLayout>
</FrameLayout>
@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/widget_time_label_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="0dp"
android:orientation="vertical"
android:gravity="end"
android:paddingEnd="3dp">
<TextView
android:id="@+id/widget_time_label_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="9sp"
android:textColor="@color/widget_text_primary"
android:singleLine="true"
android:lineSpacingExtra="-2dp"
tools:text="07:55" />
<TextView
android:id="@+id/widget_time_label_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="7sp"
android:textColor="@color/widget_text_secondary"
android:singleLine="true"
android:lineSpacingExtra="-2dp"
tools:text="1." />
</LinearLayout>
@@ -0,0 +1,185 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/widget_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/app_widget_background">
<ImageView
android:id="@+id/widget_watermark"
android:layout_width="160dp"
android:layout_height="160dp"
android:layout_gravity="bottom|end"
android:scaleType="fitCenter"
android:importantForAccessibility="no"
android:contentDescription="@null"
android:alpha="0.025"
android:src="@drawable/marianum_m_watermark" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:id="@+id/widget_week_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="@color/widget_text_primary"
android:singleLine="true"
android:ellipsize="end"
tools:text="KW 19 · 06.05.10.05." />
<TextView
android:id="@+id/widget_week_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="10sp"
android:textColor="@color/widget_text_secondary"
tools:text="Stand: 14:32" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="6dp">
<FrameLayout
android:layout_width="28dp"
android:layout_height="wrap_content" />
<FrameLayout
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/widget_divider" />
<FrameLayout
android:id="@+id/widget_week_header_mon"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<FrameLayout
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/widget_divider" />
<FrameLayout
android:id="@+id/widget_week_header_tue"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<FrameLayout
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/widget_divider" />
<FrameLayout
android:id="@+id/widget_week_header_wed"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<FrameLayout
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/widget_divider" />
<FrameLayout
android:id="@+id/widget_week_header_thu"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
<FrameLayout
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/widget_divider" />
<FrameLayout
android:id="@+id/widget_week_header_fri"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:layout_marginTop="2dp"
android:orientation="horizontal">
<FrameLayout
android:id="@+id/widget_week_time_labels"
android:layout_width="28dp"
android:layout_height="match_parent" />
<FrameLayout
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/widget_divider" />
<FrameLayout
android:id="@+id/widget_week_col_mon"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<FrameLayout
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/widget_divider" />
<FrameLayout
android:id="@+id/widget_week_col_tue"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<FrameLayout
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/widget_divider" />
<FrameLayout
android:id="@+id/widget_week_col_wed"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<FrameLayout
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/widget_divider" />
<FrameLayout
android:id="@+id/widget_week_col_thu"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<FrameLayout
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/widget_divider" />
<FrameLayout
android:id="@+id/widget_week_col_fri"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
</LinearLayout>
</LinearLayout>
</FrameLayout>
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/widget_week_day_header_root"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:gravity="center"
android:paddingTop="2dp"
android:paddingBottom="3dp">
<TextView
android:id="@+id/widget_week_day_header_weekday"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="11sp"
android:textStyle="bold"
android:textColor="@color/widget_text_primary"
tools:text="Mo" />
<TextView
android:id="@+id/widget_week_day_header_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="9sp"
android:textColor="@color/widget_text_secondary"
tools:text="06.05." />
</LinearLayout>
@@ -0,0 +1,147 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Static preview for the week widget. Five day columns with two short
demo blocks each so the structure is recognisable in the picker. -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/widget_background">
<ImageView
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="bottom|end"
android:layout_marginEnd="-36dp"
android:layout_marginBottom="-36dp"
android:scaleType="fitCenter"
android:importantForAccessibility="no"
android:contentDescription="@null"
android:alpha="0.018"
android:src="@drawable/marianum_m_watermark" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="@color/widget_text_primary"
android:singleLine="true"
android:text="KW 19 · 06.05.10.05." />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="10sp"
android:textColor="@color/widget_text_secondary"
android:text="Stand: 07:50" />
</LinearLayout>
<!-- Day-name + date row -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="6dp">
<FrameLayout android:layout_width="20dp" android:layout_height="wrap_content" />
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:gravity="center">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="11sp" android:textStyle="bold" android:textColor="@color/widget_text_primary" android:text="Mo" />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textColor="@color/widget_text_secondary" android:text="06.05." />
</LinearLayout>
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:gravity="center">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="11sp" android:textStyle="bold" android:textColor="@color/widget_text_primary" android:text="Di" />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textColor="@color/widget_text_secondary" android:text="07.05." />
</LinearLayout>
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:gravity="center">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="11sp" android:textStyle="bold" android:textColor="@color/widget_text_primary" android:text="Mi" />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textColor="@color/widget_text_secondary" android:text="08.05." />
</LinearLayout>
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:gravity="center">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="11sp" android:textStyle="bold" android:textColor="@color/widget_text_primary" android:text="Do" />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textColor="@color/widget_text_secondary" android:text="09.05." />
</LinearLayout>
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:gravity="center">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="11sp" android:textStyle="bold" android:textColor="@color/widget_text_primary" android:text="Fr" />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textColor="@color/widget_text_secondary" android:text="10.05." />
</LinearLayout>
</LinearLayout>
<!-- Time grid: time-label column + 5 day columns -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="horizontal"
android:layout_marginTop="2dp">
<FrameLayout android:layout_width="20dp" android:layout_height="match_parent">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="0dp" android:gravity="end" android:paddingEnd="3dp" android:textSize="8sp" android:textColor="@color/widget_text_secondary" android:text="08" />
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="36dp" android:gravity="end" android:paddingEnd="3dp" android:textSize="8sp" android:textColor="@color/widget_text_secondary" android:text="10" />
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="72dp" android:gravity="end" android:paddingEnd="3dp" android:textSize="8sp" android:textColor="@color/widget_text_secondary" android:text="12" />
</FrameLayout>
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
<!-- Mon -->
<FrameLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1">
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="2dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_regular">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="MA" />
</FrameLayout>
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="40dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_regular">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="EN" />
</FrameLayout>
</FrameLayout>
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
<!-- Tue -->
<FrameLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1">
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="20dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_cancelled">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="BIO" />
</FrameLayout>
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="60dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_regular">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="DE" />
</FrameLayout>
</FrameLayout>
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
<!-- Wed -->
<FrameLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1">
<FrameLayout android:layout_width="match_parent" android:layout_height="32dp" android:layout_marginTop="2dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_regular">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="MA" />
</FrameLayout>
</FrameLayout>
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
<!-- Thu -->
<FrameLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1">
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="2dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_teacher_changed">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="GE" />
</FrameLayout>
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="40dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_regular">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="PH" />
</FrameLayout>
</FrameLayout>
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
<!-- Fri -->
<FrameLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1">
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="20dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_regular">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="EN" />
</FrameLayout>
</FrameLayout>
</LinearLayout>
</LinearLayout>
</FrameLayout>
@@ -1,5 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<foreground>
<inset
android:drawable="@drawable/ic_launcher_foreground"
android:inset="16%" />
</foreground>
</adaptive-icon>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="widget_background">#FF1F1716</color>
<color name="widget_text_primary">#FFF1F1F1</color>
<color name="widget_text_secondary">#FFB0B0B0</color>
<color name="widget_divider">#33FFFFFF</color>
</resources>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Brand accent for notifications; mirrors LightAppTheme.marianumRed
(Color.fromARGB(255, 153, 51, 51)). Used as the FCM default
notification color and must stay in sync with the Dart constant. -->
<color name="notification_accent">#993333</color>
</resources>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="widget_day_label">Marianum · Heute</string>
<string name="widget_week_label">Marianum · Woche</string>
<string name="widget_day_description">Stundenplan und Vertretungen für den anstehenden Schultag.</string>
<string name="widget_week_description">Stundenplan und Vertretungen für die ganze Schulwoche.</string>
<string name="widget_no_lessons">Keine Stunden</string>
<string name="widget_holiday">Ferien</string>
<string name="widget_login_required">Bitte einloggen, um den Stundenplan zu laden</string>
<string name="widget_loading">Lade…</string>
<string name="widget_status_label">Stand: %1$s</string>
<string name="widget_today">Heute</string>
<string name="widget_tomorrow">Morgen</string>
<string name="widget_placeholder_title">Marianum Stundenplan</string>
<string name="widget_calendar_week_prefix">KW</string>
<string name="widget_yesterday_prefix">gestern</string>
</resources>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Status colors mirror lib/view/pages/timetable/data/lesson_color.dart
exactly so widget tiles match the in-app calendar. -->
<color name="widget_lesson_regular">#FF993333</color>
<color name="widget_lesson_ongoing">#FFC83333</color>
<color name="widget_lesson_past">#FF993333</color>
<color name="widget_lesson_cancelled">#FF000000</color>
<color name="widget_lesson_irregular">#FF8F19B3</color>
<color name="widget_lesson_teacher_changed">#FF29639B</color>
<color name="widget_lesson_event">#FF2E7D32</color>
<color name="widget_background">#FFFCF7F5</color>
<color name="widget_text_primary">#FF111111</color>
<color name="widget_text_secondary">#FF555555</color>
<color name="widget_divider">#22000000</color>
</resources>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Auto-Backup rules for Android 11 and below.
Excludes the home_widget plugin's SharedPreferences file
(HomeWidgetPreferences) so the cached timetable — which contains
teacher names, room numbers and personal custom events — is not
uploaded to the user's Google Drive. -->
<full-backup-content>
<exclude domain="sharedpref" path="HomeWidgetPreferences.xml"/>
</full-backup-content>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Backup + device-transfer rules for Android 12+.
Excludes the home_widget plugin's SharedPreferences file
(HomeWidgetPreferences) so the cached timetable — which contains
teacher names, room numbers and personal custom events — is not
uploaded to the user's Google Drive or transferred to a new device.
The widget repopulates from a fresh Webuntis fetch after sign-in. -->
<data-extraction-rules>
<cloud-backup>
<exclude domain="sharedpref" path="HomeWidgetPreferences.xml"/>
</cloud-backup>
<device-transfer>
<exclude domain="sharedpref" path="HomeWidgetPreferences.xml"/>
</device-transfer>
</data-extraction-rules>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget_placeholder"
android:minWidth="110dp"
android:minHeight="180dp"
android:targetCellWidth="2"
android:targetCellHeight="5"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen"
android:updatePeriodMillis="0"
android:description="@string/widget_day_description"
android:previewLayout="@layout/widget_day_preview" />
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialLayout="@layout/widget_placeholder"
android:minWidth="320dp"
android:minHeight="240dp"
android:targetCellWidth="5"
android:targetCellHeight="5"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen"
android:updatePeriodMillis="0"
android:description="@string/widget_week_description"
android:previewLayout="@layout/widget_week_preview" />
+28
View File
@@ -9,6 +9,34 @@ rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
// Pin every Android subproject to JVM 17 so plugins that ship Kotlin sources
// compiled with a higher target (e.g. receive_sharing_intent at 21) or stale
// Java compatibility (e.g. home_widget at 1.8) don't break the build under
// newer Gradle/Kotlin tooling. Registered before evaluationDependsOn so the
// afterEvaluate fires at the right point in the lifecycle.
subprojects { sub ->
sub.afterEvaluate {
if (sub.hasProperty('android')) {
sub.android {
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
// Some plugins (e.g. app_settings) hard-pin an older compileSdk
// than their transitive AndroidX deps require, which fails the
// AAR metadata check. Align every plugin with the app's
// compileSdk (flutter.compileSdkVersion) so the build passes.
compileSdkVersion 36
}
}
sub.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
jvmTarget = '17'
}
}
}
}
subprojects {
project.evaluationDependsOn(':app')
}
+6
View File
@@ -0,0 +1,6 @@
package_name("eu.mhsl.marianum.mobile.client")
# Service-Account-JSON für die Google Play Developer API. Pfad über die
# Umgebungsvariable SUPPLY_JSON_KEY setzen (die Datei selbst wird per
# .gitignore nicht eingecheckt).
json_key_file(ENV["SUPPLY_JSON_KEY"] || "fastlane/play-service-account.json")
+29
View File
@@ -0,0 +1,29 @@
default_platform(:android)
# Play-Store-Auslieferung über fastlane supply. Die Screenshots erzeugt
# ../tool/screenshots.sh (Demo-Login + integration_test) direkt in
# metadata/android/<locale>/images/. Texte/Changelogs liegen daneben in
# metadata/android/<locale>/.
platform :android do
desc "Nur Screenshots hochladen (verändert keine Texte/Binaries)"
lane :upload_screenshots do
upload_to_play_store(
skip_upload_apk: true,
skip_upload_aab: true,
skip_upload_metadata: true,
skip_upload_changelogs: true,
skip_upload_images: false,
skip_upload_screenshots: false,
metadata_path: "./fastlane/metadata/android",
)
end
desc "Store-Texte, Changelogs und Screenshots hochladen (keine Binaries)"
lane :upload_metadata do
upload_to_play_store(
skip_upload_apk: true,
skip_upload_aab: true,
metadata_path: "./fastlane/metadata/android",
)
end
end
+2
View File
@@ -0,0 +1,2 @@
# fastlane-Plugins (aktuell keine). Datei vorhanden, damit `fastlane` das
# Plugin-Handling initialisiert.
+35
View File
@@ -0,0 +1,35 @@
# Fastlane Play-Store-Screenshots & Upload
Schritt 2 der Play-Store-Automatisierung. Die App muss dafür **nicht** neu
gebaut werden der Demo-Modus (`demo@`-Login) liefert die kompletten Inhalte
aus Fixtures.
## 1. Screenshots aufnehmen
Ein Gerät/Emulator (≥ Android 8) per `adb devices` sichtbar, dann aus dem
**Client**-Wurzelverzeichnis:
```bash
tool/screenshots.sh
```
Fährt Phone (1080×1920), 7″ (1200×1920) und 10″ (1600×2560) nacheinander durch
(setzt die Displaygröße per `adb shell wm size/density`) und legt die PNGs in
`metadata/android/de-DE/images/{phone,sevenInch,tenInch}Screenshots/` ab.
- Nur eine Größe: `PROFILES="phone" tool/screenshots.sh`
- Profile-Build (flüssigere Frames): `BUILD_MODE=profile tool/screenshots.sh`
## 2. Hochladen
Service-Account-JSON der Google Play Developer API besorgen und referenzieren:
```bash
export SUPPLY_JSON_KEY=/pfad/zu/play-service-account.json
cd android
fastlane upload_screenshots # nur Bilder
# oder
fastlane upload_metadata # Bilder + Texte/Changelogs, keine Binaries
```
Der Service-Account-Key wird **nicht** eingecheckt (siehe `.gitignore`).
+7
View File
@@ -1,3 +1,10 @@
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
# This builtInKotlin flag was added automatically by Flutter migrator
android.builtInKotlin=false
# This newDsl flag was added automatically by Flutter migrator
android.newDsl=false
@@ -0,0 +1,2 @@
#This file is generated by updateDaemonJvm
toolchainVersion=21
+2 -1
View File
@@ -1,6 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+4 -2
View File
@@ -19,8 +19,10 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
id "com.android.application" version '8.13.2' apply false
id "com.android.library" version '8.13.2' apply false
id "org.jetbrains.kotlin.android" version "2.2.20" apply false
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.10.0'
}
include ":app"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 12 KiB

+29
View File
@@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIFBTCCAu2gAwIBAgIQS6hSk/eaL6JzBkuoBI110DANBgkqhkiG9w0BAQsFADBP
MQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFy
Y2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTAeFw0yNDAzMTMwMDAwMDBa
Fw0yNzAzMTIyMzU5NTlaMDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBF
bmNyeXB0MQwwCgYDVQQDEwNSMTAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQDPV+XmxFQS7bRH/sknWHZGUCiMHT6I3wWd1bUYKb3dtVq/+vbOo76vACFL
YlpaPAEvxVgD9on/jhFD68G14BQHlo9vH9fnuoE5CXVlt8KvGFs3Jijno/QHK20a
/6tYvJWuQP/py1fEtVt/eA0YYbwX51TGu0mRzW4Y0YCF7qZlNrx06rxQTOr8IfM4
FpOUurDTazgGzRYSespSdcitdrLCnF2YRVxvYXvGLe48E1KGAdlX5jgc3421H5KR
mudKHMxFqHJV8LDmowfs/acbZp4/SItxhHFYyTr6717yW0QrPHTnj7JHwQdqzZq3
DZb3EoEmUVQK7GH29/Xi8orIlQ2NAgMBAAGjgfgwgfUwDgYDVR0PAQH/BAQDAgGG
MB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATASBgNVHRMBAf8ECDAGAQH/
AgEAMB0GA1UdDgQWBBS7vMNHpeS8qcbDpHIMEI2iNeHI6DAfBgNVHSMEGDAWgBR5
tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAKG
Fmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0gBAwwCjAIBgZngQwBAgEwJwYD
VR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVuY3Iub3JnLzANBgkqhkiG9w0B
AQsFAAOCAgEAkrHnQTfreZ2B5s3iJeE6IOmQRJWjgVzPw139vaBw1bGWKCIL0vIo
zwzn1OZDjCQiHcFCktEJr59L9MhwTyAWsVrdAfYf+B9haxQnsHKNY67u4s5Lzzfd
u6PUzeetUK29v+PsPmI2cJkxp+iN3epi4hKu9ZzUPSwMqtCceb7qPVxEbpYxY1p9
1n5PJKBLBX9eb9LU6l8zSxPWV7bK3lG4XaMJgnT9x3ies7msFtpKK5bDtotij/l0
GaKeA97pb5uwD9KgWvaFXMIEt8jVTjLEvwRdvCn294GPDF08U8lAkIv7tghluaQh
1QnlE4SEN4LOECj8dsIGJXpGUk3aU3KkJz9icKy+aUgA+2cP21uh6NcDIS3XyfaZ
QjmDQ993ChII8SXWupQZVBiIpcWO4RqZk3lr7Bz5MUCwzDIA359e57SSq5CCkY0N
4B6Vulk7LktfwrdGNVI5BsC9qqxSwSKgRJeZ9wygIaehbHFHFhcBaMDKpiZlBHyz
rsnnlFXCb5s8HKn5LsUgGvB24L7sGNZP2CX7dhHov+YhD+jozLW2p9W4959Bz2Ei
RmqDtmiXLnzqTpXbI+suyCsohKRg6Un0RC47+cpiVwHiXZAW+cn8eiNIjqbVgXLx
KPpdzvvtTnOPlC7SQZSYmdunr3Bf9b77AiC/ZidstK36dRILKz7OA54=
-----END CERTIFICATE-----
+29
View File
@@ -0,0 +1,29 @@
-----BEGIN CERTIFICATE-----
MIIFBTCCAu2gAwIBAgIQWgDyEtjUtIDzkkFX6imDBTANBgkqhkiG9w0BAQsFADBP
MQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFy
Y2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTAeFw0yNDAzMTMwMDAwMDBa
Fw0yNzAzMTIyMzU5NTlaMDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBF
bmNyeXB0MQwwCgYDVQQDEwNSMTMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQClZ3CN0FaBZBUXYc25BtStGZCMJlA3mBZjklTb2cyEBZPs0+wIG6BgUUNI
fSvHSJaetC3ancgnO1ehn6vw1g7UDjDKb5ux0daknTI+WE41b0VYaHEX/D7YXYKg
L7JRbLAaXbhZzjVlyIuhrxA3/+OcXcJJFzT/jCuLjfC8cSyTDB0FxLrHzarJXnzR
yQH3nAP2/Apd9Np75tt2QnDr9E0i2gB3b9bJXxf92nUupVcM9upctuBzpWjPoXTi
dYJ+EJ/B9aLrAek4sQpEzNPCifVJNYIKNLMc6YjCR06CDgo28EdPivEpBHXazeGa
XP9enZiVuppD0EqiFwUBBDDTMrOPAgMBAAGjgfgwgfUwDgYDVR0PAQH/BAQDAgGG
MB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATASBgNVHRMBAf8ECDAGAQH/
AgEAMB0GA1UdDgQWBBTnq58PLDOgU9NeT3jIsoQOO9aSMzAfBgNVHSMEGDAWgBR5
tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAKG
Fmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0gBAwwCjAIBgZngQwBAgEwJwYD
VR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVuY3Iub3JnLzANBgkqhkiG9w0B
AQsFAAOCAgEAUTdYUqEimzW7TbrOypLqCfL7VOwYf/Q79OH5cHLCZeggfQhDconl
k7Kgh8b0vi+/XuWu7CN8n/UPeg1vo3G+taXirrytthQinAHGwc/UdbOygJa9zuBc
VyqoH3CXTXDInT+8a+c3aEVMJ2St+pSn4ed+WkDp8ijsijvEyFwE47hulW0Ltzjg
9fOV5Pmrg/zxWbRuL+k0DBDHEJennCsAen7c35Pmx7jpmJ/HtgRhcnz0yjSBvyIw
6L1QIupkCv2SBODT/xDD3gfQQyKv6roV4G2EhfEyAsWpmojxjCUCGiyg97FvDtm/
NK2LSc9lybKxB73I2+P2G3CaWpvvpAiHCVu30jW8GCxKdfhsXtnIy2imskQqVZ2m
0Pmxobb28Tucr7xBK7CtwvPrb79os7u2XP3O5f9b/H66GNyRrglRXlrYjI1oGYL/
f4I1n/Sgusda6WvA6C190kxjU15Y12mHU4+BxyR9cx2hhGS9fAjMZKJss28qxvz6
Axu4CaDmRNZpK/pQrXF17yXCXkmEWgvSOEZy6Z9pcbLIVEGckV/iVeq0AOo2pkg9
p4QRIy0tK2diRENLSF2KysFwbY6B26BFeFs3v1sYVRhFW9nLkOrQVporCS0KyZmf
wVD89qSTlnctLcZnIavjKsKUu1nA1iU0yYMdYepKR7lWbnwhdx3ewok=
-----END CERTIFICATE-----
+20
View File
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="69.999985mm"
height="82.227501mm"
viewBox="0 0 69.999985 82.227501"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="g1"
transform="matrix(0.26458333,0,0,0.26458334,120.1078,-90.601937)"><g
id="group-R5"
transform="translate(-749.41293,290.52252)"><path
id="path2"
d="m 418.75901,57.817748 c -2.19013,14.15999 -3.96053,26.66133 -5.83906,39.13333 11.5776,2.532 18.4688,3.680002 29.7848,5.841332 8.50666,1.62133 23.36533,2.71733 23.952,11.096 0.86133,12.35866 -16.62534,7.388 -25.11734,5.84133 -11.244,-2.04667 -21.21693,-3.844 -30.9596,-5.84133 -1.82346,8.09866 -3.57813,15.82533 -5.25733,23.948 -4.90253,23.77866 -9.824,49.08933 -14.01666,73.59866 2.4136,-3.42133 4.11413,-5.69733 5.84226,-8.76267 8.89267,-15.76799 18.71867,-32.67066 30.37027,-47.31066 4.46106,-5.60667 10.24506,-12.64133 15.77306,-15.188 13.12267,-6.048 22.224,2.96133 22.78,15.77067 0.49334,11.372 -3.07733,24.15733 -4.676,36.80133 -1.65333,13.08933 -3.05066,25.98933 -4.66933,36.796 9.65733,-13.69067 19.72267,-35.95334 32.12533,-50.81733 3.668,-4.4 9.80667,-10.748 15.76934,-11.68134 29.16799,-4.57866 16.87733,41.62267 16.94133,63.084 0.0547,19.32001 5.86667,34.04934 23.36133,36.21068 5.73333,0.70933 14.10933,-1.48667 15.02533,3.51466 1.15467,6.31467 -10.42133,8.068 -16.77866,8.168 -38.84667,0.60267 -42.94133,-34.97734 -41.46933,-76.51601 -2.64534,2.476 -4.76134,6.336 -7.00934,9.93067 -13.00933,20.78267 -27.192,43.62801 -39.72133,67.17068 -3.45467,6.504 -7.15867,15.29333 -18.10667,11.09733 -2.30133,-0.88267 -6.43466,-5.92 -7.008,-7.59467 -2.60786,-7.62666 0.20667,-18.32533 1.168,-27.45333 2.87334,-27.16134 7.70934,-51.87868 10.516,-78.26534 -3.50533,-0.0707 -5.85733,4.008 -7.59333,6.42667 -6.2548,8.70266 -11.47147,18.76666 -16.94133,28.61866 -7.04214,12.684 -14.42814,25.71067 -21.02867,39.13201 -10.03173,20.41333 -25.14373,66.92133 -32.28013,90.50933 -1.15573,3.82133 -2.35933,7.744 -3.5052,11.68667 -0.998,3.428 -1.38693,7.69066 -4.67347,9.928 -13.33853,-2.52534 -11.962,-13.98534 -9.3464,-27.45467 6.31414,-32.45733 22.78547,-94.19334 28.19587,-126.13867 -9.72813,9.464 -20.53067,27.65466 -38.552,31.54133 -18.1928,3.92667 -29.05788,-6.35733 -36.2136,-16.35333 -2.28489,-3.19467 -5.52188,-6.60534 -3.50417,-10.51467 11.19375,-4.73333 15.11457,8.69467 26.28537,9.93067 3.0204,0.33199 7.2756,-0.32001 9.93027,-1.17067 17.65253,-5.64667 31.67346,-29.168 39.71506,-46.14133 9.88747,-20.86267 16.70987,-44.06667 19.86094,-65.42133 -8.76254,-1.51867 -19.064,-2.95467 -29.2344,-4.86933 -14.64054,-2.76134 -21.17334,-1.432 -26.83947,-7.980002 1.2228,-5.592 5.5228,-8.108 9.34627,-11.09867 16.83653,2.24533 33.6584,4.504 49.64946,7.59333 2.72454,-11.9 6.92973,-29.13199 9.92813,-41.47199 5.40267,-3.468 11.84947,1.12533 14.0204,4.676"
style="fill:#941a1f;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.133333" /></g></g></svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

+21
View File
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="70.000168mm"
height="82.227348mm"
viewBox="0 0 70.000168 82.227348"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs1" /><g
id="g1"
transform="matrix(0.26458333,0,0,0.26458334,107.44411,-80.482198)"><g
id="group-R5"
transform="translate(-749.41293,290.52252)"><path
id="path3"
d="m 3499.67,4594.33 c -16.43,-106.19 -29.71,-199.97 -43.79,-293.49 86.83,-19 138.5,-27.61 223.38,-43.82 63.81,-12.18 175.24,-20.4 179.64,-83.23 6.46,-92.69 -124.69,-55.41 -188.38,-43.81 -84.33,15.36 -159.13,28.84 -232.2,43.81 -13.68,-60.72 -26.83,-118.68 -39.43,-179.61 -36.76,-178.32 -73.67,-368.16 -105.11,-551.97 18.09,25.66 30.84,42.72 43.8,65.7 66.7,118.26 140.39,245.04 227.76,354.83 33.49,42.05 76.86,94.81 118.31,113.91 98.42,45.36 166.68,-22.2 170.87,-118.28 3.68,-85.28 -23.09,-181.17 -35.08,-275.99 -12.4,-98.19 -22.89,-194.93 -35.03,-275.98 72.44,102.69 147.93,269.64 240.95,381.12 27.51,33 73.55,80.61 118.27,87.62 218.76,34.33 126.58,-312.17 127.05,-473.13 0.4,-144.9 44.01,-255.37 175.21,-271.59 43.02,-5.31 105.84,11.16 112.7,-26.34 8.67,-47.38 -78.15,-60.52 -125.84,-61.28 -291.34,-4.51 -322.06,262.33 -311.01,573.88 -19.85,-18.57 -35.71,-47.53 -52.57,-74.47 -97.59,-155.88 -203.95,-327.22 -297.92,-503.79 -25.93,-48.79 -53.68,-114.7 -135.8,-83.23 -17.27,6.63 -48.25,44.39 -52.56,56.96 -19.58,57.19 1.55,137.42 8.76,205.89 21.54,203.72 57.81,389.09 78.87,587.01 -26.3,0.51 -43.93,-30.07 -56.96,-48.2 -46.9,-65.27 -86.02,-140.76 -127.04,-214.64 -52.84,-95.15 -108.23,-192.84 -157.71,-293.52 -75.25,-153.09 -188.6,-501.89 -242.12,-678.81 -8.67,-28.67 -17.7,-58.08 -26.3,-87.64 -7.48,-25.72 -10.39,-57.68 -35.05,-74.46 -100.02,18.93 -89.71,104.89 -70.09,205.9 47.35,243.43 170.89,706.45 211.48,946.04 -72.97,-70.97 -153.99,-207.41 -289.14,-236.55 -136.47,-29.44 -217.95,47.68 -271.6,122.66 -17.14,23.96 -41.43,49.54 -26.29,78.84 83.96,35.51 113.37,-65.2 197.15,-74.47 22.65,-2.5 54.56,2.4 74.46,8.78 132.4,42.34 237.57,218.76 297.87,346.07 74.16,156.45 125.32,330.5 148.95,490.64 -65.71,11.4 -142.96,22.15 -219.25,36.52 -109.8,20.72 -158.81,10.75 -201.29,59.86 9.15,41.95 41.41,60.8 70.1,83.24 126.26,-16.84 252.45,-33.77 372.36,-56.97 20.43,89.25 51.98,218.51 74.45,311.05 40.53,26.02 88.88,-8.43 105.17,-35.06"
style="fill:#d3d2d2;fill-opacity:1;fill-rule:evenodd;stroke:none"
transform="matrix(0.13333333,0,0,-0.13333333,0,632.14667)" /></g></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

After

Width:  |  Height:  |  Size: 90 KiB

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