Compare commits

...

67 Commits

Author SHA1 Message Date
a8ba41a7ad add NavLib files/deps into project 2024-06-18 19:12:08 +02:00
d5697b54bd move version badge to the top bar 2024-06-17 22:52:53 +02:00
db247a876c fix release building issue & remove ripple from NavView 2024-06-17 22:38:44 +02:00
838b92add3 fix release building issue 2024-06-17 21:48:31 +02:00
eff99e7966 fix missing color resources + cleanup dependencies 2024-06-17 21:08:09 +02:00
661f0912cb fix UI issues on old Android versions 2024-06-17 21:01:12 +02:00
1e6016c446 remove useless dependencies 2024-06-16 15:22:24 +02:00
bc8c952853 fix collapsing toolbar title when doing back gesture 2024-06-16 15:12:06 +02:00
743050712b remove unneeded import statement 2024-06-16 12:13:36 +02:00
a4ac17143f set separate app ID & icon for debug version 2024-06-16 11:38:47 +02:00
9fbafd0eee center buttons in EventDetailsDialog 2024-06-15 12:39:30 +02:00
40ea700e0b remove duplicated string extension 2024-06-15 12:18:35 +02:00
2a93cd5ebd improve LessonDetailsDialog 2024-06-15 12:18:05 +02:00
8fa2ca5bd5 fix agenda view crash when building release variant 2024-06-15 11:29:12 +02:00
c187c0579f add new ProGuard rules + fix QR scanning 2024-06-14 23:58:12 +02:00
494c132c84 add new Retrofit2 rules 2024-06-14 23:17:46 +02:00
11d3502be6 fix proguard rules (attempt 1) 2024-06-14 23:07:38 +02:00
663243da8b add cosmetic ui changes + fix build issues 2024-06-14 22:48:50 +02:00
92ef6b211d grades: join code and category together 2024-06-14 18:25:16 +02:00
567dd8e6a2 add support for grade column codes 2024-06-14 17:59:54 +02:00
29fd96acb4 hide classroom heading when no classroom is specified 2024-06-14 17:06:58 +02:00
8d0b5b8fc7 add lock layout function (szkolny-eu/szkolny-android#199) 2024-06-14 16:57:48 +02:00
b65f2828cd adapt notes fragment to MD3 2024-06-14 16:24:58 +02:00
beed9f858f fix: too small heading in agenda dialog 2024-06-14 06:27:25 +02:00
b148f7197f fix: "Back button opens drawer" (redundant super call) 2024-06-13 22:58:47 +02:00
eca2028595 fix: duplicated items in about card 2024-06-13 22:53:32 +02:00
d2789342da fix: no query filtering 2024-06-13 22:47:49 +02:00
ab3af67663 adapt code to updated dependencies + align lessons (based by szkolny-eu/szkolny-android#196) 2024-06-13 19:17:18 +02:00
4b4901e440 fix kapt errors related to SelectiveDAO + migrate BuildConfig 2024-06-13 18:57:40 +02:00
4d9c9368dd merge with latest Szkolny.eu changes (TODO) 2024-06-13 10:55:14 +02:00
c123b28652 fix missing Intent receiver flags for Tiramisu+ 2024-06-13 09:44:42 +02:00
225070abd9 update dependencies 2024-06-12 21:33:34 +02:00
1540a6cfcd fix M3 UI code & upgrade kotlin 2024-06-12 21:12:02 +02:00
9915150c33 wielki powrót pr m3 do szkolnego (nie czytajcie tego kodu) 2023-11-27 17:08:17 +01:00
cefb0deba8 [Actions] Rename changelog output name. 2023-03-25 10:09:36 +01:00
90a151c129 [4.13.6] Update build.gradle, signing and changelog. 2023-03-24 22:27:27 +01:00
9fd9721ae7 [UI] Hide Debugging menu without dev mode. 2023-03-24 22:24:33 +01:00
ceca75ef4b [UI/Timetable] Add option to sync current week. 2023-03-24 22:16:34 +01:00
21c00bbe53 [API] Fix detecting session cookies. Remove expired cookies. 2023-03-24 22:09:31 +01:00
db00566ebf [UI/Login] Fallback reCAPTCHA to WebView activity. 2023-03-24 22:09:31 +01:00
07ab1b984f [API/Librus] Fix login. (#176) 2023-03-24 22:09:03 +01:00
8177d4aa2d [Widgets] Fix pending intents mutability. Hide timetable sync button. 2023-03-24 11:13:00 +01:00
beff1b6460 [App] Fix cookie persistence. 2023-03-24 10:56:35 +01:00
31b569b02e [4.13.5] Update build.gradle, signing and changelog. 2023-03-22 23:16:28 +01:00
8bf77817d2 [UI] Fix writing files on Android 13 and newer. 2023-03-22 23:15:45 +01:00
27b61adf1d [Actions] Fix Play release publishing workflow. 2022-12-27 12:30:03 +01:00
a0244841ad [4.13.4] Update build.gradle, signing and changelog. 2022-12-26 14:45:29 +01:00
12c0c6f2ec [UI] Always show event subject dropdown for university school. 2022-12-26 14:43:42 +01:00
aaa3b8626e [UI] Update event types for university school. 2022-12-26 14:01:25 +01:00
48c9e2dfe3 [4.13.3] Update build.gradle, signing and changelog. 2022-12-06 10:35:23 +01:00
81d4801d27 [UI] Add snowfall to CounterActivity. Enable in February as well. 2022-12-06 10:34:12 +01:00
5f8016061d [API/Vulcan] Fix wrong serializing of null in JSON causing API error. 2022-12-06 10:22:47 +01:00
5007587192 [UI/Agenda] Allow prioritizing event subject over event type. 2022-11-30 11:29:43 +01:00
dfd1083e41 [UI/Timetable] Show lesson replacing notes in all places. 2022-11-30 10:41:43 +01:00
678baf46e5 [4.13.2] Update build.gradle, signing and changelog. 2022-11-28 20:30:11 +01:00
4077fe448d [4.13.2-rc.4] Update build.gradle, signing and changelog. 2022-11-25 16:52:16 +01:00
f085e17ef7 [API/Vulcan] Once again fix ignoring 404 response on Addressbook. 2022-11-25 16:51:35 +01:00
7fd2cad46b [4.13.2-rc.3] Update build.gradle, signing and changelog. 2022-11-25 16:13:58 +01:00
93dc2ac9ab [API/Vulcan] Fix ignoring 404 response on Addressbook. 2022-11-25 16:11:50 +01:00
ac53e267fc [4.13.2-rc.2] Update build.gradle, signing and changelog. 2022-11-25 14:55:49 +01:00
86eb1a0f42 [API/Vulcan] Actually ignore 404 response on Addressbook. 2022-11-25 14:54:05 +01:00
710d82da27 [4.13.2-rc.1] Update build.gradle, signing and changelog. 2022-11-25 14:40:39 +01:00
0123f50810 [API/Vulcan] Ignore 404 response on Addressbook. 2022-11-25 14:20:22 +01:00
6d3eb65445 [API/Mobidziennik] Do not clear email field if not set. 2022-11-15 22:20:00 +01:00
9c79a4003f Replace MaterialComponents with Material3 2022-10-28 22:32:00 +02:00
f46d389b83 Change drawer header, dark bottombar color 2022-10-22 16:27:04 +02:00
dda64489d7 Material 3 theme and color support, filled cards 2022-10-16 00:14:15 +02:00
310 changed files with 10049 additions and 2463 deletions

View File

@ -23,11 +23,11 @@ if __name__ == "__main__":
(title, changelog) = get_changelog(project_dir, format="plain")
# plain text changelog - Firebase App Distribution
with open(dir + "whatsnew-titled.txt", "w", encoding="utf-8") as f:
with open(dir + "whatsnew_titled.txt", "w", encoding="utf-8") as f:
f.write(title)
f.write("\n")
f.write(changelog)
print("::set-output name=changelogPlainTitledFile::" + dir + "whatsnew-titled.txt")
print("::set-output name=changelogPlainTitledFile::" + dir + "whatsnew_titled.txt")
print("::set-output name=changelogTitle::" + title)

View File

@ -113,10 +113,11 @@ jobs:
with:
serviceAccountJsonPlainText: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
packageName: pl.szczodrzynski.edziennik
releaseFile: ${{ needs.sign.outputs.signedReleaseFile }}
releaseFiles: ${{ needs.sign.outputs.signedReleaseFile }}
releaseName: ${{ steps.changelog.outputs.appVersionName }}
track: ${{ secrets.PLAY_RELEASE_TRACK }}
whatsNewDirectory: ${{ steps.changelog.outputs.changelogDir }}
status: completed
- name: Upload workflow artifact
uses: actions/upload-artifact@v2

View File

@ -20,8 +20,9 @@ android {
buildConfigField "java.util.Map<String, String>", "GIT_INFO", gitInfoMap
buildConfigField "String", "VERSION_BASE", "\"${release.versionName}\""
manifestPlaceholders = [
buildTimestamp: String.valueOf(System.currentTimeMillis())
buildTimestamp: String.valueOf(System.currentTimeMillis())
]
multiDexEnabled = true
@ -36,6 +37,8 @@ android {
arguments {
arg("room.schemaLocation", "$projectDir/schemas")
}
correctErrorTypes true
}
}
@ -43,10 +46,12 @@ android {
debug {
getIsDefault().set(true)
minifyEnabled = false
applicationIdSuffix ".debug"
manifestPlaceholders = [
buildTimestamp: 0
buildTimestamp: 0
]
}
release {
minifyEnabled = true
shrinkResources = true
@ -54,28 +59,35 @@ android {
proguardFiles fileTree('proguard').asList().toArray()
}
}
flavorDimensions "platform"
flavorDimensions += "platform"
productFlavors {
unofficial {
getIsDefault().set(true)
versionName "${release.versionName}-${gitInfo.versionSuffix}"
}
official {}
play {}
}
variantFilter { variant ->
def flavors = variant.flavors*.name
setIgnore(variant.buildType.name == "debug" && !flavors.contains("unofficial") || flavors.contains("main"))
}
sourceSets {
unofficial {
java.srcDirs = ["src/main/java", "src/play-not/java"]
manifest.srcFile("src/play-not/AndroidManifest.xml")
}
official {
java.srcDirs = ["src/main/java", "src/play-not/java"]
manifest.srcFile("src/play-not/AndroidManifest.xml")
}
play {
java.srcDirs = ["src/main/java", "src/play/java"]
}
@ -84,37 +96,47 @@ android {
defaultConfig {
vectorDrawables.useSupportLibrary = true
}
buildFeatures {
dataBinding = true
viewBinding = true
buildConfig = true
}
compileOptions {
coreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = "1.8"
}
packagingOptions {
resources {
excludes += ['META-INF/library-core_release.kotlin_module']
}
}
externalNativeBuild {
cmake {
path "src/main/cpp/CMakeLists.txt"
version "3.10.2"
}
}
lint {
checkReleaseBuilds false
}
namespace 'pl.szczodrzynski.edziennik'
}
tasks.whenTaskAdded { task ->
if (!task.name.endsWith("Release") && !task.name.endsWith("ReleaseWithR8"))
return
def renameTaskName = "rename${task.name.capitalize()}"
def flavor = ""
@ -125,16 +147,20 @@ tasks.whenTaskAdded { task ->
if (task.name.startsWith("minify"))
flavor = task.name.substring("minify".length(), task.name.indexOf("Release")).uncapitalize()
def taskName = "package${flavor.capitalize()}Release"
if (flavor != "") {
tasks.create(renameTaskName, Copy) {
tasks.register(renameTaskName, Copy) {
dependsOn(taskName)
from file("${projectDir}/${flavor}/release/"),
file("${buildDir}/outputs/mapping/${flavor}Release/"),
file("${buildDir}/outputs/apk/${flavor}/release/"),
file("${buildDir}/outputs/bundle/${flavor}Release/")
file("${layout.buildDirectory}/outputs/mapping/${flavor}Release/"),
file("${layout.buildDirectory}/outputs/apk/${flavor}/release/"),
file("${layout.buildDirectory}/outputs/bundle/${flavor}Release/")
include "*.aab", "*.apk", "mapping.txt", "output-metadata.json"
destinationDir file("${projectDir}/release/")
rename ".+?\\.(.+)", "Edziennik_${android.defaultConfig.versionName}_${flavor}." + '$1'
}
task.finalizedBy(renameTaskName)
}
}
@ -142,31 +168,35 @@ tasks.whenTaskAdded { task ->
dependencies {
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation files('libs/navlib-debug.aar')
implementation files('libs/navlib-font-debug.aar')
// Language cores
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "androidx.multidex:multidex:2.0.1"
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:1.1.5"
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4"
// Android Jetpack
implementation "androidx.appcompat:appcompat:1.5.1"
implementation "androidx.appcompat:appcompat:1.7.0"
implementation "androidx.cardview:cardview:1.0.0"
implementation "androidx.constraintlayout:constraintlayout:2.1.4"
implementation "androidx.core:core-ktx:1.9.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.5.1"
implementation "androidx.navigation:navigation-fragment-ktx:2.5.2"
implementation "androidx.recyclerview:recyclerview:1.2.1"
implementation "androidx.room:room-runtime:2.4.3"
implementation "androidx.work:work-runtime-ktx:2.7.1"
kapt "androidx.room:room-compiler:2.4.3"
implementation "androidx.core:core-ktx:1.13.1"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.8.2"
implementation "androidx.navigation:navigation-fragment-ktx:2.7.7"
implementation "androidx.recyclerview:recyclerview:1.3.2"
implementation "androidx.room:room-runtime:2.6.1"
implementation "androidx.room:room-ktx:2.6.1"
implementation "androidx.work:work-runtime-ktx:2.9.0"
kapt "androidx.room:room-compiler:2.6.1"
// Google design libs
implementation "com.google.android.material:material:1.6.1"
implementation "com.google.android.material:material:1.12.0"
implementation "com.google.android.flexbox:flexbox:3.0.0"
// Play Services/Firebase
implementation "com.google.android.gms:play-services-wearable:17.1.0"
implementation "com.google.android.gms:play-services-wearable:18.2.0"
implementation("com.google.firebase:firebase-core") { version { strictly "19.0.2" } }
implementation "com.google.firebase:firebase-crashlytics:18.2.13"
implementation "com.google.firebase:firebase-crashlytics:19.0.1"
implementation("com.google.firebase:firebase-messaging") { version { strictly "20.1.3" } }
// OkHttp, Retrofit, Gson, Jsoup
@ -174,12 +204,13 @@ dependencies {
implementation "com.squareup.retrofit2:retrofit:2.9.0"
implementation "com.squareup.retrofit2:converter-gson:2.9.0"
implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
implementation 'com.google.code.gson:gson:2.8.8'
implementation 'com.google.code.gson:gson:2.11.0'
implementation 'org.jsoup:jsoup:1.14.3'
implementation "pl.droidsonroids:jspoon:1.3.2"
implementation "pl.droidsonroids.retrofit2:converter-jspoon:1.3.2"
// Szkolny.eu libraries/forks
implementation project(":navlib")
implementation "eu.szkolny:android-snowfall:1ca9ea2da3"
implementation "eu.szkolny:agendacalendarview:1.0.4"
implementation "eu.szkolny:cafebar:5bf0c618de"
@ -187,22 +218,24 @@ dependencies {
implementation "eu.szkolny:material-about-library:1d5ebaf47c"
implementation "eu.szkolny:mhttp:af4b62e6e9"
implementation "eu.szkolny:nachos:0e5dfcaceb"
implementation "eu.szkolny.selective-dao:annotation:27f8f3f194"
implementation "eu.szkolny.selective-dao:annotation:6a337f9"
officialImplementation "eu.szkolny:ssl-provider:1.0.0"
unofficialImplementation "eu.szkolny:ssl-provider:1.0.0"
implementation "pl.szczodrzynski:navlib:0.8.0"
implementation "pl.szczodrzynski:numberslidingpicker:2921225f76"
implementation "pl.szczodrzynski:recyclertablayout:700f980584"
implementation "pl.szczodrzynski:tachyon:551943a6b5"
kapt "eu.szkolny.selective-dao:codegen:27f8f3f194"
kapt "eu.szkolny.selective-dao:codegen:6a337f9"
// Iconics & related
implementation "com.mikepenz:iconics-core:5.3.2"
implementation "com.mikepenz:iconics-views:5.3.2"
implementation "com.mikepenz:materialdrawer:9.0.1"
implementation "com.mikepenz:community-material-typeface:5.8.55.0-kotlin@aar"
implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar'
implementation "eu.szkolny:szkolny-font:77e33acc2a"
// Other dependencies
debugApi "com.mikepenz:materialize:1.2.1" // required for all R.color.md_* colors
implementation "cat.ereza:customactivityoncrash:2.3.0"
implementation "com.android.volley:volley:1.2.1"
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
@ -225,6 +258,10 @@ dependencies {
implementation("com.heinrichreimersoftware:material-intro") { version { strictly "1.5.8" } }
implementation("pl.droidsonroids.gif:android-gif-drawable") { version { strictly "1.2.15" } }
// Debug-only dependencies
debugImplementation "com.github.amitshekhariitbhu.Android-Debug-Database:debug-db:v1.0.6"
// NavLib dependencies
// TODO: try to move these dependencies to NavLib build.gradle file
implementation "com.mikepenz:materialize:1.2.1"
implementation "com.mikepenz:itemanimators:1.1.0"
testImplementation 'junit:junit:4.13.2'
}

View File

@ -5,7 +5,7 @@
buildscript {
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath "org.eclipse.jgit:org.eclipse.jgit:5.5.+"

View File

@ -36,6 +36,37 @@
"status": 2
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:640759989760:android:4aa71407b25cdc8d",
"android_client_info": {
"package_name": "pl.szczodrzynski.edziennik.debug"
}
},
"oauth_client": [
{
"client_id": "640759989760-6f8q00u864lnuh3gh36e8g4cer9lv8pv.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAvq9HMPxulz9ntdAHZ0eZuPf2YQs4nDSU"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
}
],
"configuration_version": "1"

View File

@ -19,7 +19,10 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
-keepattributes Signature
-keep class android.support.v7.widget.** { *; }
-keep class com.google.gson.reflect.TypeToken { *; }
-keep class * extends com.google.gson.reflect.TypeToken
-keep class pl.szczodrzynski.edziennik.utils.models.** { *; }
-keep class pl.szczodrzynski.edziennik.data.db.enums.* { *; }
@ -55,8 +58,19 @@
-keep class com.google.android.material.tabs.** {*;}
# Exclude AgendaCalendarView
# Preserve generic type information for EventRenderer and its subclasses
-keepclassmembers class * extends com.github.tibolte.agendacalendarview.render.EventRenderer {
<fields>;
<methods>;
}
# Keep the EventRenderer class itself and all its subclasses
-keep class com.github.tibolte.agendacalendarview.render.EventRenderer
-keep class * extends com.github.tibolte.agendacalendarview.render.EventRenderer
# ServiceLoader support
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
# Most of volatile fields are updated with AFU and should not be mangled
@ -70,9 +84,36 @@
-keep class pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing { public final byte[] pleaseStopRightNow(java.lang.String, long); }
-keepclassmembers class pl.szczodrzynski.edziennik.ui.login.qr.* { *; }
-keepclassmembers class pl.szczodrzynski.edziennik.data.api.szkolny.request.** { *; }
-keepclassmembers class pl.szczodrzynski.edziennik.data.api.szkolny.response.** { *; }
-keepclassmembernames class pl.szczodrzynski.edziennik.ui.login.LoginInfo$Platform { *; }
-keepclassmembernames class pl.szczodrzynski.fslogin.realm.RealmData { *; }
-keepclassmembernames class pl.szczodrzynski.fslogin.realm.RealmData$Type { *; }
# Exclude Retrofit2
-keepattributes Signature, InnerClasses, EnclosingMethod
-keepattributes RuntimeVisibleAnnotations, RuntimeVisibleParameterAnnotations
-keepattributes AnnotationDefault
-keepclassmembers,allowshrinking,allowobfuscation interface * {
@retrofit2.http.* <methods>;
}
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn javax.annotation.**
-dontwarn kotlin.Unit
-dontwarn retrofit2.KotlinExtensions
-dontwarn retrofit2.KotlinExtensions$*
-if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation interface <1>
-if interface * { @retrofit2.http.* <methods>; }
-keep,allowobfuscation interface * extends <1>
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation
-if interface * { @retrofit2.http.* public *** *(...); }
-keep,allowoptimization,allowshrinking,allowobfuscation class <3>
-keep,allowobfuscation,allowshrinking class retrofit2.Response

View File

@ -124,11 +124,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"gradeId"
],
"autoGenerate": false
]
},
"indices": [
{
@ -197,11 +197,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"teacherId"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": []
@ -278,11 +278,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"teacherAbsenceId"
],
"autoGenerate": false
]
},
"indices": [
{
@ -321,11 +321,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"teacherAbsenceTypeId"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": []
@ -366,11 +366,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"subjectId"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": []
@ -441,11 +441,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"noticeId"
],
"autoGenerate": false
]
},
"indices": [
{
@ -502,11 +502,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"teamId"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": []
@ -619,11 +619,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"attendanceId"
],
"autoGenerate": false
]
},
"indices": [
{
@ -770,11 +770,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"eventId"
],
"autoGenerate": false
]
},
"indices": [
{
@ -843,11 +843,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"eventType"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": []
@ -882,10 +882,10 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"loginStoreId"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": []
@ -1046,10 +1046,10 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": []
@ -1084,11 +1084,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"luckyNumberDate"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": []
@ -1159,11 +1159,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"announcementId"
],
"autoGenerate": false
]
},
"indices": [
{
@ -1238,12 +1238,12 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"categoryId",
"type"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": []
@ -1308,10 +1308,10 @@
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"messageId"
],
"autoGenerate": true
]
},
"indices": [],
"foreignKeys": []
@ -1400,11 +1400,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"messageId"
],
"autoGenerate": false
]
},
"indices": [
{
@ -1456,12 +1456,12 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"messageRecipientId",
"messageId"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": []
@ -1484,10 +1484,10 @@
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
],
"autoGenerate": true
]
},
"indices": [],
"foreignKeys": []
@ -1528,11 +1528,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"endpointId"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": []
@ -1567,11 +1567,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"lessonRangeNumber"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": []
@ -1648,10 +1648,10 @@
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
],
"autoGenerate": true
]
},
"indices": [],
"foreignKeys": []
@ -1680,11 +1680,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"id"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": []
@ -1713,11 +1713,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"id"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": []
@ -1770,11 +1770,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"id"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": []
@ -1923,11 +1923,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"id"
],
"autoGenerate": false
]
},
"indices": [
{
@ -1979,11 +1979,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"key"
],
"autoGenerate": false
]
},
"indices": [],
"foreignKeys": []
@ -2024,11 +2024,11 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"profileId",
"lessonId"
],
"autoGenerate": false
]
},
"indices": [
{
@ -2127,10 +2127,10 @@
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
],
"autoGenerate": true
]
},
"indices": [
{
@ -2228,10 +2228,10 @@
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"noteId"
],
"autoGenerate": false
]
},
"indices": [
{
@ -2290,10 +2290,10 @@
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"metadataId"
],
"autoGenerate": true
]
},
"indices": [
{

File diff suppressed because it is too large Load Diff

View File

@ -137,6 +137,7 @@
"columnNames": [
"profileId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_grades_profileId` ON `${TABLE_NAME}` (`profileId`)"
}
],
@ -290,6 +291,7 @@
"columnNames": [
"profileId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_teacherAbsence_profileId` ON `${TABLE_NAME}` (`profileId`)"
}
],
@ -452,6 +454,7 @@
"columnNames": [
"profileId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_notices_profileId` ON `${TABLE_NAME}` (`profileId`)"
}
],
@ -629,6 +632,7 @@
"columnNames": [
"profileId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_attendances_profileId` ON `${TABLE_NAME}` (`profileId`)"
}
],
@ -781,6 +785,7 @@
"eventDate",
"eventTime"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_events_profileId_eventDate_eventTime` ON `${TABLE_NAME}` (`profileId`, `eventDate`, `eventTime`)"
},
{
@ -790,6 +795,7 @@
"profileId",
"eventType"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_events_profileId_eventType` ON `${TABLE_NAME}` (`profileId`, `eventType`)"
}
],
@ -1166,6 +1172,7 @@
"columnNames": [
"profileId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_announcements_profileId` ON `${TABLE_NAME}` (`profileId`)"
}
],
@ -1407,6 +1414,7 @@
"profileId",
"messageType"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_messages_profileId_messageType` ON `${TABLE_NAME}` (`profileId`, `messageType`)"
}
],
@ -1918,6 +1926,7 @@
"type",
"date"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_timetable_profileId_type_date` ON `${TABLE_NAME}` (`profileId`, `type`, `date`)"
},
{
@ -1928,6 +1937,7 @@
"type",
"oldDate"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_timetable_profileId_type_oldDate` ON `${TABLE_NAME}` (`profileId`, `type`, `oldDate`)"
}
],
@ -2015,6 +2025,7 @@
"columnNames": [
"profileId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_librusLessons_profileId` ON `${TABLE_NAME}` (`profileId`)"
}
],
@ -2117,6 +2128,7 @@
"profileId",
"date"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_timetableManual_profileId_date` ON `${TABLE_NAME}` (`profileId`, `date`)"
},
{
@ -2126,6 +2138,7 @@
"profileId",
"weekDay"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_timetableManual_profileId_weekDay` ON `${TABLE_NAME}` (`profileId`, `weekDay`)"
}
],
@ -2217,6 +2230,7 @@
"noteOwnerType",
"noteOwnerId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_notes_profileId_noteOwnerType_noteOwnerId` ON `${TABLE_NAME}` (`profileId`, `noteOwnerType`, `noteOwnerId`)"
}
],
@ -2278,6 +2292,7 @@
"thingType",
"thingId"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_metadata_profileId_thingType_thingId` ON `${TABLE_NAME}` (`profileId`, `thingType`, `thingId`)"
}
],

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-5-11.
-->
<resources>
<color name="ic_launcher_background">#FF7D54</color>
</resources>

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="pl.szczodrzynski.edziennik">
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
@ -10,11 +9,13 @@
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<!-- PowerPermission uses minSdk 21, it's safe to override as it is used only in >= 23 -->
<uses-sdk tools:overrideLibrary="com.qifan.powerpermission.coroutines, com.qifan.powerpermission.core" />
<uses-sdk tools:overrideLibrary="com.qifan.powerpermission.coroutines, com.qifan.powerpermission.core, com.mikepenz:materialdrawer, com.mikepenz.iconics.typeface.library.navlibfont" />
<application
android:name=".App"
@ -40,7 +41,6 @@
|___/ -->
<activity android:name=".MainActivity"
android:configChanges="orientation|screenSize"
android:label="@string/app_name"
android:launchMode="singleTop"
android:exported="true"
android:theme="@style/SplashTheme">
@ -84,7 +84,7 @@
android:resource="@xml/widget_timetable_info" />
</receiver>
<service android:name=".ui.widgets.timetable.WidgetTimetableService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
android:permission="android.permission.BIND_REMOTEVIEWS" android:foregroundServiceType="dataSync"/>
<activity android:name=".ui.widgets.LessonDialogActivity"
android:label=""
android:configChanges="orientation|keyboardHidden"
@ -105,7 +105,7 @@
android:resource="@xml/widget_notifications_info" />
</receiver>
<service android:name=".ui.widgets.notifications.WidgetNotificationsService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
android:permission="android.permission.BIND_REMOTEVIEWS" android:foregroundServiceType="dataSync"/>
<!-- LUCKY NUMBER -->
<receiver android:name=".ui.widgets.luckynumber.WidgetLuckyNumberProvider"
android:label="@string/widget_lucky_number_title"
@ -133,7 +133,6 @@
android:theme="@style/DeadTheme" />
<activity android:name=".ui.intro.ChangelogIntroActivity"
android:configChanges="orientation|keyboardHidden"
android:label="@string/app_name"
android:exported="false"
android:theme="@style/Theme.Intro" />
<activity android:name=".ui.login.LoginActivity"
@ -146,7 +145,6 @@
android:theme="@style/AppTheme.Black" />
<activity android:name=".ui.feedback.FeedbackActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:label="@string/app_name"
android:exported="false"
android:theme="@style/AppTheme" />
<activity android:name=".ui.settings.SettingsLicenseActivity"
@ -160,7 +158,11 @@
<activity android:name=".ui.login.oauth.OAuthLoginActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:theme="@style/AppTheme.Light" />
android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar" />
<activity android:name=".ui.login.recaptcha.RecaptchaActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:theme="@style/Theme.MaterialComponents.Light.DarkActionBar" />
<activity android:name=".ui.base.BuildInvalidActivity" android:exported="false" />
<activity android:name=".ui.settings.contributors.ContributorsActivity" android:exported="false" />
@ -198,15 +200,15 @@
____) | __/ | \ V /| | (_| __/\__ \
|_____/ \___|_| \_/ |_|\___\___||___/
-->
<service android:name=".data.api.ApiService" />
<service android:name=".data.api.ApiService" android:foregroundServiceType="dataSync"/>
<service android:name=".data.firebase.MyFirebaseService"
android:exported="false">
android:exported="false" android:foregroundServiceType="dataSync">
<intent-filter android:priority="10000000">
<action android:name="com.google.firebase.MESSAGING_EVENT" />
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
</intent-filter>
</service>
<service android:name=".sync.UpdateDownloaderService" />
<service android:name=".sync.UpdateDownloaderService" android:foregroundServiceType="dataSync"/>
<!--
_____ _ _

View File

@ -1,11 +1,10 @@
<h3>Wersja 4.13.1, 2022-11-03</h3>
<h3>Wersja 4.13.6, 2023-03-24</h3>
<ul>
<li>Edycja kart na stronie głównej znowu działa.</li>
<li>Poprawiono pobieranie nauczyciela w zakładce Uwagi. @BxOxSxS</li>
<li>Poprawiono oznaczanie daty przeczytania wiadomości wysłanych. @BxOxSxS</li>
<li>Dodano tłumaczenie karty planu lekcji. @MedzikUser</li>
<li>Naprawiono pobieranie załączników na Androidzie 13 i nowszym.</li>
<li>Dodano opcję odświeżenia planu lekcji na wybrany tydzień.</li>
<li>Usunięto błędy logowania. @BxOxSxS</li>
</ul>
<br>
<br>
Dzięki za korzystanie ze Szkolnego!<br>
<i>&copy; [Kuba Szczodrzyński](@kuba2k2) 2022</i>
<i>&copy; [Kuba Szczodrzyński](@kuba2k2) 2023</i>

View File

@ -9,7 +9,7 @@
/*secret password - removed for source code publication*/
static toys AES_IV[16] = {
0xe8, 0x2b, 0x2c, 0xd8, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
0x6d, 0xa5, 0x32, 0xe6, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat);

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -124,7 +124,8 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun getWorkManagerConfiguration() = Configuration.Builder()
override val workManagerConfiguration: Configuration = Configuration.Builder()
.setMinimumLoggingLevel(Log.VERBOSE)
.build()
@ -235,6 +236,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
}
Signing.getCert(this)
Utils.initializeStorageDir(this)
launch {
withContext(Dispatchers.Default) {
@ -422,6 +424,12 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope {
try {
App.data = AppData.get(profile.loginStoreType)
d("App", "Loaded AppData: ${App.data}")
// apply newly-added config overrides, if not changed by the user yet
for ((key, value) in App.data.configOverrides) {
val config = App.profile.config
if (!config.has(key))
config.set(key, value)
}
} catch (e: Exception) {
Log.e("App", "Cannot load AppData", e)
Toast.makeText(this, R.string.app_cannot_load_data, Toast.LENGTH_LONG).show()

View File

@ -1,5 +1,6 @@
package pl.szczodrzynski.edziennik
import android.annotation.SuppressLint
import android.app.ActivityManager
import android.content.BroadcastReceiver
import android.content.Context
@ -15,10 +16,11 @@ import android.view.Gravity
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.ColorUtils
import androidx.core.view.WindowCompat
import androidx.core.view.isVisible
import androidx.navigation.NavOptions
import com.danimahardhika.cafebar.CafeBar
import com.danimahardhika.cafebar.CafeBarTheme
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.jetradarmobile.snowfall.SnowfallView
import com.mikepenz.iconics.IconicsDrawable
@ -39,7 +41,6 @@ import pl.szczodrzynski.edziennik.data.api.events.*
import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.data.db.entity.Metadata.*
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.enums.FeatureType
import pl.szczodrzynski.edziennik.databinding.ActivitySzkolnyBinding
@ -66,19 +67,16 @@ import pl.szczodrzynski.edziennik.ui.messages.list.MessagesFragment
import pl.szczodrzynski.edziennik.ui.timetable.TimetableFragment
import pl.szczodrzynski.edziennik.utils.*
import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.Utils.dpToPx
import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type
import pl.szczodrzynski.edziennik.utils.managers.UserActionManager
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.navlib.*
import pl.szczodrzynski.navlib.SystemBarsUtil.Companion.COLOR_HALF_TRANSPARENT
import pl.szczodrzynski.navlib.bottomsheet.NavBottomSheet
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
import pl.szczodrzynski.navlib.drawer.NavDrawer
import pl.szczodrzynski.navlib.drawer.items.DrawerPrimaryItem
import java.io.IOException
import java.util.*
import kotlin.coroutines.CoroutineContext
import kotlin.math.roundToInt
@ -148,10 +146,10 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
errorSnackbar.setCoordinator(b.navView.coordinator, b.navView.bottomBar)
val versionBadge = app.buildManager.versionBadge
b.nightlyText.isVisible = versionBadge != null
b.nightlyText.text = versionBadge
navView.nightlyText.isVisible = versionBadge != null
navView.nightlyText.text = versionBadge
if (versionBadge != null) {
b.nightlyText.background.setTintColor(0xa0ff0000.toInt())
navView.nightlyText.background.setTintColor(0xa0ff0000.toInt())
}
navLoading = true
@ -159,51 +157,11 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
b.navView.apply {
drawer.init(this@MainActivity)
SystemBarsUtil(this@MainActivity).run {
//paddingByKeyboard = b.navView
appFullscreen = false
statusBarColor = getColorFromAttr(context, android.R.attr.colorBackground)
statusBarDarker = false
statusBarFallbackLight = COLOR_HALF_TRANSPARENT
statusBarFallbackGradient = COLOR_HALF_TRANSPARENT
navigationBarTransparent = false
b.navView.configSystemBarsUtil(this)
// fix for setting status bar color to window color, outside of navlib
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
window.statusBarColor = statusBarColor
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
&& ColorUtils.calculateLuminance(statusBarColor) > 0.6
) {
@Suppress("deprecation")
window.decorView.systemUiVisibility =
window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
// TODO fix navlib navbar detection, orientation change issues, status bar color setting if not fullscreen
commit()
}
toolbar.apply {
subtitleFormat = R.string.toolbar_subtitle
subtitleFormatWithUnread = R.plurals.toolbar_subtitle_with_unread
}
bottomBar.apply {
fabEnable = false
fabExtendable = true
fabExtended = false
fabGravity = Gravity.CENTER
if (Themes.isDark) {
setBackgroundColor(blendColors(
getColorFromAttr(context, R.attr.colorSurface),
getColorFromRes(R.color.colorSurface_4dp)
))
elevation = dpToPx(4).toFloat()
}
fabGravity = Gravity.RIGHT
}
bottomSheet.apply {
@ -322,7 +280,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
// IT'S WINTER MY DUDES
val today = Date.getToday()
if ((today.month % 11 == 1) && app.config.ui.snowfall) {
if ((today.month / 3 % 4 == 0) && app.config.ui.snowfall) {
b.rootFrame.addView(layoutInflater.inflate(R.layout.snowfall, b.rootFrame, false))
} else if (app.config.ui.eggfall && BigNightUtil().isDataWielkanocyNearDzisiaj()) {
val eggfall = layoutInflater.inflate(
@ -358,6 +316,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
if (app.config.appRateSnackbarTime != 0L && app.config.appRateSnackbarTime <= System.currentTimeMillis()) {
navView.coordinator.postDelayed({
CafeBar.builder(this)
.theme(CafeBarTheme.Custom(getColorFromAttr(this, R.attr.colorSurfaceInverse)))
.content(R.string.rate_snackbar_text)
.icon(IconicsDrawable(this).apply {
icon = CommunityMaterial.Icon3.cmd_star_outline
@ -560,8 +519,6 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
swipeRefreshLayout.isRefreshing = true
if (event.profileId == App.profileId) {
navView.toolbar.apply {
subtitleFormat = null
subtitleFormatWithUnread = null
subtitle = getString(R.string.toolbar_subtitle_syncing)
}
}
@ -579,8 +536,6 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
fun onApiTaskProgressEvent(event: ApiTaskProgressEvent) {
if (event.profileId == App.profileId) {
navView.toolbar.apply {
subtitleFormat = null
subtitleFormatWithUnread = null
subtitle = if (event.progress < 0f)
event.progressText ?: ""
else
@ -599,8 +554,6 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
EventBus.getDefault().removeStickyEvent(event)
if (event.profileId == App.profileId) {
navView.toolbar.apply {
subtitleFormat = R.string.toolbar_subtitle
subtitleFormatWithUnread = R.plurals.toolbar_subtitle_with_unread
subtitle = "Gotowe"
}
}
@ -621,8 +574,6 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
ErrorDetailsDialog(this, listOf(event.error)).show()
}
navView.toolbar.apply {
subtitleFormat = R.string.toolbar_subtitle
subtitleFormatWithUnread = R.plurals.toolbar_subtitle_with_unread
subtitle = "Gotowe"
}
mainSnackbar.dismiss()
@ -829,7 +780,13 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
d(TAG, "Activity resumed")
val filter = IntentFilter()
filter.addAction(Intent.ACTION_MAIN)
registerReceiver(intentReceiver, filter)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
registerReceiver(intentReceiver, filter, RECEIVER_NOT_EXPORTED)
else
@Suppress("UnspecifiedRegisterReceiverFlag")
registerReceiver(intentReceiver, filter)
EventBus.getDefault().register(this)
super.onResume()
}
@ -856,6 +813,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
handleIntent(intent?.extras)
}
@Deprecated("Deprecated in Java")
@Suppress("deprecation")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
@ -975,16 +933,19 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
bottomSheet.removeAllContextual()
bottomSheet.toggleGroupEnabled = false
drawer.close()
if (drawer.getSelection() != navTarget.id)
drawer.setSelection(navTarget.id, fireOnClick = false)
navView.toolbar.setTitle(navTarget.titleRes ?: navTarget.nameRes)
navView.bottomBar.fabEnable = false
navView.bottomBar.fabExtended = false
navView.bottomBar.setFabOnClickListener(null)
navView.toolbar.setTitle(navTarget.titleRes ?: navTarget.nameRes)
d("NavDebug", "Navigating from ${this.navTarget.name} to ${navTarget.name}")
val fragment = navTarget.fragmentClass?.newInstance() ?: return
val fragment = navTarget.fragmentClass?.getDeclaredConstructor()?.newInstance() ?: return
fragment.arguments = arguments
val transaction = fragmentManager.beginTransaction()
@ -1051,20 +1012,18 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
transaction.commitAllowingStateLoss()
// TASK DESCRIPTION
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
val bm = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
val bm = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
@Suppress("deprecation")
val taskDesc = ActivityManager.TaskDescription(
if (navTarget == NavTarget.HOME)
getString(R.string.app_name)
else
getString(R.string.app_task_format, getString(navTarget.nameRes)),
bm,
getColorFromAttr(this, R.attr.colorSurface)
)
setTaskDescription(taskDesc)
}
@Suppress("deprecation")
val taskDesc = ActivityManager.TaskDescription(
if (navTarget == NavTarget.HOME)
getString(R.string.app_name)
else
getString(R.string.app_task_format, getString(navTarget.nameRes)),
bm,
getColorFromAttr(this, R.attr.colorSurface)
)
setTaskDescription(taskDesc)
return
}
@ -1094,7 +1053,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
fun navigateUp(skipBeforeNavigate: Boolean = false) {
if (!popBackStack(skipBeforeNavigate)) {
super.onBackPressed()
super.onBackPressedDispatcher.onBackPressed()
}
}
@ -1105,9 +1064,6 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
fun gainAttention() {
if (app.config.ui.bottomSheetOpened)
return
b.navView.postDelayed({
navView.gainAttentionOnBottomBar()
}, 2000)
}
fun gainAttentionFAB() {
@ -1225,6 +1181,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
drawer.addProfileSettings(*drawerProfiles.toTypedArray())
}
@SuppressLint("MissingSuperCall")
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
if (App.config.ui.openDrawerOnBackPressed) {
if (drawer.isOpen)

View File

@ -59,10 +59,11 @@ data class AppData(
val lessonHeight: Int,
val enableMarkAsReadAnnouncements: Boolean,
val enableNoticePoints: Boolean,
val eventManualShowSubjectDropdown: Boolean,
)
data class EventType(
val id: Int,
val id: Long,
val color: String,
val name: String,
)

View File

@ -43,4 +43,6 @@ abstract class BaseConfig(
db.configDao().add(ConfigEntry(profileId ?: -1, key, value))
}
}
fun has(key: String) = values.containsKey(key)
}

View File

@ -15,6 +15,7 @@ class ConfigUI(base: Config) {
var appBackground by base.config<String?>("appBg", null)
var headerBackground by base.config<String?>("headerBg", null)
var lockLayout by base.config<Boolean>(false)
var miniMenuVisible by base.config<Boolean>(false)
var miniMenuButtons by base.config<Set<NavTarget>> {
setOf(

View File

@ -144,7 +144,7 @@ class ConfigDelegate<T>(
java.lang.Float::class.java -> value.toFloatOrNull()
// enums, maps & collections
else -> when {
Enum::class.java.isAssignableFrom(type) -> value.toIntOrNull()?.toEnum(type) as Enum<*>
Enum::class.java.isAssignableFrom(type) -> value.toIntOrNull()?.toEnum(type) as Enum
Collection::class.java.isAssignableFrom(type) -> {
val array = value.toJsonArray()
val genericType = getGenericType()

View File

@ -15,7 +15,7 @@ class ProfileConfig(
entries: List<ConfigEntry>?,
) : BaseConfig(db, profileId, entries) {
companion object {
const val DATA_VERSION = 4
const val DATA_VERSION = 5
}
val grades by lazy { ProfileConfigGrades(this) }

View File

@ -15,6 +15,7 @@ class ProfileConfigUI(base: ProfileConfig) {
var agendaGroupByType by base.config<Boolean>(false)
var agendaLessonChanges by base.config<Boolean>(true)
var agendaTeacherAbsence by base.config<Boolean>(true)
var agendaSubjectImportant by base.config<Boolean>(false)
var agendaElearningMark by base.config<Boolean>(false)
var agendaElearningGroup by base.config<Boolean>(true)

View File

@ -16,6 +16,8 @@ import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_AL
class ProfileConfigMigration(config: ProfileConfig) {
init { config.apply {
val profile = db.profileDao().getByIdNow(profileId ?: -1)
if (dataVersion < 2) {
sync.notificationFilter = sync.notificationFilter + NotificationType.TEACHER_ABSENCE
@ -37,11 +39,23 @@ class ProfileConfigMigration(config: ProfileConfig) {
// switch to new event types (USOS)
dataVersion = 4
val profile = db.profileDao().getByIdNow(profileId ?: -1)
if (profile?.loginStoreType?.schoolType == SchoolType.UNIVERSITY) {
db.eventTypeDao().clear(profileId ?: -1)
db.eventTypeDao().addDefaultTypes(profile)
}
}
if (dataVersion < 5) {
// update USOS event types and the appropriate events (2022-12-25)
dataVersion = 5
if (profile?.loginStoreType?.schoolType == SchoolType.UNIVERSITY) {
db.eventTypeDao().getAllWithDefaults(profile)
// wejściówka (4) -> kartkówka (3)
db.eventDao().getRawNow("UPDATE events SET eventType = 3 WHERE profileId = $profileId AND eventType = 4;")
// zadanie (6) -> zadanie domowe (-1)
db.eventDao().getRawNow("UPDATE events SET eventType = -1 WHERE profileId = $profileId AND eventType = 6;")
}
}
}}
}

View File

@ -26,9 +26,10 @@ val LIBRUS_USER_AGENT = "${SYSTEM_USER_AGENT}LibrusMobileApp"
const val SYNERGIA_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) Gecko/20100101 Firefox/62.0"
const val LIBRUS_CLIENT_ID = "VaItV6oRutdo8fnjJwysnTjVlvaswf52ZqmXsJGP"
const val LIBRUS_REDIRECT_URL = "app://librus"
const val LIBRUS_AUTHORIZE_URL = "https://portal.librus.pl/oauth2/authorize?client_id=$LIBRUS_CLIENT_ID&redirect_uri=$LIBRUS_REDIRECT_URL&response_type=code"
const val LIBRUS_LOGIN_URL = "https://portal.librus.pl/rodzina/login/action"
const val LIBRUS_AUTHORIZE_URL = "https://portal.librus.pl/konto-librus/redirect/dru"
const val LIBRUS_LOGIN_URL = "https://portal.librus.pl/konto-librus/login/action"
const val LIBRUS_TOKEN_URL = "https://portal.librus.pl/oauth2/access_token"
const val LIBRUS_HEADER = "pl.librus.synergiaDru2"
const val LIBRUS_ACCOUNT_URL = "/v3/SynergiaAccounts/fresh/" // + login
const val LIBRUS_ACCOUNTS_URL = "/v3/SynergiaAccounts"
@ -59,9 +60,6 @@ const val LIBRUS_SANDBOX_URL = "https://sandbox.librus.pl/index.php?action="
const val LIBRUS_SYNERGIA_HOMEWORK_ATTACHMENT_URL = "https://synergia.librus.pl/homework/downloadFile"
const val LIBRUS_SYNERGIA_MESSAGES_ATTACHMENT_URL = "https://synergia.librus.pl/wiadomosci/pobierz_zalacznik"
const val LIBRUS_PORTAL_RECAPTCHA_KEY = "6Lf48moUAAAAAB9ClhdvHr46gRWR"
const val LIBRUS_PORTAL_RECAPTCHA_REFERER = "https://portal.librus.pl/rodzina/login"
val MOBIDZIENNIK_USER_AGENT = SYSTEM_USER_AGENT

View File

@ -24,6 +24,25 @@ object Regexes {
"""^\[META:([A-z0-9-&=]+)]""".toRegex()
}
val HTML_INPUT_HIDDEN by lazy {
"""<input .*?type="hidden".+?>""".toRegex()
}
val HTML_INPUT_NAME by lazy {
"""name="(.+?)"""".toRegex()
}
val HTML_INPUT_VALUE by lazy {
"""value="(.+?)"""".toRegex()
}
val HTML_CSRF_TOKEN by lazy {
"""name="csrf-token" content="([A-z0-9=+/\-_]+?)"""".toRegex()
}
val HTML_FORM_ACTION by lazy {
"""<form .*?action="(.+?)"""".toRegex()
}
val HTML_RECAPTCHA_KEY by lazy {
"""data-sitekey="(.+?)"""".toRegex()
}
val MOBIDZIENNIK_GRADES_SUBJECT_NAME by lazy {

View File

@ -39,6 +39,7 @@ class LibrusRecaptchaHelper(
private var timedOut = false
inner class WebViewClient : android.webkit.WebViewClient() {
@Deprecated("Deprecated in Java")
override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
timeout?.cancel()
if (!timedOut) {

View File

@ -24,6 +24,9 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
private const val TAG = "LoginLibrusPortal"
}
// loop failsafe
private var loginPerformed = false
init { run {
if (data.loginStore.mode != LoginMode.LIBRUS_EMAIL) {
data.error(ApiError(TAG, ERROR_INVALID_LOGIN_MODE))
@ -33,6 +36,7 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING))
return@run
}
loginPerformed = false
// succeed having a non-expired access token and a refresh token
if (data.isPortalLoginValid()) {
@ -58,18 +62,23 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
}
}}
private fun authorize(url: String?) {
private fun authorize(url: String, referer: String? = null) {
d(TAG, "Request: Librus/Login/Portal - $url")
Request.builder()
.url(url)
.userAgent(LIBRUS_USER_AGENT)
.also {
if (referer != null)
it.addHeader("Referer", referer)
}
.addHeader("X-Requested-With", LIBRUS_HEADER)
.withClient(data.app.httpLazy)
.callback(object : TextCallbackHandler() {
override fun onSuccess(text: String, response: Response) {
val location = response.headers().get("Location")
if (location != null) {
val authMatcher = Pattern.compile("$LIBRUS_REDIRECT_URL\\?code=([A-z0-9]+?)$", Pattern.DOTALL or Pattern.MULTILINE).matcher(location)
val authMatcher = Pattern.compile("$LIBRUS_REDIRECT_URL\\?code=([^&?]+)", Pattern.DOTALL or Pattern.MULTILINE).matcher(location)
when {
authMatcher.find() -> {
accessToken(authMatcher.group(1), null)
@ -83,16 +92,31 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
authorize(location)
}
}
} else {
val csrfMatcher = Pattern.compile("name=\"csrf-token\" content=\"([A-z0-9=+/\\-_]+?)\"", Pattern.DOTALL).matcher(text)
if (csrfMatcher.find()) {
login(csrfMatcher.group(1) ?: "")
} else {
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_CSRF_MISSING)
.withResponse(response)
.withApiResponse(text))
return
}
if (checkError(text, response))
return
var loginUrl = if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL
val csrfToken = Regexes.HTML_CSRF_TOKEN.find(text)?.get(1) ?: ""
for (match in Regexes.HTML_FORM_ACTION.findAll(text)) {
val form = match.value.lowercase()
if ("login" in form && "post" in form) {
loginUrl = match[1]
}
}
val params = mutableMapOf<String, String>()
for (match in Regexes.HTML_INPUT_HIDDEN.findAll(text)) {
val input = match.value
val name = Regexes.HTML_INPUT_NAME.find(input)?.get(1) ?: continue
val value = Regexes.HTML_INPUT_VALUE.find(input)?.get(1) ?: continue
params[name] = value
}
login(url = loginUrl, referer = url, csrfToken, params)
}
override fun onFailure(response: Response, throwable: Throwable) {
@ -105,8 +129,54 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
.enqueue()
}
private fun login(csrfToken: String) {
d(TAG, "Request: Librus/Login/Portal - ${if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL}")
private fun checkError(text: String, response: Response): Boolean {
when {
text.contains("librus_account_settings_main") -> return false
text.contains("Sesja logowania wygasła") -> ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED
text.contains("Upewnij się, że nie") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
text.contains("Podany adres e-mail jest nieprawidłowy.") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
else -> null // no error for now
}?.let { errorCode ->
data.error(ApiError(TAG, errorCode)
.withApiResponse(text)
.withResponse(response))
return true
}
if ("robotem" in text || "g-recaptcha" in text || "captchaValidate" in text) {
val siteKey = Regexes.HTML_RECAPTCHA_KEY.find(text)?.get(1)
if (siteKey == null) {
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR)
.withApiResponse(text)
.withResponse(response))
return true
}
data.requireUserAction(
type = UserActionRequiredEvent.Type.RECAPTCHA,
params = Bundle(
"siteKey" to siteKey,
"referer" to response.request().url().toString(),
"userAgent" to LIBRUS_USER_AGENT,
),
errorText = R.string.notification_user_action_required_captcha_librus,
)
return true
}
return false
}
private fun login(
url: String,
referer: String,
csrfToken: String?,
params: Map<String, String>,
) {
if (loginPerformed) {
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR))
return
}
d(TAG, "Request: Librus/Login/Portal - $url")
val recaptchaCode = data.arguments?.getString("recaptchaCode") ?: data.loginStore.getLoginData("recaptchaCode", null)
val recaptchaTime = data.arguments?.getLong("recaptchaTime") ?: data.loginStore.getLoginData("recaptchaTime", 0L)
@ -116,67 +186,46 @@ class LibrusLoginPortal(val data: DataLibrus, val onSuccess: () -> Unit) {
Request.builder()
.url(if (data.fakeLogin) FAKE_LIBRUS_LOGIN else LIBRUS_LOGIN_URL)
.userAgent(LIBRUS_USER_AGENT)
.addHeader("X-Requested-With", LIBRUS_HEADER)
.addHeader("Referer", referer)
.withClient(data.app.httpLazy)
.addParameter("email", data.portalEmail)
.addParameter("password", data.portalPassword)
.also {
if (recaptchaCode != null && System.currentTimeMillis() - recaptchaTime < 2*60*1000 /* 2 minutes */)
it.addParameter("g-recaptcha-response", recaptchaCode)
if (csrfToken != null)
it.addHeader("X-CSRF-TOKEN", csrfToken)
for ((key, value) in params) {
it.addParameter(key, value)
}
}
.addHeader("X-CSRF-TOKEN", csrfToken)
.allowErrorCode(HTTP_BAD_REQUEST)
.allowErrorCode(HTTP_FORBIDDEN)
.contentType(MediaTypeUtils.APPLICATION_JSON)
.contentType(MediaTypeUtils.APPLICATION_FORM)
.post()
.callback(object : JsonCallbackHandler() {
override fun onSuccess(json: JsonObject?, response: Response) {
.callback(object : TextCallbackHandler() {
override fun onSuccess(text: String?, response: Response) {
loginPerformed = true
val location = response.headers()?.get("Location")
if (location == "$LIBRUS_REDIRECT_URL?command=close") {
data.error(ApiError(TAG, ERROR_LIBRUS_PORTAL_MAINTENANCE)
.withApiResponse(json)
.withApiResponse(text)
.withResponse(response))
return
}
if (json == null) {
if (response.parserErrorBody?.contains("wciąż nieaktywne") == true) {
data.error(ApiError(TAG, ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED)
.withResponse(response))
return
}
if (text == null) {
data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY)
.withResponse(response))
return
}
val error = if (response.code() == 200) null else
json.getJsonArray("errors")?.getString(0)
?: json.getJsonObject("errors")?.entrySet()?.firstOrNull()?.value?.asString
if (error?.contains("robotem") == true || json.getBoolean("captchaRequired") == true) {
data.requireUserAction(
type = UserActionRequiredEvent.Type.RECAPTCHA,
params = Bundle(
"siteKey" to LIBRUS_PORTAL_RECAPTCHA_KEY,
"referer" to LIBRUS_PORTAL_RECAPTCHA_REFERER,
),
errorText = R.string.notification_user_action_required_captcha_librus,
)
return
}
error?.let { code ->
when {
code.contains("Sesja logowania wygasła") -> ERROR_LOGIN_LIBRUS_PORTAL_CSRF_EXPIRED
code.contains("Upewnij się, że nie") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
code.contains("Podany adres e-mail jest nieprawidłowy.") -> ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN
else -> ERROR_LOGIN_LIBRUS_PORTAL_ACTION_ERROR
}.let { errorCode ->
data.error(ApiError(TAG, errorCode)
.withApiResponse(json)
.withResponse(response))
return
}
}
authorize(json.getString("redirect", LIBRUS_AUTHORIZE_URL))
authorize(
url = location
?: if (data.fakeLogin)
FAKE_LIBRUS_AUTHORIZE
else
LIBRUS_AUTHORIZE_URL,
referer = referer,
)
}
override fun onFailure(response: Response, throwable: Throwable) {

View File

@ -10,6 +10,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBID
import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb
import pl.szczodrzynski.edziennik.ext.DAY
import pl.szczodrzynski.edziennik.ext.get
import pl.szczodrzynski.edziennik.ext.isNotNullNorBlank
class MobidziennikWebAccountEmail(override val data: DataMobidziennik,
override val lastSync: Long?,
@ -24,7 +25,8 @@ class MobidziennikWebAccountEmail(override val data: DataMobidziennik,
MobidziennikLuckyNumberExtractor(data, text)
val email = Regexes.MOBIDZIENNIK_ACCOUNT_EMAIL.find(text)?.let { it[1] }
data.loginEmail = email
if (email.isNotNullNorBlank())
data.loginEmail = email
data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL, if (email == null) 3* DAY else 7* DAY)
onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL)

View File

@ -16,6 +16,7 @@ import pl.szczodrzynski.edziennik.data.api.models.ApiError
import pl.szczodrzynski.edziennik.ext.JsonObject
import pl.szczodrzynski.edziennik.ext.getJsonObject
import pl.szczodrzynski.edziennik.ext.getString
import pl.szczodrzynski.edziennik.ext.isNotNullNorBlank
import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.utils.Utils
@ -77,7 +78,9 @@ class MobidziennikLoginApi2(val data: DataMobidziennik, val onSuccess: () -> Uni
}
}
data.loginEmail = json.getString("email")
val email = json.getString("email")
if (email.isNotNullNorBlank())
data.loginEmail = email
data.globalId = json.getString("id_global")
data.loginId = json.getString("login")
onSuccess()

View File

@ -26,7 +26,8 @@ class PodlasieApiGrades(val data: DataPodlasie, val rows: List<JsonObject>) {
val weight = grade.getFloat("Weight") ?: 0f
val includeToAverage = grade.getInt("IncludeToAverage") != 0
val color = grade.getString("Color")?.let { Color.parseColor(it) } ?: -1
val category = grade.getString("Category") ?: ""
val category = grade.getString("Category")
val code = grade.getString("Code")
val comment = grade.getString("Comment") ?: ""
val semester = grade.getString("TermShortcut")?.length ?: data.currentSemester
@ -40,6 +41,12 @@ class PodlasieApiGrades(val data: DataPodlasie, val rows: List<JsonObject>) {
val addedDate = grade.getString("ReceivedDate")?.let { Date.fromY_m_d(it).inMillis }
?: System.currentTimeMillis()
val categoryText: String = when {
code != null && category != null -> "$code - $category"
code != null -> code
else -> category ?: ""
}
val gradeObject = Grade(
profileId = data.profileId,
id = id,
@ -48,7 +55,7 @@ class PodlasieApiGrades(val data: DataPodlasie, val rows: List<JsonObject>) {
value = value,
weight = if (includeToAverage) weight else 0f,
color = color,
category = category,
category = categoryText,
description = null,
comment = comment,
semester = semester,

View File

@ -26,6 +26,7 @@ import pl.szczodrzynski.edziennik.utils.Utils.d
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import java.net.HttpURLConnection
import java.net.HttpURLConnection.HTTP_NOT_FOUND
import java.net.URLEncoder
import java.time.Instant
import java.time.LocalDateTime
@ -183,6 +184,7 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
payload: JsonElement? = null,
baseUrl: Boolean = false,
firebaseToken: String? = null,
allow404: Boolean = false,
crossinline onSuccess: (json: T, response: Response?) -> Unit
) {
val url = "${if (baseUrl) data.apiUrl else data.fullApiUrl}$endpoint"
@ -295,6 +297,19 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
}
override fun onFailure(response: Response?, throwable: Throwable?) {
if (allow404 && response?.code() == HTTP_NOT_FOUND) {
try {
onSuccess(null as T, response)
} catch (e: Exception) {
data.error(
ApiError(tag, EXCEPTION_VULCAN_HEBE_REQUEST)
.withResponse(response)
.withThrowable(e)
)
}
return
}
data.error(
ApiError(tag, ERROR_REQUEST_FAILURE)
.withResponse(response)
@ -338,6 +353,7 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
query: Map<String, String> = mapOf(),
baseUrl: Boolean = false,
firebaseToken: String? = null,
allow404: Boolean = false,
crossinline onSuccess: (json: T, response: Response?) -> Unit
) {
val queryPath = query.map {
@ -348,6 +364,7 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
if (query.isNotEmpty()) "$endpoint?$queryPath" else endpoint,
baseUrl = baseUrl,
firebaseToken = firebaseToken,
allow404 = allow404,
onSuccess = onSuccess
)
}
@ -382,6 +399,7 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
messageBox: String? = null,
params: Map<String, String> = mapOf(),
includeFilterType: Boolean = true,
allow404: Boolean = false,
onSuccess: (data: List<JsonObject>, response: Response?) -> Unit
) {
val url = if (includeFilterType && filterType != null)
@ -406,6 +424,7 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
HebeFilterType.BY_MESSAGEBOX -> {
query["box"] = messageBox ?: data.messageBoxKey ?: ""
}
else -> {}
}
if (dateFrom != null)
@ -427,8 +446,8 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) {
)
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
apiGet(tag, url, query) { json: JsonArray, response ->
onSuccess(json.map { it.asJsonObject }, response)
apiGet(tag, url, query, allow404 = allow404) { json: JsonArray?, response ->
onSuccess(json?.map { it.asJsonObject } ?: listOf(), response)
}
}
}

View File

@ -19,6 +19,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_PARENTS_
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_STUDENT
import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_TEACHER
import pl.szczodrzynski.edziennik.ext.*
import java.net.HttpURLConnection.HTTP_NOT_FOUND
class VulcanHebeAddressbook(
override val data: DataVulcan,
@ -41,8 +42,15 @@ class VulcanHebeAddressbook(
VULCAN_HEBE_ENDPOINT_ADDRESSBOOK,
HebeFilterType.BY_PERSON,
lastSync = lastSync,
includeFilterType = false
) { list, _ ->
includeFilterType = false,
allow404 = true,
) { list, response ->
if (response?.code() == HTTP_NOT_FOUND) {
data.setSyncNext(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK, 2 * DAY)
onSuccess(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK)
return@apiGetList
}
list.forEach { person ->
val id = person.getString("Id") ?: return@forEach

View File

@ -4,7 +4,6 @@
package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_GRADES
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan
import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_GRADES
@ -39,6 +38,7 @@ class VulcanHebeGrades(
val column = grade.getJsonObject("Column")
val category = column.getJsonObject("Category")
val categoryText = category.getString("Name")
val code = column.getString("Code").takeValue()
val teacherId = getTeacherId(grade, "Creator") ?: -1
val subjectId = getSubjectId(column, "Subject") ?: -1
@ -91,6 +91,12 @@ class VulcanHebeGrades(
else
columnColor
val categoryFormattedText: String = when {
code != null && category != null -> "$code - $categoryText"
code != null -> code
else -> categoryText ?: ""
}
val gradeObject = Grade(
profileId = profileId,
id = id,
@ -99,7 +105,7 @@ class VulcanHebeGrades(
value = value ?: 0.0f,
weight = weight,
color = color,
category = categoryText,
category = categoryFormattedText,
description = finalDescription,
comment = null,
semester = getSemester(column),

View File

@ -29,11 +29,12 @@ class SignatureInterceptor(val app: App) : Interceptor {
return chain.proceed(
request.newBuilder()
.header("X-ApiKey", app.config.apiKeyCustom?.takeValue() ?: API_KEY)
.header("X-AppVersion", BuildConfig.VERSION_CODE.toString())
.header("X-Timestamp", timestamp.toString())
.header("X-Signature", sign(timestamp, body, url))
.header("X-AppBuild", BuildConfig.BUILD_TYPE)
.header("X-AppFlavor", BuildConfig.FLAVOR)
.header("X-AppVersion", BuildConfig.VERSION_CODE.toString())
.header("X-DeviceId", app.deviceId)
.header("X-Signature", sign(timestamp, body, url))
.header("X-Timestamp", timestamp.toString())
.build())
}

View File

@ -46,6 +46,6 @@ object Signing {
/*fun provideKey(param1: String, param2: Long): ByteArray {*/
fun pleaseStopRightNow(param1: String, param2: Long): ByteArray {
return "$param1.MTIzNDU2Nzg5MDsJslqno5===.$param2".sha256()
return "$param1.MTIzNDU2Nzg5MD01uMP7oW===.$param2".sha256()
}
}

View File

@ -188,7 +188,7 @@ abstract class AppDb : RoomDatabase() {
Migration97(),
Migration98(),
Migration99(),
Migration100(),
Migration100()
).allowMainThreadQueries().build()
}
}

View File

@ -25,6 +25,9 @@ abstract class EventTypeDao {
@Query("DELETE FROM eventTypes WHERE profileId = :profileId")
abstract fun clear(profileId: Int)
@Query("DELETE FROM eventTypes WHERE profileId = :profileId AND eventTypeSource = :source")
abstract fun clearBySource(profileId: Int, source: Int)
@Query("SELECT * FROM eventTypes WHERE profileId = :profileId AND eventType = :typeId")
abstract fun getByIdNow(profileId: Int, typeId: Long): EventType?
@ -43,7 +46,7 @@ abstract class EventTypeDao {
val typeList = data.eventTypes.map {
EventType(
profileId = profile.id,
id = it.id.toLong(),
id = it.id,
name = it.name,
color = Color.parseColor(it.color),
order = order++,
@ -53,4 +56,21 @@ abstract class EventTypeDao {
addAll(typeList)
return typeList
}
fun getAllWithDefaults(profile: Profile): List<EventType> {
val eventTypes = getAllNow(profile.id)
val defaultIdsExpected = AppData.get(profile.loginStoreType).eventTypes
.map { it.id }
val defaultIdsFound = eventTypes.filter { it.source == SOURCE_DEFAULT }
.sortedBy { it.order }
.map { it.id }
if (defaultIdsExpected == defaultIdsFound)
return eventTypes
clearBySource(profile.id, SOURCE_DEFAULT)
addDefaultTypes(profile)
return eventTypes
}
}

View File

@ -28,6 +28,9 @@ interface ProfileDao {
@Query("SELECT * FROM profiles WHERE profileId = :profileId")
fun getByIdNow(profileId: Int): Profile?
@Query("SELECT * FROM profiles WHERE profileId = :profileId")
suspend fun getByIdSuspend(profileId: Int): Profile?
@get:Query("SELECT * FROM profiles WHERE profileId >= 0 ORDER BY profileId")
val all: LiveData<List<Profile>>

View File

@ -5,31 +5,6 @@ package pl.szczodrzynski.edziennik.data.db.entity
import androidx.room.ColumnInfo
import androidx.room.Entity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_CLASS_EVENT
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_DEFAULT
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_ELEARNING
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_ESSAY
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_EXAM
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_EXCURSION
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_HOMEWORK
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_INFORMATION
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_PROJECT
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_PT_MEETING
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_READING
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_SHORT_QUIZ
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_CLASS_EVENT
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_DEFAULT
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_ELEARNING
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_ESSAY
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_EXAM
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_EXCURSION
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_HOMEWORK
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_INFORMATION
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_PROJECT
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_PT_MEETING
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_READING
import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_SHORT_QUIZ
@Entity(
tableName = "eventTypes",
@ -55,35 +30,5 @@ class EventType(
const val SOURCE_REGISTER = 1
const val SOURCE_CUSTOM = 2
const val SOURCE_SHARED = 3
fun getTypeColorMap() = mapOf(
TYPE_ELEARNING to COLOR_ELEARNING,
TYPE_HOMEWORK to COLOR_HOMEWORK,
TYPE_DEFAULT to COLOR_DEFAULT,
TYPE_EXAM to COLOR_EXAM,
TYPE_SHORT_QUIZ to COLOR_SHORT_QUIZ,
TYPE_ESSAY to COLOR_ESSAY,
TYPE_PROJECT to COLOR_PROJECT,
TYPE_PT_MEETING to COLOR_PT_MEETING,
TYPE_EXCURSION to COLOR_EXCURSION,
TYPE_READING to COLOR_READING,
TYPE_CLASS_EVENT to COLOR_CLASS_EVENT,
TYPE_INFORMATION to COLOR_INFORMATION
)
fun getTypeNameMap() = mapOf(
TYPE_ELEARNING to R.string.event_type_elearning,
TYPE_HOMEWORK to R.string.event_type_homework,
TYPE_DEFAULT to R.string.event_other,
TYPE_EXAM to R.string.event_exam,
TYPE_SHORT_QUIZ to R.string.event_short_quiz,
TYPE_ESSAY to R.string.event_essay,
TYPE_PROJECT to R.string.event_project,
TYPE_PT_MEETING to R.string.event_pt_meeting,
TYPE_EXCURSION to R.string.event_excursion,
TYPE_READING to R.string.event_reading,
TYPE_CLASS_EVENT to R.string.event_class_event,
TYPE_INFORMATION to R.string.event_information
)
}
}

View File

@ -6,6 +6,8 @@ package pl.szczodrzynski.edziennik.data.db.enums
import pl.szczodrzynski.edziennik.data.db.entity.LoginStore
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.ext.getString
import pl.szczodrzynski.edziennik.ext.isNotNullNorBlank
enum class LoginMethod(
val loginType: LoginType,
@ -26,7 +28,7 @@ enum class LoginMethod(
MOBIDZIENNIK_API2(
loginType = LoginType.MOBIDZIENNIK,
id = 1300,
isPossible = { profile, _ -> profile?.studentData?.has("email") ?: false },
isPossible = { profile, _ -> profile?.studentData?.getString("email").isNotNullNorBlank() },
),
LIBRUS_PORTAL(
loginType = LoginType.LIBRUS,
@ -57,7 +59,7 @@ enum class LoginMethod(
VULCAN_WEB_MAIN(
loginType = LoginType.VULCAN,
id = 4100,
isPossible = { _, loginStore -> loginStore.hasLoginData("webHost") },
isPossible = { _, loginStore -> loginStore.getLoginData("webHost", null).isNotNullNorBlank() },
),
VULCAN_HEBE(
loginType = LoginType.VULCAN,

View File

@ -83,13 +83,13 @@ fun AlertDialog.overlayBackgroundColor(color: Int, alpha: Int) {
val backgroundInsets = MaterialDialogs.getDialogBackgroundInsets(
context,
R.attr.alertDialogStyle,
R.style.MaterialAlertDialog_MaterialComponents,
R.style.MaterialAlertDialog_Material3,
)
val background = MaterialShapeDrawable(
context,
null,
R.attr.alertDialogStyle,
R.style.MaterialAlertDialog_MaterialComponents
R.style.MaterialAlertDialog_Material3
)
with(background) {
initializeElevationOverlay(context)

View File

@ -73,7 +73,8 @@ fun JsonObject(vararg properties: Pair<String, Any?>): JsonObject {
is Number -> addProperty(key, value)
is Boolean -> addProperty(key, value)
is Enum<*> -> addProperty(key, value.toInt())
else -> add(key, property.toJsonElement())
null -> add(key, null)
else -> add(key, value.toJsonElement())
}
}
}

View File

@ -21,8 +21,8 @@ import java.io.StringWriter
fun <T> LiveData<T>.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer<T>) {
observe(lifecycleOwner, object : Observer<T> {
override fun onChanged(t: T?) {
observer.onChanged(t)
override fun onChanged(value: T) {
observer.onChanged(value)
removeObserver(this)
}
})
@ -73,6 +73,12 @@ fun pendingIntentFlag(): Int {
return 0
}
fun pendingIntentMutable(): Int {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
return PendingIntent.FLAG_MUTABLE
return 0
}
fun Int?.takeValue() = if (this == -1) null else this
fun Int?.takePositive() = if (this == -1 || this == 0) null else this

View File

@ -15,6 +15,7 @@ import android.text.style.CharacterStyle
import android.text.style.ForegroundColorSpan
import android.text.style.StrikethroughSpan
import android.text.style.StyleSpan
import android.text.style.UnderlineSpan
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import com.mikepenz.materialdrawer.holder.StringHolder
@ -160,6 +161,11 @@ fun CharSequence?.asBoldSpannable(): Spannable {
spannable.setSpan(StyleSpan(Typeface.BOLD), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return spannable
}
fun CharSequence?.asUnderlineSpannable(): Spannable {
val spannable = SpannableString(this)
spannable.setSpan(UnderlineSpan(), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
return spannable
}
fun CharSequence.asSpannable(
vararg spans: CharacterStyle,
substring: CharSequence? = null,

View File

@ -8,6 +8,7 @@ import android.content.Context
import android.content.res.Resources
import android.graphics.Rect
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.*
import androidx.annotation.StringRes
@ -161,3 +162,12 @@ val SwipeRefreshLayout.onScrollListener: RecyclerView.OnScrollListener
}
}
fun View.removeFromParent() {
(parent as? ViewGroup)?.removeView(this)
}
fun View.appendView(child: View) {
val parent = parent as? ViewGroup ?: return
val index = parent.indexOfChild(this)
parent.addView(child, index + 1)
}

View File

@ -5,8 +5,20 @@
package pl.szczodrzynski.edziennik.network.cookie
import okhttp3.Cookie
import okhttp3.HttpUrl
class DumbCookie(var cookie: Cookie) {
companion object {
fun deserialize(key: String, value: String): DumbCookie? {
val (domain, _) = key.split('|', limit = 2)
val url = HttpUrl.Builder()
.scheme("https")
.host(domain)
.build()
val cookie = Cookie.parse(url, value) ?: return null
return DumbCookie(cookie)
}
}
constructor(domain: String, name: String, value: String, expiresAt: Long? = null) : this(
Cookie.Builder()
@ -21,7 +33,10 @@ class DumbCookie(var cookie: Cookie) {
cookie = Cookie.Builder()
.name(cookie.name())
.value(cookie.value())
.expiresAt(cookie.expiresAt())
.also {
if (cookie.persistent())
it.expiresAt(cookie.expiresAt())
}
.domain(cookie.domain())
.build()
}
@ -45,4 +60,7 @@ class DumbCookie(var cookie: Cookie) {
hash = 31 * hash + cookie.domain().hashCode()
return hash
}
fun serializeKey() = cookie.domain() + "|" + cookie.name()
fun serialize() = serializeKey() to cookie.toString()
}

View File

@ -5,6 +5,7 @@
package pl.szczodrzynski.edziennik.network.cookie
import android.content.Context
import androidx.core.content.edit
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
@ -26,22 +27,48 @@ class DumbCookieJar(
) : CookieJar {
private val prefs = context.getSharedPreferences("cookies", Context.MODE_PRIVATE)
val sessionCookies = mutableSetOf<DumbCookie>()
private val savedCookies = mutableSetOf<DumbCookie>()
private val sessionCookies = mutableSetOf<DumbCookie>()
init {
val toRemove = mutableListOf<String>()
prefs.all.forEach { (key, value) ->
if (value !is String)
return@forEach
val dc = DumbCookie.deserialize(key, value) ?: return@forEach
if (dc.cookie.expiresAt() > System.currentTimeMillis())
sessionCookies.add(dc)
else
toRemove.add(key)
}
prefs.edit {
for (key in toRemove) {
remove(key)
}
}
}
private fun save(dc: DumbCookie) {
sessionCookies.remove(dc)
sessionCookies.add(dc)
if (dc.cookie.persistent() || persistAll) {
savedCookies.remove(dc)
savedCookies.add(dc)
prefs.edit {
val (key, value) = dc.serialize()
putString(key, value)
}
}
}
private fun delete(vararg toRemove: DumbCookie) {
sessionCookies.removeAll(toRemove)
savedCookies.removeAll(toRemove)
sessionCookies.removeAll(toRemove.toSet())
prefs.edit {
for (dc in toRemove) {
val key = dc.serializeKey()
if (prefs.contains(key))
remove(key)
}
}
}
override fun saveFromResponse(url: HttpUrl?, cookies: List<Cookie>) {
override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) {
for (cookie in cookies) {
val dc = DumbCookie(cookie)
save(dc)
@ -54,6 +81,10 @@ class DumbCookieJar(
}.map { it.cookie }
}
fun getAllDomains(): List<Cookie> {
return sessionCookies.map { it.cookie }
}
fun get(domain: String, name: String): String? {
return sessionCookies.firstOrNull {
it.domainMatches(domain) && it.cookie.name() == name
@ -84,7 +115,7 @@ class DumbCookieJar(
fun getAll(domain: String): Map<String, String> {
return sessionCookies.filter {
it.domainMatches(domain)
}.map { it.cookie.name() to it.cookie.value() }.toMap()
}.associate { it.cookie.name() to it.cookie.value() }
}
fun remove(domain: String, name: String) {
@ -100,4 +131,11 @@ class DumbCookieJar(
}
delete(*toRemove.toTypedArray())
}
fun clearAllDomains() {
sessionCookies.clear()
prefs.edit {
clear()
}
}
}

View File

@ -23,20 +23,24 @@ object WorkerUtils {
inline fun scheduleNext(app: App, rescheduleIfFailedFound: Boolean = true, crossinline onReschedule: () -> Unit) {
AsyncTask.execute {
val workManager = WorkManager.getInstance(app) as WorkManagerImpl
val scheduledWork = workManager.workDatabase.workSpecDao().scheduledWork
val scheduledWork = workManager.workDatabase.workSpecDao().getScheduledWork() as MutableList;
scheduledWork.forEach {
Utils.d("WorkerUtils", "Work: ${it.id} at ${(it.periodStartTime + it.initialDelay).formatDate()}. State = ${it.state} (finished = ${it.state.isFinished})")
Utils.d("WorkerUtils", "Work: ${it.id} at ${it.calculateNextRunTime().formatDate()}. State = ${it.state} (finished = ${it.state.isFinished})")
}
// remove finished work and other than SyncWorker
scheduledWork.removeAll { it.workerClassName != SyncWorker::class.java.canonicalName || it.isPeriodic || it.state.isFinished }
Utils.d("WorkerUtils", "Found ${scheduledWork.size} unfinished work")
// remove all enqueued work that had to (but didn't) run at some point in the past (at least 1min ago)
val failedWork = scheduledWork.filter { it.state == WorkInfo.State.ENQUEUED && it.periodStartTime + it.initialDelay < System.currentTimeMillis() - 1 * MINUTE * 1000 }
val failedWork = scheduledWork.filter { it.state == WorkInfo.State.ENQUEUED && it.calculateNextRunTime() < System.currentTimeMillis() - 1 * MINUTE * 1000 }
Utils.d("WorkerUtils", "${failedWork.size} work requests failed to start (out of ${scheduledWork.size} requests)")
if (rescheduleIfFailedFound) {
if (failedWork.isNotEmpty()) {
Utils.d("WorkerUtils", "App Manager detected!")
EventBus.getDefault().postSticky(AppManagerDetectedEvent(failedWork.map { it.periodStartTime + it.initialDelay }))
EventBus.getDefault().postSticky(AppManagerDetectedEvent(failedWork.map { it.calculateNextRunTime() }))
}
if (scheduledWork.size - failedWork.size < 1) {
Utils.d("WorkerUtils", "No pending work found, scheduling next:")

View File

@ -142,13 +142,7 @@ class AgendaFragment : Fragment(), CoroutineScope {
private suspend fun checkEventTypes() {
withContext(Dispatchers.Default) {
val eventTypes = app.db.eventTypeDao().getAllNow(app.profileId).map {
it.id
}
val defaultEventTypes = EventType.getTypeColorMap().keys
if (!eventTypes.containsAll(defaultEventTypes)) {
app.db.eventTypeDao().addDefaultTypes(app.profile)
}
app.db.eventTypeDao().getAllWithDefaults(app.profile)
}
}

View File

@ -10,6 +10,7 @@ import android.widget.FrameLayout
import android.widget.TextView
import androidx.core.view.isVisible
import com.github.tibolte.agendacalendarview.render.EventRenderer
import com.google.android.material.color.MaterialColors
import com.mikepenz.iconics.view.IconicsTextView
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.AgendaWrappedEventBinding
@ -46,7 +47,8 @@ class AgendaEventRenderer(
) {
val event = aEvent.event
val textColor = Colors.legibleTextColor(event.eventColor)
val harmonizedColor = MaterialColors.harmonizeWithPrimary(card.context, event.eventColor)
val textColor = Colors.legibleTextColor(harmonizedColor)
val timeText = if (event.time == null)
card.context.getString(R.string.agenda_event_all_day)
@ -60,8 +62,8 @@ class AgendaEventRenderer(
event.teamName
).join(", ")
card.foreground.setTintColor(event.eventColor)
card.background.setTintColor(event.eventColor)
card.foreground.setTintColor(harmonizedColor)
card.background.setTintColor(harmonizedColor)
manager.setEventTopic(title, event, doneIconColor = textColor)
title.setTextColor(textColor)
subtitle?.text = eventSubtitle

View File

@ -17,7 +17,6 @@ import pl.szczodrzynski.edziennik.data.db.full.LessonFull
import pl.szczodrzynski.edziennik.databinding.TimetableLessonBinding
import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.utils.managers.NoteManager
import pl.szczodrzynski.navlib.getColorFromAttr
class LessonChangesAdapter(
val context: Context,
@ -64,7 +63,9 @@ class LessonChangesAdapter(
lesson.teacherName ?: "?"
else
mutableListOf<CharSequence>().apply {
lesson.oldTeacherName?.let { add(it.asStrikethroughSpannable()) }
if (lesson.teacherName == null)
lesson.oldTeacherName?.let { add(it) }
else lesson.oldTeacherName?.let { add(it.asStrikethroughSpannable()) }
lesson.teacherName?.let { add(it) }
}.concat(arrowRight)
@ -73,7 +74,9 @@ class LessonChangesAdapter(
lesson.teamName ?: "?"
else
mutableListOf<CharSequence>().apply {
lesson.oldTeamName?.let { add(it.asStrikethroughSpannable()) }
if (lesson.teamName == null)
lesson.oldTeamName?.let { add(it) }
else lesson.oldTeamName?.let { add(it.asStrikethroughSpannable()) }
lesson.teamName?.let { add(it) }
}.concat(arrowRight)
@ -82,7 +85,9 @@ class LessonChangesAdapter(
lesson.classroom ?: "?"
else
mutableListOf<CharSequence>().apply {
lesson.oldClassroom?.let { add(it.asStrikethroughSpannable()) }
if (lesson.classroom == null)
lesson.oldClassroom?.let { add(it) }
else lesson.oldClassroom?.let { add(it.asStrikethroughSpannable()) }
lesson.classroom?.let { add(it) }
}.concat(arrowRight)
@ -109,40 +114,11 @@ class LessonChangesAdapter(
Lesson.TYPE_CANCELLED -> {
b.annotationVisible = true
b.annotation.setText(R.string.timetable_lesson_cancelled)
b.annotation.background.colorFilter = PorterDuffColorFilter(
getColorFromAttr(context, R.attr.timetable_lesson_cancelled_color),
PorterDuff.Mode.SRC_ATOP
)
//lb.subjectName.typeface = Typeface.DEFAULT
}
Lesson.TYPE_CHANGE -> {
b.annotationVisible = true
when {
lesson.subjectId != lesson.oldSubjectId && lesson.teacherId != lesson.oldTeacherId
&& lesson.oldSubjectName != null && lesson.oldTeacherName != null ->
b.annotation.setText(
R.string.timetable_lesson_change_format,
"${lesson.oldSubjectName ?: "?"}, ${lesson.oldTeacherName ?: "?"}"
)
lesson.subjectId != lesson.oldSubjectId && lesson.oldSubjectName != null ->
b.annotation.setText(
R.string.timetable_lesson_change_format,
lesson.oldSubjectName ?: "?"
)
lesson.teacherId != lesson.oldTeacherId && lesson.oldTeacherName != null ->
b.annotation.setText(
R.string.timetable_lesson_change_format,
lesson.oldTeacherName ?: "?"
)
else -> b.annotation.setText(R.string.timetable_lesson_change)
}
b.annotation.background.colorFilter = PorterDuffColorFilter(
getColorFromAttr(context, R.attr.timetable_lesson_change_color),
PorterDuff.Mode.SRC_ATOP
)
b.annotation.setText(R.string.timetable_lesson_change)
}
Lesson.TYPE_SHIFTED_SOURCE -> {
b.annotationVisible = true
@ -162,8 +138,6 @@ class LessonChangesAdapter(
else -> b.annotation.setText(R.string.timetable_lesson_shifted)
}
b.annotation.background.setTintColor(R.attr.timetable_lesson_shifted_source_color.resolveAttr(context))
}
Lesson.TYPE_SHIFTED_TARGET -> {
b.annotationVisible = true
@ -183,11 +157,6 @@ class LessonChangesAdapter(
else -> b.annotation.setText(R.string.timetable_lesson_shifted_from)
}
b.annotation.background.colorFilter = PorterDuffColorFilter(
getColorFromAttr(context, R.attr.timetable_lesson_shifted_target_color),
PorterDuff.Mode.SRC_ATOP
)
}
}
}

View File

@ -7,6 +7,7 @@ package pl.szczodrzynski.edziennik.ui.agenda.lessonchanges
import android.view.View
import androidx.core.view.isVisible
import com.github.tibolte.agendacalendarview.render.EventRenderer
import com.google.android.material.color.MaterialColors
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.AgendaCounterItemBinding
import pl.szczodrzynski.edziennik.databinding.AgendaWrappedCounterBinding
@ -18,10 +19,11 @@ class LessonChangesEventRenderer : EventRenderer<LessonChangesEvent>() {
override fun render(view: View, event: LessonChangesEvent) {
val b = AgendaWrappedCounterBinding.bind(view).item
val textColor = Colors.legibleTextColor(event.color)
val harmonizedColor = MaterialColors.harmonizeWithPrimary(view.context, event.color)
val textColor = Colors.legibleTextColor(harmonizedColor)
b.card.foreground.setTintColor(event.color)
b.card.background.setTintColor(event.color)
b.card.foreground.setTintColor(harmonizedColor)
b.card.background.setTintColor(harmonizedColor)
b.name.setText(R.string.agenda_lesson_changes)
b.name.setTextColor(textColor)
b.count.text = event.count.toString()
@ -35,10 +37,10 @@ class LessonChangesEventRenderer : EventRenderer<LessonChangesEvent>() {
}
fun render(b: AgendaCounterItemBinding, event: LessonChangesEvent) {
val textColor = Colors.legibleTextColor(event.color)
b.card.foreground.setTintColor(event.color)
b.card.background.setTintColor(event.color)
val harmonizedColor = MaterialColors.harmonizeWithPrimary(b.root.context, event.color)
val textColor = Colors.legibleTextColor(harmonizedColor)
b.card.foreground.setTintColor(harmonizedColor)
b.card.background.setTintColor(harmonizedColor)
b.name.setText(R.string.agenda_lesson_changes)
b.name.setTextColor(textColor)
b.count.text = event.count.toString()

View File

@ -7,6 +7,7 @@ package pl.szczodrzynski.edziennik.ui.agenda.teacherabsence
import android.view.View
import androidx.core.view.isVisible
import com.github.tibolte.agendacalendarview.render.EventRenderer
import com.google.android.material.color.MaterialColors
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.AgendaCounterItemBinding
import pl.szczodrzynski.edziennik.databinding.AgendaWrappedCounterBinding
@ -17,10 +18,11 @@ class TeacherAbsenceEventRenderer : EventRenderer<TeacherAbsenceEvent>() {
override fun render(view: View, event: TeacherAbsenceEvent) {
val b = AgendaWrappedCounterBinding.bind(view).item
val textColor = Colors.legibleTextColor(event.color)
val harmonizedColor = MaterialColors.harmonizeWithPrimary(view.context, event.color)
val textColor = Colors.legibleTextColor(harmonizedColor)
b.card.foreground.setTintColor(event.color)
b.card.background.setTintColor(event.color)
b.card.foreground.setTintColor(harmonizedColor)
b.card.background.setTintColor(harmonizedColor)
b.name.setText(R.string.agenda_teacher_absence)
b.name.setTextColor(textColor)
b.count.text = event.count.toString()
@ -31,10 +33,11 @@ class TeacherAbsenceEventRenderer : EventRenderer<TeacherAbsenceEvent>() {
}
fun render(b: AgendaCounterItemBinding, event: TeacherAbsenceEvent) {
val textColor = Colors.legibleTextColor(event.color)
val harmonizedColor = MaterialColors.harmonizeWithPrimary(b.root.context, event.color)
val textColor = Colors.legibleTextColor(harmonizedColor)
b.card.foreground.setTintColor(event.color)
b.card.background.setTintColor(event.color)
b.card.foreground.setTintColor(harmonizedColor)
b.card.background.setTintColor(harmonizedColor)
b.name.setText(R.string.agenda_teacher_absence)
b.name.setTextColor(textColor)
b.count.text = event.count.toString()

View File

@ -66,7 +66,7 @@ class AttendanceBar : View {
}
@SuppressLint("DrawAllocation", "CanvasSize")
override fun onDraw(canvas: Canvas?) {
override fun onDraw(canvas: Canvas) {
canvas ?: return
val sum = attendancesList.sumOf { it.count }

View File

@ -217,6 +217,7 @@ enum class NavTarget(
location = NavTargetLocation.BOTTOM_SHEET,
nameRes = R.string.menu_debug,
icon = CommunityMaterial.Icon.cmd_android_debug_bridge,
devModeOnly = true,
),
GRADES_EDITOR(
id = 501,

View File

@ -1,186 +0,0 @@
package pl.szczodrzynski.edziennik.ui.behaviour;
import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;
import android.graphics.Color;
import android.os.AsyncTask;
import android.os.Bundle;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.PopupMenu;
import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.Fragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import pl.szczodrzynski.edziennik.App;
import pl.szczodrzynski.edziennik.MainActivity;
import pl.szczodrzynski.edziennik.R;
import pl.szczodrzynski.edziennik.data.db.entity.Notice;
import pl.szczodrzynski.edziennik.data.db.enums.MetadataType;
import pl.szczodrzynski.edziennik.data.db.full.NoticeFull;
import pl.szczodrzynski.edziennik.databinding.FragmentBehaviourBinding;
import pl.szczodrzynski.edziennik.utils.Themes;
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem;
public class BehaviourFragment extends Fragment {
private App app = null;
private MainActivity activity = null;
private FragmentBehaviourBinding b = null;
private int displayMode = MODE_YEAR;
private static final int MODE_YEAR = 0;
private static final int MODE_SEMESTER_1 = 1;
private static final int MODE_SEMESTER_2 = 2;
private List<NoticeFull> noticeList = null;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
activity = (MainActivity) getActivity();
if (getActivity() == null || getContext() == null)
return null;
app = (App) activity.getApplication();
getContext().getTheme().applyStyle(Themes.INSTANCE.getAppTheme(), true);
// activity, context and profile is valid
b = DataBindingUtil.inflate(inflater, R.layout.fragment_behaviour, container, false);
b.refreshLayout.setParent(activity.getSwipeRefreshLayout());
return b.getRoot();
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
if (app == null || activity == null || b == null || !isAdded())
return;
activity.getBottomSheet().prependItems(
new BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_mark_as_read)
.withIcon(CommunityMaterial.Icon.cmd_eye_check_outline)
.withOnClickListener(v3 -> {
activity.getBottomSheet().close();
AsyncTask.execute(() -> App.Companion.getDb().metadataDao().setAllSeen(App.Companion.getProfileId(), MetadataType.NOTICE, true));
Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show();
})
);
/*b.refreshLayout.setOnRefreshListener(() -> {
activity.syncCurrentFeature(MainActivity.DRAWER_ITEM_BEHAVIOUR, b.refreshLayout);
});*/
b.noticesSummaryTitle.setOnClickListener((v -> {
PopupMenu popupMenu = new PopupMenu(activity, b.noticesSummaryTitle, Gravity.END);
popupMenu.getMenu().add(0, 0, 0, R.string.summary_mode_year);
popupMenu.getMenu().add(0, 1, 1, R.string.summary_mode_semester_1);
popupMenu.getMenu().add(0, 2, 2, R.string.summary_mode_semester_2);
popupMenu.setOnMenuItemClickListener((item -> {
displayMode = item.getItemId();
updateList();
return true;
}));
popupMenu.show();
}));
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext());
b.noticesView.setHasFixedSize(true);
b.noticesView.setLayoutManager(linearLayoutManager);
b.noticesView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (recyclerView.canScrollVertically(-1)) {
b.refreshLayout.setEnabled(false);
}
if (!recyclerView.canScrollVertically(-1) && newState == SCROLL_STATE_IDLE) {
b.refreshLayout.setEnabled(true);
}
}
});
app.getDb().noticeDao().getAll(App.Companion.getProfileId()).observe(getViewLifecycleOwner(), notices -> {
if (app == null || activity == null || b == null || !isAdded())
return;
if (notices == null) {
b.noticesView.setVisibility(View.GONE);
b.noticesNoData.setVisibility(View.VISIBLE);
return;
}
noticeList = notices;
updateList();
});
}
private void updateList() {
int praisesCount = 0;
int warningsCount = 0;
int otherCount = 0;
List<NoticeFull> filteredList = new ArrayList<>();
for (NoticeFull notice: noticeList) {
if (displayMode != MODE_YEAR && notice.getSemester() != displayMode)
continue;
filteredList.add(notice);
switch (notice.getType()) {
case Notice.TYPE_POSITIVE:
praisesCount++;
break;
case Notice.TYPE_NEGATIVE:
warningsCount++;
break;
case Notice.TYPE_NEUTRAL:
otherCount++;
break;
}
}
if (filteredList.size() > 0) {
NoticesAdapter adapter;
b.noticesView.setVisibility(View.VISIBLE);
b.noticesNoData.setVisibility(View.GONE);
if ((adapter = (NoticesAdapter) b.noticesView.getAdapter()) != null) {
adapter.setNoticeList(filteredList);
adapter.notifyDataSetChanged();
}
else {
adapter = new NoticesAdapter(getContext(), filteredList);
b.noticesView.setAdapter(adapter);
}
}
else {
b.noticesView.setVisibility(View.GONE);
b.noticesNoData.setVisibility(View.VISIBLE);
}
if (displayMode == MODE_YEAR) {
b.noticesSummaryTitle.setText(getString(R.string.notices_summary_title_year));
}
else {
b.noticesSummaryTitle.setText(getString(R.string.notices_summary_title_semester_format, displayMode));
}
b.noticesPraisesCount.setText(String.format(Locale.getDefault(), "%d", praisesCount));
b.noticesWarningsCount.setText(String.format(Locale.getDefault(), "%d", warningsCount));
b.noticesOtherCount.setText(String.format(Locale.getDefault(), "%d", otherCount));
if (warningsCount >= 3) {
b.noticesWarningsCount.setTextColor(Color.RED);
}
else {
b.noticesWarningsCount.setTextColor(Themes.INSTANCE.getPrimaryTextColor(activity));
}
}
}

View File

@ -0,0 +1,146 @@
package pl.szczodrzynski.edziennik.ui.behaviour
import android.graphics.Color
import android.os.AsyncTask
import android.os.Bundle
import android.view.*
import android.widget.Toast
import androidx.databinding.DataBindingUtil
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.App.Companion.profileId
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Notice
import pl.szczodrzynski.edziennik.data.db.enums.MetadataType
import pl.szczodrzynski.edziennik.data.db.full.NoticeFull
import pl.szczodrzynski.edziennik.databinding.FragmentBehaviourBinding
import pl.szczodrzynski.edziennik.utils.Themes.getPrimaryTextColor
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
import java.util.*
class BehaviourFragment : Fragment() {
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: FragmentBehaviourBinding
private var displayMode = MODE_YEAR
private var noticeList: List<NoticeFull>? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
context ?: return null
app = activity.application as App
b = DataBindingUtil.inflate(inflater, R.layout.fragment_behaviour, container, false)
b.refreshLayout.setParent(activity.swipeRefreshLayout)
return b.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (app == null || activity == null || b == null || !isAdded) return
activity.bottomSheet.prependItems(
BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_mark_as_read)
.withIcon(CommunityMaterial.Icon.cmd_eye_check_outline)
.withOnClickListener { v3: View? ->
activity.bottomSheet.close()
AsyncTask.execute {
App.db.metadataDao().setAllSeen(profileId, MetadataType.NOTICE, true)
}
Toast.makeText(
activity,
R.string.main_menu_mark_as_read_success,
Toast.LENGTH_SHORT
).show()
}
)
b.toggleGroup.check(when (displayMode) {
0 -> R.id.allYear
1 -> R.id.semester1
2 -> R.id.semester2
else -> R.id.allYear
})
b.toggleGroup.addOnButtonCheckedListener { _, checkedId, isChecked ->
if (!isChecked)
return@addOnButtonCheckedListener
displayMode = when (checkedId) {
R.id.allYear -> 0
R.id.semester1 -> 1
R.id.semester2 -> 2
else -> 0
}
updateList()
}
/*b.refreshLayout.setOnRefreshListener(() -> {
activity.syncCurrentFeature(MainActivity.DRAWER_ITEM_BEHAVIOUR, b.refreshLayout);
});*/
val linearLayoutManager = LinearLayoutManager(context)
b.noticesView.setHasFixedSize(true)
b.noticesView.layoutManager = linearLayoutManager
b.noticesView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (recyclerView.canScrollVertically(-1)) {
b.refreshLayout.isEnabled = false
}
if (!recyclerView.canScrollVertically(-1) && newState == RecyclerView.SCROLL_STATE_IDLE) {
b.refreshLayout.isEnabled = true
}
}
})
App.db.noticeDao().getAll(profileId).observe(viewLifecycleOwner) { notices: List<NoticeFull>? ->
if (app == null || activity == null || b == null || !isAdded) return@observe
if (notices == null) {
b.noticesView.visibility = View.GONE
b.noticesNoData.visibility = View.VISIBLE
return@observe
}
noticeList = notices
updateList()
}
}
private fun updateList() {
var praisesCount = 0
var warningsCount = 0
var otherCount = 0
val filteredList: MutableList<NoticeFull> = ArrayList()
for (notice in noticeList!!) {
if (displayMode != MODE_YEAR && notice.semester != displayMode) continue
filteredList.add(notice)
when (notice.type) {
Notice.TYPE_POSITIVE -> praisesCount++
Notice.TYPE_NEGATIVE -> warningsCount++
Notice.TYPE_NEUTRAL -> otherCount++
}
}
if (filteredList.size > 0) {
val adapter = NoticesAdapter(requireContext(), filteredList)
b.noticesView.visibility = View.VISIBLE
b.noticesNoData.visibility = View.GONE
b.noticesView.adapter = adapter
adapter.noticeList = filteredList
adapter.notifyDataSetChanged()
} else {
b.noticesView.visibility = View.GONE
b.noticesNoData.visibility = View.VISIBLE
}
b.noticesPraisesCount.text = String.format(Locale.getDefault(), "%d", praisesCount)
b.noticesWarningsCount.text = String.format(Locale.getDefault(), "%d", warningsCount)
b.noticesOtherCount.text = String.format(Locale.getDefault(), "%d", otherCount)
if (warningsCount >= 3) {
b.noticesWarningsCount.setTextColor(Color.RED)
} else {
b.noticesWarningsCount.setTextColor(getPrimaryTextColor(activity))
}
}
companion object {
private const val MODE_YEAR = 0
private const val MODE_SEMESTER_1 = 1
private const val MODE_SEMESTER_2 = 2
}
}

View File

@ -11,8 +11,10 @@ import android.widget.ImageView
import android.widget.TextView
import androidx.cardview.widget.CardView
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.color.MaterialColors
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.colorRes
import com.mikepenz.iconics.utils.sizeDp
import eu.szkolny.font.SzkolnyFont
@ -20,9 +22,11 @@ import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Notice
import pl.szczodrzynski.edziennik.data.db.full.NoticeFull
import pl.szczodrzynski.edziennik.ext.resolveColor
import pl.szczodrzynski.edziennik.utils.BetterLink
import pl.szczodrzynski.edziennik.utils.Utils.bs
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.navlib.getColorFromAttr
class NoticesAdapter//getting the context and product list with constructor
(private val context: Context, var noticeList: List<NoticeFull>) : RecyclerView.Adapter<NoticesAdapter.ViewHolder>() {
@ -41,9 +45,19 @@ class NoticesAdapter//getting the context and product list with constructor
if (app.data.uiConfig.enableNoticePoints && false) {
holder.noticesItemReason.text = bs(null, notice.category, "\n") + notice.text
holder.noticesItemTeacherName.text = app.getString(R.string.notices_points_format, notice.teacherName, if (notice.points ?: 0f > 0) "+" + notice.points else notice.points)
} else {
if (notice.teacherName != null || notice.points != null) {
holder.noticesItemTeacherName.visibility = View.VISIBLE
holder.noticesItemTeacherName.text = app.getString(
R.string.notices_points_format,
notice.teacherName,
if (notice.points ?: 0f > 0) "+" + notice.points else notice.points
)
}} else {
holder.noticesItemReason.text = notice.text
if (notice.teacherName != null) {
holder.noticesItemTeacherName.visibility = View.VISIBLE
holder.noticesItemTeacherName.text = notice.teacherName
}
holder.noticesItemTeacherName.text = notice.teacherName
}
holder.noticesItemAddedDate.text = Date.fromMillis(notice.addedDate).formattedString
@ -51,21 +65,21 @@ class NoticesAdapter//getting the context and product list with constructor
if (notice.type == Notice.TYPE_POSITIVE) {
holder.noticesItemType.setImageDrawable(
IconicsDrawable(context, CommunityMaterial.Icon3.cmd_plus_circle_outline).apply {
colorRes = R.color.md_green_600
colorInt = MaterialColors.harmonizeWithPrimary(context, R.color.md_green_600.resolveColor(context))
sizeDp = 36
}
)
} else if (notice.type == Notice.TYPE_NEGATIVE) {
holder.noticesItemType.setImageDrawable(
IconicsDrawable(context, CommunityMaterial.Icon.cmd_alert_decagram_outline).apply {
colorRes = R.color.md_red_600
colorInt = MaterialColors.harmonizeWithPrimary(context, R.color.md_red_600.resolveColor(context))
sizeDp = 36
}
)
} else {
holder.noticesItemType.setImageDrawable(
IconicsDrawable(context, SzkolnyFont.Icon.szf_message_processing_outline).apply {
colorRes = R.color.md_blue_500
colorInt = MaterialColors.harmonizeWithPrimary(context, R.color.md_blue_500.resolveColor(context))
sizeDp = 36
}
)

View File

@ -25,6 +25,7 @@ class RecaptchaDialog(
private val autoRetry: Boolean = true,
private val onSuccess: (recaptchaCode: String) -> Unit,
private val onFailure: (() -> Unit)? = null,
private val onServerError: (() -> Unit)? = null,
onShowListener: ((tag: String) -> Unit)? = null,
onDismissListener: ((tag: String) -> Unit)? = null,
) : BindingDialog<RecaptchaDialogBinding>(activity, onShowListener, onDismissListener) {
@ -44,7 +45,11 @@ class RecaptchaDialog(
override suspend fun onBeforeShow(): Boolean {
val (title, text, bitmap) = withContext(Dispatchers.Default) {
val html = loadCaptchaHtml() ?: return@withContext null
val html = loadCaptchaHtml()
if (html == null) {
onServerError?.invoke()
return@withContext null
}
return@withContext loadCaptchaData(html)
} ?: run {
onFailure?.invoke()

View File

@ -19,6 +19,7 @@ class RecaptchaPromptDialog(
private val referer: String,
private val onSuccess: (recaptchaCode: String) -> Unit,
private val onCancel: (() -> Unit)?,
private val onServerError: (() -> Unit)? = null,
onShowListener: ((tag: String) -> Unit)? = null,
onDismissListener: ((tag: String) -> Unit)? = null,
) : BindingDialog<RecaptchaViewBinding>(activity, onShowListener, onDismissListener) {
@ -62,7 +63,8 @@ class RecaptchaPromptDialog(
b.checkbox.background = checkboxBackground
b.checkbox.foreground = checkboxForeground
b.progress.visibility = View.GONE
}
},
onServerError = onServerError,
).show()
}
}

View File

@ -23,6 +23,7 @@ import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.config.Config
import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.SignatureInterceptor
import pl.szczodrzynski.edziennik.data.db.entity.EventType.Companion.SOURCE_DEFAULT
import pl.szczodrzynski.edziennik.databinding.LabFragmentBinding
import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.ui.base.lazypager.LazyFragment
@ -65,6 +66,7 @@ class LabPageFragment : LazyFragment(), CoroutineScope {
b.clearEndpointTimers.isVisible = false
b.rodo.isVisible = false
b.removeHomework.isVisible = false
b.resetEventTypes.isVisible = false
b.unarchive.isVisible = false
b.profile.isVisible = false
}
@ -100,6 +102,11 @@ class LabPageFragment : LazyFragment(), CoroutineScope {
app.db.eventDao().getRawNow("UPDATE events SET homeworkBody = NULL WHERE profileId = ${App.profileId}")
}
b.resetEventTypes.onClick {
app.db.eventTypeDao().clearBySource(App.profileId, SOURCE_DEFAULT)
app.db.eventTypeDao().getAllWithDefaults(App.profile)
}
b.chucker.isChecked = App.enableChucker
b.chucker.onChange { _, isChecked ->
app.config.enableChucker = isChecked
@ -172,28 +179,39 @@ class LabPageFragment : LazyFragment(), CoroutineScope {
return@setOnChangeListener true
}
b.clearCookies.onClick {
app.cookieJar.clearAllDomains()
}
val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity)
startCoroutineTimer(500L, 300L) {
val text = app.cookieJar.sessionCookies
.map { it.cookie }
.sortedBy { it.domain() }
.groupBy { it.domain() }
.map {
listOf(
it.key.asBoldSpannable(),
":\n",
it.value
.sortedBy { it.name() }
.map {
listOf(
" ",
it.name(),
"=",
it.value().decode().take(40).asItalicSpannable().asColoredSpannable(colorSecondary)
).concat("")
}.concat("\n")
).concat("")
}.concat("\n\n")
val text = app.cookieJar.getAllDomains()
.sortedBy { it.domain() }
.groupBy { it.domain() }
.map { pair ->
listOf(
pair.key.asBoldSpannable(),
":\n",
pair.value
.sortedBy { it.name() }
.map { cookie ->
listOf(
" ",
if (cookie.persistent())
cookie.name()
.asUnderlineSpannable()
else
cookie.name(),
"=",
cookie.value()
.decode()
.take(40)
.asItalicSpannable()
.asColoredSpannable(colorSecondary),
).concat("")
}.concat("\n")
).concat("")
}.concat("\n\n")
b.cookies.text = text
}

View File

@ -129,7 +129,7 @@ class LabProfileFragment : LazyFragment(), CoroutineScope {
is String -> input
is Long -> input.toLong()
is Double -> input.toDouble()
is Enum<*> -> input.toInt().toEnum(objVal::class.java)
is Enum<*> -> input.toInt().toEnum(objVal::class.java) as Enum
else -> input
}
field.set(parent, newVal)

View File

@ -46,7 +46,7 @@ class ProfileConfigDialog(
activity,
null,
R.attr.alertDialogStyle,
R.style.MaterialAlertDialog_MaterialComponents
R.style.MaterialAlertDialog_Material3
)
val surface = MaterialColors.getColor(activity, R.attr.colorSurface, TAG)
shape.setCornerSize(18.dp.toFloat())

View File

@ -14,6 +14,7 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
@ -111,11 +112,22 @@ class EventDetailsDialog(
manager.setLegendText(b.legend, event, showNotes)
b.typeColor.background?.setTintColor(event.eventColor)
b.typeColor.background?.setTintColor(MaterialColors.harmonizeWithPrimary(b.root.context, event.eventColor))
b.details = mutableListOf(
val agendaSubjectImportant = event.subjectLongName != null
&& App.config[event.profileId].ui.agendaSubjectImportant
b.name = if (agendaSubjectImportant)
event.subjectLongName
else
event.typeName
b.details = listOfNotNull(
if (agendaSubjectImportant)
event.typeName
else
event.subjectLongName,
event.teamName?.asColoredSpannable(colorSecondary)
event.teamName?.asColoredSpannable(colorSecondary)
).concat(bullet)
b.addedBy.setText(

View File

@ -24,6 +24,7 @@ class EventListAdapter(
val showDate: Boolean = false,
val showColor: Boolean = true,
val showType: Boolean = true,
val showTypeColor: Boolean = showType,
val showTime: Boolean = true,
val showSubject: Boolean = true,
val markAsSeen: Boolean = true,

View File

@ -10,6 +10,7 @@ import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.jaredrummler.android.colorpicker.ColorPickerDialog
import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
@ -19,7 +20,9 @@ import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.config.AppData
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskAllFinishedEvent
import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent
@ -35,9 +38,11 @@ import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.data.db.full.LessonFull
import pl.szczodrzynski.edziennik.databinding.DialogEventManualV2Binding
import pl.szczodrzynski.edziennik.ext.JsonObject
import pl.szczodrzynski.edziennik.ext.appendView
import pl.szczodrzynski.edziennik.ext.getStudentData
import pl.szczodrzynski.edziennik.ext.onChange
import pl.szczodrzynski.edziennik.ext.onClick
import pl.szczodrzynski.edziennik.ext.removeFromParent
import pl.szczodrzynski.edziennik.ext.setText
import pl.szczodrzynski.edziennik.ext.setTintColor
import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog
@ -117,6 +122,15 @@ class EventManualDialog(
}
override suspend fun onShow() {
val data = withContext(Dispatchers.IO) {
val profile = app.db.profileDao().getByIdSuspend(profileId) ?: return@withContext null
AppData.get(profile.loginStoreType)
}
if (data?.uiConfig?.eventManualShowSubjectDropdown == true) {
b.subjectDropdownLayout.removeFromParent()
b.timeDropdownLayout.appendView(b.subjectDropdownLayout)
}
b.showMore.onClick { // TODO iconics is broken
it.apply {
refreshDrawableState()
@ -338,7 +352,7 @@ class EventManualDialog(
selectDefault(defaultType)
onTypeSelected = {
b.typeColor.background.setTintColor(it.color)
b.typeColor.background.setTintColor(MaterialColors.harmonizeWithPrimary(b.root.context, it.color))
customColor = null
}
}

View File

@ -9,6 +9,7 @@ import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.color.MaterialColors
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.full.EventFull
@ -112,8 +113,8 @@ class EventViewHolder(
b.attachmentIcon.isVisible = item.hasAttachments
b.typeColor.background?.setTintColor(item.eventColor)
b.typeColor.isVisible = adapter.showType && adapter.showColor
b.typeColor.background?.setTintColor(MaterialColors.harmonizeWithPrimary(b.root.context, item.eventColor))
b.typeColor.isVisible = adapter.showTypeColor
b.editButton.isVisible = !adapter.simpleMode
&& item.addedManually

View File

@ -39,7 +39,7 @@ class GradeDetailsDialog(
override suspend fun onShow() {
val manager = app.gradesManager
val gradeColor = manager.getGradeColor(grade)
val gradeColor = manager.getGradeColor(b.root.context, grade)
b.grade = grade
b.weightText = manager.getWeightString(app, grade)
b.commentVisible = false

View File

@ -15,6 +15,7 @@ import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
import android.widget.LinearLayout
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.graphics.ColorUtils
import com.google.android.material.color.MaterialColors
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Grade
import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_SEMESTER1_FINAL
@ -47,7 +48,7 @@ class GradeView : AppCompatTextView {
val gradeName = grade.name
val gradeColor = manager.getGradeColor(grade)
val gradeColor = manager.getGradeColor(context, grade)
text = when {
periodGradesTextual -> when (grade.type) {

View File

@ -4,8 +4,10 @@
package pl.szczodrzynski.edziennik.ui.home
import android.graphics.BitmapFactory
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import com.jetradarmobile.snowfall.SnowfallView
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
@ -20,6 +22,7 @@ import pl.szczodrzynski.edziennik.ext.startCoroutineTimer
import pl.szczodrzynski.edziennik.ext.timeLeft
import pl.szczodrzynski.edziennik.ext.timeTill
import pl.szczodrzynski.edziennik.ui.dialogs.BellSyncTimeChooseDialog
import pl.szczodrzynski.edziennik.utils.BigNightUtil
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import kotlin.coroutines.CoroutineContext
@ -61,6 +64,7 @@ class CounterActivity : AppCompatActivity(), CoroutineScope {
it.type != Lesson.TYPE_SHIFTED_SOURCE
})
}
lessonList.onEach { it.filterNotes() }
}
b.bellSync.setImageDrawable(
@ -81,6 +85,27 @@ class CounterActivity : AppCompatActivity(), CoroutineScope {
counterJob = startCoroutineTimer(repeatMillis = 500) {
update()
}
// IT'S WINTER MY DUDES
val today = Date.getToday()
if ((today.month / 3 % 4 == 0) && app.config.ui.snowfall) {
b.rootFrame.addView(layoutInflater.inflate(R.layout.snowfall, b.rootFrame, false))
} else if (app.config.ui.eggfall && BigNightUtil().isDataWielkanocyNearDzisiaj()) {
val eggfall = layoutInflater.inflate(
R.layout.eggfall,
b.rootFrame,
false
) as SnowfallView
eggfall.setSnowflakeBitmaps(listOf(
BitmapFactory.decodeResource(resources, R.drawable.egg1),
BitmapFactory.decodeResource(resources, R.drawable.egg2),
BitmapFactory.decodeResource(resources, R.drawable.egg3),
BitmapFactory.decodeResource(resources, R.drawable.egg4),
BitmapFactory.decodeResource(resources, R.drawable.egg5),
BitmapFactory.decodeResource(resources, R.drawable.egg6)
))
b.rootFrame.addView(eggfall)
}
}}
private fun update() {
@ -101,13 +126,15 @@ class CounterActivity : AppCompatActivity(), CoroutineScope {
when {
actual != null -> {
b.lessonName.text = actual.displaySubjectName
b.lessonName.text = actual.getNoteSubstituteText(showNotes = true)
?: actual.displaySubjectName
val left = actual.displayEndTime!! - now
b.timeLeft.text = timeLeft(left.toInt(), "\n", countInSeconds)
}
next != null -> {
b.lessonName.text = next.displaySubjectName
b.lessonName.text = next.getNoteSubstituteText(showNotes = true)
?: next.displaySubjectName
val till = next.displayStartTime!! - now
b.timeLeft.text = timeTill(till.toInt(), "\n", countInSeconds)

View File

@ -136,6 +136,7 @@ class HomeFragment : Fragment(), CoroutineScope {
Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show()
})
)
b.configureCards.onClick {
HomeConfigDialog(activity, reloadOnDismiss = true).show()
}
@ -168,56 +169,78 @@ class HomeFragment : Fragment(), CoroutineScope {
else -> null
} as HomeCard?
}
//if (App.devMode)
// items += HomeDebugCard(100, app, activity, this, app.profile)
if (app.profile.archived)
items.add(0, HomeArchiveCard(101, app, activity, this, app.profile))
val status = app.availabilityManager.check(app.profile, cacheOnly = true)?.status
val lockLayout = app.config.ui.lockLayout
val update = app.config.update
if (update != null && update.versionCode > BuildConfig.VERSION_CODE || status?.userMessage != null) {
items.add(0, HomeAvailabilityCard(102, app, activity, this, app.profile))
}
val adapter = HomeCardAdapter(items)
val itemTouchHelper = ItemTouchHelper(CardItemTouchHelperCallback(adapter, b.refreshLayout))
adapter.itemTouchHelper = itemTouchHelper
b.list.layoutManager = LinearLayoutManager(activity)
b.list.adapter = adapter
b.list.setAccessibilityDelegateCompat(object : RecyclerViewAccessibilityDelegate(b.list) {
override fun getItemDelegate(): AccessibilityDelegateCompat {
return object : ItemDelegate(this) {
override fun onInitializeAccessibilityNodeInfo(host: View, info: AccessibilityNodeInfoCompat) {
super.onInitializeAccessibilityNodeInfo(host, info)
val position: Int = b.list.getChildLayoutPosition(host)
if (position != 0) {
info.addAction(AccessibilityActionCompat(
R.id.move_card_up_action,
host.resources.getString(R.string.card_action_move_up)
))
}
if (position != adapter.itemCount - 1) {
info.addAction(AccessibilityActionCompat(
R.id.move_card_down_action,
host.resources.getString(R.string.card_action_move_down)
))
}
}
b.list.layoutManager = LinearLayoutManager(activity)
override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean {
val fromPosition: Int = b.list.getChildLayoutPosition(host)
if (action == R.id.move_card_down_action) {
swapCards(fromPosition, fromPosition + 1, adapter)
return true
} else if (action == R.id.move_card_up_action) {
swapCards(fromPosition, fromPosition - 1, adapter)
return true
val itemTouchHelper =
ItemTouchHelper(CardItemTouchHelperCallback(adapter, b.refreshLayout))
adapter.itemTouchHelper = itemTouchHelper
if (!lockLayout) {
b.list.setAccessibilityDelegateCompat(object :
RecyclerViewAccessibilityDelegate(b.list) {
override fun getItemDelegate(): AccessibilityDelegateCompat {
return object : ItemDelegate(this) {
override fun onInitializeAccessibilityNodeInfo(
host: View,
info: AccessibilityNodeInfoCompat
) {
super.onInitializeAccessibilityNodeInfo(host, info)
val position: Int = b.list.getChildLayoutPosition(host)
if (position != 0) {
info.addAction(
AccessibilityActionCompat(
R.id.move_card_up_action,
host.resources.getString(R.string.card_action_move_up)
)
)
}
if (position != adapter.itemCount - 1) {
info.addAction(
AccessibilityActionCompat(
R.id.move_card_down_action,
host.resources.getString(R.string.card_action_move_down)
)
)
}
}
override fun performAccessibilityAction(
host: View,
action: Int,
args: Bundle?
): Boolean {
val fromPosition: Int = b.list.getChildLayoutPosition(host)
if (action == R.id.move_card_down_action) {
swapCards(fromPosition, fromPosition + 1, adapter)
return true
} else if (action == R.id.move_card_up_action) {
swapCards(fromPosition, fromPosition - 1, adapter)
return true
}
return super.performAccessibilityAction(host, action, args)
}
return super.performAccessibilityAction(host, action, args)
}
}
}
})
itemTouchHelper.attachToRecyclerView(b.list)
})
itemTouchHelper.attachToRecyclerView(b.list)
}
}
}

View File

@ -63,9 +63,10 @@ class HomeEventsCard(
simpleMode = true,
showWeekDay = true,
showDate = true,
showType = true,
showType = !profile.config.ui.agendaSubjectImportant,
showTypeColor = true,
showTime = false,
showSubject = false,
showSubject = profile.config.ui.agendaSubjectImportant,
markAsSeen = false,
onEventClick = {
EventDetailsDialog(

View File

@ -63,7 +63,7 @@ class HomeLuckyNumberCard(
R.string.home_lucky_number_details_click_to_set
else
R.string.home_lucky_number_details
b.subText.setText(subTextRes, profile.name, profile.studentNumber)
b.subText.setText(subTextRes, profile.studentNumber)
app.db.luckyNumberDao().getNearestFuture(profile.id, today).observe(fragment, Observer { luckyNumber ->
val isYours = luckyNumber?.number == profile.studentNumber
@ -94,7 +94,7 @@ class HomeLuckyNumberCard(
!isYours -> R.drawable.emoji_smiling
else -> R.drawable.emoji_no_face
}
b.image.setImageResource(drawableRes)
b.image.setIconResource(drawableRes)
})
holder.root.onClick {
@ -104,7 +104,7 @@ class HomeLuckyNumberCard(
R.string.home_lucky_number_details_click_to_set
else
R.string.home_lucky_number_details
b.subText.setText(newSubTextRes, profile.name, profile.studentNumber)
b.subText.setText(newSubTextRes, profile.studentNumber)
})
}
}}

View File

@ -93,24 +93,18 @@ class HomeTimetableCard(
b.settings.setImageDrawable(
IconicsDrawable(activity, CommunityMaterial.Icon.cmd_cog_outline).apply {
colorAttr(activity, R.attr.colorIcon)
colorAttr(activity, R.attr.colorOnPrimaryContainer)
sizeDp = 24
}
)
b.bellSync.setImageDrawable(
IconicsDrawable(activity, SzkolnyFont.Icon.szf_alarm_bell_outline).apply {
colorAttr(activity, R.attr.colorIcon)
sizeDp = 24
}
)
b.bellSync.icon = IconicsDrawable(activity, SzkolnyFont.Icon.szf_alarm_bell_outline).apply {
sizeDp = 24
}
b.showCounter.setImageDrawable(
IconicsDrawable(activity, CommunityMaterial.Icon2.cmd_fullscreen).apply {
colorAttr(activity, R.attr.colorIcon)
sizeDp = 24
}
)
b.showCounter.icon = IconicsDrawable(activity, CommunityMaterial.Icon2.cmd_fullscreen).apply {
sizeDp = 24
}
b.bellSync.setOnClickListener {
BellSyncTimeChooseDialog(
@ -232,6 +226,7 @@ class HomeTimetableCard(
}
lessons = lessons.filter { it.type != Lesson.TYPE_NO_LESSONS }
lessons.onEach { it.filterNotes() }
b.timetableLayout.visibility = View.VISIBLE
b.noTimetableLayout.visibility = View.GONE
@ -266,9 +261,11 @@ class HomeTimetableCard(
R.string.home_timetable_lesson_not_started
b.lessonBig.setText(lessonRes, firstLesson.subjectSpannable)
firstLesson?.displayClassroom?.let {
b.classroomHeading.visibility = View.VISIBLE
b.classroom.visibility = View.VISIBLE
b.classroom.text = it
} ?: run {
b.classroomHeading.visibility = View.GONE
b.classroom.visibility = View.GONE
}
@ -310,9 +307,11 @@ class HomeTimetableCard(
b.counter.visibility = View.VISIBLE
b.counter.text = firstLesson?.displayStartTime?.stringHM
firstLesson?.displayClassroom?.let {
b.classroomHeading.visibility = View.VISIBLE
b.classroom.visibility = View.VISIBLE
b.classroom.text = it
} ?: run {
b.classroomHeading.visibility = View.GONE
b.classroom.visibility = View.GONE
}
@ -333,7 +332,7 @@ class HomeTimetableCard(
for (lesson in nextLessons) {
text += listOf(
lesson.displayStartTime?.stringHM,
adjustTimeWidth(lesson.displayStartTime?.stringHM),
lesson.subjectSpannable
).concat(" ")
}
@ -342,8 +341,15 @@ class HomeTimetableCard(
b.nextLessons.text = text.concat("\n")
}}
private fun adjustTimeWidth(time: String?) = when {
time == null -> ""
time.length == 4 -> " $time "
else -> "$time "
}
private val LessonFull?.subjectSpannable: CharSequence
get() = if (this == null) "?" else when {
hasReplacingNotes() -> getNoteSubstituteText(showNotes = true) ?: "?"
isCancelled -> displaySubjectName?.asStrikethroughSpannable() ?: "?"
isChange -> displaySubjectName?.asItalicSpannable() ?: "?"
else -> displaySubjectName ?: "?"
@ -395,7 +401,7 @@ class HomeTimetableCard(
if (diff >= 60 * MINUTE)
b.counter.text = counterStart.stringHM
else
b.counter.text = activity.timeTill(diff.toInt(), "\n", countInSeconds)
b.counter.text = activity.timeTill(diff.toInt(), " ", countInSeconds)
}
else {
// the lesson is right now
@ -404,7 +410,7 @@ class HomeTimetableCard(
val lessonLength = counterEnd - counterStart
val timePassed = now - counterStart
val timeLeft = counterEnd - now
b.counter.text = activity.timeLeft(timeLeft.toInt(), "\n", countInSeconds)
b.counter.text = activity.timeLeft(timeLeft.toInt(), " ", countInSeconds)
b.progress.max = lessonLength.toInt()
b.progress.progress = timePassed.toInt()
}

View File

@ -46,6 +46,7 @@ class LoginActivity : AppCompatActivity(), CoroutineScope {
fun getRootView() = b.root
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
val destination = nav.currentDestination ?: run {
nav.navigateUp()
@ -86,7 +87,7 @@ class LoginActivity : AppCompatActivity(), CoroutineScope {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTheme(R.style.AppTheme_Light)
setTheme(R.style.AppTheme_Dark)
navOptionsBuilder = NavOptions.Builder()
.setEnterAnim(R.anim.slide_in_right)

View File

@ -228,7 +228,7 @@ class LoginFormFragment : Fragment(), CoroutineScope {
val qrDecoderClass = credential.qrDecoderClass ?: return
app.permissionManager.requestCameraPermission(activity, R.string.permissions_qr_scanner) {
QrScannerDialog(activity, onCodeScanned = { code ->
val decoder = qrDecoderClass.newInstance()
val decoder = qrDecoderClass.getDeclaredConstructor().newInstance()
val values = decoder.decode(code)
if (values == null) {
Toast.makeText(activity, R.string.login_qr_decoding_error, Toast.LENGTH_SHORT).show()

View File

@ -0,0 +1,132 @@
/*
* Copyright (c) Kuba Szczodrzyński 2023-3-24.
*/
package pl.szczodrzynski.edziennik.ui.login.recaptcha
import android.annotation.SuppressLint
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.util.Base64
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.webkit.JavascriptInterface
import android.webkit.WebView
import android.widget.FrameLayout
import androidx.appcompat.app.AppCompatActivity
import org.greenrobot.eventbus.EventBus
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.SYSTEM_USER_AGENT
import pl.szczodrzynski.edziennik.utils.Themes
import java.nio.charset.Charset
class RecaptchaActivity : AppCompatActivity() {
companion object {
private const val TAG = "RecaptchaActivity"
private const val CODE = """
PCFET0NUWVBFIGh0bWw+PGh0bWw+PGhlYWQ+PHNjcmlwdCBzcmM9Imh0dHBzOi8vd3d3Lmdvb2ds
ZS5jb20vcmVjYXB0Y2hhL2FwaS5qcz9vbmxvYWQ9cmVhZHkmcmVuZGVyPWV4cGxpY2l0Ij48L3Nj
cmlwdD48L2hlYWQ+PGJvZHk+PGJyPjxkaXYgaWQ9ImdyIiBzdHlsZT0icG9zaXRpb246YWJzb2x1
dGU7dG9wOjUwJTt0cmFuc2Zvcm06dHJhbnNsYXRlKDAsLTUwJSk7Ij48L2Rpdj48YnI+PHNjcmlw
dD5mdW5jdGlvbiByZWFkeSgpe2dyZWNhcHRjaGEucmVuZGVyKCJnciIse3NpdGVrZXk6IlNJVEVL
RVkiLHRoZW1lOiJUSEVNRSIsY2FsbGJhY2s6ZnVuY3Rpb24oZSl7d2luZG93LmlmLmNhbGxiYWNr
KGUpO30sImV4cGlyZWQtY2FsbGJhY2siOndpbmRvdy5pZi5leHBpcmVkQ2FsbGJhY2ssImVycm9y
LWNhbGxiYWNrIjp3aW5kb3cuaWYuZXJyb3JDYWxsYmFja30pO308L3NjcmlwdD48L2JvZHk+PC9o
dG1sPg==
"""
}
private var isSuccessful = false
private lateinit var jsInterface: CaptchaCallbackInterface
interface CaptchaCallbackInterface {
@JavascriptInterface
fun callback(recaptchaResponse: String)
@JavascriptInterface
fun expiredCallback()
@JavascriptInterface
fun errorCallback()
}
@SuppressLint("AddJavascriptInterface", "SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setTitle(R.string.recaptcha_dialog_title)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
WebView.setWebContentsDebuggingEnabled(true)
}
val siteKey = intent.getStringExtra("siteKey") ?: return
val referer = intent.getStringExtra("referer") ?: return
val userAgent = intent.getStringExtra("userAgent") ?: SYSTEM_USER_AGENT
val htmlContent = Base64.decode(CODE, Base64.DEFAULT)
.toString(Charset.defaultCharset())
.replace("THEME", if (Themes.isDark) "dark" else "light")
.replace("SITEKEY", siteKey)
jsInterface = object : CaptchaCallbackInterface {
@JavascriptInterface
override fun callback(recaptchaResponse: String) {
isSuccessful = true
EventBus.getDefault().post(
RecaptchaResult(
isError = false,
code = recaptchaResponse,
)
)
finish()
}
@JavascriptInterface
override fun expiredCallback() {
isSuccessful = false
}
@JavascriptInterface
override fun errorCallback() {
isSuccessful = false
EventBus.getDefault().post(
RecaptchaResult(
isError = true,
code = null,
)
)
finish()
}
}
val webView = WebView(this).apply {
setBackgroundColor(Color.TRANSPARENT)
settings.javaScriptEnabled = true
settings.userAgentString = userAgent
addJavascriptInterface(jsInterface, "if")
loadDataWithBaseURL(
referer,
htmlContent,
"text/html",
"UTF-8",
null,
)
// setLayerType(WebView.LAYER_TYPE_SOFTWARE, null)
layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
}
setContentView(webView)
}
override fun onDestroy() {
super.onDestroy()
if (!isSuccessful)
EventBus.getDefault().post(
RecaptchaResult(
isError = false,
code = null,
)
)
}
}

View File

@ -0,0 +1,10 @@
/*
* Copyright (c) Kuba Szczodrzyński 2023-3-24.
*/
package pl.szczodrzynski.edziennik.ui.login.recaptcha
data class RecaptchaResult(
val isError: Boolean,
val code: String?,
)

View File

@ -4,6 +4,7 @@
package pl.szczodrzynski.edziennik.ui.messages.list
import android.graphics.Typeface
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
@ -21,6 +22,7 @@ import pl.szczodrzynski.edziennik.ui.grades.viewholder.BindableViewHolder
import pl.szczodrzynski.edziennik.ui.messages.MessagesUtils
import pl.szczodrzynski.edziennik.utils.managers.NoteManager
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.navlib.getColorFromAttr
class MessageViewHolder(
inflater: LayoutInflater,
@ -44,15 +46,19 @@ class MessageViewHolder(
b.messageBody.text = item.bodyHtml?.take(200)
val isRead = item.isSent || item.isDraft || item.seen
val typeface = if (isRead) adapter.typefaceNormal else adapter.typefaceBold
val style = if (isRead) R.style.NavView_TextView_Small else R.style.NavView_TextView_Normal
val textColor = if (isRead) getColorFromAttr(b.root.context, R.attr.colorOnSurfaceVariant) else getColorFromAttr(b.root.context, R.attr.colorOnSurface)
// set text styles
b.messageSender.setTextAppearance(activity, style)
b.messageSender.typeface = typeface
b.messageSubject.setTextAppearance(activity, style)
b.messageSender.setTextColor(textColor)
b.messageSubject.typeface = typeface
b.messageDate.setTextAppearance(activity, style)
b.messageSubject.setTextColor(textColor)
b.messageDate.typeface = typeface
b.messageDate.setTextColor(textColor)
if (adapter.onStarClick == null) {
b.messageStar.isVisible = false

View File

@ -14,6 +14,7 @@ import androidx.fragment.app.Fragment
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
import com.mikepenz.iconics.utils.sizeDp
import kotlinx.coroutines.*
import org.greenrobot.eventbus.EventBus
@ -95,27 +96,6 @@ class MessageFragment : Fragment(), CoroutineScope {
it.maxLines = if (it.maxLines == 30) 2 else 30
}
val replyDrawable = IconicsDrawable(activity, CommunityMaterial.Icon3.cmd_reply_outline).apply {
sizeDp = 24
colorAttr(activity, android.R.attr.textColorPrimary)
}
val forwardDrawable = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_arrow_right).apply {
sizeDp = 24
colorAttr(activity, android.R.attr.textColorPrimary)
}
val deleteDrawable = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_delete_outline).apply {
sizeDp = 24
colorAttr(activity, android.R.attr.textColorPrimary)
}
val downloadDrawable = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_download_outline).apply {
sizeDp = 24
colorAttr(activity, android.R.attr.textColorPrimary)
}
b.replyButton.setCompoundDrawables(null, replyDrawable, null, null)
b.forwardButton.setCompoundDrawables(null, forwardDrawable, null, null)
b.deleteButton.setCompoundDrawables(null, deleteDrawable, null, null)
b.downloadButton.setCompoundDrawables(null, downloadDrawable, null, null)
b.messageStar.onClick {
launch {
manager.starMessage(message, !message.isStarred)
@ -218,20 +198,6 @@ class MessageFragment : Fragment(), CoroutineScope {
b.replyButton.isVisible = message.isReceived || message.isDeleted
b.deleteButton.isVisible = message.isReceived
if (message.isReceived || message.isDeleted) {
activity.navView.apply {
bottomBar.apply {
fabEnable = true
fabExtendedText = getString(R.string.messages_reply)
fabIcon = CommunityMaterial.Icon3.cmd_reply_outline
}
setFabOnClickListener {
b.replyButton.performClick()
}
}
activity.gainAttentionFAB()
}
val messageRecipients = StringBuilder("<ul>")
message.recipients?.forEach { recipient ->

View File

@ -26,15 +26,8 @@ fun MaterialButton.setupNotesButton(
) {
if (!isVisible)
return
icon = IconicsDrawable(activity, CommunityMaterial.Icon3.cmd_playlist_edit)
setText(R.string.notes_button)
iconPadding = 8.dp
iconSize = 24.dp
updateLayoutParams<LinearLayout.LayoutParams> {
gravity = Gravity.CENTER_HORIZONTAL
}
updatePadding(left = 12.dp)
setText(R.string.notes_button)
onClick {
NoteListDialog(

View File

@ -5,6 +5,7 @@ import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.colorRes
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -15,6 +16,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Notification
import pl.szczodrzynski.edziennik.databinding.NotificationsListItemBinding
import pl.szczodrzynski.edziennik.ext.*
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.navlib.getColorFromAttr
import kotlin.coroutines.CoroutineContext
class NotificationsAdapter(
@ -48,7 +50,7 @@ class NotificationsAdapter(
val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity)
b.notificationIcon.background = IconicsDrawable(app, item.type.icon).apply {
colorRes = R.color.colorPrimary
colorInt = getColorFromAttr(b.root.context, R.attr.colorPrimary)
}
b.title.text = item.text

View File

@ -32,9 +32,9 @@ class SettingsLicenseActivity : MaterialAboutActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(
if (Themes.isDark)
R.style.Theme_MaterialComponents
R.style.Theme_Material3_Dark
else
R.style.Theme_MaterialComponents_Light
R.style.Theme_Material3_Light
)
foregroundColor = if (Themes.isDark)
R.color.primaryTextDark.resolveColor(this)

View File

@ -17,6 +17,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.ext.after
import pl.szczodrzynski.edziennik.utils.Colors
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.Utils
class SettingsUtil(
val activity: MainActivity,

View File

@ -20,6 +20,8 @@ import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update
import pl.szczodrzynski.edziennik.ext.after
import pl.szczodrzynski.edziennik.ext.resolveAttr
import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.dialogs.ChangelogDialog
import pl.szczodrzynski.edziennik.ui.settings.SettingsCard
import pl.szczodrzynski.edziennik.ui.settings.SettingsLicenseActivity
@ -39,13 +41,13 @@ class SettingsAboutCard(util: SettingsUtil) : SettingsCard(util), CoroutineScope
MediaPlayer.create(activity, R.raw.ogarnij_sie)
}
override fun buildCard() = util.createCard(
null,
items = ::getItems,
itemsMore = ::getItemsMore,
backgroundColor = 0xff1976d2.toInt(),
theme = R.style.AppTheme_Dark
)
override fun buildCard(): MaterialAboutCard =
util.createCard(
null,
items = ::getItems,
itemsMore = ::getItemsMore,
backgroundColor = R.attr.colorPrimaryContainer.resolveAttr(activity)
)
private val versionDetailsItem by lazy {
util.createActionItem(

View File

@ -74,6 +74,16 @@ class SettingsThemeCard(util: SettingsUtil) : SettingsCard(util) {
) { _, it ->
configGlobal.ui.miniMenuVisible = it
activity.navView.drawer.miniDrawerVisiblePortrait = it
},
util.createPropertyItem(
text = R.string.settings_ui_lock_layout_text,
subText = R.string.settings_ui_lock_layout_subtext,
icon = CommunityMaterial.Icon.cmd_axis_lock,
value = configGlobal.ui.lockLayout
) { _, it ->
configGlobal.ui.lockLayout = it
activity.recreate()
}
)

View File

@ -109,12 +109,10 @@ class GenerateBlockTimetableDialog(
.show()
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.onClick {
app.permissionManager.requestStoragePermission(activity, permissionMessage = R.string.permissions_generate_timetable) {
when (b.weekSelectionRadioGroup.checkedRadioButtonId) {
R.id.withChangesCurrentWeekRadio -> generateBlockTimetable(weekCurrentStart, weekCurrentEnd)
R.id.withChangesNextWeekRadio -> generateBlockTimetable(weekNextStart, weekNextEnd)
R.id.forSelectedWeekRadio -> selectDate()
}
when (b.weekSelectionRadioGroup.checkedRadioButtonId) {
R.id.withChangesCurrentWeekRadio -> generateBlockTimetable(weekCurrentStart, weekCurrentEnd)
R.id.withChangesNextWeekRadio -> generateBlockTimetable(weekNextStart, weekNextEnd)
R.id.forSelectedWeekRadio -> selectDate()
}
}
}}
@ -390,7 +388,9 @@ class GenerateBlockTimetableDialog(
try {
val uri = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values) ?: return@withContext null
resolver.openOutputStream(uri).use {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
if (it != null) {
bitmap.compress(Bitmap.CompressFormat.PNG, 100, it)
}
}
uri
} catch (e: Exception) {

View File

@ -49,7 +49,6 @@ class LessonDetailsDialog(
DialogLessonDetailsBinding.inflate(layoutInflater)
override fun getPositiveButtonText() = R.string.close
override fun getNeutralButtonText() = R.string.add
private lateinit var adapter: EventListAdapter
private val manager
@ -57,7 +56,7 @@ class LessonDetailsDialog(
private val attendanceManager
get() = app.attendanceManager
override suspend fun onNeutralClick(): Boolean {
fun openAddEventDialog(): Boolean {
EventManualDialog(
activity,
lesson.profileId,
@ -227,6 +226,8 @@ class LessonDetailsDialog(
addItemDecoration(SimpleDividerItemDecoration(context))
}
}
@Suppress("NotifyDataSetChanged")
adapter.notifyDataSetChanged()
if (events != null && events.isNotEmpty()) {
@ -252,6 +253,7 @@ class LessonDetailsDialog(
)
}
b.addEventButton.onClick { openAddEventDialog() }
b.notesButton.isVisible = showNotes
b.notesButton.setupNotesButton(
activity = activity,
@ -259,6 +261,7 @@ class LessonDetailsDialog(
onShowListener = onShowListener,
onDismissListener = onDismissListener,
)
b.legend.isVisible = showNotes
if (showNotes)
NoteManager.setLegendText(lesson, b.legend)

View File

@ -40,6 +40,7 @@ import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.data.db.full.LessonFull
import pl.szczodrzynski.edziennik.databinding.TimetableDayFragmentBinding
import pl.szczodrzynski.edziennik.databinding.TimetableLessonBinding
import pl.szczodrzynski.edziennik.databinding.TimetableNoLessonsBinding
import pl.szczodrzynski.edziennik.databinding.TimetableNoTimetableBinding
import pl.szczodrzynski.edziennik.ext.Intent
import pl.szczodrzynski.edziennik.ext.JsonObject
@ -63,6 +64,7 @@ import pl.szczodrzynski.edziennik.utils.Colors
import pl.szczodrzynski.edziennik.utils.managers.NoteManager
import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time
import pl.szczodrzynski.edziennik.utils.models.Week
import pl.szczodrzynski.edziennik.utils.mutableLazy
import kotlin.coroutines.CoroutineContext
import kotlin.math.max
@ -182,6 +184,20 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
b.root.removeAllViews()
b.root.addView(view)
viewsRemoved = true
val b = TimetableNoLessonsBinding.bind(view)
val weekStart = date.weekStart.stringY_m_d
b.noLessonsSync.onClick {
it.isEnabled = false
EdziennikTask.syncProfile(
profileId = App.profileId,
featureTypes = setOf(FeatureType.TIMETABLE),
arguments = JsonObject(
"weekStart" to weekStart
)
).enqueue(activity)
}
b.noLessonsSync.isVisible = date.weekDay !in Week.SATURDAY..Week.SUNDAY
}
return
}
@ -318,27 +334,24 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
lesson.teacherName ?: "?"
else
mutableListOf<CharSequence>().apply {
lesson.oldTeacherName?.let { add(it.asStrikethroughSpannable()) }
lesson.teacherName?.let { add(it) }
}.concat(arrowRight)
}.concat()
// team
val teamInfo = if (lesson.teamId != null && lesson.teamId == lesson.oldTeamId)
lesson.teamName ?: "?"
else
mutableListOf<CharSequence>().apply {
lesson.oldTeamName?.let { add(it.asStrikethroughSpannable()) }
lesson.teamName?.let { add(it) }
}.concat(arrowRight)
}.concat()
// classroom
val classroomInfo = if (lesson.classroom != null && lesson.classroom == lesson.oldClassroom)
lesson.classroom ?: "?"
else
mutableListOf<CharSequence>().apply {
lesson.oldClassroom?.let { add(it.asStrikethroughSpannable()) }
lesson.classroom?.let { add(it) }
}.concat(arrowRight)
}.concat()
lb.annotationVisible = manager.getAnnotation(activity, lesson, lb.annotation)
@ -366,8 +379,8 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
if (subjectTextPrimary != null)
lb.lessonNumberText.setTextColor(subjectTextPrimary.toInt())
lb.subjectName.text = lessonText?.let {
if (lesson.type == Lesson.TYPE_CANCELLED || lesson.type == Lesson.TYPE_SHIFTED_SOURCE)
it.asStrikethroughSpannable().asColoredSpannable(subjectTextSecondary.toInt())
if (lesson.type == Lesson.TYPE_SHIFTED_SOURCE)
it.asStrikethroughSpannable().asColoredSpannable(colorSecondary)
else if (subjectTextPrimary != null)
it.asColoredSpannable(subjectTextPrimary.toInt())
else
@ -396,12 +409,7 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope {
}
//lb.subjectName.typeface = Typeface.create("sans-serif-light", Typeface.BOLD)
val lessonNumberMargin =
if (lb.annotationVisible) (-8).dp
else 0
lb.lessonNumberText.updateLayoutParams<LinearLayout.LayoutParams> {
updateMargins(top = lessonNumberMargin, bottom = lessonNumberMargin)
}
lb.annotationVisible = manager.getAnnotation(activity, lesson, lb.annotation)
// The day view needs the event time ranges in the start minute/end minute format,
// so calculate those here

View File

@ -4,11 +4,13 @@
package pl.szczodrzynski.edziennik.ui.timetable
import android.annotation.SuppressLint
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.AsyncTask
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -27,8 +29,11 @@ import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask
import pl.szczodrzynski.edziennik.data.db.enums.FeatureType
import pl.szczodrzynski.edziennik.data.db.enums.MetadataType
import pl.szczodrzynski.edziennik.databinding.FragmentTimetableV2Binding
import pl.szczodrzynski.edziennik.ext.JsonObject
import pl.szczodrzynski.edziennik.ext.getSchoolYearConstrains
import pl.szczodrzynski.edziennik.ext.getStudentData
import pl.szczodrzynski.edziennik.ui.dialogs.settings.TimetableConfigDialog
@ -87,8 +92,16 @@ class TimetableFragment : Fragment(), CoroutineScope {
}
override fun onResume() {
super.onResume()
activity.registerReceiver(broadcastReceiver, IntentFilter(ACTION_SCROLL_TO_DATE))
activity.registerReceiver(broadcastReceiver, IntentFilter(ACTION_RELOAD_PAGES))
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
activity.registerReceiver(broadcastReceiver, IntentFilter(ACTION_SCROLL_TO_DATE ),
Context.RECEIVER_NOT_EXPORTED)
activity.registerReceiver(broadcastReceiver, IntentFilter(ACTION_RELOAD_PAGES),
Context.RECEIVER_NOT_EXPORTED)
} else @Suppress("UnspecifiedRegisterReceiverFlag") {
activity.registerReceiver(broadcastReceiver, IntentFilter(ACTION_SCROLL_TO_DATE))
activity.registerReceiver(broadcastReceiver, IntentFilter(ACTION_RELOAD_PAGES))
}
}
override fun onPause() {
super.onPause()
@ -174,10 +187,25 @@ class TimetableFragment : Fragment(), CoroutineScope {
val selectedDate = arguments?.getString("timetableDate", "")?.let { if (it.isBlank()) null else Date.fromY_m_d(it) }
b.tabLayout.setUpWithViewPager(b.viewPager)
b.tabLayout.setCurrentItem(items.indexOfFirst { it.value == selectedDate?.value ?: today }, false)
b.tabLayout.setupWithViewPager(b.viewPager)
b.viewPager.setCurrentItem(items.indexOfFirst { it.value == selectedDate?.value ?: today }, false)
activity.navView.bottomSheet.prependItems(
BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_timetable_sync)
.withIcon(CommunityMaterial.Icon.cmd_calendar_sync_outline)
.withOnClickListener {
activity.bottomSheet.close()
val date = pageSelection ?: Date.getToday()
val weekStart = date.weekStart.stringY_m_d
EdziennikTask.syncProfile(
profileId = App.profileId,
featureTypes = setOf(FeatureType.TIMETABLE),
arguments = JsonObject(
"weekStart" to weekStart
)
).enqueue(activity)
},
BottomSheetPrimaryItem(true)
.withTitle(R.string.timetable_select_day)
.withIcon(SzkolnyFont.Icon.szf_calendar_today_outline)
@ -193,7 +221,7 @@ class TimetableFragment : Fragment(), CoroutineScope {
val dateSelected = Date.fromMillisUtc(millis)
val index = items.indexOfFirst { it == dateSelected }
if (index != -1)
b.tabLayout.setCurrentItem(index, true)
b.viewPager.setCurrentItem(index, true)
}
}
.show(activity.supportFragmentManager, TAG)
@ -236,7 +264,7 @@ class TimetableFragment : Fragment(), CoroutineScope {
activity.navView.bottomBar.fabExtendedText = getString(R.string.timetable_today)
activity.navView.bottomBar.fabIcon = SzkolnyFont.Icon.szf_calendar_today_outline
activity.navView.setFabOnClickListener(View.OnClickListener {
b.tabLayout.setCurrentItem(items.indexOfFirst { it.value == today }, true)
b.viewPager.setCurrentItem(items.indexOfFirst { it.value == today }, true)
})
}}

View File

@ -5,6 +5,7 @@
package pl.szczodrzynski.edziennik.ui.views
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.util.AttributeSet
import androidx.appcompat.app.AppCompatActivity
@ -50,6 +51,10 @@ class AttachmentsView @JvmOverloads constructor(
val attachmentSizes = arguments.getLongArray("attachmentSizes")
val adapter = AttachmentAdapter(context, onAttachmentClick = { item ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
downloadAttachment(item)
return@AttachmentAdapter
}
app.permissionManager.requestStoragePermission(activity, R.string.permissions_attachment) {
downloadAttachment(item)
}
@ -57,6 +62,10 @@ class AttachmentsView @JvmOverloads constructor(
val popupMenu = PopupMenu(chip.context, chip)
popupMenu.menu.add(0, 1, 0, R.string.messages_attachment_download_again)
popupMenu.setOnMenuItemClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
downloadAttachment(item)
return@setOnMenuItemClickListener true
}
app.permissionManager.requestStoragePermission(activity, R.string.permissions_attachment) {
downloadAttachment(item, forceDownload = true)
}

View File

@ -33,13 +33,8 @@ class EventTypeDropdown : TextInputDropDown {
suspend fun loadItems() {
val types = withContext(Dispatchers.Default) {
val list = mutableListOf<Item>()
var types = db.eventTypeDao().getAllNow(profileId)
if (types.none { it.id in -1L..10L }) {
val profile = db.profileDao().getByIdNow(profileId) ?: return@withContext listOf()
types = db.eventTypeDao().addDefaultTypes(profile)
}
val types = db.eventTypeDao().getAllNow(profileId)
.sortedBy { it.order }
list += types.map {
Item(it.id, it.name, tag = it, icon = IconicsDrawable(context).apply {

View File

@ -23,6 +23,7 @@ import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.ext.Bundle
import pl.szczodrzynski.edziennik.ext.getJsonObject
import pl.szczodrzynski.edziennik.ext.pendingIntentFlag
import pl.szczodrzynski.edziennik.ext.pendingIntentMutable
import pl.szczodrzynski.edziennik.ext.putExtras
import pl.szczodrzynski.edziennik.receivers.SzkolnyReceiver
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
@ -50,7 +51,7 @@ class WidgetNotificationsProvider : AppWidgetProvider() {
val syncIntent = SzkolnyReceiver.getIntent(context, Bundle(
"task" to "SyncRequest"
))
val syncPendingIntent = PendingIntent.getBroadcast(context, 0, syncIntent, pendingIntentFlag())
val syncPendingIntent = PendingIntent.getBroadcast(context, 0, syncIntent, pendingIntentMutable())
views.setOnClickPendingIntent(R.id.widgetNotificationsSync, syncPendingIntent)
views.setImageViewBitmap(
@ -71,13 +72,13 @@ class WidgetNotificationsProvider : AppWidgetProvider() {
val itemIntent = Intent(context, MainActivity::class.java)
itemIntent.action = Intent.ACTION_MAIN
val itemPendingIntent = PendingIntent.getActivity(context, 0, itemIntent, pendingIntentFlag())
val itemPendingIntent = PendingIntent.getActivity(context, appWidgetId, itemIntent, pendingIntentMutable())
views.setPendingIntentTemplate(R.id.widgetNotificationsListView, itemPendingIntent)
val headerIntent = Intent(context, MainActivity::class.java)
headerIntent.action = Intent.ACTION_MAIN
headerIntent.putExtras("fragmentId" to NavTarget.NOTIFICATIONS)
val headerPendingIntent = PendingIntent.getActivity(context, 0, headerIntent, pendingIntentFlag())
val headerPendingIntent = PendingIntent.getActivity(context, appWidgetId, headerIntent, pendingIntentMutable())
views.setOnClickPendingIntent(R.id.widgetNotificationsHeader, headerPendingIntent)
appWidgetManager.updateAppWidget(appWidgetId, views)

View File

@ -34,6 +34,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_NO_LESSON
import pl.szczodrzynski.edziennik.ext.filterOutArchived
import pl.szczodrzynski.edziennik.ext.getJsonObject
import pl.szczodrzynski.edziennik.ext.pendingIntentFlag
import pl.szczodrzynski.edziennik.ext.pendingIntentMutable
import pl.szczodrzynski.edziennik.ext.putExtras
import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget
import pl.szczodrzynski.edziennik.ui.widgets.LessonDialogActivity
@ -119,7 +120,7 @@ class WidgetTimetableProvider : AppWidgetProvider() {
0, refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT or pendingIntentFlag())
views.setOnClickPendingIntent(R.id.widgetTimetableRefresh, refreshPendingIntent)
views.setOnClickPendingIntent(R.id.widgetTimetableSync, getPendingSelfIntent(context, ACTION_SYNC_DATA))
views.setViewVisibility(R.id.widgetTimetableSync, View.GONE)
views.setImageViewBitmap(
R.id.widgetTimetableRefresh,
@ -129,14 +130,6 @@ class WidgetTimetableProvider : AppWidgetProvider() {
}.toBitmap()
)
views.setImageViewBitmap(
R.id.widgetTimetableSync,
IconicsDrawable(context, CommunityMaterial.Icon.cmd_download_outline).apply {
colorInt = Color.WHITE
sizeDp = if (config.bigStyle) 28 else 20
}.toBitmap()
)
prepareAppWidget(app, appWidgetId, views, config, bellSyncDiffMillis)
appWidgetManager.updateAppWidget(appWidgetId, views)
@ -337,8 +330,11 @@ class WidgetTimetableProvider : AppWidgetProvider() {
scrollPos = pos + 1
}
// remove notes from other profiles
lesson.filterNotes()
// set the subject and classroom name
model.subjectName = lesson.displaySubjectName
model.subjectName = lesson.getNoteSubstituteText(showNotes = true)
?: lesson.displaySubjectName
model.classroomName = lesson.displayClassroom
// set the bell sync to calculate progress in ListProvider
@ -399,7 +395,7 @@ class WidgetTimetableProvider : AppWidgetProvider() {
}
}
headerIntent.putExtras("fragmentId" to NavTarget.TIMETABLE)
val headerPendingIntent = PendingIntent.getActivity(app, appWidgetId, headerIntent, pendingIntentFlag())
val headerPendingIntent = PendingIntent.getActivity(app, appWidgetId, headerIntent, pendingIntentMutable())
views.setOnClickPendingIntent(R.id.widgetTimetableHeader, headerPendingIntent)
timetables!!.put(appWidgetId, models)
@ -413,7 +409,7 @@ class WidgetTimetableProvider : AppWidgetProvider() {
// create an intent used to display the lesson details dialog
val itemIntent = Intent(app, LessonDialogActivity::class.java)
itemIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK/* or Intent.FLAG_ACTIVITY_CLEAR_TASK*/)
val itemPendingIntent = PendingIntent.getActivity(app, appWidgetId, itemIntent, PendingIntent.FLAG_MUTABLE)
val itemPendingIntent = PendingIntent.getActivity(app, appWidgetId, itemIntent, pendingIntentMutable())
views.setPendingIntentTemplate(R.id.widgetTimetableListView, itemPendingIntent)
if (!unified)

View File

@ -774,14 +774,21 @@ public class Utils {
private static File storageDir = null;
public static File getStorageDir() {
if (storageDir != null)
return storageDir;
storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
storageDir = new File(storageDir, "Szkolny.eu");
storageDir.mkdirs();
return storageDir;
}
public static void initializeStorageDir(Context context) {
if (storageDir != null)
return;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
storageDir = context.getExternalFilesDir(null);
} else {
storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS);
storageDir = new File(storageDir, "Szkolny.eu");
}
storageDir.mkdirs();
}
public static void writeStringToFile(File file, String data) throws IOException {
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream(file));
outputStreamWriter.write(data);

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