1
0
forked from firka/firka

android: timetable home widget

This commit is contained in:
2025-08-13 14:13:13 +02:00
parent 4e0acd2147
commit 1768244685
12 changed files with 469 additions and 54 deletions

View File

@@ -0,0 +1,66 @@
package app.firka.naplo
import app.firka.naplo.model.NameUid
import app.firka.naplo.model.NameUidDesc
import app.firka.naplo.model.Subject
import org.json.JSONObject
fun JSONObject.getStringOrNull(key: String): String? {
return try {
if (has(key)) {
getString(key)
} else {
null
}
} catch (_: Exception) {
null
}
}
fun JSONObject.getIntOrNull(key: String): Int? {
return try {
if (has(key)) {
getInt(key)
} else {
null
}
} catch (_: Exception) {
null
}
}
fun JSONObject.getNameUidDescOrNull(key: String): NameUidDesc? {
try {
return if (has(key)) {
NameUidDesc(getJSONObject(key))
} else {
null
}
} catch (_: Exception) {
return null
}
}
fun JSONObject.getNameUidOrNull(key: String): NameUid? {
try {
return if (has(key)) {
NameUid(getJSONObject(key))
} else {
null
}
} catch (_: Exception) {
return null
}
}
fun JSONObject.getSubjectOrNull(key: String): Subject? {
return try {
if (has(key)) {
Subject(getJSONObject(key))
} else {
null
}
} catch (_: Exception) {
null
}
}

View File

@@ -1,45 +0,0 @@
package app.firka.naplo.glance
import HomeWidgetGlanceState
import HomeWidgetGlanceStateDefinition
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.provideContent
import androidx.glance.background
import androidx.glance.currentState
import androidx.glance.layout.Box
import androidx.glance.layout.Column
import androidx.glance.layout.padding
import androidx.glance.state.GlanceStateDefinition
import androidx.glance.text.Text
class AppWidget : GlanceAppWidget() {
override val stateDefinition: GlanceStateDefinition<*>?
get() = HomeWidgetGlanceStateDefinition()
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
GlanceContent(context, currentState())
}
}
@Composable
private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) {
val prefs = currentState.preferences
val counter = prefs.getInt("counter", 0)
Box(modifier = GlanceModifier.background(Color.White).padding(16.dp)) {
Column {
Text(
counter.toString()
)
}
}
}
}

View File

@@ -0,0 +1,111 @@
package app.firka.naplo.glance
import androidx.compose.runtime.Composable
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.glance.GlanceModifier
import androidx.glance.appwidget.cornerRadius
import androidx.glance.background
import androidx.glance.color.ColorProvider
import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.Row
import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.padding
import androidx.glance.layout.width
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import app.firka.naplo.model.Colors
import app.firka.naplo.model.Lesson
import java.time.format.DateTimeFormatterBuilder
val hhmm = DateTimeFormatterBuilder()
.appendPattern("HH:mm")
.toFormatter()
@Composable
fun LessonCard(lesson: Lesson, colors: Colors,
modifier: GlanceModifier = GlanceModifier) {
Box(modifier =
modifier
.fillMaxWidth()
.padding(4.dp, 0.dp)
.cornerRadius(16.dp)
.background(colors.card)
) {
var bgColor = colors.a15p
var fgColor = colors.textSecondary
if (lesson.substituteTeacher == null) {
bgColor = colors.warning15p
fgColor = colors.warningText
}
Box(modifier = GlanceModifier.padding(12.dp)) {
Row {
Row(modifier = GlanceModifier.width(226.dp), verticalAlignment = Alignment.CenterVertically) {
if (lesson.lessonNumber != null) {
Box(modifier = GlanceModifier.cornerRadius(16.dp).background(bgColor)) {
Text(
lesson.lessonNumber.toString(),
style = TextStyle(
color = ColorProvider(fgColor, fgColor),
fontSize = 14.sp,
fontWeight = FontWeight.Bold
),
modifier = GlanceModifier.padding(8.dp, 4.dp),
)
}
Spacer(modifier = GlanceModifier.width(4.dp))
}
// TODO: Add subject icons
Text(
lesson.name,
style = TextStyle(
color = ColorProvider(colors.textPrimary, colors.textPrimary),
fontSize = 14.sp,
fontWeight = FontWeight.Bold
),
)
}
// Spacer(modifier = GlanceModifier.width(10.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
lesson.start.format(hhmm),
style = TextStyle(
color = ColorProvider(colors.textPrimary, colors.textPrimary),
fontSize = 14.sp,
fontWeight = FontWeight.Bold
),
)
Spacer(modifier = GlanceModifier.width(8.dp))
Box(modifier = GlanceModifier.cornerRadius(16.dp).background(colors.a15p)) {
var roomName = "N/A";
if (lesson.roomName != null) {
roomName = lesson.roomName!!;
}
if (roomName.length < 2) {
roomName = " $roomName"
}
Text(
roomName,
style = TextStyle(
color = ColorProvider(colors.textSecondary, colors.textSecondary),
fontSize = 14.sp,
fontWeight = FontWeight.Bold
),
modifier = GlanceModifier.padding(8.dp, 4.dp),
)
}
}
}
}
}
}

View File

@@ -0,0 +1,111 @@
package app.firka.naplo.glance
import HomeWidgetGlanceState
import HomeWidgetGlanceStateDefinition
import android.content.Context
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.glance.GlanceId
import androidx.glance.GlanceModifier
import androidx.glance.appwidget.GlanceAppWidget
import androidx.glance.appwidget.provideContent
import androidx.glance.background
import androidx.glance.color.ColorProvider
import androidx.glance.currentState
import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.Column
import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxSize
import androidx.glance.layout.height
import androidx.glance.layout.padding
import androidx.glance.state.GlanceStateDefinition
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import app.firka.naplo.model.Colors
import app.firka.naplo.model.Lesson
import org.json.JSONObject
import java.io.File
import java.time.LocalDate
import java.time.LocalDateTime
class TimetableWidget : GlanceAppWidget() {
override val stateDefinition: GlanceStateDefinition<*>?
get() = HomeWidgetGlanceStateDefinition()
override suspend fun provideGlance(context: Context, id: GlanceId) {
provideContent {
GlanceContent(context, currentState())
}
}
@Composable
private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) {
val appFlutter = File(context.applicationContext.dataDir, "app_flutter")
val widgetStateFile = File(appFlutter, "widget_state.json")
if (!widgetStateFile.exists()) {
Box(modifier =
GlanceModifier
.background(Color(0xFFFAFFF0))
.padding(16.dp)
.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
Text(
"Widget használata előtt jelentkezz be",
style = TextStyle(
color = ColorProvider(Color(0xFF394C0A), Color(0xFF394C0A)),
fontSize = 12.sp,
fontWeight = FontWeight.Medium
)
)
}
return
}
val widgetState = JSONObject(widgetStateFile.readText(Charsets.UTF_8))
val colors = Colors(widgetState)
val tt = widgetState.getJSONArray("timetable")
var lessons = mutableListOf<Lesson>()
for (i in 0..<tt.length()) {
lessons.add(Lesson(tt.getJSONObject(i)))
}
val now = LocalDate.now()
val start = LocalDateTime.of(now.year, now.month, now.dayOfMonth, 0, 0)
val end = start.plusHours(23)
lessons = lessons.filter { lesson -> lesson.start.isAfter(start) && lesson.end.isBefore(end) }.toMutableList()
Box(modifier =
GlanceModifier
.background(colors.background)
.padding(16.dp)
.fillMaxSize()
) {
Column {
Text(
"Mai órarend",
style = TextStyle(
color = ColorProvider(colors.textSecondary, colors.textSecondary),
fontSize = 12.sp,
fontWeight = FontWeight.Medium
)
)
Spacer(modifier = GlanceModifier.height(4.dp))
for (lesson in lessons) {
LessonCard(lesson, colors)
Spacer(modifier = GlanceModifier.height(4.dp))
}
}
}
}
}

View File

@@ -2,6 +2,6 @@ package app.firka.naplo.glance
import HomeWidgetGlanceWidgetReceiver
class TimetableWidgetReceiver : HomeWidgetGlanceWidgetReceiver<AppWidget>() {
override val glanceAppWidget = AppWidget()
class TimetableWidgetReceiver : HomeWidgetGlanceWidgetReceiver<TimetableWidget>() {
override val glanceAppWidget = TimetableWidget()
}

View File

@@ -0,0 +1,37 @@
package app.firka.naplo.model
import androidx.compose.ui.graphics.Color
import org.json.JSONObject
class Colors(widgetState: JSONObject) {
var background: Color = Color(widgetState.getJSONObject("colors").getInt("background"))
var backgroundAmoled: Color =
Color(widgetState.getJSONObject("colors").getInt("backgroundAmoled"))
var background0p: Color = Color(widgetState.getJSONObject("colors").getInt("background0p"))
var success: Color = Color(widgetState.getJSONObject("colors").getInt("success"))
var textPrimary: Color = Color(widgetState.getJSONObject("colors").getInt("textPrimary"))
var textSecondary: Color = Color(widgetState.getJSONObject("colors").getInt("textSecondary"))
var textTertiary: Color = Color(widgetState.getJSONObject("colors").getInt("textTertiary"))
var card: Color = Color(widgetState.getJSONObject("colors").getInt("card"))
var cardTranslucent: Color =
Color(widgetState.getJSONObject("colors").getInt("cardTranslucent"))
var buttonSecondaryFill: Color =
Color(widgetState.getJSONObject("colors").getInt("buttonSecondaryFill"))
var accent: Color = Color(widgetState.getJSONObject("colors").getInt("accent"))
var secondary: Color = Color(widgetState.getJSONObject("colors").getInt("secondary"))
var shadowColor: Color = Color(widgetState.getJSONObject("colors").getInt("shadowColor"))
var a15p: Color = Color(widgetState.getJSONObject("colors").getInt("a15p"))
var warningAccent: Color = Color(widgetState.getJSONObject("colors").getInt("warningAccent"))
var warningText: Color = Color(widgetState.getJSONObject("colors").getInt("warningText"))
var warning15p: Color = Color(widgetState.getJSONObject("colors").getInt("warning15p"))
var warningCard: Color = Color(widgetState.getJSONObject("colors").getInt("warningCard"))
var errorAccent: Color = Color(widgetState.getJSONObject("colors").getInt("errorAccent"))
var errorText: Color = Color(widgetState.getJSONObject("colors").getInt("errorText"))
var error15p: Color = Color(widgetState.getJSONObject("colors").getInt("error15p"))
var errorCard: Color = Color(widgetState.getJSONObject("colors").getInt("errorCard"))
var grade5: Color = Color(widgetState.getJSONObject("colors").getInt("grade5"))
var grade4: Color = Color(widgetState.getJSONObject("colors").getInt("grade4"))
var grade3: Color = Color(widgetState.getJSONObject("colors").getInt("grade3"))
var grade2: Color = Color(widgetState.getJSONObject("colors").getInt("grade2"))
var grade1: Color = Color(widgetState.getJSONObject("colors").getInt("grade1"))
}

View File

@@ -0,0 +1,94 @@
package app.firka.naplo.model
import app.firka.naplo.getIntOrNull
import app.firka.naplo.getNameUidDescOrNull
import app.firka.naplo.getNameUidOrNull
import app.firka.naplo.getStringOrNull
import app.firka.naplo.getSubjectOrNull
import org.json.JSONObject
import java.time.LocalDateTime
import java.time.format.DateTimeFormatterBuilder
class Lesson {
val formatter = DateTimeFormatterBuilder()
.appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS")
.optionalStart()
.appendLiteral('Z')
.optionalEnd()
.toFormatter()
constructor(data: JSONObject) {
uid = data.getString("Uid")
date = data.getString("Datum")
start = LocalDateTime.parse(data.getString("KezdetIdopont"), formatter)
end = LocalDateTime.parse(data.getString("VegIdopont"), formatter)
name = data.getString("Nev")
lessonNumber = data.getIntOrNull("Oraszam")
lessonSeqNumber = data.getIntOrNull("OraEvesSorszama")
classGroup = data.getNameUidOrNull("OsztalyCsoport")
teacher = data.getStringOrNull("TanarNeve")
subject = data.getSubjectOrNull("Tantargy")
theme = data.getStringOrNull("Tema")
roomName = data.getStringOrNull("TeremNeve")
type = NameUidDesc(data.getJSONObject("Tipus"))
studentPresence = data.getNameUidDescOrNull("TanuloJelenlet")
state = NameUidDesc(data.getJSONObject("Allapot"))
substituteTeacher = data.getStringOrNull("HelyettesTanarNeve")
homeworkUid = data.getStringOrNull("HaziFeladatUid")
taskGroupUid = data.getStringOrNull("FeladatGroupUid")
languageTaskGroupUid = data.getStringOrNull("NyelviFeladatGroupUid")
assessmentUid = data.getStringOrNull("BejelentettSzamonkeresUid")
canStudentEditHomework = data.getBoolean("IsTanuloHaziFeladatEnabled")
isHomeworkComplete = data.getBoolean("IsHaziFeladatMegoldva")
if (data.has("Csatolmanyok")) {
val rawAttachments = data.getJSONArray("Csatolmanyok")
for (i in 0..<rawAttachments.length()) {
attachments.add(NameUid(rawAttachments.getJSONObject(i)))
}
}
isDigitalLesson = data.getBoolean("IsDigitalisOra")
digitalDeviceList = data.getStringOrNull("DigitalisEszkozTipus")
digitalPlatformType = data.getStringOrNull("DigitalisPlatformTipus")
if (data.has("DigitalisTamogatoEszkozTipusList")) {
val rawDigitalSupportDeviceTypeList =
data.getJSONArray("DigitalisTamogatoEszkozTipusList")
for (i in 0..<rawDigitalSupportDeviceTypeList.length()) {
digitalSupportDeviceTypeList.add(rawDigitalSupportDeviceTypeList.getString(i))
}
}
createdAt = LocalDateTime.parse(data.getString("Letrehozas"), formatter)
lastModifiedAt = LocalDateTime.parse(data.getString("UtolsoModositas"), formatter)
}
var uid: String;
var date: String;
var start: LocalDateTime;
var end: LocalDateTime;
var name: String;
var lessonNumber: Int?;
var lessonSeqNumber: Int?;
var classGroup: NameUid?;
var teacher: String?;
var subject: Subject?;
var theme: String?;
var roomName: String?;
var type: NameUidDesc;
var studentPresence: NameUidDesc?;
var state: NameUidDesc;
var substituteTeacher: String?;
var homeworkUid: String?;
var taskGroupUid: String?;
var languageTaskGroupUid: String?;
var assessmentUid: String?;
var canStudentEditHomework: Boolean;
var isHomeworkComplete: Boolean;
var attachments = mutableListOf<NameUid>()
var isDigitalLesson: Boolean
var digitalDeviceList: String?
var digitalPlatformType: String?
var digitalSupportDeviceTypeList = mutableListOf<String>()
var createdAt: LocalDateTime
var lastModifiedAt: LocalDateTime
}

View File

@@ -0,0 +1,9 @@
package app.firka.naplo.model
import app.firka.naplo.getStringOrNull
import org.json.JSONObject
class NameUid(data: JSONObject) {
var uid: String = data.getString("Uid")
var name: String? = data.getStringOrNull("Nev")
}

View File

@@ -0,0 +1,10 @@
package app.firka.naplo.model
import app.firka.naplo.getStringOrNull
import org.json.JSONObject
class NameUidDesc(data: JSONObject) {
var uid: String = data.getString("Uid")
var name: String? = data.getStringOrNull("Nev")
var description: String? = data.getStringOrNull("Leiras")
}

View File

@@ -0,0 +1,12 @@
package app.firka.naplo.model
import app.firka.naplo.getNameUidDescOrNull
import app.firka.naplo.getStringOrNull
import org.json.JSONObject
class Subject(data: JSONObject) {
var uid: String? = data.getStringOrNull("Uid")
var name: String? = data.getStringOrNull("Nev")
var category: NameUidDesc? = data.getNameUidDescOrNull("Kategoria")
var sortIndex: Int = data.getInt("SortIndex")
}

View File

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

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:math';
import 'package:firka/helpers/api/client/kreta_client.dart';
import 'package:firka/main.dart';
@@ -9,6 +10,7 @@ import 'package:firka/ui/phone/widgets/bottom_nav_icon.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:home_widget/home_widget.dart';
import 'package:majesticons_flutter/majesticons_flutter.dart';
import '../../../../helpers/db/widget.dart';
@@ -77,20 +79,24 @@ class _HomeScreenState extends State<HomeScreen> {
try {
_prefetched = true;
var random = Random();
ApiResponse<Object> res = await data.client.getGrades(forceCache: false);
if (res.err != null) throw res.err!;
if (res.err != null) throw "await data.client.getGrades\n${res.err!}";
await Future.delayed(Duration(seconds: 1 + random.nextInt(2)));
var now = timeNow();
var start = now.subtract(Duration(days: now.weekday - 1));
var end = start.add(Duration(days: 7));
var end = start.add(Duration(days: 6));
res = await data.client.getTimeTable(start, end, forceCache: false);
if (res.err != null) throw "await data.client.getTimeTable\n${res.err!}";
await WidgetCacheHelper.updateWidgetCache(appStyle, data.client);
if (res.err != null) throw res.err!;
await HomeWidget.updateWidget(
qualifiedAndroidName: "app.firka.naplo.glance.TimetableWidget");
} catch (e) {
activeToast = ActiveToastType.error;
@@ -133,7 +139,11 @@ class _HomeScreenState extends State<HomeScreen> {
Majesticon.questionCircleSolid,
color: appStyle.colors.errorAccent, size: 24),
onTap: () {
showErrorBottomSheet(context, e.toString());
var stackTrace = "";
if (e is Error && e.stackTrace != null) {
stackTrace = e.stackTrace.toString();
}
showErrorBottomSheet(context, "$e\n$stackTrace");
},
),
],