added base homescreen-widget setup, working on Android, iOS in progress
This commit is contained in:
@@ -2,7 +2,9 @@
|
||||
<application
|
||||
android:label="Marianum Fulda"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -29,6 +31,32 @@
|
||||
<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
|
||||
@@ -42,4 +70,7 @@
|
||||
</intent>
|
||||
</queries>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<!-- Workmanager periodic widget refresh needs to reschedule after device
|
||||
reboot, otherwise the widget freezes until the user opens the app. -->
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
|
||||
</manifest>
|
||||
|
||||
@@ -1,5 +1,42 @@
|
||||
package eu.mhsl.marianum.mobile.client
|
||||
|
||||
import android.content.Intent
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MainActivity: FlutterActivity()
|
||||
class MainActivity : FlutterActivity() {
|
||||
private val widgetChannel = "eu.mhsl.marianum.widget"
|
||||
/// Last seen widget tap target. Cleared by Dart via `consumePendingNavigation`
|
||||
/// so the same intent isn't replayed on every resume.
|
||||
private var pendingTimetableTap: Boolean = false
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
MethodChannel(
|
||||
flutterEngine.dartExecutor.binaryMessenger,
|
||||
widgetChannel
|
||||
).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"consumePendingNavigation" -> {
|
||||
val pending = pendingTimetableTap
|
||||
pendingTimetableTap = false
|
||||
result.success(pending)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
consumeIntentData(intent)
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
consumeIntentData(intent)
|
||||
}
|
||||
|
||||
private fun consumeIntentData(intent: Intent?) {
|
||||
if (intent?.getBooleanExtra("widget_open_timetable", false) == true) {
|
||||
pendingTimetableTap = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package eu.mhsl.marianum.mobile.client
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import eu.mhsl.marianum.mobile.client.widgets.WidgetRenderer
|
||||
|
||||
/**
|
||||
* Lives at the package root (not under `widgets/`) because the home_widget
|
||||
* Flutter plugin resolves the receiver class as `<app-package>.<androidName>`.
|
||||
*/
|
||||
class TimetableDayWidget : AppWidgetProvider() {
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
for (id in appWidgetIds) {
|
||||
val options = appWidgetManager.getAppWidgetOptions(id)
|
||||
val views = WidgetRenderer.buildDay(context, context.packageName, options)
|
||||
appWidgetManager.updateAppWidget(id, views)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAppWidgetOptionsChanged(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetId: Int,
|
||||
newOptions: Bundle,
|
||||
) {
|
||||
// Re-render on resize, otherwise the tiles stay at install-time size
|
||||
// and either clip or leave dead space.
|
||||
val views = WidgetRenderer.buildDay(context, context.packageName, newOptions)
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package eu.mhsl.marianum.mobile.client
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.appwidget.AppWidgetProvider
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import eu.mhsl.marianum.mobile.client.widgets.WidgetRenderer
|
||||
|
||||
class TimetableWeekWidget : AppWidgetProvider() {
|
||||
override fun onUpdate(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetIds: IntArray,
|
||||
) {
|
||||
for (id in appWidgetIds) {
|
||||
val options = appWidgetManager.getAppWidgetOptions(id)
|
||||
val views = WidgetRenderer.buildWeek(context, context.packageName, options)
|
||||
appWidgetManager.updateAppWidget(id, views)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAppWidgetOptionsChanged(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetId: Int,
|
||||
newOptions: Bundle,
|
||||
) {
|
||||
val views = WidgetRenderer.buildWeek(context, context.packageName, newOptions)
|
||||
appWidgetManager.updateAppWidget(appWidgetId, views)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package eu.mhsl.marianum.mobile.client.widgets
|
||||
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
// Mirror of lib/widget_data/widget_data.dart — JSON keys + enum names
|
||||
// must stay in sync.
|
||||
enum class WidgetLessonStatus {
|
||||
REGULAR, ONGOING, PAST, CANCELLED, IRREGULAR, TEACHER_CHANGED, EVENT;
|
||||
|
||||
companion object {
|
||||
fun fromWire(raw: String?): WidgetLessonStatus = when (raw) {
|
||||
"regular" -> REGULAR
|
||||
"ongoing" -> ONGOING
|
||||
"past" -> PAST
|
||||
"cancelled" -> CANCELLED
|
||||
"irregular" -> IRREGULAR
|
||||
"teacherChanged" -> TEACHER_CHANGED
|
||||
"event" -> EVENT
|
||||
else -> REGULAR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class WidgetLesson(
|
||||
val start: Date,
|
||||
val end: Date,
|
||||
val subjectShort: String,
|
||||
val subjectLong: String?,
|
||||
val room: String?,
|
||||
val teacher: String?,
|
||||
val originalTeacher: String?,
|
||||
val status: WidgetLessonStatus,
|
||||
val customColor: String?,
|
||||
val siblingCount: Int,
|
||||
)
|
||||
|
||||
data class WidgetPeriod(
|
||||
val name: String,
|
||||
val startMinutes: Int,
|
||||
val endMinutes: Int,
|
||||
val virtualStartMinutes: Int,
|
||||
val virtualEndMinutes: Int,
|
||||
)
|
||||
|
||||
data class WidgetTimetableData(
|
||||
val fetchedAt: Date,
|
||||
val anchorDate: Date,
|
||||
val lessons: List<WidgetLesson>,
|
||||
val periods: List<WidgetPeriod>,
|
||||
val isHoliday: Boolean,
|
||||
val holidayName: String?,
|
||||
)
|
||||
|
||||
object WidgetDataParser {
|
||||
private val isoFormat: SimpleDateFormat
|
||||
get() = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS", Locale.ROOT).apply {
|
||||
timeZone = TimeZone.getDefault()
|
||||
}
|
||||
|
||||
/// Dart's toIso8601String() ships microseconds (6 digits) when non-zero;
|
||||
/// SimpleDateFormat only parses 3 → strip the extra digits. Local time
|
||||
/// without Z is the default, so removeSuffix("Z") makes the parser
|
||||
/// tolerate both shapes.
|
||||
private fun parseDate(raw: String?): Date? {
|
||||
if (raw.isNullOrEmpty()) return null
|
||||
val cleaned = raw
|
||||
.replace(Regex("([.,]\\d{3})\\d+"), "$1")
|
||||
.removeSuffix("Z")
|
||||
return try {
|
||||
isoFormat.parse(cleaned)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun parse(json: String?): WidgetTimetableData? {
|
||||
if (json.isNullOrEmpty()) return null
|
||||
return try {
|
||||
val root = JSONObject(json)
|
||||
val lessonsArray = root.optJSONArray("lessons")
|
||||
val lessons = mutableListOf<WidgetLesson>()
|
||||
if (lessonsArray != null) {
|
||||
for (i in 0 until lessonsArray.length()) {
|
||||
val obj = lessonsArray.optJSONObject(i) ?: continue
|
||||
val start = parseDate(obj.stringOrNull("start")) ?: continue
|
||||
val end = parseDate(obj.stringOrNull("end")) ?: continue
|
||||
lessons += WidgetLesson(
|
||||
start = start,
|
||||
end = end,
|
||||
subjectShort = obj.stringOrNull("subjectShort") ?: "",
|
||||
subjectLong = obj.stringOrNull("subjectLong"),
|
||||
room = obj.stringOrNull("room"),
|
||||
teacher = obj.stringOrNull("teacher"),
|
||||
originalTeacher = obj.stringOrNull("originalTeacher"),
|
||||
status = WidgetLessonStatus.fromWire(obj.stringOrNull("status")),
|
||||
customColor = obj.stringOrNull("customColor"),
|
||||
siblingCount = obj.optInt("siblingCount", 0),
|
||||
)
|
||||
}
|
||||
}
|
||||
val periodsArray = root.optJSONArray("periods")
|
||||
val periods = mutableListOf<WidgetPeriod>()
|
||||
if (periodsArray != null) {
|
||||
for (i in 0 until periodsArray.length()) {
|
||||
val obj = periodsArray.optJSONObject(i) ?: continue
|
||||
periods += WidgetPeriod(
|
||||
name = obj.stringOrNull("name") ?: "",
|
||||
startMinutes = obj.optInt("startMinutes", 0),
|
||||
endMinutes = obj.optInt("endMinutes", 0),
|
||||
virtualStartMinutes = obj.optInt("virtualStartMinutes", 0),
|
||||
virtualEndMinutes = obj.optInt("virtualEndMinutes", 0),
|
||||
)
|
||||
}
|
||||
}
|
||||
WidgetTimetableData(
|
||||
fetchedAt = parseDate(root.stringOrNull("fetchedAt")) ?: Date(),
|
||||
anchorDate = parseDate(root.stringOrNull("anchorDate")) ?: Date(),
|
||||
lessons = lessons,
|
||||
periods = periods,
|
||||
isHoliday = root.optBoolean("isHoliday", false),
|
||||
holidayName = root.stringOrNull("holidayName"),
|
||||
)
|
||||
} catch (_: JSONException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun JSONObject.stringOrNull(key: String): String? {
|
||||
if (!has(key) || isNull(key)) return null
|
||||
val raw = optString(key, "")
|
||||
return if (raw.isEmpty()) null else raw
|
||||
}
|
||||
}
|
||||
|
||||
object WidgetDateUtils {
|
||||
fun startOfDay(date: Date): Date {
|
||||
val cal = Calendar.getInstance().apply { time = date }
|
||||
cal.set(Calendar.HOUR_OF_DAY, 0)
|
||||
cal.set(Calendar.MINUTE, 0)
|
||||
cal.set(Calendar.SECOND, 0)
|
||||
cal.set(Calendar.MILLISECOND, 0)
|
||||
return cal.time
|
||||
}
|
||||
|
||||
fun isSameDay(a: Date, b: Date): Boolean {
|
||||
val ca = Calendar.getInstance().apply { time = a }
|
||||
val cb = Calendar.getInstance().apply { time = b }
|
||||
return ca.get(Calendar.YEAR) == cb.get(Calendar.YEAR) &&
|
||||
ca.get(Calendar.DAY_OF_YEAR) == cb.get(Calendar.DAY_OF_YEAR)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,786 @@
|
||||
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"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Plain rounded rectangle. We deliberately avoid ?attr/appWidgetRadius
|
||||
because RemoteViews inflation does not resolve custom theme attributes
|
||||
reliably across launchers. Hard-coded 20dp matches the system home-screen
|
||||
radius closely on stock Android. -->
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="20dp" />
|
||||
<solid android:color="@color/widget_background" />
|
||||
</shape>
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
Direct port of /home/elias/Bilder/marianum_m_white.svg.
|
||||
- viewport matches the source SVG (70mm × 82.2mm).
|
||||
- Transforms recreate the SVG's three nested matrices: outer scale+offset,
|
||||
inner translate, and a final scale+y-flip that pulls the path data into
|
||||
the visible coordinate space.
|
||||
- Tinted at runtime via setColorFilter so light/dark themes can share one
|
||||
drawable.
|
||||
-->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="70dp"
|
||||
android:height="82dp"
|
||||
android:viewportWidth="70.000168"
|
||||
android:viewportHeight="82.227348">
|
||||
|
||||
<group
|
||||
android:scaleX="0.26458333"
|
||||
android:scaleY="0.26458334"
|
||||
android:translateX="107.44411"
|
||||
android:translateY="-80.482198">
|
||||
<group
|
||||
android:translateX="-749.41293"
|
||||
android:translateY="290.52252">
|
||||
<group
|
||||
android:scaleX="0.13333333"
|
||||
android:scaleY="-0.13333333"
|
||||
android:translateX="0"
|
||||
android:translateY="632.14667">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M 3499.67,4594.33 c -16.43,-106.19 -29.71,-199.97 -43.79,-293.49 86.83,-19 138.5,-27.61 223.38,-43.82 63.81,-12.18 175.24,-20.4 179.64,-83.23 6.46,-92.69 -124.69,-55.41 -188.38,-43.81 -84.33,15.36 -159.13,28.84 -232.2,43.81 -13.68,-60.72 -26.83,-118.68 -39.43,-179.61 -36.76,-178.32 -73.67,-368.16 -105.11,-551.97 18.09,25.66 30.84,42.72 43.8,65.7 66.7,118.26 140.39,245.04 227.76,354.83 33.49,42.05 76.86,94.81 118.31,113.91 98.42,45.36 166.68,-22.2 170.87,-118.28 3.68,-85.28 -23.09,-181.17 -35.08,-275.99 -12.4,-98.19 -22.89,-194.93 -35.03,-275.98 72.44,102.69 147.93,269.64 240.95,381.12 27.51,33 73.55,80.61 118.27,87.62 218.76,34.33 126.58,-312.17 127.05,-473.13 0.4,-144.9 44.01,-255.37 175.21,-271.59 43.02,-5.31 105.84,11.16 112.7,-26.34 8.67,-47.38 -78.15,-60.52 -125.84,-61.28 -291.34,-4.51 -322.06,262.33 -311.01,573.88 -19.85,-18.57 -35.71,-47.53 -52.57,-74.47 -97.59,-155.88 -203.95,-327.22 -297.92,-503.79 -25.93,-48.79 -53.68,-114.7 -135.8,-83.23 -17.27,6.63 -48.25,44.39 -52.56,56.96 -19.58,57.19 1.55,137.42 8.76,205.89 21.54,203.72 57.81,389.09 78.87,587.01 -26.3,0.51 -43.93,-30.07 -56.96,-48.2 -46.9,-65.27 -86.02,-140.76 -127.04,-214.64 -52.84,-95.15 -108.23,-192.84 -157.71,-293.52 -75.25,-153.09 -188.6,-501.89 -242.12,-678.81 -8.67,-28.67 -17.7,-58.08 -26.3,-87.64 -7.48,-25.72 -10.39,-57.68 -35.05,-74.46 -100.02,18.93 -89.71,104.89 -70.09,205.9 47.35,243.43 170.89,706.45 211.48,946.04 -72.97,-70.97 -153.99,-207.41 -289.14,-236.55 -136.47,-29.44 -217.95,47.68 -271.6,122.66 -17.14,23.96 -41.43,49.54 -26.29,78.84 83.96,35.51 113.37,-65.2 197.15,-74.47 22.65,-2.5 54.56,2.4 74.46,8.78 132.4,42.34 237.57,218.76 297.87,346.07 74.16,156.45 125.32,330.5 148.95,490.64 -65.71,11.4 -142.96,22.15 -219.25,36.52 -109.8,20.72 -158.81,10.75 -201.29,59.86 9.15,41.95 41.41,60.8 70.1,83.24 126.26,-16.84 252.45,-33.77 372.36,-56.97 20.43,89.25 51.98,218.51 74.45,311.05 40.53,26.02 88.88,-8.43 105.17,-35.06 z" />
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="@color/widget_divider" />
|
||||
</shape>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape android:shape="rectangle">
|
||||
<corners android:radius="6dp" />
|
||||
<solid android:color="@color/widget_lesson_cancelled" />
|
||||
<stroke android:width="1.5dp" android:color="#C8FF0000" />
|
||||
</shape>
|
||||
</item>
|
||||
<item
|
||||
android:left="3dp"
|
||||
android:top="3dp"
|
||||
android:right="3dp"
|
||||
android:bottom="3dp"
|
||||
android:drawable="@drawable/widget_lesson_cancelled_x" />
|
||||
</layer-list>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="6dp" />
|
||||
<solid android:color="#FF2196F3" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="6dp" />
|
||||
<solid android:color="#FF4CAF50" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="6dp" />
|
||||
<solid android:color="#FFEF6C00" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="6dp" />
|
||||
<solid android:color="#FF993333" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="6dp" />
|
||||
<solid android:color="@color/widget_lesson_irregular" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="6dp" />
|
||||
<solid android:color="@color/widget_lesson_ongoing" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="6dp" />
|
||||
<solid android:color="@color/widget_lesson_past" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="6dp" />
|
||||
<solid android:color="@color/widget_lesson_regular" />
|
||||
</shape>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<corners android:radius="6dp" />
|
||||
<solid android:color="@color/widget_lesson_teacher_changed" />
|
||||
</shape>
|
||||
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="100dp"
|
||||
android:height="100dp"
|
||||
android:viewportWidth="100"
|
||||
android:viewportHeight="100">
|
||||
<path
|
||||
android:strokeColor="#C8FF0000"
|
||||
android:strokeWidth="5"
|
||||
android:pathData="M0,0 L100,100 M100,0 L0,100" />
|
||||
</vector>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/widget_break_block_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="14dp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginEnd="2dp" />
|
||||
@@ -0,0 +1,91 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- FrameLayout root so the Marianum watermark can sit at bottom|end behind
|
||||
the actual widget content without disturbing the LinearLayout flow. -->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/widget_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/app_widget_background">
|
||||
|
||||
<!-- Bottom-leading so the mark looks like it's peeking out of the lower
|
||||
left corner. Width/height/start+bottom margins are overridden in the
|
||||
renderer so the mark scales with the widget size. -->
|
||||
<ImageView
|
||||
android:id="@+id/widget_watermark"
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="160dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:scaleType="fitCenter"
|
||||
android:importantForAccessibility="no"
|
||||
android:contentDescription="@null"
|
||||
android:alpha="0.025"
|
||||
android:src="@drawable/marianum_m_watermark" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="10dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
<TextView
|
||||
android:id="@+id/widget_day_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/widget_text_primary"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
tools:text="Heute · 08.05." />
|
||||
<TextView
|
||||
android:id="@+id/widget_day_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="10sp"
|
||||
android:textColor="@color/widget_text_secondary"
|
||||
tools:text="Stand: 14:32" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginTop="6dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/widget_day_time_labels"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/widget_divider" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/widget_day_grid"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_day_empty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
android:textSize="13sp"
|
||||
android:textColor="@color/widget_text_secondary"
|
||||
android:gravity="center"
|
||||
android:padding="12dp"
|
||||
tools:text="Keine Stunden" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,185 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/widget_background">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="160dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginEnd="-28dp"
|
||||
android:layout_marginBottom="-28dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:importantForAccessibility="no"
|
||||
android:contentDescription="@null"
|
||||
android:alpha="0.018"
|
||||
android:src="@drawable/marianum_m_watermark" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="10dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/widget_text_primary"
|
||||
android:singleLine="true"
|
||||
android:text="Heute · 06.05." />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="10sp"
|
||||
android:textColor="@color/widget_text_secondary"
|
||||
android:text="Stand: 07:50" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="6dp">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
|
||||
android:orientation="vertical" android:gravity="end" android:paddingEnd="3dp"
|
||||
android:layout_marginTop="22dp">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:textSize="11sp" android:textStyle="bold"
|
||||
android:textColor="@color/widget_text_primary" android:text="1." />
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:textSize="7sp" android:textColor="@color/widget_text_secondary" android:text="07:55" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
|
||||
android:orientation="vertical" android:gravity="end" android:paddingEnd="3dp"
|
||||
android:layout_marginTop="50dp">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:textSize="11sp" android:textStyle="bold"
|
||||
android:textColor="@color/widget_text_primary" android:text="2." />
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:textSize="7sp" android:textColor="@color/widget_text_secondary" android:text="08:40" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
|
||||
android:orientation="vertical" android:gravity="end" android:paddingEnd="3dp"
|
||||
android:layout_marginTop="79dp">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:textSize="11sp" android:textStyle="bold"
|
||||
android:textColor="@color/widget_text_primary" android:text="3." />
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:textSize="7sp" android:textColor="@color/widget_text_secondary" android:text="09:30" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
|
||||
android:orientation="vertical" android:gravity="end" android:paddingEnd="3dp"
|
||||
android:layout_marginTop="115dp">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:textSize="11sp" android:textStyle="bold"
|
||||
android:textColor="@color/widget_text_primary" android:text="4." />
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:textSize="7sp" android:textColor="@color/widget_text_secondary" android:text="10:35" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
|
||||
android:orientation="vertical" android:gravity="end" android:paddingEnd="3dp"
|
||||
android:layout_marginTop="142dp">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:textSize="11sp" android:textStyle="bold"
|
||||
android:textColor="@color/widget_text_primary" android:text="5." />
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:textSize="7sp" android:textColor="@color/widget_text_secondary" android:text="11:25" />
|
||||
</LinearLayout>
|
||||
<LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content"
|
||||
android:orientation="vertical" android:gravity="end" android:paddingEnd="3dp"
|
||||
android:layout_marginTop="169dp">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:textSize="11sp" android:textStyle="bold"
|
||||
android:textColor="@color/widget_text_primary" android:text="6." />
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:textSize="7sp" android:textColor="@color/widget_text_secondary" android:text="12:15" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1">
|
||||
|
||||
<FrameLayout android:layout_width="match_parent" android:layout_height="1dp" android:layout_marginTop="29dp" android:background="@color/widget_divider" />
|
||||
<FrameLayout android:layout_width="match_parent" android:layout_height="1dp" android:layout_marginTop="56dp" android:background="@color/widget_divider" />
|
||||
<FrameLayout android:layout_width="match_parent" android:layout_height="1dp" android:layout_marginTop="83dp" android:background="@color/widget_divider" />
|
||||
<FrameLayout android:layout_width="match_parent" android:layout_height="1dp" android:layout_marginTop="115dp" android:background="@color/widget_divider" />
|
||||
<FrameLayout android:layout_width="match_parent" android:layout_height="1dp" android:layout_marginTop="143dp" android:background="@color/widget_divider" />
|
||||
<FrameLayout android:layout_width="match_parent" android:layout_height="1dp" android:layout_marginTop="170dp" android:background="@color/widget_divider" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="22dp"
|
||||
android:layout_marginTop="29dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:padding="3dp"
|
||||
android:background="@drawable/widget_lesson_block_regular">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:textSize="11sp" android:textStyle="bold"
|
||||
android:textColor="@android:color/white" android:text="MA-LK" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="55dp"
|
||||
android:layout_marginTop="83dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:padding="3dp"
|
||||
android:background="@drawable/widget_lesson_block_regular">
|
||||
<LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:textSize="11sp" android:textStyle="bold"
|
||||
android:textColor="@android:color/white" android:text="DE-GK" />
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:textSize="9sp" android:textColor="#CCFFFFFF" android:text="B11" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="22dp"
|
||||
android:layout_marginTop="143dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:padding="3dp"
|
||||
android:background="@drawable/widget_lesson_block_cancelled">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:textSize="11sp" android:textStyle="bold"
|
||||
android:textColor="@android:color/white" android:text="BIO" />
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="22dp"
|
||||
android:layout_marginTop="170dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:padding="3dp"
|
||||
android:background="@drawable/widget_lesson_block_teacher_changed">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content"
|
||||
android:textSize="11sp" android:textStyle="bold"
|
||||
android:textColor="@android:color/white" android:text="GE" />
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/widget_grid_line_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:background="@drawable/widget_grid_line" />
|
||||
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/widget_lesson_block_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="40dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:paddingStart="7dp"
|
||||
android:paddingEnd="7dp"
|
||||
android:paddingTop="3dp"
|
||||
android:paddingBottom="3dp"
|
||||
android:background="@drawable/widget_lesson_block_regular">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="top"
|
||||
android:baselineAligned="false">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_lesson_subject"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@android:color/white"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:gravity="top|start"
|
||||
android:includeFontPadding="false"
|
||||
android:autoSizeTextType="uniform"
|
||||
android:autoSizeMinTextSize="5sp"
|
||||
android:autoSizeMaxTextSize="11sp"
|
||||
android:autoSizeStepGranularity="1sp"
|
||||
tools:text="MA-LK" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/widget_lesson_secondary_stack"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:gravity="top|end"
|
||||
android:layout_marginStart="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_lesson_room"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="12dp"
|
||||
android:textColor="#CCFFFFFF"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:gravity="end|top"
|
||||
android:includeFontPadding="false"
|
||||
android:autoSizeTextType="uniform"
|
||||
android:autoSizeMinTextSize="5sp"
|
||||
android:autoSizeMaxTextSize="11sp"
|
||||
android:autoSizeStepGranularity="1sp"
|
||||
tools:text="A12" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_lesson_teacher"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="12dp"
|
||||
android:textColor="#B3FFFFFF"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:gravity="end|top"
|
||||
android:includeFontPadding="false"
|
||||
android:autoSizeTextType="uniform"
|
||||
android:autoSizeMinTextSize="5sp"
|
||||
android:autoSizeMaxTextSize="11sp"
|
||||
android:autoSizeStepGranularity="1sp"
|
||||
tools:text="Müller" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_lesson_sibling_badge"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|start"
|
||||
android:textStyle="bold"
|
||||
android:textSize="12sp"
|
||||
android:textColor="@android:color/white"
|
||||
android:maxLines="1"
|
||||
android:includeFontPadding="false"
|
||||
android:visibility="gone"
|
||||
tools:text="+1" />
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?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" />
|
||||
@@ -0,0 +1,46 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/widget_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/app_widget_background">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/widget_watermark"
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="160dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:scaleType="fitCenter"
|
||||
android:importantForAccessibility="no"
|
||||
android:contentDescription="@null"
|
||||
android:alpha="0.025"
|
||||
android:src="@drawable/marianum_m_watermark" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_placeholder_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/widget_text_primary"
|
||||
tools:text="Marianum Vertretungsplan" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_placeholder_message"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:textColor="@color/widget_text_secondary"
|
||||
tools:text="Bitte einloggen, um den Stundenplan zu laden" />
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,31 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/widget_time_label_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="0dp"
|
||||
android:orientation="vertical"
|
||||
android:gravity="end"
|
||||
android:paddingEnd="3dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_time_label_time"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="9sp"
|
||||
android:textColor="@color/widget_text_primary"
|
||||
android:singleLine="true"
|
||||
android:lineSpacingExtra="-2dp"
|
||||
tools:text="07:55" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_time_label_number"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="7sp"
|
||||
android:textColor="@color/widget_text_secondary"
|
||||
android:singleLine="true"
|
||||
android:lineSpacingExtra="-2dp"
|
||||
tools:text="1." />
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,185 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/widget_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@drawable/app_widget_background">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/widget_watermark"
|
||||
android:layout_width="160dp"
|
||||
android:layout_height="160dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:scaleType="fitCenter"
|
||||
android:importantForAccessibility="no"
|
||||
android:contentDescription="@null"
|
||||
android:alpha="0.025"
|
||||
android:src="@drawable/marianum_m_watermark" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="10dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
<TextView
|
||||
android:id="@+id/widget_week_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/widget_text_primary"
|
||||
android:singleLine="true"
|
||||
android:ellipsize="end"
|
||||
tools:text="KW 19 · 06.05.–10.05." />
|
||||
<TextView
|
||||
android:id="@+id/widget_week_subtitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="10sp"
|
||||
android:textColor="@color/widget_text_secondary"
|
||||
tools:text="Stand: 14:32" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="6dp">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/widget_divider" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/widget_week_header_mon"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/widget_divider" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/widget_week_header_tue"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/widget_divider" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/widget_week_header_wed"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/widget_divider" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/widget_week_header_thu"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/widget_divider" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/widget_week_header_fri"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginTop="2dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/widget_week_time_labels"
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/widget_divider" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/widget_week_col_mon"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/widget_divider" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/widget_week_col_tue"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/widget_divider" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/widget_week_col_wed"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/widget_divider" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/widget_week_col_thu"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/widget_divider" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/widget_week_col_fri"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/widget_week_day_header_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingBottom="3dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_week_day_header_weekday"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="11sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/widget_text_primary"
|
||||
tools:text="Mo" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/widget_week_day_header_date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="9sp"
|
||||
android:textColor="@color/widget_text_secondary"
|
||||
tools:text="06.05." />
|
||||
</LinearLayout>
|
||||
@@ -0,0 +1,147 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Static preview for the week widget. Five day columns with two short
|
||||
demo blocks each so the structure is recognisable in the picker. -->
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/widget_background">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="200dp"
|
||||
android:layout_height="200dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginEnd="-36dp"
|
||||
android:layout_marginBottom="-36dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:importantForAccessibility="no"
|
||||
android:contentDescription="@null"
|
||||
android:alpha="0.018"
|
||||
android:src="@drawable/marianum_m_watermark" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:padding="10dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="@color/widget_text_primary"
|
||||
android:singleLine="true"
|
||||
android:text="KW 19 · 06.05.–10.05." />
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="10sp"
|
||||
android:textColor="@color/widget_text_secondary"
|
||||
android:text="Stand: 07:50" />
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Day-name + date row -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="6dp">
|
||||
<FrameLayout android:layout_width="20dp" android:layout_height="wrap_content" />
|
||||
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:gravity="center">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="11sp" android:textStyle="bold" android:textColor="@color/widget_text_primary" android:text="Mo" />
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textColor="@color/widget_text_secondary" android:text="06.05." />
|
||||
</LinearLayout>
|
||||
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:gravity="center">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="11sp" android:textStyle="bold" android:textColor="@color/widget_text_primary" android:text="Di" />
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textColor="@color/widget_text_secondary" android:text="07.05." />
|
||||
</LinearLayout>
|
||||
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:gravity="center">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="11sp" android:textStyle="bold" android:textColor="@color/widget_text_primary" android:text="Mi" />
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textColor="@color/widget_text_secondary" android:text="08.05." />
|
||||
</LinearLayout>
|
||||
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:gravity="center">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="11sp" android:textStyle="bold" android:textColor="@color/widget_text_primary" android:text="Do" />
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textColor="@color/widget_text_secondary" android:text="09.05." />
|
||||
</LinearLayout>
|
||||
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
|
||||
<LinearLayout android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:orientation="vertical" android:gravity="center">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="11sp" android:textStyle="bold" android:textColor="@color/widget_text_primary" android:text="Fr" />
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textColor="@color/widget_text_secondary" android:text="10.05." />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Time grid: time-label column + 5 day columns -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="2dp">
|
||||
|
||||
<FrameLayout android:layout_width="20dp" android:layout_height="match_parent">
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="0dp" android:gravity="end" android:paddingEnd="3dp" android:textSize="8sp" android:textColor="@color/widget_text_secondary" android:text="08" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="36dp" android:gravity="end" android:paddingEnd="3dp" android:textSize="8sp" android:textColor="@color/widget_text_secondary" android:text="10" />
|
||||
<TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="72dp" android:gravity="end" android:paddingEnd="3dp" android:textSize="8sp" android:textColor="@color/widget_text_secondary" android:text="12" />
|
||||
</FrameLayout>
|
||||
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
|
||||
|
||||
<!-- Mon -->
|
||||
<FrameLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1">
|
||||
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="2dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_regular">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="MA" />
|
||||
</FrameLayout>
|
||||
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="40dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_regular">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="EN" />
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
|
||||
|
||||
<!-- Tue -->
|
||||
<FrameLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1">
|
||||
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="20dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_cancelled">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="BIO" />
|
||||
</FrameLayout>
|
||||
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="60dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_regular">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="DE" />
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
|
||||
|
||||
<!-- Wed -->
|
||||
<FrameLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1">
|
||||
<FrameLayout android:layout_width="match_parent" android:layout_height="32dp" android:layout_marginTop="2dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_regular">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="MA" />
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
|
||||
|
||||
<!-- Thu -->
|
||||
<FrameLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1">
|
||||
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="2dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_teacher_changed">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="GE" />
|
||||
</FrameLayout>
|
||||
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="40dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_regular">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="PH" />
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
<FrameLayout android:layout_width="1dp" android:layout_height="match_parent" android:background="@color/widget_divider" />
|
||||
|
||||
<!-- Fri -->
|
||||
<FrameLayout android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1">
|
||||
<FrameLayout android:layout_width="match_parent" android:layout_height="14dp" android:layout_marginTop="20dp" android:layout_marginStart="1dp" android:layout_marginEnd="1dp" android:padding="2dp" android:background="@drawable/widget_lesson_block_regular">
|
||||
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:textSize="9sp" android:textStyle="bold" android:textColor="@android:color/white" android:text="EN" />
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="widget_background">#FF1F1716</color>
|
||||
<color name="widget_text_primary">#FFF1F1F1</color>
|
||||
<color name="widget_text_secondary">#FFB0B0B0</color>
|
||||
<color name="widget_divider">#33FFFFFF</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="widget_day_label">Marianum · Heute</string>
|
||||
<string name="widget_week_label">Marianum · Woche</string>
|
||||
<string name="widget_day_description">Stundenplan und Vertretungen für den anstehenden Schultag.</string>
|
||||
<string name="widget_week_description">Stundenplan und Vertretungen für die ganze Schulwoche.</string>
|
||||
<string name="widget_no_lessons">Keine Stunden</string>
|
||||
<string name="widget_holiday">Ferien</string>
|
||||
<string name="widget_login_required">Bitte einloggen, um den Stundenplan zu laden</string>
|
||||
<string name="widget_loading">Lade…</string>
|
||||
<string name="widget_status_label">Stand: %1$s</string>
|
||||
<string name="widget_today">Heute</string>
|
||||
<string name="widget_tomorrow">Morgen</string>
|
||||
<string name="widget_placeholder_title">Marianum Stundenplan</string>
|
||||
<string name="widget_calendar_week_prefix">KW</string>
|
||||
<string name="widget_yesterday_prefix">gestern</string>
|
||||
</resources>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Status colors mirror lib/view/pages/timetable/data/lesson_color.dart
|
||||
exactly so widget tiles match the in-app calendar. -->
|
||||
<color name="widget_lesson_regular">#FF993333</color>
|
||||
<color name="widget_lesson_ongoing">#FFC83333</color>
|
||||
<color name="widget_lesson_past">#FF993333</color>
|
||||
<color name="widget_lesson_cancelled">#FF000000</color>
|
||||
<color name="widget_lesson_irregular">#FF8F19B3</color>
|
||||
<color name="widget_lesson_teacher_changed">#FF29639B</color>
|
||||
<color name="widget_lesson_event">#FF2E7D32</color>
|
||||
|
||||
<color name="widget_background">#FFFCF7F5</color>
|
||||
<color name="widget_text_primary">#FF111111</color>
|
||||
<color name="widget_text_secondary">#FF555555</color>
|
||||
<color name="widget_divider">#22000000</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Auto-Backup rules for Android 11 and below.
|
||||
Excludes the home_widget plugin's SharedPreferences file
|
||||
(HomeWidgetPreferences) so the cached timetable — which contains
|
||||
teacher names, room numbers and personal custom events — is not
|
||||
uploaded to the user's Google Drive. -->
|
||||
<full-backup-content>
|
||||
<exclude domain="sharedpref" path="HomeWidgetPreferences.xml"/>
|
||||
</full-backup-content>
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Backup + device-transfer rules for Android 12+.
|
||||
Excludes the home_widget plugin's SharedPreferences file
|
||||
(HomeWidgetPreferences) so the cached timetable — which contains
|
||||
teacher names, room numbers and personal custom events — is not
|
||||
uploaded to the user's Google Drive or transferred to a new device.
|
||||
The widget repopulates from a fresh Webuntis fetch after sign-in. -->
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<exclude domain="sharedpref" path="HomeWidgetPreferences.xml"/>
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<exclude domain="sharedpref" path="HomeWidgetPreferences.xml"/>
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/widget_placeholder"
|
||||
android:minWidth="110dp"
|
||||
android:minHeight="180dp"
|
||||
android:targetCellWidth="2"
|
||||
android:targetCellHeight="5"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:widgetCategory="home_screen"
|
||||
android:updatePeriodMillis="0"
|
||||
android:description="@string/widget_day_description"
|
||||
android:previewLayout="@layout/widget_day_preview" />
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/widget_placeholder"
|
||||
android:minWidth="320dp"
|
||||
android:minHeight="240dp"
|
||||
android:targetCellWidth="5"
|
||||
android:targetCellHeight="5"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:widgetCategory="home_screen"
|
||||
android:updatePeriodMillis="0"
|
||||
android:description="@string/widget_week_description"
|
||||
android:previewLayout="@layout/widget_week_preview" />
|
||||
Reference in New Issue
Block a user