7 Commits

Author SHA1 Message Date
MineTec 5d264f7651 added notes for next tasks 2024-03-30 18:33:52 +01:00
MineTec a6f7c09671 wip 2024-03-29 21:58:51 +01:00
MineTec eaf6d9f547 updated new linter rules 2024-03-29 18:47:54 +01:00
MineTec 5b34afd6cb Merge branch 'develop' into feature-highEduGraduationCalculator
# Conflicts:
#	lib/view/pages/overhang.dart
2024-03-29 18:32:59 +01:00
MineTec 25d901d093 WIP lk select dialog 2024-03-29 17:29:31 +01:00
MineTec 6237a2e9cf Merge branch 'develop' into feature-highEduGraduationCalculator
# Conflicts:
#	lib/view/pages/overhang.dart
2024-03-24 14:21:01 +01:00
MineTec c4f5be2205 WIP high education graduation calculator 2024-03-23 15:42:31 +01:00
707 changed files with 10801 additions and 34071 deletions
+8 -8
View File
@@ -48,14 +48,14 @@ lib/generated_plugin_registrant.dart
#pubspec.lock
# Android related
materials/screenshots/android/**/gradle-wrapper.jar
materials/screenshots/android/.gradle
materials/screenshots/android/captures/
materials/screenshots/android/gradlew
materials/screenshots/android/gradlew.bat
materials/screenshots/android/key.properties
materials/screenshots/android/local.properties
materials/screenshots/android/**/GeneratedPluginRegistrant.java
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/key.properties
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
# iOS/XCode related
**/ios/**/*.mode1v3
-77
View File
@@ -1,77 +0,0 @@
# 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.
+24 -79
View File
@@ -1,88 +1,33 @@
# Static analysis configuration for the Flutter project.
# https://dart.dev/guides/language/analysis-options
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# Base ruleset: flutter_lints (recommended Flutter defaults).
# Additional lints below catch real bugs and enforce consistent style.
# 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`.
# 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:
# === Project conventions ===
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
file_names: false
prefer_relative_imports: true
prefer_single_quotes: true
eol_at_end_of_file: true
omit_local_variable_types: true
avoid_multiple_declarations_per_line: true
# === Bug catchers ===
always_declare_return_types: true
avoid_empty_else: true
avoid_slow_async_io: true
avoid_type_to_string: true
avoid_void_async: true
await_only_futures: true
cancel_subscriptions: true
cast_nullable_to_non_nullable: true
close_sinks: true
empty_catches: true
hash_and_equals: true
no_adjacent_strings_in_list: true
no_duplicate_case_values: true
test_types_in_equals: true
throw_in_finally: true
unawaited_futures: true
unnecessary_statements: true
unrelated_type_equality_checks: true
use_build_context_synchronously: true
valid_regexps: true
# === Flutter widget hygiene ===
avoid_unnecessary_containers: true
sized_box_for_whitespace: true
sort_child_properties_last: true
use_colored_box: true
use_decorated_box: true
use_full_hex_values_for_flutter_colors: true
use_key_in_widget_constructors: true
# === Code clarity ===
directives_ordering: true
library_prefixes: true
no_leading_underscores_for_local_identifiers: true
prefer_conditional_assignment: true
prefer_if_elements_to_conditional_expressions: true
prefer_if_null_operators: true
prefer_initializing_formals: true
prefer_interpolation_to_compose_strings: true
prefer_is_empty: true
prefer_is_not_empty: true
prefer_is_not_operator: true
prefer_iterable_whereType: true
prefer_null_aware_operators: true
prefer_spread_collections: true
prefer_void_to_null: true
unnecessary_await_in_return: true
unnecessary_brace_in_string_interps: true
unnecessary_lambdas: true
unnecessary_null_aware_assignments: true
unnecessary_null_checks: true
unnecessary_parenthesis: true
unnecessary_string_interpolations: true
use_super_parameters: true
prefer_single_quotes: true
# === File naming ===
file_names: true
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+9 -8
View File
@@ -25,16 +25,15 @@ if (flutterVersionName == null) {
android {
namespace "eu.mhsl.marianum.mobile.client"
compileSdk flutter.compileSdkVersion
ndkVersion "28.2.13676358"
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
coreLibraryDesugaringEnabled true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '17'
jvmTarget = '1.8'
}
sourceSets {
@@ -42,8 +41,11 @@ android {
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "eu.mhsl.marianum.mobile.client"
minSdkVersion 26
// You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
minSdkVersion 19
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
@@ -64,6 +66,5 @@ flutter {
}
dependencies {
implementation 'com.android.support:multidex:2.0.1'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4'
implementation 'com.android.support:multidex:1.0.3'
}
View File
+1 -47
View File
@@ -2,9 +2,7 @@
<application
android:label="Marianum Fulda"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:fullBackupContent="@xml/backup_rules"
android:dataExtractionRules="@xml/data_extraction_rules">
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
@@ -25,53 +23,12 @@
<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" />
<!-- 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 to query activities that can process text, see:
https://developer.android.com/training/package-visibility?hl=en and
@@ -85,7 +42,4 @@
</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,42 +1,5 @@
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() {
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
}
}
}
class MainActivity: FlutterActivity()
@@ -1,37 +0,0 @@
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)
}
}
@@ -1,31 +0,0 @@
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)
}
}
@@ -1,157 +0,0 @@
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)
}
}
@@ -1,786 +0,0 @@
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,
)
}
maybeAddNowIndicator(
packageName,
views,
R.id.widget_day_grid,
hourHeightDp,
anchorDate = data.anchorDate,
periods = data.periods,
)
}
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,
)
}
if (WidgetDateUtils.isSameDay(day, Date())) {
maybeAddNowIndicator(
packageName,
views,
columnId,
hourHeightDp,
anchorDate = day,
periods = data.periods,
)
}
}
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)
}
}
private fun maybeAddNowIndicator(
packageName: String,
parent: RemoteViews,
containerId: Int,
hourHeightDp: Float,
anchorDate: Date,
periods: List<WidgetPeriod>,
) {
if (!WidgetDateUtils.isSameDay(anchorDate, Date())) return
val now = Calendar.getInstance()
val nowMinutes = now.get(Calendar.HOUR_OF_DAY) * 60 + now.get(Calendar.MINUTE)
if (periods.isNotEmpty()) {
if (nowMinutes < periods.first().startMinutes ||
nowMinutes > periods.last().endMinutes
) return
}
val virtualNow = realMinutesToVirtual(nowMinutes, periods)
val topDp = virtualNow * hourHeightDp / 60.0f
val indicator = RemoteViews(packageName, R.layout.widget_now_indicator)
indicator.setViewLayoutMargin(
R.id.widget_now_indicator_root,
RemoteViews.MARGIN_TOP,
topDp,
TypedValue.COMPLEX_UNIT_DIP,
)
parent.addView(containerId, indicator)
}
/// 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"
}
@@ -1,10 +0,0 @@
<?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>
@@ -1,36 +0,0 @@
<?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>
@@ -1,5 +0,0 @@
<?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>
@@ -1,16 +0,0 @@
<?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>
@@ -1,6 +0,0 @@
<?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>
@@ -1,6 +0,0 @@
<?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>
@@ -1,6 +0,0 @@
<?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>
@@ -1,6 +0,0 @@
<?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>
@@ -1,6 +0,0 @@
<?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>
@@ -1,6 +0,0 @@
<?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>
@@ -1,6 +0,0 @@
<?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>
@@ -1,6 +0,0 @@
<?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>
@@ -1,6 +0,0 @@
<?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>
@@ -1,11 +0,0 @@
<?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>
@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#FFE53935" />
</shape>
@@ -1,8 +0,0 @@
<?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" />
@@ -1,91 +0,0 @@
<?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>
@@ -1,185 +0,0 @@
<?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>
@@ -1,7 +0,0 @@
<?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" />
@@ -1,93 +0,0 @@
<?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>
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/widget_now_indicator_root"
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_marginTop="0dp"
android:background="@drawable/widget_now_indicator" />
@@ -1,46 +0,0 @@
<?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>
@@ -1,31 +0,0 @@
<?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>
@@ -1,185 +0,0 @@
<?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>
@@ -1,28 +0,0 @@
<?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>
@@ -1,147 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Static preview for the week widget. Five day columns with two short
demo blocks each so the structure is recognisable in the picker. -->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/widget_background">
<ImageView
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_gravity="bottom|end"
android:layout_marginEnd="-36dp"
android:layout_marginBottom="-36dp"
android:scaleType="fitCenter"
android:importantForAccessibility="no"
android:contentDescription="@null"
android:alpha="0.018"
android:src="@drawable/marianum_m_watermark" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="10dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="14sp"
android:textStyle="bold"
android:textColor="@color/widget_text_primary"
android:singleLine="true"
android:text="KW 19 · 06.05.10.05." />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="10sp"
android:textColor="@color/widget_text_secondary"
android:text="Stand: 07:50" />
</LinearLayout>
<!-- Day-name + date row -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginTop="6dp">
<FrameLayout android:layout_width="20dp" android:layout_height="wrap_content" />
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:gravity="center">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="11sp" android:textStyle="bold" android:textColor="@color/widget_text_primary" android:text="Mo" />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textColor="@color/widget_text_secondary" android:text="06.05." />
</LinearLayout>
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:gravity="center">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="11sp" android:textStyle="bold" android:textColor="@color/widget_text_primary" android:text="Di" />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textColor="@color/widget_text_secondary" android:text="07.05." />
</LinearLayout>
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:gravity="center">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="11sp" android:textStyle="bold" android:textColor="@color/widget_text_primary" android:text="Mi" />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textColor="@color/widget_text_secondary" android:text="08.05." />
</LinearLayout>
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:gravity="center">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="11sp" android:textStyle="bold" android:textColor="@color/widget_text_primary" android:text="Do" />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textColor="@color/widget_text_secondary" android:text="09.05." />
</LinearLayout>
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:gravity="center">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="11sp" android:textStyle="bold" android:textColor="@color/widget_text_primary" android:text="Fr" />
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textColor="@color/widget_text_secondary" android:text="10.05." />
</LinearLayout>
</LinearLayout>
<!-- Time grid: time-label column + 5 day columns -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="horizontal"
android:layout_marginTop="2dp">
<FrameLayout android:layout_width="20dp" android:layout_height="match_parent">
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="0dp" android:gravity="end" android:paddingEnd="3dp" android:textSize="8sp" android:textColor="@color/widget_text_secondary" android:text="08" />
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="36dp" android:gravity="end" android:paddingEnd="3dp" android:textSize="8sp" android:textColor="@color/widget_text_secondary" android:text="10" />
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="72dp" android:gravity="end" android:paddingEnd="3dp" android:textSize="8sp" android:textColor="@color/widget_text_secondary" android:text="12" />
</FrameLayout>
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
<!-- Mon -->
<FrameLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1">
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="2dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_regular">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="MA" />
</FrameLayout>
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="40dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_regular">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="EN" />
</FrameLayout>
</FrameLayout>
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
<!-- Tue -->
<FrameLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1">
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="20dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_cancelled">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="BIO" />
</FrameLayout>
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="60dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_regular">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="DE" />
</FrameLayout>
</FrameLayout>
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
<!-- Wed -->
<FrameLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1">
<FrameLayout android:layout_width="match_parent" android:layout_height="32dp" android:layout_marginTop="2dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_regular">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="MA" />
</FrameLayout>
</FrameLayout>
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
<!-- Thu -->
<FrameLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1">
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="2dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_teacher_changed">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="GE" />
</FrameLayout>
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="40dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_regular">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="PH" />
</FrameLayout>
</FrameLayout>
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
<!-- Fri -->
<FrameLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1">
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="20dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_regular">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="EN" />
</FrameLayout>
</FrameLayout>
</LinearLayout>
</LinearLayout>
</FrameLayout>
@@ -1,7 +0,0 @@
<?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>
@@ -1,17 +0,0 @@
<?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>
@@ -1,17 +0,0 @@
<?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>
@@ -1,9 +0,0 @@
<?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>
@@ -1,15 +0,0 @@
<?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>
@@ -1,12 +0,0 @@
<?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" />
@@ -1,12 +0,0 @@
<?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,29 +9,6 @@ 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')
}
-3
View File
@@ -1,6 +1,3 @@
org.gradle.jvmargs=-Xmx4G
android.useAndroidX=true
android.enableJetifier=true
android.defaults.buildfeatures.buildconfig=true
android.nonTransitiveRClass=false
android.nonFinalResIds=false
@@ -1,2 +0,0 @@
#This file is generated by updateDaemonJvm
toolchainVersion=21
+1 -2
View File
@@ -1,7 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
+2 -4
View File
@@ -19,10 +19,8 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
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.1.10" apply false
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.10.0'
id "com.android.application" version "7.3.0" apply false
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
}
include ":app"
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 68 KiB

-29
View File
@@ -1,29 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIFBTCCAu2gAwIBAgIQS6hSk/eaL6JzBkuoBI110DANBgkqhkiG9w0BAQsFADBP
MQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFy
Y2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTAeFw0yNDAzMTMwMDAwMDBa
Fw0yNzAzMTIyMzU5NTlaMDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBF
bmNyeXB0MQwwCgYDVQQDEwNSMTAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQDPV+XmxFQS7bRH/sknWHZGUCiMHT6I3wWd1bUYKb3dtVq/+vbOo76vACFL
YlpaPAEvxVgD9on/jhFD68G14BQHlo9vH9fnuoE5CXVlt8KvGFs3Jijno/QHK20a
/6tYvJWuQP/py1fEtVt/eA0YYbwX51TGu0mRzW4Y0YCF7qZlNrx06rxQTOr8IfM4
FpOUurDTazgGzRYSespSdcitdrLCnF2YRVxvYXvGLe48E1KGAdlX5jgc3421H5KR
mudKHMxFqHJV8LDmowfs/acbZp4/SItxhHFYyTr6717yW0QrPHTnj7JHwQdqzZq3
DZb3EoEmUVQK7GH29/Xi8orIlQ2NAgMBAAGjgfgwgfUwDgYDVR0PAQH/BAQDAgGG
MB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATASBgNVHRMBAf8ECDAGAQH/
AgEAMB0GA1UdDgQWBBS7vMNHpeS8qcbDpHIMEI2iNeHI6DAfBgNVHSMEGDAWgBR5
tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAKG
Fmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0gBAwwCjAIBgZngQwBAgEwJwYD
VR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVuY3Iub3JnLzANBgkqhkiG9w0B
AQsFAAOCAgEAkrHnQTfreZ2B5s3iJeE6IOmQRJWjgVzPw139vaBw1bGWKCIL0vIo
zwzn1OZDjCQiHcFCktEJr59L9MhwTyAWsVrdAfYf+B9haxQnsHKNY67u4s5Lzzfd
u6PUzeetUK29v+PsPmI2cJkxp+iN3epi4hKu9ZzUPSwMqtCceb7qPVxEbpYxY1p9
1n5PJKBLBX9eb9LU6l8zSxPWV7bK3lG4XaMJgnT9x3ies7msFtpKK5bDtotij/l0
GaKeA97pb5uwD9KgWvaFXMIEt8jVTjLEvwRdvCn294GPDF08U8lAkIv7tghluaQh
1QnlE4SEN4LOECj8dsIGJXpGUk3aU3KkJz9icKy+aUgA+2cP21uh6NcDIS3XyfaZ
QjmDQ993ChII8SXWupQZVBiIpcWO4RqZk3lr7Bz5MUCwzDIA359e57SSq5CCkY0N
4B6Vulk7LktfwrdGNVI5BsC9qqxSwSKgRJeZ9wygIaehbHFHFhcBaMDKpiZlBHyz
rsnnlFXCb5s8HKn5LsUgGvB24L7sGNZP2CX7dhHov+YhD+jozLW2p9W4959Bz2Ei
RmqDtmiXLnzqTpXbI+suyCsohKRg6Un0RC47+cpiVwHiXZAW+cn8eiNIjqbVgXLx
KPpdzvvtTnOPlC7SQZSYmdunr3Bf9b77AiC/ZidstK36dRILKz7OA54=
-----END CERTIFICATE-----
-29
View File
@@ -1,29 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIFBTCCAu2gAwIBAgIQWgDyEtjUtIDzkkFX6imDBTANBgkqhkiG9w0BAQsFADBP
MQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFy
Y2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMTAeFw0yNDAzMTMwMDAwMDBa
Fw0yNzAzMTIyMzU5NTlaMDMxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBF
bmNyeXB0MQwwCgYDVQQDEwNSMTMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
AoIBAQClZ3CN0FaBZBUXYc25BtStGZCMJlA3mBZjklTb2cyEBZPs0+wIG6BgUUNI
fSvHSJaetC3ancgnO1ehn6vw1g7UDjDKb5ux0daknTI+WE41b0VYaHEX/D7YXYKg
L7JRbLAaXbhZzjVlyIuhrxA3/+OcXcJJFzT/jCuLjfC8cSyTDB0FxLrHzarJXnzR
yQH3nAP2/Apd9Np75tt2QnDr9E0i2gB3b9bJXxf92nUupVcM9upctuBzpWjPoXTi
dYJ+EJ/B9aLrAek4sQpEzNPCifVJNYIKNLMc6YjCR06CDgo28EdPivEpBHXazeGa
XP9enZiVuppD0EqiFwUBBDDTMrOPAgMBAAGjgfgwgfUwDgYDVR0PAQH/BAQDAgGG
MB0GA1UdJQQWMBQGCCsGAQUFBwMCBggrBgEFBQcDATASBgNVHRMBAf8ECDAGAQH/
AgEAMB0GA1UdDgQWBBTnq58PLDOgU9NeT3jIsoQOO9aSMzAfBgNVHSMEGDAWgBR5
tFnme7bl5AFzgAiIyBpY9umbbjAyBggrBgEFBQcBAQQmMCQwIgYIKwYBBQUHMAKG
Fmh0dHA6Ly94MS5pLmxlbmNyLm9yZy8wEwYDVR0gBAwwCjAIBgZngQwBAgEwJwYD
VR0fBCAwHjAcoBqgGIYWaHR0cDovL3gxLmMubGVuY3Iub3JnLzANBgkqhkiG9w0B
AQsFAAOCAgEAUTdYUqEimzW7TbrOypLqCfL7VOwYf/Q79OH5cHLCZeggfQhDconl
k7Kgh8b0vi+/XuWu7CN8n/UPeg1vo3G+taXirrytthQinAHGwc/UdbOygJa9zuBc
VyqoH3CXTXDInT+8a+c3aEVMJ2St+pSn4ed+WkDp8ijsijvEyFwE47hulW0Ltzjg
9fOV5Pmrg/zxWbRuL+k0DBDHEJennCsAen7c35Pmx7jpmJ/HtgRhcnz0yjSBvyIw
6L1QIupkCv2SBODT/xDD3gfQQyKv6roV4G2EhfEyAsWpmojxjCUCGiyg97FvDtm/
NK2LSc9lybKxB73I2+P2G3CaWpvvpAiHCVu30jW8GCxKdfhsXtnIy2imskQqVZ2m
0Pmxobb28Tucr7xBK7CtwvPrb79os7u2XP3O5f9b/H66GNyRrglRXlrYjI1oGYL/
f4I1n/Sgusda6WvA6C190kxjU15Y12mHU4+BxyR9cx2hhGS9fAjMZKJss28qxvz6
Axu4CaDmRNZpK/pQrXF17yXCXkmEWgvSOEZy6Z9pcbLIVEGckV/iVeq0AOo2pkg9
p4QRIy0tK2diRENLSF2KysFwbY6B26BFeFs3v1sYVRhFW9nLkOrQVporCS0KyZmf
wVD89qSTlnctLcZnIavjKsKUu1nA1iU0yYMdYepKR7lWbnwhdx3ewok=
-----END CERTIFICATE-----
-21
View File
@@ -1,21 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

-7
View File
@@ -1,7 +0,0 @@
targets:
$default:
builders:
json_serializable:
options:
explicit_to_json: false
generic_argument_factories: true
-3
View File
@@ -1,4 +1 @@
extensions:
- hive_ce: true
- shared_preferences: true
- provider: true
-9
View File
@@ -1,9 +0,0 @@
flutter_launcher_icons:
android: true
ios: true
remove_alpha_ios: true
image_path_android: "assets/logo/icon/ic_launcher.png"
image_path_ios: "assets/logo/icon/1024.png"
adaptive_icon_background: "assets/logo/icon/ic_launcher_adaptive_back.png" # only available for Android 8.0 devices and above
adaptive_icon_foreground: "assets/logo/icon/ic_launcher_adaptive_fore.png" # only available for Android 8.0 devices and above
min_sdk_android: 16 # android min sdk min:16, default 21
+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/icon-android12.png
image: assets/logo/icon-android12.png
# Splash screen background color.
color: "#993333"

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

BIN
View File
Binary file not shown.
+1 -1
View File
@@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
<string>11.0</string>
</dict>
</plist>
+1 -7
View File
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '15.0'
platform :ios, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -31,13 +31,7 @@ target 'Runner' do
use_frameworks!
use_modular_headers!
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
+157 -161
View File
@@ -1,190 +1,185 @@
PODS:
- connectivity_plus (0.0.1):
- better_open_file (0.0.1):
- Flutter
- device_info_plus (0.0.1):
- Flutter
- DKImagePickerController/Core (4.3.9):
- DKImagePickerController/Core (4.3.4):
- DKImagePickerController/ImageDataManager
- DKImagePickerController/Resource
- DKImagePickerController/ImageDataManager (4.3.9)
- DKImagePickerController/PhotoGallery (4.3.9):
- DKImagePickerController/ImageDataManager (4.3.4)
- DKImagePickerController/PhotoGallery (4.3.4):
- DKImagePickerController/Core
- DKPhotoGallery
- DKImagePickerController/Resource (4.3.9)
- DKPhotoGallery (0.0.19):
- DKPhotoGallery/Core (= 0.0.19)
- DKPhotoGallery/Model (= 0.0.19)
- DKPhotoGallery/Preview (= 0.0.19)
- DKPhotoGallery/Resource (= 0.0.19)
- DKImagePickerController/Resource (4.3.4)
- DKPhotoGallery (0.0.17):
- DKPhotoGallery/Core (= 0.0.17)
- DKPhotoGallery/Model (= 0.0.17)
- DKPhotoGallery/Preview (= 0.0.17)
- DKPhotoGallery/Resource (= 0.0.17)
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Core (0.0.19):
- DKPhotoGallery/Core (0.0.17):
- DKPhotoGallery/Model
- DKPhotoGallery/Preview
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Model (0.0.19):
- DKPhotoGallery/Model (0.0.17):
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Preview (0.0.19):
- DKPhotoGallery/Preview (0.0.17):
- DKPhotoGallery/Model
- DKPhotoGallery/Resource
- SDWebImage
- SwiftyGif
- DKPhotoGallery/Resource (0.0.19):
- DKPhotoGallery/Resource (0.0.17):
- SDWebImage
- SwiftyGif
- emoji_picker_flutter (0.0.1):
- Flutter
- fast_rsa (0.7.0):
- fast_rsa (0.6.0):
- 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 (10.12.0):
- FirebaseCore (= 10.12.0)
- Firebase/InAppMessaging (10.12.0):
- Firebase/CoreOnly
- FirebaseInAppMessaging (~> 12.4.0-beta)
- Firebase/Messaging (12.4.0):
- FirebaseInAppMessaging (~> 10.12.0-beta)
- Firebase/Messaging (10.12.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 12.4.0)
- firebase_core (4.2.1):
- Firebase/CoreOnly (= 12.4.0)
- FirebaseMessaging (~> 10.12.0)
- firebase_core (2.15.1):
- Firebase/CoreOnly (= 10.12.0)
- Flutter
- firebase_in_app_messaging (0.9.0-4):
- Firebase/InAppMessaging (= 12.4.0)
- firebase_in_app_messaging (0.7.3-5):
- Firebase/InAppMessaging (= 10.12.0)
- firebase_core
- Flutter
- firebase_messaging (16.0.4):
- Firebase/Messaging (= 12.4.0)
- firebase_messaging (14.6.6):
- Firebase/Messaging (= 10.12.0)
- firebase_core
- Flutter
- FirebaseABTesting (12.4.0):
- FirebaseCore (~> 12.4.0)
- FirebaseCore (12.4.0):
- FirebaseCoreInternal (~> 12.4.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreInternal (12.4.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)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (12.4.0):
- FirebaseCore (~> 12.4.0)
- FirebaseInstallations (~> 12.4.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Reachability (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- FirebaseABTesting (10.13.0):
- FirebaseCore (~> 10.0)
- FirebaseCore (10.12.0):
- FirebaseCoreInternal (~> 10.0)
- GoogleUtilities/Environment (~> 7.8)
- GoogleUtilities/Logger (~> 7.8)
- FirebaseCoreInternal (10.13.0):
- "GoogleUtilities/NSData+zlib (~> 7.8)"
- FirebaseInAppMessaging (10.12.0-beta):
- FirebaseABTesting (~> 10.0)
- FirebaseCore (~> 10.0)
- FirebaseInstallations (~> 10.0)
- GoogleUtilities/Environment (~> 7.8)
- nanopb (< 2.30910.0, >= 2.30908.0)
- FirebaseInstallations (10.13.0):
- FirebaseCore (~> 10.0)
- GoogleUtilities/Environment (~> 7.8)
- GoogleUtilities/UserDefaults (~> 7.8)
- PromisesObjC (~> 2.1)
- FirebaseMessaging (10.12.0):
- FirebaseCore (~> 10.0)
- FirebaseInstallations (~> 10.0)
- GoogleDataTransport (~> 9.2)
- GoogleUtilities/AppDelegateSwizzler (~> 7.8)
- GoogleUtilities/Environment (~> 7.8)
- GoogleUtilities/Reachability (~> 7.8)
- GoogleUtilities/UserDefaults (~> 7.8)
- nanopb (< 2.30910.0, >= 2.30908.0)
- Flutter (1.0.0)
- flutter_app_badge (2.0.0):
- flutter_app_badger (1.3.0):
- Flutter
- flutter_local_notifications (0.0.1):
- Flutter
- flutter_native_splash (2.4.3):
- flutter_native_splash (0.0.1):
- Flutter
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
- fluttertoast (0.0.2):
- Flutter
- Toast
- FMDB (2.7.5):
- FMDB/standard (= 2.7.5)
- FMDB/standard (2.7.5)
- GoogleDataTransport (9.2.5):
- GoogleUtilities/Environment (~> 7.7)
- nanopb (< 2.30910.0, >= 2.30908.0)
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/AppDelegateSwizzler (7.11.5):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment (7.11.5):
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/Logger (7.11.5):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Network (7.11.5):
- GoogleUtilities/Logger
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.0)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/Reachability (8.1.0):
- "GoogleUtilities/NSData+zlib (7.11.5)"
- GoogleUtilities/Reachability (7.11.5):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/UserDefaults (7.11.5):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- image_picker_ios (0.0.1):
- Flutter
- in_app_review (2.0.0):
- libphonenumber_plugin (0.0.1):
- Flutter
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- open_filex (0.0.2):
- Flutter
- package_info_plus (0.4.5):
- PhoneNumberKit
- nanopb (2.30909.0):
- nanopb/decode (= 2.30909.0)
- nanopb/encode (= 2.30909.0)
- nanopb/decode (2.30909.0)
- nanopb/encode (2.30909.0)
- package_info (0.0.1):
- Flutter
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- PhoneNumberKit (3.7.11):
- PhoneNumberKit/PhoneNumberKitCore (= 3.7.11)
- PhoneNumberKit/UIKit (= 3.7.11)
- PhoneNumberKit/PhoneNumberKitCore (3.7.11)
- PhoneNumberKit/UIKit (3.7.11):
- PhoneNumberKit (3.6.7):
- PhoneNumberKit/PhoneNumberKitCore (= 3.6.7)
- PhoneNumberKit/UIKit (= 3.6.7)
- PhoneNumberKit/PhoneNumberKitCore (3.6.7)
- PhoneNumberKit/UIKit (3.6.7):
- PhoneNumberKit/PhoneNumberKitCore
- PromisesObjC (2.4.0)
- SDWebImage (5.21.2):
- SDWebImage/Core (= 5.21.2)
- SDWebImage/Core (5.21.2)
- PromisesObjC (2.3.1)
- SDWebImage (5.17.0):
- SDWebImage/Core (= 5.17.0)
- SDWebImage/Core (5.17.0)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqflite_darwin (0.0.4):
- sqflite (0.0.3):
- Flutter
- FlutterMacOS
- SwiftyGif (5.4.5)
- FMDB (>= 2.7.5)
- SwiftyGif (5.4.4)
- syncfusion_flutter_pdfviewer (0.0.1):
- Flutter
- Toast (4.0.0)
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- better_open_file (from `.symlinks/plugins/better_open_file/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`)
- 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_app_badger (from `.symlinks/plugins/flutter_app_badger/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/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`)
- libphonenumber_plugin (from `.symlinks/plugins/libphonenumber_plugin/ios`)
- package_info (from `.symlinks/plugins/package_info/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- PhoneNumberKit (~> 3.7.6)
- 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`)
- sqflite (from `.symlinks/plugins/sqflite/ios`)
- syncfusion_flutter_pdfviewer (from `.symlinks/plugins/syncfusion_flutter_pdfviewer/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
@@ -199,6 +194,7 @@ SPEC REPOS:
- FirebaseInAppMessaging
- FirebaseInstallations
- FirebaseMessaging
- FMDB
- GoogleDataTransport
- GoogleUtilities
- nanopb
@@ -206,14 +202,13 @@ SPEC REPOS:
- PromisesObjC
- SDWebImage
- SwiftyGif
- Toast
EXTERNAL SOURCES:
connectivity_plus:
:path: ".symlinks/plugins/connectivity_plus/ios"
better_open_file:
:path: ".symlinks/plugins/better_open_file/ios"
device_info_plus:
: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"
file_picker:
@@ -226,73 +221,74 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/firebase_messaging/ios"
Flutter:
:path: Flutter
flutter_app_badge:
:path: ".symlinks/plugins/flutter_app_badge/ios"
flutter_app_badger:
:path: ".symlinks/plugins/flutter_app_badger/ios"
flutter_local_notifications:
:path: ".symlinks/plugins/flutter_local_notifications/ios"
flutter_native_splash:
:path: ".symlinks/plugins/flutter_native_splash/ios"
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
in_app_review:
:path: ".symlinks/plugins/in_app_review/ios"
open_filex:
:path: ".symlinks/plugins/open_filex/ios"
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
libphonenumber_plugin:
:path: ".symlinks/plugins/libphonenumber_plugin/ios"
package_info:
:path: ".symlinks/plugins/package_info/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
sqflite_darwin:
:path: ".symlinks/plugins/sqflite_darwin/darwin"
sqflite:
:path: ".symlinks/plugins/sqflite/ios"
syncfusion_flutter_pdfviewer:
:path: ".symlinks/plugins/syncfusion_flutter_pdfviewer/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
emoji_picker_flutter: ece213fc274bdddefb77d502d33080dc54e616cc
fast_rsa: fb70897d51040b094c780d5f1d7358614738b879
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
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_app_badge: ca742dd659a157c1090ef7cd881cb78f48f3bcdf
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
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
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
syncfusion_flutter_pdfviewer: 90dc48305d2e33d4aa20681d1e98ddeda891bc14
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
better_open_file: 03cf320415d4d3f46b6e00adc4a567d76c1a399d
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179
fast_rsa: f696740d492d562e76f17b0a81dfc8ec3e635374
file_picker: ce3938a0df3cc1ef404671531facef740d03f920
Firebase: 07150e75d142fb9399f6777fa56a187b17f833a0
firebase_core: 4a3246a02f828a01c74a2c26427037786d90f17f
firebase_in_app_messaging: aebdbc10109a0ce44a3294f4ea57ed89ebe1d8bd
firebase_messaging: 13b378c8449cae7ec96c79570170943dd73d4738
FirebaseABTesting: 86ac5a4fc749088bb4d55a1cbfb2c4cb42c6d5de
FirebaseCore: f86a1394906b97ac445ae49c92552a9425831bed
FirebaseCoreInternal: b342e37cd4f5b4454ec34308f073420e7920858e
FirebaseInAppMessaging: dc24f50aebaf81a377f0b8abf360778f94208931
FirebaseInstallations: b28af1b9f997f1a799efe818c94695a3728c352f
FirebaseMessaging: bb2c4f6422a753038fe137d90ae7c1af57251316
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_app_badger: b87fc231847b03b92ce1412aa351842e7e97932f
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
GoogleDataTransport: 54dee9d48d14580407f8f5fbf2f496e92437a2f2
GoogleUtilities: 13e2c67ede716b8741c7989e26893d151b2b2084
image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5
libphonenumber_plugin: e8a7d64a6624a7c25f2c4ab0b7ead2a8e341e35e
nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431
package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
PhoneNumberKit: 43b5169526cc417398c8f13f77c97552c1c6ed76
PromisesObjC: c50d2056b5253dadbd6c2bea79b0674bd5a52fa4
SDWebImage: 750adf017a315a280c60fde706ab1e552a3ae4e9
share_plus: 599aa54e4ea31d4b4c0e9c911bcc26c55e791028
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a
SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f
syncfusion_flutter_pdfviewer: bb9998884b864cfedf72628df3503bdf57e397c0
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4
PODFILE CHECKSUM: e21c9d4c7b9623c73c6784ddc132fd50a603ad93
PODFILE CHECKSUM: 3d89a90682e2cd438911ad748b8ba5992f8b6a0e
COCOAPODS: 1.16.2
COCOAPODS: 1.12.1
+8 -34
View File
@@ -8,8 +8,8 @@
/* Begin PBXBuildFile section */
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
33FDB0982EE9ABDC000B2391 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 33FDB0972EE9ABDC000B2391 /* GoogleService-Info.plist */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
6483E4432A911EA00063B51E /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 6483E4422A911EA00063B51E /* GoogleService-Info.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 */; };
@@ -33,10 +33,10 @@
/* 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>"; };
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>"; };
64801C012A9112D500E8B558 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
6483E4422A911EA00063B51E /* GoogleService-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "../../../../Downloads/GoogleService-Info.plist"; 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>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
@@ -115,12 +115,12 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
33FDB0972EE9ABDC000B2391 /* GoogleService-Info.plist */,
64801C012A9112D500E8B558 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
6483E4422A911EA00063B51E /* GoogleService-Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
@@ -144,7 +144,6 @@
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
174B54D80220E5F588BD9737 /* [CP] Embed Pods Frameworks */,
859FAB4E05FAC31B7B1A62D7 /* [CP] Copy Pods Resources */,
);
buildRules = (
);
@@ -162,7 +161,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1510;
LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@@ -196,7 +195,7 @@
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
33FDB0982EE9ABDC000B2391 /* GoogleService-Info.plist in Resources */,
6483E4432A911EA00063B51E /* GoogleService-Info.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
@@ -213,14 +212,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";
@@ -242,27 +237,6 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
};
859FAB4E05FAC31B7B1A62D7 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
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";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -375,7 +349,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 = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -460,7 +434,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 = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -509,7 +483,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 = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -26,7 +26,6 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
@@ -44,13 +43,11 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
+1 -1
View File
@@ -1,7 +1,7 @@
import UIKit
import Flutter
@main
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
-15
View File
@@ -2,19 +2,6 @@
<!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>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
@@ -39,8 +26,6 @@
<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>UIApplicationSupportsIndirectInputEvents</key>
-5
View File
@@ -4,10 +4,5 @@
<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>
-54
View File
@@ -1,54 +0,0 @@
<?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>
@@ -1,25 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="j1y-V4-xli">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<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">
<view key="view" contentMode="scaleToFill" id="wbc-yd-nQP">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<viewLayoutGuide key="safeArea" id="bcg-RR-FT9"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="CzN-xT-EUl" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
-93
View File
@@ -1,93 +0,0 @@
# iOS Share Extension — Xcode Setup
Die Quellen unter `ios/Share Extension/` müssen einmalig in Xcode als **Share Extension Target** verdrahtet werden — analog zur `TimetableWidgetExtension`. Erst danach taucht „Marianum Fulda" im System-Share-Sheet auf.
## Schritt 1 — Share-Extension-Target anlegen
1. `ios/Runner.xcworkspace` in Xcode öffnen.
2. Projekt-Sidebar → `Runner` (Projekt-Root) → **+ Add Target** unten links.
3. **iOS → Share Extension** wählen.
4. Eigenschaften:
- Product Name: `Share Extension` (mit Leerzeichen, exakt so — der Ordnername und Podfile-Eintrag matchen).
- Bundle Identifier: `eu.mhsl.marianum.mobile.client.Share-Extension`.
- Language: Swift.
- Embed in: Runner.
5. Beim Activate-Scheme-Dialog auf **Cancel** klicken.
6. Deployment Target = mind. iOS 12.0 (Plugin-Mindestanforderung).
## Schritt 2 — Vorhandene Quelldateien ins Target ziehen
Xcode legt Dummy-Dateien an. Diese **löschen** (Move to Trash). Dann:
1. Sidebar → Rechtsklick auf den Ordner `Share Extension`**Add Files to "Runner"…**
2. Im File-Picker zu `ios/Share Extension/` navigieren und folgende Dateien selektieren:
- `ShareViewController.swift`
- `Info.plist`
- `MainInterface.storyboard`
- `Share Extension.entitlements`
3. **Wichtig**: bei „Add to targets" nur `Share Extension` ankreuzen, **nicht** Runner.
## Schritt 3 — App Group aktivieren
Beide Targets brauchen die App-Group-Berechtigung, damit die Extension geteilte Dateien für die Hauptapp im gemeinsamen Container ablegen kann.
1. **Runner**-Target → **Signing & Capabilities****+ Capability** → **App Groups**.
- Group-ID hinzufügen: `group.eu.mhsl.marianum.mobile.client.share` (zusätzlich zur bereits existierenden Widget-Group).
2. Dasselbe für **Share Extension**-Target — mit derselben Group-ID `group.eu.mhsl.marianum.mobile.client.share`.
Im Apple-Developer-Portal muss diese App-Group bei beiden App-IDs eingetragen sein, sonst schlägt das Provisioning fehl.
## Schritt 4 — User-Defined Build Setting `CUSTOM_GROUP_ID`
Beide Targets brauchen das User-Defined Setting, das in `Runner/Info.plist` und `Share Extension/Info.plist` als `$(CUSTOM_GROUP_ID)` referenziert wird.
1. **Runner** → Build Settings → `+` (oben links) → **Add User-Defined Setting**.
- Name: `CUSTOM_GROUP_ID`
- Wert: `group.eu.mhsl.marianum.mobile.client.share`
2. Dasselbe für **Share Extension**-Target.
## Schritt 5 — Entitlements verlinken
1. **Runner** → Build Settings → `CODE_SIGN_ENTITLEMENTS` zeigt bereits auf `Runner/Runner.entitlements` (jetzt mit beiden Groups).
2. **Share Extension** → Build Settings → `CODE_SIGN_ENTITLEMENTS` → auf `Share Extension/Share Extension.entitlements` setzen.
## Schritt 6 — Info.plist-Pfad
**Share Extension** → Build Settings → `INFOPLIST_FILE` → auf `Share Extension/Info.plist` setzen.
## Schritt 7 — Build Phases reorder
Damit das Plugin-Modul vom Extension-Target gefunden wird:
1. **Runner**-Target → **Build Phases**.
2. `Embed Foundation Extensions` per Drag-and-Drop **vor** `Thin Binary` ziehen.
## Schritt 8 — Pods installieren
```bash
cd ios && pod install
```
Der Podfile-Eintrag (`target 'Share Extension' do inherit! :search_paths end`) ist bereits vorhanden.
## Schritt 9 — Build & Run
1. Scheme `Runner` wählen → Run auf Device oder Simulator (≥ iOS 12).
2. Foto in der Fotos-App auswählen → Teilen → „Marianum Fulda" sollte erscheinen.
3. Auswahl → App öffnet sich, ShareTargetPage erscheint.
## Troubleshooting
- **Error: No such module 'receive_sharing_intent'**
→ Schritt 7 (Build Phases reorder) wurde übersprungen.
- **Error: Frameworks' not allowed in extension**
→ In Build Settings der Share Extension `Other Linker Flags` und `Framework Search Paths` leeren (nur die geerbten Pod-Pfade behalten).
- **Share-Sheet zeigt App nicht an**
`NSExtensionActivationRule`-Limits in `Share Extension/Info.plist` zu klein? Werte testweise erhöhen. Außerdem: App muss **mindestens einmal nach Install** geöffnet worden sein, sonst wird die Extension von iOS nicht registriert.
- **Files kommen mit `nil` Pfad an**
→ App-Group nicht konsistent. Prüfen, dass `CUSTOM_GROUP_ID` in beiden Targets identisch ist und die Entitlement-Files dieselbe Group enthalten.
## Was am Mac noch zu tun ist
- Schritte 18 oben (~15 Min).
- Auf physischem iPhone testen — Simulator-Share-Sheet ist eingeschränkt.
@@ -1,10 +0,0 @@
<?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>
@@ -1,8 +0,0 @@
import UIKit
import receive_sharing_intent
class ShareViewController: RSIShareViewController {
override func shouldAutoRedirect() -> Bool {
return true
}
}
@@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -1,16 +0,0 @@
{
"images" : [
{
"filename" : "marianum_m_white.svg",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"preserves-vector-representation" : true,
"template-rendering-intent" : "template"
}
}
@@ -1,21 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 2.6 KiB

-29
View File
@@ -1,29 +0,0 @@
<?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>
@@ -1,28 +0,0 @@
import SwiftUI
/// Marianum-M peeking out of the bottom-right corner. Sized to the longer
/// widget edge so it scales with resize; offset nudges a sliver behind the
/// edge.
struct MarianumWatermark: View {
@Environment(\.colorScheme) private var colorScheme
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(.primary)
.frame(width: markSize, height: markSize)
.opacity(colorScheme == .dark ? 0.025 : 0.014)
.offset(x: offsetX, y: offsetY)
}
}
.clipped()
}
}
-72
View File
@@ -1,72 +0,0 @@
# iOS Widget Extension — Xcode Setup
Die Swift-Quellen unter `ios/TimetableWidgetExtension/` müssen einmalig in Xcode als **Widget Extension Target** verdrahtet werden — ohne diesen Schritt bleibt der Code unkompiliert.
## Schritt 1 — Widget-Extension-Target anlegen
1. `ios/Runner.xcworkspace` in Xcode öffnen.
2. Projekt-Sidebar → `Runner` (Projekt-Root) → **+ Add Target** unten links.
3. **iOS → Widget Extension** wählen.
4. Eigenschaften:
- Product Name: `TimetableWidgetExtension`
- Bundle Identifier: `eu.mhsl.marianum.mobile.client.TimetableWidgetExtension`
- Language: Swift
- Include Configuration Intent: **OFF** (StaticConfiguration reicht)
- Embed in: Runner
5. Beim Activate-Scheme-Dialog auf **Cancel** klicken.
## Schritt 2 — Vorhandene Quelldateien ins Target ziehen
Xcode hat zunächst Dummy-Dateien (`TimetableWidgetExtension.swift`, `TimetableWidgetExtensionBundle.swift`) angelegt. Diese **löschen** (Move to Trash). Dann:
1. Sidebar → Rechtsklick auf den Ordner `TimetableWidgetExtension`**Add Files to "Runner"…**
2. Im File-Picker zu `ios/TimetableWidgetExtension/` navigieren und alle `.swift`-Dateien, die `Info.plist`, `TimetableWidgetExtension.entitlements` **und den `Assets.xcassets`-Ordner** selektieren (mit `marianum_m`-Asset darin — gleicher Asset-Name wie auf Android-Seite).
3. **Wichtig**: bei „Add to targets" nur `TimetableWidgetExtension` ankreuzen, **nicht** Runner.
## Schritt 3 — App Group aktivieren
Beide Targets brauchen die App-Group-Berechtigung, damit Hauptapp und Widget über `UserDefaults(suiteName:)` schreiben/lesen können.
1. **Runner**-Target → **Signing & Capabilities****+ Capability** → **App Groups**.
- Group-ID hinzufügen: `group.eu.mhsl.marianum.mobile.client.widget`
2. Dasselbe für **TimetableWidgetExtension** — mit derselben Group-ID.
Im Apple-Developer-Portal muss die App-Group bei beiden App-IDs eingetragen sein, sonst schlägt das Provisioning fehl.
## Schritt 4 — Entitlements verlinken
1. **Runner** → Build Settings → `CODE_SIGN_ENTITLEMENTS` sollte bereits auf `Runner/Runner.entitlements` zeigen.
2. **TimetableWidgetExtension** → Build Settings → `CODE_SIGN_ENTITLEMENTS` → auf `TimetableWidgetExtension/TimetableWidgetExtension.entitlements` setzen.
## Schritt 5 — Info.plist + Deployment Target
1. **TimetableWidgetExtension** → Build Settings → `INFOPLIST_FILE` → auf `TimetableWidgetExtension/Info.plist` setzen.
2. Build Settings → `IPHONEOS_DEPLOYMENT_TARGET` ≥ 16.0 (Code gated `.containerBackground` mit `if #available(iOS 17, *)`, läuft also auch auf 16).
## Schritt 6 — Build & Run
- Scheme `Runner` (nicht das Widget-Scheme) wählen → Run.
- Auf Home-Screen langes Drücken → Widget hinzufügen → "Marianum · Heute" / "Marianum · Woche".
- Widget-Tap öffnet die App im zuletzt sichtbaren Tab. Eine Tab-Navigation auf den Stundenplan ist bewusst nicht implementiert (Android nutzt Intent-Extras, iOS würde dafür ein URL-Scheme oder AppIntent brauchen — beides bewusst ausgespart).
## Troubleshooting
- **Widget zeigt „Lade…"** auch nach Refresh: App-Group greift nicht. Prüfen, ob beide Targets dieselbe Group-ID haben und das Provisioning aktualisiert wurde.
- **Stale-Daten nach Logout**: `WidgetSync.clear()` schreibt `widget_data_logged_in_v1 = false`; Widget zeigt dann den Login-Placeholder.
- **Lessons um 12 Stunden verschoben**: Date-Parser-Bug. Sollte gefixt sein in `WidgetData.swift::parseDartDate` — verifizieren, dass die ISO-8601-Strings ohne Z-Suffix als `TimeZone.current` geparsed werden.
- **App-Store-Submit später**: `Runner.entitlements` `aps-environment` von `development` auf `production` umbiegen.
## Was bereits im Repo erledigt ist
- Alle Swift-Quellen, Info.plist, Entitlements liegen unter `ios/TimetableWidgetExtension/`.
- App-Group-ID konsistent zwischen Dart (`WidgetSync.iosAppGroupId`), Swift (`WidgetDataKey.appGroupId`) und der Entitlements-Datei.
- `home_widget`-Plugin auf der Dart-Seite konfiguriert; ruft `HomeWidget.setAppGroupId` beim ersten Sync.
- `containerBackground` für iOS 17+ gegated, fällt auf iOS 16 sauber zurück.
- Date-Parser fixt das fehlende Z-Suffix (Dart schreibt lokale Zeit ohne TZ-Marker).
## Was am Mac noch zu tun ist
- Schritte 15 oben in Xcode durchklicken (1015 Min).
- `flutter pub get` + `cd ios && pod install`.
- Auf physischem Gerät oder iOS-Simulator (≥ 16.0) bauen.
- Widget aufs Home-Screen ziehen, prüfen dass Lesson-Zeiten korrekt rendern.
@@ -1,451 +0,0 @@
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
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())
.widgetThemeOverride(entry.themeMode)
}
@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,
anchorDate: data.anchorDate,
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(.primary)
Spacer()
Text("Stand: \(freshnessLabel(for: data.fetchedAt))")
.font(.system(size: 10))
.foregroundStyle(.secondary)
}
}
private func emptyState(text: String) -> some View {
VStack {
Spacer()
Text(text)
.font(.caption)
.foregroundStyle(.secondary)
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))
Text(message)
.font(.caption)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct TimeGridView: View {
let lessons: [WidgetLesson]
let periods: [WidgetPeriod]
let anchorDate: Date
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
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 {
timeLabelsColumn
.frame(width: 32, alignment: .topTrailing)
Rectangle()
.fill(Color.primary.opacity(0.13))
.frame(width: 1)
}
ZStack(alignment: .top) {
gridLines
breakBlocks
ForEach(lessons.indices, id: \.self) { idx in
lessonBlock(lessons[idx])
}
if Calendar.current.isDate(anchorDate, inSameDayAs: Date()) {
nowIndicator
}
}
.frame(maxWidth: .infinity, minHeight: totalHeight, alignment: .top)
}
}
private var timeLabelsColumn: some View {
ZStack(alignment: .topTrailing) {
// Hour rules continue through the time-label column so it reads
// as a real table column rather than a free-floating tick list.
// Hour rules extend through the time-label column so it reads
// as a table column rather than a free-floating tick list.
ForEach(periodBoundaries(periods), id: \.self) { virtualMin in
Rectangle()
.fill(Color.primary.opacity(0.08))
.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(.primary)
.lineLimit(1)
} else {
Text(formatHm(period.startMinutes))
.font(.system(size: 9))
.foregroundStyle(.primary)
.lineLimit(1)
Text("\(period.name).")
.font(.system(size: 7, weight: .bold))
.foregroundStyle(.secondary)
.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) {
// Hour rules continue through the time-label column so it reads
// as a real table column rather than a free-floating tick list.
ForEach(periodBoundaries(periods), id: \.self) { virtualMin in
Rectangle()
.fill(Color.primary.opacity(0.08))
.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(Color.primary.opacity(0.03))
.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 var nowIndicator: some View {
let cal = Calendar.current
let comps = cal.dateComponents([.hour, .minute], from: Date())
let nowMinutes = (comps.hour ?? 0) * 60 + (comps.minute ?? 0)
let inside: Bool
if let first = periods.first, let last = periods.last {
inside = nowMinutes >= first.startMinutes && nowMinutes <= last.endMinutes
} else {
inside = true
}
let top = realMinutesToVirtual(nowMinutes, periods: periods) * hourHeight / 60.0
return Group {
if inside {
Rectangle()
.fill(Color.red)
.frame(height: 2)
.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)
}
@@ -1,161 +0,0 @@
import SwiftUI
import WidgetKit
struct TimetableWeekView: View {
let entry: TimetableEntry
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())
.widgetThemeOverride(entry.themeMode)
}
@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(Color.primary.opacity(0.13))
.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(.primary)
Spacer()
Text("Stand: \(freshnessLabel(for: data.fetchedAt))")
.font(.system(size: 10))
.foregroundStyle(.secondary)
}
}
private func dayHeaderRow(data: WidgetTimetableData) -> some View {
let cal = Calendar.current
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(.primary)
Text(shortDate(day))
.font(.system(size: 9))
.foregroundStyle(.secondary)
}
.frame(maxWidth: .infinity)
if offset < 4 { columnDivider }
}
}
}
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(Color.primary.opacity(0.08))
.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(.primary)
.lineLimit(1)
Text("\(period.name).")
.font(.system(size: 6, weight: .bold))
.foregroundStyle(.secondary)
.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,
anchorDate: day,
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))
Text(message)
.font(.caption)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
@@ -1,10 +0,0 @@
<?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>
@@ -1,138 +0,0 @@
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).widgetContainerBackground()
}
.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).widgetContainerBackground()
}
.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 {
@ViewBuilder
func widgetThemeOverride(_ mode: String) -> some View {
switch mode {
case "light": self.environment(\.colorScheme, .light)
case "dark": self.environment(\.colorScheme, .dark)
default: self
}
}
/// `.containerBackground(_:for:)` is iOS 17+. Older iOS uses the
/// implicit `.background(...)` model and renders fine without it.
@ViewBuilder
func widgetContainerBackground() -> some View {
if #available(iOS 17.0, *) {
self.containerBackground(.fill.tertiary, for: .widget)
} else {
self
}
}
}
@@ -1,128 +0,0 @@
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
}
}
}
+10
View File
@@ -0,0 +1,10 @@
class ApiError {
String message;
ApiError(this.message);
@override
String toString() {
return 'ApiError: $message';
}
}
+3
View File
@@ -0,0 +1,3 @@
class ApiParams {
}

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