Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd3884de16 | ||
|
|
c646ea2d51 | ||
|
|
a22459794a | ||
|
|
91a526703e | ||
|
|
38ff8af578 | ||
|
|
58c16e9aa8 | ||
|
|
748bff63ea | ||
|
|
812c1a008e | ||
|
|
b71aa12751 | ||
|
|
c16cbdb186 | ||
|
|
8f28fa328c | ||
|
|
8af53422dc | ||
|
|
dda4bfd9d3 | ||
|
|
d92e420b34 | ||
|
|
b54fa36671 | ||
|
|
60375e93d1 | ||
|
|
eb1e4b4cfd | ||
|
|
f4eb4e7487 | ||
|
|
584f340778 | ||
|
|
b9de46f0ed | ||
|
|
0f3dcf58a5 | ||
|
|
b58e60a1f8 | ||
|
|
42b8eea0ba | ||
|
|
ce9781f1c0 | ||
|
|
e5224cbfff | ||
|
|
2d14c41070 | ||
|
|
ecb1745d9e | ||
|
|
0abc568a64 | ||
|
|
0845290929 | ||
|
|
3a0eb5fe54 | ||
|
|
503a51ca23 | ||
|
|
0781685015 | ||
|
|
2a4836c42f | ||
|
|
4ff6f2fdb0 | ||
|
|
f80ce9bc4f | ||
|
|
91bf7a359c | ||
|
|
6d7d3641ea | ||
|
|
873e0f209b | ||
|
|
8d768ca6b8 | ||
|
|
229eabfd4f | ||
|
|
80599c13d8 | ||
|
|
c92e83aadd | ||
|
|
47670fb558 | ||
|
|
b8058cd4cb | ||
|
|
4fd3e2a09b | ||
|
|
0e0fa549cf | ||
| 39e9c097a0 | |||
|
|
ea8315a993 | ||
|
|
6d33f6b0d8 | ||
|
|
8c4bbd0905 | ||
|
|
fe70fc7bd1 | ||
|
|
eb3ed957f1 | ||
| cd525898bb | |||
|
|
626d6aefdd |
15
.gitmodules
vendored
@@ -1,9 +1,18 @@
|
||||
[submodule "firka/vendor/isar_generator"]
|
||||
path = firka/vendor/isar_generator
|
||||
url = https://git.qwit.cloud/firka/isar_generator
|
||||
[submodule "firka/vendor/isar"]
|
||||
path = firka/vendor/isar
|
||||
url = https://git.qwit.cloud/firka/isar
|
||||
[submodule "firka/vendor/isar_flutter_libs"]
|
||||
path = firka/vendor/isar_flutter_libs
|
||||
url = https://git.qwit.cloud/firka/isar_flutter_libs
|
||||
[submodule "firka/lib/l10n"]
|
||||
path = firka/lib/l10n
|
||||
url = https://github.com/QwIT-Development/firka-localization
|
||||
[submodule "firka_wear/vendor/wear_plus"]
|
||||
path = firka_wear/vendor/wear_plus
|
||||
url = https://git.firka.app/firka/wear_plus
|
||||
[submodule "firka/android/app/src/main/java/org/brotli"]
|
||||
path = firka/android/app/src/main/java/org/brotli
|
||||
url = https://git.firka.app/firka/org_brotli
|
||||
[submodule "firka_wear/lib/l10n"]
|
||||
path = firka_wear/lib/l10n
|
||||
url = https://github.com/qwit-development/firka-localization
|
||||
|
||||
5
firka/.gitignore
vendored
@@ -49,7 +49,4 @@ app.*.map.json
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
coverage
|
||||
|
||||
# Generated files
|
||||
*.g.dart
|
||||
coverage
|
||||
@@ -1,18 +1,36 @@
|
||||
import org.apache.commons.io.FileUtils
|
||||
import java.io.FileInputStream
|
||||
import java.security.MessageDigest
|
||||
import java.util.Properties
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.Future
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipFile
|
||||
import java.util.zip.ZipOutputStream
|
||||
import java.util.zip.ZipOutputStream.DEFLATED
|
||||
import java.util.zip.ZipOutputStream.STORED
|
||||
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("kotlin-android")
|
||||
id("org.jetbrains.kotlin.plugin.compose")
|
||||
id("org.jetbrains.kotlin.plugin.compose") version "2.2.0"
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
fun loadProperties(file: File): Properties {
|
||||
val properties = Properties()
|
||||
FileInputStream(file).use { inputStream ->
|
||||
properties.load(inputStream)
|
||||
}
|
||||
return properties
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "app.firka.naplo"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
ndkVersion = flutter.ndkVersion
|
||||
ndkVersion = "27.0.12077973"
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
@@ -31,8 +49,8 @@ android {
|
||||
applicationId = "app.firka.naplo"
|
||||
// You can update the following values to match your application needs.
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = flutter.targetSdkVersion
|
||||
minSdk = 29
|
||||
targetSdk = 36
|
||||
versionCode = flutter.versionCode
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
@@ -41,7 +59,7 @@ android {
|
||||
val propsFile = File(secretsDir, "keystore.properties")
|
||||
|
||||
if (propsFile.exists()) {
|
||||
val props = Properties().apply { FileInputStream(propsFile).use { load(it) } }
|
||||
val props = loadProperties(propsFile)
|
||||
val store = File(secretsDir, props["storeFile"].toString())
|
||||
|
||||
signingConfigs {
|
||||
@@ -50,10 +68,6 @@ android {
|
||||
storePassword = props["storePassword"] as String
|
||||
keyPassword = props["keyPassword"] as String
|
||||
keyAlias = props["keyAlias"] as String
|
||||
// Use APK Signature Scheme v3 (and v4 for streaming verification). See:
|
||||
// https://source.android.com/docs/security/features/apksigning/v3
|
||||
enableV3Signing = true
|
||||
enableV4Signing = true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -75,25 +89,808 @@ android {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
implementation("androidx.wear:wear-ongoing:1.0.0")
|
||||
implementation("androidx.glance:glance-appwidget:1.1.1")
|
||||
implementation("com.google.android.gms:play-services-wearable:18.1.0")
|
||||
}
|
||||
|
||||
// Ensure .env exists before Flutter bundles assets (copy from .env.example if missing)
|
||||
val envFile = file("${project.projectDir}/../../.env")
|
||||
val envExampleFile = file("${project.projectDir}/../../.env.example")
|
||||
tasks.register("ensureEnv") {
|
||||
doLast {
|
||||
if (!envFile.exists() && envExampleFile.exists()) {
|
||||
envExampleFile.copyTo(envFile, overwrite = false)
|
||||
println("Created .env from .env.example for asset bundling.")
|
||||
}
|
||||
}
|
||||
}
|
||||
tasks.matching { it.name.startsWith("compileFlutterBuild") }.configureEach {
|
||||
dependsOn("ensureEnv")
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("transformAndResignDebugApk") {
|
||||
group = "build"
|
||||
description = "Transform and resign APK with debug key"
|
||||
|
||||
dependsOn("assembleDebug")
|
||||
|
||||
doLast {
|
||||
if (System.getenv("TRANSFORM_APK") != null
|
||||
&& System.getenv("TRANSFORM_APK") == "true") {
|
||||
transformApks(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("transformAndResignReleaseApk") {
|
||||
group = "build"
|
||||
description = "Transform and resign APK with release key"
|
||||
|
||||
dependsOn("assembleRelease")
|
||||
|
||||
doLast {
|
||||
checkReleaseKey()
|
||||
if (System.getenv("TRANSFORM_APK") != null
|
||||
&& System.getenv("TRANSFORM_APK") == "true") {
|
||||
transformApks(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.register("transformAndResignReleaseBundle") {
|
||||
group = "build"
|
||||
description = "Transform and resign bundle with release key"
|
||||
|
||||
dependsOn("bundleRelease")
|
||||
|
||||
doLast {
|
||||
if (System.getenv("TRANSFORM_AAB") != null
|
||||
&& System.getenv("TRANSFORM_AAB") == "true") {
|
||||
transformAppBundle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
tasks.findByName("assembleDebug")?.finalizedBy("transformAndResignDebugApk")
|
||||
tasks.findByName("assembleRelease")?.finalizedBy("transformAndResignReleaseApk")
|
||||
tasks.findByName("bundleRelease")?.finalizedBy("transformAndResignReleaseBundle")
|
||||
}
|
||||
|
||||
fun checkReleaseKey() {
|
||||
val secretsDir = File(projectDir.absolutePath, "../../../secrets/")
|
||||
val propsFile = File(secretsDir, "keystore.properties")
|
||||
|
||||
if (propsFile.exists()) {
|
||||
val props = loadProperties(propsFile)
|
||||
val store = File(secretsDir, props["storeFile"].toString())
|
||||
|
||||
println(
|
||||
"Signing with:\n" +
|
||||
"\t- store: ${store.name}\n" +
|
||||
"\t- key: ${props["keyAlias"]}"
|
||||
)
|
||||
} else {
|
||||
throw Exception("Release keystore not found!")
|
||||
}
|
||||
}
|
||||
|
||||
fun transformApks(debug: Boolean, i : Int = 0) {
|
||||
try {
|
||||
_transformApks(debug)
|
||||
} catch (e: Exception) {
|
||||
if (i < 5) {
|
||||
e.printStackTrace()
|
||||
|
||||
println("Retrying: ${i + 1}")
|
||||
transformApks(debug, i + 1)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun _transformApks(debug: Boolean) {
|
||||
println("Starting APK transformation process...")
|
||||
|
||||
val buildDir = project.buildDir
|
||||
val apkDir = File(buildDir, "outputs/flutter-apk")
|
||||
val apks = getApks(debug)
|
||||
var c = 0
|
||||
apks
|
||||
.forEach { c++; transformAndSignApk(apkDir, it.nameWithoutExtension, debug) }
|
||||
|
||||
println("Transformed: $c apks")
|
||||
}
|
||||
|
||||
fun transformAndSignApk(apkDir: File, name: String, debug: Boolean) {
|
||||
val originalApk = File(apkDir, "$name.apk")
|
||||
val transformedApk = File(apkDir, "$name-transformed.apk")
|
||||
val finalApk = File(apkDir, "$name-resigned.apk")
|
||||
val finalIdsig = File(apkDir, "$name-resigned.apk.idsig")
|
||||
|
||||
if (!originalApk.exists()) {
|
||||
throw GradleException("Original APK not found at: ${originalApk.absolutePath}")
|
||||
}
|
||||
|
||||
if (transformedApk.exists()) transformedApk.delete()
|
||||
if (finalApk.exists()) finalApk.delete()
|
||||
|
||||
println("Original APK: ${originalApk.absolutePath}")
|
||||
|
||||
try {
|
||||
println("Transforming APK...")
|
||||
transformApk(originalApk, transformedApk, if (debug) { "6" } else {"Z"})
|
||||
|
||||
if (debug) {
|
||||
println("Signing with debug key...")
|
||||
signWithDebugKey(transformedApk, finalApk)
|
||||
} else {
|
||||
println("Signing with release key...")
|
||||
signWithReleaseKey(transformedApk, finalApk)
|
||||
}
|
||||
|
||||
if (finalApk.exists()) {
|
||||
originalApk.delete()
|
||||
finalIdsig.delete()
|
||||
finalApk.renameTo(originalApk)
|
||||
println("APK successfully transformed")
|
||||
println("Final APK: ${originalApk.absolutePath}")
|
||||
}
|
||||
|
||||
transformedApk.delete()
|
||||
} catch (e: Exception) {
|
||||
throw GradleException("Failed to transform and resign APK: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun transformApk(input: File, output: File, compressionLevel: String = "Z") {
|
||||
val tempDir = File(project.buildDir, "tmp/apk-transform")
|
||||
val cacheDir = File(project.buildDir, "cache")
|
||||
val optipngCacheDir = File(cacheDir, "optipng")
|
||||
val assetCompressionDir = File(cacheDir, "assets")
|
||||
tempDir.deleteRecursively()
|
||||
tempDir.mkdirs()
|
||||
if (!optipngCacheDir.exists()) optipngCacheDir.mkdirs()
|
||||
if (!assetCompressionDir.exists()) assetCompressionDir.mkdirs()
|
||||
|
||||
val brotli = findToolInPath("brotli")
|
||||
?: throw Exception("Brotli not found in path")
|
||||
val optipng = findToolInPath("optipng")
|
||||
|
||||
if (optipng == null || optipng.isEmpty()) {
|
||||
println("Optipng was not found in PATH, optimizing images will be skipped.")
|
||||
}
|
||||
|
||||
copy {
|
||||
from(zipTree(input))
|
||||
into(tempDir)
|
||||
}
|
||||
|
||||
val metaInf = File(tempDir, "META-INF")
|
||||
val metaInfFiles = metaInf.listFiles()
|
||||
for (file in metaInfFiles!!) {
|
||||
if (file.name.endsWith("MF") || file.name.endsWith("SF")
|
||||
|| file.name.endsWith("RSA")) {
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
|
||||
val arches = File(tempDir, "lib").listFiles()
|
||||
val compressedLibs = mutableMapOf<String, String>()
|
||||
for (arch in arches!!) {
|
||||
val libFlutter = File(arch, "libflutter.so")
|
||||
|
||||
if (!libFlutter.exists()) continue
|
||||
|
||||
val compressedFlutter = File(arch, "libflutter-br.so")
|
||||
|
||||
compressedLibs["libflutter.so"] = libFlutter.sha256()
|
||||
|
||||
println("Compressing ${arch.name}/libflutter.so with brotli")
|
||||
exec {
|
||||
commandLine(
|
||||
brotli,
|
||||
"-$compressionLevel",
|
||||
libFlutter.absolutePath,
|
||||
"-o", compressedFlutter.absolutePath
|
||||
)
|
||||
}
|
||||
libFlutter.delete()
|
||||
|
||||
val json = groovy.json.JsonBuilder(compressedLibs)
|
||||
File(arch, "index.so").writeText(json.toString())
|
||||
}
|
||||
|
||||
val topDirL = tempDir.absolutePath.length + 1
|
||||
val zos = ZipOutputStream(output.outputStream())
|
||||
|
||||
val coreCount = Runtime.getRuntime().availableProcessors()
|
||||
val flutterResources = tempDir.walkTopDown().filter{f -> f.absolutePath.contains("flutter_assets")}
|
||||
val pngFiles = tempDir.walkTopDown().filter{f -> f.name.endsWith(".png")}
|
||||
|
||||
val assetIndex = mutableMapOf<String, String>()
|
||||
val indexReadWriteLock = ReentrantReadWriteLock()
|
||||
|
||||
if (compressionLevel == "Z") {
|
||||
if (optipng != null) {
|
||||
val executor = Executors.newFixedThreadPool(coreCount)
|
||||
val futures = mutableListOf<Future<*>>()
|
||||
|
||||
pngFiles.forEach { pngFile ->
|
||||
val cacheFile = File(optipngCacheDir, pngFile.sha256())
|
||||
|
||||
if (cacheFile.exists()) {
|
||||
cacheFile.copyTo(pngFile, true)
|
||||
} else {
|
||||
val future = executor.submit {
|
||||
exec {
|
||||
commandLine(
|
||||
optipng,
|
||||
"-zm", "9",
|
||||
"-zw", "32k",
|
||||
"-o9",
|
||||
pngFile.absolutePath
|
||||
)
|
||||
}
|
||||
|
||||
pngFile.copyTo(cacheFile, true)
|
||||
}
|
||||
|
||||
futures.add(future)
|
||||
}
|
||||
}
|
||||
|
||||
futures.forEach { it.get() }
|
||||
executor.shutdown()
|
||||
}
|
||||
|
||||
val executor = Executors.newFixedThreadPool(coreCount)
|
||||
val futures = mutableListOf<Future<*>>()
|
||||
|
||||
val blacklist = listOf(
|
||||
// "AssetManifest.bin",
|
||||
"AssetManifest.json",
|
||||
"FontManifest.json",
|
||||
"isolate_snapshot_data",
|
||||
"kernel_blob.bin",
|
||||
"NativeAssetsManifest.json",
|
||||
"NOTICES.Z",
|
||||
"vm_snapshot_data",
|
||||
"fonts",
|
||||
"shaders"
|
||||
)
|
||||
|
||||
flutterResources.forEach { f ->
|
||||
val relName = f.absolutePath.substring(topDirL).replace("\\", "/")
|
||||
if (f.isDirectory) return@forEach
|
||||
|
||||
val cacheFileRaw = File(assetCompressionDir, f.sha256()+".r")
|
||||
val cacheFileGz = File(assetCompressionDir, f.sha256()+".gz")
|
||||
val cacheFileBr = File(assetCompressionDir, f.sha256()+".br")
|
||||
|
||||
if (cacheFileRaw.exists() || cacheFileGz.exists() || cacheFileBr.exists()) {
|
||||
if (cacheFileRaw.exists()) {
|
||||
cacheFileRaw.copyTo(f, true)
|
||||
|
||||
indexReadWriteLock.writeLock().lock()
|
||||
assetIndex[relName] = "r"
|
||||
indexReadWriteLock.writeLock().unlock()
|
||||
} else if (cacheFileGz.exists()) {
|
||||
cacheFileGz.copyTo(f, true)
|
||||
|
||||
indexReadWriteLock.writeLock().lock()
|
||||
assetIndex[relName] = "g"
|
||||
indexReadWriteLock.writeLock().unlock()
|
||||
} else {
|
||||
cacheFileBr.copyTo(f, true)
|
||||
|
||||
indexReadWriteLock.writeLock().lock()
|
||||
assetIndex[relName] = "b"
|
||||
indexReadWriteLock.writeLock().unlock()
|
||||
}
|
||||
} else {
|
||||
val future = executor.submit {
|
||||
val brTmp = File(f.absolutePath + ".br.tmp")
|
||||
val gzTmp = File(f.absolutePath + ".gz.tmp")
|
||||
|
||||
var blacklisted = false
|
||||
for (f in blacklist) {
|
||||
if (relName.contains(f)) {
|
||||
blacklisted = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!blacklisted) {
|
||||
println("$relName: Testing with brotli")
|
||||
exec {
|
||||
commandLine(
|
||||
brotli,
|
||||
"-$compressionLevel",
|
||||
f.absolutePath,
|
||||
"-o", brTmp.absolutePath
|
||||
)
|
||||
}
|
||||
|
||||
println("$relName: Testing with gzip")
|
||||
ant.invokeMethod(
|
||||
"gzip", mapOf(
|
||||
"src" to f.absolutePath,
|
||||
"destfile" to gzTmp.absolutePath,
|
||||
)
|
||||
)
|
||||
|
||||
println("$brTmp: ${brTmp.length()}")
|
||||
println("$gzTmp: ${gzTmp.length()}")
|
||||
if (f.length() < gzTmp.length() && f.length() < brTmp.length()) {
|
||||
println("$relName: Raw file wins")
|
||||
|
||||
f.copyTo(cacheFileRaw, true)
|
||||
|
||||
indexReadWriteLock.writeLock().lock()
|
||||
assetIndex[relName] = "r"
|
||||
indexReadWriteLock.writeLock().unlock()
|
||||
} else {
|
||||
if (brTmp.length() < gzTmp.length()) {
|
||||
println("$relName: Brotli wins")
|
||||
|
||||
f.delete()
|
||||
brTmp.copyTo(f, true)
|
||||
brTmp.copyTo(cacheFileBr, true)
|
||||
|
||||
indexReadWriteLock.writeLock().lock()
|
||||
assetIndex[relName] = "b"
|
||||
indexReadWriteLock.writeLock().unlock()
|
||||
} else {
|
||||
println("$relName: Gzip wins")
|
||||
|
||||
f.delete()
|
||||
gzTmp.copyTo(f, true)
|
||||
gzTmp.copyTo(cacheFileGz, true)
|
||||
|
||||
indexReadWriteLock.writeLock().lock()
|
||||
assetIndex[relName] = "g"
|
||||
indexReadWriteLock.writeLock().unlock()
|
||||
}
|
||||
}
|
||||
|
||||
brTmp.delete()
|
||||
gzTmp.delete()
|
||||
}
|
||||
}
|
||||
|
||||
futures.add(future)
|
||||
}
|
||||
}
|
||||
|
||||
futures.forEach { it.get() }
|
||||
executor.shutdown()
|
||||
}
|
||||
|
||||
tempDir.walkTopDown().forEach { f ->
|
||||
if (f.absolutePath == tempDir.absolutePath) return@forEach
|
||||
|
||||
var relName = f.absolutePath.substring(topDirL).replace("\\", "/")
|
||||
if (f.isDirectory && !relName.endsWith("/")) relName += "/"
|
||||
|
||||
if (compressionLevel == "Z") {
|
||||
if (relName == "assets/flutter_assets/assets/firka.i") return@forEach
|
||||
}
|
||||
|
||||
println(relName)
|
||||
|
||||
val compress = !relName.endsWith(".so") && !relName.endsWith(".arsc")
|
||||
zos.setMethod(if (compress) { DEFLATED } else { STORED })
|
||||
val entry = ZipEntry(relName)
|
||||
if (!compress) {
|
||||
entry.size = f.length()
|
||||
entry.crc = FileUtils.checksumCRC32(f)
|
||||
}
|
||||
zos.putNextEntry(entry)
|
||||
if (f.isFile) {
|
||||
zos.write(f.readBytes())
|
||||
}
|
||||
zos.closeEntry()
|
||||
}
|
||||
if (compressionLevel == "Z") {
|
||||
zos.setMethod(DEFLATED)
|
||||
zos.putNextEntry(ZipEntry("assets/flutter_assets/assets/firka.i"))
|
||||
|
||||
val indexUncompressed = File(tempDir, "index.json")
|
||||
indexReadWriteLock.readLock().lock()
|
||||
val json = groovy.json.JsonBuilder(assetIndex)
|
||||
indexReadWriteLock.readLock().unlock()
|
||||
indexUncompressed.writeText(json.toString())
|
||||
|
||||
val indexCompressed = File(tempDir, "index.json.br")
|
||||
|
||||
exec {
|
||||
commandLine(
|
||||
brotli,
|
||||
"-$compressionLevel",
|
||||
indexUncompressed.absolutePath,
|
||||
"-o", indexCompressed.absolutePath
|
||||
)
|
||||
}
|
||||
|
||||
zos.write(indexCompressed.readBytes())
|
||||
indexUncompressed.delete()
|
||||
indexCompressed.delete()
|
||||
|
||||
zos.closeEntry()
|
||||
}
|
||||
zos.close()
|
||||
|
||||
tempDir.deleteRecursively()
|
||||
println("APK transformed successfully")
|
||||
}
|
||||
|
||||
fun transformAppBundle() {
|
||||
val buildDir = project.buildDir
|
||||
val bundle = File(buildDir, "outputs/bundle/release/app-release.aab")
|
||||
val bundleTmp = File(buildDir, "outputs/bundle/release/tmp.zip")
|
||||
|
||||
val apks = getApks(false)
|
||||
val apkCount = apks.count { it.name.startsWith("app-") && it.name.endsWith("-release.apk") }
|
||||
|
||||
if (!bundle.exists()) {
|
||||
throw Exception("Bundle not found at: $bundle")
|
||||
}
|
||||
|
||||
if (apkCount < 3) {
|
||||
throw Exception("Excepected 3 apks per abi but only found $apkCount")
|
||||
}
|
||||
|
||||
val aabTempDir = File(project.buildDir, "tmp/aab-transform")
|
||||
aabTempDir.deleteRecursively()
|
||||
aabTempDir.mkdirs()
|
||||
|
||||
val apksUnzipped = File(project.buildDir, "tmp/apks-unzipped")
|
||||
apksUnzipped.deleteRecursively()
|
||||
|
||||
val arm32TempDir = File(apksUnzipped, "armeabi-v7a")
|
||||
arm32TempDir.mkdirs()
|
||||
val arm64TempDir = File(apksUnzipped, "arm64-v8a")
|
||||
arm64TempDir.mkdirs()
|
||||
val x86TempDir = File(apksUnzipped, "x86_64")
|
||||
x86TempDir.mkdirs()
|
||||
|
||||
copy {
|
||||
from(zipTree(bundle))
|
||||
into(aabTempDir)
|
||||
}
|
||||
copy {
|
||||
from(zipTree(apks.first { it.name.contains("armeabi-v7a") }))
|
||||
into(arm32TempDir)
|
||||
}
|
||||
copy {
|
||||
from(zipTree(apks.first { it.name.contains("arm64-v8a") }))
|
||||
into(arm64TempDir)
|
||||
}
|
||||
copy {
|
||||
from(zipTree(apks.first { it.name.contains("x86_64") }))
|
||||
into(x86TempDir)
|
||||
}
|
||||
|
||||
val libs = File(aabTempDir, "base/lib").listFiles()!!
|
||||
|
||||
for (dstLibs in libs) {
|
||||
println("Copying lib: ${dstLibs.name}")
|
||||
val srcDir = File(apksUnzipped, dstLibs.name)
|
||||
if (!srcDir.exists()) {
|
||||
continue
|
||||
}
|
||||
val srcLibs = File(srcDir, "lib/${dstLibs.name}/")
|
||||
|
||||
dstLibs.listFiles()!!.forEach { it.delete() }
|
||||
srcLibs.listFiles()!!.forEach { it.copyTo(File(dstLibs, it.name)) }
|
||||
}
|
||||
|
||||
val zos = ZipOutputStream(bundleTmp.outputStream())
|
||||
val bundleZip = ZipFile(bundle)
|
||||
val bundleEntries = bundleZip.entries()
|
||||
|
||||
val brotli = findToolInPath("brotli")
|
||||
?: throw Exception("Brotli not found in path")
|
||||
val optipng = findToolInPath("optipng")
|
||||
?: throw Exception("Optipng not found in path")
|
||||
|
||||
val indexReadWriteLock = ReentrantReadWriteLock()
|
||||
val assetIndex = mutableMapOf<String, String>()
|
||||
|
||||
while (bundleEntries.hasMoreElements()) {
|
||||
val entry = bundleEntries.nextElement()
|
||||
|
||||
/*
|
||||
if (entry.name == "base/assets/flutter_assets/assets/firka.i") {
|
||||
println("Patching: ${entry.name}")
|
||||
zos.putNextEntry(ZipEntry("assets/flutter_assets/assets/firka.i"))
|
||||
|
||||
val indexUncompressed = File(aabTempDir, "index.json")
|
||||
indexReadWriteLock.readLock().lock()
|
||||
val json = groovy.json.JsonBuilder(assetIndex)
|
||||
indexReadWriteLock.readLock().unlock()
|
||||
indexUncompressed.writeText(json.toString())
|
||||
|
||||
val indexCompressed = File(aabTempDir, "index.json.br")
|
||||
|
||||
exec {
|
||||
commandLine(
|
||||
brotli,
|
||||
"-Z",
|
||||
indexUncompressed.absolutePath,
|
||||
"-o", indexCompressed.absolutePath
|
||||
)
|
||||
}
|
||||
|
||||
zos.write(indexCompressed.readBytes())
|
||||
indexUncompressed.delete()
|
||||
indexCompressed.delete()
|
||||
|
||||
zos.closeEntry()
|
||||
continue
|
||||
}
|
||||
if (entry.name.startsWith("base/lib")) {
|
||||
println("Patching: ${entry.name}")
|
||||
zos.putNextEntry(ZipEntry(entry.name))
|
||||
|
||||
|
||||
|
||||
zos.closeEntry()
|
||||
continue
|
||||
}
|
||||
*/
|
||||
|
||||
println("Adding: ${entry.name}")
|
||||
|
||||
zos.putNextEntry(ZipEntry(entry.name))
|
||||
|
||||
if (!entry.isDirectory) {
|
||||
val data = bundleZip.getInputStream(entry).readAllBytes()
|
||||
zos.write(data)
|
||||
}
|
||||
zos.closeEntry()
|
||||
}
|
||||
bundleZip.close()
|
||||
zos.close()
|
||||
|
||||
bundle.delete()
|
||||
signBundle(bundleTmp, bundle)
|
||||
bundleTmp.delete()
|
||||
|
||||
aabTempDir.deleteRecursively()
|
||||
println("AAB transformed successfully")
|
||||
|
||||
}
|
||||
|
||||
fun File.sha256(): String {
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
val digest = md.digest(this.readBytes())
|
||||
return digest.fold("") { str, it -> str + "%02x".format(it) }
|
||||
}
|
||||
|
||||
fun getApks(debug: Boolean): List<File> {
|
||||
val buildDir = project.buildDir
|
||||
val apkDir = File(buildDir, "outputs/flutter-apk")
|
||||
val apks = apkDir.listFiles()!!
|
||||
val flavor = if (debug) { "debug" } else { "release" }
|
||||
|
||||
return apks
|
||||
.filter { apk -> apk.name.startsWith("app-") && apk.name.endsWith("-$flavor.apk") }
|
||||
.toList()
|
||||
}
|
||||
|
||||
fun getDebugKeystorePath(): String {
|
||||
val userHome = System.getProperty("user.home")
|
||||
val debugKeystore = File(userHome, ".android/debug.keystore")
|
||||
|
||||
if (!debugKeystore.exists()) {
|
||||
throw GradleException("Debug keystore not found at: ${debugKeystore.absolutePath}")
|
||||
}
|
||||
|
||||
return debugKeystore.absolutePath
|
||||
}
|
||||
|
||||
fun getDefaultAndroidSdkPath(): String? {
|
||||
val os = System.getProperty("os.name").lowercase()
|
||||
val userHome = System.getProperty("user.home")
|
||||
val zipAlign = File("/usr/bin/zipalign")
|
||||
|
||||
if (zipAlign.exists()) {
|
||||
return "/usr/bin"
|
||||
}
|
||||
|
||||
return when {
|
||||
os.contains("win") ->
|
||||
"$userHome\\AppData\\Local\\Android\\Sdk"
|
||||
os.contains("mac") ->
|
||||
"$userHome/Library/Android/sdk"
|
||||
os.contains("linux") ->
|
||||
"$userHome/Android/Sdk"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
fun findToolInPath(toolName: String): String? {
|
||||
val pathEnvironment = System.getenv("PATH")
|
||||
val pathDirs = pathEnvironment.split(File.pathSeparator)
|
||||
|
||||
val executableNames = when {
|
||||
System.getProperty("os.name").lowercase().contains("win") ->
|
||||
listOf("$toolName.exe", toolName)
|
||||
else ->
|
||||
listOf(toolName)
|
||||
}
|
||||
|
||||
for (pathDir in pathDirs) {
|
||||
for (execName in executableNames) {
|
||||
val possibleTool = File(pathDir, execName)
|
||||
if (possibleTool.exists() && possibleTool.canExecute()) {
|
||||
return possibleTool.absolutePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun findToolInSdkPath(toolName: String): String? {
|
||||
var androidHome : String? = System.getenv("ANDROID_HOME")
|
||||
?: System.getenv("ANDROID_SDK_ROOT")
|
||||
|
||||
if (androidHome == null) androidHome = getDefaultAndroidSdkPath()
|
||||
|
||||
if (androidHome != null) {
|
||||
val buildTools = File(androidHome, "build-tools")
|
||||
if (buildTools.exists()) {
|
||||
val latestVersion = buildTools.listFiles()
|
||||
?.filter { it.isDirectory }
|
||||
?.filter { it.name != "debian" }
|
||||
?.maxByOrNull { it.name }
|
||||
|
||||
if (latestVersion != null) {
|
||||
val toolExec = File(latestVersion, toolName)
|
||||
if (toolExec.exists()) {
|
||||
return toolExec.absolutePath
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val toolExec = File(androidHome, toolName)
|
||||
if (toolExec.exists()) {
|
||||
return toolExec.absolutePath
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!toolName.contains(".exe")) {
|
||||
val exeTool = findToolInSdkPath("$toolName.exe")
|
||||
if (exeTool != null) return exeTool
|
||||
}
|
||||
if (!toolName.contains(".sh")) {
|
||||
val shTool = findToolInSdkPath("$toolName.sh")
|
||||
if (shTool != null) return shTool
|
||||
}
|
||||
if (!toolName.contains(".bat")) {
|
||||
val batTool = findToolInSdkPath("$toolName.bat")
|
||||
if (batTool != null) return batTool
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
fun signWithDebugKey(input: File, output: File) {
|
||||
val debugKeystore = getDebugKeystorePath()
|
||||
val debugKeystorePassword = "android"
|
||||
val debugKeyAlias = "androiddebugkey"
|
||||
val debugKeyPassword = "android"
|
||||
|
||||
val zipAlign: String = findToolInSdkPath("zipalign")
|
||||
?: throw Exception("Could not find zipalign in ANDROID_SDK")
|
||||
val apksigner: String = findToolInSdkPath("apksigner")
|
||||
?: throw Exception("Could not find zipalign in ANDROID_SDK")
|
||||
|
||||
exec {
|
||||
commandLine(
|
||||
zipAlign,
|
||||
"-v", "4",
|
||||
input.absolutePath,
|
||||
output.absolutePath
|
||||
)
|
||||
}
|
||||
|
||||
exec {
|
||||
commandLine(
|
||||
apksigner, "sign",
|
||||
"--ks", debugKeystore,
|
||||
"--ks-pass", "pass:$debugKeystorePassword",
|
||||
"--ks-key-alias", debugKeyAlias,
|
||||
"--key-pass", "pass:$debugKeyPassword",
|
||||
output.absolutePath
|
||||
)
|
||||
}
|
||||
|
||||
println("APK signed and aligned successfully")
|
||||
}
|
||||
|
||||
fun signWithReleaseKey(input: File, output: File) {
|
||||
val secretsDir = File(projectDir.absolutePath, "../../../secrets/")
|
||||
val propsFile = File(secretsDir, "keystore.properties")
|
||||
|
||||
if (!propsFile.exists()) {
|
||||
throw Exception("Release keystore not found!")
|
||||
}
|
||||
|
||||
val props = loadProperties(propsFile)
|
||||
|
||||
val releaseKeystore = File(secretsDir, props["storeFile"].toString())
|
||||
val releaseKeystorePassword = props["storePassword"] as String
|
||||
val releaseKeyAlias = props["keyAlias"] as String
|
||||
val releaseKeyPassword = props["keyPassword"] as String
|
||||
|
||||
val zipAlign: String = findToolInSdkPath("zipalign")
|
||||
?: throw Exception("Could not find zipalign either in ANDROID_SDK")
|
||||
val apksigner: String = findToolInSdkPath("apksigner")
|
||||
?: throw Exception("Could not find zipalign either in ANDROID_SDK")
|
||||
|
||||
exec {
|
||||
commandLine(
|
||||
zipAlign,
|
||||
"-v", "4",
|
||||
input.absolutePath,
|
||||
output.absolutePath
|
||||
)
|
||||
}
|
||||
|
||||
exec {
|
||||
commandLine(
|
||||
apksigner, "sign",
|
||||
"--ks", releaseKeystore,
|
||||
"--ks-pass", "pass:$releaseKeystorePassword",
|
||||
"--ks-key-alias", releaseKeyAlias,
|
||||
"--key-pass", "pass:$releaseKeyPassword",
|
||||
output.absolutePath
|
||||
)
|
||||
}
|
||||
|
||||
println("APK signed and aligned successfully")
|
||||
}
|
||||
|
||||
fun signBundle(input: File, output: File) {
|
||||
val secretsDir = File(projectDir.absolutePath, "../../../secrets/")
|
||||
val propsFile = File(secretsDir, "keystore.properties")
|
||||
|
||||
if (!propsFile.exists()) {
|
||||
throw Exception("Release keystore not found!")
|
||||
}
|
||||
|
||||
val props = loadProperties(propsFile)
|
||||
|
||||
val releaseKeystore = File(secretsDir, props["storeFile"].toString())
|
||||
val releaseKeystorePassword = props["storePassword"] as String
|
||||
val releaseKeyAlias = props["keyAlias"] as String
|
||||
val releaseKeyPassword = props["keyPassword"] as String
|
||||
|
||||
// val zipAlign: String = findToolInSdkPath("zipalign")
|
||||
// ?: throw Exception("Could not find zipalign in ANDROID_SDK")
|
||||
val jarsigner: String = findToolInPath("jarsigner")
|
||||
?: throw Exception("Could not find jarsigner in PATH")
|
||||
|
||||
/*
|
||||
exec {
|
||||
commandLine(
|
||||
zipAlign,
|
||||
"-v", "4",
|
||||
input.absolutePath,
|
||||
output.absolutePath
|
||||
)
|
||||
}
|
||||
*/
|
||||
input.copyTo(output, true)
|
||||
|
||||
exec {
|
||||
// -keystore $KEYSTORE -storetype $STORETYPE -storepass $STOREPASS -digestalg SHA1 -sigalg SHA256withRSA application.zip $KEYALIAS
|
||||
commandLine(
|
||||
jarsigner,
|
||||
"-verbose",
|
||||
"-sigalg", "SHA256withRSA",
|
||||
"-digestalg", "SHA-256",
|
||||
"-keystore", releaseKeystore,
|
||||
"-storepass", releaseKeystorePassword,
|
||||
output.absolutePath,
|
||||
releaseKeyAlias
|
||||
)
|
||||
}
|
||||
|
||||
println("AAB signed and aligned successfully")
|
||||
}
|
||||
|
||||
@@ -5,18 +5,12 @@
|
||||
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
|
||||
<application
|
||||
android:name=".AppMain"
|
||||
android:icon="@mipmap/launcher_icon">
|
||||
|
||||
|
||||
<service
|
||||
android:name=".WearSyncForegroundService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
@@ -41,7 +35,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_ace"
|
||||
android:roundIcon="@mipmap/ic_ace_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -55,7 +49,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_ace_f"
|
||||
android:roundIcon="@mipmap/ic_ace_f_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -69,7 +63,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_bi"
|
||||
android:roundIcon="@mipmap/ic_bi_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -83,7 +77,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_bi_f"
|
||||
android:roundIcon="@mipmap/ic_bi_f_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -97,7 +91,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_cactus"
|
||||
android:roundIcon="@mipmap/ic_cactus_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -111,7 +105,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_cc"
|
||||
android:roundIcon="@mipmap/ic_cc_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -125,7 +119,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_enby"
|
||||
android:roundIcon="@mipmap/ic_enby_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -139,7 +133,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_enby_f"
|
||||
android:roundIcon="@mipmap/ic_enby_f_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -153,7 +147,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_fidesz"
|
||||
android:roundIcon="@mipmap/ic_fidesz_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -167,7 +161,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_filc"
|
||||
android:roundIcon="@mipmap/ic_filc_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -181,7 +175,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_filco"
|
||||
android:roundIcon="@mipmap/ic_filco_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -195,7 +189,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_galaxy"
|
||||
android:roundIcon="@mipmap/ic_galaxy_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -209,7 +203,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_gay"
|
||||
android:roundIcon="@mipmap/ic_gay_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -223,7 +217,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_gay_f"
|
||||
android:roundIcon="@mipmap/ic_gay_f_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -237,7 +231,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_half_firka_2"
|
||||
android:roundIcon="@mipmap/ic_half_firka_2_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -251,7 +245,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_kreta"
|
||||
android:roundIcon="@mipmap/ic_kreta_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -265,7 +259,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_lesb"
|
||||
android:roundIcon="@mipmap/ic_lesb_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -279,7 +273,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_lesb_f"
|
||||
android:roundIcon="@mipmap/ic_lesb_f_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -293,7 +287,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_lgbtq"
|
||||
android:roundIcon="@mipmap/ic_lgbtq_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -307,7 +301,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_lgbtq_f"
|
||||
android:roundIcon="@mipmap/ic_lgbtq_f_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -321,7 +315,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_lgbtqp"
|
||||
android:roundIcon="@mipmap/ic_lgbtqp_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -335,7 +329,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_lgbtqp_f"
|
||||
android:roundIcon="@mipmap/ic_lgbtqp_f_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -349,7 +343,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_lidl"
|
||||
android:roundIcon="@mipmap/ic_lidl_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -363,7 +357,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_mkkp"
|
||||
android:roundIcon="@mipmap/ic_mkkp_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -377,7 +371,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_nuke"
|
||||
android:roundIcon="@mipmap/ic_nuke_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -391,7 +385,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_modern"
|
||||
android:roundIcon="@mipmap/ic_modern_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -405,7 +399,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_o1g"
|
||||
android:roundIcon="@mipmap/ic_o1g_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -419,7 +413,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_old"
|
||||
android:roundIcon="@mipmap/ic_old_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -433,7 +427,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_paper"
|
||||
android:roundIcon="@mipmap/ic_paper_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -447,7 +441,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_pear"
|
||||
android:roundIcon="@mipmap/ic_pear_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -461,7 +455,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_pixel"
|
||||
android:roundIcon="@mipmap/ic_pixel_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -475,7 +469,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_pixelized"
|
||||
android:roundIcon="@mipmap/ic_pixelized_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -489,7 +483,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_pride"
|
||||
android:roundIcon="@mipmap/ic_pride_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -503,7 +497,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_proto"
|
||||
android:roundIcon="@mipmap/ic_proto_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -517,7 +511,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_refilc"
|
||||
android:roundIcon="@mipmap/ic_refilc_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -531,7 +525,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_refulc"
|
||||
android:roundIcon="@mipmap/ic_refulc_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -545,7 +539,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_repont"
|
||||
android:roundIcon="@mipmap/ic_repont_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -559,7 +553,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_szivacs"
|
||||
android:roundIcon="@mipmap/ic_szivacs_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -573,7 +567,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_tisza"
|
||||
android:roundIcon="@mipmap/ic_tisza_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -587,7 +581,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_trans"
|
||||
android:roundIcon="@mipmap/ic_trans_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -601,7 +595,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_trans_f"
|
||||
android:roundIcon="@mipmap/ic_trans_f_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -615,7 +609,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_void_icon"
|
||||
android:roundIcon="@mipmap/ic_void_icon_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -629,7 +623,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_xmas1"
|
||||
android:roundIcon="@mipmap/ic_xmas1_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -643,7 +637,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_xmas2"
|
||||
android:roundIcon="@mipmap/ic_xmas2_round" >
|
||||
|
||||
<intent-filter>
|
||||
@@ -657,7 +651,7 @@
|
||||
android:targetActivity=".MainActivity"
|
||||
android:exported="true"
|
||||
android:enabled="false"
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:icon="@mipmap/ic_xmas3"
|
||||
android:roundIcon="@mipmap/ic_xmas3_round" >
|
||||
|
||||
<intent-filter>
|
||||
|
||||
@@ -1,5 +1,99 @@
|
||||
package app.firka.naplo
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import org.brotli.dec.BrotliInputStream
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.security.MessageDigest
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
class AppMain : Application() {}
|
||||
class AppMain : Application() {
|
||||
|
||||
private fun File.sha256(): String {
|
||||
if (!exists()) return "0000000000000000000000000000000000000000000000000000000000000000"
|
||||
|
||||
val md = MessageDigest.getInstance("SHA-256")
|
||||
val digest = md.digest(this.readBytes())
|
||||
return digest.fold("") { str, it -> str + "%02x".format(it) }
|
||||
}
|
||||
|
||||
@SuppressLint("UnsafeDynamicallyLoadedCode")
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
var useUncompressedLibs = false
|
||||
|
||||
val abi = Build.SUPPORTED_ABIS[0]
|
||||
|
||||
val apks = File(applicationInfo.nativeLibraryDir, "../..").absoluteFile
|
||||
.listFiles()!!
|
||||
.filter { file -> file.name.endsWith(".apk") }
|
||||
.toList()
|
||||
|
||||
var nativesApkN: ZipFile? = null
|
||||
for (apk in apks) {
|
||||
if (nativesApkN != null) break
|
||||
|
||||
val zip = ZipFile(apk)
|
||||
val entries = zip.entries()
|
||||
|
||||
while (entries.hasMoreElements()) {
|
||||
val entry = entries.nextElement()
|
||||
|
||||
if (entry.name.endsWith("$abi/index.so")) {
|
||||
zip.close()
|
||||
nativesApkN = ZipFile(apk)
|
||||
break
|
||||
}
|
||||
if (entry.name.endsWith("$abi/libflutter.so")) {
|
||||
useUncompressedLibs = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
zip.close()
|
||||
}
|
||||
|
||||
if (useUncompressedLibs) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (nativesApkN == null) {
|
||||
throw Exception("Can't find native libraries")
|
||||
}
|
||||
val nativesApk: ZipFile = nativesApkN
|
||||
|
||||
val compressedLibsIndex = nativesApk.getInputStream(
|
||||
nativesApk.getEntry("lib/$abi/index.so")
|
||||
)
|
||||
val compressedLibs = JSONObject(compressedLibsIndex.readBytes().toString(Charsets.UTF_8))
|
||||
|
||||
for (so in compressedLibs.keys()) {
|
||||
val soFile = File(cacheDir, so)
|
||||
|
||||
if (soFile.sha256() == compressedLibs.getString(so)) {
|
||||
System.load(soFile.absolutePath)
|
||||
return
|
||||
}
|
||||
|
||||
Log.d("AppMain", "Decompressing: $so")
|
||||
val brInput = nativesApk.getInputStream(
|
||||
nativesApk.getEntry("lib/$abi/${so.replace(".so", "-br.so")}")
|
||||
)
|
||||
val soOutput = FileOutputStream(soFile)
|
||||
|
||||
val brIn = BrotliInputStream(brInput)
|
||||
brIn.copyTo(soOutput)
|
||||
|
||||
brInput.close()
|
||||
soOutput.close()
|
||||
|
||||
System.load(soFile.absolutePath)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,28 +1,19 @@
|
||||
package app.firka.naplo
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.glance.appwidget.updateAll
|
||||
import app.firka.naplo.glance.TimetableWidget
|
||||
import app.firka.naplo.glance.TimetableWidgetReceiver
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.system.exitProcess
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
|
||||
private val channel = "firka.app/main"
|
||||
private val wearSyncChannel = "app.firka/wear_sync"
|
||||
|
||||
private fun forceIconUpdate() {
|
||||
try {
|
||||
@@ -39,57 +30,6 @@ class MainActivity : FlutterActivity() {
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, wearSyncChannel).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"startWearSyncService" -> {
|
||||
val args = call.arguments as? Map<*, *>
|
||||
val cachePath = args?.get("cachePath") as? String
|
||||
val appDirPath = args?.get("appDirPath") as? String
|
||||
if (cachePath != null && appDirPath != null) {
|
||||
val messenger = flutterEngine.dartExecutor.binaryMessenger
|
||||
val ch = MethodChannel(messenger, wearSyncChannel)
|
||||
ch.invokeMethod("getLocalizedString", "wearSyncNotificationTitle", object : MethodChannel.Result {
|
||||
override fun success(titleResult: Any?) {
|
||||
val title = titleResult as? String ?: "Syncing with watch"
|
||||
ch.invokeMethod("getLocalizedString", "wearSyncNotificationText", object : MethodChannel.Result {
|
||||
override fun success(textResult: Any?) {
|
||||
val text = textResult as? String ?: ""
|
||||
val intent = Intent(this@MainActivity, WearSyncForegroundService::class.java).apply {
|
||||
action = WearSyncForegroundService.ACTION_START
|
||||
putExtra(WearSyncForegroundService.EXTRA_CACHE_PATH, cachePath)
|
||||
putExtra(WearSyncForegroundService.EXTRA_APP_DIR_PATH, appDirPath)
|
||||
putExtra(WearSyncForegroundService.EXTRA_NOTIFICATION_TITLE, title)
|
||||
putExtra(WearSyncForegroundService.EXTRA_NOTIFICATION_TEXT, text)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
startForegroundService(intent)
|
||||
} else {
|
||||
startService(intent)
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
override fun error(code: String, msg: String?, details: Any?) { result.success(null) }
|
||||
override fun notImplemented() { result.success(null) }
|
||||
})
|
||||
}
|
||||
override fun error(code: String, msg: String?, details: Any?) { result.error(code, msg, details) }
|
||||
override fun notImplemented() { result.notImplemented() }
|
||||
})
|
||||
} else {
|
||||
result.error("INVALID_ARGS", "cachePath and appDirPath required", null)
|
||||
}
|
||||
}
|
||||
"stopWearSyncService" -> {
|
||||
val intent = Intent(this, WearSyncForegroundService::class.java).apply {
|
||||
action = WearSyncForegroundService.ACTION_STOP
|
||||
}
|
||||
startService(intent)
|
||||
result.success(null)
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channel).setMethodCallHandler {
|
||||
call, result ->
|
||||
when (call.method) {
|
||||
@@ -157,29 +97,7 @@ class MainActivity : FlutterActivity() {
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
result.success(true)
|
||||
}
|
||||
"refreshTimetableWidget" -> {
|
||||
CoroutineScope(SupervisorJob() + Dispatchers.Default).launch {
|
||||
try {
|
||||
val appContext = context.applicationContext
|
||||
val appWidgetManager = AppWidgetManager.getInstance(appContext)
|
||||
val componentName = ComponentName(appContext, TimetableWidgetReceiver::class.java)
|
||||
val ids = appWidgetManager.getAppWidgetIds(componentName)
|
||||
if (ids.isNotEmpty()) {
|
||||
val intent = Intent(appContext, TimetableWidgetReceiver::class.java).apply {
|
||||
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
|
||||
addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
|
||||
}
|
||||
appContext.sendBroadcast(intent)
|
||||
}
|
||||
TimetableWidget().updateAll(appContext)
|
||||
result.success(true)
|
||||
} catch (e: Exception) {
|
||||
result.error("refresh_failed", e.message, null)
|
||||
}
|
||||
}
|
||||
result.success(true)
|
||||
}
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
|
||||
@@ -1,221 +0,0 @@
|
||||
package app.firka.naplo
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ObjectInputStream
|
||||
import com.google.android.gms.wearable.MessageClient
|
||||
import com.google.android.gms.wearable.MessageEvent
|
||||
import com.google.android.gms.wearable.Wearable
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.embedding.engine.dart.DartExecutor
|
||||
import io.flutter.embedding.engine.loader.FlutterLoader
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.coroutines.delay
|
||||
|
||||
/**
|
||||
* Foreground service that keeps the app able to respond to Wear OS sync requests.
|
||||
* When the watch sends request_sync, starts a Dart background isolate to fetch data,
|
||||
* then reads the cache file and sends sync_data to the watch.
|
||||
*/
|
||||
class WearSyncForegroundService : Service(), MessageClient.OnMessageReceivedListener {
|
||||
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
|
||||
|
||||
private var cachePath: String? = null
|
||||
private var appDirPath: String? = null
|
||||
|
||||
private val channelId = "firka_wear_sync"
|
||||
private val notificationId = 4001
|
||||
private var notificationTitle: String = "Syncing with watch"
|
||||
private var notificationText: String = ""
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
createNotificationChannel()
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
when (intent?.action) {
|
||||
ACTION_START -> {
|
||||
cachePath = intent.getStringExtra(EXTRA_CACHE_PATH)
|
||||
appDirPath = intent.getStringExtra(EXTRA_APP_DIR_PATH)
|
||||
notificationTitle = intent.getStringExtra(EXTRA_NOTIFICATION_TITLE) ?: "Syncing with watch"
|
||||
notificationText = intent.getStringExtra(EXTRA_NOTIFICATION_TEXT) ?: ""
|
||||
startForegroundWithNotification()
|
||||
Wearable.getMessageClient(this@WearSyncForegroundService)
|
||||
.addListener(this@WearSyncForegroundService)
|
||||
}
|
||||
ACTION_STOP -> {
|
||||
stopForegroundService()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
}
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onDestroy() {
|
||||
try {
|
||||
Wearable.getMessageClient(this@WearSyncForegroundService)
|
||||
.removeListener(this@WearSyncForegroundService)
|
||||
.addOnCompleteListener { }
|
||||
} catch (_: Exception) { }
|
||||
scope.cancel()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onMessageReceived(messageEvent: MessageEvent) {
|
||||
if (messageEvent.path != PATH_WATCH_CONNECTIVITY ||
|
||||
!isRequestSyncPayload(messageEvent.data)
|
||||
) return
|
||||
val cPath = cachePath
|
||||
val aPath = appDirPath
|
||||
if (cPath == null || aPath == null) return
|
||||
scope.launch {
|
||||
runSyncInBackground(cPath, aPath)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* watch_connectivity plugin sends with path "watch_connectivity" and serializes the message
|
||||
* map with Java ObjectOutputStream. Parse payload and check for id == "request_sync".
|
||||
*/
|
||||
private fun isRequestSyncPayload(data: ByteArray?): Boolean {
|
||||
if (data == null || data.isEmpty()) return false
|
||||
return try {
|
||||
ObjectInputStream(ByteArrayInputStream(data)).use { ois ->
|
||||
val map = ois.readObject()
|
||||
if (map is Map<*, *>) map["id"] == "request_sync" else false
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun startForegroundWithNotification() {
|
||||
val notification = buildNotification()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(notificationId, notification, android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildNotification(): Notification {
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this,
|
||||
0,
|
||||
packageManager.getLaunchIntentForPackage(packageName),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
return NotificationCompat.Builder(this, channelId)
|
||||
.setContentTitle(notificationTitle)
|
||||
.setContentText(notificationText)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentIntent(pendingIntent)
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
val channel = NotificationChannel(
|
||||
channelId,
|
||||
"Wear sync",
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
).apply { setShowBadge(false) }
|
||||
(getSystemService(NOTIFICATION_SERVICE) as NotificationManager)
|
||||
.createNotificationChannel(channel)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopForegroundService() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
stopForeground(true)
|
||||
}
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private suspend fun runSyncInBackground(cPath: String, aPath: String) = withContext(Dispatchers.Default) {
|
||||
val flutterLoader = FlutterLoader()
|
||||
if (!flutterLoader.initialized()) {
|
||||
withContext(Dispatchers.Main) {
|
||||
flutterLoader.startInitialization(applicationContext)
|
||||
flutterLoader.ensureInitializationComplete(applicationContext, null)
|
||||
}
|
||||
}
|
||||
val (engine, bgChannel) = withContext(Dispatchers.Main) {
|
||||
val eng = FlutterEngine(applicationContext)
|
||||
val entrypoint = DartExecutor.DartEntrypoint(
|
||||
flutterLoader.findAppBundlePath(),
|
||||
"package:firka/services/wear_sync_background.dart",
|
||||
"wearSyncBackgroundEntrypoint"
|
||||
)
|
||||
eng.dartExecutor.executeDartEntrypoint(entrypoint)
|
||||
val ch = MethodChannel(eng.dartExecutor.binaryMessenger, "app.firka/wear_sync_background")
|
||||
Pair(eng, ch)
|
||||
}
|
||||
val completer = CompletableDeferred<Unit>()
|
||||
delay(500)
|
||||
withContext(Dispatchers.Main) {
|
||||
bgChannel.invokeMethod("request_sync", mapOf(
|
||||
"cachePath" to cPath,
|
||||
"appDirPath" to aPath
|
||||
), object : MethodChannel.Result {
|
||||
override fun success(result: Any?) {
|
||||
completer.complete(Unit)
|
||||
}
|
||||
override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) {
|
||||
Log.e(TAG, "request_sync error: $errorCode $errorMessage")
|
||||
completer.complete(Unit)
|
||||
}
|
||||
override fun notImplemented() {
|
||||
completer.complete(Unit)
|
||||
}
|
||||
})
|
||||
}
|
||||
try {
|
||||
withTimeout(30_000) {
|
||||
completer.await()
|
||||
}
|
||||
} catch (_: kotlinx.coroutines.TimeoutCancellationException) {
|
||||
Log.w(TAG, "Wear sync isolate timed out")
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
engine.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "WearSyncService"
|
||||
const val ACTION_START = "app.firka.naplo.WearSyncForegroundService.START"
|
||||
const val ACTION_STOP = "app.firka.naplo.WearSyncForegroundService.STOP"
|
||||
const val EXTRA_CACHE_PATH = "cachePath"
|
||||
const val EXTRA_APP_DIR_PATH = "appDirPath"
|
||||
const val EXTRA_NOTIFICATION_TITLE = "notificationTitle"
|
||||
const val EXTRA_NOTIFICATION_TEXT = "notificationText"
|
||||
private const val PATH_WATCH_CONNECTIVITY = "watch_connectivity"
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ 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.glance.WidgetLesson
|
||||
import app.firka.naplo.model.Lesson
|
||||
import java.time.format.DateTimeFormatterBuilder
|
||||
|
||||
val hhmm = DateTimeFormatterBuilder()
|
||||
@@ -26,12 +26,8 @@ val hhmm = DateTimeFormatterBuilder()
|
||||
.toFormatter()
|
||||
|
||||
@Composable
|
||||
fun LessonCard(
|
||||
lesson: WidgetLesson,
|
||||
colors: Colors,
|
||||
modifier: GlanceModifier = GlanceModifier,
|
||||
roomBadgeWidthDp: Float = 48f,
|
||||
) {
|
||||
fun LessonCard(lesson: Lesson, colors: Colors,
|
||||
modifier: GlanceModifier = GlanceModifier) {
|
||||
Box(modifier =
|
||||
modifier
|
||||
.fillMaxWidth()
|
||||
@@ -42,7 +38,7 @@ fun LessonCard(
|
||||
var bgColor = colors.a15p
|
||||
var fgColor = colors.textSecondary
|
||||
|
||||
if (lesson.substituteTeacher != null) {
|
||||
if (lesson.substituteTeacher == null) {
|
||||
bgColor = colors.warning15p
|
||||
fgColor = colors.warningText
|
||||
}
|
||||
@@ -50,21 +46,9 @@ fun LessonCard(
|
||||
|
||||
Box(modifier = GlanceModifier.padding(12.dp)) {
|
||||
Row {
|
||||
val badgeStyle = TextStyle(
|
||||
color = ColorProvider(colors.textSecondary, colors.textSecondary),
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
val badgePadding = GlanceModifier.padding(8.dp, 4.dp)
|
||||
val lessonNumberBadgeModifier = GlanceModifier.cornerRadius(16.dp).width(24.dp)
|
||||
val roomBadgeModifier = GlanceModifier.cornerRadius(16.dp).width(roomBadgeWidthDp.dp)
|
||||
|
||||
Row(modifier = GlanceModifier.width(226.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
if (lesson.lessonNumber != null) {
|
||||
Box(
|
||||
modifier = lessonNumberBadgeModifier.background(bgColor),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Box(modifier = GlanceModifier.cornerRadius(16.dp).background(bgColor)) {
|
||||
Text(
|
||||
lesson.lessonNumber.toString(),
|
||||
style = TextStyle(
|
||||
@@ -72,7 +56,7 @@ fun LessonCard(
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
modifier = GlanceModifier.padding(4.dp, 4.dp),
|
||||
modifier = GlanceModifier.padding(8.dp, 4.dp),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = GlanceModifier.width(4.dp))
|
||||
@@ -88,6 +72,8 @@ fun LessonCard(
|
||||
)
|
||||
}
|
||||
|
||||
// Spacer(modifier = GlanceModifier.width(10.dp))
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
lesson.start.format(hhmm),
|
||||
@@ -98,15 +84,24 @@ fun LessonCard(
|
||||
),
|
||||
)
|
||||
Spacer(modifier = GlanceModifier.width(8.dp))
|
||||
val roomName = (lesson.roomName ?: "N/A").take(5)
|
||||
Box(
|
||||
modifier = roomBadgeModifier.background(colors.a15p),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
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 = badgeStyle,
|
||||
modifier = GlanceModifier.padding(4.dp, 4.dp),
|
||||
style = TextStyle(
|
||||
color = ColorProvider(colors.textSecondary, colors.textSecondary),
|
||||
fontSize = 14.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
),
|
||||
modifier = GlanceModifier.padding(8.dp, 4.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,8 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.glance.GlanceId
|
||||
import androidx.glance.GlanceModifier
|
||||
import androidx.glance.LocalSize
|
||||
import androidx.glance.appwidget.GlanceAppWidget
|
||||
import androidx.glance.appwidget.provideContent
|
||||
import androidx.glance.appwidget.SizeMode
|
||||
import androidx.glance.background
|
||||
import androidx.glance.color.ColorProvider
|
||||
import androidx.glance.currentState
|
||||
@@ -28,9 +26,7 @@ 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.glance.WidgetLesson
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import app.firka.naplo.model.Lesson
|
||||
import org.json.JSONObject
|
||||
import java.io.File
|
||||
import java.time.LocalDate
|
||||
@@ -41,51 +37,20 @@ class TimetableWidget : GlanceAppWidget() {
|
||||
override val stateDefinition: GlanceStateDefinition<*>?
|
||||
get() = HomeWidgetGlanceStateDefinition()
|
||||
|
||||
override val sizeMode: SizeMode
|
||||
get() = SizeMode.Exact
|
||||
|
||||
override suspend fun provideGlance(context: Context, id: GlanceId) {
|
||||
val data = withContext(Dispatchers.IO) {
|
||||
loadWidgetData(context)
|
||||
}
|
||||
provideContent {
|
||||
GlanceContent(context, currentState(), data)
|
||||
GlanceContent(context, currentState())
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadWidgetData(context: Context): WidgetData? {
|
||||
val appFlutter = File(context.applicationContext.dataDir, "app_flutter")
|
||||
val widgetStateFile = File(appFlutter, "widget_state.json")
|
||||
if (!widgetStateFile.exists()) return null
|
||||
val widgetState = JSONObject(widgetStateFile.readText(Charsets.UTF_8))
|
||||
val colors = Colors(widgetState)
|
||||
val tt = widgetState.getJSONArray("timetable")
|
||||
val lessons = mutableListOf<WidgetLesson>()
|
||||
for (i in 0..<tt.length()) {
|
||||
lessons.add(WidgetLesson(tt.getJSONObject(i)))
|
||||
}
|
||||
val displayDateStr = widgetState.optString("displayDate", "")
|
||||
val targetDate = if (displayDateStr.isNotEmpty()) {
|
||||
try {
|
||||
LocalDate.parse(displayDateStr)
|
||||
} catch (_: Exception) {
|
||||
LocalDate.now()
|
||||
}
|
||||
} else {
|
||||
LocalDate.now()
|
||||
}
|
||||
val start = LocalDateTime.of(targetDate.year, targetDate.month, targetDate.dayOfMonth, 0, 0)
|
||||
val end = start.plusHours(23)
|
||||
val filtered = lessons.filter { it.start.isAfter(start) && it.end.isBefore(end) }
|
||||
val headerText = if (displayDateStr.isNotEmpty()) displayDateStr else "Mai órarend"
|
||||
return WidgetData(colors, headerText, filtered)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState, data: WidgetData?) {
|
||||
if (data == null) {
|
||||
Box(
|
||||
modifier = GlanceModifier
|
||||
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(),
|
||||
@@ -100,67 +65,47 @@ class TimetableWidget : GlanceAppWidget() {
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
val size = LocalSize.current
|
||||
val lessonRowHeightDp = 52f
|
||||
val scale = lessonRowHeightDp / 52f
|
||||
val headerHeightDp = 20f * scale
|
||||
val verticalPaddingDp = 32f * scale
|
||||
val spacerDp = 4f * scale
|
||||
val paddingDp = 16f * scale
|
||||
val availableHeightDp = size.height.value - verticalPaddingDp - headerHeightDp - spacerDp
|
||||
val maxVisibleLessons = (availableHeightDp / lessonRowHeightDp).toInt().coerceAtLeast(0)
|
||||
val maxLessons = (maxVisibleLessons.coerceAtMost(16) / 2 * 2).coerceAtLeast(1)
|
||||
val displayLessons = data.lessons.take(maxLessons)
|
||||
val lessonChunks = displayLessons.chunked(2)
|
||||
val showDate = maxLessons > 1
|
||||
val maxRoomNameLen = displayLessons.maxOfOrNull { (it.roomName ?: "N/A").take(5).length } ?: 0
|
||||
val roomBadgeWidthDp = if (maxRoomNameLen <= 3) 28f else 48f
|
||||
val dateSectionHeight = if (showDate) headerHeightDp + spacerDp else 0f
|
||||
val lessonListHeight = when (val n = displayLessons.size) {
|
||||
0 -> 0f
|
||||
else -> n * lessonRowHeightDp + (n - 1) * spacerDp
|
||||
}
|
||||
val remainingHeight = (size.height.value - 2 * paddingDp - dateSectionHeight - lessonListHeight).coerceAtLeast(0f)
|
||||
val verticalPaddingAroundLessons = remainingHeight / 2f
|
||||
val widgetState = JSONObject(widgetStateFile.readText(Charsets.UTF_8))
|
||||
val colors = Colors(widgetState)
|
||||
|
||||
Box(
|
||||
modifier = GlanceModifier
|
||||
.background(data.colors.background)
|
||||
.padding(paddingDp.dp)
|
||||
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 {
|
||||
if (showDate) {
|
||||
Text(
|
||||
data.headerText,
|
||||
style = TextStyle(
|
||||
color = ColorProvider(data.colors.textSecondary, data.colors.textSecondary),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Text(
|
||||
"Mai órarend",
|
||||
style = TextStyle(
|
||||
color = ColorProvider(colors.textSecondary, colors.textSecondary),
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
Spacer(modifier = GlanceModifier.height(spacerDp.dp))
|
||||
)
|
||||
Spacer(modifier = GlanceModifier.height(4.dp))
|
||||
for (lesson in lessons) {
|
||||
LessonCard(lesson, colors)
|
||||
Spacer(modifier = GlanceModifier.height(4.dp))
|
||||
}
|
||||
Spacer(modifier = GlanceModifier.height(verticalPaddingAroundLessons.dp))
|
||||
for (chunk in lessonChunks) {
|
||||
Column {
|
||||
for (lesson in chunk) {
|
||||
LessonCard(lesson, data.colors, roomBadgeWidthDp = roomBadgeWidthDp)
|
||||
Spacer(modifier = GlanceModifier.height(spacerDp.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = GlanceModifier.height(verticalPaddingAroundLessons.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class WidgetData(
|
||||
val colors: Colors,
|
||||
val headerText: String,
|
||||
val lessons: List<WidgetLesson>,
|
||||
)
|
||||
}
|
||||
@@ -1,25 +1,7 @@
|
||||
package app.firka.naplo.glance
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import HomeWidgetGlanceWidgetReceiver
|
||||
import androidx.glance.appwidget.GlanceAppWidgetManager
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
class TimetableWidgetReceiver : HomeWidgetGlanceWidgetReceiver<TimetableWidget>() {
|
||||
override val glanceAppWidget = TimetableWidget()
|
||||
|
||||
override fun onAppWidgetOptionsChanged(
|
||||
context: Context,
|
||||
appWidgetManager: AppWidgetManager,
|
||||
appWidgetId: Int,
|
||||
newOptions: Bundle,
|
||||
) {
|
||||
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
|
||||
runBlocking {
|
||||
val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(appWidgetId)
|
||||
glanceAppWidget.update(context, glanceId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
package app.firka.naplo.glance
|
||||
|
||||
import app.firka.naplo.getIntOrNull
|
||||
import app.firka.naplo.getStringOrNull
|
||||
import org.json.JSONObject
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatterBuilder
|
||||
|
||||
class WidgetLesson(data: JSONObject) {
|
||||
val formatter = DateTimeFormatterBuilder()
|
||||
.appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS")
|
||||
.optionalStart()
|
||||
.appendLiteral('Z')
|
||||
.optionalEnd()
|
||||
.toFormatter()
|
||||
|
||||
val name: String = data.getString("Nev")
|
||||
val start: LocalDateTime = LocalDateTime.parse(data.getString("KezdetIdopont"), formatter)
|
||||
val end: LocalDateTime = LocalDateTime.parse(data.getString("VegIdopont"), formatter)
|
||||
val lessonNumber: Int? = data.getIntOrNull("Oraszam")
|
||||
val roomName: String? = data.getStringOrNull("TeremNeve")
|
||||
val substituteTeacher: String? = data.getStringOrNull("HelyettesTanarNeve")
|
||||
}
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 9.7 KiB After Width: | Height: | Size: 8.8 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.1 KiB |
@@ -8,7 +8,6 @@
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenBackground">#7ca120</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
|
||||
<item name="android:windowSplashScreenIconBackgroundColor">#7ca120</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
|
||||
<item name="android:windowSplashScreenBackground">#7ca120</item>
|
||||
<item name="android:windowSplashScreenAnimatedIcon">@drawable/android12splash</item>
|
||||
<item name="android:windowSplashScreenIconBackgroundColor">#7ca120</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:initialLayout="@layout/glance_default_loading_layout"
|
||||
android:minWidth="250dp"
|
||||
android:minHeight="93dp"
|
||||
android:minResizeWidth="180dp"
|
||||
android:minResizeHeight="93dp"
|
||||
android:minWidth="300dp"
|
||||
android:minHeight="100dp"
|
||||
android:resizeMode="horizontal|vertical"
|
||||
android:updatePeriodMillis="1800000">
|
||||
android:updatePeriodMillis="10000">
|
||||
</appwidget-provider>
|
||||
@@ -22,8 +22,8 @@ subprojects {
|
||||
if (plugins.hasPlugin("com.android.application") || plugins.hasPlugin("com.android.library")) {
|
||||
val androidExtension = extensions.getByName("android") as BaseExtension
|
||||
androidExtension.apply {
|
||||
compileSdkVersion(36)
|
||||
buildToolsVersion = "36.1.0"
|
||||
compileSdkVersion(35)
|
||||
buildToolsVersion = "35.0.0"
|
||||
}
|
||||
}
|
||||
if (hasProperty("android")) {
|
||||
@@ -40,6 +40,9 @@ subprojects {
|
||||
|
||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||
}
|
||||
subprojects {
|
||||
project.evaluationDependsOn(":app")
|
||||
}
|
||||
|
||||
tasks.register<Delete>("clean") {
|
||||
delete(rootProject.layout.buildDirectory)
|
||||
|
||||
@@ -1,13 +1,3 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
# Disabled for faster config and incremental builds; re-enable if any dependency needs support-library
|
||||
android.enableJetifier=false
|
||||
|
||||
# Build performance (cold and warm)
|
||||
org.gradle.caching=true
|
||||
org.gradle.parallel=true
|
||||
# Configuration cache disabled: Flutter/AGP/Kotlin plugin not fully compatible (KotlinBaseApiPlugin / ProjectServices)
|
||||
# org.gradle.configuration-cache=true
|
||||
org.gradle.daemon=true
|
||||
# Better Kotlin incremental compilation (warm builds)
|
||||
kotlin.incremental.useClasspathSnapshot=true
|
||||
android.enableJetifier=true
|
||||
|
||||
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
|
||||
|
||||
@@ -18,9 +18,8 @@ pluginManagement {
|
||||
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.11.1" apply false
|
||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||
id("org.jetbrains.kotlin.plugin.compose") version "2.1.0" apply false
|
||||
id("com.android.application") version "8.7.0" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.8.22" apply false
|
||||
}
|
||||
|
||||
include(":app")
|
||||
|
||||
|
Before Width: | Height: | Size: 30 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="23" height="18" viewBox="0 0 23 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.8252 0L3.09442 6.29374L2.5175 3.93359C3.26925 3.93359 3.88114 4.15212 4.35317 4.58919C4.8252 5.02625 5.06122 5.6294 5.06122 6.39864C5.06122 7.15039 4.81646 7.76228 4.32695 8.23431C3.85492 8.68886 3.26051 8.91613 2.54372 8.91613C1.80945 8.91613 1.19756 8.68886 0.708046 8.23431C0.236015 7.76228 0 7.15039 0 6.39864C0 6.17136 0.0174826 5.95283 0.0524478 5.74304C0.0874131 5.51576 0.157344 5.25352 0.262239 4.95632C0.367135 4.65912 0.515737 4.26576 0.708046 3.77624L2.22903 0H4.8252ZM11.014 0L9.28327 6.29374L8.70634 3.93359C9.45809 3.93359 10.07 4.15212 10.542 4.58919C11.014 5.02625 11.2501 5.6294 11.2501 6.39864C11.2501 7.15039 11.0053 7.76228 10.5158 8.23431C10.0438 8.68886 9.44935 8.91613 8.73256 8.91613C7.99829 8.91613 7.3864 8.68886 6.89689 8.23431C6.42486 7.76228 6.18884 7.15039 6.18884 6.39864C6.18884 6.17136 6.20633 5.95283 6.24129 5.74304C6.27626 5.51576 6.34619 5.25352 6.45108 4.95632C6.55598 4.65912 6.70458 4.26576 6.89689 3.77624L8.41788 0H11.014Z" fill="#A0D025"/>
|
||||
<path d="M17.6748 17.832L19.4056 11.5383L19.9825 13.8984C19.2308 13.8984 18.6189 13.6799 18.1468 13.2428C17.6748 12.8058 17.4388 12.2026 17.4388 11.4334C17.4388 10.6816 17.6835 10.0698 18.1731 9.59772C18.6451 9.14317 19.2395 8.9159 19.9563 8.9159C20.6905 8.9159 21.3024 9.14317 21.792 9.59772C22.264 10.0698 22.5 10.6816 22.5 11.4334C22.5 11.6607 22.4825 11.8792 22.4476 12.089C22.4126 12.3163 22.3427 12.5785 22.2378 12.8757C22.1329 13.1729 21.9843 13.5663 21.792 14.0558L20.271 17.832H17.6748ZM11.486 17.832L13.2167 11.5383L13.7937 13.8984C13.0419 13.8984 12.43 13.6799 11.958 13.2428C11.486 12.8058 11.2499 12.2026 11.2499 11.4334C11.2499 10.6816 11.4947 10.0698 11.9842 9.59772C12.4562 9.14317 13.0506 8.9159 13.7674 8.9159C14.5017 8.9159 15.1136 9.14317 15.6031 9.59772C16.0751 10.0698 16.3112 10.6816 16.3112 11.4334C16.3112 11.6607 16.2937 11.8792 16.2587 12.089C16.2237 12.3163 16.1538 12.5785 16.0489 12.8757C15.944 13.1729 15.7954 13.5663 15.6031 14.0558L14.0821 17.832H11.486Z" fill="#A0D025"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.1 KiB |
@@ -1,4 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M21.2187 15.3754C21.5637 14.9441 22.1937 14.8741 22.6249 15.2191C23.0562 15.5641 23.1262 16.1941 22.7812 16.6254L18.7812 21.6254C18.6036 21.8474 18.3394 21.9826 18.0556 21.9984C17.7716 22.0142 17.494 21.9086 17.2929 21.7074L15.2929 19.7074C14.9024 19.3169 14.9024 18.6839 15.2929 18.2934C15.6834 17.9028 16.3164 17.9028 16.707 18.2934L17.9159 19.5023L21.2187 15.3754Z" fill="#A0D025"/>
|
||||
<path d="M10.7998 3.65137C11.146 3.39172 11.5673 3.25098 12 3.25098C12.4327 3.25098 12.854 3.39172 13.2002 3.65137L20.2002 8.90137C20.4484 9.08763 20.6503 9.32886 20.7891 9.60645C20.9279 9.88415 21 10.1905 21 10.501V12.4565C21 12.9796 20.4396 13.3258 19.9346 13.1894C19.4773 13.066 18.9964 13 18.5 13C16.5152 13 14.776 14.0513 13.8089 15.6274C13.6713 15.8515 13.4337 16.001 13.1708 16.001H13H11V19.001C11 19.5314 10.7891 20.04 10.4141 20.415C10.039 20.7901 9.53043 21.001 9 21.001H5C4.46957 21.001 3.96101 20.7901 3.58594 20.415C3.21087 20.04 3 19.5314 3 19.001V10.501C3 10.1905 3.07208 9.88416 3.21094 9.60645C3.34973 9.32885 3.55156 9.08763 3.7998 8.90137L10.7998 3.65137Z" fill="#A0D025"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,21 +1,16 @@
|
||||
flutter_native_splash:
|
||||
color: "#7ca120"
|
||||
image: assets/images/logos/splash.png
|
||||
# Keep image centered instead of fill-scaled (stops icon looking zoomed/cropped)
|
||||
android_gravity: center
|
||||
|
||||
# Dark mode - same color as light mode for consistency
|
||||
color_dark: "#7ca120"
|
||||
image_dark: assets/images/logos/splash.png
|
||||
|
||||
# Android 12+ uses 960×960 image with logo in 640px circle (generated by codegen) to avoid cropping
|
||||
android_12:
|
||||
image: assets/images/logos/splash_android12.png
|
||||
image: assets/images/logos/splash.png
|
||||
color: "#7ca120"
|
||||
color_dark: "#7ca120"
|
||||
image_dark: assets/images/logos/splash_android12.png
|
||||
icon_background_color: "#7ca120"
|
||||
icon_background_color_dark: "#7ca120"
|
||||
image_dark: assets/images/logos/splash.png
|
||||
|
||||
ios: true
|
||||
web: false
|
||||
|
||||
42
firka/integration_test/phone_main_home_test.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:firka/helpers/db/models/generic_cache_model.dart';
|
||||
import 'package:firka/helpers/db/models/timetable_cache_model.dart';
|
||||
import 'package:firka/helpers/db/models/token_model.dart';
|
||||
import 'package:firka/main.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import 'test_helpers.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await resetAppData();
|
||||
setApiUrls();
|
||||
|
||||
group('main', () {
|
||||
testWidgets('InitializationScreen -> HomeScreen', (tester) async {
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
|
||||
var isar = await Isar.open(
|
||||
[TokenModelSchema, GenericCacheModelSchema, TimetableCacheModelSchema],
|
||||
inspector: true,
|
||||
directory: dir.path,
|
||||
);
|
||||
isarInit = isar;
|
||||
|
||||
await isar.writeTxn(() async {
|
||||
await isar.tokenModels.put(TokenModel());
|
||||
});
|
||||
|
||||
await tester.pumpWidget(InitializationScreen());
|
||||
|
||||
await waitUntil(Duration(minutes: 2), tester, () async {
|
||||
var ele = find.byKey(const Key('homeScreen'));
|
||||
return ele.allCandidates.isNotEmpty;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
24
firka/integration_test/phone_main_login_test.dart
Normal file
@@ -0,0 +1,24 @@
|
||||
import 'package:firka/main.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'test_helpers.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
await resetAppData();
|
||||
setApiUrls();
|
||||
|
||||
group('main', () {
|
||||
testWidgets('InitializationScreen -> LoginScreen', (tester) async {
|
||||
await tester.pumpWidget(InitializationScreen());
|
||||
|
||||
await waitUntil(Duration(minutes: 2), tester, () async {
|
||||
var ele = find.byKey(const Key('loginScreen'));
|
||||
return ele.allCandidates.isNotEmpty;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
42
firka/integration_test/test_helpers.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:firka/helpers/api/consts.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
Future<bool> isWear() async {
|
||||
const platform = MethodChannel('firka.app/main');
|
||||
|
||||
return await platform.invokeMethod("isWear");
|
||||
}
|
||||
|
||||
Future<bool> isPhone() async {
|
||||
return !(await isWear());
|
||||
}
|
||||
|
||||
Future<void> resetAppData() async {
|
||||
final isarDir = await getApplicationDocumentsDirectory();
|
||||
if (await isarDir.exists()) await isarDir.delete(recursive: true);
|
||||
}
|
||||
|
||||
void setApiUrls() {
|
||||
KretaEndpoints.kretaBase = "localhost:8060";
|
||||
KretaEndpoints.kretaIdp = "http://localhost:8060";
|
||||
KretaEndpoints.kretaLoginUrl =
|
||||
"${KretaEndpoints.kretaIdp}/Account/Login?ReturnUrl=%2Fconnect%2Fauthorize%2Fcallback%3Fprompt%3Dlogin%26nonce%3DwylCrqT4oN6PPgQn2yQB0euKei9nJeZ6_ffJ-VpSKZU%26response_type%3Dcode%26code_challenge_method%3DS256%26scope%3Dopenid%2520email%2520offline_access%2520kreta-ellenorzo-webapi.public%2520kreta-eugyintezes-webapi.public%2520kreta-fileservice-webapi.public%2520kreta-mobile-global-webapi.public%2520kreta-dkt-webapi.public%2520kreta-ier-webapi.public%26code_challenge%3DHByZRRnPGb-Ko_wTI7ibIba1HQ6lor0ws4bcgReuYSQ%26redirect_uri%3Dhttps%253A%252F%252Fmobil.e-kreta.hu%252Fellenorzo-student%252Fprod%252Foauthredirect%26client_id%3Dkreta-ellenorzo-student-mobile-ios%26state%3Dkreta_student_mobile%26suppressed_prompt%3Dlogin";
|
||||
KretaEndpoints.tokenGrantUrl = "${KretaEndpoints.kretaIdp}/connect/token";
|
||||
}
|
||||
|
||||
Future<void> waitUntil(Duration timeout, WidgetTester tester,
|
||||
Future<bool> Function() callback) async {
|
||||
var now = DateTime.now();
|
||||
while (
|
||||
now.difference(DateTime.now()).inMilliseconds < timeout.inMilliseconds) {
|
||||
await tester.pump(Duration(milliseconds: 100));
|
||||
|
||||
if (await callback()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw Exception("waitUntil timed out");
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:firka/api/client/kreta_client.dart';
|
||||
import 'package:firka/core/bloc/home_refresh_cubit.dart';
|
||||
import 'package:firka/core/bloc/profile_picture_cubit.dart';
|
||||
import 'package:firka/core/bloc/reauth_cubit.dart';
|
||||
import 'package:firka/core/bloc/settings_cubit.dart';
|
||||
import 'package:firka/core/bloc/theme_cubit.dart';
|
||||
import 'package:firka/data/models/token_model.dart';
|
||||
import 'package:firka/core/settings.dart';
|
||||
import 'package:firka/l10n/app_localizations.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:isar_community/isar.dart';
|
||||
import 'dart:io';
|
||||
|
||||
late final Logger logger;
|
||||
|
||||
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||
late AppInitialization initData;
|
||||
bool initDone = false;
|
||||
|
||||
/// Set when app router is created; used for deep links and notifications.
|
||||
GoRouter? appRouter;
|
||||
|
||||
final dio = Dio();
|
||||
final isBeta = true;
|
||||
|
||||
class DeviceInfo {
|
||||
String model;
|
||||
|
||||
String versionRelease;
|
||||
String versionSdkInt;
|
||||
|
||||
DeviceInfo(this.model, this.versionRelease, this.versionSdkInt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "DeviceInfo(model = \"$model\", versionRelease = \"$versionRelease\""
|
||||
", versionSdkInt = \"$versionSdkInt\"";
|
||||
}
|
||||
}
|
||||
|
||||
class AppInitialization {
|
||||
final Isar isar;
|
||||
final Directory appDir;
|
||||
final PackageInfo packageInfo;
|
||||
final DeviceInfo devInfo;
|
||||
late KretaClient client;
|
||||
List<TokenModel> tokens;
|
||||
bool hasWatchListener = false;
|
||||
|
||||
/// Set by the wear pairing modal; called when watch sends init_done or sync_done to dismiss the sheet.
|
||||
void Function()? dismissWearPairingSheet;
|
||||
Uint8List? profilePicture;
|
||||
SettingsStore settings;
|
||||
ThemeCubit? themeCubit;
|
||||
SettingsCubit? settingsCubit;
|
||||
ProfilePictureCubit? profilePictureCubit;
|
||||
ReauthCubit? reauthCubit;
|
||||
HomeRefreshCubit? homeRefreshCubit;
|
||||
AppLocalizations l10n;
|
||||
final GlobalKey<NavigatorState> navigatorKey;
|
||||
|
||||
AppInitialization({
|
||||
required this.isar,
|
||||
required this.appDir,
|
||||
required this.devInfo,
|
||||
required this.packageInfo,
|
||||
required this.tokens,
|
||||
required this.settings,
|
||||
required this.l10n,
|
||||
required this.navigatorKey,
|
||||
});
|
||||
}
|
||||
@@ -1,407 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:firka/app/app_state.dart';
|
||||
import 'package:firka/core/bloc/home_refresh_cubit.dart';
|
||||
import 'package:firka/core/bloc/profile_picture_cubit.dart';
|
||||
import 'package:firka/core/bloc/reauth_cubit.dart';
|
||||
import 'package:firka/core/bloc/settings_cubit.dart';
|
||||
import 'package:firka/core/bloc/theme_cubit.dart';
|
||||
import 'package:firka/services/active_account_helper.dart';
|
||||
import 'package:firka/api/client/kreta_client.dart';
|
||||
import 'package:firka/data/models/app_settings_model.dart';
|
||||
import 'package:firka/data/models/generic_cache_model.dart';
|
||||
import 'package:firka/data/models/homework_cache_model.dart';
|
||||
import 'package:firka/data/models/timetable_cache_model.dart';
|
||||
import 'package:firka/data/models/token_model.dart';
|
||||
import 'package:firka/services/live_activity_service.dart';
|
||||
import 'package:firka/core/settings.dart';
|
||||
import 'package:firka/services/watch_sync_helper.dart';
|
||||
import 'package:firka/l10n/app_localizations_de.dart';
|
||||
import 'package:firka/l10n/app_localizations_en.dart';
|
||||
import 'package:firka/l10n/app_localizations_hu.dart';
|
||||
import 'package:firka/core/swear_generator.dart';
|
||||
import 'package:firka/ui/theme/style.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:isar_community/isar.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
Isar? isarInit;
|
||||
|
||||
Future<Isar> initDB() async {
|
||||
if (isarInit != null) return isarInit!;
|
||||
final dir = await getApplicationDocumentsDirectory();
|
||||
|
||||
isarInit = await Isar.open(
|
||||
[
|
||||
TokenModelSchema,
|
||||
GenericCacheModelSchema,
|
||||
TimetableCacheModelSchema,
|
||||
HomeworkCacheModelSchema,
|
||||
AppSettingsModelSchema,
|
||||
HomeworkDoneModelSchema,
|
||||
],
|
||||
inspector: true,
|
||||
directory: dir.path,
|
||||
);
|
||||
|
||||
return isarInit!;
|
||||
}
|
||||
|
||||
Future<void> initLang(AppInitialization data) async {
|
||||
String? languageCode;
|
||||
|
||||
switch ((data.settings.group("settings").subGroup("application")["language"]
|
||||
as SettingsItemsRadio)
|
||||
.activeIndex) {
|
||||
case 1: // hu
|
||||
data.l10n = AppLocalizationsHu();
|
||||
languageCode = 'hu';
|
||||
break;
|
||||
case 2: // en
|
||||
data.l10n = AppLocalizationsEn();
|
||||
languageCode = 'en';
|
||||
break;
|
||||
case 3: // de
|
||||
data.l10n = AppLocalizationsDe();
|
||||
languageCode = 'de';
|
||||
break;
|
||||
default: // auto
|
||||
switch (ui.PlatformDispatcher.instance.locale.languageCode) {
|
||||
case 'hu':
|
||||
data.l10n = AppLocalizationsHu();
|
||||
languageCode = 'hu';
|
||||
break;
|
||||
case 'en':
|
||||
data.l10n = AppLocalizationsEn();
|
||||
languageCode = 'en';
|
||||
break;
|
||||
case 'de':
|
||||
data.l10n = AppLocalizationsDe();
|
||||
languageCode = 'de';
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (languageCode != null && Platform.isIOS) {
|
||||
try {
|
||||
await LiveActivityService.updateLanguagePreference(languageCode);
|
||||
} catch (e) {
|
||||
logger.warning('Failed to update language preference on backend: $e');
|
||||
}
|
||||
|
||||
try {
|
||||
await WatchSyncHelper.sendLanguageToWatch();
|
||||
} catch (e) {
|
||||
logger.warning('Failed to send language to Watch: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void initTheme(AppInitialization data) {
|
||||
final themeCubit = data.themeCubit;
|
||||
if (themeCubit == null) return;
|
||||
|
||||
final brightness =
|
||||
SchedulerBinding.instance.platformDispatcher.platformBrightness;
|
||||
|
||||
switch ((data.settings.group("settings").subGroup("customization")["theme"]
|
||||
as SettingsItemsRadio)
|
||||
.activeIndex) {
|
||||
case 1:
|
||||
appStyle = lightStyle;
|
||||
themeCubit.setLightMode(true);
|
||||
break;
|
||||
case 2:
|
||||
appStyle = darkStyle;
|
||||
themeCubit.setLightMode(false);
|
||||
break;
|
||||
default:
|
||||
if (brightness == Brightness.dark) {
|
||||
appStyle = darkStyle;
|
||||
themeCubit.setLightMode(false);
|
||||
} else {
|
||||
appStyle = lightStyle;
|
||||
themeCubit.setLightMode(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _initData(AppInitialization init) async {
|
||||
init.themeCubit ??= ThemeCubit();
|
||||
init.settingsCubit ??= SettingsCubit();
|
||||
init.profilePictureCubit ??= ProfilePictureCubit();
|
||||
init.reauthCubit ??= ReauthCubit();
|
||||
init.homeRefreshCubit ??= HomeRefreshCubit();
|
||||
await init.settings.load(init.isar.appSettingsModels);
|
||||
await initLang(init);
|
||||
initTheme(init);
|
||||
init.settings = SettingsStore(init.l10n);
|
||||
await init.settings.load(init.isar.appSettingsModels);
|
||||
|
||||
var dispatcher = SchedulerBinding.instance.platformDispatcher;
|
||||
|
||||
dispatcher.onPlatformBrightnessChanged = () {
|
||||
initTheme(init);
|
||||
};
|
||||
|
||||
dispatcher.onLocaleChanged = () {
|
||||
final languageSetting =
|
||||
init.settings.group("settings").subGroup("application")["language"]
|
||||
as SettingsItemsRadio;
|
||||
final isAutoLanguage = languageSetting.activeIndex == 0;
|
||||
if (!isAutoLanguage) {
|
||||
return;
|
||||
}
|
||||
|
||||
final previousLocale = init.l10n.localeName;
|
||||
unawaited(() async {
|
||||
await initLang(init);
|
||||
final nextLocale = init.l10n.localeName;
|
||||
if (previousLocale != nextLocale) {
|
||||
logger.info(
|
||||
"[Init] System locale changed in auto mode: $previousLocale -> $nextLocale",
|
||||
);
|
||||
}
|
||||
init.themeCubit?.refresh();
|
||||
}());
|
||||
};
|
||||
|
||||
resetOldTimeTableCache(init.isar);
|
||||
resetOldHomeworkCache(init.isar);
|
||||
|
||||
var didRunFreshInstallCleanup = false;
|
||||
if (Platform.isIOS) {
|
||||
try {
|
||||
didRunFreshInstallCleanup =
|
||||
await WatchSyncHelper.runFreshInstallCleanupIfNeeded(isar: init.isar);
|
||||
if (didRunFreshInstallCleanup) {
|
||||
logger.info(
|
||||
'[Init] Fresh-install cleanup completed; skipping startup iCloud recovery on this launch',
|
||||
);
|
||||
} else {
|
||||
await WatchSyncHelper.checkAndRecoverFromiCloud(
|
||||
isar: init.isar,
|
||||
tokens: init.tokens,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warning('[Init] iCloud bootstrap/recovery failed: $e');
|
||||
}
|
||||
}
|
||||
|
||||
final allTokens = await init.isar.tokenModels.where().findAll();
|
||||
init.tokens = allTokens;
|
||||
|
||||
if (allTokens.isNotEmpty) {
|
||||
final token = pickActiveToken(tokens: allTokens, settings: init.settings);
|
||||
if (token == null) {
|
||||
logger.warning(
|
||||
"[Init] Tokens disappeared during initialization; skipping client setup",
|
||||
);
|
||||
return;
|
||||
}
|
||||
logger.fine("Initializing kréta client as: ${token.studentId}");
|
||||
init.client = KretaClient(token, init.isar, init.reauthCubit!);
|
||||
|
||||
if (Platform.isIOS) {
|
||||
final expiryDate = token.expiryDate;
|
||||
if (expiryDate != null && expiryDate.isAfter(DateTime.now())) {
|
||||
init.reauthCubit?.clear();
|
||||
}
|
||||
|
||||
unawaited(() async {
|
||||
try {
|
||||
await WatchSyncHelper.saveTokenToiCloud(token);
|
||||
} catch (e) {
|
||||
logger.warning('[Init] Failed to sync active token to iCloud: $e');
|
||||
}
|
||||
|
||||
try {
|
||||
await WatchSyncHelper.sendTokenModelToWatch(token);
|
||||
} catch (e) {
|
||||
logger.warning('[Init] Failed to sync active token to Watch: $e');
|
||||
}
|
||||
}());
|
||||
}
|
||||
}
|
||||
|
||||
final dataDir = await getApplicationDocumentsDirectory();
|
||||
var pfpFile = File(p.join(dataDir.path, "profile.webp"));
|
||||
|
||||
if (await pfpFile.exists()) {
|
||||
init.profilePicture = await pfpFile.readAsBytes();
|
||||
}
|
||||
}
|
||||
|
||||
Future<AppInitialization> initializeApp() async {
|
||||
if (initDone) {
|
||||
await _initData(initData);
|
||||
return initData;
|
||||
}
|
||||
final isar = await initDB();
|
||||
final tokens = await isar.tokenModels.where().findAll();
|
||||
|
||||
logger.finest('Token count: ${tokens.length}');
|
||||
|
||||
var devInfoFetched = false;
|
||||
var devInfo = DeviceInfo("SM-A705FN", "11", "30");
|
||||
|
||||
try {
|
||||
if (Platform.isAndroid) {
|
||||
const channel = MethodChannel("firka.app/main");
|
||||
final rawInfo = ((await channel.invokeMethod("get_info")) as String)
|
||||
.split(";");
|
||||
|
||||
devInfo = DeviceInfo(rawInfo[0], rawInfo[1], rawInfo[2]);
|
||||
devInfoFetched = true;
|
||||
}
|
||||
} catch (e) {
|
||||
if (e is Error) {
|
||||
logger.shout("Error in initializeApp()", e.toString(), e.stackTrace);
|
||||
} else {
|
||||
logger.shout("Error in initializeApp()", e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
logger.fine("Fetched device info: ${devInfoFetched ? "yes" : "no"}");
|
||||
logger.fine("Using device info: ${devInfo.toString()}");
|
||||
|
||||
var init = AppInitialization(
|
||||
isar: isar,
|
||||
appDir: await getApplicationDocumentsDirectory(),
|
||||
devInfo: devInfo,
|
||||
packageInfo: await PackageInfo.fromPlatform(),
|
||||
tokens: tokens,
|
||||
settings: SettingsStore(AppLocalizationsHu()),
|
||||
l10n: AppLocalizationsHu(),
|
||||
navigatorKey: navigatorKey,
|
||||
);
|
||||
|
||||
if (Platform.isIOS) {
|
||||
try {
|
||||
await LiveActivityService.initialize().timeout(
|
||||
const Duration(seconds: 8),
|
||||
);
|
||||
} on TimeoutException catch (e, st) {
|
||||
logger.warning('LiveActivity init timed out: $e', e, st);
|
||||
} catch (e, st) {
|
||||
logger.severe('Failed to initialize LiveActivity: $e', e, st);
|
||||
}
|
||||
}
|
||||
|
||||
await _initData(init);
|
||||
|
||||
return init;
|
||||
}
|
||||
|
||||
Future<void> setupLogging() async {
|
||||
final jwtPattern = RegExp(
|
||||
r'([A-Za-z0-9-_]+)\.([A-Za-z0-9-_]+)\.([A-Za-z0-9-_]+)',
|
||||
);
|
||||
final omPattern = RegExp(r'(\d{3})(\d{6})([A-Za-z0-9]?)');
|
||||
final refreshTokenPattern = RegExp(
|
||||
r'"(?=.{21,}$)([A-Z0-9]+-[A-Z0-9_\-.~+]*)"',
|
||||
);
|
||||
|
||||
final docs = await getApplicationDocumentsDirectory();
|
||||
|
||||
Future<void> deleteOldLogFiles() async {
|
||||
final docs = await getApplicationDocumentsDirectory();
|
||||
final dir = Directory(docs.path);
|
||||
if (!dir.existsSync()) return;
|
||||
|
||||
final now = DateTime.now();
|
||||
final cutoff = now.subtract(Duration(days: 30));
|
||||
|
||||
final logFileRegex = RegExp(r'^(\d{4})_(\d{2})_(\d{2})\.log$');
|
||||
|
||||
for (final entity in dir.listSync()) {
|
||||
if (entity is! File) continue;
|
||||
final name = entity.uri.pathSegments.last;
|
||||
final m = logFileRegex.firstMatch(name);
|
||||
if (m == null) continue;
|
||||
|
||||
try {
|
||||
final y = int.parse(m.group(1)!);
|
||||
final mo = int.parse(m.group(2)!);
|
||||
final d = int.parse(m.group(3)!);
|
||||
final fileDate = DateTime(y, mo, d);
|
||||
if (fileDate.isBefore(
|
||||
DateTime(cutoff.year, cutoff.month, cutoff.day),
|
||||
)) {
|
||||
logger.info("Removing old log file: $name");
|
||||
await entity.delete();
|
||||
}
|
||||
} catch (_) {
|
||||
// ignore parse/delete errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String logFilePathForDate(DateTime dt) {
|
||||
final fileName = "${DateFormat("yyyy_MM_dd").format(dt)}.log";
|
||||
return Directory(docs.path).uri.resolve(fileName).toFilePath();
|
||||
}
|
||||
|
||||
File fileForDate(DateTime dt) {
|
||||
final path = logFilePathForDate(dt);
|
||||
final file = File(path);
|
||||
if (!file.existsSync()) file.createSync(recursive: true);
|
||||
return file;
|
||||
}
|
||||
|
||||
String censorLog(String msg) {
|
||||
return msg
|
||||
.replaceAll(jwtPattern, '***')
|
||||
.replaceAllMapped(omPattern, (match) {
|
||||
return "${match.group(1)}******${match.group(3)}";
|
||||
})
|
||||
.replaceAll(refreshTokenPattern, '"***"');
|
||||
}
|
||||
|
||||
hierarchicalLoggingEnabled = true;
|
||||
logger.level = Level.ALL;
|
||||
|
||||
DateTime currentDate = DateTime.now();
|
||||
IOSink sink = fileForDate(currentDate).openWrite(mode: FileMode.append);
|
||||
|
||||
logger.onRecord.listen((record) {
|
||||
final now = DateTime.now();
|
||||
if (now.year != currentDate.year ||
|
||||
now.month != currentDate.month ||
|
||||
now.day != currentDate.day) {
|
||||
sink.flush();
|
||||
sink.close();
|
||||
currentDate = now;
|
||||
sink = fileForDate(currentDate).openWrite(mode: FileMode.append);
|
||||
}
|
||||
|
||||
final censored = censorLog(record.message);
|
||||
final timestamp = DateFormat('yyyy-MM-dd HH:mm:ss.SSS').format(now);
|
||||
final level = record.level.name;
|
||||
final line = '[$timestamp] [$level] [$censored]';
|
||||
sink.writeln(line);
|
||||
|
||||
debugPrint(
|
||||
"[Firka] [${record.level.name}] ${kDebugMode ? record.message : censored}",
|
||||
);
|
||||
});
|
||||
|
||||
unawaited(deleteOldLogFiles());
|
||||
|
||||
try {
|
||||
logger.finest('loading dirty words');
|
||||
await loadDirtyWords();
|
||||
logger.finest('loaded dirty words');
|
||||
} catch (e, st) {
|
||||
logger.severe('Failed to load dirty words: $e', e, st);
|
||||
}
|
||||
}
|
||||
@@ -1,237 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_native_splash/flutter_native_splash.dart';
|
||||
import 'package:firka/app/app_state.dart';
|
||||
import 'package:firka/app/initialization.dart';
|
||||
import 'package:firka/core/bloc/home_refresh_cubit.dart';
|
||||
import 'package:firka/core/settings.dart';
|
||||
import 'package:firka/core/bloc/profile_picture_cubit.dart';
|
||||
import 'package:firka/core/bloc/reauth_cubit.dart';
|
||||
import 'package:firka/core/bloc/settings_cubit.dart';
|
||||
import 'package:firka/core/bloc/theme_cubit.dart';
|
||||
import 'package:firka/core/firka_bundle.dart';
|
||||
import 'package:firka/routing/app_router.dart';
|
||||
import 'package:firka/services/watch_sync_helper.dart';
|
||||
import 'package:firka/ui/phone/pages/extras/main_wear_pair.dart';
|
||||
import 'package:firka/l10n/app_localizations.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
class InitializationScreen extends StatefulWidget {
|
||||
const InitializationScreen({super.key});
|
||||
|
||||
@override
|
||||
State<InitializationScreen> createState() => _InitializationScreenState();
|
||||
}
|
||||
|
||||
class _InitializationScreenState extends State<InitializationScreen> {
|
||||
GoRouter? _router;
|
||||
final Future<AppInitialization> _init = initializeApp().timeout(
|
||||
const Duration(seconds: 20),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<AppInitialization>(
|
||||
future: _init,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.hasError) {
|
||||
logger.shout(
|
||||
"Error in InitializationScreen",
|
||||
snapshot.error.toString(),
|
||||
snapshot.stackTrace,
|
||||
);
|
||||
|
||||
FlutterNativeSplash.remove();
|
||||
|
||||
return MaterialApp(
|
||||
key: const ValueKey('errorPage'),
|
||||
home: DefaultAssetBundle(
|
||||
bundle: FirkaBundle(),
|
||||
child: Scaffold(
|
||||
body: Center(
|
||||
child: Text(
|
||||
'Error initializing app: ${snapshot.error}',
|
||||
style: const TextStyle(color: Colors.red),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
assert(snapshot.data != null);
|
||||
initData = snapshot.data!;
|
||||
initDone = true;
|
||||
|
||||
FlutterNativeSplash.remove();
|
||||
|
||||
WatchSyncHelper.initialize();
|
||||
if (Platform.isAndroid) {
|
||||
WatchSyncHelper.setWearSyncMethodCallHandler();
|
||||
}
|
||||
if (Platform.isIOS) {
|
||||
unawaited(() async {
|
||||
try {
|
||||
await WatchSyncHelper.sendLanguageToWatch();
|
||||
} catch (e) {
|
||||
logger.warning(
|
||||
'[Init] Failed to publish language to Watch after sync init: $e',
|
||||
);
|
||||
}
|
||||
}());
|
||||
}
|
||||
|
||||
if (!initData.hasWatchListener) {
|
||||
initData.hasWatchListener = true;
|
||||
|
||||
WatchSyncHelper.onWatchMessage = (msg) {
|
||||
logger.finest("WatchOS IPC [Watch -> Phone]: ${msg["id"]}");
|
||||
|
||||
switch (msg["id"]) {
|
||||
case "ping":
|
||||
if (initData.tokens.isNotEmpty) {
|
||||
logger.finest("WatchOS IPC [Phone -> Watch]: pong");
|
||||
unawaited(
|
||||
WatchSyncHelper.sendMessageToWatch({'id': 'pong'}),
|
||||
);
|
||||
_router?.go('/home');
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final ctx = navigatorKey.currentContext;
|
||||
if (ctx != null && ctx.mounted) {
|
||||
logger.info('Watch init_data: ${jsonEncode(msg)}');
|
||||
showWearBottomSheet(
|
||||
ctx,
|
||||
initData,
|
||||
Platform.isAndroid ? msg['model'] : 'Apple Watch',
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case "init_done":
|
||||
case "sync_done":
|
||||
final ctx = navigatorKey.currentContext;
|
||||
if (ctx != null && ctx.mounted) {
|
||||
ScaffoldMessenger.of(ctx).hideCurrentSnackBar();
|
||||
}
|
||||
initData.dismissWearPairingSheet?.call();
|
||||
initData.dismissWearPairingSheet = null;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
WatchSyncHelper.watchMessageStream.listen((msg) async {
|
||||
WatchSyncHelper.onWatchMessage?.call(msg);
|
||||
if (msg['id'] == 'request_sync' &&
|
||||
initDone &&
|
||||
isWearOsSupportEnabled()) {
|
||||
final ctx = navigatorKey.currentContext;
|
||||
if (ctx != null && ctx.mounted) {
|
||||
ScaffoldMessenger.of(ctx).showSnackBar(
|
||||
SnackBar(content: Text(initData.l10n.wear_syncing)),
|
||||
);
|
||||
}
|
||||
await WatchSyncHelper.runWearSyncInForeground(
|
||||
initData.client,
|
||||
);
|
||||
}
|
||||
});
|
||||
if (isWearOsSupportEnabled()) {
|
||||
unawaited(() async {
|
||||
try {
|
||||
await WatchSyncHelper.startWearSyncServiceWithFreshCache(
|
||||
initData.client,
|
||||
initData.appDir.path,
|
||||
);
|
||||
} catch (e) {
|
||||
logger.warning(
|
||||
'[Init] Failed to start Wear sync service on launch: $e',
|
||||
);
|
||||
}
|
||||
}());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (_router == null) {
|
||||
_router = createAppRouter();
|
||||
appRouter = _router;
|
||||
}
|
||||
|
||||
final themeCubit = initData.themeCubit!;
|
||||
final settingsCubit = initData.settingsCubit!;
|
||||
final profilePictureCubit = initData.profilePictureCubit!;
|
||||
final reauthCubit = initData.reauthCubit!;
|
||||
final homeRefreshCubit = initData.homeRefreshCubit!;
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider<ThemeCubit>.value(value: themeCubit),
|
||||
BlocProvider<SettingsCubit>.value(value: settingsCubit),
|
||||
BlocProvider<ProfilePictureCubit>.value(
|
||||
value: profilePictureCubit,
|
||||
),
|
||||
BlocProvider<ReauthCubit>.value(value: reauthCubit),
|
||||
BlocProvider<HomeRefreshCubit>.value(value: homeRefreshCubit),
|
||||
],
|
||||
child: MaterialApp.router(
|
||||
title: 'Firka',
|
||||
key: const ValueKey('firkaApp'),
|
||||
routerConfig: _router!,
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.lightGreen,
|
||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||
),
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
builder: (context, child) {
|
||||
return BlocBuilder<ThemeCubit, ThemeState>(
|
||||
builder: (context, themeState) {
|
||||
final isLight = themeState.isLightMode;
|
||||
final overlay = SystemUiOverlayStyle(
|
||||
statusBarColor: Colors.transparent,
|
||||
statusBarIconBrightness: isLight
|
||||
? Brightness.dark
|
||||
: Brightness.light,
|
||||
statusBarBrightness: isLight
|
||||
? Brightness.light
|
||||
: Brightness.dark,
|
||||
systemStatusBarContrastEnforced: false,
|
||||
);
|
||||
|
||||
SystemChrome.setSystemUIOverlayStyle(overlay);
|
||||
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: overlay,
|
||||
child: child ?? const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return MaterialApp(
|
||||
home: DefaultAssetBundle(
|
||||
bundle: FirkaBundle(),
|
||||
child: Scaffold(
|
||||
backgroundColor: const Color(0xFF7CA120),
|
||||
body: Container(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import 'package:kreta_api/kreta_api.dart';
|
||||
|
||||
double calculateAverage(List<Grade> sortedGrades) {
|
||||
double totalWeight = 0.0;
|
||||
double weightedSum = 0.0;
|
||||
|
||||
for (final grade in sortedGrades) {
|
||||
final value = grade.numericValue;
|
||||
final weight = grade.weightPercentage;
|
||||
|
||||
if (value != null && weight != null) {
|
||||
weightedSum += value * weight;
|
||||
totalWeight += weight;
|
||||
}
|
||||
}
|
||||
|
||||
if (totalWeight == 0) {
|
||||
return double.parse(0.0.toStringAsFixed(2));
|
||||
}
|
||||
|
||||
final avg = weightedSum / totalWeight;
|
||||
return double.parse(avg.toStringAsFixed(2));
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class HomeRefreshState {
|
||||
final int refreshTrigger;
|
||||
|
||||
const HomeRefreshState({this.refreshTrigger = 0});
|
||||
}
|
||||
|
||||
class HomeRefreshCubit extends Cubit<HomeRefreshState> {
|
||||
HomeRefreshCubit() : super(const HomeRefreshState());
|
||||
|
||||
void requestRefresh() {
|
||||
emit(HomeRefreshState(refreshTrigger: state.refreshTrigger + 1));
|
||||
}
|
||||
|
||||
void onRefreshComplete() {
|
||||
emit(HomeRefreshState(refreshTrigger: state.refreshTrigger));
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ProfilePictureState {
|
||||
final int version;
|
||||
|
||||
const ProfilePictureState({this.version = 0});
|
||||
}
|
||||
|
||||
class ProfilePictureCubit extends Cubit<ProfilePictureState> {
|
||||
ProfilePictureCubit() : super(const ProfilePictureState());
|
||||
|
||||
void notifyChanged() {
|
||||
emit(ProfilePictureState(version: state.version + 1));
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ReauthState {
|
||||
final bool needsReauth;
|
||||
|
||||
const ReauthState({this.needsReauth = false});
|
||||
}
|
||||
|
||||
class ReauthCubit extends Cubit<ReauthState> {
|
||||
ReauthCubit() : super(const ReauthState());
|
||||
|
||||
void setNeedsReauth(bool value) {
|
||||
emit(ReauthState(needsReauth: value));
|
||||
}
|
||||
|
||||
void clear() {
|
||||
emit(const ReauthState(needsReauth: false));
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class SettingsState {
|
||||
final int version;
|
||||
|
||||
const SettingsState({this.version = 0});
|
||||
}
|
||||
|
||||
class SettingsCubit extends Cubit<SettingsState> {
|
||||
SettingsCubit() : super(const SettingsState());
|
||||
|
||||
void notifyChanged() {
|
||||
emit(SettingsState(version: state.version + 1));
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ThemeState {
|
||||
final bool isLightMode;
|
||||
|
||||
const ThemeState({required this.isLightMode});
|
||||
}
|
||||
|
||||
class ThemeCubit extends Cubit<ThemeState> {
|
||||
ThemeCubit({bool initialLightMode = true})
|
||||
: super(ThemeState(isLightMode: initialLightMode));
|
||||
|
||||
void setLightMode(bool isLight) {
|
||||
emit(ThemeState(isLightMode: isLight));
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
emit(ThemeState(isLightMode: state.isLightMode));
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
abstract class FirkaState<T extends StatefulWidget> extends State<T> {}
|
||||
@@ -1,68 +0,0 @@
|
||||
import 'package:isar_community/isar.dart';
|
||||
|
||||
import 'package:firka/core/debug_helper.dart';
|
||||
import 'package:firka/data/util.dart';
|
||||
|
||||
part 'homework_cache_model.g.dart';
|
||||
|
||||
@collection
|
||||
class HomeworkCacheModel extends DatedCacheEntry {
|
||||
HomeworkCacheModel();
|
||||
}
|
||||
|
||||
Future<void> resetOldHomeworkCache(Isar isar) async {
|
||||
var now = timeNow();
|
||||
var weeks = await isar.homeworkCacheModels.where().findAll();
|
||||
var weeksToRemove = List<Id>.empty(growable: true);
|
||||
|
||||
for (var week in weeks) {
|
||||
var date = getDate(week.cacheKey!);
|
||||
|
||||
if (date.millisecondsSinceEpoch <
|
||||
now.subtract(Duration(days: 120)).millisecondsSinceEpoch) {
|
||||
weeksToRemove.add(week.cacheKey!);
|
||||
}
|
||||
}
|
||||
await isar.writeTxn(() async {
|
||||
await isar.homeworkCacheModels.deleteAll(weeksToRemove);
|
||||
});
|
||||
}
|
||||
|
||||
@collection
|
||||
class HomeworkDoneModel {
|
||||
Id? id;
|
||||
|
||||
late String homeworkId;
|
||||
late DateTime doneAt;
|
||||
|
||||
HomeworkDoneModel();
|
||||
}
|
||||
|
||||
Future<void> markAsDone(Isar isar, String homeWorkUid) async {
|
||||
await isar.writeTxn(() async {
|
||||
await isar.homeworkDoneModels.put(
|
||||
HomeworkDoneModel()
|
||||
..homeworkId = homeWorkUid
|
||||
..doneAt = DateTime.now(),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> markAsNotDone(Isar isar, String homeWorkUid) async {
|
||||
await isar.writeTxn(() async {
|
||||
final idsToDelete = await isar.homeworkDoneModels
|
||||
.filter()
|
||||
.homeworkIdEqualTo(homeWorkUid)
|
||||
.idProperty()
|
||||
.findAll();
|
||||
await isar.homeworkDoneModels.deleteAll(idsToDelete);
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> isHomeworkDone(Isar isar, String homeWorkUid) async {
|
||||
var existing = await isar.homeworkDoneModels
|
||||
.filter()
|
||||
.homeworkIdEqualTo(homeWorkUid)
|
||||
.findFirst();
|
||||
return existing != null;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:firka/data/models/token_model.dart';
|
||||
import 'db/models/token_model.dart';
|
||||
|
||||
int resolveActiveAccountIndex(dynamic settings) {
|
||||
try {
|
||||
@@ -8,7 +8,8 @@ int resolveActiveAccountIndex(dynamic settings) {
|
||||
if (accountIndex is int && accountIndex >= 0) {
|
||||
return accountIndex;
|
||||
}
|
||||
} catch (_) {}
|
||||
} catch (_) {
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -3,20 +3,28 @@ import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:firka/data/models/generic_cache_model.dart';
|
||||
import 'package:firka/data/models/timetable_cache_model.dart';
|
||||
import 'package:firka/helpers/api/model/all_lessons.dart';
|
||||
import 'package:firka/helpers/api/model/class_group.dart';
|
||||
import 'package:firka/helpers/api/model/homework.dart';
|
||||
import 'package:firka/helpers/api/model/timetable.dart';
|
||||
import 'package:firka/helpers/db/models/generic_cache_model.dart';
|
||||
import 'package:firka/helpers/db/models/timetable_cache_model.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:isar_community/isar.dart';
|
||||
import 'package:kreta_api/kreta_api.dart' hide KretaEndpoints;
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
import 'package:firka/app/app_state.dart';
|
||||
import 'package:firka/core/bloc/reauth_cubit.dart';
|
||||
import 'package:firka/data/models/token_model.dart';
|
||||
import 'package:firka/data/util.dart';
|
||||
import 'package:firka/core/debug_helper.dart';
|
||||
import 'package:firka/services/active_account_helper.dart';
|
||||
import 'package:firka/services/watch_sync_helper.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../db/models/token_model.dart';
|
||||
import '../../db/util.dart';
|
||||
import '../../debug_helper.dart';
|
||||
import '../../active_account_helper.dart';
|
||||
import '../../watch_sync_helper.dart';
|
||||
import '../consts.dart';
|
||||
import '../exceptions/token.dart';
|
||||
import '../model/grade.dart';
|
||||
import '../model/notice_board.dart';
|
||||
import '../model/omission.dart';
|
||||
import '../model/student.dart';
|
||||
import '../model/test.dart';
|
||||
import '../token_grant.dart';
|
||||
|
||||
import 'dart:io';
|
||||
@@ -29,30 +37,56 @@ const backoffCount = 4;
|
||||
const backoffMin = 100;
|
||||
const backoffStep = 500;
|
||||
|
||||
class ApiResponse<T> {
|
||||
T? response;
|
||||
int statusCode;
|
||||
String? err;
|
||||
bool cached;
|
||||
|
||||
ApiResponse(
|
||||
this.response,
|
||||
this.statusCode,
|
||||
this.err,
|
||||
this.cached,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return "ApiResponse("
|
||||
"response: $response, "
|
||||
"statusCode: $statusCode, "
|
||||
"err: \"$err\", "
|
||||
"cached: $cached"
|
||||
")";
|
||||
}
|
||||
}
|
||||
|
||||
class KretaClient {
|
||||
Completer<void>? _tokenMutexCompleter;
|
||||
TokenModel model;
|
||||
Isar isar;
|
||||
final ReauthCubit _reauthCubit;
|
||||
|
||||
KretaClient(this.model, this.isar, this._reauthCubit);
|
||||
static bool needsReauth = false;
|
||||
|
||||
bool get needsReauth => _reauthCubit.state.needsReauth;
|
||||
static final ValueNotifier<bool> reauthStateNotifier = ValueNotifier(false);
|
||||
|
||||
void clearReauthFlag() {
|
||||
_reauthCubit.clear();
|
||||
static void clearReauthFlag() {
|
||||
needsReauth = false;
|
||||
reauthStateNotifier.value = false;
|
||||
debugPrint('[KretaClient] Reauth flag cleared');
|
||||
}
|
||||
|
||||
Future<void> _setReauthFlag() async {
|
||||
static Future<void> _setReauthFlag() async {
|
||||
if (needsReauth) return;
|
||||
_reauthCubit.setNeedsReauth(true);
|
||||
needsReauth = true;
|
||||
reauthStateNotifier.value = true;
|
||||
debugPrint('[KretaClient] Reauth flag set');
|
||||
}
|
||||
|
||||
KretaClient(this.model, this.isar);
|
||||
|
||||
Future<TokenModel> _refreshModelWithCrossDeviceLease(
|
||||
TokenModel sourceToken,
|
||||
) async {
|
||||
TokenModel sourceToken) async {
|
||||
final studentIdNorm = sourceToken.studentIdNorm;
|
||||
String? leaseOperationId;
|
||||
|
||||
@@ -78,7 +112,9 @@ class KretaClient {
|
||||
final extended = await extendToken(sourceToken);
|
||||
return TokenModel.fromResp(extended);
|
||||
} finally {
|
||||
if (Platform.isIOS && studentIdNorm != null && leaseOperationId != null) {
|
||||
if (Platform.isIOS &&
|
||||
studentIdNorm != null &&
|
||||
leaseOperationId != null) {
|
||||
await WatchSyncHelper.releaseIPhoneRefreshLease(
|
||||
studentIdNorm: studentIdNorm,
|
||||
operationId: leaseOperationId,
|
||||
@@ -98,8 +134,7 @@ class KretaClient {
|
||||
final watchInstalled = await WatchSyncHelper.isWatchAppInstalled();
|
||||
if (!watchInstalled) {
|
||||
debugPrint(
|
||||
'[KretaClient] Skipping Apple token sync because no paired Watch app is installed',
|
||||
);
|
||||
'[KretaClient] Skipping Apple token sync because no paired Watch app is installed');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -152,8 +187,7 @@ class KretaClient {
|
||||
if (localExpiry != null &&
|
||||
localExpiry.isAfter(now.add(const Duration(seconds: 60)))) {
|
||||
logger.info(
|
||||
"[Recovery] Existing token is still valid, skipping recovery steps",
|
||||
);
|
||||
"[Recovery] Existing token is still valid, skipping recovery steps");
|
||||
clearReauthFlag();
|
||||
return true;
|
||||
}
|
||||
@@ -176,9 +210,8 @@ class KretaClient {
|
||||
}
|
||||
|
||||
if (!Platform.isIOS || !initDone) {
|
||||
logger.warning(
|
||||
"[Recovery] Not iOS or not initialized, cannot try iCloud",
|
||||
);
|
||||
logger
|
||||
.warning("[Recovery] Not iOS or not initialized, cannot try iCloud");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -195,14 +228,12 @@ class KretaClient {
|
||||
break;
|
||||
}
|
||||
logger.info(
|
||||
"[Recovery] Waiting ${delay}s before attempt ${attempt + 1}...",
|
||||
);
|
||||
"[Recovery] Waiting ${delay}s before attempt ${attempt + 1}...");
|
||||
await Future.delayed(Duration(seconds: delay));
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"[Recovery] iCloud attempt ${attempt + 1}/${retryDelays.length}...",
|
||||
);
|
||||
"[Recovery] iCloud attempt ${attempt + 1}/${retryDelays.length}...");
|
||||
|
||||
final recovered = await WatchSyncHelper.checkAndRecoverFromiCloud(
|
||||
isar: isar,
|
||||
@@ -214,24 +245,20 @@ class KretaClient {
|
||||
if (recovered) {
|
||||
iCloudHasToken = true;
|
||||
await _reloadActiveTokenModel(
|
||||
preferredStudentIdNorm: model.studentIdNorm,
|
||||
);
|
||||
preferredStudentIdNorm: model.studentIdNorm);
|
||||
|
||||
final recoveredExpiry = model.expiryDate;
|
||||
if (recoveredExpiry != null &&
|
||||
recoveredExpiry.isAfter(
|
||||
timeNow().add(const Duration(seconds: 60)),
|
||||
)) {
|
||||
recoveredExpiry
|
||||
.isAfter(timeNow().add(const Duration(seconds: 60)))) {
|
||||
logger.info(
|
||||
"[Recovery] Step 2 SUCCESS on attempt ${attempt + 1}: usable iCloud token applied without immediate refresh",
|
||||
);
|
||||
"[Recovery] Step 2 SUCCESS on attempt ${attempt + 1}: usable iCloud token applied without immediate refresh");
|
||||
clearReauthFlag();
|
||||
return true;
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"[Recovery] Found iCloud token close to expiry, trying refresh...",
|
||||
);
|
||||
"[Recovery] Found iCloud token close to expiry, trying refresh...");
|
||||
try {
|
||||
var tokenModel = await _refreshModelWithCrossDeviceLease(model);
|
||||
|
||||
@@ -246,14 +273,12 @@ class KretaClient {
|
||||
return true;
|
||||
} catch (e) {
|
||||
logger.warning(
|
||||
"[Recovery] iCloud token refresh failed on attempt ${attempt + 1}: $e",
|
||||
);
|
||||
"[Recovery] iCloud token refresh failed on attempt ${attempt + 1}: $e");
|
||||
iCloudHasToken = true;
|
||||
}
|
||||
} else {
|
||||
logger.info(
|
||||
"[Recovery] No fresh token in iCloud on attempt ${attempt + 1}",
|
||||
);
|
||||
"[Recovery] No fresh token in iCloud on attempt ${attempt + 1}");
|
||||
if (attempt == 0) {
|
||||
iCloudHasToken = false;
|
||||
}
|
||||
@@ -271,8 +296,7 @@ class KretaClient {
|
||||
if (model.expiryDate == null ||
|
||||
model.expiryDate!.isBefore(fiveMinutesFromNow)) {
|
||||
logger.info(
|
||||
"[Proactive] Token expired or expiring soon, starting recovery...",
|
||||
);
|
||||
"[Proactive] Token expired or expiring soon, starting recovery...");
|
||||
|
||||
final recovered = await recoverToken();
|
||||
if (recovered) {
|
||||
@@ -292,8 +316,7 @@ class KretaClient {
|
||||
}
|
||||
|
||||
logger.fine(
|
||||
"[Proactive] Token still valid until ${model.expiryDate}, no refresh needed",
|
||||
);
|
||||
"[Proactive] Token still valid until ${model.expiryDate}, no refresh needed");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -302,19 +325,15 @@ class KretaClient {
|
||||
|
||||
if (_tokenMutexCompleter != null) {
|
||||
try {
|
||||
await _tokenMutexCompleter!.future.timeout(
|
||||
maxWaitTime,
|
||||
onTimeout: () {
|
||||
logger.warning(
|
||||
"[Mutex] Timeout waiting for token mutex, forcing release",
|
||||
);
|
||||
if (_tokenMutexCompleter != null &&
|
||||
!_tokenMutexCompleter!.isCompleted) {
|
||||
_tokenMutexCompleter!.complete();
|
||||
}
|
||||
},
|
||||
);
|
||||
} catch (_) {}
|
||||
await _tokenMutexCompleter!.future.timeout(maxWaitTime, onTimeout: () {
|
||||
logger.warning(
|
||||
"[Mutex] Timeout waiting for token mutex, forcing release");
|
||||
if (_tokenMutexCompleter != null && !_tokenMutexCompleter!.isCompleted) {
|
||||
_tokenMutexCompleter!.complete();
|
||||
}
|
||||
});
|
||||
} catch (_) {
|
||||
}
|
||||
}
|
||||
|
||||
_tokenMutexCompleter = Completer<void>();
|
||||
@@ -336,8 +355,7 @@ class KretaClient {
|
||||
if (now.millisecondsSinceEpoch >=
|
||||
model.expiryDate!.millisecondsSinceEpoch) {
|
||||
logger.info(
|
||||
"Token expired at ${model.expiryDate}, starting recovery for user: ${model.studentId}",
|
||||
);
|
||||
"Token expired at ${model.expiryDate}, starting recovery for user: ${model.studentId}");
|
||||
|
||||
final recovered = await recoverToken();
|
||||
if (!recovered) {
|
||||
@@ -354,21 +372,15 @@ class KretaClient {
|
||||
"accept": "*/*",
|
||||
"user-agent": Constants.userAgent,
|
||||
"authorization": "Bearer $localToken",
|
||||
"apiKey": "21ff6c25-d1da-4a68-a811-c881a6057463",
|
||||
"apiKey": "21ff6c25-d1da-4a68-a811-c881a6057463"
|
||||
};
|
||||
|
||||
return await dio.get(
|
||||
url,
|
||||
options: Options(method: method, headers: headers),
|
||||
data: data,
|
||||
);
|
||||
return await dio.get(url,
|
||||
options: Options(method: method, headers: headers), data: data);
|
||||
}
|
||||
|
||||
Future<(dynamic, int)> _authJson(
|
||||
String method,
|
||||
String url, [
|
||||
Object? data,
|
||||
]) async {
|
||||
Future<(dynamic, int)> _authJson(String method, String url,
|
||||
[Object? data]) async {
|
||||
Response<dynamic> resp;
|
||||
|
||||
try {
|
||||
@@ -384,17 +396,13 @@ class KretaClient {
|
||||
(responseData is List && responseData.isEmpty) ||
|
||||
(responseData is Map && responseData.isEmpty)) {
|
||||
logger.warning(
|
||||
"API returned ${resp.statusCode} with empty data for: $url - possible stale session",
|
||||
);
|
||||
"API returned ${resp.statusCode} with empty data for: $url - possible stale session");
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
if (ex is Error) {
|
||||
logger.shout(
|
||||
"Request to url: $url failed",
|
||||
ex.toString(),
|
||||
ex.stackTrace,
|
||||
);
|
||||
"Request to url: $url failed", ex.toString(), ex.stackTrace);
|
||||
} else {
|
||||
logger.shout("Request to url: $url failed", ex.toString());
|
||||
}
|
||||
@@ -406,11 +414,7 @@ class KretaClient {
|
||||
}
|
||||
|
||||
Future<(dynamic, int, Object?, bool)> _cachingGet(
|
||||
CacheId id,
|
||||
String url,
|
||||
bool forceCache,
|
||||
int counter,
|
||||
) async {
|
||||
CacheId id, String url, bool forceCache, int counter) async {
|
||||
// it would be *ideal* to use xor and left shift here, however
|
||||
// binary operations seem to round the number down to
|
||||
// 32 bits for some reason???
|
||||
@@ -422,8 +426,7 @@ class KretaClient {
|
||||
try {
|
||||
if (forceCache && cache != null) {
|
||||
logger.finest(
|
||||
"_cachingGet(forceCache: $forceCache}): decoding cached response for: $url",
|
||||
);
|
||||
"_cachingGet(forceCache: $forceCache}): decoding cached response for: $url");
|
||||
return (jsonDecode(cache.cacheData!), 200, null, true);
|
||||
}
|
||||
|
||||
@@ -433,18 +436,14 @@ class KretaClient {
|
||||
if (statusCode >= 400) {
|
||||
if (cache != null) {
|
||||
logger.finest(
|
||||
"_cachingGet(forceCache: $forceCache}): decoding uncached response for: $url",
|
||||
);
|
||||
"_cachingGet(forceCache: $forceCache}): decoding uncached response for: $url");
|
||||
return (jsonDecode(cache.cacheData!), statusCode, null, true);
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
if (ex is Error) {
|
||||
logger.finest(
|
||||
"Request failed for $url",
|
||||
ex.toString(),
|
||||
ex.stackTrace,
|
||||
);
|
||||
"Request failed for $url", ex.toString(), ex.stackTrace);
|
||||
} else {
|
||||
logger.finest("Request failed for $url", ex.toString());
|
||||
}
|
||||
@@ -503,12 +502,8 @@ class KretaClient {
|
||||
} else if (studentCache != null) {
|
||||
return studentCache!;
|
||||
}
|
||||
var (resp, status, ex, cached) = await _cachingGet(
|
||||
CacheId.getStudent,
|
||||
KretaEndpoints.getStudentUrl(model.iss!),
|
||||
forceCache,
|
||||
0,
|
||||
);
|
||||
var (resp, status, ex, cached) = await _cachingGet(CacheId.getStudent,
|
||||
KretaEndpoints.getStudentUrl(model.iss!), forceCache, 0);
|
||||
|
||||
Student? student;
|
||||
String? err;
|
||||
@@ -529,20 +524,15 @@ class KretaClient {
|
||||
|
||||
ApiResponse<List<ClassGroup>>? classGroupCache;
|
||||
|
||||
Future<ApiResponse<List<ClassGroup>>> getClassGroups({
|
||||
bool forceCache = true,
|
||||
}) async {
|
||||
Future<ApiResponse<List<ClassGroup>>> getClassGroups(
|
||||
{bool forceCache = true}) async {
|
||||
if (!forceCache) {
|
||||
classGroupCache = null;
|
||||
} else {
|
||||
if (classGroupCache != null) return classGroupCache!;
|
||||
}
|
||||
var (resp, status, ex, cached) = await _cachingGet(
|
||||
CacheId.getClassGroup,
|
||||
KretaEndpoints.getClassGroups(model.iss!),
|
||||
forceCache,
|
||||
0,
|
||||
);
|
||||
var (resp, status, ex, cached) = await _cachingGet(CacheId.getClassGroup,
|
||||
KretaEndpoints.getClassGroups(model.iss!), forceCache, 0);
|
||||
|
||||
final classGroups = List<ClassGroup>.empty(growable: true);
|
||||
String? err;
|
||||
@@ -566,20 +556,15 @@ class KretaClient {
|
||||
|
||||
ApiResponse<List<NoticeBoardItem>>? noticeBoardCache;
|
||||
|
||||
Future<ApiResponse<List<NoticeBoardItem>>> getNoticeBoard({
|
||||
bool forceCache = true,
|
||||
}) async {
|
||||
Future<ApiResponse<List<NoticeBoardItem>>> getNoticeBoard(
|
||||
{bool forceCache = true}) async {
|
||||
if (!forceCache) {
|
||||
noticeBoardCache = null;
|
||||
} else if (noticeBoardCache != null) {
|
||||
return noticeBoardCache!;
|
||||
}
|
||||
var (resp, status, ex, cached) = await _cachingGet(
|
||||
CacheId.getNoticeBoard,
|
||||
KretaEndpoints.getNoticeBoard(model.iss!),
|
||||
forceCache,
|
||||
0,
|
||||
);
|
||||
var (resp, status, ex, cached) = await _cachingGet(CacheId.getNoticeBoard,
|
||||
KretaEndpoints.getNoticeBoard(model.iss!), forceCache, 0);
|
||||
|
||||
var items = List<NoticeBoardItem>.empty(growable: true);
|
||||
String? err;
|
||||
@@ -603,16 +588,11 @@ class KretaClient {
|
||||
|
||||
ApiResponse<List<InfoBoardItem>>? infoBoardCache;
|
||||
|
||||
Future<ApiResponse<List<InfoBoardItem>>> getInfoBoard({
|
||||
bool forceCache = true,
|
||||
}) async {
|
||||
Future<ApiResponse<List<InfoBoardItem>>> getInfoBoard(
|
||||
{bool forceCache = true}) async {
|
||||
if (forceCache && infoBoardCache != null) return infoBoardCache!;
|
||||
var (resp, status, ex, cached) = await _cachingGet(
|
||||
CacheId.getInfoBoard,
|
||||
KretaEndpoints.getInfoBoard(model.iss!),
|
||||
forceCache,
|
||||
0,
|
||||
);
|
||||
var (resp, status, ex, cached) = await _cachingGet(CacheId.getInfoBoard,
|
||||
KretaEndpoints.getInfoBoard(model.iss!), forceCache, 0);
|
||||
|
||||
var items = List<InfoBoardItem>.empty(growable: true);
|
||||
String? err;
|
||||
@@ -643,11 +623,7 @@ class KretaClient {
|
||||
return gradeCache!;
|
||||
}
|
||||
var (resp, status, ex, cached) = await _cachingGet(
|
||||
CacheId.getGrades,
|
||||
KretaEndpoints.getGrades(model.iss!),
|
||||
forceCache,
|
||||
0,
|
||||
);
|
||||
CacheId.getGrades, KretaEndpoints.getGrades(model.iss!), forceCache, 0);
|
||||
|
||||
var items = List<Grade>.empty(growable: true);
|
||||
String? err;
|
||||
@@ -674,19 +650,14 @@ class KretaClient {
|
||||
ApiResponse<List<SubjectAverage>>? subjectAverageCache;
|
||||
|
||||
Future<ApiResponse<List<SubjectAverage>>> getSubjectAverage(
|
||||
ClassGroup classGroup, {
|
||||
bool forceCache = true,
|
||||
}) async {
|
||||
ClassGroup classGroup,
|
||||
{bool forceCache = true}) async {
|
||||
String? err;
|
||||
if (classGroup.studyTask == null) {
|
||||
err = "classGroup.studyTask is null";
|
||||
logger.warning(err);
|
||||
return ApiResponse(
|
||||
List<SubjectAverage>.empty(growable: true),
|
||||
0,
|
||||
err,
|
||||
false,
|
||||
);
|
||||
List<SubjectAverage>.empty(growable: true), 0, err, false);
|
||||
}
|
||||
if (!forceCache) {
|
||||
subjectAverageCache = null;
|
||||
@@ -694,12 +665,8 @@ class KretaClient {
|
||||
return subjectAverageCache!;
|
||||
}
|
||||
var studyTaskUid = classGroup.studyTask!.uid.toString().split(",").first;
|
||||
var (resp, status, ex, cached) = await _cachingGet(
|
||||
CacheId.getSubjectAvg,
|
||||
KretaEndpoints.getSubjectAvg(model.iss!, studyTaskUid),
|
||||
forceCache,
|
||||
0,
|
||||
);
|
||||
var (resp, status, ex, cached) = await _cachingGet(CacheId.getSubjectAvg,
|
||||
KretaEndpoints.getSubjectAvg(model.iss!, studyTaskUid), forceCache, 0);
|
||||
|
||||
var items = List<SubjectAverage>.empty(growable: true);
|
||||
try {
|
||||
@@ -720,15 +687,14 @@ class KretaClient {
|
||||
}
|
||||
|
||||
Future<(List<dynamic>, int, Object?, bool)>
|
||||
_timedCachingGet<T extends DatedCacheEntry>(
|
||||
IsarCollection<T> cacheModel,
|
||||
String endpoint,
|
||||
DateTime from,
|
||||
DateTime? to,
|
||||
bool forceCache,
|
||||
int counter,
|
||||
Future<void> Function(dynamic, int) storeCache,
|
||||
) async {
|
||||
_timedCachingGet<T extends DatedCacheEntry>(
|
||||
IsarCollection<T> cacheModel,
|
||||
String endpoint,
|
||||
DateTime from,
|
||||
DateTime? to,
|
||||
bool forceCache,
|
||||
int counter,
|
||||
Future<void> Function(dynamic, int) storeCache) async {
|
||||
var cacheKey = genCacheKey(from, model.studentIdNorm!);
|
||||
var cache = await cacheModel.get(cacheKey);
|
||||
var formatter = DateFormat('yyyy-MM-dd');
|
||||
@@ -754,16 +720,14 @@ class KretaClient {
|
||||
try {
|
||||
if (toStr == null) {
|
||||
(resp, statusCode) = await _authJson(
|
||||
"GET",
|
||||
"$endpoint?"
|
||||
"datumTol=$fromStr",
|
||||
);
|
||||
"GET",
|
||||
"$endpoint?"
|
||||
"datumTol=$fromStr");
|
||||
} else {
|
||||
(resp, statusCode) = await _authJson(
|
||||
"GET",
|
||||
"$endpoint?"
|
||||
"datumTol=$fromStr&datumIg=$toStr",
|
||||
);
|
||||
"GET",
|
||||
"$endpoint?"
|
||||
"datumTol=$fromStr&datumIg=$toStr");
|
||||
}
|
||||
|
||||
if (statusCode >= 400) {
|
||||
@@ -783,25 +747,16 @@ class KretaClient {
|
||||
}
|
||||
|
||||
await Future.delayed(
|
||||
Duration(milliseconds: backoffMin + (counter * backoffStep)),
|
||||
);
|
||||
Duration(milliseconds: backoffMin + (counter * backoffStep)));
|
||||
|
||||
return _timedCachingGet(
|
||||
cacheModel,
|
||||
endpoint,
|
||||
from,
|
||||
to,
|
||||
forceCache,
|
||||
counter + 1,
|
||||
storeCache,
|
||||
);
|
||||
return _timedCachingGet(cacheModel, endpoint, from, to, forceCache,
|
||||
counter + 1, storeCache);
|
||||
}
|
||||
} catch (ex) {
|
||||
if (_isTokenExpired(ex)) {
|
||||
await _setReauthFlag();
|
||||
logger.warning(
|
||||
"Token expired in timed request, setting needsReauth flag",
|
||||
);
|
||||
"Token expired in timed request, setting needsReauth flag");
|
||||
}
|
||||
|
||||
if (cache != null) {
|
||||
@@ -832,36 +787,27 @@ class KretaClient {
|
||||
|
||||
/// Expects from and to to be 7 days apart
|
||||
Future<ApiResponse<List<Lesson>>> _getTimeTable(
|
||||
DateTime from,
|
||||
DateTime to,
|
||||
bool forceCache,
|
||||
) async {
|
||||
var (
|
||||
resp,
|
||||
status,
|
||||
ex,
|
||||
cached,
|
||||
) = await _timedCachingGet<TimetableCacheModel>(
|
||||
isar.timetableCacheModels,
|
||||
KretaEndpoints.getTimeTable(model.iss!),
|
||||
from,
|
||||
to,
|
||||
forceCache,
|
||||
0,
|
||||
(dynamic resp, int cacheKey) async {
|
||||
TimetableCacheModel cache = TimetableCacheModel();
|
||||
var rawClasses = List<String>.empty(growable: true);
|
||||
DateTime from, DateTime to, bool forceCache) async {
|
||||
var (resp, status, ex, cached) =
|
||||
await _timedCachingGet<TimetableCacheModel>(
|
||||
isar.timetableCacheModels,
|
||||
KretaEndpoints.getTimeTable(model.iss!),
|
||||
from,
|
||||
to,
|
||||
forceCache,
|
||||
0, (dynamic resp, int cacheKey) async {
|
||||
TimetableCacheModel cache = TimetableCacheModel();
|
||||
var rawClasses = List<String>.empty(growable: true);
|
||||
|
||||
for (var obj in resp) {
|
||||
rawClasses.add(jsonEncode(obj));
|
||||
}
|
||||
for (var obj in resp) {
|
||||
rawClasses.add(jsonEncode(obj));
|
||||
}
|
||||
|
||||
cache.cacheKey = cacheKey;
|
||||
cache.values = rawClasses;
|
||||
cache.cacheKey = cacheKey;
|
||||
cache.values = rawClasses;
|
||||
|
||||
await isar.timetableCacheModels.put(cache as dynamic);
|
||||
},
|
||||
);
|
||||
await isar.timetableCacheModels.put(cache as dynamic);
|
||||
});
|
||||
|
||||
var items = List<Lesson>.empty(growable: true);
|
||||
String? err;
|
||||
@@ -881,18 +827,16 @@ class KretaClient {
|
||||
return ApiResponse(items, status, err, cached);
|
||||
}
|
||||
|
||||
Future<ApiResponse<List<Homework>>> getHomework({
|
||||
bool forceCache = true,
|
||||
}) async {
|
||||
Future<ApiResponse<List<Homework>>> getHomework(
|
||||
{bool forceCache = true}) async {
|
||||
final now = timeNow().subtract(Duration(days: 365));
|
||||
var formatter = DateFormat('yyyy-MM-dd');
|
||||
var start = formatter.format(now);
|
||||
var (resp, status, ex, cached) = await _cachingGet(
|
||||
CacheId.getHomework,
|
||||
"${KretaEndpoints.getHomework(model.iss!)}?datumTol=$start",
|
||||
forceCache,
|
||||
0,
|
||||
);
|
||||
CacheId.getHomework,
|
||||
"${KretaEndpoints.getHomework(model.iss!)}?datumTol=$start",
|
||||
forceCache,
|
||||
0);
|
||||
|
||||
var items = List<Homework>.empty(growable: true);
|
||||
String? err;
|
||||
@@ -915,20 +859,15 @@ class KretaClient {
|
||||
}
|
||||
|
||||
/// Automatically aligns requests to start at Monday and end at Sunday
|
||||
Future<ApiResponse<List<Lesson>>> getTimeTable(
|
||||
DateTime from,
|
||||
DateTime to, {
|
||||
bool forceCache = true,
|
||||
}) async {
|
||||
Future<ApiResponse<List<Lesson>>> getTimeTable(DateTime from, DateTime to,
|
||||
{bool forceCache = true}) async {
|
||||
var lessons = List<Lesson>.empty(growable: true);
|
||||
String? err;
|
||||
bool cached = true;
|
||||
|
||||
for (
|
||||
var i = from.millisecondsSinceEpoch;
|
||||
i < to.millisecondsSinceEpoch;
|
||||
i += 604800000
|
||||
) {
|
||||
for (var i = from.millisecondsSinceEpoch;
|
||||
i < to.millisecondsSinceEpoch;
|
||||
i += 604800000) {
|
||||
var from = DateTime.fromMillisecondsSinceEpoch(i);
|
||||
var start = from.subtract(Duration(days: from.weekday - 1));
|
||||
var end = start.add(Duration(days: 6));
|
||||
@@ -950,16 +889,14 @@ class KretaClient {
|
||||
lessons.sort((a, b) => a.start.compareTo(b.start));
|
||||
lessons = lessons
|
||||
.where(
|
||||
(lesson) => lesson.start.isAfter(from) && lesson.end.isBefore(to),
|
||||
)
|
||||
(lesson) => lesson.start.isAfter(from) && lesson.end.isBefore(to))
|
||||
.toList();
|
||||
|
||||
return ApiResponse(lessons, 200, err, cached);
|
||||
}
|
||||
|
||||
Future<ApiResponse<List<AllLessons>>> getLessons({
|
||||
bool forceCache = true,
|
||||
}) async {
|
||||
Future<ApiResponse<List<AllLessons>>> getLessons(
|
||||
{bool forceCache = true}) async {
|
||||
var (resp, status, ex, cached) = await _cachingGet(
|
||||
CacheId.getLessons,
|
||||
KretaEndpoints.getLessons(model.iss!),
|
||||
@@ -996,11 +933,7 @@ class KretaClient {
|
||||
|
||||
Future<ApiResponse<List<Test>>> getTests({bool forceCache = true}) async {
|
||||
var (resp, status, ex, cached) = await _cachingGet(
|
||||
CacheId.getTests,
|
||||
KretaEndpoints.getTests(model.iss!),
|
||||
forceCache,
|
||||
0,
|
||||
);
|
||||
CacheId.getTests, KretaEndpoints.getTests(model.iss!), forceCache, 0);
|
||||
|
||||
var items = List<Test>.empty(growable: true);
|
||||
String? err;
|
||||
@@ -1024,20 +957,15 @@ class KretaClient {
|
||||
|
||||
ApiResponse<List<Omission>>? omissionsCache;
|
||||
|
||||
Future<ApiResponse<List<Omission>>> getOmissions({
|
||||
bool forceCache = true,
|
||||
}) async {
|
||||
Future<ApiResponse<List<Omission>>> getOmissions(
|
||||
{bool forceCache = true}) async {
|
||||
if (!forceCache) {
|
||||
omissionsCache = null;
|
||||
} else {
|
||||
if (omissionsCache != null) return omissionsCache!;
|
||||
}
|
||||
var (resp, status, ex, cached) = await _cachingGet(
|
||||
CacheId.getOmissions,
|
||||
KretaEndpoints.getOmissions(model.iss!),
|
||||
forceCache,
|
||||
0,
|
||||
);
|
||||
var (resp, status, ex, cached) = await _cachingGet(CacheId.getOmissions,
|
||||
KretaEndpoints.getOmissions(model.iss!), forceCache, 0);
|
||||
|
||||
var items = List<Omission>.empty(growable: true);
|
||||
String? err;
|
||||
@@ -1,5 +1,12 @@
|
||||
import 'package:kreta_api/kreta_api.dart';
|
||||
import 'package:firka/helpers/api/model/class_group.dart';
|
||||
import 'package:firka/helpers/api/model/homework.dart';
|
||||
import 'package:firka/helpers/api/model/notice_board.dart';
|
||||
import 'package:firka/helpers/api/model/omission.dart';
|
||||
import 'package:firka/helpers/api/model/test.dart';
|
||||
import 'package:firka/helpers/api/model/timetable.dart';
|
||||
|
||||
import '../model/grade.dart';
|
||||
import '../model/student.dart';
|
||||
import 'kreta_client.dart';
|
||||
|
||||
bool getStudentFL = false;
|
||||
@@ -14,9 +21,8 @@ bool getTestsStreamFL = false;
|
||||
bool getOmissionsStreamFL = false;
|
||||
|
||||
extension KretaStream on KretaClient {
|
||||
Stream<ApiResponse<Student>> getStudentStream({
|
||||
bool cacheOnly = true,
|
||||
}) async* {
|
||||
Stream<ApiResponse<Student>> getStudentStream(
|
||||
{bool cacheOnly = true}) async* {
|
||||
while (getStudentFL) {
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
}
|
||||
@@ -29,9 +35,8 @@ extension KretaStream on KretaClient {
|
||||
getStudentFL = false;
|
||||
}
|
||||
|
||||
Stream<ApiResponse<List<ClassGroup>>> getClassGroupsStream({
|
||||
bool cacheOnly = true,
|
||||
}) async* {
|
||||
Stream<ApiResponse<List<ClassGroup>>> getClassGroupsStream(
|
||||
{bool cacheOnly = true}) async* {
|
||||
while (getClassGroupsFL) {
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
}
|
||||
@@ -44,9 +49,8 @@ extension KretaStream on KretaClient {
|
||||
getClassGroupsFL = false;
|
||||
}
|
||||
|
||||
Stream<ApiResponse<List<NoticeBoardItem>>> getNoticeBoardStream({
|
||||
bool cacheOnly = true,
|
||||
}) async* {
|
||||
Stream<ApiResponse<List<NoticeBoardItem>>> getNoticeBoardStream(
|
||||
{bool cacheOnly = true}) async* {
|
||||
while (getNoticeBoardStreamFL) {
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
}
|
||||
@@ -59,9 +63,8 @@ extension KretaStream on KretaClient {
|
||||
getNoticeBoardStreamFL = false;
|
||||
}
|
||||
|
||||
Stream<ApiResponse<List<InfoBoardItem>>> getInfoBoardStream({
|
||||
bool cacheOnly = true,
|
||||
}) async* {
|
||||
Stream<ApiResponse<List<InfoBoardItem>>> getInfoBoardStream(
|
||||
{bool cacheOnly = true}) async* {
|
||||
while (getInfoBoardStreamFL) {
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
}
|
||||
@@ -74,9 +77,8 @@ extension KretaStream on KretaClient {
|
||||
getInfoBoardStreamFL = false;
|
||||
}
|
||||
|
||||
Stream<ApiResponse<List<Grade>>> getGradesStream({
|
||||
bool cacheOnly = true,
|
||||
}) async* {
|
||||
Stream<ApiResponse<List<Grade>>> getGradesStream(
|
||||
{bool cacheOnly = true}) async* {
|
||||
while (getGradesStreamFL) {
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
}
|
||||
@@ -90,9 +92,8 @@ extension KretaStream on KretaClient {
|
||||
}
|
||||
|
||||
Stream<ApiResponse<List<SubjectAverage>>> getSubjectAverageStream(
|
||||
ClassGroup classGroup, {
|
||||
bool cacheOnly = true,
|
||||
}) async* {
|
||||
ClassGroup classGroup,
|
||||
{bool cacheOnly = true}) async* {
|
||||
while (getSubjectAverageStreamFL) {
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
}
|
||||
@@ -105,9 +106,8 @@ extension KretaStream on KretaClient {
|
||||
getSubjectAverageStreamFL = false;
|
||||
}
|
||||
|
||||
Stream<ApiResponse<List<Homework>>> getHomeworkStream({
|
||||
bool cacheOnly = true,
|
||||
}) async* {
|
||||
Stream<ApiResponse<List<Homework>>> getHomeworkStream(
|
||||
{bool cacheOnly = true}) async* {
|
||||
while (getHomeworkStreamFL) {
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
}
|
||||
@@ -121,10 +121,8 @@ extension KretaStream on KretaClient {
|
||||
}
|
||||
|
||||
Stream<ApiResponse<List<Lesson>>> getTimeTableStream(
|
||||
DateTime from,
|
||||
DateTime to, {
|
||||
bool cacheOnly = true,
|
||||
}) async* {
|
||||
DateTime from, DateTime to,
|
||||
{bool cacheOnly = true}) async* {
|
||||
while (getTimeTableStreamFL) {
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
}
|
||||
@@ -137,9 +135,8 @@ extension KretaStream on KretaClient {
|
||||
getTimeTableStreamFL = false;
|
||||
}
|
||||
|
||||
Stream<ApiResponse<List<Test>>> getTestsStream({
|
||||
bool cacheOnly = true,
|
||||
}) async* {
|
||||
Stream<ApiResponse<List<Test>>> getTestsStream(
|
||||
{bool cacheOnly = true}) async* {
|
||||
while (getTestsStreamFL) {
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
}
|
||||
@@ -152,9 +149,8 @@ extension KretaStream on KretaClient {
|
||||
getTestsStreamFL = false;
|
||||
}
|
||||
|
||||
Stream<ApiResponse<List<Omission>>> getOmissionsStream({
|
||||
bool cacheOnly = true,
|
||||
}) async* {
|
||||
Stream<ApiResponse<List<Omission>>> getOmissionsStream(
|
||||
{bool cacheOnly = true}) async* {
|
||||
while (getOmissionsStreamFL) {
|
||||
await Future.delayed(Duration(milliseconds: 10));
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:kreta_api/kreta_api.dart';
|
||||
import 'package:firka/helpers/api/model/timetable.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
|
||||
@@ -55,8 +55,7 @@ class LiveActivityBackendClient {
|
||||
'roomName': lesson.roomName,
|
||||
'isSubstitution': lesson.substituteTeacher != null,
|
||||
'substituteTeacher': lesson.substituteTeacher,
|
||||
'isCancelled':
|
||||
lesson.state.name?.toLowerCase().contains('elmarad') ?? false,
|
||||
'isCancelled': lesson.state.name?.toLowerCase().contains('elmarad') ?? false,
|
||||
'lastModified': validLastModified.toIso8601String(),
|
||||
};
|
||||
}).toList();
|
||||
@@ -87,9 +86,7 @@ class LiveActivityBackendClient {
|
||||
requestData['liveActivityEnabled'] = liveActivityEnabled;
|
||||
}
|
||||
|
||||
_logger.info(
|
||||
'Registering device with backend. Sending ${lessonsData.length} lessons.',
|
||||
);
|
||||
_logger.info('Registering device with backend. Sending ${lessonsData.length} lessons.');
|
||||
if (_logger.isLoggable(Level.FINE)) {
|
||||
for (var lesson in lessonsData) {
|
||||
_logger.fine(' Lesson data: $lesson');
|
||||
@@ -102,9 +99,7 @@ class LiveActivityBackendClient {
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||
_logger.info(
|
||||
'Device registered successfully with ${timetable.length} lessons',
|
||||
);
|
||||
_logger.info('Device registered successfully with ${timetable.length} lessons');
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -141,15 +136,12 @@ class LiveActivityBackendClient {
|
||||
'roomName': lesson.roomName,
|
||||
'isSubstitution': lesson.substituteTeacher != null,
|
||||
'substituteTeacher': lesson.substituteTeacher,
|
||||
'isCancelled':
|
||||
lesson.state.name?.toLowerCase().contains('elmarad') ?? false,
|
||||
'isCancelled': lesson.state.name?.toLowerCase().contains('elmarad') ?? false,
|
||||
'lastModified': validLastModified.toIso8601String(),
|
||||
};
|
||||
}).toList();
|
||||
|
||||
_logger.info(
|
||||
'Updating timetable with backend. Sending ${lessonsData.length} lessons.',
|
||||
);
|
||||
_logger.info('Updating timetable with backend. Sending ${lessonsData.length} lessons.');
|
||||
if (_logger.isLoggable(Level.FINE)) {
|
||||
for (var lesson in lessonsData) {
|
||||
_logger.fine(' Lesson data: $lesson');
|
||||
@@ -185,11 +177,15 @@ class LiveActivityBackendClient {
|
||||
}
|
||||
|
||||
/// Unregister device (called when user logs out)
|
||||
Future<bool> unregisterDevice({required String deviceToken}) async {
|
||||
Future<bool> unregisterDevice({
|
||||
required String deviceToken,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.delete(
|
||||
'/live-activity/unregister',
|
||||
data: {'deviceToken': deviceToken},
|
||||
data: {
|
||||
'deviceToken': deviceToken,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
@@ -232,11 +228,15 @@ class LiveActivityBackendClient {
|
||||
}
|
||||
|
||||
/// Get current timetable from backend
|
||||
Future<List<Lesson>?> getTimetable({required String deviceToken}) async {
|
||||
Future<List<Lesson>?> getTimetable({
|
||||
required String deviceToken,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.get(
|
||||
'/live-activity/timetable',
|
||||
queryParameters: {'deviceToken': deviceToken},
|
||||
queryParameters: {
|
||||
'deviceToken': deviceToken,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 && response.data is Map) {
|
||||
@@ -261,7 +261,10 @@ class LiveActivityBackendClient {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
'/live-activity/push-token',
|
||||
data: {'deviceToken': deviceToken, 'pushToken': pushToken},
|
||||
data: {
|
||||
'deviceToken': deviceToken,
|
||||
'pushToken': pushToken,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
@@ -285,7 +288,10 @@ class LiveActivityBackendClient {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
'/live-activity/apns-token',
|
||||
data: {'deviceToken': deviceToken, 'apnsPushToken': apnsPushToken},
|
||||
data: {
|
||||
'deviceToken': deviceToken,
|
||||
'apnsPushToken': apnsPushToken,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
@@ -293,9 +299,7 @@ class LiveActivityBackendClient {
|
||||
return true;
|
||||
}
|
||||
|
||||
_logger.warning(
|
||||
'Failed to update APNs push token: ${response.statusCode}',
|
||||
);
|
||||
_logger.warning('Failed to update APNs push token: ${response.statusCode}');
|
||||
return false;
|
||||
} catch (e) {
|
||||
_logger.severe('Error updating APNs push token: $e');
|
||||
@@ -304,11 +308,15 @@ class LiveActivityBackendClient {
|
||||
}
|
||||
|
||||
/// Send a test notification (for debugging)
|
||||
Future<bool> sendTestNotification({required String deviceToken}) async {
|
||||
Future<bool> sendTestNotification({
|
||||
required String deviceToken,
|
||||
}) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
'/live-activity/test-notification',
|
||||
data: {'deviceToken': deviceToken},
|
||||
data: {
|
||||
'deviceToken': deviceToken,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
@@ -316,9 +324,7 @@ class LiveActivityBackendClient {
|
||||
return true;
|
||||
}
|
||||
|
||||
_logger.warning(
|
||||
'Failed to send test notification: ${response.statusCode}',
|
||||
);
|
||||
_logger.warning('Failed to send test notification: ${response.statusCode}');
|
||||
return false;
|
||||
} catch (e) {
|
||||
_logger.severe('Error sending test notification: $e');
|
||||
@@ -334,7 +340,10 @@ class LiveActivityBackendClient {
|
||||
try {
|
||||
final response = await _dio.put(
|
||||
'/live-activity/language',
|
||||
data: {'deviceToken': deviceToken, 'language': language},
|
||||
data: {
|
||||
'deviceToken': deviceToken,
|
||||
'language': language,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
@@ -358,7 +367,10 @@ class LiveActivityBackendClient {
|
||||
try {
|
||||
final response = await _dio.put(
|
||||
'/live-activity/bell-delay',
|
||||
data: {'deviceToken': deviceToken, 'bellDelay': bellDelay},
|
||||
data: {
|
||||
'deviceToken': deviceToken,
|
||||
'bellDelay': bellDelay,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
@@ -385,25 +397,17 @@ class LiveActivityBackendClient {
|
||||
'/live-activity/morning-notification',
|
||||
data: {
|
||||
'deviceToken': deviceToken,
|
||||
...?(morningNotificationTime != null
|
||||
? {'morningNotificationTime': morningNotificationTime}
|
||||
: null),
|
||||
...?(morningNotificationEnabled != null
|
||||
? {'morningNotificationEnabled': morningNotificationEnabled}
|
||||
: null),
|
||||
if (morningNotificationTime != null) 'morningNotificationTime': morningNotificationTime,
|
||||
if (morningNotificationEnabled != null) 'morningNotificationEnabled': morningNotificationEnabled,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
_logger.info(
|
||||
'Morning notification settings updated successfully: enabled=$morningNotificationEnabled, time=$morningNotificationTime',
|
||||
);
|
||||
_logger.info('Morning notification settings updated successfully: enabled=$morningNotificationEnabled, time=$morningNotificationTime');
|
||||
return true;
|
||||
}
|
||||
|
||||
_logger.warning(
|
||||
'Failed to update morning notification settings: ${response.statusCode}',
|
||||
);
|
||||
_logger.warning('Failed to update morning notification settings: ${response.statusCode}');
|
||||
return false;
|
||||
} catch (e) {
|
||||
_logger.severe('Error updating morning notification settings: $e');
|
||||
@@ -426,9 +430,7 @@ class LiveActivityBackendClient {
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
_logger.info(
|
||||
'Live Activity ${liveActivityEnabled ? "enabled" : "disabled"} successfully',
|
||||
);
|
||||
_logger.info('Live Activity ${liveActivityEnabled ? "enabled" : "disabled"} successfully');
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -440,3 +442,4 @@ class LiveActivityBackendClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,7 @@ import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:firka/app/app_state.dart';
|
||||
import 'package:kreta_api/kreta_api.dart' as ka;
|
||||
import 'package:firka/main.dart';
|
||||
|
||||
class Constants {
|
||||
static String get clientId {
|
||||
@@ -42,6 +41,8 @@ class TimetableConsts {
|
||||
}
|
||||
|
||||
class KretaEndpoints {
|
||||
static String kretaBase = "e-kreta.hu";
|
||||
|
||||
static String _generateCodeVerifier() {
|
||||
var random = Random.secure();
|
||||
final bytes = List<int>.generate(32, (i) => random.nextInt(256));
|
||||
@@ -63,7 +64,13 @@ class KretaEndpoints {
|
||||
return base64Url.encode(bytes).replaceAll('=', '');
|
||||
}
|
||||
|
||||
static String kreta(String iss) => ka.KretaEndpoints.kreta(iss);
|
||||
static String kreta(String iss) {
|
||||
if (iss == "firka-test") {
|
||||
return kretaBase;
|
||||
} else {
|
||||
return "https://$iss.$kretaBase";
|
||||
}
|
||||
}
|
||||
|
||||
static final String codeVerifier = _generateCodeVerifier();
|
||||
static final String _codeChallenge = _generateCodeChallenge(codeVerifier);
|
||||
@@ -79,28 +86,38 @@ class KretaEndpoints {
|
||||
static String tokenGrantUrl = "$kretaIdp/connect/token";
|
||||
|
||||
static String getStudentUrl(String iss) =>
|
||||
ka.KretaEndpoints.getStudentUrl(iss);
|
||||
"${kreta(iss)}/ellenorzo/v3/sajat/TanuloAdatlap";
|
||||
|
||||
static String getClassGroups(String iss) =>
|
||||
ka.KretaEndpoints.getClassGroups(iss);
|
||||
"${kreta(iss)}/ellenorzo/v3/sajat/OsztalyCsoportok";
|
||||
|
||||
static String getNoticeBoard(String iss) =>
|
||||
ka.KretaEndpoints.getNoticeBoard(iss);
|
||||
"${kreta(iss)}/ellenorzo/v3/sajat/FaliujsagElemek";
|
||||
|
||||
static String getInfoBoard(String iss) => ka.KretaEndpoints.getInfoBoard(iss);
|
||||
// for some reason the [redacted] devs decided to make
|
||||
// two different apis to get items for the notice board
|
||||
// that appears on the home screen, like wtf
|
||||
static String getInfoBoard(String iss) =>
|
||||
"${kreta(iss)}/ellenorzo/v3/sajat/Feljegyzesek";
|
||||
|
||||
static String getGrades(String iss) => ka.KretaEndpoints.getGrades(iss);
|
||||
static String getGrades(String iss) =>
|
||||
"${kreta(iss)}/ellenorzo/v3/sajat/Ertekelesek";
|
||||
|
||||
static String getSubjectAvg(String iss, String studyGroupId) =>
|
||||
ka.KretaEndpoints.getSubjectAvg(iss, studyGroupId);
|
||||
"${kreta(iss)}/ellenorzo/v3/sajat/Ertekelesek/Atlagok/TantargyiAtlagok?oktatasiNevelesiFeladatUid=$studyGroupId&oktatasiNevelesiFeladatUid=$studyGroupId";
|
||||
|
||||
static String getTimeTable(String iss) => ka.KretaEndpoints.getTimeTable(iss);
|
||||
static String getTimeTable(String iss) =>
|
||||
"${kreta(iss)}/ellenorzo/v3/sajat/OrarendElemek";
|
||||
|
||||
static String getOmissions(String iss) => ka.KretaEndpoints.getOmissions(iss);
|
||||
static String getOmissions(String iss) =>
|
||||
"${kreta(iss)}/ellenorzo/v3/sajat/Mulasztasok";
|
||||
|
||||
static String getHomework(String iss) => ka.KretaEndpoints.getHomework(iss);
|
||||
static String getHomework(String iss) =>
|
||||
"${kreta(iss)}/ellenorzo/v3/sajat/HaziFeladatok";
|
||||
|
||||
static String getTests(String iss) => ka.KretaEndpoints.getTests(iss);
|
||||
|
||||
static String getLessons(String iss) => ka.KretaEndpoints.getLessons(iss);
|
||||
}
|
||||
static String getTests(String iss) =>
|
||||
"${kreta(iss)}/ellenorzo/v3/sajat/BejelentettSzamonkeresek";
|
||||
|
||||
static String getLessons(String iss) =>
|
||||
"${kreta(iss)}/dktapi/intezmenyek/munkaterek/tanulok";
|
||||
}
|
||||
132
firka/lib/helpers/api/model/all_lessons.dart
Normal file
@@ -0,0 +1,132 @@
|
||||
import 'dart:convert';
|
||||
|
||||
class AllLessons {
|
||||
final String schoolId;
|
||||
final String yearId;
|
||||
final dynamic classId;
|
||||
final String? className;
|
||||
final bool classWorkspace;
|
||||
final dynamic groupId;
|
||||
final String? groupName;
|
||||
final bool groupWorkspace;
|
||||
final String groupWorkspaceName;
|
||||
final dynamic subjectId;
|
||||
final String subjectName;
|
||||
final dynamic teacherId;
|
||||
final String teacherGuid;
|
||||
final String teacherName;
|
||||
final dynamic teacherAnnoId;
|
||||
final dynamic annoId;
|
||||
final String? languageId;
|
||||
final dynamic subjectCategoryId;
|
||||
final String subjectCategoryName;
|
||||
final dynamic typeId;
|
||||
final String typeName;
|
||||
final dynamic gradeTypeId;
|
||||
final String gradeTypeName;
|
||||
final dynamic taskPlaceId;
|
||||
final String taskPlaceName;
|
||||
final dynamic teacherAvatarTypeId;
|
||||
final String teacherAvatarTypePath;
|
||||
final dynamic taskGroupId;
|
||||
|
||||
AllLessons({
|
||||
required this.schoolId,
|
||||
required this.yearId,
|
||||
this.classId,
|
||||
this.className,
|
||||
required this.classWorkspace,
|
||||
this.groupId,
|
||||
this.groupName,
|
||||
required this.groupWorkspace,
|
||||
required this.groupWorkspaceName,
|
||||
required this.subjectId,
|
||||
required this.subjectName,
|
||||
required this.teacherId,
|
||||
required this.teacherGuid,
|
||||
required this.teacherName,
|
||||
this.teacherAnnoId,
|
||||
this.annoId,
|
||||
this.languageId,
|
||||
required this.subjectCategoryId,
|
||||
required this.subjectCategoryName,
|
||||
required this.typeId,
|
||||
required this.typeName,
|
||||
required this.gradeTypeId,
|
||||
required this.gradeTypeName,
|
||||
required this.taskPlaceId,
|
||||
required this.taskPlaceName,
|
||||
required this.teacherAvatarTypeId,
|
||||
required this.teacherAvatarTypePath,
|
||||
this.taskGroupId,
|
||||
});
|
||||
|
||||
factory AllLessons.fromJson(Map<String, dynamic> json) => AllLessons(
|
||||
schoolId: json['intezmenyId']?.toString() ?? '',
|
||||
yearId: json['tanevId']?.toString() ?? '',
|
||||
classId: json['osztalyId'],
|
||||
className: json['osztalyNev']?.toString(),
|
||||
classWorkspace: json['osztalyMunkaTer'] == true,
|
||||
groupId: json['csoportId'],
|
||||
groupName: json['csoportNev']?.toString(),
|
||||
groupWorkspace: json['csoportMunkaTer'] == true,
|
||||
groupWorkspaceName: json['osztalyCsoportNev']?.toString() ?? '',
|
||||
subjectId: json['tantargyId'],
|
||||
subjectName: json['tantargyNev']?.toString() ?? '',
|
||||
teacherId: json['alkalmazottId'],
|
||||
teacherGuid: json['alkalmazottGuid']?.toString() ?? '',
|
||||
teacherName: json['alkalmazottNev']?.toString() ?? '',
|
||||
teacherAnnoId: json['alkalmazottUzenoFalId'],
|
||||
annoId: json['uzenoFalId'],
|
||||
languageId: json['nyelvId']?.toString(),
|
||||
subjectCategoryId: json['tantargyKategoriaId'],
|
||||
subjectCategoryName: json['tantargyKategoriaNev']?.toString() ?? '',
|
||||
typeId: json['tipusId'],
|
||||
typeName: json['tipusNev']?.toString() ?? '',
|
||||
gradeTypeId: json['evfolyamTipusId'],
|
||||
gradeTypeName: json['evfolyamTipusNev']?.toString() ?? '',
|
||||
taskPlaceId: json['feladatEllatasiHelyId'],
|
||||
taskPlaceName: json['feladatEllatasiHelyNev']?.toString() ?? '',
|
||||
teacherAvatarTypeId: json['alkalmazottAvatarTipusId'],
|
||||
teacherAvatarTypePath:
|
||||
json['alkalmazottAvatarEleres']?.toString() ?? '',
|
||||
taskGroupId: json['oraiFeladatGroupId'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'intezmenyId': schoolId,
|
||||
'tanevId': yearId,
|
||||
'osztalyId': classId,
|
||||
'osztalyNev': className,
|
||||
'osztalyMunkaTer': classWorkspace,
|
||||
'csoportId': groupId,
|
||||
'csoportNev': groupName,
|
||||
'csoportMunkaTer': groupWorkspace,
|
||||
'osztalyCsoportNev': groupWorkspaceName,
|
||||
'tantargyId': subjectId,
|
||||
'tantargyNev': subjectName,
|
||||
'alkalmazottId': teacherId,
|
||||
'alkalmazottGuid': teacherGuid,
|
||||
'alkalmazottNev': teacherName,
|
||||
'alkalmazottUzenoFalId': teacherAnnoId,
|
||||
'uzenoFalId': annoId,
|
||||
'nyelvId': languageId,
|
||||
'tantargyKategoriaId': subjectCategoryId,
|
||||
'tantargyKategoriaNev': subjectCategoryName,
|
||||
'tipusId': typeId,
|
||||
'tipusNev': typeName,
|
||||
'evfolyamTipusId': gradeTypeId,
|
||||
'evfolyamTipusNev': gradeTypeName,
|
||||
'feladatEllatasiHelyId': taskPlaceId,
|
||||
'feladatEllatasiHelyNev': taskPlaceName,
|
||||
'alkalmazottAvatarTipusId': teacherAvatarTypeId,
|
||||
'alkalmazottAvatarEleres': teacherAvatarTypePath,
|
||||
'oraiFeladatGroupId': taskGroupId,
|
||||
};
|
||||
}
|
||||
|
||||
List<AllLessons> lessonsFromJson(String str) =>
|
||||
List<AllLessons>.from(json.decode(str).map((x) => AllLessons.fromJson(x)));
|
||||
|
||||
String lessonsToJson(List<AllLessons> data) =>
|
||||
json.encode(List<dynamic>.from(data.map((x) => x.toJson())));
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'generic.dart';
|
||||
import 'package:firka/helpers/api/model/generic.dart';
|
||||
|
||||
class ClassGroup {
|
||||
final String uid;
|
||||
@@ -11,36 +11,34 @@ class ClassGroup {
|
||||
final bool isActive;
|
||||
final String type;
|
||||
|
||||
ClassGroup({
|
||||
required this.uid,
|
||||
required this.name,
|
||||
required this.headTeacher,
|
||||
required this.substituteHeadTeacher,
|
||||
required this.studyGroup,
|
||||
required this.studyGroupSortIndex,
|
||||
required this.studyTask,
|
||||
required this.isActive,
|
||||
required this.type,
|
||||
});
|
||||
ClassGroup(
|
||||
{required this.uid,
|
||||
required this.name,
|
||||
required this.headTeacher,
|
||||
required this.substituteHeadTeacher,
|
||||
required this.studyGroup,
|
||||
required this.studyGroupSortIndex,
|
||||
required this.studyTask,
|
||||
required this.isActive,
|
||||
required this.type});
|
||||
|
||||
factory ClassGroup.fromJson(Map<String, dynamic> json) {
|
||||
return ClassGroup(
|
||||
uid: json['Uid'],
|
||||
name: json['Nev'],
|
||||
headTeacher: json['OsztalyFonok'] != null
|
||||
? UidObj.fromJson(json['OsztalyFonok'])
|
||||
: null,
|
||||
substituteHeadTeacher: json['OsztalyFonokHelyettes'] != null
|
||||
? UidObj.fromJson(json['OsztalyFonokHelyettes'])
|
||||
: null,
|
||||
studyGroup: NameUidDesc.fromJson(json['OktatasNevelesiKategoria']),
|
||||
studyGroupSortIndex: json['OktatasNevelesiKategoriaSortIndex'],
|
||||
studyTask: json['OktatasNevelesiFeladat'] != null
|
||||
? NameUidDesc.fromJson(json['OktatasNevelesiFeladat'])
|
||||
: null,
|
||||
isActive: json['IsAktiv'],
|
||||
type: json['Tipus'],
|
||||
);
|
||||
uid: json['Uid'],
|
||||
name: json['Nev'],
|
||||
headTeacher: json['OsztalyFonok'] != null
|
||||
? UidObj.fromJson(json['OsztalyFonok'])
|
||||
: null,
|
||||
substituteHeadTeacher: json['OsztalyFonokHelyettes'] != null
|
||||
? UidObj.fromJson(json['OsztalyFonokHelyettes'])
|
||||
: null,
|
||||
studyGroup: NameUidDesc.fromJson(json['OktatasNevelesiKategoria']),
|
||||
studyGroupSortIndex: json['OktatasNevelesiKategoriaSortIndex'],
|
||||
studyTask: json['OktatasNevelesiFeladat'] != null
|
||||
? NameUidDesc.fromJson(json['OktatasNevelesiFeladat'])
|
||||
: null,
|
||||
isActive: json['IsAktiv'],
|
||||
type: json['Tipus']);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3,18 +3,12 @@ class NameUidDesc {
|
||||
final String? name;
|
||||
final String? description;
|
||||
|
||||
NameUidDesc({
|
||||
required this.uid,
|
||||
required this.name,
|
||||
required this.description,
|
||||
});
|
||||
NameUidDesc(
|
||||
{required this.uid, required this.name, required this.description});
|
||||
|
||||
factory NameUidDesc.fromJson(Map<String, dynamic> json) {
|
||||
return NameUidDesc(
|
||||
uid: json['Uid'],
|
||||
name: json['Nev'],
|
||||
description: json['Leiras'],
|
||||
);
|
||||
uid: json['Uid'], name: json['Nev'], description: json['Leiras']);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
@@ -35,14 +29,23 @@ class NameUid {
|
||||
final String uid;
|
||||
final String name;
|
||||
|
||||
NameUid({required this.uid, required this.name});
|
||||
NameUid({
|
||||
required this.uid,
|
||||
required this.name,
|
||||
});
|
||||
|
||||
factory NameUid.fromJson(Map<String, dynamic> json) {
|
||||
return NameUid(uid: json['Uid'], name: json['Nev']);
|
||||
return NameUid(
|
||||
uid: json['Uid'],
|
||||
name: json['Nev'],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {'Uid': uid, 'Nev': name};
|
||||
return {
|
||||
'Uid': uid,
|
||||
'Nev': name,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +55,9 @@ class UidObj {
|
||||
UidObj({required this.uid});
|
||||
|
||||
factory UidObj.fromJson(Map<String, dynamic> json) {
|
||||
return UidObj(uid: json['Uid']);
|
||||
return UidObj(
|
||||
uid: json['Uid'],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'generic.dart';
|
||||
import 'subject.dart';
|
||||
import 'package:firka/helpers/api/model/generic.dart';
|
||||
import 'package:firka/helpers/api/model/subject.dart';
|
||||
|
||||
class Grade {
|
||||
final String uid;
|
||||
@@ -20,25 +20,24 @@ class Grade {
|
||||
final UidObj? classGroup;
|
||||
final int sortIndex;
|
||||
|
||||
Grade({
|
||||
required this.uid,
|
||||
required this.recordDate,
|
||||
required this.creationDate,
|
||||
this.ackDate,
|
||||
required this.subject,
|
||||
this.topic,
|
||||
required this.type,
|
||||
this.mode,
|
||||
required this.valueType,
|
||||
required this.teacher,
|
||||
this.kind,
|
||||
this.numericValue,
|
||||
required this.strValue,
|
||||
this.weightPercentage,
|
||||
this.shortStrValue,
|
||||
this.classGroup,
|
||||
required this.sortIndex,
|
||||
});
|
||||
Grade(
|
||||
{required this.uid,
|
||||
required this.recordDate,
|
||||
required this.creationDate,
|
||||
this.ackDate,
|
||||
required this.subject,
|
||||
this.topic,
|
||||
required this.type,
|
||||
this.mode,
|
||||
required this.valueType,
|
||||
required this.teacher,
|
||||
this.kind,
|
||||
this.numericValue,
|
||||
required this.strValue,
|
||||
this.weightPercentage,
|
||||
this.shortStrValue,
|
||||
this.classGroup,
|
||||
required this.sortIndex});
|
||||
|
||||
factory Grade.fromJson(Map<String, dynamic> json) {
|
||||
return Grade(
|
||||
@@ -66,28 +65,6 @@ class Grade {
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'Uid': uid,
|
||||
'RogzitesDatuma': recordDate.toUtc().toIso8601String(),
|
||||
'KeszitesDatuma': creationDate.toUtc().toIso8601String(),
|
||||
'LattamozasDatuma': ackDate?.toUtc().toIso8601String(),
|
||||
'Tantargy': subject.toJson(),
|
||||
'Tema': topic,
|
||||
'Tipus': type.toJson(),
|
||||
'Mod': mode?.toJson(),
|
||||
'ErtekFajta': valueType.toJson(),
|
||||
'ErtekeloTanarNeve': teacher,
|
||||
'Kind': kind,
|
||||
'SzamErtek': numericValue,
|
||||
'SzovegesErtek': strValue,
|
||||
'SulySzazalekErteke': weightPercentage,
|
||||
'SzovegesErtekelesRovidNev': shortStrValue,
|
||||
'OsztalyCsoport': classGroup != null ? {'Uid': classGroup!.uid} : null,
|
||||
'SortIndex': sortIndex,
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Grade('
|
||||
@@ -5,22 +5,20 @@ class Guardian {
|
||||
final String? phoneNumber;
|
||||
final String uid;
|
||||
|
||||
Guardian({
|
||||
required this.email,
|
||||
required this.isLegalRepresentative,
|
||||
required this.name,
|
||||
required this.phoneNumber,
|
||||
required this.uid,
|
||||
});
|
||||
Guardian(
|
||||
{required this.email,
|
||||
required this.isLegalRepresentative,
|
||||
required this.name,
|
||||
required this.phoneNumber,
|
||||
required this.uid});
|
||||
|
||||
factory Guardian.fromJson(Map<String, dynamic> json) {
|
||||
return Guardian(
|
||||
email: json['EmailCim'],
|
||||
isLegalRepresentative: json['IsTorvenyesKepviselo'],
|
||||
name: json['Nev'],
|
||||
phoneNumber: json['Telefonszam'],
|
||||
uid: json['Uid'],
|
||||
);
|
||||
email: json['EmailCim'],
|
||||
isLegalRepresentative: json['IsTorvenyesKepviselo'],
|
||||
name: json['Nev'],
|
||||
phoneNumber: json['Telefonszam'],
|
||||
uid: json['Uid']);
|
||||
}
|
||||
|
||||
@override
|
||||
70
firka/lib/helpers/api/model/homework.dart
Normal file
@@ -0,0 +1,70 @@
|
||||
import 'package:firka/helpers/api/model/subject.dart';
|
||||
|
||||
import 'generic.dart';
|
||||
|
||||
class Homework {
|
||||
final String uid;
|
||||
final Subject subject;
|
||||
final String subjectName;
|
||||
final String teacherName;
|
||||
final String description;
|
||||
final DateTime startDate;
|
||||
final DateTime dueDate;
|
||||
final DateTime creationDate;
|
||||
final bool isCreatedByTeacher;
|
||||
final bool isDone;
|
||||
final bool canBeSubmitted;
|
||||
final UidObj classGroup;
|
||||
final bool canAttach;
|
||||
|
||||
Homework(
|
||||
{required this.uid,
|
||||
required this.subject,
|
||||
required this.subjectName,
|
||||
required this.teacherName,
|
||||
required this.description,
|
||||
required this.startDate,
|
||||
required this.dueDate,
|
||||
required this.creationDate,
|
||||
required this.isCreatedByTeacher,
|
||||
required this.isDone,
|
||||
required this.canBeSubmitted,
|
||||
required this.classGroup,
|
||||
required this.canAttach});
|
||||
|
||||
factory Homework.fromJson(Map<String, dynamic> json) {
|
||||
return Homework(
|
||||
uid: json["Uid"],
|
||||
subject: Subject.fromJson(json["Tantargy"]),
|
||||
subjectName: json["TantargyNeve"],
|
||||
teacherName: json["RogzitoTanarNeve"],
|
||||
description: json["Szoveg"],
|
||||
startDate: DateTime.parse(json["FeladasDatuma"]).toLocal(),
|
||||
dueDate: DateTime.parse(json["HataridoDatuma"]).toLocal(),
|
||||
creationDate: DateTime.parse(json["RogzitesIdopontja"]).toLocal(),
|
||||
isCreatedByTeacher: json["IsTanarRogzitette"],
|
||||
isDone: json["IsMegoldva"],
|
||||
canBeSubmitted: json["IsBeadhato"],
|
||||
classGroup: UidObj.fromJson(json["OsztalyCsoport"]),
|
||||
canAttach: json["IsCsatolasEngedelyezes"]);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Homework('
|
||||
'uid: "$uid", '
|
||||
'subject: $subject, '
|
||||
'subjectName: "$subjectName", '
|
||||
'teacherName: "$teacherName", '
|
||||
'description: "$description", '
|
||||
'startDate: $startDate, '
|
||||
'dueDate: $dueDate, '
|
||||
'creationDate: $creationDate, '
|
||||
'isCreatedByTeacher: $isCreatedByTeacher, '
|
||||
'isDone: $isDone, '
|
||||
'canBeSubmitted: $canBeSubmitted, '
|
||||
'classGroup: $classGroup, '
|
||||
'canAttach: $canAttach'
|
||||
')';
|
||||
}
|
||||
}
|
||||
@@ -4,12 +4,11 @@ class Institution {
|
||||
final List<SystemModule> systemModuleList;
|
||||
final String uid;
|
||||
|
||||
Institution({
|
||||
required this.customizationSettings,
|
||||
required this.shortName,
|
||||
required this.systemModuleList,
|
||||
required this.uid,
|
||||
});
|
||||
Institution(
|
||||
{required this.customizationSettings,
|
||||
required this.shortName,
|
||||
required this.systemModuleList,
|
||||
required this.uid});
|
||||
|
||||
factory Institution.fromJson(Map<String, dynamic> json) {
|
||||
var systemModuleList = List<SystemModule>.empty(growable: true);
|
||||
@@ -19,9 +18,8 @@ class Institution {
|
||||
}
|
||||
|
||||
return Institution(
|
||||
customizationSettings: CustomizationSettings.fromJson(
|
||||
json['TestreszabasBeallitasok'],
|
||||
),
|
||||
customizationSettings:
|
||||
CustomizationSettings.fromJson(json['TestreszabasBeallitasok']),
|
||||
shortName: json['RovidNev'],
|
||||
systemModuleList: systemModuleList,
|
||||
uid: json['Uid'],
|
||||
@@ -35,21 +33,19 @@ class CustomizationSettings {
|
||||
final bool isLessonsThemeVisible;
|
||||
final String nextServerDeployAsString;
|
||||
|
||||
CustomizationSettings({
|
||||
required this.delayForNotifications,
|
||||
required this.isClassAverageVisible,
|
||||
required this.isLessonsThemeVisible,
|
||||
required this.nextServerDeployAsString,
|
||||
});
|
||||
CustomizationSettings(
|
||||
{required this.delayForNotifications,
|
||||
required this.isClassAverageVisible,
|
||||
required this.isLessonsThemeVisible,
|
||||
required this.nextServerDeployAsString});
|
||||
|
||||
factory CustomizationSettings.fromJson(Map<String, dynamic> json) {
|
||||
return CustomizationSettings(
|
||||
delayForNotifications:
|
||||
json['ErtekelesekMegjelenitesenekKesleltetesenekMerteke'],
|
||||
isClassAverageVisible: json['IsOsztalyAtlagMegjeleniteseEllenorzoben'],
|
||||
isLessonsThemeVisible: json['IsTanorakTemajaMegtekinthetoEllenorzoben'],
|
||||
nextServerDeployAsString: json['KovetkezoTelepitesDatuma'],
|
||||
);
|
||||
delayForNotifications:
|
||||
json['ErtekelesekMegjelenitesenekKesleltetesenekMerteke'],
|
||||
isClassAverageVisible: json['IsOsztalyAtlagMegjeleniteseEllenorzoben'],
|
||||
isLessonsThemeVisible: json['IsTanorakTemajaMegtekinthetoEllenorzoben'],
|
||||
nextServerDeployAsString: json['KovetkezoTelepitesDatuma']);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -72,10 +68,7 @@ class SystemModule {
|
||||
|
||||
factory SystemModule.fromJson(Map<String, dynamic> json) {
|
||||
return SystemModule(
|
||||
isActive: json['IsAktiv'],
|
||||
type: json['Tipus'],
|
||||
url: json['Url'],
|
||||
);
|
||||
isActive: json['IsAktiv'], type: json['Tipus'], url: json['Url']);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'generic.dart';
|
||||
import 'package:firka/helpers/api/model/generic.dart';
|
||||
|
||||
class NoticeBoardItem {
|
||||
final String uid;
|
||||
@@ -9,26 +9,24 @@ class NoticeBoardItem {
|
||||
final String contentHTML;
|
||||
final String contentText;
|
||||
|
||||
NoticeBoardItem({
|
||||
required this.uid,
|
||||
required this.author,
|
||||
required this.validFrom,
|
||||
required this.validTo,
|
||||
required this.title,
|
||||
required this.contentHTML,
|
||||
required this.contentText,
|
||||
});
|
||||
NoticeBoardItem(
|
||||
{required this.uid,
|
||||
required this.author,
|
||||
required this.validFrom,
|
||||
required this.validTo,
|
||||
required this.title,
|
||||
required this.contentHTML,
|
||||
required this.contentText});
|
||||
|
||||
factory NoticeBoardItem.fromJson(Map<String, dynamic> json) {
|
||||
return NoticeBoardItem(
|
||||
uid: json['Uid'],
|
||||
author: json['RogzitoNeve'],
|
||||
validFrom: DateTime.parse(json['ErvenyessegKezdete']),
|
||||
validTo: DateTime.parse(json['ErvenyessegVege']),
|
||||
title: json['Cim'],
|
||||
contentHTML: json['Tartalom'],
|
||||
contentText: json['TartalomText'],
|
||||
);
|
||||
uid: json['Uid'],
|
||||
author: json['RogzitoNeve'],
|
||||
validFrom: DateTime.parse(json['ErvenyessegKezdete']),
|
||||
validTo: DateTime.parse(json['ErvenyessegVege']),
|
||||
title: json['Cim'],
|
||||
contentHTML: json['Tartalom'],
|
||||
contentText: json['TartalomText']);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -55,28 +53,26 @@ class InfoBoardItem {
|
||||
final String contentText;
|
||||
final NameUidDesc type;
|
||||
|
||||
InfoBoardItem({
|
||||
required this.uid,
|
||||
required this.title,
|
||||
required this.date,
|
||||
required this.author,
|
||||
required this.createdAt,
|
||||
required this.contentHTML,
|
||||
required this.contentText,
|
||||
required this.type,
|
||||
});
|
||||
InfoBoardItem(
|
||||
{required this.uid,
|
||||
required this.title,
|
||||
required this.date,
|
||||
required this.author,
|
||||
required this.createdAt,
|
||||
required this.contentHTML,
|
||||
required this.contentText,
|
||||
required this.type});
|
||||
|
||||
factory InfoBoardItem.fromJson(Map<String, dynamic> json) {
|
||||
return InfoBoardItem(
|
||||
uid: json['Uid'],
|
||||
title: json['Cim'],
|
||||
date: DateTime.parse(json['Datum']),
|
||||
author: json['KeszitoTanarNeve'],
|
||||
createdAt: DateTime.parse(json['KeszitesDatuma']),
|
||||
contentText: json['Tartalom'],
|
||||
contentHTML: json['TartalomFormazott'],
|
||||
type: NameUidDesc.fromJson(json['Tipus']),
|
||||
);
|
||||
uid: json['Uid'],
|
||||
title: json['Cim'],
|
||||
date: DateTime.parse(json['Datum']),
|
||||
author: json['KeszitoTanarNeve'],
|
||||
createdAt: DateTime.parse(json['KeszitesDatuma']),
|
||||
contentText: json['Tartalom'],
|
||||
contentHTML: json['TartalomFormazott'],
|
||||
type: NameUidDesc.fromJson(json['Tipus']));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'generic.dart';
|
||||
import 'subject.dart';
|
||||
import 'package:firka/helpers/api/model/generic.dart';
|
||||
import 'package:firka/helpers/api/model/subject.dart';
|
||||
|
||||
class Omission {
|
||||
final String uid;
|
||||
@@ -75,7 +75,11 @@ class Class {
|
||||
final DateTime end;
|
||||
final int classNo;
|
||||
|
||||
Class({required this.start, required this.end, required this.classNo});
|
||||
Class({
|
||||
required this.start,
|
||||
required this.end,
|
||||
required this.classNo,
|
||||
});
|
||||
|
||||
factory Class.fromJson(Map<String, dynamic> json) {
|
||||
return Class(
|
||||
115
firka/lib/helpers/api/model/student.dart
Normal file
@@ -0,0 +1,115 @@
|
||||
import 'package:firka/helpers/api/model/guardian.dart';
|
||||
import 'package:firka/helpers/api/model/institution.dart';
|
||||
import 'package:firka/helpers/json_helper.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class Student {
|
||||
final List<String> addressDataList;
|
||||
final BankAccount bankAccount;
|
||||
|
||||
// final int yearOfBirth;
|
||||
// final int monthOfBirth;
|
||||
// final int dayOfBirth;
|
||||
final DateTime birthdate;
|
||||
|
||||
final String? emailAddress;
|
||||
final String name;
|
||||
final String? phoneNumber;
|
||||
|
||||
final String schoolYearUID;
|
||||
final String uid;
|
||||
|
||||
final List<Guardian> guardianList;
|
||||
final String instituteCode;
|
||||
final String instituteName;
|
||||
|
||||
final Institution institution;
|
||||
|
||||
Student(
|
||||
{required this.addressDataList,
|
||||
required this.bankAccount,
|
||||
// required this.yearOfBirth,
|
||||
// required this.monthOfBirth,
|
||||
// required this.dayOfBirth,
|
||||
required this.birthdate,
|
||||
required this.emailAddress,
|
||||
required this.name,
|
||||
required this.phoneNumber,
|
||||
required this.schoolYearUID,
|
||||
required this.uid,
|
||||
required this.guardianList,
|
||||
required this.instituteCode,
|
||||
required this.instituteName,
|
||||
required this.institution});
|
||||
|
||||
factory Student.fromJson(Map<String, dynamic> json) {
|
||||
var guardianList = List<Guardian>.empty(growable: true);
|
||||
|
||||
for (var item in json['Gondviselok']) {
|
||||
guardianList.add(Guardian.fromJson(item));
|
||||
}
|
||||
|
||||
return Student(
|
||||
addressDataList: listToTyped<String>(json['Cimek']),
|
||||
bankAccount: BankAccount.fromJson(json['Bankszamla']),
|
||||
birthdate: DateFormat('yyyy-M-d').parse(
|
||||
"${json['SzuletesiEv']}-${json['SzuletesiHonap']}-${json['SzuletesiNap']}"),
|
||||
emailAddress: json['EmailCim'],
|
||||
name: json['Nev'],
|
||||
phoneNumber: json['Telefonszam'],
|
||||
schoolYearUID: json['TanevUid'],
|
||||
uid: json['Uid'],
|
||||
guardianList: guardianList,
|
||||
instituteCode: json['IntezmenyAzonosito'],
|
||||
instituteName: json['IntezmenyNev'],
|
||||
institution: Institution.fromJson(json['Intezmeny']));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Student('
|
||||
'addressDataList: [$addressDataList], '
|
||||
'bankAccount: $bankAccount, '
|
||||
'birthDate: $birthdate, '
|
||||
'emailAddress: "$emailAddress", '
|
||||
'name: "$name", '
|
||||
'phoneNumber: "$phoneNumber", '
|
||||
'schoolYearUID: "$schoolYearUID", '
|
||||
'uid: "$uid", '
|
||||
'guardianList: [$guardianList], '
|
||||
'instituteCode: "$instituteCode", '
|
||||
'instituteName: "$instituteName", '
|
||||
')';
|
||||
}
|
||||
}
|
||||
|
||||
class BankAccount {
|
||||
final String? accountNumber;
|
||||
final bool? isReadOnly;
|
||||
final String? ownerName;
|
||||
final int? ownerType;
|
||||
|
||||
BankAccount(
|
||||
{required this.accountNumber,
|
||||
required this.isReadOnly,
|
||||
required this.ownerName,
|
||||
required this.ownerType});
|
||||
|
||||
factory BankAccount.fromJson(Map<String, dynamic> json) {
|
||||
return BankAccount(
|
||||
accountNumber: json['BankszamlaSzam'],
|
||||
isReadOnly: json['IsReadOnly'],
|
||||
ownerName: json['BankszamlaTulajdonosNeve'],
|
||||
ownerType: json['BankszamlaTulajdonosTipusId']);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BankAccount('
|
||||
'accountNumber: "$accountNumber", '
|
||||
'isReadOnly: "$isReadOnly", '
|
||||
'ownerName: "$ownerName", '
|
||||
'ownerType: "$ownerType"'
|
||||
')';
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:firka/helpers/api/model/subject.dart';
|
||||
|
||||
import 'generic.dart';
|
||||
import 'subject.dart';
|
||||
|
||||
class Test {
|
||||
final String uid;
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'generic.dart';
|
||||
import 'subject.dart';
|
||||
import 'package:firka/helpers/api/model/generic.dart';
|
||||
import 'package:firka/helpers/api/model/subject.dart';
|
||||
|
||||
class Lesson {
|
||||
final String uid;
|
||||
@@ -83,9 +83,8 @@ class Lesson {
|
||||
? NameUid.fromJson(json['OsztalyCsoport'])
|
||||
: null,
|
||||
teacher: json['TanarNeve'],
|
||||
subject: json['Tantargy'] != null
|
||||
? Subject.fromJson(json['Tantargy'])
|
||||
: null,
|
||||
subject:
|
||||
json['Tantargy'] != null ? Subject.fromJson(json['Tantargy']) : null,
|
||||
theme: json['Tema'],
|
||||
roomName: json['TeremNeve'],
|
||||
type: NameUidDesc.fromJson(json['Tipus']),
|
||||
@@ -106,8 +105,8 @@ class Lesson {
|
||||
digitalPlatformType: json['DigitalisPlatformTipus'],
|
||||
digitalSupportDeviceTypeList:
|
||||
json['DigitalisTamogatoEszkozTipusList'] != null
|
||||
? List<String>.from(json['DigitalisTamogatoEszkozTipusList'])
|
||||
: List<String>.empty(),
|
||||
? List<String>.from(json['DigitalisTamogatoEszkozTipusList'])
|
||||
: List<String>.empty(),
|
||||
createdAt: DateTime.parse(json['Letrehozas']).toLocal(),
|
||||
lastModifiedAt: DateTime.parse(json['UtolsoModositas']).toLocal(),
|
||||
);
|
||||
@@ -128,7 +127,7 @@ class Lesson {
|
||||
'Nev': name,
|
||||
'Oraszam': lessonNumber,
|
||||
'OraEvesSorszama': lessonSeqNumber,
|
||||
'OsztalyCsoport': classGroup?.toJson(),
|
||||
'OsztalyCsoport': classGroup,
|
||||
'TanarNeve': teacher,
|
||||
'Tantargy': subject?.toJson(),
|
||||
'Tema': theme,
|
||||
@@ -6,24 +6,22 @@ class TokenGrantResponse {
|
||||
final String refreshToken;
|
||||
final String scope;
|
||||
|
||||
TokenGrantResponse({
|
||||
required this.idToken,
|
||||
required this.accessToken,
|
||||
required this.expiresIn,
|
||||
required this.tokenType,
|
||||
required this.refreshToken,
|
||||
required this.scope,
|
||||
});
|
||||
TokenGrantResponse(
|
||||
{required this.idToken,
|
||||
required this.accessToken,
|
||||
required this.expiresIn,
|
||||
required this.tokenType,
|
||||
required this.refreshToken,
|
||||
required this.scope});
|
||||
|
||||
factory TokenGrantResponse.fromJson(Map<String, dynamic> json) {
|
||||
return TokenGrantResponse(
|
||||
idToken: json['id_token'],
|
||||
accessToken: json['access_token'],
|
||||
expiresIn: json['expires_in'],
|
||||
tokenType: json['token_type'],
|
||||
refreshToken: json['refresh_token'],
|
||||
scope: json['scope'],
|
||||
);
|
||||
idToken: json['id_token'],
|
||||
accessToken: json['access_token'],
|
||||
expiresIn: json['expires_in'],
|
||||
tokenType: json['token_type'],
|
||||
refreshToken: json['refresh_token'],
|
||||
scope: json['scope']);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -1,8 +1,9 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:firka/data/models/token_model.dart';
|
||||
import 'package:kreta_api/kreta_api.dart' hide KretaEndpoints;
|
||||
import 'package:firka/helpers/api/exceptions/token.dart';
|
||||
import 'package:firka/helpers/api/resp/token_grant.dart';
|
||||
import 'package:firka/helpers/db/models/token_model.dart';
|
||||
|
||||
import 'package:firka/app/app_state.dart';
|
||||
import '../../main.dart';
|
||||
import 'consts.dart';
|
||||
|
||||
Future<TokenGrantResponse> getAccessToken(String code) async {
|
||||
@@ -22,11 +23,8 @@ Future<TokenGrantResponse> getAccessToken(String code) async {
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await dio.post(
|
||||
KretaEndpoints.tokenGrantUrl,
|
||||
options: Options(headers: headers),
|
||||
data: formData,
|
||||
);
|
||||
final response = await dio.post(KretaEndpoints.tokenGrantUrl,
|
||||
options: Options(headers: headers), data: formData);
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
@@ -35,8 +33,7 @@ Future<TokenGrantResponse> getAccessToken(String code) async {
|
||||
throw Exception("Invalid grant");
|
||||
default:
|
||||
throw Exception(
|
||||
"Failed to get access token, response code: ${response.statusCode}",
|
||||
);
|
||||
"Failed to get access token, response code: ${response.statusCode}");
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
@@ -47,8 +44,7 @@ const _tokenRefreshRetryDelays = [1000, 3000, 5000];
|
||||
|
||||
Future<TokenGrantResponse> extendToken(TokenModel model) async {
|
||||
logger.info(
|
||||
"Extending token for user: ${model.studentId}, institute: ${model.iss}",
|
||||
);
|
||||
"Extending token for user: ${model.studentId}, institute: ${model.iss}");
|
||||
|
||||
final headers = <String, String>{
|
||||
"content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
@@ -70,38 +66,30 @@ Future<TokenGrantResponse> extendToken(TokenModel model) async {
|
||||
if (attempt > 0) {
|
||||
final delay = _tokenRefreshRetryDelays[attempt - 1];
|
||||
logger.info(
|
||||
"Token refresh attempt ${attempt + 1}, waiting ${delay}ms...",
|
||||
);
|
||||
"Token refresh attempt ${attempt + 1}, waiting ${delay}ms...");
|
||||
await Future.delayed(Duration(milliseconds: delay));
|
||||
}
|
||||
|
||||
final response = await dio.post(
|
||||
KretaEndpoints.tokenGrantUrl,
|
||||
options: Options(headers: headers),
|
||||
data: formData,
|
||||
);
|
||||
final response = await dio.post(KretaEndpoints.tokenGrantUrl,
|
||||
options: Options(headers: headers), data: formData);
|
||||
|
||||
switch (response.statusCode) {
|
||||
case 200:
|
||||
logger.info(
|
||||
"Token extended successfully for user: ${model.studentId}",
|
||||
);
|
||||
logger
|
||||
.info("Token extended successfully for user: ${model.studentId}");
|
||||
return TokenGrantResponse.fromJson(response.data);
|
||||
case 400:
|
||||
case 401:
|
||||
logger.warning(
|
||||
"Token refresh failed (${response.statusCode}) - refresh token invalid for user: ${model.studentId}",
|
||||
);
|
||||
"Token refresh failed (${response.statusCode}) - refresh token invalid for user: ${model.studentId}");
|
||||
throw response.statusCode == 400
|
||||
? TokenExpiredException()
|
||||
: InvalidGrantException();
|
||||
default:
|
||||
logger.warning(
|
||||
"Token refresh failed (${response.statusCode}) for user: ${model.studentId}, attempt ${attempt + 1}",
|
||||
);
|
||||
"Token refresh failed (${response.statusCode}) for user: ${model.studentId}, attempt ${attempt + 1}");
|
||||
lastError = Exception(
|
||||
"Failed to get access token, response code: ${response.statusCode}",
|
||||
);
|
||||
"Failed to get access token, response code: ${response.statusCode}");
|
||||
// Continue to retry for network errors
|
||||
continue;
|
||||
}
|
||||
@@ -111,8 +99,7 @@ Future<TokenGrantResponse> extendToken(TokenModel model) async {
|
||||
rethrow;
|
||||
} on DioException catch (e) {
|
||||
logger.warning(
|
||||
"Token refresh network error for user: ${model.studentId}, attempt ${attempt + 1}: $e",
|
||||
);
|
||||
"Token refresh network error for user: ${model.studentId}, attempt ${attempt + 1}: $e");
|
||||
lastError = e;
|
||||
continue;
|
||||
} catch (e) {
|
||||
@@ -122,8 +109,7 @@ Future<TokenGrantResponse> extendToken(TokenModel model) async {
|
||||
}
|
||||
}
|
||||
|
||||
logger.severe(
|
||||
"All token refresh attempts failed for user: ${model.studentId}",
|
||||
);
|
||||
logger
|
||||
.severe("All token refresh attempts failed for user: ${model.studentId}");
|
||||
throw lastError ?? Exception("Token refresh failed after all retries");
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:kreta_api/kreta_api.dart';
|
||||
import 'package:firka/helpers/api/model/grade.dart';
|
||||
import 'package:firka/helpers/api/model/timetable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -13,9 +14,7 @@ class IOSWidgetHelper {
|
||||
if (!Platform.isIOS) return null;
|
||||
|
||||
try {
|
||||
final result = await _channel.invokeMethod<String>(
|
||||
'getAppGroupDirectory',
|
||||
);
|
||||
final result = await _channel.invokeMethod<String>('getAppGroupDirectory');
|
||||
if (result != null) {
|
||||
return Directory(result);
|
||||
}
|
||||
@@ -40,12 +39,8 @@ class IOSWidgetHelper {
|
||||
if (!Platform.isIOS) return;
|
||||
|
||||
debugPrint('[IOSWidget] Starting updateWidgetData...');
|
||||
debugPrint(
|
||||
'[IOSWidget] todayLessons: ${todayLessons.length}, tomorrowLessons: ${tomorrowLessons.length}',
|
||||
);
|
||||
debugPrint(
|
||||
'[IOSWidget] grades: ${grades.length}, subjectAverages: ${subjectAverages.length}',
|
||||
);
|
||||
debugPrint('[IOSWidget] todayLessons: ${todayLessons.length}, tomorrowLessons: ${tomorrowLessons.length}');
|
||||
debugPrint('[IOSWidget] grades: ${grades.length}, subjectAverages: ${subjectAverages.length}');
|
||||
|
||||
final dir = await _getAppGroupDirectory();
|
||||
if (dir == null) {
|
||||
@@ -61,31 +56,23 @@ class IOSWidgetHelper {
|
||||
'timetable': {
|
||||
'today': todayLessons.map((l) => _lessonToJson(l)).toList(),
|
||||
'tomorrow': tomorrowLessons.map((l) => _lessonToJson(l)).toList(),
|
||||
'nextSchoolDay': nextSchoolDayLessons
|
||||
.map((l) => _lessonToJson(l))
|
||||
.toList(),
|
||||
'nextSchoolDay': nextSchoolDayLessons.map((l) => _lessonToJson(l)).toList(),
|
||||
'nextSchoolDayDate': nextSchoolDayDate?.toIso8601String(),
|
||||
'currentBreak': currentBreak != null
|
||||
? {
|
||||
'name': currentBreak.name,
|
||||
'nameKey': currentBreak.nameKey,
|
||||
'endDate': currentBreak.endDate.toIso8601String(),
|
||||
}
|
||||
: null,
|
||||
'currentBreak': currentBreak != null ? {
|
||||
'name': currentBreak.name,
|
||||
'nameKey': currentBreak.nameKey,
|
||||
'endDate': currentBreak.endDate.toIso8601String(),
|
||||
} : null,
|
||||
},
|
||||
'grades': grades.take(20).map((g) => _gradeToJson(g)).toList(),
|
||||
'averages': {
|
||||
'overall': overallAverage,
|
||||
'subjects': subjectAverages.entries
|
||||
.map(
|
||||
(e) => {
|
||||
'uid': e.key,
|
||||
'name': _getSubjectNameFromGrades(e.key, grades),
|
||||
'average': e.value,
|
||||
'gradeCount': _getGradeCount(e.key, grades),
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
'subjects': subjectAverages.entries.map((e) => {
|
||||
'uid': e.key,
|
||||
'name': _getSubjectNameFromGrades(e.key, grades),
|
||||
'average': e.value,
|
||||
'gradeCount': _getGradeCount(e.key, grades),
|
||||
}).toList(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -131,29 +118,26 @@ class IOSWidgetHelper {
|
||||
'name': lesson.name,
|
||||
'lessonNumber': lesson.lessonNumber,
|
||||
'teacher': lesson.teacher,
|
||||
'subject': subject != null
|
||||
? {
|
||||
'uid': subject.uid,
|
||||
'name': subject.name,
|
||||
'category': {
|
||||
'uid': subject.category.uid,
|
||||
'name': subject.category.name,
|
||||
'description': subject.category.description,
|
||||
},
|
||||
'sortIndex': subject.sortIndex,
|
||||
'teacherName': subject.teacherName,
|
||||
}
|
||||
: {
|
||||
'uid': '',
|
||||
'name': lesson.name,
|
||||
'category': null,
|
||||
'sortIndex': 0,
|
||||
'teacherName': null,
|
||||
},
|
||||
'subject': subject != null ? {
|
||||
'uid': subject.uid,
|
||||
'name': subject.name,
|
||||
'category': subject.category != null ? {
|
||||
'uid': subject.category!.uid,
|
||||
'name': subject.category!.name,
|
||||
'description': subject.category!.description,
|
||||
} : null,
|
||||
'sortIndex': subject.sortIndex,
|
||||
'teacherName': subject.teacherName,
|
||||
} : {
|
||||
'uid': '',
|
||||
'name': lesson.name,
|
||||
'category': null,
|
||||
'sortIndex': 0,
|
||||
'teacherName': null,
|
||||
},
|
||||
'theme': lesson.theme,
|
||||
'roomName': lesson.roomName,
|
||||
'isCancelled':
|
||||
lesson.state.name?.toLowerCase().contains('elmarad') ?? false,
|
||||
'isCancelled': lesson.state.name?.toLowerCase().contains('elmarad') ?? false,
|
||||
'isSubstitution': lesson.substituteTeacher != null,
|
||||
};
|
||||
}
|
||||
@@ -165,11 +149,11 @@ class IOSWidgetHelper {
|
||||
'subject': {
|
||||
'uid': grade.subject.uid,
|
||||
'name': grade.subject.name,
|
||||
'category': {
|
||||
'uid': grade.subject.category.uid,
|
||||
'name': grade.subject.category.name,
|
||||
'description': grade.subject.category.description,
|
||||
},
|
||||
'category': grade.subject.category != null ? {
|
||||
'uid': grade.subject.category!.uid,
|
||||
'name': grade.subject.category!.name,
|
||||
'description': grade.subject.category!.description,
|
||||
} : null,
|
||||
'sortIndex': grade.subject.sortIndex,
|
||||
// Use the grade's teacher field, not subject.teacherName (which is usually null for grades)
|
||||
'teacherName': grade.teacher,
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:isar_community/isar.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
part 'app_settings_model.g.dart';
|
||||
|
||||
827
firka/lib/helpers/db/models/app_settings_model.g.dart
Normal file
@@ -0,0 +1,827 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'app_settings_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// IsarCollectionGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
|
||||
|
||||
extension GetAppSettingsModelCollection on Isar {
|
||||
IsarCollection<AppSettingsModel> get appSettingsModels => this.collection();
|
||||
}
|
||||
|
||||
const AppSettingsModelSchema = CollectionSchema(
|
||||
name: r'AppSettingsModel',
|
||||
id: -638838212012723081,
|
||||
properties: {
|
||||
r'valueBool': PropertySchema(
|
||||
id: 0,
|
||||
name: r'valueBool',
|
||||
type: IsarType.bool,
|
||||
),
|
||||
r'valueDouble': PropertySchema(
|
||||
id: 1,
|
||||
name: r'valueDouble',
|
||||
type: IsarType.double,
|
||||
),
|
||||
r'valueIndex': PropertySchema(
|
||||
id: 2,
|
||||
name: r'valueIndex',
|
||||
type: IsarType.long,
|
||||
),
|
||||
r'valueString': PropertySchema(
|
||||
id: 3,
|
||||
name: r'valueString',
|
||||
type: IsarType.string,
|
||||
)
|
||||
},
|
||||
estimateSize: _appSettingsModelEstimateSize,
|
||||
serialize: _appSettingsModelSerialize,
|
||||
deserialize: _appSettingsModelDeserialize,
|
||||
deserializeProp: _appSettingsModelDeserializeProp,
|
||||
idName: r'id',
|
||||
indexes: {},
|
||||
links: {},
|
||||
embeddedSchemas: {},
|
||||
getId: _appSettingsModelGetId,
|
||||
getLinks: _appSettingsModelGetLinks,
|
||||
attach: _appSettingsModelAttach,
|
||||
version: '3.1.0+1',
|
||||
);
|
||||
|
||||
int _appSettingsModelEstimateSize(
|
||||
AppSettingsModel object,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
var bytesCount = offsets.last;
|
||||
{
|
||||
final value = object.valueString;
|
||||
if (value != null) {
|
||||
bytesCount += 3 + value.length * 3;
|
||||
}
|
||||
}
|
||||
return bytesCount;
|
||||
}
|
||||
|
||||
void _appSettingsModelSerialize(
|
||||
AppSettingsModel object,
|
||||
IsarWriter writer,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
writer.writeBool(offsets[0], object.valueBool);
|
||||
writer.writeDouble(offsets[1], object.valueDouble);
|
||||
writer.writeLong(offsets[2], object.valueIndex);
|
||||
writer.writeString(offsets[3], object.valueString);
|
||||
}
|
||||
|
||||
AppSettingsModel _appSettingsModelDeserialize(
|
||||
Id id,
|
||||
IsarReader reader,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
final object = AppSettingsModel();
|
||||
object.id = id;
|
||||
object.valueBool = reader.readBoolOrNull(offsets[0]);
|
||||
object.valueDouble = reader.readDoubleOrNull(offsets[1]);
|
||||
object.valueIndex = reader.readLongOrNull(offsets[2]);
|
||||
object.valueString = reader.readStringOrNull(offsets[3]);
|
||||
return object;
|
||||
}
|
||||
|
||||
P _appSettingsModelDeserializeProp<P>(
|
||||
IsarReader reader,
|
||||
int propertyId,
|
||||
int offset,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
switch (propertyId) {
|
||||
case 0:
|
||||
return (reader.readBoolOrNull(offset)) as P;
|
||||
case 1:
|
||||
return (reader.readDoubleOrNull(offset)) as P;
|
||||
case 2:
|
||||
return (reader.readLongOrNull(offset)) as P;
|
||||
case 3:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
}
|
||||
}
|
||||
|
||||
Id _appSettingsModelGetId(AppSettingsModel object) {
|
||||
return object.id ?? Isar.autoIncrement;
|
||||
}
|
||||
|
||||
List<IsarLinkBase<dynamic>> _appSettingsModelGetLinks(AppSettingsModel object) {
|
||||
return [];
|
||||
}
|
||||
|
||||
void _appSettingsModelAttach(
|
||||
IsarCollection<dynamic> col, Id id, AppSettingsModel object) {
|
||||
object.id = id;
|
||||
}
|
||||
|
||||
extension AppSettingsModelQueryWhereSort
|
||||
on QueryBuilder<AppSettingsModel, AppSettingsModel, QWhere> {
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterWhere> anyId() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(const IdWhereClause.any());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AppSettingsModelQueryWhere
|
||||
on QueryBuilder<AppSettingsModel, AppSettingsModel, QWhereClause> {
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterWhereClause> idEqualTo(
|
||||
Id id) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: id,
|
||||
upper: id,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterWhereClause>
|
||||
idNotEqualTo(Id id) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: id, includeUpper: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: id, includeLower: false),
|
||||
);
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: id, includeLower: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: id, includeUpper: false),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterWhereClause>
|
||||
idGreaterThan(Id id, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: id, includeLower: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterWhereClause>
|
||||
idLessThan(Id id, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: id, includeUpper: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterWhereClause> idBetween(
|
||||
Id lowerId,
|
||||
Id upperId, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: lowerId,
|
||||
includeLower: includeLower,
|
||||
upper: upperId,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AppSettingsModelQueryFilter
|
||||
on QueryBuilder<AppSettingsModel, AppSettingsModel, QFilterCondition> {
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
idIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
property: r'id',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
idIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||
property: r'id',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
idEqualTo(Id? value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'id',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
idGreaterThan(
|
||||
Id? value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'id',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
idLessThan(
|
||||
Id? value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'id',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
idBetween(
|
||||
Id? lower,
|
||||
Id? upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'id',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueBoolIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
property: r'valueBool',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueBoolIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||
property: r'valueBool',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueBoolEqualTo(bool? value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'valueBool',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueDoubleIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
property: r'valueDouble',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueDoubleIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||
property: r'valueDouble',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueDoubleEqualTo(
|
||||
double? value, {
|
||||
double epsilon = Query.epsilon,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'valueDouble',
|
||||
value: value,
|
||||
epsilon: epsilon,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueDoubleGreaterThan(
|
||||
double? value, {
|
||||
bool include = false,
|
||||
double epsilon = Query.epsilon,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'valueDouble',
|
||||
value: value,
|
||||
epsilon: epsilon,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueDoubleLessThan(
|
||||
double? value, {
|
||||
bool include = false,
|
||||
double epsilon = Query.epsilon,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'valueDouble',
|
||||
value: value,
|
||||
epsilon: epsilon,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueDoubleBetween(
|
||||
double? lower,
|
||||
double? upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
double epsilon = Query.epsilon,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'valueDouble',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
epsilon: epsilon,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueIndexIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
property: r'valueIndex',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueIndexIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||
property: r'valueIndex',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueIndexEqualTo(int? value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'valueIndex',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueIndexGreaterThan(
|
||||
int? value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'valueIndex',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueIndexLessThan(
|
||||
int? value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'valueIndex',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueIndexBetween(
|
||||
int? lower,
|
||||
int? upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'valueIndex',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueStringIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
property: r'valueString',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueStringIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||
property: r'valueString',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueStringEqualTo(
|
||||
String? value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'valueString',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueStringGreaterThan(
|
||||
String? value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'valueString',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueStringLessThan(
|
||||
String? value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'valueString',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueStringBetween(
|
||||
String? lower,
|
||||
String? upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'valueString',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueStringStartsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.startsWith(
|
||||
property: r'valueString',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueStringEndsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.endsWith(
|
||||
property: r'valueString',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueStringContains(String value, {bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.contains(
|
||||
property: r'valueString',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueStringMatches(String pattern, {bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.matches(
|
||||
property: r'valueString',
|
||||
wildcard: pattern,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueStringIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'valueString',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterFilterCondition>
|
||||
valueStringIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
property: r'valueString',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AppSettingsModelQueryObject
|
||||
on QueryBuilder<AppSettingsModel, AppSettingsModel, QFilterCondition> {}
|
||||
|
||||
extension AppSettingsModelQueryLinks
|
||||
on QueryBuilder<AppSettingsModel, AppSettingsModel, QFilterCondition> {}
|
||||
|
||||
extension AppSettingsModelQuerySortBy
|
||||
on QueryBuilder<AppSettingsModel, AppSettingsModel, QSortBy> {
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterSortBy>
|
||||
sortByValueBool() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'valueBool', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterSortBy>
|
||||
sortByValueBoolDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'valueBool', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterSortBy>
|
||||
sortByValueDouble() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'valueDouble', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterSortBy>
|
||||
sortByValueDoubleDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'valueDouble', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterSortBy>
|
||||
sortByValueIndex() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'valueIndex', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterSortBy>
|
||||
sortByValueIndexDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'valueIndex', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterSortBy>
|
||||
sortByValueString() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'valueString', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterSortBy>
|
||||
sortByValueStringDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'valueString', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AppSettingsModelQuerySortThenBy
|
||||
on QueryBuilder<AppSettingsModel, AppSettingsModel, QSortThenBy> {
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterSortBy> thenById() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterSortBy>
|
||||
thenByIdDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'id', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterSortBy>
|
||||
thenByValueBool() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'valueBool', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterSortBy>
|
||||
thenByValueBoolDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'valueBool', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterSortBy>
|
||||
thenByValueDouble() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'valueDouble', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterSortBy>
|
||||
thenByValueDoubleDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'valueDouble', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterSortBy>
|
||||
thenByValueIndex() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'valueIndex', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterSortBy>
|
||||
thenByValueIndexDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'valueIndex', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterSortBy>
|
||||
thenByValueString() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'valueString', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QAfterSortBy>
|
||||
thenByValueStringDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'valueString', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AppSettingsModelQueryWhereDistinct
|
||||
on QueryBuilder<AppSettingsModel, AppSettingsModel, QDistinct> {
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QDistinct>
|
||||
distinctByValueBool() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'valueBool');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QDistinct>
|
||||
distinctByValueDouble() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'valueDouble');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QDistinct>
|
||||
distinctByValueIndex() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'valueIndex');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, AppSettingsModel, QDistinct>
|
||||
distinctByValueString({bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'valueString', caseSensitive: caseSensitive);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension AppSettingsModelQueryProperty
|
||||
on QueryBuilder<AppSettingsModel, AppSettingsModel, QQueryProperty> {
|
||||
QueryBuilder<AppSettingsModel, int, QQueryOperations> idProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'id');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, bool?, QQueryOperations> valueBoolProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'valueBool');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, double?, QQueryOperations>
|
||||
valueDoubleProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'valueDouble');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, int?, QQueryOperations> valueIndexProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'valueIndex');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<AppSettingsModel, String?, QQueryOperations>
|
||||
valueStringProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'valueString');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:isar_community/isar.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
part 'generic_cache_model.g.dart';
|
||||
|
||||
@@ -12,7 +12,7 @@ enum CacheId {
|
||||
getClassGroup,
|
||||
getSubjectAvg,
|
||||
getLessons,
|
||||
getHomework,
|
||||
getHomework
|
||||
}
|
||||
|
||||
@collection
|
||||
494
firka/lib/helpers/db/models/generic_cache_model.g.dart
Normal file
@@ -0,0 +1,494 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'generic_cache_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// IsarCollectionGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
|
||||
|
||||
extension GetGenericCacheModelCollection on Isar {
|
||||
IsarCollection<GenericCacheModel> get genericCacheModels => this.collection();
|
||||
}
|
||||
|
||||
const GenericCacheModelSchema = CollectionSchema(
|
||||
name: r'GenericCacheModel',
|
||||
id: 3174486726793780620,
|
||||
properties: {
|
||||
r'cacheData': PropertySchema(
|
||||
id: 0,
|
||||
name: r'cacheData',
|
||||
type: IsarType.string,
|
||||
)
|
||||
},
|
||||
estimateSize: _genericCacheModelEstimateSize,
|
||||
serialize: _genericCacheModelSerialize,
|
||||
deserialize: _genericCacheModelDeserialize,
|
||||
deserializeProp: _genericCacheModelDeserializeProp,
|
||||
idName: r'cacheKey',
|
||||
indexes: {},
|
||||
links: {},
|
||||
embeddedSchemas: {},
|
||||
getId: _genericCacheModelGetId,
|
||||
getLinks: _genericCacheModelGetLinks,
|
||||
attach: _genericCacheModelAttach,
|
||||
version: '3.1.0+1',
|
||||
);
|
||||
|
||||
int _genericCacheModelEstimateSize(
|
||||
GenericCacheModel object,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
var bytesCount = offsets.last;
|
||||
{
|
||||
final value = object.cacheData;
|
||||
if (value != null) {
|
||||
bytesCount += 3 + value.length * 3;
|
||||
}
|
||||
}
|
||||
return bytesCount;
|
||||
}
|
||||
|
||||
void _genericCacheModelSerialize(
|
||||
GenericCacheModel object,
|
||||
IsarWriter writer,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
writer.writeString(offsets[0], object.cacheData);
|
||||
}
|
||||
|
||||
GenericCacheModel _genericCacheModelDeserialize(
|
||||
Id id,
|
||||
IsarReader reader,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
final object = GenericCacheModel();
|
||||
object.cacheData = reader.readStringOrNull(offsets[0]);
|
||||
object.cacheKey = id;
|
||||
return object;
|
||||
}
|
||||
|
||||
P _genericCacheModelDeserializeProp<P>(
|
||||
IsarReader reader,
|
||||
int propertyId,
|
||||
int offset,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
switch (propertyId) {
|
||||
case 0:
|
||||
return (reader.readStringOrNull(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
}
|
||||
}
|
||||
|
||||
Id _genericCacheModelGetId(GenericCacheModel object) {
|
||||
return object.cacheKey ?? Isar.autoIncrement;
|
||||
}
|
||||
|
||||
List<IsarLinkBase<dynamic>> _genericCacheModelGetLinks(
|
||||
GenericCacheModel object) {
|
||||
return [];
|
||||
}
|
||||
|
||||
void _genericCacheModelAttach(
|
||||
IsarCollection<dynamic> col, Id id, GenericCacheModel object) {
|
||||
object.cacheKey = id;
|
||||
}
|
||||
|
||||
extension GenericCacheModelQueryWhereSort
|
||||
on QueryBuilder<GenericCacheModel, GenericCacheModel, QWhere> {
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterWhere>
|
||||
anyCacheKey() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(const IdWhereClause.any());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension GenericCacheModelQueryWhere
|
||||
on QueryBuilder<GenericCacheModel, GenericCacheModel, QWhereClause> {
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterWhereClause>
|
||||
cacheKeyEqualTo(Id cacheKey) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: cacheKey,
|
||||
upper: cacheKey,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterWhereClause>
|
||||
cacheKeyNotEqualTo(Id cacheKey) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: cacheKey, includeUpper: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: cacheKey, includeLower: false),
|
||||
);
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: cacheKey, includeLower: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: cacheKey, includeUpper: false),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterWhereClause>
|
||||
cacheKeyGreaterThan(Id cacheKey, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: cacheKey, includeLower: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterWhereClause>
|
||||
cacheKeyLessThan(Id cacheKey, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: cacheKey, includeUpper: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterWhereClause>
|
||||
cacheKeyBetween(
|
||||
Id lowerCacheKey,
|
||||
Id upperCacheKey, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: lowerCacheKey,
|
||||
includeLower: includeLower,
|
||||
upper: upperCacheKey,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension GenericCacheModelQueryFilter
|
||||
on QueryBuilder<GenericCacheModel, GenericCacheModel, QFilterCondition> {
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterFilterCondition>
|
||||
cacheDataIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
property: r'cacheData',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterFilterCondition>
|
||||
cacheDataIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||
property: r'cacheData',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterFilterCondition>
|
||||
cacheDataEqualTo(
|
||||
String? value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'cacheData',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterFilterCondition>
|
||||
cacheDataGreaterThan(
|
||||
String? value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'cacheData',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterFilterCondition>
|
||||
cacheDataLessThan(
|
||||
String? value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'cacheData',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterFilterCondition>
|
||||
cacheDataBetween(
|
||||
String? lower,
|
||||
String? upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'cacheData',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterFilterCondition>
|
||||
cacheDataStartsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.startsWith(
|
||||
property: r'cacheData',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterFilterCondition>
|
||||
cacheDataEndsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.endsWith(
|
||||
property: r'cacheData',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterFilterCondition>
|
||||
cacheDataContains(String value, {bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.contains(
|
||||
property: r'cacheData',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterFilterCondition>
|
||||
cacheDataMatches(String pattern, {bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.matches(
|
||||
property: r'cacheData',
|
||||
wildcard: pattern,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterFilterCondition>
|
||||
cacheDataIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'cacheData',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterFilterCondition>
|
||||
cacheDataIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
property: r'cacheData',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterFilterCondition>
|
||||
cacheKeyIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
property: r'cacheKey',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterFilterCondition>
|
||||
cacheKeyIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||
property: r'cacheKey',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterFilterCondition>
|
||||
cacheKeyEqualTo(Id? value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'cacheKey',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterFilterCondition>
|
||||
cacheKeyGreaterThan(
|
||||
Id? value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'cacheKey',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterFilterCondition>
|
||||
cacheKeyLessThan(
|
||||
Id? value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'cacheKey',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterFilterCondition>
|
||||
cacheKeyBetween(
|
||||
Id? lower,
|
||||
Id? upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'cacheKey',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension GenericCacheModelQueryObject
|
||||
on QueryBuilder<GenericCacheModel, GenericCacheModel, QFilterCondition> {}
|
||||
|
||||
extension GenericCacheModelQueryLinks
|
||||
on QueryBuilder<GenericCacheModel, GenericCacheModel, QFilterCondition> {}
|
||||
|
||||
extension GenericCacheModelQuerySortBy
|
||||
on QueryBuilder<GenericCacheModel, GenericCacheModel, QSortBy> {
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterSortBy>
|
||||
sortByCacheData() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'cacheData', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterSortBy>
|
||||
sortByCacheDataDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'cacheData', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension GenericCacheModelQuerySortThenBy
|
||||
on QueryBuilder<GenericCacheModel, GenericCacheModel, QSortThenBy> {
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterSortBy>
|
||||
thenByCacheData() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'cacheData', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterSortBy>
|
||||
thenByCacheDataDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'cacheData', Sort.desc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterSortBy>
|
||||
thenByCacheKey() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'cacheKey', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QAfterSortBy>
|
||||
thenByCacheKeyDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'cacheKey', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension GenericCacheModelQueryWhereDistinct
|
||||
on QueryBuilder<GenericCacheModel, GenericCacheModel, QDistinct> {
|
||||
QueryBuilder<GenericCacheModel, GenericCacheModel, QDistinct>
|
||||
distinctByCacheData({bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'cacheData', caseSensitive: caseSensitive);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension GenericCacheModelQueryProperty
|
||||
on QueryBuilder<GenericCacheModel, GenericCacheModel, QQueryProperty> {
|
||||
QueryBuilder<GenericCacheModel, int, QQueryOperations> cacheKeyProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'cacheKey');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<GenericCacheModel, String?, QQueryOperations>
|
||||
cacheDataProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'cacheData');
|
||||
});
|
||||
}
|
||||
}
|
||||
29
firka/lib/helpers/db/models/homework_cache_model.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
import '../../debug_helper.dart';
|
||||
import '../util.dart';
|
||||
|
||||
part 'homework_cache_model.g.dart';
|
||||
|
||||
@collection
|
||||
class HomeworkCacheModel extends DatedCacheEntry {
|
||||
HomeworkCacheModel();
|
||||
}
|
||||
|
||||
Future<void> resetOldHomeworkCache(Isar isar) async {
|
||||
var now = timeNow();
|
||||
var weeks = await isar.homeworkCacheModels.where().findAll();
|
||||
var weeksToRemove = List<Id>.empty(growable: true);
|
||||
|
||||
for (var week in weeks) {
|
||||
var date = getDate(week.cacheKey!);
|
||||
|
||||
if (date.millisecondsSinceEpoch <
|
||||
now.subtract(Duration(days: 120)).millisecondsSinceEpoch) {
|
||||
weeksToRemove.add(week.cacheKey!);
|
||||
}
|
||||
}
|
||||
await isar.writeTxn(() async {
|
||||
await isar.homeworkCacheModels.deleteAll(weeksToRemove);
|
||||
});
|
||||
}
|
||||
562
firka/lib/helpers/db/models/homework_cache_model.g.dart
Normal file
@@ -0,0 +1,562 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'homework_cache_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// IsarCollectionGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
|
||||
|
||||
extension GetHomeworkCacheModelCollection on Isar {
|
||||
IsarCollection<HomeworkCacheModel> get homeworkCacheModels =>
|
||||
this.collection();
|
||||
}
|
||||
|
||||
const HomeworkCacheModelSchema = CollectionSchema(
|
||||
name: r'HomeworkCacheModel',
|
||||
id: -356692531669197690,
|
||||
properties: {
|
||||
r'values': PropertySchema(
|
||||
id: 0,
|
||||
name: r'values',
|
||||
type: IsarType.stringList,
|
||||
)
|
||||
},
|
||||
estimateSize: _homeworkCacheModelEstimateSize,
|
||||
serialize: _homeworkCacheModelSerialize,
|
||||
deserialize: _homeworkCacheModelDeserialize,
|
||||
deserializeProp: _homeworkCacheModelDeserializeProp,
|
||||
idName: r'cacheKey',
|
||||
indexes: {},
|
||||
links: {},
|
||||
embeddedSchemas: {},
|
||||
getId: _homeworkCacheModelGetId,
|
||||
getLinks: _homeworkCacheModelGetLinks,
|
||||
attach: _homeworkCacheModelAttach,
|
||||
version: '3.1.0+1',
|
||||
);
|
||||
|
||||
int _homeworkCacheModelEstimateSize(
|
||||
HomeworkCacheModel object,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
var bytesCount = offsets.last;
|
||||
{
|
||||
final list = object.values;
|
||||
if (list != null) {
|
||||
bytesCount += 3 + list.length * 3;
|
||||
{
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
final value = list[i];
|
||||
bytesCount += value.length * 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return bytesCount;
|
||||
}
|
||||
|
||||
void _homeworkCacheModelSerialize(
|
||||
HomeworkCacheModel object,
|
||||
IsarWriter writer,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
writer.writeStringList(offsets[0], object.values);
|
||||
}
|
||||
|
||||
HomeworkCacheModel _homeworkCacheModelDeserialize(
|
||||
Id id,
|
||||
IsarReader reader,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
final object = HomeworkCacheModel();
|
||||
object.cacheKey = id;
|
||||
object.values = reader.readStringList(offsets[0]);
|
||||
return object;
|
||||
}
|
||||
|
||||
P _homeworkCacheModelDeserializeProp<P>(
|
||||
IsarReader reader,
|
||||
int propertyId,
|
||||
int offset,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
switch (propertyId) {
|
||||
case 0:
|
||||
return (reader.readStringList(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
}
|
||||
}
|
||||
|
||||
Id _homeworkCacheModelGetId(HomeworkCacheModel object) {
|
||||
return object.cacheKey ?? Isar.autoIncrement;
|
||||
}
|
||||
|
||||
List<IsarLinkBase<dynamic>> _homeworkCacheModelGetLinks(
|
||||
HomeworkCacheModel object) {
|
||||
return [];
|
||||
}
|
||||
|
||||
void _homeworkCacheModelAttach(
|
||||
IsarCollection<dynamic> col, Id id, HomeworkCacheModel object) {
|
||||
object.cacheKey = id;
|
||||
}
|
||||
|
||||
extension HomeworkCacheModelQueryWhereSort
|
||||
on QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QWhere> {
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterWhere>
|
||||
anyCacheKey() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(const IdWhereClause.any());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeworkCacheModelQueryWhere
|
||||
on QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QWhereClause> {
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterWhereClause>
|
||||
cacheKeyEqualTo(Id cacheKey) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: cacheKey,
|
||||
upper: cacheKey,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterWhereClause>
|
||||
cacheKeyNotEqualTo(Id cacheKey) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: cacheKey, includeUpper: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: cacheKey, includeLower: false),
|
||||
);
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: cacheKey, includeLower: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: cacheKey, includeUpper: false),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterWhereClause>
|
||||
cacheKeyGreaterThan(Id cacheKey, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: cacheKey, includeLower: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterWhereClause>
|
||||
cacheKeyLessThan(Id cacheKey, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: cacheKey, includeUpper: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterWhereClause>
|
||||
cacheKeyBetween(
|
||||
Id lowerCacheKey,
|
||||
Id upperCacheKey, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: lowerCacheKey,
|
||||
includeLower: includeLower,
|
||||
upper: upperCacheKey,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeworkCacheModelQueryFilter
|
||||
on QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QFilterCondition> {
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
cacheKeyIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
property: r'cacheKey',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
cacheKeyIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||
property: r'cacheKey',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
cacheKeyEqualTo(Id? value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'cacheKey',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
cacheKeyGreaterThan(
|
||||
Id? value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'cacheKey',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
cacheKeyLessThan(
|
||||
Id? value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'cacheKey',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
cacheKeyBetween(
|
||||
Id? lower,
|
||||
Id? upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'cacheKey',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
valuesIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
property: r'values',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
valuesIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||
property: r'values',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
valuesElementEqualTo(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'values',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
valuesElementGreaterThan(
|
||||
String value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'values',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
valuesElementLessThan(
|
||||
String value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'values',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
valuesElementBetween(
|
||||
String lower,
|
||||
String upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'values',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
valuesElementStartsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.startsWith(
|
||||
property: r'values',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
valuesElementEndsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.endsWith(
|
||||
property: r'values',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
valuesElementContains(String value, {bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.contains(
|
||||
property: r'values',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
valuesElementMatches(String pattern, {bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.matches(
|
||||
property: r'values',
|
||||
wildcard: pattern,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
valuesElementIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'values',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
valuesElementIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
property: r'values',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
valuesLengthEqualTo(int length) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'values',
|
||||
length,
|
||||
true,
|
||||
length,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
valuesIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'values',
|
||||
0,
|
||||
true,
|
||||
0,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
valuesIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'values',
|
||||
0,
|
||||
false,
|
||||
999999,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
valuesLengthLessThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'values',
|
||||
0,
|
||||
true,
|
||||
length,
|
||||
include,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
valuesLengthGreaterThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'values',
|
||||
length,
|
||||
include,
|
||||
999999,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterFilterCondition>
|
||||
valuesLengthBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'values',
|
||||
lower,
|
||||
includeLower,
|
||||
upper,
|
||||
includeUpper,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeworkCacheModelQueryObject
|
||||
on QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QFilterCondition> {}
|
||||
|
||||
extension HomeworkCacheModelQueryLinks
|
||||
on QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QFilterCondition> {}
|
||||
|
||||
extension HomeworkCacheModelQuerySortBy
|
||||
on QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QSortBy> {}
|
||||
|
||||
extension HomeworkCacheModelQuerySortThenBy
|
||||
on QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QSortThenBy> {
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterSortBy>
|
||||
thenByCacheKey() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'cacheKey', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QAfterSortBy>
|
||||
thenByCacheKeyDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'cacheKey', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeworkCacheModelQueryWhereDistinct
|
||||
on QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QDistinct> {
|
||||
QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QDistinct>
|
||||
distinctByValues() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'values');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension HomeworkCacheModelQueryProperty
|
||||
on QueryBuilder<HomeworkCacheModel, HomeworkCacheModel, QQueryProperty> {
|
||||
QueryBuilder<HomeworkCacheModel, int, QQueryOperations> cacheKeyProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'cacheKey');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<HomeworkCacheModel, List<String>?, QQueryOperations>
|
||||
valuesProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'values');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:isar_community/isar.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
import 'package:firka/core/debug_helper.dart';
|
||||
import 'package:firka/data/util.dart';
|
||||
import '../../debug_helper.dart';
|
||||
import '../util.dart';
|
||||
|
||||
part 'timetable_cache_model.g.dart';
|
||||
|
||||
562
firka/lib/helpers/db/models/timetable_cache_model.g.dart
Normal file
@@ -0,0 +1,562 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'timetable_cache_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// IsarCollectionGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
|
||||
|
||||
extension GetTimetableCacheModelCollection on Isar {
|
||||
IsarCollection<TimetableCacheModel> get timetableCacheModels =>
|
||||
this.collection();
|
||||
}
|
||||
|
||||
const TimetableCacheModelSchema = CollectionSchema(
|
||||
name: r'TimetableCacheModel',
|
||||
id: -8626340955125680275,
|
||||
properties: {
|
||||
r'values': PropertySchema(
|
||||
id: 0,
|
||||
name: r'values',
|
||||
type: IsarType.stringList,
|
||||
)
|
||||
},
|
||||
estimateSize: _timetableCacheModelEstimateSize,
|
||||
serialize: _timetableCacheModelSerialize,
|
||||
deserialize: _timetableCacheModelDeserialize,
|
||||
deserializeProp: _timetableCacheModelDeserializeProp,
|
||||
idName: r'cacheKey',
|
||||
indexes: {},
|
||||
links: {},
|
||||
embeddedSchemas: {},
|
||||
getId: _timetableCacheModelGetId,
|
||||
getLinks: _timetableCacheModelGetLinks,
|
||||
attach: _timetableCacheModelAttach,
|
||||
version: '3.1.0+1',
|
||||
);
|
||||
|
||||
int _timetableCacheModelEstimateSize(
|
||||
TimetableCacheModel object,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
var bytesCount = offsets.last;
|
||||
{
|
||||
final list = object.values;
|
||||
if (list != null) {
|
||||
bytesCount += 3 + list.length * 3;
|
||||
{
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
final value = list[i];
|
||||
bytesCount += value.length * 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return bytesCount;
|
||||
}
|
||||
|
||||
void _timetableCacheModelSerialize(
|
||||
TimetableCacheModel object,
|
||||
IsarWriter writer,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
writer.writeStringList(offsets[0], object.values);
|
||||
}
|
||||
|
||||
TimetableCacheModel _timetableCacheModelDeserialize(
|
||||
Id id,
|
||||
IsarReader reader,
|
||||
List<int> offsets,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
final object = TimetableCacheModel();
|
||||
object.cacheKey = id;
|
||||
object.values = reader.readStringList(offsets[0]);
|
||||
return object;
|
||||
}
|
||||
|
||||
P _timetableCacheModelDeserializeProp<P>(
|
||||
IsarReader reader,
|
||||
int propertyId,
|
||||
int offset,
|
||||
Map<Type, List<int>> allOffsets,
|
||||
) {
|
||||
switch (propertyId) {
|
||||
case 0:
|
||||
return (reader.readStringList(offset)) as P;
|
||||
default:
|
||||
throw IsarError('Unknown property with id $propertyId');
|
||||
}
|
||||
}
|
||||
|
||||
Id _timetableCacheModelGetId(TimetableCacheModel object) {
|
||||
return object.cacheKey ?? Isar.autoIncrement;
|
||||
}
|
||||
|
||||
List<IsarLinkBase<dynamic>> _timetableCacheModelGetLinks(
|
||||
TimetableCacheModel object) {
|
||||
return [];
|
||||
}
|
||||
|
||||
void _timetableCacheModelAttach(
|
||||
IsarCollection<dynamic> col, Id id, TimetableCacheModel object) {
|
||||
object.cacheKey = id;
|
||||
}
|
||||
|
||||
extension TimetableCacheModelQueryWhereSort
|
||||
on QueryBuilder<TimetableCacheModel, TimetableCacheModel, QWhere> {
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterWhere>
|
||||
anyCacheKey() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(const IdWhereClause.any());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension TimetableCacheModelQueryWhere
|
||||
on QueryBuilder<TimetableCacheModel, TimetableCacheModel, QWhereClause> {
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterWhereClause>
|
||||
cacheKeyEqualTo(Id cacheKey) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: cacheKey,
|
||||
upper: cacheKey,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterWhereClause>
|
||||
cacheKeyNotEqualTo(Id cacheKey) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
if (query.whereSort == Sort.asc) {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: cacheKey, includeUpper: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: cacheKey, includeLower: false),
|
||||
);
|
||||
} else {
|
||||
return query
|
||||
.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: cacheKey, includeLower: false),
|
||||
)
|
||||
.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: cacheKey, includeUpper: false),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterWhereClause>
|
||||
cacheKeyGreaterThan(Id cacheKey, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.greaterThan(lower: cacheKey, includeLower: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterWhereClause>
|
||||
cacheKeyLessThan(Id cacheKey, {bool include = false}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(
|
||||
IdWhereClause.lessThan(upper: cacheKey, includeUpper: include),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterWhereClause>
|
||||
cacheKeyBetween(
|
||||
Id lowerCacheKey,
|
||||
Id upperCacheKey, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addWhereClause(IdWhereClause.between(
|
||||
lower: lowerCacheKey,
|
||||
includeLower: includeLower,
|
||||
upper: upperCacheKey,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension TimetableCacheModelQueryFilter on QueryBuilder<TimetableCacheModel,
|
||||
TimetableCacheModel, QFilterCondition> {
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
cacheKeyIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
property: r'cacheKey',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
cacheKeyIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||
property: r'cacheKey',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
cacheKeyEqualTo(Id? value) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'cacheKey',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
cacheKeyGreaterThan(
|
||||
Id? value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'cacheKey',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
cacheKeyLessThan(
|
||||
Id? value, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'cacheKey',
|
||||
value: value,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
cacheKeyBetween(
|
||||
Id? lower,
|
||||
Id? upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'cacheKey',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
valuesIsNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNull(
|
||||
property: r'values',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
valuesIsNotNull() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(const FilterCondition.isNotNull(
|
||||
property: r'values',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
valuesElementEqualTo(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'values',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
valuesElementGreaterThan(
|
||||
String value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
include: include,
|
||||
property: r'values',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
valuesElementLessThan(
|
||||
String value, {
|
||||
bool include = false,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.lessThan(
|
||||
include: include,
|
||||
property: r'values',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
valuesElementBetween(
|
||||
String lower,
|
||||
String upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.between(
|
||||
property: r'values',
|
||||
lower: lower,
|
||||
includeLower: includeLower,
|
||||
upper: upper,
|
||||
includeUpper: includeUpper,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
valuesElementStartsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.startsWith(
|
||||
property: r'values',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
valuesElementEndsWith(
|
||||
String value, {
|
||||
bool caseSensitive = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.endsWith(
|
||||
property: r'values',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
valuesElementContains(String value, {bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.contains(
|
||||
property: r'values',
|
||||
value: value,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
valuesElementMatches(String pattern, {bool caseSensitive = true}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.matches(
|
||||
property: r'values',
|
||||
wildcard: pattern,
|
||||
caseSensitive: caseSensitive,
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
valuesElementIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.equalTo(
|
||||
property: r'values',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
valuesElementIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addFilterCondition(FilterCondition.greaterThan(
|
||||
property: r'values',
|
||||
value: '',
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
valuesLengthEqualTo(int length) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'values',
|
||||
length,
|
||||
true,
|
||||
length,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
valuesIsEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'values',
|
||||
0,
|
||||
true,
|
||||
0,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
valuesIsNotEmpty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'values',
|
||||
0,
|
||||
false,
|
||||
999999,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
valuesLengthLessThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'values',
|
||||
0,
|
||||
true,
|
||||
length,
|
||||
include,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
valuesLengthGreaterThan(
|
||||
int length, {
|
||||
bool include = false,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'values',
|
||||
length,
|
||||
include,
|
||||
999999,
|
||||
true,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterFilterCondition>
|
||||
valuesLengthBetween(
|
||||
int lower,
|
||||
int upper, {
|
||||
bool includeLower = true,
|
||||
bool includeUpper = true,
|
||||
}) {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.listLength(
|
||||
r'values',
|
||||
lower,
|
||||
includeLower,
|
||||
upper,
|
||||
includeUpper,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension TimetableCacheModelQueryObject on QueryBuilder<TimetableCacheModel,
|
||||
TimetableCacheModel, QFilterCondition> {}
|
||||
|
||||
extension TimetableCacheModelQueryLinks on QueryBuilder<TimetableCacheModel,
|
||||
TimetableCacheModel, QFilterCondition> {}
|
||||
|
||||
extension TimetableCacheModelQuerySortBy
|
||||
on QueryBuilder<TimetableCacheModel, TimetableCacheModel, QSortBy> {}
|
||||
|
||||
extension TimetableCacheModelQuerySortThenBy
|
||||
on QueryBuilder<TimetableCacheModel, TimetableCacheModel, QSortThenBy> {
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterSortBy>
|
||||
thenByCacheKey() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'cacheKey', Sort.asc);
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QAfterSortBy>
|
||||
thenByCacheKeyDesc() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addSortBy(r'cacheKey', Sort.desc);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension TimetableCacheModelQueryWhereDistinct
|
||||
on QueryBuilder<TimetableCacheModel, TimetableCacheModel, QDistinct> {
|
||||
QueryBuilder<TimetableCacheModel, TimetableCacheModel, QDistinct>
|
||||
distinctByValues() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addDistinctBy(r'values');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
extension TimetableCacheModelQueryProperty
|
||||
on QueryBuilder<TimetableCacheModel, TimetableCacheModel, QQueryProperty> {
|
||||
QueryBuilder<TimetableCacheModel, int, QQueryOperations> cacheKeyProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'cacheKey');
|
||||
});
|
||||
}
|
||||
|
||||
QueryBuilder<TimetableCacheModel, List<String>?, QQueryOperations>
|
||||
valuesProperty() {
|
||||
return QueryBuilder.apply(this, (query) {
|
||||
return query.addPropertyName(r'values');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,11 @@ import 'dart:convert';
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
|
||||
import 'package:kreta_api/kreta_api.dart';
|
||||
import 'package:firka/core/debug_helper.dart';
|
||||
import 'package:firka/core/extensions.dart';
|
||||
import 'package:isar_community/isar.dart';
|
||||
import 'package:firka/helpers/api/resp/token_grant.dart';
|
||||
import 'package:firka/helpers/extensions.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
import '../../debug_helper.dart';
|
||||
|
||||
part 'token_model.g.dart';
|
||||
|
||||
@@ -63,8 +64,7 @@ class TokenModel {
|
||||
// you would expect all usernames to be numeric
|
||||
// and for them be the student's student id, but NO
|
||||
final hash = sha256.convert(utf8.encode(username));
|
||||
final value =
|
||||
((hash.bytes[0] << 24) |
|
||||
final value = ((hash.bytes[0] << 24) |
|
||||
(hash.bytes[1] << 16) |
|
||||
(hash.bytes[2] << 8) |
|
||||
(hash.bytes[3])) >>>
|
||||
1651
firka/lib/helpers/db/models/token_model.g.dart
Normal file
@@ -1,9 +1,9 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:isar_community/isar.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
import 'package:firka/core/debug_helper.dart';
|
||||
import '../debug_helper.dart';
|
||||
|
||||
class DatedCacheEntry {
|
||||
Id? cacheKey;
|
||||
@@ -1,17 +1,18 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:firka/api/client/kreta_client.dart';
|
||||
import 'package:kreta_api/kreta_api.dart';
|
||||
import 'package:firka/core/debug_helper.dart';
|
||||
import 'package:firka/data/ios_widget_helper.dart';
|
||||
import 'package:firka/core/settings.dart';
|
||||
import 'package:firka/helpers/api/client/kreta_client.dart';
|
||||
import 'package:firka/helpers/api/model/grade.dart';
|
||||
import 'package:firka/helpers/api/model/timetable.dart';
|
||||
import 'package:firka/helpers/db/ios_widget_helper.dart';
|
||||
import 'package:firka/helpers/debug_helper.dart';
|
||||
import 'package:firka/helpers/settings.dart';
|
||||
import 'package:firka/main.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import 'package:firka/ui/theme/style.dart';
|
||||
import '../../ui/model/style.dart';
|
||||
|
||||
class WidgetCacheHelper {
|
||||
static Map<String, dynamic> toJson(FirkaStyle style, List<Lesson> timetable) {
|
||||
@@ -21,63 +22,42 @@ class WidgetCacheHelper {
|
||||
timetableJson.add(lesson.toJson());
|
||||
}
|
||||
|
||||
return {'colors': _colorsMap(style), 'timetable': timetableJson};
|
||||
}
|
||||
|
||||
static Map<String, dynamic> toAndroidWidgetJson(
|
||||
FirkaStyle style,
|
||||
List<Lesson> timetable,
|
||||
) {
|
||||
final timetableJson = <Map<String, dynamic>>[];
|
||||
for (var lesson in timetable) {
|
||||
timetableJson.add({
|
||||
'Nev': lesson.name,
|
||||
'KezdetIdopont': lesson.start.toUtc().toIso8601String(),
|
||||
'VegIdopont': lesson.end.toUtc().toIso8601String(),
|
||||
'Oraszam': lesson.lessonNumber,
|
||||
'TeremNeve': lesson.roomName,
|
||||
'HelyettesTanarNeve': lesson.substituteTeacher,
|
||||
});
|
||||
}
|
||||
return {'colors': _colorsMap(style), 'timetable': timetableJson};
|
||||
}
|
||||
|
||||
static Map<String, dynamic> _colorsMap(FirkaStyle style) {
|
||||
return {
|
||||
'background': style.colors.background.toARGB32(),
|
||||
'backgroundAmoled': style.colors.backgroundAmoled.toARGB32(),
|
||||
'background0p': style.colors.background0p.toARGB32(),
|
||||
'success': style.colors.success.toARGB32(),
|
||||
'textPrimary': style.colors.textPrimary.toARGB32(),
|
||||
'textSecondary': style.colors.textSecondary.toARGB32(),
|
||||
'textTertiary': style.colors.textTertiary.toARGB32(),
|
||||
'card': style.colors.card.toARGB32(),
|
||||
'cardTranslucent': style.colors.cardTranslucent.toARGB32(),
|
||||
'buttonSecondaryFill': style.colors.buttonSecondaryFill.toARGB32(),
|
||||
'accent': style.colors.accent.toARGB32(),
|
||||
'secondary': style.colors.secondary.toARGB32(),
|
||||
'shadowColor': style.colors.shadowColor.toARGB32(),
|
||||
'a15p': style.colors.a15p.toARGB32(),
|
||||
'warningAccent': style.colors.warningAccent.toARGB32(),
|
||||
'warningText': style.colors.warningText.toARGB32(),
|
||||
'warning15p': style.colors.warning15p.toARGB32(),
|
||||
'warningCard': style.colors.warningCard.toARGB32(),
|
||||
'errorAccent': style.colors.errorAccent.toARGB32(),
|
||||
'errorText': style.colors.errorText.toARGB32(),
|
||||
'error15p': style.colors.error15p.toARGB32(),
|
||||
'errorCard': style.colors.errorCard.toARGB32(),
|
||||
'grade5': style.colors.grade5.toARGB32(),
|
||||
'grade4': style.colors.grade4.toARGB32(),
|
||||
'grade3': style.colors.grade3.toARGB32(),
|
||||
'grade2': style.colors.grade2.toARGB32(),
|
||||
'grade1': style.colors.grade1.toARGB32(),
|
||||
'colors': {
|
||||
'background': style.colors.background.toARGB32(),
|
||||
'backgroundAmoled': style.colors.backgroundAmoled.toARGB32(),
|
||||
'background0p': style.colors.background0p.toARGB32(),
|
||||
'success': style.colors.success.toARGB32(),
|
||||
'textPrimary': style.colors.textPrimary.toARGB32(),
|
||||
'textSecondary': style.colors.textSecondary.toARGB32(),
|
||||
'textTertiary': style.colors.textTertiary.toARGB32(),
|
||||
'card': style.colors.card.toARGB32(),
|
||||
'cardTranslucent': style.colors.cardTranslucent.toARGB32(),
|
||||
'buttonSecondaryFill': style.colors.buttonSecondaryFill.toARGB32(),
|
||||
'accent': style.colors.accent.toARGB32(),
|
||||
'secondary': style.colors.secondary.toARGB32(),
|
||||
'shadowColor': style.colors.shadowColor.toARGB32(),
|
||||
'a15p': style.colors.a15p.toARGB32(),
|
||||
'warningAccent': style.colors.warningAccent.toARGB32(),
|
||||
'warningText': style.colors.warningText.toARGB32(),
|
||||
'warning15p': style.colors.warning15p.toARGB32(),
|
||||
'warningCard': style.colors.warningCard.toARGB32(),
|
||||
'errorAccent': style.colors.errorAccent.toARGB32(),
|
||||
'errorText': style.colors.errorText.toARGB32(),
|
||||
'error15p': style.colors.error15p.toARGB32(),
|
||||
'errorCard': style.colors.errorCard.toARGB32(),
|
||||
'grade5': style.colors.grade5.toARGB32(),
|
||||
'grade4': style.colors.grade4.toARGB32(),
|
||||
'grade3': style.colors.grade3.toARGB32(),
|
||||
'grade2': style.colors.grade2.toARGB32(),
|
||||
'grade1': style.colors.grade1.toARGB32(),
|
||||
},
|
||||
'timetable': timetableJson,
|
||||
};
|
||||
}
|
||||
|
||||
static Future<void> updateWidgetCache(
|
||||
FirkaStyle style,
|
||||
KretaClient client,
|
||||
) async {
|
||||
FirkaStyle style, KretaClient client) async {
|
||||
final dataDir = await getApplicationDocumentsDirectory();
|
||||
|
||||
final now = timeNow();
|
||||
@@ -89,40 +69,14 @@ class WidgetCacheHelper {
|
||||
final widgetFile = File(p.join(dataDir.path, "widget_state.json"));
|
||||
|
||||
if (lessons.response != null) {
|
||||
debugPrint(
|
||||
'Android widget cache: ${lessons.response!.length} lessons (cached: ${lessons.cached})',
|
||||
);
|
||||
debugPrint('Android widget cache: ${lessons.response!.length} lessons (cached: ${lessons.cached})');
|
||||
widgetFile.writeAsString(
|
||||
jsonEncode(WidgetCacheHelper.toJson(style, lessons.response!)),
|
||||
);
|
||||
jsonEncode(WidgetCacheHelper.toJson(style, lessons.response!)));
|
||||
} else {
|
||||
debugPrint('Android widget cache: No lessons to cache');
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> generateWidgetStateForDate(
|
||||
DateTime date,
|
||||
FirkaStyle style,
|
||||
KretaClient client,
|
||||
) async {
|
||||
final dataDir = await getApplicationDocumentsDirectory();
|
||||
final dayStart = DateTime(date.year, date.month, date.day);
|
||||
final dayEnd = dayStart.add(Duration(hours: 23, minutes: 59));
|
||||
final lessons = await client.getTimeTable(
|
||||
dayStart,
|
||||
dayEnd,
|
||||
forceCache: false,
|
||||
);
|
||||
final dayLessons = lessons.response ?? [];
|
||||
|
||||
final json = toJson(style, dayLessons);
|
||||
json['displayDate'] =
|
||||
'${date.year.toString().padLeft(4, '0')}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||
|
||||
final widgetFile = File(p.join(dataDir.path, "widget_state.json"));
|
||||
await widgetFile.writeAsString(jsonEncode(json));
|
||||
}
|
||||
|
||||
static Future<void> updateIOSWidgets({
|
||||
required String locale,
|
||||
required String theme,
|
||||
@@ -151,17 +105,13 @@ class WidgetCacheHelper {
|
||||
|
||||
/// Comprehensive iOS widget refresh that collects all necessary data
|
||||
/// Call this on: app open, user switch, data refresh
|
||||
static Future<void> refreshIOSWidgets(
|
||||
KretaClient client,
|
||||
SettingsStore settings,
|
||||
) async {
|
||||
static Future<void> refreshIOSWidgets(KretaClient client, SettingsStore settings) async {
|
||||
if (!Platform.isIOS) return;
|
||||
|
||||
try {
|
||||
final langIndex =
|
||||
(settings.group("settings").subGroup("application")["language"]
|
||||
as SettingsItemsRadio)
|
||||
.activeIndex;
|
||||
final langIndex = (settings.group("settings").subGroup("application")["language"]
|
||||
as SettingsItemsRadio)
|
||||
.activeIndex;
|
||||
String locale;
|
||||
switch (langIndex) {
|
||||
case 1:
|
||||
@@ -177,10 +127,9 @@ class WidgetCacheHelper {
|
||||
locale = 'hu';
|
||||
}
|
||||
|
||||
final themeIndex =
|
||||
(settings.group("settings").subGroup("customization")["theme"]
|
||||
as SettingsItemsRadio)
|
||||
.activeIndex;
|
||||
final themeIndex = (settings.group("settings").subGroup("customization")["theme"]
|
||||
as SettingsItemsRadio)
|
||||
.activeIndex;
|
||||
String theme;
|
||||
switch (themeIndex) {
|
||||
case 1:
|
||||
@@ -190,11 +139,7 @@ class WidgetCacheHelper {
|
||||
theme = 'dark';
|
||||
break;
|
||||
default:
|
||||
theme =
|
||||
SchedulerBinding.instance.platformDispatcher.platformBrightness ==
|
||||
Brightness.light
|
||||
? 'light'
|
||||
: 'dark';
|
||||
theme = isLightMode.value ? 'light' : 'dark';
|
||||
}
|
||||
|
||||
final now = timeNow();
|
||||
@@ -215,9 +160,7 @@ class WidgetCacheHelper {
|
||||
final todayLessons = todayResponse.response ?? [];
|
||||
final tomorrowLessons = tomorrowResponse.response ?? [];
|
||||
|
||||
debugPrint(
|
||||
'iOS widget refresh: ${todayLessons.length} today lessons, ${tomorrowLessons.length} tomorrow lessons',
|
||||
);
|
||||
debugPrint('iOS widget refresh: ${todayLessons.length} today lessons, ${tomorrowLessons.length} tomorrow lessons');
|
||||
|
||||
List<Lesson> nextSchoolDayLessons = [];
|
||||
DateTime? nextSchoolDayDate;
|
||||
@@ -233,9 +176,7 @@ class WidgetCacheHelper {
|
||||
if (dayLessons.isNotEmpty) {
|
||||
nextSchoolDayLessons = dayLessons;
|
||||
nextSchoolDayDate = dayMidnight;
|
||||
debugPrint(
|
||||
'iOS widget: Next school day found $i days ahead with ${dayLessons.length} lessons',
|
||||
);
|
||||
debugPrint('iOS widget: Next school day found ${i} days ahead with ${dayLessons.length} lessons');
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -244,9 +185,7 @@ class WidgetCacheHelper {
|
||||
final gradesResponse = await client.getGrades(forceCache: false);
|
||||
final grades = gradesResponse.response ?? [];
|
||||
|
||||
debugPrint(
|
||||
'iOS widget refresh: ${grades.length} grades fetched (cached: ${gradesResponse.cached})',
|
||||
);
|
||||
debugPrint('iOS widget refresh: ${grades.length} grades fetched (cached: ${gradesResponse.cached})');
|
||||
|
||||
final Map<String, double> subjectAverages = {};
|
||||
final Set<String> subjectUids = {};
|
||||
@@ -259,9 +198,7 @@ class WidgetCacheHelper {
|
||||
int validSubjectCount = 0;
|
||||
|
||||
for (var uid in subjectUids) {
|
||||
final subjectGrades = grades
|
||||
.where((g) => g.subject.uid == uid)
|
||||
.toList();
|
||||
final subjectGrades = grades.where((g) => g.subject.uid == uid).toList();
|
||||
final avg = _calculateWeightedAverage(subjectGrades);
|
||||
if (!avg.isNaN && avg > 0) {
|
||||
subjectAverages[uid] = avg;
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'package:kreta_api/kreta_api.dart';
|
||||
import 'package:firka/core/debug_helper.dart';
|
||||
import 'package:firka/l10n/app_localizations.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import 'api/model/timetable.dart';
|
||||
import 'debug_helper.dart';
|
||||
|
||||
extension TimetableExtension on Iterable<Lesson> {
|
||||
List<Lesson> getAllSeqs(Lesson reference) {
|
||||
@@ -12,43 +12,29 @@ extension TimetableExtension on Iterable<Lesson> {
|
||||
if (lesson.lessonNumber == null) continue;
|
||||
|
||||
if (lessons.firstWhereOrNull(
|
||||
(lesson2) => lesson.lessonNumber == lesson2.lessonNumber,
|
||||
) ==
|
||||
(lesson2) => lesson.lessonNumber == lesson2.lessonNumber) ==
|
||||
null) {
|
||||
final ref = reference.start;
|
||||
final newStart = DateTime(
|
||||
ref.year,
|
||||
ref.month,
|
||||
ref.day,
|
||||
lesson.start.hour,
|
||||
lesson.start.minute,
|
||||
lesson.start.second,
|
||||
);
|
||||
final newEnd = DateTime(
|
||||
ref.year,
|
||||
ref.month,
|
||||
ref.day,
|
||||
lesson.end.hour,
|
||||
lesson.end.minute,
|
||||
lesson.end.second,
|
||||
);
|
||||
final newStart = DateTime(ref.year, ref.month, ref.day,
|
||||
lesson.start.hour, lesson.start.minute, lesson.start.second);
|
||||
final newEnd = DateTime(ref.year, ref.month, ref.day, lesson.end.hour,
|
||||
lesson.end.minute, lesson.end.second);
|
||||
final lessonCopy = Lesson(
|
||||
uid: lesson.uid,
|
||||
date: lesson.date,
|
||||
start: newStart,
|
||||
end: newEnd,
|
||||
name: lesson.name,
|
||||
type: lesson.type,
|
||||
state: lesson.state,
|
||||
canStudentEditHomework: lesson.canStudentEditHomework,
|
||||
isHomeworkComplete: lesson.isHomeworkComplete,
|
||||
attachments: lesson.attachments,
|
||||
lessonNumber: lesson.lessonNumber,
|
||||
isDigitalLesson: lesson.isDigitalLesson,
|
||||
digitalSupportDeviceTypeList: lesson.digitalSupportDeviceTypeList,
|
||||
createdAt: lesson.createdAt,
|
||||
lastModifiedAt: lesson.lastModifiedAt,
|
||||
);
|
||||
uid: lesson.uid,
|
||||
date: lesson.date,
|
||||
start: newStart,
|
||||
end: newEnd,
|
||||
name: lesson.name,
|
||||
type: lesson.type,
|
||||
state: lesson.state,
|
||||
canStudentEditHomework: lesson.canStudentEditHomework,
|
||||
isHomeworkComplete: lesson.isHomeworkComplete,
|
||||
attachments: lesson.attachments,
|
||||
lessonNumber: lesson.lessonNumber,
|
||||
isDigitalLesson: lesson.isDigitalLesson,
|
||||
digitalSupportDeviceTypeList: lesson.digitalSupportDeviceTypeList,
|
||||
createdAt: lesson.createdAt,
|
||||
lastModifiedAt: lesson.lastModifiedAt);
|
||||
lessons.add(lessonCopy);
|
||||
}
|
||||
}
|
||||
@@ -99,7 +85,7 @@ enum FormatMode {
|
||||
yyyymmddwedd,
|
||||
yyyymmmm,
|
||||
yyyymmdd,
|
||||
yyyymmddhhmmss,
|
||||
yyyymmddhhmmss
|
||||
}
|
||||
|
||||
enum Cycle { morning, day, afternoon, night }
|
||||
@@ -119,12 +105,7 @@ extension DateExtension on DateTime {
|
||||
switch (mode) {
|
||||
case FormatMode.grades:
|
||||
if (isBefore(yesterdayLim)) {
|
||||
final month = DateFormat(
|
||||
'MMMM',
|
||||
l10n.localeName,
|
||||
).format(this).firstUpper();
|
||||
final day = DateFormat('d', l10n.localeName).format(this);
|
||||
return "$month $day";
|
||||
return format(l10n, FormatMode.yearly);
|
||||
}
|
||||
if (isAfter(yesterdayLim) && isBefore(today)) {
|
||||
return l10n.yesterday;
|
||||
@@ -142,23 +123,14 @@ extension DateExtension on DateTime {
|
||||
case FormatMode.hmm:
|
||||
return DateFormat('H:mm', l10n.localeName).format(this);
|
||||
case FormatMode.welcome:
|
||||
final dayName = DateFormat(
|
||||
'EEEE',
|
||||
l10n.localeName,
|
||||
).format(this).firstUpper();
|
||||
final monthAbbr = DateFormat(
|
||||
'MMM',
|
||||
l10n.localeName,
|
||||
).format(this).firstUpper();
|
||||
final day = DateFormat('d', l10n.localeName).format(this);
|
||||
return "$dayName, $monthAbbr $day";
|
||||
return DateFormat('EEE, MMM d', l10n.localeName).format(this);
|
||||
case FormatMode.d:
|
||||
return DateFormat('d', l10n.localeName).format(this);
|
||||
case FormatMode.da:
|
||||
return DateFormat(
|
||||
'EEEE',
|
||||
l10n.localeName,
|
||||
).format(this).substring(0, 2).firstUpper();
|
||||
return DateFormat('EEEE', l10n.localeName)
|
||||
.format(this)
|
||||
.substring(0, 2)
|
||||
.firstUpper();
|
||||
case FormatMode.dd:
|
||||
return DateFormat('dd', l10n.localeName).format(this);
|
||||
case FormatMode.yyyymmddwedd:
|
||||
@@ -190,15 +162,12 @@ extension DateExtension on DateTime {
|
||||
}
|
||||
|
||||
DateTime getMidnight() {
|
||||
return subtract(
|
||||
Duration(
|
||||
return subtract(Duration(
|
||||
hours: hour,
|
||||
minutes: minute,
|
||||
seconds: second,
|
||||
milliseconds: millisecond,
|
||||
microseconds: microsecond,
|
||||
),
|
||||
);
|
||||
microseconds: microsecond));
|
||||
}
|
||||
|
||||
Cycle getDayCycle() {
|
||||
@@ -225,28 +194,22 @@ extension DateGrouper<T> on Iterable<T> {
|
||||
Map<DateTime, List<T>> newList = {};
|
||||
|
||||
var today = timeNow();
|
||||
today = today.subtract(
|
||||
Duration(
|
||||
today = today.subtract(Duration(
|
||||
hours: today.hour,
|
||||
minutes: today.minute,
|
||||
seconds: today.second,
|
||||
milliseconds: today.millisecond,
|
||||
),
|
||||
);
|
||||
milliseconds: today.millisecond));
|
||||
|
||||
var tomorrow = today.add(Duration(days: 1));
|
||||
var yesterday = today.subtract(Duration(days: 1));
|
||||
|
||||
for (var elem in this) {
|
||||
var date = getDate(elem);
|
||||
var day = date.subtract(
|
||||
Duration(
|
||||
var day = date.subtract(Duration(
|
||||
hours: date.hour,
|
||||
minutes: date.minute,
|
||||
seconds: date.second,
|
||||
milliseconds: date.millisecond,
|
||||
),
|
||||
);
|
||||
milliseconds: date.millisecond));
|
||||
|
||||
if (date.isAfter(tomorrow.add(Duration(days: 1)))) {
|
||||
if (newList[day] == null) {
|
||||
@@ -292,20 +255,17 @@ extension LessonExtension on List<Lesson> {
|
||||
|
||||
Lesson? getCurrentLesson(DateTime now) {
|
||||
return firstWhereOrNull(
|
||||
(lesson) => now.isAfter(lesson.start) && now.isBefore(lesson.end),
|
||||
);
|
||||
(lesson) => now.isAfter(lesson.start) && now.isBefore(lesson.end));
|
||||
}
|
||||
|
||||
Lesson? getPrevLesson(DateTime now) {
|
||||
return firstWhereOrNull(
|
||||
(lesson) => lesson.end.isBefore(now.add(Duration(milliseconds: 1))),
|
||||
);
|
||||
(lesson) => lesson.end.isBefore(now.add(Duration(milliseconds: 1))));
|
||||
}
|
||||
|
||||
Lesson? getNextLesson(DateTime now) {
|
||||
return firstWhereOrNull(
|
||||
(lesson) => lesson.start.isAfter(now.add(Duration(milliseconds: 1))),
|
||||
);
|
||||
(lesson) => lesson.start.isAfter(now.add(Duration(milliseconds: 1))));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:brotli/brotli.dart';
|
||||
import 'package:firka/app/app_state.dart';
|
||||
import 'package:firka/main.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class FirkaBundle extends CachingAssetBundle {
|
||||
@@ -34,9 +34,8 @@ class FirkaBundle extends CachingAssetBundle {
|
||||
@override
|
||||
Future<ByteData> load(String key) async {
|
||||
if (!_compressedBundle) {
|
||||
logger.finest(
|
||||
"Loading asset from root bundle: assets/flutter_assets/$key",
|
||||
);
|
||||
logger
|
||||
.finest("Loading asset from root bundle: assets/flutter_assets/$key");
|
||||
return rootBundle.load(key);
|
||||
} else {
|
||||
index ??= await loadIndex();
|
||||
@@ -44,8 +43,7 @@ class FirkaBundle extends CachingAssetBundle {
|
||||
final gzip = GZipCodec();
|
||||
|
||||
logger.finest(
|
||||
"Loading asset from firka bundle: assets/flutter_assets/$key",
|
||||
);
|
||||
"Loading asset from firka bundle: assets/flutter_assets/$key");
|
||||
switch (index!["assets/flutter_assets/$key"]!) {
|
||||
case "b": // brotli
|
||||
return decode(brotli, await rootBundle.load(key));
|
||||
32
firka/lib/helpers/firka_state.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:firka/main.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
abstract class FirkaState<T extends StatefulWidget> extends State<T> {
|
||||
@override
|
||||
@mustCallSuper
|
||||
void initState() {
|
||||
super.initState();
|
||||
globalUpdate.addListener(_doUpdate);
|
||||
}
|
||||
|
||||
void _doUpdate() {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
@mustCallSuper
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
globalUpdate.removeListener(_doUpdate);
|
||||
globalUpdate.addListener(_doUpdate);
|
||||
}
|
||||
|
||||
@override
|
||||
@mustCallSuper
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
|
||||
globalUpdate.removeListener(_doUpdate);
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ enum ClassIcon {
|
||||
linux,
|
||||
database,
|
||||
applications,
|
||||
project,
|
||||
project
|
||||
}
|
||||
|
||||
Map<ClassIcon, RegExp> _descriptors = {
|
||||
@@ -49,9 +49,8 @@ Map<ClassIcon, RegExp> _descriptors = {
|
||||
ClassIcon.pe: RegExp(r'^tes(i|tneveles)|sport|edzeselmelet'),
|
||||
ClassIcon.chemistry: RegExp(r'kemia'),
|
||||
ClassIcon.biology: RegExp(r'biologia'),
|
||||
ClassIcon.env: RegExp(
|
||||
r'kornyezet|termeszet ?(tudomany|ismeret)|hon( es nep)?ismeret',
|
||||
),
|
||||
ClassIcon.env:
|
||||
RegExp(r'kornyezet|termeszet ?(tudomany|ismeret)|hon( es nep)?ismeret'),
|
||||
ClassIcon.religion: RegExp(r'(hit|erkolcs)tan|vallas|etika|bibliaismeret'),
|
||||
ClassIcon.economics: RegExp(r'penzugy|gazdasag'),
|
||||
ClassIcon.it: RegExp(r'informatika|szoftver|iroda|digitalis'),
|
||||
@@ -67,13 +66,12 @@ Map<ClassIcon, RegExp> _descriptors = {
|
||||
ClassIcon.ofo: RegExp(r'osztaly(fonoki|kozosseg)|kozossegi|neveles'),
|
||||
ClassIcon.diligence: RegExp(r'szorgalom'),
|
||||
ClassIcon.attitude: RegExp(r'magatartas'),
|
||||
ClassIcon.language: RegExp(
|
||||
r'angol|nemet|francia|olasz|orosz|spanyol|latin|kinai|nyelv',
|
||||
),
|
||||
ClassIcon.language:
|
||||
RegExp(r'angol|nemet|francia|olasz|orosz|spanyol|latin|kinai|nyelv'),
|
||||
ClassIcon.linux: RegExp(r'linux'),
|
||||
ClassIcon.database: RegExp(r'adatbazis.*'),
|
||||
ClassIcon.applications: RegExp(r'asztali alkalmazasok'),
|
||||
ClassIcon.project: RegExp(r'projekt'),
|
||||
ClassIcon.project: RegExp(r'projekt')
|
||||
};
|
||||
|
||||
Map<ClassIcon, Uint8List> _iconMap = {
|
||||
@@ -119,19 +117,17 @@ ClassIcon? getIconType(String uid, String className, String category) {
|
||||
|
||||
if (icon == null) {
|
||||
for (var desc in _descriptors.entries) {
|
||||
if (desc.value.hasMatch(
|
||||
className
|
||||
.replaceAll("ö", "o")
|
||||
.replaceAll("ü", "u")
|
||||
.replaceAll("ó", "o")
|
||||
.replaceAll("ő", "o")
|
||||
.replaceAll("ú", "u")
|
||||
.replaceAll("é", "e")
|
||||
.replaceAll("á", "a")
|
||||
.replaceAll("ű", "u")
|
||||
.replaceAll("í", "i")
|
||||
.toLowerCase(),
|
||||
)) {
|
||||
if (desc.value.hasMatch(className
|
||||
.replaceAll("ö", "o")
|
||||
.replaceAll("ü", "u")
|
||||
.replaceAll("ó", "o")
|
||||
.replaceAll("ő", "o")
|
||||
.replaceAll("ú", "u")
|
||||
.replaceAll("é", "e")
|
||||
.replaceAll("á", "a")
|
||||
.replaceAll("ű", "u")
|
||||
.replaceAll("í", "i")
|
||||
.toLowerCase())) {
|
||||
icon = desc.key;
|
||||
|
||||
break;
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:firka/app/app_state.dart';
|
||||
import 'package:firka/main.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -10,9 +10,7 @@ class ImagePreloader {
|
||||
static final Map<String, Future<ui.Image>> _loadingFutures = {};
|
||||
|
||||
static Future<ui.Image> preloadAssetImage(
|
||||
AssetBundle bundle,
|
||||
String assetPath,
|
||||
) async {
|
||||
AssetBundle bundle, String assetPath) async {
|
||||
if (_cache.containsKey(assetPath)) {
|
||||
return _cache[assetPath]!;
|
||||
}
|
||||
@@ -34,12 +32,9 @@ class ImagePreloader {
|
||||
}
|
||||
|
||||
static Future<List<ui.Image>> preloadMultipleAssets(
|
||||
AssetBundle bundle,
|
||||
List<String> assetPaths,
|
||||
) async {
|
||||
final futures = assetPaths
|
||||
.map((path) => preloadAssetImage(bundle, path))
|
||||
.toList();
|
||||
AssetBundle bundle, List<String> assetPaths) async {
|
||||
final futures =
|
||||
assetPaths.map((path) => preloadAssetImage(bundle, path)).toList();
|
||||
return await Future.wait(futures);
|
||||
}
|
||||
|
||||
@@ -96,9 +91,7 @@ class ImagePreloader {
|
||||
}
|
||||
|
||||
static Future<ui.Image> _loadAssetImage(
|
||||
AssetBundle bundle,
|
||||
String assetPath,
|
||||
) async {
|
||||
AssetBundle bundle, String assetPath) async {
|
||||
logger.finest("Caching: $assetPath");
|
||||
final ByteData data = await bundle.load(assetPath);
|
||||
final Uint8List bytes = data.buffer.asUint8List();
|
||||
@@ -126,9 +119,7 @@ class PreloadedImageProvider extends ImageProvider<PreloadedImageProvider> {
|
||||
|
||||
@override
|
||||
ImageStreamCompleter loadImage(
|
||||
PreloadedImageProvider key,
|
||||
ImageDecoderCallback decode,
|
||||
) {
|
||||
PreloadedImageProvider key, ImageDecoderCallback decode) {
|
||||
return OneFrameImageStreamCompleter(_loadAsync(key));
|
||||
}
|
||||
|
||||
@@ -142,10 +133,8 @@ class PreloadedImageProvider extends ImageProvider<PreloadedImageProvider> {
|
||||
}
|
||||
|
||||
try {
|
||||
final image = await ImagePreloader.preloadAssetImage(
|
||||
assetBundle,
|
||||
key.assetPath,
|
||||
);
|
||||
final image =
|
||||
await ImagePreloader.preloadAssetImage(assetBundle, key.assetPath);
|
||||
return ImageInfo(image: image.clone());
|
||||
} catch (e) {
|
||||
final ByteData data = await assetBundle.load(key.assetPath);
|
||||
@@ -1,13 +1,11 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:kreta_api/kreta_api.dart';
|
||||
import 'package:firka/helpers/api/model/timetable.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
||||
class LiveActivityManager {
|
||||
static const MethodChannel _channel = MethodChannel(
|
||||
'firka.app/live_activity',
|
||||
);
|
||||
static const MethodChannel _channel = MethodChannel('firka.app/live_activity');
|
||||
static final Logger _logger = Logger('LiveActivityManager');
|
||||
|
||||
static String? _activityId;
|
||||
@@ -33,9 +31,7 @@ class LiveActivityManager {
|
||||
if (activeActivities.isNotEmpty) {
|
||||
_activityId = activeActivities.first;
|
||||
_isActivityActive = true;
|
||||
_logger.info(
|
||||
'Synced activity state: Found existing activity $_activityId',
|
||||
);
|
||||
_logger.info('Synced activity state: Found existing activity $_activityId');
|
||||
} else {
|
||||
_activityId = null;
|
||||
_isActivityActive = false;
|
||||
@@ -52,9 +48,7 @@ class LiveActivityManager {
|
||||
final args = call.arguments as Map;
|
||||
final activityId = args['activityId'] as String;
|
||||
final pushToken = args['pushToken'] as String;
|
||||
_logger.info(
|
||||
'Received LiveActivity push token: ${pushToken.substring(0, 10)}...',
|
||||
);
|
||||
_logger.info('Received LiveActivity push token: ${pushToken.substring(0, 10)}...');
|
||||
_onPushTokenReceived?.call(activityId, pushToken);
|
||||
break;
|
||||
default:
|
||||
@@ -62,9 +56,7 @@ class LiveActivityManager {
|
||||
}
|
||||
}
|
||||
|
||||
static void setOnPushTokenReceived(
|
||||
Function(String activityId, String pushToken) callback,
|
||||
) {
|
||||
static void setOnPushTokenReceived(Function(String activityId, String pushToken) callback) {
|
||||
_onPushTokenReceived = callback;
|
||||
}
|
||||
|
||||
@@ -100,9 +92,7 @@ class LiveActivityManager {
|
||||
try {
|
||||
await _syncActivityState();
|
||||
if (_isActivityActive) {
|
||||
_logger.info(
|
||||
'Activity already exists, ending it to create new one with fresh token',
|
||||
);
|
||||
_logger.info('Activity already exists, ending it to create new one with fresh token');
|
||||
await endAllActivities();
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
}
|
||||
@@ -227,23 +217,18 @@ class LiveActivityManager {
|
||||
|
||||
final payload = {
|
||||
'isBreak': isBeforeSchool ? false : isBreak,
|
||||
'lessonName': isBeforeSchool
|
||||
? currentLesson.name
|
||||
: (isBreak ? 'Szünet' : currentLesson.name),
|
||||
'lessonName': isBeforeSchool ? currentLesson.name : (isBreak ? 'Szünet' : currentLesson.name),
|
||||
'lessonTheme': (isBeforeSchool || isBreak) ? null : currentLesson.theme,
|
||||
'roomName': (isBeforeSchool || isBreak) ? null : currentLesson.roomName,
|
||||
'teacherName': (isBeforeSchool || isBreak) ? null : currentLesson.teacher,
|
||||
'startTime': startTimeForActivity.toUtc().toIso8601String(),
|
||||
'endTime': endTimeForActivity.toUtc().toIso8601String(),
|
||||
'lessonNumber': (isBeforeSchool || isBreak)
|
||||
? null
|
||||
: currentLesson.lessonNumber,
|
||||
'lessonNumber': (isBeforeSchool || isBreak) ? null : currentLesson.lessonNumber,
|
||||
'nextLessonName': isBeforeSchool ? null : nextLesson?.name,
|
||||
'nextRoomName': isBeforeSchool ? null : nextLesson?.roomName,
|
||||
'nextStartTime': nextStartTimeForActivity?.toUtc().toIso8601String(),
|
||||
'isSubstitution': currentLesson.substituteTeacher != null,
|
||||
'isCancelled':
|
||||
currentLesson.state.name?.toLowerCase().contains('elmarad') ?? false,
|
||||
'isCancelled': currentLesson.state.name?.toLowerCase().contains('elmarad') ?? false,
|
||||
'substituteTeacher': currentLesson.substituteTeacher,
|
||||
'currentTime': now.toUtc().toIso8601String(),
|
||||
'mode': mode,
|
||||
@@ -1,15 +1,13 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:firka/app/app_state.dart';
|
||||
import 'package:firka/main.dart';
|
||||
import 'package:image/image.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
Future<void> pickProfilePicture(
|
||||
AppInitialization data,
|
||||
ImagePicker picker,
|
||||
) async {
|
||||
AppInitialization data, ImagePicker picker) async {
|
||||
var imageFile = await picker.pickImage(source: ImageSource.gallery);
|
||||
if (imageFile == null) return;
|
||||
|
||||
@@ -21,5 +19,5 @@ Future<void> pickProfilePicture(
|
||||
await File(p.join(dataDir.path, "profile.webp")).writeAsBytes(bytes);
|
||||
|
||||
data.profilePicture = bytes;
|
||||
data.profilePictureCubit?.notifyChanged();
|
||||
data.profilePictureUpdateNotifier.update();
|
||||
}
|
||||