75 Commits

Author SHA1 Message Date
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
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
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
641 changed files with 32204 additions and 8927 deletions
+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.
+73 -29
View File
@@ -1,44 +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
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/**"
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
# === Project conventions ===
prefer_relative_imports: true
unnecessary_lambdas: true
prefer_single_quotes: true
prefer_if_elements_to_conditional_expressions: true
prefer_expression_function_bodies: true
omit_local_variable_types: true
eol_at_end_of_file: true
cast_nullable_to_non_nullable: true
avoid_void_async: true
omit_local_variable_types: true
avoid_multiple_declarations_per_line: true
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
# === 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
+1 -1
View File
@@ -25,7 +25,7 @@ if (flutterVersionName == null) {
android {
namespace "eu.mhsl.marianum.mobile.client"
compileSdk flutter.compileSdkVersion
ndkVersion "27.0.12077973"
ndkVersion "28.2.13676358"
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
+70 -6
View File
@@ -2,7 +2,9 @@
<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 +25,85 @@
<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. -->
<!-- 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"/>
<!-- 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"/>
</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,740 @@
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
WidgetLessonStatus.ONGOING -> R.drawable.widget_lesson_block_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"
}
@@ -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>
@@ -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>
@@ -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,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" />
+23
View File
@@ -9,6 +9,29 @@ 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
}
}
}
sub.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
jvmTarget = '17'
}
}
}
}
subprojects {
project.evaluationDependsOn(':app')
}
@@ -0,0 +1,2 @@
#This file is generated by updateDaemonJvm
toolchainVersion=21
+1 -1
View File
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
+4 -3
View File
@@ -19,9 +19,10 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.9.1" apply false
id "com.android.library" version "8.9.1" apply false
id "org.jetbrains.kotlin.android" version "2.1.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

+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: 101 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: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 283 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

+3
View File
@@ -1 +1,4 @@
extensions:
- hive_ce: true
- shared_preferences: true
- provider: true
+1 -1
View File
@@ -54,7 +54,7 @@ flutter_native_splash:
# 640 pixels in diameter.
# App icon without an icon background: This should be 1152×1152 pixels, and fit within a circle
# 768 pixels in diameter.
image: assets/logo/icon-android12.png
image: assets/logo/icon/icon-android12.png
# Splash screen background color.
color: "#993333"
-2
View File
@@ -20,7 +20,5 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>
@@ -0,0 +1,2 @@
#include? "../Pods/Target Support Files/Pods-Share Extension/Pods-Share Extension.debug.xcconfig"
#include "Generated.xcconfig"
@@ -0,0 +1,2 @@
#include? "../Pods/Target Support Files/Pods-Share Extension/Pods-Share Extension.profile.xcconfig"
#include "Generated.xcconfig"
@@ -0,0 +1,2 @@
#include? "../Pods/Target Support Files/Pods-Share Extension/Pods-Share Extension.release.xcconfig"
#include "Generated.xcconfig"
@@ -0,0 +1 @@
#include "Generated.xcconfig"
@@ -0,0 +1 @@
#include "Generated.xcconfig"
@@ -0,0 +1 @@
#include "Generated.xcconfig"
+4
View File
@@ -34,6 +34,10 @@ target 'Runner' do
pod 'PhoneNumberKit', '~> 3.7.6'
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'Share Extension' do
inherit! :search_paths
end
# target 'RunnerTests' do
# inherit! :search_paths
# end
+78 -65
View File
@@ -36,53 +36,37 @@ PODS:
- SwiftyGif
- emoji_picker_flutter (0.0.1):
- Flutter
- fast_rsa (0.7.0):
- eraser (0.0.1):
- Flutter
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Firebase/CoreOnly (12.4.0):
- FirebaseCore (~> 12.4.0)
- Firebase/InAppMessaging (12.4.0):
- Firebase/CoreOnly (12.12.0):
- FirebaseCore (~> 12.12.0)
- Firebase/Messaging (12.12.0):
- Firebase/CoreOnly
- FirebaseInAppMessaging (~> 12.4.0-beta)
- Firebase/Messaging (12.4.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 12.4.0)
- firebase_core (4.2.1):
- Firebase/CoreOnly (= 12.4.0)
- FirebaseMessaging (~> 12.12.0)
- firebase_core (4.7.0):
- Firebase/CoreOnly (= 12.12.0)
- Flutter
- firebase_in_app_messaging (0.9.0-4):
- Firebase/InAppMessaging (= 12.4.0)
- firebase_messaging (16.2.0):
- Firebase/Messaging (= 12.12.0)
- firebase_core
- Flutter
- firebase_messaging (16.0.4):
- Firebase/Messaging (= 12.4.0)
- firebase_core
- Flutter
- FirebaseABTesting (12.4.0):
- FirebaseCore (~> 12.4.0)
- FirebaseCore (12.4.0):
- FirebaseCoreInternal (~> 12.4.0)
- FirebaseCore (12.12.1):
- FirebaseCoreInternal (~> 12.12.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreInternal (12.4.0):
- FirebaseCoreInternal (12.12.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseInAppMessaging (12.4.0-beta):
- FirebaseABTesting (~> 12.4.0)
- FirebaseCore (~> 12.4.0)
- FirebaseInstallations (~> 12.4.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- FirebaseInstallations (12.4.0):
- FirebaseCore (~> 12.4.0)
- FirebaseInstallations (12.12.0):
- FirebaseCore (~> 12.12.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (12.4.0):
- FirebaseCore (~> 12.4.0)
- FirebaseInstallations (~> 12.4.0)
- FirebaseMessaging (12.12.0):
- FirebaseCore (~> 12.12.0)
- FirebaseInstallations (~> 12.12.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
@@ -96,6 +80,9 @@ PODS:
- Flutter
- flutter_native_splash (2.4.3):
- Flutter
- flutter_secure_storage_darwin (10.0.0):
- Flutter
- FlutterMacOS
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
@@ -123,6 +110,8 @@ PODS:
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- home_widget (0.0.1):
- Flutter
- image_picker_ios (0.0.1):
- Flutter
- in_app_review (2.0.0):
@@ -136,9 +125,6 @@ PODS:
- Flutter
- package_info_plus (0.4.5):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- PhoneNumberKit (3.7.11):
- PhoneNumberKit/PhoneNumberKitCore (= 3.7.11)
- PhoneNumberKit/UIKit (= 3.7.11)
@@ -146,9 +132,13 @@ PODS:
- PhoneNumberKit/UIKit (3.7.11):
- PhoneNumberKit/PhoneNumberKitCore
- PromisesObjC (2.4.0)
- SDWebImage (5.21.2):
- SDWebImage/Core (= 5.21.2)
- SDWebImage/Core (5.21.2)
- receive_sharing_intent (1.8.1):
- Flutter
- screen_brightness_ios (2.1.3):
- Flutter
- SDWebImage (5.21.7):
- SDWebImage/Core (= 5.21.7)
- SDWebImage/Core (5.21.7)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@@ -162,41 +152,51 @@ PODS:
- Flutter
- url_launcher_ios (0.0.1):
- Flutter
- video_player_avfoundation (0.0.1):
- Flutter
- FlutterMacOS
- wakelock_plus (0.0.1):
- Flutter
- workmanager_apple (0.0.1):
- Flutter
DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- emoji_picker_flutter (from `.symlinks/plugins/emoji_picker_flutter/ios`)
- fast_rsa (from `.symlinks/plugins/fast_rsa/ios`)
- eraser (from `.symlinks/plugins/eraser/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_in_app_messaging (from `.symlinks/plugins/firebase_in_app_messaging/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`)
- flutter_app_badge (from `.symlinks/plugins/flutter_app_badge/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- flutter_secure_storage_darwin (from `.symlinks/plugins/flutter_secure_storage_darwin/darwin`)
- home_widget (from `.symlinks/plugins/home_widget/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
- open_filex (from `.symlinks/plugins/open_filex/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- PhoneNumberKit (~> 3.7.6)
- receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`)
- screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
- syncfusion_flutter_pdfviewer (from `.symlinks/plugins/syncfusion_flutter_pdfviewer/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`)
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
- workmanager_apple (from `.symlinks/plugins/workmanager_apple/ios`)
SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- Firebase
- FirebaseABTesting
- FirebaseCore
- FirebaseCoreInternal
- FirebaseInAppMessaging
- FirebaseInstallations
- FirebaseMessaging
- GoogleDataTransport
@@ -214,14 +214,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/device_info_plus/ios"
emoji_picker_flutter:
:path: ".symlinks/plugins/emoji_picker_flutter/ios"
fast_rsa:
:path: ".symlinks/plugins/fast_rsa/ios"
eraser:
:path: ".symlinks/plugins/eraser/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_in_app_messaging:
:path: ".symlinks/plugins/firebase_in_app_messaging/ios"
firebase_messaging:
:path: ".symlinks/plugins/firebase_messaging/ios"
Flutter:
@@ -232,6 +230,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
flutter_secure_storage_darwin:
:path: ".symlinks/plugins/flutter_secure_storage_darwin/darwin"
home_widget:
:path: ".symlinks/plugins/home_widget/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
in_app_review:
@@ -240,8 +242,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/open_filex/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
receive_sharing_intent:
:path: ".symlinks/plugins/receive_sharing_intent/ios"
screen_brightness_ios:
:path: ".symlinks/plugins/screen_brightness_ios/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
@@ -252,6 +256,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
video_player_avfoundation:
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
wakelock_plus:
:path: ".symlinks/plugins/wakelock_plus/ios"
workmanager_apple:
:path: ".symlinks/plugins/workmanager_apple/ios"
SPEC CHECKSUMS:
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
@@ -259,40 +269,43 @@ SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc
fast_rsa: fb70897d51040b094c780d5f1d7358614738b879
eraser: 83a4b06985f3702aa3d8dec816f9693266012937
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
firebase_core: f1aafb21c14f497e5498f7ffc4dc63cbb52b2594
firebase_in_app_messaging: 04dfc07ab81578ef83bf0c0229be258ddf287c4f
firebase_messaging: c17a29984eafce4b2997fe078bb0a9e0b06f5dde
FirebaseABTesting: c05b5ec9f1d9f21a65909525de301d375032d9a4
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
FirebaseCoreInternal: d7f5a043c2cd01a08103ab586587c1468047bca6
FirebaseInAppMessaging: 606dd4d4d5590a3d8229f363fdebb485235985b2
FirebaseInstallations: ae9f4902cb5bf1d0c5eaa31ec1f4e5495a0714e2
FirebaseMessaging: d33971b7bb252745ea6cd31ab190d1a1df4b8ed5
Firebase: aa154fee4e9b8eac17aa42344988865b3e857d33
firebase_core: 9156a152117c843440b0b990c785aa0259bc5447
firebase_messaging: 0d962ab44ff24ed36deb8fa2ee043c4671858269
FirebaseCore: 86241206e656f5c80c995e370e6c975913b9b284
FirebaseCoreInternal: 7c12fc3011d889085e765e317d7b9fd1cef97af9
FirebaseInstallations: 4e6e162aa4abaaeeeb01dd00179dfc5ad9c2194e
FirebaseMessaging: 341004946fa7ffc741344b20f1b667514fc93e31
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_app_badge: ca742dd659a157c1090ef7cd881cb78f48f3bcdf
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
flutter_local_notifications: 643a3eda1ce1c0599413ca31672536d423dee214
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
flutter_secure_storage_darwin: acdb3f316ed05a3e68f856e0353b133eec373a23
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
home_widget: f169fc41fd807b4d46ab6615dc44d62adbf9f64f
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
open_filex: 432f3cd11432da3e39f47fcc0df2b1603854eff1
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
PhoneNumberKit: ced55861269312a5e3bc2ef82a58d6255b1c976a
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a
receive_sharing_intent: 222384f00ffe7e952bbfabaa9e3967cb87e5fe00
screen_brightness_ios: 212d950bb99c915eee971c884f4a6c87c92cd13d
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
video_player_avfoundation: 3453f792138786248960ca029747fcd9f318ef52
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
workmanager_apple: 904529ae31e97fc5be632cf628507652294a0778
PODFILE CHECKSUM: e21c9d4c7b9623c73c6784ddc132fd50a603ad93
PODFILE CHECKSUM: 424a9b4c0fe81d8ebeaa9cb0dfedb60a68b19a0d
COCOAPODS: 1.16.2
+514 -14
View File
@@ -7,17 +7,50 @@
objects = {
/* Begin PBXBuildFile section */
034BD2FF7A860C6DC2FED514 /* Pods_Share_Extension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F2428AC5384E0EF8DAB462A /* Pods_Share_Extension.framework */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3321F80F2FB1C00C0011C712 /* Share Extension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 3321F8052FB1C00C0011C712 /* Share Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
33FDB0982EE9ABDC000B2391 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 33FDB0972EE9ABDC000B2391 /* GoogleService-Info.plist */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
AA0101070000000011111111 /* TimetableWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = AA0101020000000011111111 /* TimetableWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
AA0102010000000022222222 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0102020000000022222222 /* SceneDelegate.swift */; };
B8263932DB64B022CCEE7A53 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 90960A132A5F91779B3FBE28 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
3321F80D2FB1C00C0011C712 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 3321F8042FB1C00C0011C712;
remoteInfo = "Share Extension";
};
AA0101080000000011111111 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = AA0101010000000011111111;
remoteInfo = TimetableWidgetExtension;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
3321F8102FB1C00C0011C712 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
3321F80F2FB1C00C0011C712 /* Share Extension.appex in Embed Foundation Extensions */,
AA0101070000000011111111 /* TimetableWidgetExtension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@@ -33,9 +66,12 @@
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3321F8052FB1C00C0011C712 /* Share Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Share Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
33FDB0972EE9ABDC000B2391 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
4509EC31CB08BA9BF367AF6C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
4F2428AC5384E0EF8DAB462A /* Pods_Share_Extension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Share_Extension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
60E1803A3FB28FCC6F435E99 /* Pods-Share Extension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.release.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.release.xcconfig"; sourceTree = "<group>"; };
64801C012A9112D500E8B558 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -48,11 +84,73 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
AA0101020000000011111111 /* TimetableWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TimetableWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
AA0102020000000022222222 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
AA6B03D1433E7395021F7730 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
BB0001010000000011111111 /* ShareExtension-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "ShareExtension-Debug.xcconfig"; path = "Flutter/ShareExtension-Debug.xcconfig"; sourceTree = "<group>"; };
BB0001020000000011111111 /* ShareExtension-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "ShareExtension-Release.xcconfig"; path = "Flutter/ShareExtension-Release.xcconfig"; sourceTree = "<group>"; };
BB0001030000000011111111 /* ShareExtension-Profile.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "ShareExtension-Profile.xcconfig"; path = "Flutter/ShareExtension-Profile.xcconfig"; sourceTree = "<group>"; };
BB0001040000000011111111 /* TimetableWidget-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "TimetableWidget-Debug.xcconfig"; path = "Flutter/TimetableWidget-Debug.xcconfig"; sourceTree = "<group>"; };
BB0001050000000011111111 /* TimetableWidget-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "TimetableWidget-Release.xcconfig"; path = "Flutter/TimetableWidget-Release.xcconfig"; sourceTree = "<group>"; };
BB0001060000000011111111 /* TimetableWidget-Profile.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "TimetableWidget-Profile.xcconfig"; path = "Flutter/TimetableWidget-Profile.xcconfig"; sourceTree = "<group>"; };
C7E1879BE78835C7E3256316 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
DD904D7C0FC0AD11449CEB80 /* Pods-Share Extension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.debug.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.debug.xcconfig"; sourceTree = "<group>"; };
EF5279D9BF8FCBB117AF998E /* Pods-Share Extension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Share Extension.profile.xcconfig"; path = "Target Support Files/Pods-Share Extension/Pods-Share Extension.profile.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
AA01010E0000000011111111 /* Exceptions for "Share Extension" folder in "Share Extension" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 3321F8042FB1C00C0011C712 /* Share Extension */;
};
AA01010F0000000011111111 /* Exceptions for "TimetableWidgetExtension" folder in "TimetableWidgetExtension" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = AA0101010000000011111111 /* TimetableWidgetExtension */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
3321F8062FB1C00C0011C712 /* Share Extension */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
AA01010E0000000011111111 /* Exceptions for "Share Extension" folder in "Share Extension" target */,
);
explicitFileTypes = {
};
explicitFolders = (
);
path = "Share Extension";
sourceTree = "<group>";
};
AA0101030000000011111111 /* TimetableWidgetExtension */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
AA01010F0000000011111111 /* Exceptions for "TimetableWidgetExtension" folder in "TimetableWidgetExtension" target */,
);
explicitFileTypes = {
};
explicitFolders = (
);
path = TimetableWidgetExtension;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
3321F8022FB1C00C0011C712 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
034BD2FF7A860C6DC2FED514 /* Pods_Share_Extension.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -61,6 +159,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
AA0101050000000011111111 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -70,6 +175,9 @@
C7E1879BE78835C7E3256316 /* Pods-Runner.debug.xcconfig */,
AA6B03D1433E7395021F7730 /* Pods-Runner.release.xcconfig */,
4509EC31CB08BA9BF367AF6C /* Pods-Runner.profile.xcconfig */,
DD904D7C0FC0AD11449CEB80 /* Pods-Share Extension.debug.xcconfig */,
60E1803A3FB28FCC6F435E99 /* Pods-Share Extension.release.xcconfig */,
EF5279D9BF8FCBB117AF998E /* Pods-Share Extension.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@@ -78,6 +186,7 @@
isa = PBXGroup;
children = (
90960A132A5F91779B3FBE28 /* Pods_Runner.framework */,
4F2428AC5384E0EF8DAB462A /* Pods_Share_Extension.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -89,6 +198,12 @@
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
BB0001010000000011111111 /* ShareExtension-Debug.xcconfig */,
BB0001020000000011111111 /* ShareExtension-Release.xcconfig */,
BB0001030000000011111111 /* ShareExtension-Profile.xcconfig */,
BB0001040000000011111111 /* TimetableWidget-Debug.xcconfig */,
BB0001050000000011111111 /* TimetableWidget-Release.xcconfig */,
BB0001060000000011111111 /* TimetableWidget-Profile.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
@@ -98,6 +213,8 @@
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
3321F8062FB1C00C0011C712 /* Share Extension */,
AA0101030000000011111111 /* TimetableWidgetExtension */,
97C146EF1CF9000F007C117D /* Products */,
345F4BD4143471FDA71626DE /* Pods */,
731388A08E3B330B216381D0 /* Frameworks */,
@@ -108,6 +225,8 @@
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
3321F8052FB1C00C0011C712 /* Share Extension.appex */,
AA0101020000000011111111 /* TimetableWidgetExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -124,6 +243,7 @@
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
AA0102020000000022222222 /* SceneDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
);
path = Runner;
@@ -132,6 +252,27 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
3321F8042FB1C00C0011C712 /* Share Extension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 3321F8152FB1C00C0011C712 /* Build configuration list for PBXNativeTarget "Share Extension" */;
buildPhases = (
AC0316D13BD5FB74CD9B5223 /* [CP] Check Pods Manifest.lock */,
3321F8012FB1C00C0011C712 /* Sources */,
3321F8022FB1C00C0011C712 /* Frameworks */,
3321F8032FB1C00C0011C712 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
3321F8062FB1C00C0011C712 /* Share Extension */,
);
name = "Share Extension";
productName = "Share Extension";
productReference = 3321F8052FB1C00C0011C712 /* Share Extension.appex */;
productType = "com.apple.product-type.app-extension";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
@@ -142,6 +283,7 @@
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3321F8102FB1C00C0011C712 /* Embed Foundation Extensions */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
174B54D80220E5F588BD9737 /* [CP] Embed Pods Frameworks */,
859FAB4E05FAC31B7B1A62D7 /* [CP] Copy Pods Resources */,
@@ -149,12 +291,34 @@
buildRules = (
);
dependencies = (
3321F80E2FB1C00C0011C712 /* PBXTargetDependency */,
AA0101090000000011111111 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
AA0101010000000011111111 /* TimetableWidgetExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = AA01010D0000000011111111 /* Build configuration list for PBXNativeTarget "TimetableWidgetExtension" */;
buildPhases = (
AA0101040000000011111111 /* Sources */,
AA0101050000000011111111 /* Frameworks */,
AA0101060000000011111111 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
AA0101030000000011111111 /* TimetableWidgetExtension */,
);
name = TimetableWidgetExtension;
productName = TimetableWidgetExtension;
productReference = AA0101020000000011111111 /* TimetableWidgetExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -162,13 +326,20 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 2610;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
3321F8042FB1C00C0011C712 = {
CreatedOnToolsVersion = 26.1.1;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
AA0101010000000011111111 = {
CreatedOnToolsVersion = 26.1.1;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
@@ -185,11 +356,20 @@
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
3321F8042FB1C00C0011C712 /* Share Extension */,
AA0101010000000011111111 /* TimetableWidgetExtension */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
3321F8032FB1C00C0011C712 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -202,6 +382,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
AA0101060000000011111111 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@@ -213,14 +400,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
@@ -250,14 +433,10 @@
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
@@ -278,6 +457,28 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
AC0316D13BD5FB74CD9B5223 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Share Extension-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
EE78ADC5E762D17A29097E92 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
@@ -303,17 +504,45 @@
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
3321F8012FB1C00C0011C712 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
AA0102010000000022222222 /* SceneDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
AA0101040000000011111111 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
3321F80E2FB1C00C0011C712 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 3321F8042FB1C00C0011C712 /* Share Extension */;
targetProxy = 3321F80D2FB1C00C0011C712 /* PBXContainerItemProxy */;
};
AA0101090000000011111111 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = AA0101010000000011111111 /* TimetableWidgetExtension */;
targetProxy = AA0101080000000011111111 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
@@ -375,7 +604,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -394,6 +623,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CUSTOM_GROUP_ID = group.eu.mhsl.marianum.mobile.client.share;
DEVELOPMENT_TEAM = MY55VF3KPG;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -403,7 +633,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = "${FLUTTER_BUILD_NAME)";
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
PRODUCT_BUNDLE_IDENTIFIER = eu.mhsl.marianum.mobile.client;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -413,6 +643,132 @@
};
name = Profile;
};
3321F8112FB1C00C0011C712 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = BB0001010000000011111111 /* ShareExtension-Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CUSTOM_GROUP_ID = group.eu.mhsl.marianum.mobile.client.share;
DEVELOPMENT_TEAM = MY55VF3KPG;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
INFOPLIST_FILE = "Share Extension/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "eu.mhsl.marianum.mobile.client.Share-Extension";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
3321F8122FB1C00C0011C712 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = BB0001020000000011111111 /* ShareExtension-Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CUSTOM_GROUP_ID = group.eu.mhsl.marianum.mobile.client.share;
DEVELOPMENT_TEAM = MY55VF3KPG;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
INFOPLIST_FILE = "Share Extension/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "eu.mhsl.marianum.mobile.client.Share-Extension";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
3321F8132FB1C00C0011C712 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = BB0001030000000011111111 /* ShareExtension-Profile.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = "Share Extension/Share Extension.entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CUSTOM_GROUP_ID = group.eu.mhsl.marianum.mobile.client.share;
DEVELOPMENT_TEAM = MY55VF3KPG;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
INFOPLIST_FILE = "Share Extension/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "eu.mhsl.marianum.mobile.client.Share-Extension";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -460,7 +816,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -509,7 +865,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -530,6 +886,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CUSTOM_GROUP_ID = group.eu.mhsl.marianum.mobile.client.share;
DEVELOPMENT_TEAM = MY55VF3KPG;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -539,7 +896,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = "${FLUTTER_BUILD_NAME)";
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
PRODUCT_BUNDLE_IDENTIFIER = eu.mhsl.marianum.mobile.client;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -560,6 +917,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
CUSTOM_GROUP_ID = group.eu.mhsl.marianum.mobile.client.share;
DEVELOPMENT_TEAM = MY55VF3KPG;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -569,7 +927,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = "${FLUTTER_BUILD_NAME)";
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
PRODUCT_BUNDLE_IDENTIFIER = eu.mhsl.marianum.mobile.client;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -579,9 +937,141 @@
};
name = Release;
};
AA01010A0000000011111111 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = BB0001040000000011111111 /* TimetableWidget-Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = TimetableWidgetExtension/TimetableWidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = MY55VF3KPG;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
INFOPLIST_FILE = TimetableWidgetExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = eu.mhsl.marianum.mobile.client.TimetableWidgetExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
AA01010B0000000011111111 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = BB0001050000000011111111 /* TimetableWidget-Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = TimetableWidgetExtension/TimetableWidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = MY55VF3KPG;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
INFOPLIST_FILE = TimetableWidgetExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = eu.mhsl.marianum.mobile.client.TimetableWidgetExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
AA01010C0000000011111111 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = BB0001060000000011111111 /* TimetableWidget-Profile.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = TimetableWidgetExtension/TimetableWidgetExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = MY55VF3KPG;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
INFOPLIST_FILE = TimetableWidgetExtension/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 16.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = eu.mhsl.marianum.mobile.client.TimetableWidgetExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SKIP_INSTALL = YES;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Profile;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
3321F8152FB1C00C0011C712 /* Build configuration list for PBXNativeTarget "Share Extension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
3321F8112FB1C00C0011C712 /* Debug */,
3321F8122FB1C00C0011C712 /* Release */,
3321F8132FB1C00C0011C712 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@@ -602,6 +1092,16 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
AA01010D0000000011111111 /* Build configuration list for PBXNativeTarget "TimetableWidgetExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
AA01010A0000000011111111 /* Debug */,
AA01010B0000000011111111 /* Release */,
AA01010C0000000011111111 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2610"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3321F8042FB1C00C0011C712"
BuildableName = "Share Extension.appex"
BlueprintName = "Share Extension"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
@@ -0,0 +1,96 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "2610"
wasCreatedForAppExtension = "YES"
version = "2.0">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"
buildArchitectures = "Automatic">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "AA0101010000000011111111"
BuildableName = "TimetableWidgetExtension.appex"
BlueprintName = "TimetableWidgetExtension"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = ""
selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
launchStyle = "0"
askForAppToLaunch = "Yes"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES"
launchAutomaticallySubstyle = "2">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+6 -3
View File
@@ -1,13 +1,16 @@
import UIKit
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
}
+38 -2
View File
@@ -1,7 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<dict>
<key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
@@ -22,12 +24,46 @@
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Um Fotos direkt aus der App aufnehmen und teilen zu können wird Zugriff auf die Kamera benötigt.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Um Medien mit anderen zu teilen wird Zugriff zu deine Dateien benötigt.</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
@@ -50,5 +86,5 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
</dict>
</dict>
</plist>
+5
View File
@@ -4,5 +4,10 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.eu.mhsl.marianum.mobile.client.widget</string>
<string>group.eu.mhsl.marianum.mobile.client.share</string>
</array>
</dict>
</plist>
+33
View File
@@ -0,0 +1,33 @@
import Flutter
import UIKit
import receive_sharing_intent
// FlutterSceneDelegate has a fallback that forwards URL events to plugins
// registered via addApplicationDelegate, but the fallback is best-effort and
// has not always fired in our setup. This subclass forwards URLs explicitly
// to receive_sharing_intent so cold-start and warm shares both reach Dart.
class SceneDelegate: FlutterSceneDelegate {
override func scene(
_ scene: UIScene,
willConnectTo session: UISceneSession,
options connectionOptions: UIScene.ConnectionOptions
) {
super.scene(scene, willConnectTo: session, options: connectionOptions)
for context in connectionOptions.urlContexts {
_ = SwiftReceiveSharingIntentPlugin.instance.application(
UIApplication.shared,
didFinishLaunchingWithOptions: [UIApplication.LaunchOptionsKey.url: context.url]
)
}
}
override func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
for context in URLContexts {
_ = SwiftReceiveSharingIntentPlugin.instance.application(
UIApplication.shared,
open: context.url,
options: [:]
)
}
}
}
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="j1y-V4-xli">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22504"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Share View Controller-->
<scene sceneID="ceB-am-kn3">
<objects>
<viewController id="j1y-V4-xli" customClass="ShareViewController" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" opaque="NO" contentMode="scaleToFill" id="wbc-yd-nQP">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="1Xd-am-t49"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="CEy-Cv-SGf" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
+54
View File
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Marianum Fulda</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>PHSupportedMediaTypes</key>
<array>
<string>Video</string>
<string>Image</string>
</array>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsText</key>
<true/>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<integer>1</integer>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<integer>10</integer>
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
<integer>10</integer>
<key>NSExtensionActivationSupportsFileWithMaxCount</key>
<integer>10</integer>
</dict>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
</dict>
</dict>
</plist>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.eu.mhsl.marianum.mobile.client.share</string>
</array>
</dict>
</plist>
@@ -0,0 +1,240 @@
import UIKit
import UniformTypeIdentifiers
import AVFoundation
// Datenmodell muss byte-für-byte zu dem passen, was
// SwiftReceiveSharingIntentPlugin auf der Host-App-Seite decodiert.
private enum SharedMediaType: String, Codable {
case image, video, text, file, url
}
private struct SharedMediaFile: Codable {
let path: String
let mimeType: String?
let thumbnail: String?
let duration: Double?
let message: String?
let type: SharedMediaType
}
final class ShareViewController: UIViewController {
// Schlüssel sind die, die das Plugin liest.
private let userDefaultsKey = "ShareKey"
private let userDefaultsMessageKey = "ShareMessageKey"
private let urlSchemePrefix = "ShareMedia"
private var appGroupId = ""
private var hostBundleId = ""
override func viewDidLoad() {
super.viewDidLoad()
resolveIds()
setupPlaceholderUI()
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
processAttachments()
}
private func resolveIds() {
let extBundleId = Bundle.main.bundleIdentifier ?? ""
if let lastDot = extBundleId.lastIndex(of: ".") {
hostBundleId = String(extBundleId[..<lastDot])
}
let custom = Bundle.main.object(forInfoDictionaryKey: "AppGroupId") as? String
if let custom, !custom.isEmpty, !custom.contains("$(") {
appGroupId = custom
} else {
appGroupId = "group.\(hostBundleId)"
}
}
private func setupPlaceholderUI() {
view.backgroundColor = .systemBackground
let spinner = UIActivityIndicatorView(style: .medium)
spinner.startAnimating()
let label = UILabel()
label.text = "Wird geteilt …"
label.font = .preferredFont(forTextStyle: .footnote)
label.textColor = .secondaryLabel
let stack = UIStackView(arrangedSubviews: [spinner, label])
stack.axis = .vertical
stack.alignment = .center
stack.spacing = 8
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
NSLayoutConstraint.activate([
stack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
stack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
])
}
private func processAttachments() {
let attachments: [NSItemProvider] = (extensionContext?.inputItems ?? [])
.compactMap { $0 as? NSExtensionItem }
.flatMap { $0.attachments ?? [] }
guard !attachments.isEmpty else {
finish()
return
}
let group = DispatchGroup()
var collected: [SharedMediaFile] = []
let lock = NSLock()
for provider in attachments {
group.enter()
handle(provider: provider) { file in
if let f = file {
lock.lock(); collected.append(f); lock.unlock()
}
group.leave()
}
}
group.notify(queue: .main) { [weak self] in
self?.saveAndRedirect(items: collected)
}
}
// Reihenfolge entspricht SharedMediaType.allCases im Plugin
// spezifische Typen (image, video) vor generischen (file, url).
private func handle(provider: NSItemProvider,
completion: @escaping (SharedMediaFile?) -> Void) {
let order: [(String, SharedMediaType)] = [
(UTType.image.identifier, .image),
(UTType.movie.identifier, .video),
(UTType.text.identifier, .text),
(UTType.fileURL.identifier, .file),
(UTType.url.identifier, .url),
(UTType.data.identifier, .file),
]
for (typeId, kind) in order {
if provider.hasItemConformingToTypeIdentifier(typeId) {
provider.loadItem(forTypeIdentifier: typeId) { [weak self] data, error in
guard let self else { completion(nil); return }
if error != nil { completion(nil); return }
completion(self.toSharedFile(data: data, kind: kind))
}
return
}
}
completion(nil)
}
private func toSharedFile(data: Any?, kind: SharedMediaType) -> SharedMediaFile? {
switch kind {
case .text:
guard let s = data as? String else { return nil }
return SharedMediaFile(path: s, mimeType: "text/plain",
thumbnail: nil, duration: nil, message: nil, type: .text)
case .url:
guard let u = data as? URL else { return nil }
return SharedMediaFile(path: u.absoluteString, mimeType: nil,
thumbnail: nil, duration: nil, message: nil, type: .url)
case .image:
if let u = data as? URL, let dst = copyIntoAppGroup(src: u) {
return SharedMediaFile(path: pathString(for: dst), mimeType: mime(for: u),
thumbnail: nil, duration: nil, message: nil, type: .image)
}
if let img = data as? UIImage, let dst = writePng(image: img) {
return SharedMediaFile(path: pathString(for: dst), mimeType: "image/png",
thumbnail: nil, duration: nil, message: nil, type: .image)
}
return nil
case .video:
guard let u = data as? URL, let dst = copyIntoAppGroup(src: u) else { return nil }
return SharedMediaFile(path: pathString(for: dst), mimeType: mime(for: u),
thumbnail: nil, duration: videoDurationMs(url: u),
message: nil, type: .video)
case .file:
guard let u = data as? URL, let dst = copyIntoAppGroup(src: u) else { return nil }
return SharedMediaFile(path: pathString(for: dst), mimeType: mime(for: u),
thumbnail: nil, duration: nil, message: nil, type: .file)
}
}
private func copyIntoAppGroup(src: URL) -> URL? {
guard let container = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: appGroupId) else {
return nil
}
let name = src.lastPathComponent.isEmpty ? UUID().uuidString : src.lastPathComponent
let dst = container.appendingPathComponent(name)
do {
if FileManager.default.fileExists(atPath: dst.path) {
try FileManager.default.removeItem(at: dst)
}
try FileManager.default.copyItem(at: src, to: dst)
return dst
} catch {
return nil
}
}
private func writePng(image: UIImage) -> URL? {
guard let container = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: appGroupId),
let data = image.pngData() else { return nil }
let dst = container.appendingPathComponent("\(UUID().uuidString).png")
return (try? data.write(to: dst)) != nil ? dst : nil
}
// Das Plugin macht `path.replacingOccurrences(of: "file://", with: "")`,
// also liefern wir absoluteString mit Prozent-Decoding selbes Format wie
// im Original-RSIShareViewController.
private func pathString(for url: URL) -> String {
url.absoluteString.removingPercentEncoding ?? url.absoluteString
}
private func mime(for url: URL) -> String? {
UTType(filenameExtension: url.pathExtension)?.preferredMIMEType
}
private func videoDurationMs(url: URL) -> Double {
(CMTimeGetSeconds(AVURLAsset(url: url).duration) * 1000).rounded()
}
private func saveAndRedirect(items: [SharedMediaFile]) {
guard !items.isEmpty else {
finish()
return
}
let defaults = UserDefaults(suiteName: appGroupId)
guard let encoded = try? JSONEncoder().encode(items) else {
finish()
return
}
defaults?.set(encoded, forKey: userDefaultsKey)
defaults?.removeObject(forKey: userDefaultsMessageKey)
let urlStr = "\(urlSchemePrefix)-\(hostBundleId):share"
guard let url = URL(string: urlStr) else {
finish()
return
}
// Apple-DTS says Share Extensions are not officially allowed to open
// URLs. Both `extensionContext.open` and the responder-chain trick
// succeed in some iOS versions and fail silently in others, so we fire
// both in parallel and let whichever Apple is honouring this release
// win the race.
extensionContext?.open(url, completionHandler: nil)
var responder: UIResponder? = self
while responder != nil {
if let app = responder as? UIApplication {
app.open(url, options: [:], completionHandler: nil)
break
}
responder = responder?.next
}
// Brief window so the open request reaches the system before we tear
// the extension down.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
self?.finish()
}
}
private func finish() {
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
}
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,16 @@
{
"images" : [
{
"filename" : "marianum_m_white.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -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

+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Marianum Stundenplan</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>
@@ -0,0 +1,29 @@
import SwiftUI
/// Marianum-M peeking out of the bottom-right corner. Sized to the longer
/// widget edge so it scales with the picked WidgetFamily; offset nudges a
/// sliver behind the edge. Opacity comes from the widget palette so the
/// watermark matches Android's `watermarkAlpha` at the same brightness.
struct MarianumWatermark: View {
@Environment(\.widgetPalette) private var palette
var body: some View {
GeometryReader { geo in
let markSize = min(400, max(160, max(geo.size.width, geo.size.height) * 0.8))
let offsetX = markSize * 0.18
let offsetY = markSize * 0.18
ZStack(alignment: .bottomTrailing) {
Color.clear
Image("marianum_m")
.resizable()
.renderingMode(.template)
.aspectRatio(contentMode: .fit)
.foregroundStyle(palette.textPrimary)
.frame(width: markSize, height: markSize)
.opacity(palette.watermarkOpacity)
.offset(x: offsetX, y: offsetY)
}
}
.clipped()
}
}
@@ -0,0 +1,424 @@
import SwiftUI
import WidgetKit
// Layout constants must mirror WidgetRenderer.kt on Android, otherwise
// the platforms drift apart on the same widget size.
let FALLBACK_VIRTUAL_MINUTES = 11 * 60
let MIN_HOUR_HEIGHT: CGFloat = 18
let MAX_HOUR_HEIGHT: CGFloat = 72
let MIN_BLOCK_HEIGHT: CGFloat = 16
let LESSON_GAP: CGFloat = 1.5
func realMinutesToVirtual(_ realMin: Int, periods: [WidgetPeriod]) -> CGFloat {
guard !periods.isEmpty else { return CGFloat(realMin) }
for p in periods where realMin >= p.startMinutes && realMin <= p.endMinutes {
return CGFloat(p.virtualStartMinutes + (realMin - p.startMinutes))
}
let first = periods.first!
if realMin < first.startMinutes {
return CGFloat(realMin - first.startMinutes + first.virtualStartMinutes)
}
let last = periods.last!
if realMin > last.endMinutes {
return CGFloat(last.virtualEndMinutes + (realMin - last.endMinutes))
}
var prev = first
for i in 1..<periods.count {
let curr = periods[i]
if realMin > prev.endMinutes && realMin < curr.startMinutes {
let gap = curr.startMinutes - prev.endMinutes
let virtualGap = curr.virtualStartMinutes - prev.virtualEndMinutes
if gap > 0 {
return CGFloat(prev.virtualEndMinutes) +
CGFloat(realMin - prev.endMinutes) * CGFloat(virtualGap) / CGFloat(gap)
}
return CGFloat(curr.virtualStartMinutes)
}
prev = curr
}
return 0
}
let BLOCK_SHOW_ROOM_MIN: CGFloat = 18
let BLOCK_SHOW_TEACHER_SEPARATE_MIN: CGFloat = 30
let MIN_SUBJECT_FONT: CGFloat = 9
let MAX_SUBJECT_FONT: CGFloat = 14
let MIN_SECONDARY_FONT: CGFloat = 7
func subjectFont(forHourHeight hourHeight: CGFloat) -> CGFloat {
let t = max(0, min(1, (hourHeight - MIN_HOUR_HEIGHT) / (MAX_HOUR_HEIGHT - MIN_HOUR_HEIGHT)))
return MIN_SUBJECT_FONT + t * (MAX_SUBJECT_FONT - MIN_SUBJECT_FONT)
}
struct TimetableDayView: View {
let entry: TimetableEntry
@Environment(\.widgetPalette) private var palette
var body: some View {
ZStack {
if !entry.isLoggedIn {
placeholder("Bitte einloggen, um den Stundenplan zu laden")
} else if let data = entry.data {
content(data: data)
} else {
placeholder("Lade…")
}
}
.background(MarianumWatermark())
}
@ViewBuilder
private func content(data: WidgetTimetableData) -> some View {
VStack(alignment: .leading, spacing: 6) {
header(data: data)
if data.isHoliday {
emptyState(text: data.holidayName ?? "Ferien")
} else if data.lessons.isEmpty {
emptyState(text: "Keine Stunden")
} else {
GeometryReader { geo in
let totalMin = CGFloat(data.periods.last?.virtualEndMinutes ?? FALLBACK_VIRTUAL_MINUTES)
TimeGridView(
lessons: data.lessons,
periods: data.periods,
hourHeight: max(
MIN_HOUR_HEIGHT,
min(MAX_HOUR_HEIGHT, geo.size.height / max(totalMin, 60) * 60)
),
showRoom: true,
showTeacher: true,
showTimeLabels: true
)
}
}
}
}
private func header(data: WidgetTimetableData) -> some View {
HStack {
Text(dayLabel(for: data.anchorDate))
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(palette.textPrimary)
Spacer()
Text("Stand: \(freshnessLabel(for: data.fetchedAt))")
.font(.system(size: 10))
.foregroundStyle(palette.textSecondary)
}
}
private func emptyState(text: String) -> some View {
VStack {
Spacer()
Text(text)
.font(.caption)
.foregroundStyle(palette.textSecondary)
Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
private func placeholder(_ message: String) -> some View {
VStack(spacing: 4) {
Text("Marianum Stundenplan")
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(palette.textPrimary)
Text(message)
.font(.caption)
.foregroundStyle(palette.textSecondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct TimeGridView: View {
let lessons: [WidgetLesson]
let periods: [WidgetPeriod]
let hourHeight: CGFloat
let showRoom: Bool
let showTeacher: Bool
let showTimeLabels: Bool
/// Week-widget passes 3 for narrow columns; day-widget keeps 7.
var horizontalPadding: CGFloat = 7
@Environment(\.widgetPalette) private var palette
private var totalVirtualMinutes: Int {
periods.last?.virtualEndMinutes ?? FALLBACK_VIRTUAL_MINUTES
}
private var totalHeight: CGFloat {
CGFloat(totalVirtualMinutes) * hourHeight / 60.0
}
/// Below this per-hour height the two-line label collapses to a single
/// period number time + number overlap otherwise.
private var compactLabels: Bool { hourHeight < 26 }
var body: some View {
HStack(alignment: .top, spacing: 0) {
if showTimeLabels {
// 28pt matches the Week widget's time-label column so the
// visual rhythm is identical across both widgets.
timeLabelsColumn
.frame(width: 28, alignment: .topTrailing)
Rectangle()
.fill(palette.divider)
.frame(width: 1)
}
ZStack(alignment: .top) {
gridLines
breakBlocks
ForEach(lessons.indices, id: \.self) { idx in
lessonBlock(lessons[idx])
}
}
.frame(maxWidth: .infinity, minHeight: totalHeight, alignment: .top)
}
}
private var timeLabelsColumn: some View {
ZStack(alignment: .topTrailing) {
ForEach(periodBoundaries(periods), id: \.self) { virtualMin in
Rectangle()
.fill(palette.divider)
.frame(height: 1)
.offset(y: CGFloat(virtualMin) * hourHeight / 60.0)
}
ForEach(periods, id: \.startMinutes) { period in
VStack(alignment: .trailing, spacing: -2) {
if compactLabels {
Text("\(period.name).")
.font(.system(size: 9, weight: .bold))
.foregroundStyle(palette.textPrimary)
.lineLimit(1)
} else {
Text(formatHm(period.startMinutes))
.font(.system(size: 9))
.foregroundStyle(palette.textPrimary)
.lineLimit(1)
Text("\(period.name).")
.font(.system(size: 7, weight: .bold))
.foregroundStyle(palette.textSecondary)
.lineLimit(1)
}
}
.padding(.trailing, 4)
.offset(y: CGFloat(period.virtualStartMinutes) * hourHeight / 60.0)
}
}
.frame(height: totalHeight, alignment: .topTrailing)
}
private var gridLines: some View {
ZStack(alignment: .top) {
ForEach(periodBoundaries(periods), id: \.self) { virtualMin in
Rectangle()
.fill(palette.divider)
.frame(height: 1)
.offset(y: CGFloat(virtualMin) * hourHeight / 60.0)
}
}
.frame(height: totalHeight)
}
private var breakBlocks: some View {
ZStack(alignment: .top) {
ForEach(0..<max(0, periods.count - 1), id: \.self) { i in
let curr = periods[i]
let next = periods[i + 1]
let virtualGap = next.virtualStartMinutes - curr.virtualEndMinutes
if virtualGap > 0 {
Rectangle()
.fill(palette.breakBlock)
.frame(height: CGFloat(virtualGap) * hourHeight / 60.0)
.padding(.horizontal, 1)
.offset(y: CGFloat(curr.virtualEndMinutes) * hourHeight / 60.0)
}
}
}
.frame(height: totalHeight)
}
private func formatHm(_ minutes: Int) -> String {
String(format: "%02d:%02d", minutes / 60, minutes % 60)
}
@ViewBuilder
private func lessonBlock(_ lesson: WidgetLesson) -> some View {
let cal = Calendar.current
let comps = cal.dateComponents([.hour, .minute], from: lesson.start)
let startMinutes = (comps.hour ?? 0) * 60 + (comps.minute ?? 0)
let durationMinutes = max(15, Int(lesson.end.timeIntervalSince(lesson.start) / 60))
let virtualStart = realMinutesToVirtual(startMinutes, periods: periods)
let virtualEnd = realMinutesToVirtual(startMinutes + durationMinutes, periods: periods)
if virtualEnd > virtualStart {
let top = virtualStart * hourHeight / 60.0 + LESSON_GAP / 2
let height = max(
MIN_BLOCK_HEIGHT,
(virtualEnd - virtualStart) * hourHeight / 60.0 - LESSON_GAP
)
let subjectSize = subjectFont(forHourHeight: hourHeight)
let secondarySize = max(MIN_SECONDARY_FONT, subjectSize - 2)
let room = lesson.room
let teacher = lesson.teacher ?? lesson.originalTeacher
let hasSecondary = (room?.isEmpty == false) || (teacher?.isEmpty == false)
HStack(alignment: .top, spacing: 4) {
Text(subjectLabel(lesson))
.font(.system(size: subjectSize, weight: .semibold))
.foregroundStyle(.white)
.lineLimit(hasSecondary ? 1 : 2)
.minimumScaleFactor(0.5)
if hasSecondary {
Spacer(minLength: 0)
VStack(alignment: .trailing, spacing: -1) {
if showRoom && height >= BLOCK_SHOW_ROOM_MIN {
if let room, !room.isEmpty {
Text(room)
.font(.system(size: secondarySize))
.foregroundStyle(.white.opacity(0.85))
.lineLimit(1)
.minimumScaleFactor(0.5)
}
if showTeacher,
height >= BLOCK_SHOW_TEACHER_SEPARATE_MIN,
let teacher,
!teacher.isEmpty {
Text(teacher)
.font(.system(size: secondarySize))
.foregroundStyle(.white.opacity(0.7))
.lineLimit(1)
.minimumScaleFactor(0.5)
}
}
}
}
}
.padding(.horizontal, horizontalPadding)
.padding(.vertical, 3)
.frame(maxWidth: .infinity, alignment: .topLeading)
.frame(height: height, alignment: .topLeading)
.background(blockColor(lesson))
.cornerRadius(6)
.overlay(alignment: .bottomLeading) {
// Separate fixed-size badge so the +N hint stays readable
// when the subject autoshrinks on narrow tiles.
if let count = lesson.siblingCount, count > 0 {
Text("+\(count)")
.font(.system(size: 12, weight: .bold))
.foregroundStyle(.white)
.padding(.leading, horizontalPadding)
.padding(.bottom, 2)
}
}
.overlay {
// CrossPainter parity: clip cross to the rounded shape so
// the diagonals don't bleed past the corners.
if lesson.status == .cancelled {
ZStack {
RoundedRectangle(cornerRadius: 6)
.stroke(Color.red.opacity(0.78), lineWidth: 1.5)
GeometryReader { geo in
Path { p in
p.move(to: .zero)
p.addLine(to: CGPoint(x: geo.size.width, y: geo.size.height))
p.move(to: CGPoint(x: geo.size.width, y: 0))
p.addLine(to: CGPoint(x: 0, y: geo.size.height))
}
.stroke(Color.red.opacity(0.78), lineWidth: 3)
}
}
.clipShape(RoundedRectangle(cornerRadius: 6))
}
}
.padding(.horizontal, showRoom ? 2 : 1)
.offset(y: top)
}
}
private func subjectLabel(_ lesson: WidgetLesson) -> String {
!lesson.subjectShort.isEmpty
? lesson.subjectShort
: (lesson.subjectLong ?? "")
}
/// Mirrors lesson_color.dart + custom_event_colors.dart so the widget
/// matches the in-app calendar exactly.
private func blockColor(_ lesson: WidgetLesson) -> Color {
if lesson.status == .event, let custom = lesson.customColor {
switch custom {
case "orange": return Color(red: 239/255.0, green: 108/255.0, blue: 0/255.0)
case "red": return Color(red: 153/255.0, green: 51/255.0, blue: 51/255.0)
case "green": return Color(red: 76/255.0, green: 175/255.0, blue: 80/255.0)
case "blue": return Color(red: 33/255.0, green: 150/255.0, blue: 243/255.0)
default: break
}
}
switch lesson.status {
case .regular, .past: return Color(red: 153/255.0, green: 51/255.0, blue: 51/255.0)
case .ongoing: return Color(red: 200/255.0, green: 51/255.0, blue: 51/255.0)
case .cancelled: return .black
case .irregular: return Color(red: 143/255.0, green: 25/255.0, blue: 179/255.0)
case .teacherChanged: return Color(red: 41/255.0, green: 99/255.0, blue: 155/255.0)
case .event: return Color(red: 239/255.0, green: 108/255.0, blue: 0/255.0)
}
}
}
/// Period boundaries deduped: adjacent periods share a line, periods on
/// either side of a break get their own (bracketing the break block).
func periodBoundaries(_ periods: [WidgetPeriod]) -> [Int] {
var seen = Set<Int>()
var result: [Int] = []
for p in periods {
for v in [p.virtualStartMinutes, p.virtualEndMinutes] {
if seen.insert(v).inserted { result.append(v) }
}
}
return result.sorted()
}
func dayLabel(for date: Date) -> String {
let cal = Calendar.current
let today = cal.startOfDay(for: Date())
let anchor = cal.startOfDay(for: date)
if anchor == today {
return "Heute · \(shortDate(date))"
}
if let tomorrow = cal.date(byAdding: .day, value: 1, to: today), anchor == tomorrow {
return "Morgen · \(shortDate(date))"
}
let formatter = DateFormatter()
formatter.locale = Locale(identifier: "de_DE")
formatter.dateFormat = "EEEE · dd.MM."
return formatter.string(from: date)
}
func shortDate(_ date: Date) -> String {
let f = DateFormatter()
f.locale = Locale(identifier: "de_DE")
f.dateFormat = "dd.MM."
return f.string(from: date)
}
func freshnessLabel(for fetchedAt: Date) -> String {
let cal = Calendar.current
let today = cal.startOfDay(for: Date())
let fetchedDay = cal.startOfDay(for: fetchedAt)
let timeFmt = DateFormatter()
timeFmt.locale = Locale(identifier: "de_DE")
timeFmt.dateFormat = "HH:mm"
if fetchedDay == today {
return timeFmt.string(from: fetchedAt)
}
if let yesterday = cal.date(byAdding: .day, value: -1, to: today),
fetchedDay == yesterday {
return "gestern \(timeFmt.string(from: fetchedAt))"
}
let dateTimeFmt = DateFormatter()
dateTimeFmt.locale = Locale(identifier: "de_DE")
dateTimeFmt.dateFormat = "dd.MM. HH:mm"
return dateTimeFmt.string(from: fetchedAt)
}
@@ -0,0 +1,169 @@
import SwiftUI
import WidgetKit
struct TimetableWeekView: View {
let entry: TimetableEntry
@Environment(\.widgetPalette) private var palette
var body: some View {
ZStack {
if !entry.isLoggedIn {
placeholder("Bitte einloggen, um den Stundenplan zu laden")
} else if let data = entry.data {
content(data: data)
} else {
placeholder("Lade…")
}
}
.background(MarianumWatermark())
}
@ViewBuilder
private func content(data: WidgetTimetableData) -> some View {
VStack(alignment: .leading, spacing: 4) {
header(data: data)
dayHeaderRow(data: data)
GeometryReader { geo in
let totalMin = CGFloat(data.periods.last?.virtualEndMinutes ?? FALLBACK_VIRTUAL_MINUTES)
let hourHeight = max(
MIN_HOUR_HEIGHT,
min(MAX_HOUR_HEIGHT, geo.size.height / max(totalMin, 60) * 60)
)
let dayColumnWidth = (geo.size.width - 28 - 4) / 5
let subjectOnly = dayColumnWidth < 70
HStack(alignment: .top, spacing: 0) {
timeLabelsColumn(hourHeight: hourHeight, periods: data.periods)
.frame(width: 28, alignment: .topTrailing)
columnDivider
ForEach(0..<5, id: \.self) { offset in
column(
data: data,
offset: offset,
hourHeight: hourHeight,
subjectOnly: subjectOnly
)
.frame(maxWidth: .infinity)
if offset < 4 { columnDivider }
}
}
}
}
}
private var columnDivider: some View {
Rectangle()
.fill(palette.divider)
.frame(width: 1)
}
private func header(data: WidgetTimetableData) -> some View {
let cal = Calendar.current
let week = cal.component(.weekOfYear, from: data.anchorDate)
let endDate = cal.date(byAdding: .day, value: 4, to: data.anchorDate) ?? data.anchorDate
return HStack {
Text("KW \(week) · \(shortDate(data.anchorDate))\(shortDate(endDate))")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(palette.textPrimary)
Spacer()
Text("Stand: \(freshnessLabel(for: data.fetchedAt))")
.font(.system(size: 10))
.foregroundStyle(palette.textSecondary)
}
}
private func dayHeaderRow(data: WidgetTimetableData) -> some View {
let cal = Calendar.current
// `.frame(maxWidth: .infinity)` is critical: without an explicit
// greedy width (or a true `Spacer()` inside), this HStack collapses
// to the sum of its fixed-width children in a `.leading` VStack
// and the 5 day-columns end up sharing ~5pt of width the labels
// crunch to the left and stop aligning with the grid columns below.
// The fixed height keeps `columnDivider`'s vertically-flexible
// Rectangle from stealing space from the GeometryReader.
return HStack(spacing: 0) {
Spacer().frame(width: 28)
columnDivider
ForEach(0..<5, id: \.self) { offset in
let day = cal.date(byAdding: .day, value: offset, to: data.anchorDate) ?? data.anchorDate
VStack(spacing: 0) {
Text(weekday(for: day))
.font(.system(size: 11, weight: .bold))
.foregroundStyle(palette.textPrimary)
Text(shortDate(day))
.font(.system(size: 9))
.foregroundStyle(palette.textSecondary)
}
.frame(maxWidth: .infinity)
if offset < 4 { columnDivider }
}
}
.frame(maxWidth: .infinity, minHeight: 26, maxHeight: 26)
}
private func timeLabelsColumn(hourHeight: CGFloat, periods: [WidgetPeriod]) -> some View {
let totalMin = periods.last?.virtualEndMinutes ?? FALLBACK_VIRTUAL_MINUTES
let totalHeight = CGFloat(totalMin) * hourHeight / 60.0
return ZStack(alignment: .topTrailing) {
ForEach(periodBoundaries(periods), id: \.self) { virtualMin in
Rectangle()
.fill(palette.divider)
.frame(height: 1)
.offset(y: CGFloat(virtualMin) * hourHeight / 60.0)
}
ForEach(periods, id: \.startMinutes) { period in
VStack(alignment: .trailing, spacing: -2) {
Text(String(format: "%02d:%02d", period.startMinutes / 60, period.startMinutes % 60))
.font(.system(size: 8))
.foregroundStyle(palette.textPrimary)
.lineLimit(1)
Text("\(period.name).")
.font(.system(size: 6, weight: .bold))
.foregroundStyle(palette.textSecondary)
.lineLimit(1)
}
.offset(y: CGFloat(period.virtualStartMinutes) * hourHeight / 60.0)
}
}
.frame(height: totalHeight, alignment: .topTrailing)
}
private func column(
data: WidgetTimetableData,
offset: Int,
hourHeight: CGFloat,
subjectOnly: Bool
) -> some View {
let cal = Calendar.current
let day = cal.date(byAdding: .day, value: offset, to: data.anchorDate) ?? data.anchorDate
let lessonsForDay = data.lessons.filter { cal.isDate($0.start, inSameDayAs: day) }
return TimeGridView(
lessons: lessonsForDay,
periods: data.periods,
hourHeight: hourHeight,
showRoom: !subjectOnly,
showTeacher: !subjectOnly,
showTimeLabels: false,
horizontalPadding: 3
)
}
private func weekday(for date: Date) -> String {
let f = DateFormatter()
f.locale = Locale(identifier: "de_DE")
f.dateFormat = "EE"
return f.string(from: date)
}
private func placeholder(_ message: String) -> some View {
VStack(spacing: 4) {
Text("Marianum Stundenplan")
.font(.system(size: 14, weight: .semibold))
.foregroundStyle(palette.textPrimary)
Text(message)
.font(.caption)
.foregroundStyle(palette.textSecondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.eu.mhsl.marianum.mobile.client.widget</string>
</array>
</dict>
</plist>
@@ -0,0 +1,164 @@
import SwiftUI
import WidgetKit
@main
struct MarianumWidgetBundle: WidgetBundle {
var body: some Widget {
TimetableDayWidget()
TimetableWeekWidget()
}
}
// MARK: - Day widget
struct TimetableDayWidget: Widget {
let kind: String = "TimetableDayWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: TimetableDayProvider()) { entry in
TimetableDayView(entry: entry).widgetSurface(entry: entry)
}
.configurationDisplayName("Marianum · Heute")
.description("Stundenplan und Vertretungen für den anstehenden Schultag.")
.supportedFamilies([.systemSmall, .systemMedium, .systemLarge])
}
}
struct TimetableDayProvider: TimelineProvider {
func placeholder(in context: Context) -> TimetableEntry {
TimetableEntry.placeholder()
}
func getSnapshot(in context: Context, completion: @escaping (TimetableEntry) -> Void) {
completion(TimetableEntry.current(variant: .day))
}
func getTimeline(
in context: Context,
completion: @escaping (Timeline<TimetableEntry>) -> Void
) {
let entry = TimetableEntry.current(variant: .day)
// 30 min mirrors the Dart workmanager cadence. iOS treats this as
// advisory; the "Stand:" label tells the user when data is stale.
let next = Calendar.current.date(byAdding: .minute, value: 30, to: Date()) ?? Date()
completion(Timeline(entries: [entry], policy: .after(next)))
}
}
// MARK: - Week widget
struct TimetableWeekWidget: Widget {
let kind: String = "TimetableWeekWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: TimetableWeekProvider()) { entry in
TimetableWeekView(entry: entry).widgetSurface(entry: entry)
}
.configurationDisplayName("Marianum · Woche")
.description("Stundenplan und Vertretungen für die ganze Schulwoche.")
.supportedFamilies([.systemMedium, .systemLarge, .systemExtraLarge])
}
}
struct TimetableWeekProvider: TimelineProvider {
func placeholder(in context: Context) -> TimetableEntry {
TimetableEntry.placeholder()
}
func getSnapshot(in context: Context, completion: @escaping (TimetableEntry) -> Void) {
completion(TimetableEntry.current(variant: .week))
}
func getTimeline(
in context: Context,
completion: @escaping (Timeline<TimetableEntry>) -> Void
) {
let entry = TimetableEntry.current(variant: .week)
let next = Calendar.current.date(byAdding: .minute, value: 30, to: Date()) ?? Date()
completion(Timeline(entries: [entry], policy: .after(next)))
}
}
// MARK: - Entry
enum TimetableVariant { case day, week }
struct TimetableEntry: TimelineEntry {
let date: Date
let variant: TimetableVariant
let data: WidgetTimetableData?
let isLoggedIn: Bool
let themeMode: String
static func placeholder() -> TimetableEntry {
TimetableEntry(
date: Date(),
variant: .day,
data: nil,
isLoggedIn: true,
themeMode: "system"
)
}
static func current(variant: TimetableVariant) -> TimetableEntry {
let isLoggedIn = WidgetDataLoader.isLoggedIn()
let data = isLoggedIn
? (variant == .day ? WidgetDataLoader.loadDay() : WidgetDataLoader.loadWeek())
: nil
return TimetableEntry(
date: Date(),
variant: variant,
data: data,
isLoggedIn: isLoggedIn,
themeMode: WidgetDataLoader.themeMode()
)
}
}
extension View {
/// Applies the user's chosen light/dark override on top of the system
/// scheme so the widget honours the in-app theme setting.
@ViewBuilder
func widgetThemeOverride(_ mode: String) -> some View {
switch mode {
case "light": self.environment(\.colorScheme, .light)
case "dark": self.environment(\.colorScheme, .dark)
default: self
}
}
/// Wraps the widget view in the Marianum palette + container background
/// so all subviews can read `\.widgetPalette` and so the widget renders
/// in our warm off-white / dark-clay instead of system grey.
@ViewBuilder
func widgetSurface(entry: TimetableEntry) -> some View {
WidgetSurface(entry: entry) { self }
}
}
private struct WidgetSurface<Content: View>: View {
let entry: TimetableEntry
@ViewBuilder let content: () -> Content
@Environment(\.colorScheme) private var colorScheme
var body: some View {
let palette = WidgetPalette.resolve(
themeMode: entry.themeMode,
colorScheme: colorScheme
)
return AnyView(
background(content: content(), palette: palette)
.environment(\.widgetPalette, palette)
.widgetThemeOverride(entry.themeMode)
)
}
@ViewBuilder
private func background<C: View>(content: C, palette: WidgetPalette) -> some View {
if #available(iOS 17.0, *) {
content.containerBackground(palette.background, for: .widget)
} else {
content.background(palette.background)
}
}
}
@@ -0,0 +1,128 @@
import Foundation
/// Mirrors lib/widget_data/widget_data.dart. JSON keys must stay in sync
/// the bridge is one-way: Dart writes, Swift reads.
enum WidgetLessonStatus: String, Codable {
case regular
case ongoing
case past
case cancelled
case irregular
case teacherChanged
case event
}
struct WidgetLesson: Codable {
let start: Date
let end: Date
let subjectShort: String
let subjectLong: String?
let room: String?
let teacher: String?
let originalTeacher: String?
let status: WidgetLessonStatus
let customColor: String?
let siblingCount: Int?
}
struct WidgetPeriod: Codable {
let name: String
let startMinutes: Int
let endMinutes: Int
let virtualStartMinutes: Int
let virtualEndMinutes: Int
}
struct WidgetTimetableData: Codable {
let fetchedAt: Date
let anchorDate: Date
let lessons: [WidgetLesson]
let periods: [WidgetPeriod]
let isHoliday: Bool
let holidayName: String?
}
enum WidgetDataKey {
static let appGroupId = "group.eu.mhsl.marianum.mobile.client.widget"
static let dayData = "widget_data_day_v1"
static let weekData = "widget_data_week_v1"
static let loggedIn = "widget_data_logged_in_v1"
static let themeMode = "widget_setting_theme_mode_v1"
}
enum WidgetDataLoader {
/// Dart's `DateTime.toIso8601String()` on a non-UTC DateTime drops the
/// trailing Z and ships local wall-clock time. ISO8601DateFormatter's
/// default treats that as UTC and shifts every lesson by the local TZ
/// offset dispatch by suffix instead, mirroring WidgetDataParser.kt.
private static func parseDartDate(_ raw: String) -> Date? {
let hasTzSuffix = raw.hasSuffix("Z")
|| raw.range(of: #"[+-]\d{2}:?\d{2}$"#, options: .regularExpression) != nil
if hasTzSuffix {
let iso = ISO8601DateFormatter()
iso.formatOptions = [.withFullDate, .withFullTime, .withFractionalSeconds]
if let d = iso.date(from: raw) { return d }
iso.formatOptions = [.withFullDate, .withFullTime]
return iso.date(from: raw)
}
for pattern in [
"yyyy-MM-dd'T'HH:mm:ss.SSSSSS",
"yyyy-MM-dd'T'HH:mm:ss.SSS",
"yyyy-MM-dd'T'HH:mm:ss",
] {
let f = DateFormatter()
f.dateFormat = pattern
f.timeZone = TimeZone.current
f.locale = Locale(identifier: "en_US_POSIX")
if let d = f.date(from: raw) { return d }
}
return nil
}
private static func decoder() -> JSONDecoder {
let dec = JSONDecoder()
dec.dateDecodingStrategy = .custom { decoder in
let container = try decoder.singleValueContainer()
let raw = try container.decode(String.self)
if let d = parseDartDate(raw) { return d }
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Unparseable date: \(raw)"
)
}
return dec
}
static func loadDay() -> WidgetTimetableData? {
load(key: WidgetDataKey.dayData)
}
static func loadWeek() -> WidgetTimetableData? {
load(key: WidgetDataKey.weekData)
}
static func isLoggedIn() -> Bool {
let defaults = UserDefaults(suiteName: WidgetDataKey.appGroupId)
return defaults?.bool(forKey: WidgetDataKey.loggedIn) ?? false
}
/// "light" / "dark" / "system". The view's `.environment(\.colorScheme)`
/// reads this so the App's theme choice wins over the OS-level setting.
static func themeMode() -> String {
let defaults = UserDefaults(suiteName: WidgetDataKey.appGroupId)
return defaults?.string(forKey: WidgetDataKey.themeMode) ?? "system"
}
private static func load(key: String) -> WidgetTimetableData? {
guard let defaults = UserDefaults(suiteName: WidgetDataKey.appGroupId),
let raw = defaults.string(forKey: key),
let data = raw.data(using: .utf8) else {
return nil
}
do {
return try decoder().decode(WidgetTimetableData.self, from: data)
} catch {
return nil
}
}
}
@@ -0,0 +1,53 @@
import SwiftUI
/// Mirrors the Kotlin `WidgetPalette` in WidgetRenderer.kt so day/week widgets
/// look identical across platforms. All values are hex tokens from the in-app
/// LightAppTheme / DarkAppTheme do not swap to system colors, the whole
/// point is platform-independent branding.
struct WidgetPalette {
let background: Color
let textPrimary: Color
let textSecondary: Color
let divider: Color
let breakBlock: Color
let watermarkOpacity: Double
static let light = WidgetPalette(
background: Color(red: 0xFC / 255, green: 0xF7 / 255, blue: 0xF5 / 255),
textPrimary: Color(red: 0x11 / 255, green: 0x11 / 255, blue: 0x11 / 255),
textSecondary: Color(red: 0x55 / 255, green: 0x55 / 255, blue: 0x55 / 255),
divider: Color.black.opacity(0x22 / 255.0),
breakBlock: Color.black.opacity(0x0C / 255.0),
watermarkOpacity: 0.014
)
static let dark = WidgetPalette(
background: Color(red: 0x1F / 255, green: 0x17 / 255, blue: 0x16 / 255),
textPrimary: Color(red: 0xF1 / 255, green: 0xF1 / 255, blue: 0xF1 / 255),
textSecondary: Color(red: 0xB0 / 255, green: 0xB0 / 255, blue: 0xB0 / 255),
divider: Color.white.opacity(0x33 / 255.0),
breakBlock: Color.white.opacity(0x14 / 255.0),
watermarkOpacity: 0.025
)
static func resolve(themeMode: String, colorScheme: ColorScheme) -> WidgetPalette {
let isDark: Bool
switch themeMode {
case "light": isDark = false
case "dark": isDark = true
default: isDark = colorScheme == .dark
}
return isDark ? .dark : .light
}
}
private struct WidgetPaletteKey: EnvironmentKey {
static let defaultValue = WidgetPalette.light
}
extension EnvironmentValues {
var widgetPalette: WidgetPalette {
get { self[WidgetPaletteKey.self] }
set { self[WidgetPaletteKey.self] = newValue }
}
}
-3
View File
@@ -1,3 +0,0 @@
class ApiParams {
}
-5
View File
@@ -1,5 +0,0 @@
class ApiRequest {
}
@@ -1,4 +1,4 @@
class ApiError {
class ApiError implements Exception {
String message;
ApiError(this.message);
+1
View File
@@ -0,0 +1 @@
class ApiParams {}
+1
View File
@@ -0,0 +1 @@
class ApiRequest {}
@@ -1,5 +1,6 @@
import 'package:http/http.dart' as http;
import 'package:json_annotation/json_annotation.dart';
abstract class ApiResponse {
@JsonKey(includeFromJson: false, includeToJson: false)
late http.Response rawResponse;
+16
View File
@@ -0,0 +1,16 @@
abstract class AppException implements Exception {
final String userMessage;
final String? technicalDetails;
final bool allowRetry;
const AppException({
required this.userMessage,
this.technicalDetails,
this.allowRetry = true,
});
@override
String toString() => technicalDetails == null
? '$runtimeType: $userMessage'
: '$runtimeType: $userMessage ($technicalDetails)';
}
+24
View File
@@ -0,0 +1,24 @@
import 'app_exception.dart';
class AuthException extends AppException {
final int statusCode;
const AuthException({
required this.statusCode,
required super.userMessage,
super.technicalDetails,
}) : super(allowRetry: false);
factory AuthException.unauthorized({String? technicalDetails}) =>
AuthException(
statusCode: 401,
userMessage: 'Bitte melde dich erneut an, um fortzufahren.',
technicalDetails: technicalDetails,
);
factory AuthException.forbidden({String? technicalDetails}) => AuthException(
statusCode: 403,
userMessage: 'Du hast keine Berechtigung für diese Aktion.',
technicalDetails: technicalDetails,
);
}
+115
View File
@@ -0,0 +1,115 @@
import 'dart:async';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:http/http.dart' as http;
import '../api_error.dart';
import '../marianumcloud/talk/talk_error.dart';
import '../webuntis/webuntis_error.dart';
import 'app_exception.dart';
import 'network_exception.dart';
import 'parse_exception.dart';
import 'server_exception.dart';
import 'talk_exception.dart';
import 'webuntis_exception.dart';
const String _defaultFallback =
'Etwas ist schiefgelaufen. Bitte versuche es erneut.';
const String _tlsErrorMessage =
'Die sichere Verbindung zum Server wurde abgelehnt (Zertifikat oder TLS-Fehler). '
'Häufige Ursachen: falsche Geräte-Uhrzeit oder ein WLAN mit Anmeldeseite (z.B. Café/Hotel).';
AppException? _dioToAppException(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return NetworkException.timeout(technicalDetails: error.message);
case DioExceptionType.connectionError:
return NetworkException(technicalDetails: error.message);
case DioExceptionType.badCertificate:
return const NetworkException(userMessage: _tlsErrorMessage);
case DioExceptionType.badResponse:
final status = error.response?.statusCode;
return ServerException(
statusCode: status ?? -1,
technicalDetails: 'HTTP $status: ${error.message}',
);
case DioExceptionType.cancel:
case DioExceptionType.unknown:
final inner = error.error;
if (inner is SocketException) {
return NetworkException(technicalDetails: inner.message);
}
if (inner is HandshakeException) {
return const NetworkException(userMessage: _tlsErrorMessage);
}
if (inner is FormatException) {
return ParseException(technicalDetails: inner.message);
}
return null;
}
}
String errorToUserMessage(Object? error, {String fallback = _defaultFallback}) {
if (error == null) return fallback;
if (error is AppException) return error.userMessage;
if (error is TalkError) return TalkException(error).userMessage;
if (error is WebuntisError) return WebuntisException(error).userMessage;
if (error is DioException) {
final mapped = _dioToAppException(error);
if (mapped != null) return mapped.userMessage;
}
if (error is SocketException) {
return const NetworkException().userMessage;
}
if (error is TimeoutException) {
return NetworkException.timeout().userMessage;
}
if (error is http.ClientException) {
return const NetworkException().userMessage;
}
if (error is HandshakeException) {
return _tlsErrorMessage;
}
if (error is FormatException) {
return const ParseException().userMessage;
}
if (error is ApiError) {
return _stripDioPrefix(error.message);
}
return fallback;
}
String? errorToTechnicalDetails(Object? error) {
if (error == null) return null;
if (error is AppException) return error.technicalDetails ?? error.toString();
if (error is TalkError) return TalkException(error).technicalDetails;
if (error is WebuntisError) return WebuntisException(error).technicalDetails;
if (error is DioException) {
final mapped = _dioToAppException(error);
if (mapped != null) return mapped.technicalDetails ?? mapped.toString();
}
return error.toString();
}
bool errorAllowsRetry(Object? error) {
if (error == null) return true;
if (error is AppException) return error.allowRetry;
if (error is DioException) {
final mapped = _dioToAppException(error);
if (mapped != null) return mapped.allowRetry;
}
return true;
}
String _stripDioPrefix(String raw) {
// ApiError messages embed full request URIs; only surface the first line.
final firstLine = raw.split('\n').first.trim();
return firstLine.isEmpty ? _defaultFallback : firstLine;
}

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