diff --git a/.github/workflows/build-nightly-apk.yml b/.github/workflows/build-nightly-apk.yml index bca267be..0951149e 100644 --- a/.github/workflows/build-nightly-apk.yml +++ b/.github/workflows/build-nightly-apk.yml @@ -51,10 +51,11 @@ jobs: androidHome: ${{ env.ANDROID_HOME }} androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }} steps: - - name: Setup JDK 1.8 - uses: actions/setup-java@v1 + - name: Setup JDK 11 + uses: actions/setup-java@v2 with: - java-version: 1.8 + distribution: 'temurin' + java-version: '11' - name: Setup Android SDK uses: android-actions/setup-android@v2 - name: Clean build artifacts @@ -63,7 +64,9 @@ jobs: rm -rf app/build/outputs/apk/* rm -rf app/build/outputs/bundle/* - name: Assemble official release with Gradle - run: ./gradlew assembleOfficialRelease + uses: gradle/gradle-build-action@v2 + with: + arguments: assembleOfficialRelease sign: name: Sign APK runs-on: self-hosted diff --git a/.github/workflows/build-release-aab-play.yml b/.github/workflows/build-release-aab-play.yml index c5bfb2ea..eb6923bb 100644 --- a/.github/workflows/build-release-aab-play.yml +++ b/.github/workflows/build-release-aab-play.yml @@ -43,10 +43,11 @@ jobs: androidHome: ${{ env.ANDROID_HOME }} androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }} steps: - - name: Setup JDK 1.8 - uses: actions/setup-java@v1 + - name: Setup JDK 11 + uses: actions/setup-java@v2 with: - java-version: 1.8 + distribution: 'temurin' + java-version: '11' - name: Setup Android SDK uses: android-actions/setup-android@v2 - name: Clean build artifacts @@ -55,7 +56,9 @@ jobs: rm -rf app/build/outputs/apk/* rm -rf app/build/outputs/bundle/* - name: Bundle play release with Gradle - run: ./gradlew bundlePlayRelease + uses: gradle/gradle-build-action@v2 + with: + arguments: bundlePlayRelease sign: name: Sign App Bundle runs-on: self-hosted @@ -110,15 +113,15 @@ 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 }} - userFraction: 1.0 whatsNewDirectory: ${{ steps.changelog.outputs.changelogDir }} + status: completed - name: Upload workflow artifact uses: actions/upload-artifact@v2 - if: true + if: always() with: name: ${{ steps.changelog.outputs.appVersionName }} path: | diff --git a/.github/workflows/build-release-apk.yml b/.github/workflows/build-release-apk.yml index 169704c1..5ca055b9 100644 --- a/.github/workflows/build-release-apk.yml +++ b/.github/workflows/build-release-apk.yml @@ -43,10 +43,11 @@ jobs: androidHome: ${{ env.ANDROID_HOME }} androidSdkRoot: ${{ env.ANDROID_SDK_ROOT }} steps: - - name: Setup JDK 1.8 - uses: actions/setup-java@v1 + - name: Setup JDK 11 + uses: actions/setup-java@v2 with: - java-version: 1.8 + distribution: 'temurin' + java-version: '11' - name: Setup Android SDK uses: android-actions/setup-android@v2 - name: Clean build artifacts @@ -55,7 +56,9 @@ jobs: rm -rf app/build/outputs/apk/* rm -rf app/build/outputs/bundle/* - name: Assemble official release with Gradle - run: ./gradlew assembleOfficialRelease + uses: gradle/gradle-build-action@v2 + with: + arguments: assembleOfficialRelease sign: name: Sign APK runs-on: self-hosted diff --git a/.gitignore b/.gitignore index 07e9f679..375b15e2 100644 --- a/.gitignore +++ b/.gitignore @@ -265,3 +265,4 @@ fabric.properties # End of https://www.toptal.com/developers/gitignore/api/android,androidstudio,gradle,java,kotlin signatures/ +.idea/*.xml diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 29204aff..c8ff3936 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -1,8 +1,17 @@ + + + + + \ No newline at end of file diff --git a/.idea/discord.xml b/.idea/discord.xml deleted file mode 100644 index a04e4e5f..00000000 --- a/.idea/discord.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml deleted file mode 100644 index 0dd4b354..00000000 --- a/.idea/kotlinc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index e497da99..00000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 72c03e94..0ceae1a1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,6 +1,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-kapt' +apply plugin: 'kotlin-parcelize' apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.firebase.crashlytics' @@ -30,11 +31,21 @@ android { cppFlags "-std=c++11" } } + + kapt { + arguments { + arg("room.schemaLocation", "$projectDir/schemas") + } + } } buildTypes { debug { + getIsDefault().set(true) minifyEnabled = false + manifestPlaceholders = [ + buildTimestamp: 0 + ] } release { minifyEnabled = true @@ -45,23 +56,34 @@ android { } flavorDimensions "platform" productFlavors { - main { - versionName gitInfo.versionHuman + 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("main")) + 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"] + } } defaultConfig { vectorDrawables.useSupportLibrary = true } - lintOptions { - checkReleaseBuilds = false - } buildFeatures { dataBinding = true viewBinding = true @@ -75,7 +97,9 @@ android { jvmTarget = "1.8" } packagingOptions { - exclude 'META-INF/library-core_release.kotlin_module' + resources { + excludes += ['META-INF/library-core_release.kotlin_module'] + } } externalNativeBuild { cmake { @@ -83,6 +107,9 @@ android { version "3.10.2" } } + lint { + checkReleaseBuilds false + } } tasks.whenTaskAdded { task -> @@ -117,28 +144,30 @@ dependencies { // 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" // Android Jetpack - implementation "androidx.appcompat:appcompat:1.2.0" + implementation "androidx.appcompat:appcompat:1.5.1" implementation "androidx.cardview:cardview:1.0.0" - implementation "androidx.constraintlayout:constraintlayout:2.0.4" - implementation "androidx.core:core-ktx:1.3.2" - implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.0" - implementation "androidx.navigation:navigation-fragment-ktx:2.3.4" - implementation "androidx.recyclerview:recyclerview:1.1.0" - implementation "androidx.room:room-runtime:2.2.6" - implementation "androidx.work:work-runtime-ktx:2.5.0" - kapt "androidx.room:room-compiler:2.2.6" + 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.room:room-ktx:2.4.3" + implementation "androidx.work:work-runtime-ktx:2.7.1" + kapt "androidx.room:room-compiler:2.4.3" // Google design libs - implementation "com.google.android.material:material:1.3.0" - implementation "com.google.android:flexbox:2.0.1" + implementation "com.google.android.material:material:1.6.1" + implementation "com.google.android.flexbox:flexbox:3.0.0" // Play Services/Firebase - implementation "com.google.android.gms:play-services-wearable:17.0.0" - implementation "com.google.firebase:firebase-core:18.0.2" - implementation "com.google.firebase:firebase-crashlytics:17.4.0" + implementation "com.google.android.gms:play-services-wearable:17.1.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-messaging") { version { strictly "20.1.3" } } // OkHttp, Retrofit, Gson, Jsoup @@ -146,20 +175,22 @@ 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.6' - implementation "org.jsoup:jsoup:1.13.1" + implementation 'com.google.code.gson:gson:2.8.8' + 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 "eu.szkolny:agendacalendarview:1799f8ef47" + implementation "eu.szkolny:android-snowfall:1ca9ea2da3" + implementation "eu.szkolny:agendacalendarview:1.0.4" implementation "eu.szkolny:cafebar:5bf0c618de" implementation "eu.szkolny.fslogin:lib:2.0.0" 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:ssl-provider:1.0.0" + 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" @@ -167,34 +198,34 @@ dependencies { kapt "eu.szkolny.selective-dao:codegen:27f8f3f194" // Iconics & related - implementation "com.mikepenz:iconics-core:5.3.0-b01" - implementation "com.mikepenz:iconics-views:5.3.0-b01" + implementation "com.mikepenz:iconics-core:5.3.2" + implementation "com.mikepenz:iconics-views:5.3.2" implementation "com.mikepenz:community-material-typeface:5.8.55.0-kotlin@aar" - implementation "eu.szkolny:szkolny-font:1.3" + implementation "eu.szkolny:szkolny-font:77e33acc2a" // Other dependencies implementation "cat.ereza:customactivityoncrash:2.3.0" - implementation "com.applandeo:material-calendar-view:1.5.0" + implementation "com.android.volley:volley:1.2.1" implementation "com.daimajia.swipelayout:library:1.2.0@aar" - implementation "com.github.antonKozyriatskyi:CircularProgressIndicator:1.2.2" - implementation "com.github.bassaer:chatmessageview:2.0.1" - implementation "com.github.CanHub:Android-Image-Cropper:2.2.2" - implementation "com.github.ChuckerTeam.Chucker:library:3.0.1" - implementation "com.github.jetradarmobile:android-snowfall:1.2.0" - implementation "com.github.wulkanowy.uonet-request-signer:hebe-jvm:a99ca50a31" - implementation("com.heinrichreimersoftware:material-intro") { version { strictly "1.5.8" } } - implementation "com.hypertrack:hyperlog:0.0.10" + implementation "com.github.Applandeo:Material-Calendar-View:15de569cbc" // https://github.com/Applandeo/Material-Calendar-View + implementation "com.github.CanHub:Android-Image-Cropper:2.2.2" // https://github.com/CanHub/Android-Image-Cropper + implementation "com.github.ChuckerTeam.Chucker:library:3.5.2" // https://github.com/ChuckerTeam/chucker + implementation "com.github.antonKozyriatskyi:CircularProgressIndicator:1.2.2" // https://github.com/antonKozyriatskyi/CircularProgressIndicator + implementation "com.github.bassaer:chatmessageview:2.0.1" // https://github.com/bassaer/ChatMessageView + implementation "com.github.hypertrack:hyperlog-android:0.0.10" // https://github.com/hypertrack/hyperlog-android + implementation "com.github.smuyyh:JsonViewer:V1.0.6" // https://github.com/smuyyh/JsonViewer + implementation "com.github.underwindfall.PowerPermission:powerpermission-coroutines:1.4.0" // https://github.com/underwindfall/PowerPermission + implementation "com.github.underwindfall.PowerPermission:powerpermission:1.4.0" // https://github.com/underwindfall/PowerPermission + implementation "com.github.wulkanowy.uonet-request-signer:hebe-jvm:a99ca50a31" // https://github.com/wulkanowy/uonet-request-signer implementation "com.jaredrummler:colorpicker:1.1.0" - implementation "com.qifan.powerpermission:powerpermission-coroutines:1.3.0" - implementation "com.qifan.powerpermission:powerpermission:1.3.0" - implementation "com.yuyh.json:jsonviewer:1.0.6" implementation "io.coil-kt:coil:1.1.1" implementation "me.dm7.barcodescanner:zxing:1.9.8" implementation "me.grantland:autofittextview:0.2.1" implementation "me.leolin:ShortcutBadger:1.1.22@aar" implementation "org.greenrobot:eventbus:3.2.0" + 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.amitshekhar.android:debug-db:1.0.5" + debugImplementation "com.github.amitshekhariitbhu.Android-Debug-Database:debug-db:v1.0.6" } diff --git a/app/git-info.gradle b/app/git-info.gradle index 2ddd8c2d..3577f668 100644 --- a/app/git-info.gradle +++ b/app/git-info.gradle @@ -97,18 +97,17 @@ private def buildGitInfo() { def tag = getLastTag(repo, git, head) def tagName = tag[1] def tagRevCount = tag[2] - def versionName = tagName.replace("v", "") def result = [ - hash : head.objectId.name, - branch : repo.branch, - dirty : dirty, - remotes : remotes, - unstaged : status.uncommittedChanges.join("; "), - tag : tagName, - revCount : tagRevCount, - version : """$tagName-$tagRevCount-g${head.objectId.name.substring(0, 8)}""" + (dirty ? ".dirty" : ""), - versionHuman: """$versionName-${repo.branch.replace("/", "_")}""" + (dirty ? ".dirty" : "") + hash : head.objectId.name, + branch : repo.branch, + dirty : dirty, + remotes : remotes, + unstaged : status.uncommittedChanges.join("; "), + tag : tagName, + revCount : tagRevCount, + version : """$tagName-$tagRevCount-g${head.objectId.name.substring(0, 8)}""" + (dirty ? ".dirty" : ""), + versionSuffix : """${repo.branch.replace("/", "_")}""" + (dirty ? ".dirty" : "") ] return result } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 4d84d11d..8618f020 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -22,18 +22,24 @@ -keep class android.support.v7.widget.** { *; } -keep class pl.szczodrzynski.edziennik.utils.models.** { *; } +-keep class pl.szczodrzynski.edziennik.data.db.enums.* { *; } -keep class pl.szczodrzynski.edziennik.data.db.entity.Event { *; } -keep class pl.szczodrzynski.edziennik.data.db.full.EventFull { *; } -keep class pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage { *; } --keep class pl.szczodrzynski.edziennik.ui.modules.home.HomeCardModel { *; } +-keep class pl.szczodrzynski.edziennik.data.db.entity.Note { *; } +-keep class pl.szczodrzynski.edziennik.ui.home.HomeCardModel { *; } -keepclassmembers class pl.szczodrzynski.edziennik.ui.widgets.WidgetConfig { public *; } -keepnames class pl.szczodrzynski.edziennik.ui.widgets.timetable.WidgetTimetableProvider -keepnames class pl.szczodrzynski.edziennik.ui.widgets.notifications.WidgetNotificationsProvider -keepnames class pl.szczodrzynski.edziennik.ui.widgets.luckynumber.WidgetLuckyNumberProvider +-keep class pl.szczodrzynski.edziennik.config.AppData { *; } +-keep class pl.szczodrzynski.edziennik.config.AppData$** { *; } +-keep class pl.szczodrzynski.edziennik.utils.managers.TextStylingManager$HtmlMode { *; } -keepnames class androidx.appcompat.view.menu.MenuBuilder { setHeaderTitleInt(java.lang.CharSequence); } --keepclassmembernames class androidx.appcompat.view.menu.StandardMenuPopup { private *; } -keepnames class androidx.appcompat.view.menu.MenuPopupHelper { showPopup(int, int, boolean, boolean); } +-keepclassmembernames class androidx.appcompat.view.menu.StandardMenuPopup { private *; } +-keepclassmembernames class androidx.appcompat.view.menu.MenuItemImpl { private *; } -keepclassmembernames class com.mikepenz.materialdrawer.widget.MiniDrawerSliderView { private *; } @@ -66,7 +72,7 @@ -keepclassmembers class pl.szczodrzynski.edziennik.data.api.szkolny.request.** { *; } -keepclassmembers class pl.szczodrzynski.edziennik.data.api.szkolny.response.** { *; } --keepclassmembernames class pl.szczodrzynski.edziennik.ui.modules.login.LoginInfo$Platform { *; } +-keepclassmembernames class pl.szczodrzynski.edziennik.ui.login.LoginInfo$Platform { *; } -keepclassmembernames class pl.szczodrzynski.fslogin.realm.RealmData { *; } -keepclassmembernames class pl.szczodrzynski.fslogin.realm.RealmData$Type { *; } diff --git a/app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/100.json b/app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/100.json new file mode 100644 index 00000000..1bbdcf44 --- /dev/null +++ b/app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/100.json @@ -0,0 +1,2320 @@ +{ + "formatVersion": 1, + "database": { + "version": 100, + "identityHash": "4c141460d807d32d00faad4fb3c12522", + "entities": [ + { + "tableName": "grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `gradeId` INTEGER NOT NULL, `gradeName` TEXT NOT NULL, `gradeType` INTEGER NOT NULL, `gradeValue` REAL NOT NULL, `gradeWeight` REAL NOT NULL, `gradeColor` INTEGER NOT NULL, `gradeCategory` TEXT, `gradeDescription` TEXT, `gradeComment` TEXT, `gradeSemester` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `gradeValueMax` REAL, `gradeClassAverage` REAL, `gradeParentId` INTEGER, `gradeIsImprovement` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `gradeId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "gradeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "gradeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "gradeType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "gradeValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "gradeWeight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "gradeColor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "gradeCategory", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "gradeDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "gradeComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "semester", + "columnName": "gradeSemester", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "valueMax", + "columnName": "gradeValueMax", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "classAverage", + "columnName": "gradeClassAverage", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "gradeParentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isImprovement", + "columnName": "gradeIsImprovement", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "gradeId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_grades_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_grades_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `teacherLoginId` TEXT, `teacherName` TEXT, `teacherSurname` TEXT, `teacherType` INTEGER NOT NULL, `teacherTypeDescription` TEXT, `teacherSubjects` TEXT NOT NULL, PRIMARY KEY(`profileId`, `teacherId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "teacherLoginId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "teacherName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "surname", + "columnName": "teacherSurname", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "teacherType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "typeDescription", + "columnName": "teacherTypeDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subjects", + "columnName": "teacherSubjects", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "teacherId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "teacherAbsence", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teacherAbsenceId` INTEGER NOT NULL, `teacherAbsenceType` INTEGER NOT NULL, `teacherAbsenceName` TEXT, `teacherAbsenceDateFrom` TEXT NOT NULL, `teacherAbsenceDateTo` TEXT NOT NULL, `teacherAbsenceTimeFrom` TEXT, `teacherAbsenceTimeTo` TEXT, `teacherId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `teacherAbsenceId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "teacherAbsenceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "teacherAbsenceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "teacherAbsenceName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dateFrom", + "columnName": "teacherAbsenceDateFrom", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateTo", + "columnName": "teacherAbsenceDateTo", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeFrom", + "columnName": "teacherAbsenceTimeFrom", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timeTo", + "columnName": "teacherAbsenceTimeTo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "teacherAbsenceId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_teacherAbsence_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_teacherAbsence_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "teacherAbsenceTypes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teacherAbsenceTypeId` INTEGER NOT NULL, `teacherAbsenceTypeName` TEXT NOT NULL, PRIMARY KEY(`profileId`, `teacherAbsenceTypeId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "teacherAbsenceTypeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "teacherAbsenceTypeName", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "teacherAbsenceTypeId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `subjectLongName` TEXT, `subjectShortName` TEXT, `subjectColor` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `subjectId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "subjectLongName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shortName", + "columnName": "subjectShortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "subjectColor", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "subjectId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `noticeId` INTEGER NOT NULL, `noticeType` INTEGER NOT NULL, `noticeSemester` INTEGER NOT NULL, `noticeText` TEXT NOT NULL, `noticeCategory` TEXT, `noticePoints` REAL, `teacherId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `noticeId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "noticeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "noticeType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semester", + "columnName": "noticeSemester", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "noticeText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "noticeCategory", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "noticePoints", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "noticeId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_notices_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notices_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "teams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teamId` INTEGER NOT NULL, `teamType` INTEGER NOT NULL, `teamName` TEXT, `teamCode` TEXT, `teamTeacherId` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `teamId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "teamType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "teamName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "code", + "columnName": "teamCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teamTeacherId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "teamId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "attendances", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `attendanceId` INTEGER NOT NULL, `attendanceBaseType` INTEGER NOT NULL, `attendanceTypeName` TEXT NOT NULL, `attendanceTypeShort` TEXT NOT NULL, `attendanceTypeSymbol` TEXT NOT NULL, `attendanceTypeColor` INTEGER, `attendanceDate` TEXT NOT NULL, `attendanceTime` TEXT, `attendanceSemester` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `attendanceLessonTopic` TEXT, `attendanceLessonNumber` INTEGER, `attendanceIsCounted` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `attendanceId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "attendanceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseType", + "columnName": "attendanceBaseType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "typeName", + "columnName": "attendanceTypeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeShort", + "columnName": "attendanceTypeShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeSymbol", + "columnName": "attendanceTypeSymbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeColor", + "columnName": "attendanceTypeColor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "date", + "columnName": "attendanceDate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startTime", + "columnName": "attendanceTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "semester", + "columnName": "attendanceSemester", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lessonTopic", + "columnName": "attendanceLessonTopic", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lessonNumber", + "columnName": "attendanceLessonNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isCounted", + "columnName": "attendanceIsCounted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "attendanceId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_attendances_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_attendances_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `eventId` INTEGER NOT NULL, `eventDate` TEXT NOT NULL, `eventTime` TEXT, `eventTopic` TEXT NOT NULL, `eventColor` INTEGER, `eventType` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `teamId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `eventAddedManually` INTEGER NOT NULL, `eventSharedBy` TEXT, `eventSharedByName` TEXT, `eventBlacklisted` INTEGER NOT NULL, `eventIsDone` INTEGER NOT NULL, `eventIsDownloaded` INTEGER NOT NULL, `homeworkBody` TEXT, `attachmentIds` TEXT, `attachmentNames` TEXT, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `eventId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "eventId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "eventDate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "eventTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "topic", + "columnName": "eventTopic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "eventColor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "eventType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teamId", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedManually", + "columnName": "eventAddedManually", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sharedBy", + "columnName": "eventSharedBy", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedByName", + "columnName": "eventSharedByName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "blacklisted", + "columnName": "eventBlacklisted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "eventIsDone", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDownloaded", + "columnName": "eventIsDownloaded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homeworkBody", + "columnName": "homeworkBody", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentIds", + "columnName": "attachmentIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentNames", + "columnName": "attachmentNames", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "eventId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_events_profileId_eventDate_eventTime", + "unique": false, + "columnNames": [ + "profileId", + "eventDate", + "eventTime" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_events_profileId_eventDate_eventTime` ON `${TABLE_NAME}` (`profileId`, `eventDate`, `eventTime`)" + }, + { + "name": "index_events_profileId_eventType", + "unique": false, + "columnNames": [ + "profileId", + "eventType" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_events_profileId_eventType` ON `${TABLE_NAME}` (`profileId`, `eventType`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "eventTypes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `eventType` INTEGER NOT NULL, `eventTypeName` TEXT NOT NULL, `eventTypeColor` INTEGER NOT NULL, `eventTypeOrder` INTEGER NOT NULL, `eventTypeSource` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `eventType`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "eventType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "eventTypeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "eventTypeColor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "eventTypeOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "eventTypeSource", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "eventType" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "loginStores", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`loginStoreId` INTEGER NOT NULL, `loginStoreType` INTEGER NOT NULL, `loginStoreMode` INTEGER NOT NULL, `loginStoreData` TEXT NOT NULL, PRIMARY KEY(`loginStoreId`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "loginStoreId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "loginStoreType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "loginStoreMode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "loginStoreData", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "loginStoreId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "profiles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `loginStoreId` INTEGER NOT NULL, `loginStoreType` INTEGER NOT NULL, `name` TEXT NOT NULL, `subname` TEXT, `studentNameLong` TEXT NOT NULL, `studentNameShort` TEXT NOT NULL, `accountName` TEXT, `studentData` TEXT NOT NULL, `image` TEXT, `empty` INTEGER NOT NULL, `archived` INTEGER NOT NULL, `syncEnabled` INTEGER NOT NULL, `enableSharedEvents` INTEGER NOT NULL, `registration` INTEGER NOT NULL, `userCode` TEXT NOT NULL, `archiveId` INTEGER, `studentNumber` INTEGER NOT NULL, `studentClassName` TEXT, `studentSchoolYearStart` INTEGER NOT NULL, `dateSemester1Start` TEXT NOT NULL, `dateSemester2Start` TEXT NOT NULL, `dateYearEnd` TEXT NOT NULL, `disabledNotifications` TEXT, `lastReceiversSync` INTEGER NOT NULL, PRIMARY KEY(`profileId`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loginStoreId", + "columnName": "loginStoreId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loginStoreType", + "columnName": "loginStoreType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subname", + "columnName": "subname", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "studentNameLong", + "columnName": "studentNameLong", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentNameShort", + "columnName": "studentNameShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "studentData", + "columnName": "studentData", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "empty", + "columnName": "empty", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "archived", + "columnName": "archived", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "syncEnabled", + "columnName": "syncEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unused1", + "columnName": "enableSharedEvents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registration", + "columnName": "registration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userCode", + "columnName": "userCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "archiveId", + "columnName": "archiveId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "studentNumber", + "columnName": "studentNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentClassName", + "columnName": "studentClassName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "studentSchoolYearStart", + "columnName": "studentSchoolYearStart", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateSemester1Start", + "columnName": "dateSemester1Start", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateSemester2Start", + "columnName": "dateSemester2Start", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateYearEnd", + "columnName": "dateYearEnd", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "disabledNotifications", + "columnName": "disabledNotifications", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastReceiversSync", + "columnName": "lastReceiversSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "luckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `luckyNumberDate` INTEGER NOT NULL, `luckyNumber` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `luckyNumberDate`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "luckyNumberDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "luckyNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "luckyNumberDate" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "announcements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `announcementId` INTEGER NOT NULL, `announcementSubject` TEXT NOT NULL, `announcementText` TEXT, `announcementStartDate` TEXT, `announcementEndDate` TEXT, `teacherId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `announcementIdString` TEXT, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `announcementId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "announcementId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "announcementSubject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "announcementText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startDate", + "columnName": "announcementStartDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endDate", + "columnName": "announcementEndDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "idString", + "columnName": "announcementIdString", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "announcementId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_announcements_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_announcements_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "gradeCategories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `categoryId` INTEGER NOT NULL, `weight` REAL NOT NULL, `color` INTEGER NOT NULL, `text` TEXT, `columns` TEXT, `valueFrom` REAL NOT NULL, `valueTo` REAL NOT NULL, `type` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `categoryId`, `type`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "categoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "columns", + "columnName": "columns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "valueFrom", + "columnName": "valueFrom", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "valueTo", + "columnName": "valueTo", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "categoryId", + "type" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "feedbackMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `received` INTEGER NOT NULL, `text` TEXT NOT NULL, `senderName` TEXT NOT NULL, `deviceId` TEXT, `deviceName` TEXT, `devId` INTEGER, `devImage` TEXT, `sentTime` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "messageId", + "columnName": "messageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "received", + "columnName": "received", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "senderName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deviceName", + "columnName": "deviceName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "devId", + "columnName": "devId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "devImage", + "columnName": "devImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sentTime", + "columnName": "sentTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "messageId" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `messageId` INTEGER NOT NULL, `messageType` INTEGER NOT NULL, `messageSubject` TEXT NOT NULL, `messageBody` TEXT, `senderId` INTEGER, `addedDate` INTEGER NOT NULL, `messageIsPinned` INTEGER NOT NULL, `hasAttachments` INTEGER NOT NULL, `attachmentIds` TEXT, `attachmentNames` TEXT, `attachmentSizes` TEXT, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `messageId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "messageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "messageType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "messageSubject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "body", + "columnName": "messageBody", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "senderId", + "columnName": "senderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isStarred", + "columnName": "messageIsPinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasAttachments", + "columnName": "hasAttachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachmentIds", + "columnName": "attachmentIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentNames", + "columnName": "attachmentNames", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentSizes", + "columnName": "attachmentSizes", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "messageId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_messages_profileId_messageType", + "unique": false, + "columnNames": [ + "profileId", + "messageType" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_profileId_messageType` ON `${TABLE_NAME}` (`profileId`, `messageType`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "messageRecipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `messageRecipientId` INTEGER NOT NULL, `messageRecipientReplyId` INTEGER NOT NULL, `messageRecipientReadDate` INTEGER NOT NULL, `messageId` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `messageRecipientId`, `messageId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "messageRecipientId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyId", + "columnName": "messageRecipientReplyId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readDate", + "columnName": "messageRecipientReadDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "messageId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "messageRecipientId", + "messageId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "debugLogs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "endpointTimers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `endpointId` INTEGER NOT NULL, `endpointLastSync` INTEGER, `endpointNextSync` INTEGER NOT NULL, `endpointViewId` INTEGER, PRIMARY KEY(`profileId`, `endpointId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endpointId", + "columnName": "endpointId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "endpointLastSync", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nextSync", + "columnName": "endpointNextSync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "featureType", + "columnName": "endpointViewId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "endpointId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "lessonRanges", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `lessonRangeNumber` INTEGER NOT NULL, `lessonRangeStart` TEXT NOT NULL, `lessonRangeEnd` TEXT NOT NULL, PRIMARY KEY(`profileId`, `lessonRangeNumber`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lessonNumber", + "columnName": "lessonRangeNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startTime", + "columnName": "lessonRangeStart", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "endTime", + "columnName": "lessonRangeEnd", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "lessonRangeNumber" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `text` TEXT NOT NULL, `textLong` TEXT, `type` INTEGER NOT NULL, `profileId` INTEGER, `profileName` TEXT, `posted` INTEGER NOT NULL, `viewId` INTEGER, `extras` TEXT, `addedDate` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "textLong", + "columnName": "textLong", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileName", + "columnName": "profileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "posted", + "columnName": "posted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "navTarget", + "columnName": "viewId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "extras", + "columnName": "extras", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "classrooms", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`profileId`, `id`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "noticeTypes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`profileId`, `id`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "attendanceTypes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `baseType` INTEGER NOT NULL, `typeName` TEXT NOT NULL, `typeShort` TEXT NOT NULL, `typeSymbol` TEXT NOT NULL, `typeColor` INTEGER, PRIMARY KEY(`profileId`, `id`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseType", + "columnName": "baseType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "typeName", + "columnName": "typeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeShort", + "columnName": "typeShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeSymbol", + "columnName": "typeSymbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeColor", + "columnName": "typeColor", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `date` TEXT, `lessonNumber` INTEGER, `startTime` TEXT, `endTime` TEXT, `subjectId` INTEGER, `teacherId` INTEGER, `teamId` INTEGER, `classroom` TEXT, `oldDate` TEXT, `oldLessonNumber` INTEGER, `oldStartTime` TEXT, `oldEndTime` TEXT, `oldSubjectId` INTEGER, `oldTeacherId` INTEGER, `oldTeamId` INTEGER, `oldClassroom` TEXT, `isExtra` INTEGER NOT NULL, `ownerId` INTEGER NOT NULL, `color` INTEGER, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `id`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lessonNumber", + "columnName": "lessonNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startTime", + "columnName": "startTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endTime", + "columnName": "endTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "teamId", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "classroom", + "columnName": "classroom", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldDate", + "columnName": "oldDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldLessonNumber", + "columnName": "oldLessonNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "oldStartTime", + "columnName": "oldStartTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldEndTime", + "columnName": "oldEndTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldSubjectId", + "columnName": "oldSubjectId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "oldTeacherId", + "columnName": "oldTeacherId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "oldTeamId", + "columnName": "oldTeamId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "oldClassroom", + "columnName": "oldClassroom", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isExtra", + "columnName": "isExtra", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ownerId", + "columnName": "ownerId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_timetable_profileId_type_date", + "unique": false, + "columnNames": [ + "profileId", + "type", + "date" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_timetable_profileId_type_date` ON `${TABLE_NAME}` (`profileId`, `type`, `date`)" + }, + { + "name": "index_timetable_profileId_type_oldDate", + "unique": false, + "columnNames": [ + "profileId", + "type", + "oldDate" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_timetable_profileId_type_oldDate` ON `${TABLE_NAME}` (`profileId`, `type`, `oldDate`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "config", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `key` TEXT NOT NULL, `value` TEXT, PRIMARY KEY(`profileId`, `key`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "librusLessons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `lessonId` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `teamId` INTEGER, PRIMARY KEY(`profileId`, `lessonId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lessonId", + "columnName": "lessonId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teamId", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "lessonId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_librusLessons_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_librusLessons_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "timetableManual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `repeatBy` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `date` INTEGER, `weekDay` INTEGER, `lessonNumber` INTEGER, `startTime` TEXT, `endTime` TEXT, `subjectId` INTEGER, `teacherId` INTEGER, `teamId` INTEGER, `classroom` TEXT)", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatBy", + "columnName": "repeatBy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "weekDay", + "columnName": "weekDay", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lessonNumber", + "columnName": "lessonNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startTime", + "columnName": "startTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endTime", + "columnName": "endTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "teamId", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "classroom", + "columnName": "classroom", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_timetableManual_profileId_date", + "unique": false, + "columnNames": [ + "profileId", + "date" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_timetableManual_profileId_date` ON `${TABLE_NAME}` (`profileId`, `date`)" + }, + { + "name": "index_timetableManual_profileId_weekDay", + "unique": false, + "columnNames": [ + "profileId", + "weekDay" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_timetableManual_profileId_weekDay` ON `${TABLE_NAME}` (`profileId`, `weekDay`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `noteId` INTEGER NOT NULL, `noteOwnerType` TEXT, `noteOwnerId` INTEGER, `noteReplacesOriginal` INTEGER NOT NULL, `noteTopic` TEXT, `noteBody` TEXT NOT NULL, `noteColor` INTEGER, `noteSharedBy` TEXT, `noteSharedByName` TEXT, `addedDate` INTEGER NOT NULL, PRIMARY KEY(`noteId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "noteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ownerType", + "columnName": "noteOwnerType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "noteOwnerId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "replacesOriginal", + "columnName": "noteReplacesOriginal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "noteTopic", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "noteBody", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "noteColor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharedBy", + "columnName": "noteSharedBy", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedByName", + "columnName": "noteSharedByName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "noteId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_notes_profileId_noteOwnerType_noteOwnerId", + "unique": false, + "columnNames": [ + "profileId", + "noteOwnerType", + "noteOwnerId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notes_profileId_noteOwnerType_noteOwnerId` ON `${TABLE_NAME}` (`profileId`, `noteOwnerType`, `noteOwnerId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `metadataId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `thingType` INTEGER NOT NULL, `thingId` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "metadataId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thingType", + "columnName": "thingType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thingId", + "columnName": "thingId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "seen", + "columnName": "seen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notified", + "columnName": "notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "metadataId" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_metadata_profileId_thingType_thingId", + "unique": true, + "columnNames": [ + "profileId", + "thingType", + "thingId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_metadata_profileId_thingType_thingId` ON `${TABLE_NAME}` (`profileId`, `thingType`, `thingId`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4c141460d807d32d00faad4fb3c12522')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/97.json b/app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/97.json new file mode 100644 index 00000000..37b6d0f4 --- /dev/null +++ b/app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/97.json @@ -0,0 +1,2293 @@ +{ + "formatVersion": 1, + "database": { + "version": 97, + "identityHash": "08a8998e311e4e62a9e9554132b5c011", + "entities": [ + { + "tableName": "grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `gradeId` INTEGER NOT NULL, `gradeName` TEXT NOT NULL, `gradeType` INTEGER NOT NULL, `gradeValue` REAL NOT NULL, `gradeWeight` REAL NOT NULL, `gradeColor` INTEGER NOT NULL, `gradeCategory` TEXT, `gradeDescription` TEXT, `gradeComment` TEXT, `gradeSemester` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `gradeValueMax` REAL, `gradeClassAverage` REAL, `gradeParentId` INTEGER, `gradeIsImprovement` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `gradeId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "gradeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "gradeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "gradeType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "gradeValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "gradeWeight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "gradeColor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "gradeCategory", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "gradeDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "gradeComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "semester", + "columnName": "gradeSemester", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "valueMax", + "columnName": "gradeValueMax", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "classAverage", + "columnName": "gradeClassAverage", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "gradeParentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isImprovement", + "columnName": "gradeIsImprovement", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "gradeId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_grades_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_grades_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `teacherLoginId` TEXT, `teacherName` TEXT, `teacherSurname` TEXT, `teacherType` INTEGER NOT NULL, `teacherTypeDescription` TEXT, `teacherSubjects` TEXT NOT NULL, PRIMARY KEY(`profileId`, `teacherId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "teacherLoginId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "teacherName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "surname", + "columnName": "teacherSurname", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "teacherType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "typeDescription", + "columnName": "teacherTypeDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subjects", + "columnName": "teacherSubjects", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "teacherId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "teacherAbsence", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teacherAbsenceId` INTEGER NOT NULL, `teacherAbsenceType` INTEGER NOT NULL, `teacherAbsenceName` TEXT, `teacherAbsenceDateFrom` TEXT NOT NULL, `teacherAbsenceDateTo` TEXT NOT NULL, `teacherAbsenceTimeFrom` TEXT, `teacherAbsenceTimeTo` TEXT, `teacherId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `teacherAbsenceId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "teacherAbsenceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "teacherAbsenceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "teacherAbsenceName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dateFrom", + "columnName": "teacherAbsenceDateFrom", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateTo", + "columnName": "teacherAbsenceDateTo", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeFrom", + "columnName": "teacherAbsenceTimeFrom", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timeTo", + "columnName": "teacherAbsenceTimeTo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "teacherAbsenceId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_teacherAbsence_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_teacherAbsence_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "teacherAbsenceTypes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teacherAbsenceTypeId` INTEGER NOT NULL, `teacherAbsenceTypeName` TEXT NOT NULL, PRIMARY KEY(`profileId`, `teacherAbsenceTypeId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "teacherAbsenceTypeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "teacherAbsenceTypeName", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "teacherAbsenceTypeId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `subjectLongName` TEXT, `subjectShortName` TEXT, `subjectColor` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `subjectId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "subjectLongName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shortName", + "columnName": "subjectShortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "subjectColor", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "subjectId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `noticeId` INTEGER NOT NULL, `noticeType` INTEGER NOT NULL, `noticeSemester` INTEGER NOT NULL, `noticeText` TEXT NOT NULL, `noticeCategory` TEXT, `noticePoints` REAL, `teacherId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `noticeId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "noticeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "noticeType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semester", + "columnName": "noticeSemester", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "noticeText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "noticeCategory", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "noticePoints", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "noticeId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_notices_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notices_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "teams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teamId` INTEGER NOT NULL, `teamType` INTEGER NOT NULL, `teamName` TEXT, `teamCode` TEXT, `teamTeacherId` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `teamId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "teamType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "teamName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "code", + "columnName": "teamCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teamTeacherId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "teamId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "attendances", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `attendanceId` INTEGER NOT NULL, `attendanceBaseType` INTEGER NOT NULL, `attendanceTypeName` TEXT NOT NULL, `attendanceTypeShort` TEXT NOT NULL, `attendanceTypeSymbol` TEXT NOT NULL, `attendanceTypeColor` INTEGER, `attendanceDate` TEXT NOT NULL, `attendanceTime` TEXT, `attendanceSemester` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `attendanceLessonTopic` TEXT, `attendanceLessonNumber` INTEGER, `attendanceIsCounted` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `attendanceId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "attendanceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseType", + "columnName": "attendanceBaseType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "typeName", + "columnName": "attendanceTypeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeShort", + "columnName": "attendanceTypeShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeSymbol", + "columnName": "attendanceTypeSymbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeColor", + "columnName": "attendanceTypeColor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "date", + "columnName": "attendanceDate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startTime", + "columnName": "attendanceTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "semester", + "columnName": "attendanceSemester", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lessonTopic", + "columnName": "attendanceLessonTopic", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lessonNumber", + "columnName": "attendanceLessonNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isCounted", + "columnName": "attendanceIsCounted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "attendanceId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_attendances_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_attendances_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `eventId` INTEGER NOT NULL, `eventDate` TEXT NOT NULL, `eventTime` TEXT, `eventTopic` TEXT NOT NULL, `eventColor` INTEGER, `eventType` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `teamId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `eventAddedManually` INTEGER NOT NULL, `eventSharedBy` TEXT, `eventSharedByName` TEXT, `eventBlacklisted` INTEGER NOT NULL, `eventIsDone` INTEGER NOT NULL, `eventIsDownloaded` INTEGER NOT NULL, `homeworkBody` TEXT, `attachmentIds` TEXT, `attachmentNames` TEXT, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `eventId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "eventId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "eventDate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "eventTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "topic", + "columnName": "eventTopic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "eventColor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "eventType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teamId", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedManually", + "columnName": "eventAddedManually", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sharedBy", + "columnName": "eventSharedBy", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedByName", + "columnName": "eventSharedByName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "blacklisted", + "columnName": "eventBlacklisted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "eventIsDone", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDownloaded", + "columnName": "eventIsDownloaded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homeworkBody", + "columnName": "homeworkBody", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentIds", + "columnName": "attachmentIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentNames", + "columnName": "attachmentNames", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "eventId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_events_profileId_eventDate_eventTime", + "unique": false, + "columnNames": [ + "profileId", + "eventDate", + "eventTime" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_events_profileId_eventDate_eventTime` ON `${TABLE_NAME}` (`profileId`, `eventDate`, `eventTime`)" + }, + { + "name": "index_events_profileId_eventType", + "unique": false, + "columnNames": [ + "profileId", + "eventType" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_events_profileId_eventType` ON `${TABLE_NAME}` (`profileId`, `eventType`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "eventTypes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `eventType` INTEGER NOT NULL, `eventTypeName` TEXT NOT NULL, `eventTypeColor` INTEGER NOT NULL, `eventTypeOrder` INTEGER NOT NULL, `eventTypeSource` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `eventType`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "eventType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "eventTypeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "eventTypeColor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "eventTypeOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "eventTypeSource", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "eventType" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "loginStores", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`loginStoreId` INTEGER NOT NULL, `loginStoreType` INTEGER NOT NULL, `loginStoreMode` INTEGER NOT NULL, `loginStoreData` TEXT NOT NULL, PRIMARY KEY(`loginStoreId`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "loginStoreId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "loginStoreType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "loginStoreMode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "loginStoreData", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "loginStoreId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "profiles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `loginStoreId` INTEGER NOT NULL, `loginStoreType` INTEGER NOT NULL, `name` TEXT NOT NULL, `subname` TEXT, `studentNameLong` TEXT NOT NULL, `studentNameShort` TEXT NOT NULL, `accountName` TEXT, `studentData` TEXT NOT NULL, `image` TEXT, `empty` INTEGER NOT NULL, `archived` INTEGER NOT NULL, `archiveId` INTEGER, `syncEnabled` INTEGER NOT NULL, `enableSharedEvents` INTEGER NOT NULL, `registration` INTEGER NOT NULL, `userCode` TEXT NOT NULL, `studentNumber` INTEGER NOT NULL, `studentClassName` TEXT, `studentSchoolYearStart` INTEGER NOT NULL, `dateSemester1Start` TEXT NOT NULL, `dateSemester2Start` TEXT NOT NULL, `dateYearEnd` TEXT NOT NULL, `disabledNotifications` TEXT, `lastReceiversSync` INTEGER NOT NULL, PRIMARY KEY(`profileId`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loginStoreId", + "columnName": "loginStoreId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loginStoreType", + "columnName": "loginStoreType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subname", + "columnName": "subname", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "studentNameLong", + "columnName": "studentNameLong", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentNameShort", + "columnName": "studentNameShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "studentData", + "columnName": "studentData", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "empty", + "columnName": "empty", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "archived", + "columnName": "archived", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "archiveId", + "columnName": "archiveId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "syncEnabled", + "columnName": "syncEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableSharedEvents", + "columnName": "enableSharedEvents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registration", + "columnName": "registration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userCode", + "columnName": "userCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentNumber", + "columnName": "studentNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentClassName", + "columnName": "studentClassName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "studentSchoolYearStart", + "columnName": "studentSchoolYearStart", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateSemester1Start", + "columnName": "dateSemester1Start", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateSemester2Start", + "columnName": "dateSemester2Start", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateYearEnd", + "columnName": "dateYearEnd", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "disabledNotifications", + "columnName": "disabledNotifications", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastReceiversSync", + "columnName": "lastReceiversSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "luckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `luckyNumberDate` INTEGER NOT NULL, `luckyNumber` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `luckyNumberDate`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "luckyNumberDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "luckyNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "luckyNumberDate" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "announcements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `announcementId` INTEGER NOT NULL, `announcementSubject` TEXT NOT NULL, `announcementText` TEXT, `announcementStartDate` TEXT, `announcementEndDate` TEXT, `teacherId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `announcementIdString` TEXT, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `announcementId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "announcementId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "announcementSubject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "announcementText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startDate", + "columnName": "announcementStartDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endDate", + "columnName": "announcementEndDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "idString", + "columnName": "announcementIdString", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "announcementId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_announcements_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_announcements_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "gradeCategories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `categoryId` INTEGER NOT NULL, `weight` REAL NOT NULL, `color` INTEGER NOT NULL, `text` TEXT, `columns` TEXT, `valueFrom` REAL NOT NULL, `valueTo` REAL NOT NULL, `type` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `categoryId`, `type`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "categoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "columns", + "columnName": "columns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "valueFrom", + "columnName": "valueFrom", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "valueTo", + "columnName": "valueTo", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "categoryId", + "type" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "feedbackMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `received` INTEGER NOT NULL, `text` TEXT NOT NULL, `senderName` TEXT NOT NULL, `deviceId` TEXT, `deviceName` TEXT, `devId` INTEGER, `devImage` TEXT, `sentTime` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "messageId", + "columnName": "messageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "received", + "columnName": "received", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "senderName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deviceName", + "columnName": "deviceName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "devId", + "columnName": "devId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "devImage", + "columnName": "devImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sentTime", + "columnName": "sentTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "messageId" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `messageId` INTEGER NOT NULL, `messageType` INTEGER NOT NULL, `messageSubject` TEXT NOT NULL, `messageBody` TEXT, `senderId` INTEGER, `addedDate` INTEGER NOT NULL, `messageIsPinned` INTEGER NOT NULL, `hasAttachments` INTEGER NOT NULL, `attachmentIds` TEXT, `attachmentNames` TEXT, `attachmentSizes` TEXT, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `messageId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "messageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "messageType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "messageSubject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "body", + "columnName": "messageBody", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "senderId", + "columnName": "senderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isStarred", + "columnName": "messageIsPinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasAttachments", + "columnName": "hasAttachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachmentIds", + "columnName": "attachmentIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentNames", + "columnName": "attachmentNames", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentSizes", + "columnName": "attachmentSizes", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "messageId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_messages_profileId_messageType", + "unique": false, + "columnNames": [ + "profileId", + "messageType" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_profileId_messageType` ON `${TABLE_NAME}` (`profileId`, `messageType`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "messageRecipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `messageRecipientId` INTEGER NOT NULL, `messageRecipientReplyId` INTEGER NOT NULL, `messageRecipientReadDate` INTEGER NOT NULL, `messageId` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `messageRecipientId`, `messageId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "messageRecipientId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyId", + "columnName": "messageRecipientReplyId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readDate", + "columnName": "messageRecipientReadDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "messageId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "messageRecipientId", + "messageId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "debugLogs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "endpointTimers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `endpointId` INTEGER NOT NULL, `endpointLastSync` INTEGER, `endpointNextSync` INTEGER NOT NULL, `endpointViewId` INTEGER, PRIMARY KEY(`profileId`, `endpointId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endpointId", + "columnName": "endpointId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "endpointLastSync", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nextSync", + "columnName": "endpointNextSync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viewId", + "columnName": "endpointViewId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "endpointId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "lessonRanges", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `lessonRangeNumber` INTEGER NOT NULL, `lessonRangeStart` TEXT NOT NULL, `lessonRangeEnd` TEXT NOT NULL, PRIMARY KEY(`profileId`, `lessonRangeNumber`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lessonNumber", + "columnName": "lessonRangeNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startTime", + "columnName": "lessonRangeStart", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "endTime", + "columnName": "lessonRangeEnd", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "lessonRangeNumber" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `text` TEXT NOT NULL, `textLong` TEXT, `type` INTEGER NOT NULL, `profileId` INTEGER, `profileName` TEXT, `posted` INTEGER NOT NULL, `viewId` INTEGER, `extras` TEXT, `addedDate` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "textLong", + "columnName": "textLong", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileName", + "columnName": "profileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "posted", + "columnName": "posted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viewId", + "columnName": "viewId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "extras", + "columnName": "extras", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "classrooms", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`profileId`, `id`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "noticeTypes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`profileId`, `id`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "attendanceTypes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `baseType` INTEGER NOT NULL, `typeName` TEXT NOT NULL, `typeShort` TEXT NOT NULL, `typeSymbol` TEXT NOT NULL, `typeColor` INTEGER, PRIMARY KEY(`profileId`, `id`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseType", + "columnName": "baseType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "typeName", + "columnName": "typeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeShort", + "columnName": "typeShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeSymbol", + "columnName": "typeSymbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeColor", + "columnName": "typeColor", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `date` TEXT, `lessonNumber` INTEGER, `startTime` TEXT, `endTime` TEXT, `subjectId` INTEGER, `teacherId` INTEGER, `teamId` INTEGER, `classroom` TEXT, `oldDate` TEXT, `oldLessonNumber` INTEGER, `oldStartTime` TEXT, `oldEndTime` TEXT, `oldSubjectId` INTEGER, `oldTeacherId` INTEGER, `oldTeamId` INTEGER, `oldClassroom` TEXT, `isExtra` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `id`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lessonNumber", + "columnName": "lessonNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startTime", + "columnName": "startTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endTime", + "columnName": "endTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "teamId", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "classroom", + "columnName": "classroom", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldDate", + "columnName": "oldDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldLessonNumber", + "columnName": "oldLessonNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "oldStartTime", + "columnName": "oldStartTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldEndTime", + "columnName": "oldEndTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldSubjectId", + "columnName": "oldSubjectId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "oldTeacherId", + "columnName": "oldTeacherId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "oldTeamId", + "columnName": "oldTeamId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "oldClassroom", + "columnName": "oldClassroom", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isExtra", + "columnName": "isExtra", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_timetable_profileId_type_date", + "unique": false, + "columnNames": [ + "profileId", + "type", + "date" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_timetable_profileId_type_date` ON `${TABLE_NAME}` (`profileId`, `type`, `date`)" + }, + { + "name": "index_timetable_profileId_type_oldDate", + "unique": false, + "columnNames": [ + "profileId", + "type", + "oldDate" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_timetable_profileId_type_oldDate` ON `${TABLE_NAME}` (`profileId`, `type`, `oldDate`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "config", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `key` TEXT NOT NULL, `value` TEXT, PRIMARY KEY(`profileId`, `key`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "librusLessons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `lessonId` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `teamId` INTEGER, PRIMARY KEY(`profileId`, `lessonId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lessonId", + "columnName": "lessonId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teamId", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "lessonId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_librusLessons_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_librusLessons_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "timetableManual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `repeatBy` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `date` INTEGER, `weekDay` INTEGER, `lessonNumber` INTEGER, `startTime` TEXT, `endTime` TEXT, `subjectId` INTEGER, `teacherId` INTEGER, `teamId` INTEGER, `classroom` TEXT)", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatBy", + "columnName": "repeatBy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "weekDay", + "columnName": "weekDay", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lessonNumber", + "columnName": "lessonNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startTime", + "columnName": "startTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endTime", + "columnName": "endTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "teamId", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "classroom", + "columnName": "classroom", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_timetableManual_profileId_date", + "unique": false, + "columnNames": [ + "profileId", + "date" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_timetableManual_profileId_date` ON `${TABLE_NAME}` (`profileId`, `date`)" + }, + { + "name": "index_timetableManual_profileId_weekDay", + "unique": false, + "columnNames": [ + "profileId", + "weekDay" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_timetableManual_profileId_weekDay` ON `${TABLE_NAME}` (`profileId`, `weekDay`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `noteId` INTEGER NOT NULL, `noteOwnerType` TEXT, `noteOwnerId` INTEGER, `noteReplacesOriginal` INTEGER NOT NULL, `noteTopic` TEXT, `noteBody` TEXT NOT NULL, `noteColor` INTEGER, `noteSharedBy` TEXT, `noteSharedByName` TEXT, `addedDate` INTEGER NOT NULL, PRIMARY KEY(`noteId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "noteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ownerType", + "columnName": "noteOwnerType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "noteOwnerId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "replacesOriginal", + "columnName": "noteReplacesOriginal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "noteTopic", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "noteBody", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "noteColor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharedBy", + "columnName": "noteSharedBy", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedByName", + "columnName": "noteSharedByName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "noteId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_notes_profileId_noteOwnerType_noteOwnerId", + "unique": false, + "columnNames": [ + "profileId", + "noteOwnerType", + "noteOwnerId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notes_profileId_noteOwnerType_noteOwnerId` ON `${TABLE_NAME}` (`profileId`, `noteOwnerType`, `noteOwnerId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `metadataId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `thingType` INTEGER NOT NULL, `thingId` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "metadataId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thingType", + "columnName": "thingType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thingId", + "columnName": "thingId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "seen", + "columnName": "seen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notified", + "columnName": "notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "metadataId" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_metadata_profileId_thingType_thingId", + "unique": true, + "columnNames": [ + "profileId", + "thingType", + "thingId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_metadata_profileId_thingType_thingId` ON `${TABLE_NAME}` (`profileId`, `thingType`, `thingId`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '08a8998e311e4e62a9e9554132b5c011')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/98.json b/app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/98.json new file mode 100644 index 00000000..1a291aef --- /dev/null +++ b/app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/98.json @@ -0,0 +1,2314 @@ +{ + "formatVersion": 1, + "database": { + "version": 98, + "identityHash": "2612ebba9802eedc7ebc69724606a23c", + "entities": [ + { + "tableName": "grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `gradeId` INTEGER NOT NULL, `gradeName` TEXT NOT NULL, `gradeType` INTEGER NOT NULL, `gradeValue` REAL NOT NULL, `gradeWeight` REAL NOT NULL, `gradeColor` INTEGER NOT NULL, `gradeCategory` TEXT, `gradeDescription` TEXT, `gradeComment` TEXT, `gradeSemester` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `gradeValueMax` REAL, `gradeClassAverage` REAL, `gradeParentId` INTEGER, `gradeIsImprovement` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `gradeId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "gradeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "gradeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "gradeType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "gradeValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "gradeWeight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "gradeColor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "gradeCategory", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "gradeDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "gradeComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "semester", + "columnName": "gradeSemester", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "valueMax", + "columnName": "gradeValueMax", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "classAverage", + "columnName": "gradeClassAverage", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "gradeParentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isImprovement", + "columnName": "gradeIsImprovement", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "gradeId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_grades_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_grades_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `teacherLoginId` TEXT, `teacherName` TEXT, `teacherSurname` TEXT, `teacherType` INTEGER NOT NULL, `teacherTypeDescription` TEXT, `teacherSubjects` TEXT NOT NULL, PRIMARY KEY(`profileId`, `teacherId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "teacherLoginId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "teacherName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "surname", + "columnName": "teacherSurname", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "teacherType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "typeDescription", + "columnName": "teacherTypeDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subjects", + "columnName": "teacherSubjects", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "teacherId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "teacherAbsence", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teacherAbsenceId` INTEGER NOT NULL, `teacherAbsenceType` INTEGER NOT NULL, `teacherAbsenceName` TEXT, `teacherAbsenceDateFrom` TEXT NOT NULL, `teacherAbsenceDateTo` TEXT NOT NULL, `teacherAbsenceTimeFrom` TEXT, `teacherAbsenceTimeTo` TEXT, `teacherId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `teacherAbsenceId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "teacherAbsenceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "teacherAbsenceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "teacherAbsenceName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dateFrom", + "columnName": "teacherAbsenceDateFrom", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateTo", + "columnName": "teacherAbsenceDateTo", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeFrom", + "columnName": "teacherAbsenceTimeFrom", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timeTo", + "columnName": "teacherAbsenceTimeTo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "teacherAbsenceId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_teacherAbsence_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_teacherAbsence_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "teacherAbsenceTypes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teacherAbsenceTypeId` INTEGER NOT NULL, `teacherAbsenceTypeName` TEXT NOT NULL, PRIMARY KEY(`profileId`, `teacherAbsenceTypeId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "teacherAbsenceTypeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "teacherAbsenceTypeName", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "teacherAbsenceTypeId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `subjectLongName` TEXT, `subjectShortName` TEXT, `subjectColor` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `subjectId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "subjectLongName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shortName", + "columnName": "subjectShortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "subjectColor", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "subjectId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `noticeId` INTEGER NOT NULL, `noticeType` INTEGER NOT NULL, `noticeSemester` INTEGER NOT NULL, `noticeText` TEXT NOT NULL, `noticeCategory` TEXT, `noticePoints` REAL, `teacherId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `noticeId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "noticeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "noticeType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semester", + "columnName": "noticeSemester", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "noticeText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "noticeCategory", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "noticePoints", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "noticeId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_notices_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notices_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "teams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teamId` INTEGER NOT NULL, `teamType` INTEGER NOT NULL, `teamName` TEXT, `teamCode` TEXT, `teamTeacherId` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `teamId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "teamType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "teamName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "code", + "columnName": "teamCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teamTeacherId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "teamId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "attendances", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `attendanceId` INTEGER NOT NULL, `attendanceBaseType` INTEGER NOT NULL, `attendanceTypeName` TEXT NOT NULL, `attendanceTypeShort` TEXT NOT NULL, `attendanceTypeSymbol` TEXT NOT NULL, `attendanceTypeColor` INTEGER, `attendanceDate` TEXT NOT NULL, `attendanceTime` TEXT, `attendanceSemester` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `attendanceLessonTopic` TEXT, `attendanceLessonNumber` INTEGER, `attendanceIsCounted` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `attendanceId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "attendanceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseType", + "columnName": "attendanceBaseType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "typeName", + "columnName": "attendanceTypeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeShort", + "columnName": "attendanceTypeShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeSymbol", + "columnName": "attendanceTypeSymbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeColor", + "columnName": "attendanceTypeColor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "date", + "columnName": "attendanceDate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startTime", + "columnName": "attendanceTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "semester", + "columnName": "attendanceSemester", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lessonTopic", + "columnName": "attendanceLessonTopic", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lessonNumber", + "columnName": "attendanceLessonNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isCounted", + "columnName": "attendanceIsCounted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "attendanceId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_attendances_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_attendances_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `eventId` INTEGER NOT NULL, `eventDate` TEXT NOT NULL, `eventTime` TEXT, `eventTopic` TEXT NOT NULL, `eventColor` INTEGER, `eventType` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `teamId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `eventAddedManually` INTEGER NOT NULL, `eventSharedBy` TEXT, `eventSharedByName` TEXT, `eventBlacklisted` INTEGER NOT NULL, `eventIsDone` INTEGER NOT NULL, `eventIsDownloaded` INTEGER NOT NULL, `homeworkBody` TEXT, `attachmentIds` TEXT, `attachmentNames` TEXT, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `eventId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "eventId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "eventDate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "eventTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "topic", + "columnName": "eventTopic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "eventColor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "eventType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teamId", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedManually", + "columnName": "eventAddedManually", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sharedBy", + "columnName": "eventSharedBy", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedByName", + "columnName": "eventSharedByName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "blacklisted", + "columnName": "eventBlacklisted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "eventIsDone", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDownloaded", + "columnName": "eventIsDownloaded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homeworkBody", + "columnName": "homeworkBody", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentIds", + "columnName": "attachmentIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentNames", + "columnName": "attachmentNames", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "eventId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_events_profileId_eventDate_eventTime", + "unique": false, + "columnNames": [ + "profileId", + "eventDate", + "eventTime" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_events_profileId_eventDate_eventTime` ON `${TABLE_NAME}` (`profileId`, `eventDate`, `eventTime`)" + }, + { + "name": "index_events_profileId_eventType", + "unique": false, + "columnNames": [ + "profileId", + "eventType" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_events_profileId_eventType` ON `${TABLE_NAME}` (`profileId`, `eventType`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "eventTypes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `eventType` INTEGER NOT NULL, `eventTypeName` TEXT NOT NULL, `eventTypeColor` INTEGER NOT NULL, `eventTypeOrder` INTEGER NOT NULL, `eventTypeSource` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `eventType`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "eventType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "eventTypeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "eventTypeColor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "eventTypeOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "eventTypeSource", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "eventType" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "loginStores", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`loginStoreId` INTEGER NOT NULL, `loginStoreType` INTEGER NOT NULL, `loginStoreMode` INTEGER NOT NULL, `loginStoreData` TEXT NOT NULL, PRIMARY KEY(`loginStoreId`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "loginStoreId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "loginStoreType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "loginStoreMode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "loginStoreData", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "loginStoreId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "profiles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `loginStoreId` INTEGER NOT NULL, `loginStoreType` INTEGER NOT NULL, `name` TEXT NOT NULL, `subname` TEXT, `studentNameLong` TEXT NOT NULL, `studentNameShort` TEXT NOT NULL, `accountName` TEXT, `studentData` TEXT NOT NULL, `image` TEXT, `empty` INTEGER NOT NULL, `archived` INTEGER NOT NULL, `archiveId` INTEGER, `syncEnabled` INTEGER NOT NULL, `enableSharedEvents` INTEGER NOT NULL, `registration` INTEGER NOT NULL, `userCode` TEXT NOT NULL, `studentNumber` INTEGER NOT NULL, `studentClassName` TEXT, `studentSchoolYearStart` INTEGER NOT NULL, `dateSemester1Start` TEXT NOT NULL, `dateSemester2Start` TEXT NOT NULL, `dateYearEnd` TEXT NOT NULL, `disabledNotifications` TEXT, `lastReceiversSync` INTEGER NOT NULL, PRIMARY KEY(`profileId`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loginStoreId", + "columnName": "loginStoreId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loginStoreType", + "columnName": "loginStoreType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subname", + "columnName": "subname", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "studentNameLong", + "columnName": "studentNameLong", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentNameShort", + "columnName": "studentNameShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "studentData", + "columnName": "studentData", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "empty", + "columnName": "empty", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "archived", + "columnName": "archived", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "archiveId", + "columnName": "archiveId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "syncEnabled", + "columnName": "syncEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableSharedEvents", + "columnName": "enableSharedEvents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registration", + "columnName": "registration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userCode", + "columnName": "userCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentNumber", + "columnName": "studentNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentClassName", + "columnName": "studentClassName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "studentSchoolYearStart", + "columnName": "studentSchoolYearStart", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateSemester1Start", + "columnName": "dateSemester1Start", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateSemester2Start", + "columnName": "dateSemester2Start", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateYearEnd", + "columnName": "dateYearEnd", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "disabledNotifications", + "columnName": "disabledNotifications", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastReceiversSync", + "columnName": "lastReceiversSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "luckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `luckyNumberDate` INTEGER NOT NULL, `luckyNumber` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `luckyNumberDate`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "luckyNumberDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "luckyNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "luckyNumberDate" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "announcements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `announcementId` INTEGER NOT NULL, `announcementSubject` TEXT NOT NULL, `announcementText` TEXT, `announcementStartDate` TEXT, `announcementEndDate` TEXT, `teacherId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `announcementIdString` TEXT, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `announcementId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "announcementId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "announcementSubject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "announcementText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startDate", + "columnName": "announcementStartDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endDate", + "columnName": "announcementEndDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "idString", + "columnName": "announcementIdString", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "announcementId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_announcements_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_announcements_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "gradeCategories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `categoryId` INTEGER NOT NULL, `weight` REAL NOT NULL, `color` INTEGER NOT NULL, `text` TEXT, `columns` TEXT, `valueFrom` REAL NOT NULL, `valueTo` REAL NOT NULL, `type` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `categoryId`, `type`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "categoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "columns", + "columnName": "columns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "valueFrom", + "columnName": "valueFrom", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "valueTo", + "columnName": "valueTo", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "categoryId", + "type" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "feedbackMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `received` INTEGER NOT NULL, `text` TEXT NOT NULL, `senderName` TEXT NOT NULL, `deviceId` TEXT, `deviceName` TEXT, `devId` INTEGER, `devImage` TEXT, `sentTime` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "messageId", + "columnName": "messageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "received", + "columnName": "received", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "senderName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deviceName", + "columnName": "deviceName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "devId", + "columnName": "devId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "devImage", + "columnName": "devImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sentTime", + "columnName": "sentTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "messageId" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `messageId` INTEGER NOT NULL, `messageType` INTEGER NOT NULL, `messageSubject` TEXT NOT NULL, `messageBody` TEXT, `senderId` INTEGER, `addedDate` INTEGER NOT NULL, `messageIsPinned` INTEGER NOT NULL, `hasAttachments` INTEGER NOT NULL, `attachmentIds` TEXT, `attachmentNames` TEXT, `attachmentSizes` TEXT, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `messageId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "messageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "messageType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "messageSubject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "body", + "columnName": "messageBody", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "senderId", + "columnName": "senderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isStarred", + "columnName": "messageIsPinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasAttachments", + "columnName": "hasAttachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachmentIds", + "columnName": "attachmentIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentNames", + "columnName": "attachmentNames", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentSizes", + "columnName": "attachmentSizes", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "messageId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_messages_profileId_messageType", + "unique": false, + "columnNames": [ + "profileId", + "messageType" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_profileId_messageType` ON `${TABLE_NAME}` (`profileId`, `messageType`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "messageRecipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `messageRecipientId` INTEGER NOT NULL, `messageRecipientReplyId` INTEGER NOT NULL, `messageRecipientReadDate` INTEGER NOT NULL, `messageId` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `messageRecipientId`, `messageId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "messageRecipientId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyId", + "columnName": "messageRecipientReplyId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readDate", + "columnName": "messageRecipientReadDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "messageId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "messageRecipientId", + "messageId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "debugLogs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "endpointTimers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `endpointId` INTEGER NOT NULL, `endpointLastSync` INTEGER, `endpointNextSync` INTEGER NOT NULL, `endpointViewId` INTEGER, PRIMARY KEY(`profileId`, `endpointId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endpointId", + "columnName": "endpointId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "endpointLastSync", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nextSync", + "columnName": "endpointNextSync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viewId", + "columnName": "endpointViewId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "endpointId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "lessonRanges", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `lessonRangeNumber` INTEGER NOT NULL, `lessonRangeStart` TEXT NOT NULL, `lessonRangeEnd` TEXT NOT NULL, PRIMARY KEY(`profileId`, `lessonRangeNumber`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lessonNumber", + "columnName": "lessonRangeNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startTime", + "columnName": "lessonRangeStart", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "endTime", + "columnName": "lessonRangeEnd", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "lessonRangeNumber" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `text` TEXT NOT NULL, `textLong` TEXT, `type` INTEGER NOT NULL, `profileId` INTEGER, `profileName` TEXT, `posted` INTEGER NOT NULL, `viewId` INTEGER, `extras` TEXT, `addedDate` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "textLong", + "columnName": "textLong", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileName", + "columnName": "profileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "posted", + "columnName": "posted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viewId", + "columnName": "viewId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "extras", + "columnName": "extras", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "classrooms", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`profileId`, `id`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "noticeTypes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`profileId`, `id`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "attendanceTypes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `baseType` INTEGER NOT NULL, `typeName` TEXT NOT NULL, `typeShort` TEXT NOT NULL, `typeSymbol` TEXT NOT NULL, `typeColor` INTEGER, PRIMARY KEY(`profileId`, `id`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseType", + "columnName": "baseType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "typeName", + "columnName": "typeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeShort", + "columnName": "typeShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeSymbol", + "columnName": "typeSymbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeColor", + "columnName": "typeColor", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `date` TEXT, `lessonNumber` INTEGER, `startTime` TEXT, `endTime` TEXT, `subjectId` INTEGER, `teacherId` INTEGER, `teamId` INTEGER, `classroom` TEXT, `oldDate` TEXT, `oldLessonNumber` INTEGER, `oldStartTime` TEXT, `oldEndTime` TEXT, `oldSubjectId` INTEGER, `oldTeacherId` INTEGER, `oldTeamId` INTEGER, `oldClassroom` TEXT, `isExtra` INTEGER NOT NULL, `color` INTEGER, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `id`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lessonNumber", + "columnName": "lessonNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startTime", + "columnName": "startTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endTime", + "columnName": "endTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "teamId", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "classroom", + "columnName": "classroom", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldDate", + "columnName": "oldDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldLessonNumber", + "columnName": "oldLessonNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "oldStartTime", + "columnName": "oldStartTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldEndTime", + "columnName": "oldEndTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldSubjectId", + "columnName": "oldSubjectId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "oldTeacherId", + "columnName": "oldTeacherId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "oldTeamId", + "columnName": "oldTeamId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "oldClassroom", + "columnName": "oldClassroom", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isExtra", + "columnName": "isExtra", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_timetable_profileId_type_date", + "unique": false, + "columnNames": [ + "profileId", + "type", + "date" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_timetable_profileId_type_date` ON `${TABLE_NAME}` (`profileId`, `type`, `date`)" + }, + { + "name": "index_timetable_profileId_type_oldDate", + "unique": false, + "columnNames": [ + "profileId", + "type", + "oldDate" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_timetable_profileId_type_oldDate` ON `${TABLE_NAME}` (`profileId`, `type`, `oldDate`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "config", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `key` TEXT NOT NULL, `value` TEXT, PRIMARY KEY(`profileId`, `key`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "librusLessons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `lessonId` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `teamId` INTEGER, PRIMARY KEY(`profileId`, `lessonId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lessonId", + "columnName": "lessonId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teamId", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "lessonId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_librusLessons_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_librusLessons_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "timetableManual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `repeatBy` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `date` INTEGER, `weekDay` INTEGER, `lessonNumber` INTEGER, `startTime` TEXT, `endTime` TEXT, `subjectId` INTEGER, `teacherId` INTEGER, `teamId` INTEGER, `classroom` TEXT)", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatBy", + "columnName": "repeatBy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "weekDay", + "columnName": "weekDay", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lessonNumber", + "columnName": "lessonNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startTime", + "columnName": "startTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endTime", + "columnName": "endTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "teamId", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "classroom", + "columnName": "classroom", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_timetableManual_profileId_date", + "unique": false, + "columnNames": [ + "profileId", + "date" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_timetableManual_profileId_date` ON `${TABLE_NAME}` (`profileId`, `date`)" + }, + { + "name": "index_timetableManual_profileId_weekDay", + "unique": false, + "columnNames": [ + "profileId", + "weekDay" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_timetableManual_profileId_weekDay` ON `${TABLE_NAME}` (`profileId`, `weekDay`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `noteId` INTEGER NOT NULL, `noteOwnerType` TEXT, `noteOwnerId` INTEGER, `noteReplacesOriginal` INTEGER NOT NULL, `noteTopic` TEXT, `noteBody` TEXT NOT NULL, `noteColor` INTEGER, `noteSharedBy` TEXT, `noteSharedByName` TEXT, `addedDate` INTEGER NOT NULL, PRIMARY KEY(`noteId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "noteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ownerType", + "columnName": "noteOwnerType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "noteOwnerId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "replacesOriginal", + "columnName": "noteReplacesOriginal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "noteTopic", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "noteBody", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "noteColor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharedBy", + "columnName": "noteSharedBy", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedByName", + "columnName": "noteSharedByName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "noteId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_notes_profileId_noteOwnerType_noteOwnerId", + "unique": false, + "columnNames": [ + "profileId", + "noteOwnerType", + "noteOwnerId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notes_profileId_noteOwnerType_noteOwnerId` ON `${TABLE_NAME}` (`profileId`, `noteOwnerType`, `noteOwnerId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `metadataId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `thingType` INTEGER NOT NULL, `thingId` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "metadataId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thingType", + "columnName": "thingType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thingId", + "columnName": "thingId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "seen", + "columnName": "seen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notified", + "columnName": "notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "metadataId" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_metadata_profileId_thingType_thingId", + "unique": true, + "columnNames": [ + "profileId", + "thingType", + "thingId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_metadata_profileId_thingType_thingId` ON `${TABLE_NAME}` (`profileId`, `thingType`, `thingId`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2612ebba9802eedc7ebc69724606a23c')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/99.json b/app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/99.json new file mode 100644 index 00000000..7db65929 --- /dev/null +++ b/app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/99.json @@ -0,0 +1,2314 @@ +{ + "formatVersion": 1, + "database": { + "version": 99, + "identityHash": "2612ebba9802eedc7ebc69724606a23c", + "entities": [ + { + "tableName": "grades", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `gradeId` INTEGER NOT NULL, `gradeName` TEXT NOT NULL, `gradeType` INTEGER NOT NULL, `gradeValue` REAL NOT NULL, `gradeWeight` REAL NOT NULL, `gradeColor` INTEGER NOT NULL, `gradeCategory` TEXT, `gradeDescription` TEXT, `gradeComment` TEXT, `gradeSemester` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `gradeValueMax` REAL, `gradeClassAverage` REAL, `gradeParentId` INTEGER, `gradeIsImprovement` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `gradeId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "gradeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "gradeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "gradeType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "gradeValue", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "gradeWeight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "gradeColor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "gradeCategory", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "gradeDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "comment", + "columnName": "gradeComment", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "semester", + "columnName": "gradeSemester", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "valueMax", + "columnName": "gradeValueMax", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "classAverage", + "columnName": "gradeClassAverage", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "parentId", + "columnName": "gradeParentId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isImprovement", + "columnName": "gradeIsImprovement", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "gradeId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_grades_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_grades_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "teachers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `teacherLoginId` TEXT, `teacherName` TEXT, `teacherSurname` TEXT, `teacherType` INTEGER NOT NULL, `teacherTypeDescription` TEXT, `teacherSubjects` TEXT NOT NULL, PRIMARY KEY(`profileId`, `teacherId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loginId", + "columnName": "teacherLoginId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "teacherName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "surname", + "columnName": "teacherSurname", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "teacherType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "typeDescription", + "columnName": "teacherTypeDescription", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subjects", + "columnName": "teacherSubjects", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "teacherId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "teacherAbsence", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teacherAbsenceId` INTEGER NOT NULL, `teacherAbsenceType` INTEGER NOT NULL, `teacherAbsenceName` TEXT, `teacherAbsenceDateFrom` TEXT NOT NULL, `teacherAbsenceDateTo` TEXT NOT NULL, `teacherAbsenceTimeFrom` TEXT, `teacherAbsenceTimeTo` TEXT, `teacherId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `teacherAbsenceId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "teacherAbsenceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "teacherAbsenceType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "teacherAbsenceName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dateFrom", + "columnName": "teacherAbsenceDateFrom", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateTo", + "columnName": "teacherAbsenceDateTo", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timeFrom", + "columnName": "teacherAbsenceTimeFrom", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timeTo", + "columnName": "teacherAbsenceTimeTo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "teacherAbsenceId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_teacherAbsence_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_teacherAbsence_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "teacherAbsenceTypes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teacherAbsenceTypeId` INTEGER NOT NULL, `teacherAbsenceTypeName` TEXT NOT NULL, PRIMARY KEY(`profileId`, `teacherAbsenceTypeId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "teacherAbsenceTypeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "teacherAbsenceTypeName", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "teacherAbsenceTypeId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "subjects", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `subjectLongName` TEXT, `subjectShortName` TEXT, `subjectColor` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `subjectId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "longName", + "columnName": "subjectLongName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shortName", + "columnName": "subjectShortName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "subjectColor", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "subjectId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notices", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `noticeId` INTEGER NOT NULL, `noticeType` INTEGER NOT NULL, `noticeSemester` INTEGER NOT NULL, `noticeText` TEXT NOT NULL, `noticeCategory` TEXT, `noticePoints` REAL, `teacherId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `noticeId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "noticeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "noticeType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "semester", + "columnName": "noticeSemester", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "noticeText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "noticeCategory", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "points", + "columnName": "noticePoints", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "noticeId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_notices_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notices_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "teams", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `teamId` INTEGER NOT NULL, `teamType` INTEGER NOT NULL, `teamName` TEXT, `teamCode` TEXT, `teamTeacherId` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `teamId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "teamType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "teamName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "code", + "columnName": "teamCode", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teamTeacherId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "teamId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "attendances", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `attendanceId` INTEGER NOT NULL, `attendanceBaseType` INTEGER NOT NULL, `attendanceTypeName` TEXT NOT NULL, `attendanceTypeShort` TEXT NOT NULL, `attendanceTypeSymbol` TEXT NOT NULL, `attendanceTypeColor` INTEGER, `attendanceDate` TEXT NOT NULL, `attendanceTime` TEXT, `attendanceSemester` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `attendanceLessonTopic` TEXT, `attendanceLessonNumber` INTEGER, `attendanceIsCounted` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `attendanceId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "attendanceId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseType", + "columnName": "attendanceBaseType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "typeName", + "columnName": "attendanceTypeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeShort", + "columnName": "attendanceTypeShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeSymbol", + "columnName": "attendanceTypeSymbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeColor", + "columnName": "attendanceTypeColor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "date", + "columnName": "attendanceDate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "startTime", + "columnName": "attendanceTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "semester", + "columnName": "attendanceSemester", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lessonTopic", + "columnName": "attendanceLessonTopic", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lessonNumber", + "columnName": "attendanceLessonNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "isCounted", + "columnName": "attendanceIsCounted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "attendanceId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_attendances_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_attendances_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `eventId` INTEGER NOT NULL, `eventDate` TEXT NOT NULL, `eventTime` TEXT, `eventTopic` TEXT NOT NULL, `eventColor` INTEGER, `eventType` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `teamId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `eventAddedManually` INTEGER NOT NULL, `eventSharedBy` TEXT, `eventSharedByName` TEXT, `eventBlacklisted` INTEGER NOT NULL, `eventIsDone` INTEGER NOT NULL, `eventIsDownloaded` INTEGER NOT NULL, `homeworkBody` TEXT, `attachmentIds` TEXT, `attachmentNames` TEXT, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `eventId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "eventId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "eventDate", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "time", + "columnName": "eventTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "topic", + "columnName": "eventTopic", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "eventColor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "eventType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teamId", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedManually", + "columnName": "eventAddedManually", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sharedBy", + "columnName": "eventSharedBy", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedByName", + "columnName": "eventSharedByName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "blacklisted", + "columnName": "eventBlacklisted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDone", + "columnName": "eventIsDone", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isDownloaded", + "columnName": "eventIsDownloaded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "homeworkBody", + "columnName": "homeworkBody", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentIds", + "columnName": "attachmentIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentNames", + "columnName": "attachmentNames", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "eventId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_events_profileId_eventDate_eventTime", + "unique": false, + "columnNames": [ + "profileId", + "eventDate", + "eventTime" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_events_profileId_eventDate_eventTime` ON `${TABLE_NAME}` (`profileId`, `eventDate`, `eventTime`)" + }, + { + "name": "index_events_profileId_eventType", + "unique": false, + "columnNames": [ + "profileId", + "eventType" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_events_profileId_eventType` ON `${TABLE_NAME}` (`profileId`, `eventType`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "eventTypes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `eventType` INTEGER NOT NULL, `eventTypeName` TEXT NOT NULL, `eventTypeColor` INTEGER NOT NULL, `eventTypeOrder` INTEGER NOT NULL, `eventTypeSource` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `eventType`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "eventType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "eventTypeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "eventTypeColor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "eventTypeOrder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "source", + "columnName": "eventTypeSource", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "eventType" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "loginStores", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`loginStoreId` INTEGER NOT NULL, `loginStoreType` INTEGER NOT NULL, `loginStoreMode` INTEGER NOT NULL, `loginStoreData` TEXT NOT NULL, PRIMARY KEY(`loginStoreId`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "loginStoreId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "loginStoreType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mode", + "columnName": "loginStoreMode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "loginStoreData", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "loginStoreId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "profiles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `loginStoreId` INTEGER NOT NULL, `loginStoreType` INTEGER NOT NULL, `name` TEXT NOT NULL, `subname` TEXT, `studentNameLong` TEXT NOT NULL, `studentNameShort` TEXT NOT NULL, `accountName` TEXT, `studentData` TEXT NOT NULL, `image` TEXT, `empty` INTEGER NOT NULL, `archived` INTEGER NOT NULL, `archiveId` INTEGER, `syncEnabled` INTEGER NOT NULL, `enableSharedEvents` INTEGER NOT NULL, `registration` INTEGER NOT NULL, `userCode` TEXT NOT NULL, `studentNumber` INTEGER NOT NULL, `studentClassName` TEXT, `studentSchoolYearStart` INTEGER NOT NULL, `dateSemester1Start` TEXT NOT NULL, `dateSemester2Start` TEXT NOT NULL, `dateYearEnd` TEXT NOT NULL, `disabledNotifications` TEXT, `lastReceiversSync` INTEGER NOT NULL, PRIMARY KEY(`profileId`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loginStoreId", + "columnName": "loginStoreId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "loginStoreType", + "columnName": "loginStoreType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subname", + "columnName": "subname", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "studentNameLong", + "columnName": "studentNameLong", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentNameShort", + "columnName": "studentNameShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "studentData", + "columnName": "studentData", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "empty", + "columnName": "empty", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "archived", + "columnName": "archived", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "archiveId", + "columnName": "archiveId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "syncEnabled", + "columnName": "syncEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enableSharedEvents", + "columnName": "enableSharedEvents", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "registration", + "columnName": "registration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "userCode", + "columnName": "userCode", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "studentNumber", + "columnName": "studentNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "studentClassName", + "columnName": "studentClassName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "studentSchoolYearStart", + "columnName": "studentSchoolYearStart", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateSemester1Start", + "columnName": "dateSemester1Start", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateSemester2Start", + "columnName": "dateSemester2Start", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateYearEnd", + "columnName": "dateYearEnd", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "disabledNotifications", + "columnName": "disabledNotifications", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastReceiversSync", + "columnName": "lastReceiversSync", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "luckyNumbers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `luckyNumberDate` INTEGER NOT NULL, `luckyNumber` INTEGER NOT NULL, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `luckyNumberDate`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "luckyNumberDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "luckyNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "luckyNumberDate" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "announcements", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `announcementId` INTEGER NOT NULL, `announcementSubject` TEXT NOT NULL, `announcementText` TEXT, `announcementStartDate` TEXT, `announcementEndDate` TEXT, `teacherId` INTEGER NOT NULL, `addedDate` INTEGER NOT NULL, `announcementIdString` TEXT, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `announcementId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "announcementId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "announcementSubject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "announcementText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "startDate", + "columnName": "announcementStartDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endDate", + "columnName": "announcementEndDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "idString", + "columnName": "announcementIdString", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "announcementId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_announcements_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_announcements_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "gradeCategories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `categoryId` INTEGER NOT NULL, `weight` REAL NOT NULL, `color` INTEGER NOT NULL, `text` TEXT, `columns` TEXT, `valueFrom` REAL NOT NULL, `valueTo` REAL NOT NULL, `type` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `categoryId`, `type`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "categoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "columns", + "columnName": "columns", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "valueFrom", + "columnName": "valueFrom", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "valueTo", + "columnName": "valueTo", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "categoryId", + "type" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "feedbackMessages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`messageId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `received` INTEGER NOT NULL, `text` TEXT NOT NULL, `senderName` TEXT NOT NULL, `deviceId` TEXT, `deviceName` TEXT, `devId` INTEGER, `devImage` TEXT, `sentTime` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "messageId", + "columnName": "messageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "received", + "columnName": "received", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "senderName", + "columnName": "senderName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "deviceId", + "columnName": "deviceId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deviceName", + "columnName": "deviceName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "devId", + "columnName": "devId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "devImage", + "columnName": "devImage", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sentTime", + "columnName": "sentTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "messageId" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "messages", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `messageId` INTEGER NOT NULL, `messageType` INTEGER NOT NULL, `messageSubject` TEXT NOT NULL, `messageBody` TEXT, `senderId` INTEGER, `addedDate` INTEGER NOT NULL, `messageIsPinned` INTEGER NOT NULL, `hasAttachments` INTEGER NOT NULL, `attachmentIds` TEXT, `attachmentNames` TEXT, `attachmentSizes` TEXT, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `messageId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "messageId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "messageType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "messageSubject", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "body", + "columnName": "messageBody", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "senderId", + "columnName": "senderId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isStarred", + "columnName": "messageIsPinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hasAttachments", + "columnName": "hasAttachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachmentIds", + "columnName": "attachmentIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentNames", + "columnName": "attachmentNames", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "attachmentSizes", + "columnName": "attachmentSizes", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "messageId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_messages_profileId_messageType", + "unique": false, + "columnNames": [ + "profileId", + "messageType" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_messages_profileId_messageType` ON `${TABLE_NAME}` (`profileId`, `messageType`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "messageRecipients", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `messageRecipientId` INTEGER NOT NULL, `messageRecipientReplyId` INTEGER NOT NULL, `messageRecipientReadDate` INTEGER NOT NULL, `messageId` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `messageRecipientId`, `messageId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "messageRecipientId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "replyId", + "columnName": "messageRecipientReplyId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "readDate", + "columnName": "messageRecipientReadDate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "messageId", + "columnName": "messageId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "messageRecipientId", + "messageId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "debugLogs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "endpointTimers", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `endpointId` INTEGER NOT NULL, `endpointLastSync` INTEGER, `endpointNextSync` INTEGER NOT NULL, `endpointViewId` INTEGER, PRIMARY KEY(`profileId`, `endpointId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endpointId", + "columnName": "endpointId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastSync", + "columnName": "endpointLastSync", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "nextSync", + "columnName": "endpointNextSync", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viewId", + "columnName": "endpointViewId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "endpointId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "lessonRanges", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `lessonRangeNumber` INTEGER NOT NULL, `lessonRangeStart` TEXT NOT NULL, `lessonRangeEnd` TEXT NOT NULL, PRIMARY KEY(`profileId`, `lessonRangeNumber`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lessonNumber", + "columnName": "lessonRangeNumber", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startTime", + "columnName": "lessonRangeStart", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "endTime", + "columnName": "lessonRangeEnd", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "lessonRangeNumber" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "notifications", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `text` TEXT NOT NULL, `textLong` TEXT, `type` INTEGER NOT NULL, `profileId` INTEGER, `profileName` TEXT, `posted` INTEGER NOT NULL, `viewId` INTEGER, `extras` TEXT, `addedDate` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "textLong", + "columnName": "textLong", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "profileName", + "columnName": "profileName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "posted", + "columnName": "posted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "viewId", + "columnName": "viewId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "extras", + "columnName": "extras", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "classrooms", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`profileId`, `id`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "noticeTypes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `name` TEXT NOT NULL, PRIMARY KEY(`profileId`, `id`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "attendanceTypes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `baseType` INTEGER NOT NULL, `typeName` TEXT NOT NULL, `typeShort` TEXT NOT NULL, `typeSymbol` TEXT NOT NULL, `typeColor` INTEGER, PRIMARY KEY(`profileId`, `id`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "baseType", + "columnName": "baseType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "typeName", + "columnName": "typeName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeShort", + "columnName": "typeShort", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeSymbol", + "columnName": "typeSymbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "typeColor", + "columnName": "typeColor", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "timetable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `id` INTEGER NOT NULL, `type` INTEGER NOT NULL, `date` TEXT, `lessonNumber` INTEGER, `startTime` TEXT, `endTime` TEXT, `subjectId` INTEGER, `teacherId` INTEGER, `teamId` INTEGER, `classroom` TEXT, `oldDate` TEXT, `oldLessonNumber` INTEGER, `oldStartTime` TEXT, `oldEndTime` TEXT, `oldSubjectId` INTEGER, `oldTeacherId` INTEGER, `oldTeamId` INTEGER, `oldClassroom` TEXT, `isExtra` INTEGER NOT NULL, `color` INTEGER, `keep` INTEGER NOT NULL, PRIMARY KEY(`profileId`, `id`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lessonNumber", + "columnName": "lessonNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startTime", + "columnName": "startTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endTime", + "columnName": "endTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "teamId", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "classroom", + "columnName": "classroom", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldDate", + "columnName": "oldDate", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldLessonNumber", + "columnName": "oldLessonNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "oldStartTime", + "columnName": "oldStartTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldEndTime", + "columnName": "oldEndTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "oldSubjectId", + "columnName": "oldSubjectId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "oldTeacherId", + "columnName": "oldTeacherId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "oldTeamId", + "columnName": "oldTeamId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "oldClassroom", + "columnName": "oldClassroom", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isExtra", + "columnName": "isExtra", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "keep", + "columnName": "keep", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_timetable_profileId_type_date", + "unique": false, + "columnNames": [ + "profileId", + "type", + "date" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_timetable_profileId_type_date` ON `${TABLE_NAME}` (`profileId`, `type`, `date`)" + }, + { + "name": "index_timetable_profileId_type_oldDate", + "unique": false, + "columnNames": [ + "profileId", + "type", + "oldDate" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_timetable_profileId_type_oldDate` ON `${TABLE_NAME}` (`profileId`, `type`, `oldDate`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "config", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `key` TEXT NOT NULL, `value` TEXT, PRIMARY KEY(`profileId`, `key`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "key" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "librusLessons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `lessonId` INTEGER NOT NULL, `teacherId` INTEGER NOT NULL, `subjectId` INTEGER NOT NULL, `teamId` INTEGER, PRIMARY KEY(`profileId`, `lessonId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lessonId", + "columnName": "lessonId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "teamId", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "profileId", + "lessonId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_librusLessons_profileId", + "unique": false, + "columnNames": [ + "profileId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_librusLessons_profileId` ON `${TABLE_NAME}` (`profileId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "timetableManual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `type` INTEGER NOT NULL, `repeatBy` INTEGER NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `date` INTEGER, `weekDay` INTEGER, `lessonNumber` INTEGER, `startTime` TEXT, `endTime` TEXT, `subjectId` INTEGER, `teacherId` INTEGER, `teamId` INTEGER, `classroom` TEXT)", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repeatBy", + "columnName": "repeatBy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "date", + "columnName": "date", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "weekDay", + "columnName": "weekDay", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lessonNumber", + "columnName": "lessonNumber", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "startTime", + "columnName": "startTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "endTime", + "columnName": "endTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "subjectId", + "columnName": "subjectId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "teacherId", + "columnName": "teacherId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "teamId", + "columnName": "teamId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "classroom", + "columnName": "classroom", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_timetableManual_profileId_date", + "unique": false, + "columnNames": [ + "profileId", + "date" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_timetableManual_profileId_date` ON `${TABLE_NAME}` (`profileId`, `date`)" + }, + { + "name": "index_timetableManual_profileId_weekDay", + "unique": false, + "columnNames": [ + "profileId", + "weekDay" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_timetableManual_profileId_weekDay` ON `${TABLE_NAME}` (`profileId`, `weekDay`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "notes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `noteId` INTEGER NOT NULL, `noteOwnerType` TEXT, `noteOwnerId` INTEGER, `noteReplacesOriginal` INTEGER NOT NULL, `noteTopic` TEXT, `noteBody` TEXT NOT NULL, `noteColor` INTEGER, `noteSharedBy` TEXT, `noteSharedByName` TEXT, `addedDate` INTEGER NOT NULL, PRIMARY KEY(`noteId`))", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "noteId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ownerType", + "columnName": "noteOwnerType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ownerId", + "columnName": "noteOwnerId", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "replacesOriginal", + "columnName": "noteReplacesOriginal", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "topic", + "columnName": "noteTopic", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "body", + "columnName": "noteBody", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "noteColor", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sharedBy", + "columnName": "noteSharedBy", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sharedByName", + "columnName": "noteSharedByName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "addedDate", + "columnName": "addedDate", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "noteId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_notes_profileId_noteOwnerType_noteOwnerId", + "unique": false, + "columnNames": [ + "profileId", + "noteOwnerType", + "noteOwnerId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_notes_profileId_noteOwnerType_noteOwnerId` ON `${TABLE_NAME}` (`profileId`, `noteOwnerType`, `noteOwnerId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`profileId` INTEGER NOT NULL, `metadataId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `thingType` INTEGER NOT NULL, `thingId` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `notified` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "profileId", + "columnName": "profileId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "metadataId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thingType", + "columnName": "thingType", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "thingId", + "columnName": "thingId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "seen", + "columnName": "seen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notified", + "columnName": "notified", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "metadataId" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_metadata_profileId_thingType_thingId", + "unique": true, + "columnNames": [ + "profileId", + "thingType", + "thingId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_metadata_profileId_thingType_thingId` ON `${TABLE_NAME}` (`profileId`, `thingType`, `thingId`)" + } + ], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '2612ebba9802eedc7ebc69724606a23c')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index af4b653a..da531480 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,7 +3,6 @@ xmlns:tools="http://schemas.android.com/tools" package="pl.szczodrzynski.edziennik"> - @@ -13,7 +12,7 @@ - + @@ -43,6 +42,7 @@ android:configChanges="orientation|screenSize" android:label="@string/app_name" android:launchMode="singleTop" + android:exported="true" android:theme="@style/SplashTheme"> @@ -66,6 +66,7 @@ android:configChanges="orientation|keyboardHidden" android:excludeFromRecents="true" android:noHistory="true" + android:exported="true" android:theme="@style/AppTheme.Dark.NoDisplay"> @@ -73,7 +74,8 @@ + android:label="@string/widget_timetable_title" + android:exported="true"> @@ -88,10 +90,12 @@ android:configChanges="orientation|keyboardHidden" android:excludeFromRecents="true" android:noHistory="true" + android:exported="true" android:theme="@style/AppTheme.Dark.NoDisplay" /> + android:label="@string/widget_notifications_title" + android:exported="true"> @@ -104,7 +108,8 @@ android:permission="android.permission.BIND_REMOTEVIEWS" /> + android:label="@string/widget_lucky_number_title" + android:exported="true"> @@ -121,31 +126,47 @@ / ____ \ (__| |_| |\ V /| | |_| | __/\__ \ /_/ \_\___|\__|_| \_/ |_|\__|_|\___||___/ --> - - - - - - - + + + + + android:enabled="true" + android:exported="true"> - + diff --git a/app/src/main/assets/pl-changelog.html b/app/src/main/assets/pl-changelog.html index 2219ad8c..af4aaca4 100644 --- a/app/src/main/assets/pl-changelog.html +++ b/app/src/main/assets/pl-changelog.html @@ -1,15 +1,10 @@ -

Wersja 4.7, 2021-04-07

+

Wersja 4.13.6, 2023-03-24

    -
  • Szkolny.eu jest teraz open source! Zapraszamy na stronę https://szkolny.eu/ po więcej ważnych informacji.
  • -
  • Poprawiono wybieranie obrazków (tła nagłówka, tła aplikacji oraz profilu) z dowolnego źródła.
  • -
  • Ukończono tłumaczenie na język angielski. @MarcinK50
  • -
  • Dodano ekran informacji o kompilacji w Ustawieniach.
  • -
  • Zaktualizowano ekran licencji open source.
  • -
  • Naprawiono zatrzymanie aplikacji na Androidzie 4.4 i starszych.
  • -
  • Naprawiono problemy z połączeniem internetowym na Androidzie 4.4 i starszych.
  • -
  • Zoptymalizowano wielkość aplikacji.
  • +
  • Naprawiono pobieranie załączników na Androidzie 13 i nowszym.
  • +
  • Dodano opcję odświeżenia planu lekcji na wybrany tydzień.
  • +
  • Usunięto błędy logowania. @BxOxSxS


Dzięki za korzystanie ze Szkolnego!
-© [Kuba Szczodrzyński](@kuba2k2), [Kacper Ziubryniewicz](@kapi2289) 2021 +© [Kuba Szczodrzyński](@kuba2k2) 2023 diff --git a/app/src/main/cpp/szkolny-signing.cpp b/app/src/main/cpp/szkolny-signing.cpp index f2b1cf8a..9ae8f745 100644 --- a/app/src/main/cpp/szkolny-signing.cpp +++ b/app/src/main/cpp/szkolny-signing.cpp @@ -9,7 +9,7 @@ /*secret password - removed for source code publication*/ static toys AES_IV[16] = { - 0xda, 0x9f, 0xd4, 0x2b, 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); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt index a9186d94..74e23d88 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt @@ -12,6 +12,7 @@ import android.graphics.drawable.Icon import android.os.Build import android.provider.Settings import android.util.Log +import android.widget.Toast import androidx.appcompat.app.AppCompatDelegate import androidx.multidex.MultiDexApplication import androidx.work.Configuration @@ -26,50 +27,88 @@ import com.google.firebase.messaging.FirebaseMessaging import com.google.gson.Gson import com.hypertrack.hyperlog.HyperLog import com.mikepenz.iconics.Iconics -import eu.szkolny.sslprovider.SSLProvider -import eu.szkolny.sslprovider.enableSupportedTls import im.wangchao.mhttp.MHttp -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import me.leolin.shortcutbadger.ShortcutBadger import okhttp3.OkHttpClient import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.config.AppData import pl.szczodrzynski.edziennik.config.Config import pl.szczodrzynski.edziennik.data.api.events.ProfileListEmptyEvent +import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing import pl.szczodrzynski.edziennik.data.db.AppDb import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.enums.LoginType +import pl.szczodrzynski.edziennik.ext.DAY +import pl.szczodrzynski.edziennik.ext.MS +import pl.szczodrzynski.edziennik.ext.putExtras +import pl.szczodrzynski.edziennik.ext.setLanguage +import pl.szczodrzynski.edziennik.network.SSLProviderInstaller import pl.szczodrzynski.edziennik.network.cookie.DumbCookieJar import pl.szczodrzynski.edziennik.sync.SyncWorker import pl.szczodrzynski.edziennik.sync.UpdateWorker -import pl.szczodrzynski.edziennik.ui.modules.base.CrashActivity -import pl.szczodrzynski.edziennik.utils.* +import pl.szczodrzynski.edziennik.ui.base.CrashActivity +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget +import pl.szczodrzynski.edziennik.utils.DebugLogFormat +import pl.szczodrzynski.edziennik.utils.PermissionChecker +import pl.szczodrzynski.edziennik.utils.Themes +import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.Utils.d -import pl.szczodrzynski.edziennik.utils.managers.* -import timber.log.Timber +import pl.szczodrzynski.edziennik.utils.managers.AttendanceManager +import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager +import pl.szczodrzynski.edziennik.utils.managers.BuildManager +import pl.szczodrzynski.edziennik.utils.managers.EventManager +import pl.szczodrzynski.edziennik.utils.managers.GradesManager +import pl.szczodrzynski.edziennik.utils.managers.MessageManager +import pl.szczodrzynski.edziennik.utils.managers.NoteManager +import pl.szczodrzynski.edziennik.utils.managers.NotificationChannelsManager +import pl.szczodrzynski.edziennik.utils.managers.PermissionManager +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager +import pl.szczodrzynski.edziennik.utils.managers.TimetableManager +import pl.szczodrzynski.edziennik.utils.managers.UpdateManager +import pl.szczodrzynski.edziennik.utils.managers.UserActionManager import java.util.concurrent.TimeUnit import kotlin.coroutines.CoroutineContext +import kotlin.system.exitProcess class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { companion object { @Volatile lateinit var db: AppDb + private set lateinit var config: Config + // private set // for LabFragment lateinit var profile: Profile + private set + lateinit var data: AppData + private set val profileId get() = profile.id + var enableChucker = false var debugMode = false var devMode = false } - val notificationChannelsManager by lazy { NotificationChannelsManager(this) } - val userActionManager by lazy { UserActionManager(this) } - val gradesManager by lazy { GradesManager(this) } - val timetableManager by lazy { TimetableManager(this) } - val eventManager by lazy { EventManager(this) } - val permissionManager by lazy { PermissionManager(this) } + val api by lazy { SzkolnyApi(this) } val attendanceManager by lazy { AttendanceManager(this) } + val availabilityManager by lazy { AvailabilityManager(this) } val buildManager by lazy { BuildManager(this) } + val eventManager by lazy { EventManager(this) } + val gradesManager by lazy { GradesManager(this) } + val messageManager by lazy { MessageManager(this) } + val noteManager by lazy { NoteManager(this) } + val notificationChannelsManager by lazy { NotificationChannelsManager(this) } + val permissionManager by lazy { PermissionManager(this) } + val textStylingManager by lazy { TextStylingManager(this) } + val timetableManager by lazy { TimetableManager(this) } + val updateManager by lazy { UpdateManager(this) } + val userActionManager by lazy { UserActionManager(this) } val db get() = App.db @@ -79,6 +118,8 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { get() = App.profile val profileId get() = App.profileId + val data + get() = App.data private val job = Job() override val coroutineContext: CoroutineContext @@ -109,15 +150,15 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { .connectTimeout(15, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) - .enableSupportedTls(enableCleartext = true) + + SSLProviderInstaller.enableSupportedTls(builder, enableCleartext = true) if (devMode) { - HyperLog.initialize(this) - HyperLog.setLogLevel(Log.VERBOSE) - HyperLog.setLogFormat(DebugLogFormat(this)) - val chuckerCollector = ChuckerCollector(this, true, RetentionManager.Period.ONE_HOUR) - val chuckerInterceptor = ChuckerInterceptor(this, chuckerCollector) - builder.addInterceptor(chuckerInterceptor) + if (enableChucker) { + val chuckerCollector = ChuckerCollector(this, true, RetentionManager.Period.ONE_HOUR) + val chuckerInterceptor = ChuckerInterceptor(this, chuckerCollector) + builder.addInterceptor(chuckerInterceptor) + } } http = builder.build() @@ -167,14 +208,23 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { Iconics.respectFontBoundsDefault = true // initialize companion object values + AppData.read(this) App.db = AppDb(this) App.config = Config(App.db) - App.profile = Profile(0, 0, 0, "") debugMode = BuildConfig.DEBUG - devMode = config.debugMode || debugMode + devMode = config.devMode ?: debugMode + enableChucker = config.enableChucker ?: devMode + + if (devMode) { + HyperLog.initialize(this) + HyperLog.setLogLevel(Log.VERBOSE) + HyperLog.setLogFormat(DebugLogFormat(this)) + } if (!profileLoadById(config.lastProfileId)) { - db.profileDao().firstId?.let { profileLoadById(it) } + val success = db.profileDao().firstId?.let { profileLoadById(it) } + if (success != true) + profileLoad(Profile(0, 0, LoginType.TEMPLATE, "")) } buildHttp() @@ -185,23 +235,13 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { } Signing.getCert(this) + Utils.initializeStorageDir(this) launch { withContext(Dispatchers.Default) { config.migrate(this@App) - SSLProvider.install( - applicationContext, - downloadIfNeeded = true, - supportTls13 = false, - onFinish = { - buildHttp() - }, - onError = { - Timber.e("Failed to install SSLProvider: $it") - it.printStackTrace() - } - ) + SSLProviderInstaller.install(applicationContext, this@App::buildHttp) if (config.devModePassword != null) checkDevModePassword() @@ -223,35 +263,35 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { .setShortLabel(getString(R.string.shortcut_timetable)).setLongLabel(getString(R.string.shortcut_timetable)) .setIcon(Icon.createWithResource(this@App, R.mipmap.ic_shortcut_timetable)) .setIntent(Intent(Intent.ACTION_MAIN, null, this@App, MainActivity::class.java) - .putExtra("fragmentId", MainActivity.DRAWER_ITEM_TIMETABLE)) + .putExtras("fragmentId" to NavTarget.TIMETABLE)) .build() val shortcutAgenda = ShortcutInfo.Builder(this@App, "item_agenda") .setShortLabel(getString(R.string.shortcut_agenda)).setLongLabel(getString(R.string.shortcut_agenda)) .setIcon(Icon.createWithResource(this@App, R.mipmap.ic_shortcut_agenda)) .setIntent(Intent(Intent.ACTION_MAIN, null, this@App, MainActivity::class.java) - .putExtra("fragmentId", MainActivity.DRAWER_ITEM_AGENDA)) + .putExtras("fragmentId" to NavTarget.AGENDA)) .build() val shortcutGrades = ShortcutInfo.Builder(this@App, "item_grades") .setShortLabel(getString(R.string.shortcut_grades)).setLongLabel(getString(R.string.shortcut_grades)) .setIcon(Icon.createWithResource(this@App, R.mipmap.ic_shortcut_grades)) .setIntent(Intent(Intent.ACTION_MAIN, null, this@App, MainActivity::class.java) - .putExtra("fragmentId", MainActivity.DRAWER_ITEM_GRADES)) + .putExtras("fragmentId" to NavTarget.GRADES)) .build() val shortcutHomework = ShortcutInfo.Builder(this@App, "item_homeworks") .setShortLabel(getString(R.string.shortcut_homework)).setLongLabel(getString(R.string.shortcut_homework)) .setIcon(Icon.createWithResource(this@App, R.mipmap.ic_shortcut_homework)) .setIntent(Intent(Intent.ACTION_MAIN, null, this@App, MainActivity::class.java) - .putExtra("fragmentId", MainActivity.DRAWER_ITEM_HOMEWORK)) + .putExtras("fragmentId" to NavTarget.HOMEWORK)) .build() val shortcutMessages = ShortcutInfo.Builder(this@App, "item_messages") .setShortLabel(getString(R.string.shortcut_messages)).setLongLabel(getString(R.string.shortcut_messages)) .setIcon(Icon.createWithResource(this@App, R.mipmap.ic_shortcut_messages)) .setIntent(Intent(Intent.ACTION_MAIN, null, this@App, MainActivity::class.java) - .putExtra("fragmentId", MainActivity.DRAWER_ITEM_MESSAGES)) + .putExtras("fragmentId" to NavTarget.MESSAGES)) .build() shortcutManager.dynamicShortcuts = listOf( @@ -377,10 +417,28 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { } } + fun profileLoad(profile: Profile) { + App.profile = profile + App.config.lastProfileId = profile.id + 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() + exitProcess(0) + } + } + private fun profileLoadById(profileId: Int): Boolean { db.profileDao().getByIdNow(profileId)?.also { - App.profile = it - App.config.lastProfileId = it.id + profileLoad(it) return true } return false @@ -411,6 +469,8 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { } fun profileSave() = profileSave(profile) fun profileSave(profile: Profile) { + if (profile.id == profileId) + App.profile = profile launch(Dispatchers.Default) { App.db.profileDao().add(profile) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Binding.kt b/app/src/main/java/pl/szczodrzynski/edziennik/Binding.kt index 3dc9d0a6..fba06502 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/Binding.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/Binding.kt @@ -4,8 +4,11 @@ package pl.szczodrzynski.edziennik import android.graphics.Paint +import android.view.View import android.widget.TextView +import androidx.core.view.isVisible import androidx.databinding.BindingAdapter +import pl.szczodrzynski.edziennik.ext.dp object Binding { @JvmStatic @@ -17,4 +20,64 @@ object Binding { textView.paintFlags = textView.paintFlags and Paint.STRIKE_THRU_TEXT_FLAG.inv() } } + + @JvmStatic + @BindingAdapter("android:isVisible") + fun isVisible(view: View, isVisible: Boolean) { + view.isVisible = isVisible + } + + private fun resizeDrawable(textView: TextView, index: Int, size: Int) { + val drawables = textView.compoundDrawables + drawables[index]?.setBounds(0, 0, size, size) + textView.setCompoundDrawables(drawables[0], drawables[1], drawables[2], drawables[3]) + } + + @JvmStatic + @BindingAdapter("android:drawableLeftAutoSize") + fun drawableLeftAutoSize(textView: TextView, enable: Boolean) = resizeDrawable( + textView, + index = 0, + size = textView.textSize.toInt(), + ) + + @JvmStatic + @BindingAdapter("android:drawableRightAutoSize") + fun drawableRightAutoSize(textView: TextView, enable: Boolean) = resizeDrawable( + textView, + index = 2, + size = textView.textSize.toInt(), + ) + + @JvmStatic + @BindingAdapter("android:drawableLeftSize") + fun drawableLeftSize(textView: TextView, sizeDp: Int) = resizeDrawable( + textView, + index = 0, + size = sizeDp.dp, + ) + + @JvmStatic + @BindingAdapter("android:drawableTopSize") + fun drawableTopSize(textView: TextView, sizeDp: Int) = resizeDrawable( + textView, + index = 1, + size = sizeDp.dp, + ) + + @JvmStatic + @BindingAdapter("android:drawableRightSize") + fun drawableRightSize(textView: TextView, sizeDp: Int) = resizeDrawable( + textView, + index = 2, + size = sizeDp.dp, + ) + + @JvmStatic + @BindingAdapter("android:drawableBottomSize") + fun drawableBottomSize(textView: TextView, sizeDp: Int) = resizeDrawable( + textView, + index = 3, + size = sizeDp.dp, + ) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt deleted file mode 100644 index a07634a1..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/Extensions.kt +++ /dev/null @@ -1,1289 +0,0 @@ -package pl.szczodrzynski.edziennik - -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent -import android.content.res.ColorStateList -import android.content.res.Resources -import android.database.Cursor -import android.graphics.PorterDuff -import android.graphics.PorterDuffColorFilter -import android.graphics.Rect -import android.graphics.Typeface -import android.graphics.drawable.Drawable -import android.os.Build -import android.os.Bundle -import android.text.* -import android.text.style.ForegroundColorSpan -import android.text.style.StrikethroughSpan -import android.text.style.StyleSpan -import android.util.* -import android.util.Base64 -import android.util.Base64.NO_WRAP -import android.util.Base64.encodeToString -import android.view.View -import android.view.WindowManager -import android.widget.* -import androidx.annotation.* -import androidx.core.database.getIntOrNull -import androidx.core.database.getLongOrNull -import androidx.core.database.getStringOrNull -import androidx.core.util.forEach -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.LiveData -import androidx.lifecycle.Observer -import androidx.recyclerview.widget.RecyclerView -import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import androidx.viewpager.widget.ViewPager -import com.google.android.material.button.MaterialButton -import com.google.android.material.datepicker.CalendarConstraints -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.gson.* -import com.google.gson.JsonArray -import com.google.gson.JsonObject -import im.wangchao.mhttp.Response -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import okhttp3.RequestBody -import okio.Buffer -import pl.szczodrzynski.edziennik.data.api.* -import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApiException -import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse -import pl.szczodrzynski.edziennik.data.db.entity.Notification -import pl.szczodrzynski.edziennik.data.db.entity.Profile -import pl.szczodrzynski.edziennik.data.db.entity.Teacher -import pl.szczodrzynski.edziennik.data.db.entity.Team -import pl.szczodrzynski.edziennik.utils.models.Time -import java.io.InterruptedIOException -import java.io.PrintWriter -import java.io.StringWriter -import java.math.BigInteger -import java.net.ConnectException -import java.net.SocketTimeoutException -import java.net.UnknownHostException -import java.nio.charset.Charset -import java.security.MessageDigest -import java.text.SimpleDateFormat -import java.util.* -import java.util.zip.CRC32 -import javax.crypto.Mac -import javax.crypto.spec.SecretKeySpec -import javax.net.ssl.SSLException -import kotlin.Pair - - -fun List.byId(id: Long) = firstOrNull { it.id == id } -fun List.byNameFirstLast(nameFirstLast: String) = firstOrNull { it.name + " " + it.surname == nameFirstLast } -fun List.byNameLastFirst(nameLastFirst: String) = firstOrNull { it.surname + " " + it.name == nameLastFirst } -fun List.byNameFDotLast(nameFDotLast: String) = firstOrNull { it.name + "." + it.surname == nameFDotLast } -fun List.byNameFDotSpaceLast(nameFDotSpaceLast: String) = firstOrNull { it.name + ". " + it.surname == nameFDotSpaceLast } - -fun JsonObject?.get(key: String): JsonElement? = this?.get(key) - -fun JsonObject?.getBoolean(key: String): Boolean? = get(key)?.let { if (!it.isJsonPrimitive) null else it.asBoolean } -fun JsonObject?.getString(key: String): String? = get(key)?.let { if (!it.isJsonPrimitive) null else it.asString } -fun JsonObject?.getInt(key: String): Int? = get(key)?.let { if (!it.isJsonPrimitive) null else it.asInt } -fun JsonObject?.getLong(key: String): Long? = get(key)?.let { if (!it.isJsonPrimitive) null else it.asLong } -fun JsonObject?.getFloat(key: String): Float? = get(key)?.let { if(!it.isJsonPrimitive) null else it.asFloat } -fun JsonObject?.getChar(key: String): Char? = get(key)?.let { if(!it.isJsonPrimitive) null else it.asCharacter } -fun JsonObject?.getJsonObject(key: String): JsonObject? = get(key)?.let { if (it.isJsonObject) it.asJsonObject else null } -fun JsonObject?.getJsonArray(key: String): JsonArray? = get(key)?.let { if (it.isJsonArray) it.asJsonArray else null } - -fun JsonObject?.getBoolean(key: String, defaultValue: Boolean): Boolean = get(key)?.let { if (!it.isJsonPrimitive) defaultValue else it.asBoolean } ?: defaultValue -fun JsonObject?.getString(key: String, defaultValue: String): String = get(key)?.let { if (!it.isJsonPrimitive) defaultValue else it.asString } ?: defaultValue -fun JsonObject?.getInt(key: String, defaultValue: Int): Int = get(key)?.let { if (!it.isJsonPrimitive) defaultValue else it.asInt } ?: defaultValue -fun JsonObject?.getLong(key: String, defaultValue: Long): Long = get(key)?.let { if (!it.isJsonPrimitive) defaultValue else it.asLong } ?: defaultValue -fun JsonObject?.getFloat(key: String, defaultValue: Float): Float = get(key)?.let { if(!it.isJsonPrimitive) defaultValue else it.asFloat } ?: defaultValue -fun JsonObject?.getChar(key: String, defaultValue: Char): Char = get(key)?.let { if(!it.isJsonPrimitive) defaultValue else it.asCharacter } ?: defaultValue -fun JsonObject?.getJsonObject(key: String, defaultValue: JsonObject): JsonObject = get(key)?.let { if (it.isJsonObject) it.asJsonObject else defaultValue } ?: defaultValue -fun JsonObject?.getJsonArray(key: String, defaultValue: JsonArray): JsonArray = get(key)?.let { if (it.isJsonArray) it.asJsonArray else defaultValue } ?: defaultValue - -fun JsonArray.getBoolean(key: Int): Boolean? = if (key >= size()) null else get(key)?.let { if (!it.isJsonPrimitive) null else it.asBoolean } -fun JsonArray.getString(key: Int): String? = if (key >= size()) null else get(key)?.let { if (!it.isJsonPrimitive) null else it.asString } -fun JsonArray.getInt(key: Int): Int? = if (key >= size()) null else get(key)?.let { if (!it.isJsonPrimitive) null else it.asInt } -fun JsonArray.getLong(key: Int): Long? = if (key >= size()) null else get(key)?.let { if (!it.isJsonPrimitive) null else it.asLong } -fun JsonArray.getFloat(key: Int): Float? = if (key >= size()) null else get(key)?.let { if(!it.isJsonPrimitive) null else it.asFloat } -fun JsonArray.getChar(key: Int): Char? = if (key >= size()) null else get(key)?.let { if(!it.isJsonPrimitive) null else it.asCharacter } -fun JsonArray.getJsonObject(key: Int): JsonObject? = if (key >= size()) null else get(key)?.let { if (it.isJsonObject) it.asJsonObject else null } -fun JsonArray.getJsonArray(key: Int): JsonArray? = if (key >= size()) null else get(key)?.let { if (it.isJsonArray) it.asJsonArray else null } - -fun String.toJsonObject(): JsonObject? = try { JsonParser().parse(this).asJsonObject } catch (ignore: Exception) { null } - -operator fun JsonObject.set(key: String, value: JsonElement) = this.add(key, value) -operator fun JsonObject.set(key: String, value: Boolean) = this.addProperty(key, value) -operator fun JsonObject.set(key: String, value: String?) = this.addProperty(key, value) -operator fun JsonObject.set(key: String, value: Number) = this.addProperty(key, value) -operator fun JsonObject.set(key: String, value: Char) = this.addProperty(key, value) - -operator fun Profile.set(key: String, value: JsonElement) = this.studentData.add(key, value) -operator fun Profile.set(key: String, value: Boolean) = this.studentData.addProperty(key, value) -operator fun Profile.set(key: String, value: String?) = this.studentData.addProperty(key, value) -operator fun Profile.set(key: String, value: Number) = this.studentData.addProperty(key, value) -operator fun Profile.set(key: String, value: Char) = this.studentData.addProperty(key, value) - -fun JsonArray.asJsonObjectList() = this.mapNotNull { it.asJsonObject } - -fun CharSequence?.isNotNullNorEmpty(): Boolean { - return this != null && this.isNotEmpty() -} - -fun Collection?.isNotNullNorEmpty(): Boolean { - return this != null && this.isNotEmpty() -} - -fun CharSequence?.isNotNullNorBlank(): Boolean { - return this != null && this.isNotBlank() -} - -fun currentTimeUnix() = System.currentTimeMillis() / 1000 - -fun Bundle?.getInt(key: String, defaultValue: Int): Int { - return this?.getInt(key, defaultValue) ?: defaultValue -} -fun Bundle?.getLong(key: String, defaultValue: Long): Long { - return this?.getLong(key, defaultValue) ?: defaultValue -} -fun Bundle?.getFloat(key: String, defaultValue: Float): Float { - return this?.getFloat(key, defaultValue) ?: defaultValue -} -fun Bundle?.getString(key: String, defaultValue: String): String { - return this?.getString(key, defaultValue) ?: defaultValue -} - -fun Bundle?.getIntOrNull(key: String): Int? { - return this?.get(key) as? Int -} -fun Bundle?.get(key: String): T? { - return this?.get(key) as? T? -} - -/** - * ` The quick BROWN_fox Jumps OveR THE LAZy-DOG. ` - * - * converts to - * - * `The Quick Brown_fox Jumps Over The Lazy-Dog.` - */ -fun String?.fixName(): String { - return this?.fixWhiteSpaces()?.toProperCase() ?: "" -} - -/** - * `The quick BROWN_fox Jumps OveR THE LAZy-DOG.` - * - * converts to - * - * `The Quick Brown_fox Jumps Over The Lazy-Dog.` - */ -fun String.toProperCase(): String = changeStringCase(this) - -/** - * `John Smith` -> `Smith John` - * - * `JOHN SMith` -> `SMith JOHN` - */ -fun String.swapFirstLastName(): String { - return this.split(" ").let { - if (it.size > 1) - it[1]+" "+it[0] - else - it[0] - } -} - -fun String.splitName(): Pair? { - return this.split(" ").let { - if (it.size >= 2) Pair(it[0], it[1]) - else null - } -} - -fun changeStringCase(s: String): String { - val delimiters = " '-/" - val sb = StringBuilder() - var capNext = true - for (ch in s.toCharArray()) { - var c = ch - c = if (capNext) - Character.toUpperCase(c) - else - Character.toLowerCase(c) - sb.append(c) - capNext = delimiters.indexOf(c) >= 0 - } - return sb.toString() -} - -fun buildFullName(firstName: String?, lastName: String?): String { - return "$firstName $lastName".fixName() -} - -fun String.getShortName(): String { - return split(" ").let { - if (it.size > 1) - "${it[0]} ${it[1][0]}." - else - it[0] - } -} - -/** - * "John Smith" -> "JS" - * - * "JOHN SMith" -> "JS" - * - * "John" -> "J" - * - * "John " -> "J" - * - * "John Smith " -> "JS" - * - * " " -> "" - * - * " " -> "" - */ -fun String?.getNameInitials(): String { - if (this.isNullOrBlank()) return "" - return this.toUpperCase().fixWhiteSpaces().split(" ").take(2).map { it[0] }.joinToString("") -} - -fun List.join(delimiter: String): String { - return concat(delimiter).toString() -} - -fun colorFromName(name: String?): Int { - val i = (name ?: "").crc32() - return when ((i / 10 % 16 + 1).toInt()) { - 13 -> 0xffF44336 - 4 -> 0xffF50057 - 2 -> 0xffD500F9 - 9 -> 0xff6200EA - 5 -> 0xffFFAB00 - 1 -> 0xff304FFE - 6 -> 0xff40C4FF - 14 -> 0xff26A69A - 15 -> 0xff00C853 - 7 -> 0xffFFD600 - 3 -> 0xffFF3D00 - 8 -> 0xffDD2C00 - 10 -> 0xff795548 - 12 -> 0xff2979FF - 11 -> 0xffFF6D00 - else -> 0xff64DD17 - }.toInt() -} - -fun colorFromCssName(name: String): Int { - return when (name) { - "red" -> 0xffff0000 - "green" -> 0xff008000 - "blue" -> 0xff0000ff - "violet" -> 0xffee82ee - "brown" -> 0xffa52a2a - "orange" -> 0xffffa500 - "black" -> 0xff000000 - "white" -> 0xffffffff - else -> -1L - }.toInt() -} - -fun List.filterOutArchived() = this.filter { !it.archived } - -fun Response?.getUnixDate(): Long { - val rfcDate = this?.headers()?.get("date") ?: return currentTimeUnix() - val pattern = "EEE, dd MMM yyyy HH:mm:ss Z" - val format = SimpleDateFormat(pattern, Locale.ENGLISH) - return format.parse(rfcDate).time / 1000 -} - -const val MINUTE = 60L -const val HOUR = 60L*MINUTE -const val DAY = 24L*HOUR -const val WEEK = 7L*DAY -const val MONTH = 30L*DAY -const val YEAR = 365L*DAY -const val MS = 1000L - -fun LongSparseArray.values(): List { - val result = mutableListOf() - forEach { _, value -> - result += value - } - return result -} - -fun SparseArray<*>.keys(): List { - val result = mutableListOf() - forEach { key, _ -> - result += key - } - return result -} -fun SparseArray.values(): List { - val result = mutableListOf() - forEach { _, value -> - result += value - } - return result -} - -fun SparseIntArray.keys(): List { - val result = mutableListOf() - forEach { key, _ -> - result += key - } - return result -} -fun SparseIntArray.values(): List { - val result = mutableListOf() - forEach { _, value -> - result += value - } - return result -} - -fun List>.keys(): List { - val result = mutableListOf() - forEach { pair -> - result += pair.first - } - return result -} -fun List>.values(): List { - val result = mutableListOf() - forEach { pair -> - result += pair.second - } - return result -} - -fun List.toSparseArray(destination: SparseArray, key: (T) -> Int) { - forEach { - destination.put(key(it), it) - } -} -fun List.toSparseArray(destination: LongSparseArray, key: (T) -> Long) { - forEach { - destination.put(key(it), it) - } -} - -fun List.toSparseArray(key: (T) -> Int): SparseArray { - val result = SparseArray() - toSparseArray(result, key) - return result -} -fun List.toSparseArray(key: (T) -> Long): LongSparseArray { - val result = LongSparseArray() - toSparseArray(result, key) - return result -} - -fun SparseArray.singleOrNull(predicate: (T) -> Boolean): T? { - forEach { _, value -> - if (predicate(value)) - return value - } - return null -} -fun LongSparseArray.singleOrNull(predicate: (T) -> Boolean): T? { - forEach { _, value -> - if (predicate(value)) - return value - } - return null -} - -fun String.fixWhiteSpaces() = buildString(length) { - var wasWhiteSpace = true - for (c in this@fixWhiteSpaces) { - if (c.isWhitespace()) { - if (!wasWhiteSpace) { - append(c) - wasWhiteSpace = true - } - } else { - append(c) - wasWhiteSpace = false - } - } -}.trimEnd() - -fun List.getById(id: Long): Team? { - return singleOrNull { it.id == id } -} -fun LongSparseArray.getById(id: Long): Team? { - forEach { _, value -> - if (value.id == id) - return value - } - return null -} - -operator fun MatchResult.get(group: Int): String { - if (group >= groupValues.size) - return "" - return groupValues[group] -} - -fun Context.setLanguage(language: String) { - val locale = Locale(language.toLowerCase(Locale.ROOT)) - val configuration = resources.configuration - Locale.setDefault(locale) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { - configuration.setLocale(locale) - } - configuration.locale = locale - resources.updateConfiguration(configuration, resources.displayMetrics) -} - -/* - Code copied from android-28/java.util.Locale.initDefault() - */ -fun initDefaultLocale() { - run { - // user.locale gets priority - /*val languageTag: String? = System.getProperty("user.locale", "") - if (languageTag.isNotNullNorEmpty()) { - return@run Locale(languageTag) - }*/ - - // user.locale is empty - val language: String? = System.getProperty("user.language", "pl") - val region: String? = System.getProperty("user.region") - val country: String? - val variant: String? - // for compatibility, check for old user.region property - if (region != null) { - // region can be of form country, country_variant, or _variant - val i = region.indexOf('_') - if (i >= 0) { - country = region.substring(0, i) - variant = region.substring(i + 1) - } else { - country = region - variant = "" - } - } else { - country = System.getProperty("user.country", "") - variant = System.getProperty("user.variant", "") - } - return@run Locale(language) - }.let { - Locale.setDefault(it) - } -} - -fun String.crc16(): Int { - var crc = 0xFFFF - for (aBuffer in this) { - crc = crc.ushr(8) or (crc shl 8) and 0xffff - crc = crc xor (aBuffer.toInt() and 0xff) // byte to int, trunc sign - crc = crc xor (crc and 0xff shr 4) - crc = crc xor (crc shl 12 and 0xffff) - crc = crc xor (crc and 0xFF shl 5 and 0xffff) - } - crc = crc and 0xffff - return crc + 32768 -} - -fun String.crc32(): Long { - val crc = CRC32() - crc.update(toByteArray()) - return crc.value -} - -fun String.hmacSHA1(password: String): String { - val key = SecretKeySpec(password.toByteArray(), "HmacSHA1") - - val mac = Mac.getInstance("HmacSHA1").apply { - init(key) - update(this@hmacSHA1.toByteArray()) - } - - return encodeToString(mac.doFinal(), NO_WRAP) -} - -fun String.md5(): String { - val md = MessageDigest.getInstance("MD5") - return BigInteger(1, md.digest(toByteArray())).toString(16).padStart(32, '0') -} - -fun String.sha1Hex(): String { - val md = MessageDigest.getInstance("SHA-1") - md.update(toByteArray()) - return md.digest().joinToString("") { "%02x".format(it) } -} - -fun String.sha256(): ByteArray { - val md = MessageDigest.getInstance("SHA-256") - md.update(toByteArray()) - return md.digest() -} - -fun RequestBody.bodyToString(): String { - val buffer = Buffer() - writeTo(buffer) - return buffer.readUtf8() -} - -fun Long.formatDate(format: String = "yyyy-MM-dd HH:mm:ss"): String = SimpleDateFormat(format).format(this) - -fun CharSequence?.asColoredSpannable(colorInt: Int): Spannable { - val spannable = SpannableString(this) - spannable.setSpan(ForegroundColorSpan(colorInt), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - return spannable -} -fun CharSequence?.asStrikethroughSpannable(): Spannable { - val spannable = SpannableString(this) - spannable.setSpan(StrikethroughSpan(), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - return spannable -} -fun CharSequence?.asItalicSpannable(): Spannable { - val spannable = SpannableString(this) - spannable.setSpan(StyleSpan(Typeface.ITALIC), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - return spannable -} -fun CharSequence?.asBoldSpannable(): Spannable { - val spannable = SpannableString(this) - spannable.setSpan(StyleSpan(Typeface.BOLD), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - return spannable -} -fun CharSequence.asSpannable(vararg spans: Any, substring: String? = null, ignoreCase: Boolean = false, ignoreDiacritics: Boolean = false): Spannable { - val spannable = SpannableString(this) - if (substring == null) { - spans.forEach { - spannable.setSpan(it, 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - } - } - else if (substring.isNotEmpty()) { - val string = - if (ignoreDiacritics) - this.cleanDiacritics() - else this - - var index = string.indexOf(substring, ignoreCase = ignoreCase) - .takeIf { it != -1 } ?: indexOf(substring, ignoreCase = ignoreCase) - while (index >= 0) { - spans.forEach { - spannable.setSpan(it, index, index + substring.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - } - index = string.indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase) - .takeIf { it != -1 } ?: indexOf(substring, startIndex = index + 1, ignoreCase = ignoreCase) - } - } - return spannable -} - -fun CharSequence.cleanDiacritics(): String { - val nameClean = StringBuilder() - forEach { - val ch = when (it) { - 'ż' -> 'z' - 'ó' -> 'o' - 'ł' -> 'l' - 'ć' -> 'c' - 'ę' -> 'e' - 'ś' -> 's' - 'ą' -> 'a' - 'ź' -> 'z' - 'ń' -> 'n' - else -> it - } - nameClean.append(ch) - } - return nameClean.toString() -} - -/** - * Returns a new read-only list only of those given elements, that are not empty. - * Applies for CharSequence and descendants. - */ -fun listOfNotEmpty(vararg elements: T): List = elements.filterNot { it.isEmpty() } - -fun List.concat(delimiter: CharSequence? = null): CharSequence { - if (this.isEmpty()) { - return "" - } - - if (this.size == 1) { - return this[0] ?: "" - } - - var spanned = delimiter is Spanned - if (!spanned) { - for (piece in this) { - if (piece is Spanned) { - spanned = true - break - } - } - } - - var first = true - if (spanned) { - val ssb = SpannableStringBuilder() - for (piece in this) { - if (piece == null) - continue - if (!first && delimiter != null) - ssb.append(delimiter) - first = false - ssb.append(piece) - } - return SpannedString(ssb) - } else { - val sb = StringBuilder() - for (piece in this) { - if (piece == null) - continue - if (!first && delimiter != null) - sb.append(delimiter) - first = false - sb.append(piece) - } - return sb.toString() - } -} - -fun TextView.setText(@StringRes resid: Int, vararg formatArgs: Any) { - text = context.getString(resid, *formatArgs) -} - -fun MaterialAlertDialogBuilder.setTitle(@StringRes resid: Int, vararg formatArgs: Any): MaterialAlertDialogBuilder { - setTitle(context.getString(resid, *formatArgs)) - return this -} - -fun MaterialAlertDialogBuilder.setMessage(@StringRes resid: Int, vararg formatArgs: Any): MaterialAlertDialogBuilder { - setMessage(context.getString(resid, *formatArgs)) - return this -} - -fun JsonObject(vararg properties: Pair): JsonObject { - return JsonObject().apply { - for (property in properties) { - when (property.second) { - is JsonElement -> add(property.first, property.second as JsonElement?) - is String -> addProperty(property.first, property.second as String?) - is Char -> addProperty(property.first, property.second as Char?) - is Number -> addProperty(property.first, property.second as Number?) - is Boolean -> addProperty(property.first, property.second as Boolean?) - } - } - } -} - -fun JsonObject.toBundle(): Bundle { - return Bundle().also { - for ((key, value) in this.entrySet()) { - when (value) { - is JsonObject -> it.putBundle(key, value.toBundle()) - is JsonPrimitive -> when { - value.isString -> it.putString(key, value.asString) - value.isBoolean -> it.putBoolean(key, value.asBoolean) - value.isNumber -> it.putInt(key, value.asInt) - } - } - } - } -} - -fun JsonArray(vararg properties: Any?): JsonArray { - return JsonArray().apply { - for (property in properties) { - when (property) { - is JsonElement -> add(property as JsonElement?) - is String -> add(property as String?) - is Char -> add(property as Char?) - is Number -> add(property as Number?) - is Boolean -> add(property as Boolean?) - } - } - } -} - -fun Bundle(vararg properties: Pair): Bundle { - return Bundle().apply { - for (property in properties) { - when (property.second) { - is String -> putString(property.first, property.second as String?) - is Char -> putChar(property.first, property.second as Char) - is Int -> putInt(property.first, property.second as Int) - is Long -> putLong(property.first, property.second as Long) - is Float -> putFloat(property.first, property.second as Float) - is Short -> putShort(property.first, property.second as Short) - is Double -> putDouble(property.first, property.second as Double) - is Boolean -> putBoolean(property.first, property.second as Boolean) - } - } - } -} -fun Intent(action: String? = null, vararg properties: Pair): Intent { - return Intent(action).putExtras(Bundle(*properties)) -} -fun Intent(packageContext: Context, cls: Class<*>, vararg properties: Pair): Intent { - return Intent(packageContext, cls).putExtras(Bundle(*properties)) -} - -fun Bundle.toJsonObject(): JsonObject { - val json = JsonObject() - keySet()?.forEach { key -> - get(key)?.let { - when (it) { - is String -> json.addProperty(key, it) - is Char -> json.addProperty(key, it) - is Int -> json.addProperty(key, it) - is Long -> json.addProperty(key, it) - is Float -> json.addProperty(key, it) - is Short -> json.addProperty(key, it) - is Double -> json.addProperty(key, it) - is Boolean -> json.addProperty(key, it) - is Bundle -> json.add(key, it.toJsonObject()) - else -> json.addProperty(key, it.toString()) - } - } - } - return json -} -fun Intent.toJsonObject() = extras?.toJsonObject() - -fun JsonArray?.isNullOrEmpty(): Boolean = (this?.size() ?: 0) == 0 -fun JsonArray.isEmpty(): Boolean = this.size() == 0 -operator fun JsonArray.plusAssign(o: JsonElement) = this.add(o) -operator fun JsonArray.plusAssign(o: String) = this.add(o) -operator fun JsonArray.plusAssign(o: Char) = this.add(o) -operator fun JsonArray.plusAssign(o: Number) = this.add(o) -operator fun JsonArray.plusAssign(o: Boolean) = this.add(o) - -@Suppress("UNCHECKED_CAST") -inline fun T.onClick(crossinline onClickListener: (v: T) -> Unit) { - setOnClickListener { v: View -> - onClickListener(v as T) - } -} - -@Suppress("UNCHECKED_CAST") -inline fun T.onLongClick(crossinline onLongClickListener: (v: T) -> Boolean) { - setOnLongClickListener { v: View -> - onLongClickListener(v as T) - } -} - -@Suppress("UNCHECKED_CAST") -inline fun T.onChange(crossinline onChangeListener: (v: T, isChecked: Boolean) -> Unit) { - setOnCheckedChangeListener { buttonView, isChecked -> - onChangeListener(buttonView as T, isChecked) - } -} - -@Suppress("UNCHECKED_CAST") -inline fun T.onChange(crossinline onChangeListener: (v: T, isChecked: Boolean) -> Unit) { - clearOnCheckedChangeListeners() - addOnCheckedChangeListener { buttonView, isChecked -> - onChangeListener(buttonView as T, isChecked) - } -} - -fun View.attachToastHint(stringRes: Int) = onLongClick { - Toast.makeText(it.context, stringRes, Toast.LENGTH_SHORT).show() - true -} - -fun LiveData.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer) { - observe(lifecycleOwner, object : Observer { - override fun onChanged(t: T?) { - observer.onChanged(t) - removeObserver(this) - } - }) -} - -/** - * Convert a value in dp to pixels. - */ -val Int.dp: Int - get() = (this * Resources.getSystem().displayMetrics.density).toInt() -/** - * Convert a value in pixels to dp. - */ -val Int.px: Int - get() = (this / Resources.getSystem().displayMetrics.density).toInt() - -@ColorInt -fun @receiver:AttrRes Int.resolveAttr(context: Context?): Int { - val typedValue = TypedValue() - context?.theme?.resolveAttribute(this, typedValue, true) - return typedValue.data -} -@ColorInt -fun @receiver:ColorRes Int.resolveColor(context: Context): Int { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - context.resources.getColor(this, context.theme) - } - else { - context.resources.getColor(this) - } -} -fun @receiver:DrawableRes Int.resolveDrawable(context: Context): Drawable { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - context.resources.getDrawable(this, context.theme) - } - else { - context.resources.getDrawable(this) - } -} - -fun View.findParentById(targetId: Int): View? { - if (id == targetId) { - return this - } - val viewParent = this.parent ?: return null - if (viewParent is View) { - return viewParent.findParentById(targetId) - } - return null -} - -fun CoroutineScope.startCoroutineTimer(delayMillis: Long = 0, repeatMillis: Long = 0, action: suspend CoroutineScope.() -> Unit) = launch { - delay(delayMillis) - if (repeatMillis > 0) { - while (true) { - action() - delay(repeatMillis) - } - } else { - action() - } -} - -operator fun Time?.compareTo(other: Time?): Int { - if (this == null && other == null) - return 0 - if (this == null) - return -1 - if (other == null) - return 1 - return this.compareTo(other) -} - -operator fun StringBuilder.plusAssign(str: String?) { - this.append(str) -} - -fun Context.timeTill(time: Int, delimiter: String = " ", countInSeconds: Boolean = false): String { - val parts = mutableListOf>() - - val hours = time / 3600 - val minutes = (time - hours*3600) / 60 - val seconds = time - minutes*60 - hours*3600 - - if (!countInSeconds) { - var prefixAdded = false - if (hours > 0) { - if (!prefixAdded) parts += R.plurals.time_till_text to hours - prefixAdded = true - parts += R.plurals.time_till_hours to hours - } - if (minutes > 0) { - if (!prefixAdded) parts += R.plurals.time_till_text to minutes - prefixAdded = true - parts += R.plurals.time_till_minutes to minutes - } - if (hours == 0 && minutes < 10) { - if (!prefixAdded) parts += R.plurals.time_till_text to seconds - prefixAdded = true - parts += R.plurals.time_till_seconds to seconds - } - } else { - parts += R.plurals.time_till_text to time - parts += R.plurals.time_till_seconds to time - } - - return parts.joinToString(delimiter) { resources.getQuantityString(it.first, it.second, it.second) } -} - -fun Context.timeLeft(time: Int, delimiter: String = " ", countInSeconds: Boolean = false): String { - val parts = mutableListOf>() - - val hours = time / 3600 - val minutes = (time - hours*3600) / 60 - val seconds = time - minutes*60 - hours*3600 - - if (!countInSeconds) { - var prefixAdded = false - if (hours > 0) { - if (!prefixAdded) parts += R.plurals.time_left_text to hours - prefixAdded = true - parts += R.plurals.time_left_hours to hours - } - if (minutes > 0) { - if (!prefixAdded) parts += R.plurals.time_left_text to minutes - prefixAdded = true - parts += R.plurals.time_left_minutes to minutes - } - if (hours == 0 && minutes < 10) { - if (!prefixAdded) parts += R.plurals.time_left_text to seconds - prefixAdded = true - parts += R.plurals.time_left_seconds to seconds - } - } else { - parts += R.plurals.time_left_text to time - parts += R.plurals.time_left_seconds to time - } - - return parts.joinToString(delimiter) { resources.getQuantityString(it.first, it.second, it.second) } -} - -inline fun Any?.instanceOfOrNull(): T? { - return when (this) { - is T -> this - else -> null - } -} - -fun Drawable.setTintColor(color: Int): Drawable { - colorFilter = PorterDuffColorFilter( - color, - PorterDuff.Mode.SRC_ATOP - ) - return this -} - -inline fun List.ifNotEmpty(block: (List) -> Unit) { - if (!isEmpty()) - block(this) -} - -val String.firstLettersName: String - get() { - var nameShort = "" - this.split(" ").forEach { - if (it.isBlank()) - return@forEach - nameShort += it[0].toLowerCase() - } - return nameShort - } - -val Throwable.stackTraceString: String - get() { - val sw = StringWriter() - printStackTrace(PrintWriter(sw)) - return sw.toString() - } - -inline fun LongSparseArray.filter(predicate: (T) -> Boolean): List { - val destination = ArrayList() - this.forEach { _, element -> if (predicate(element)) destination.add(element) } - return destination -} - -fun CharSequence.replace(oldValue: String, newValue: CharSequence, ignoreCase: Boolean = false): CharSequence = - splitToSequence(oldValue, ignoreCase = ignoreCase).toList().concat(newValue) - -fun Int.toColorStateList(): ColorStateList { - val states = arrayOf( - intArrayOf( android.R.attr.state_enabled ), - intArrayOf(-android.R.attr.state_enabled ), - intArrayOf(-android.R.attr.state_checked ), - intArrayOf( android.R.attr.state_pressed ) - ) - - val colors = intArrayOf( - this, - this, - this, - this - ) - - return ColorStateList(states, colors) -} - -fun SpannableStringBuilder.appendText(text: CharSequence): SpannableStringBuilder { - append(text) - return this -} -fun SpannableStringBuilder.appendSpan(text: CharSequence, what: Any, flags: Int): SpannableStringBuilder { - val start: Int = length - append(text) - setSpan(what, start, length, flags) - return this -} - -fun joinNotNullStrings(delimiter: String = "", vararg parts: String?): String { - var first = true - val sb = StringBuilder() - for (part in parts) { - if (part == null) - continue - if (!first) - sb += delimiter - first = false - sb += part - } - return sb.toString() -} - -fun String.notEmptyOrNull(): String? { - return if (isEmpty()) - null - else - this -} - -fun String.base64Encode(): String { - return encodeToString(toByteArray(), NO_WRAP) -} -fun ByteArray.base64Encode(): String { - return encodeToString(this, NO_WRAP) -} -fun String.base64Decode(): ByteArray { - return Base64.decode(this, Base64.DEFAULT) -} -fun String.base64DecodeToString(): String { - return Base64.decode(this, Base64.DEFAULT).toString(Charset.defaultCharset()) -} - -fun CheckBox.trigger() { isChecked = !isChecked } - -fun Context.plural(@PluralsRes resId: Int, value: Int): String = resources.getQuantityString(resId, value, value) - -fun Context.getNotificationTitle(type: Int): String { - return getString(when (type) { - Notification.TYPE_UPDATE -> R.string.notification_type_update - Notification.TYPE_ERROR -> R.string.notification_type_error - Notification.TYPE_TIMETABLE_CHANGED -> R.string.notification_type_timetable_change - Notification.TYPE_TIMETABLE_LESSON_CHANGE -> R.string.notification_type_timetable_lesson_change - Notification.TYPE_NEW_GRADE -> R.string.notification_type_new_grade - Notification.TYPE_NEW_EVENT -> R.string.notification_type_new_event - Notification.TYPE_NEW_HOMEWORK -> R.string.notification_type_new_homework - Notification.TYPE_NEW_SHARED_EVENT -> R.string.notification_type_new_shared_event - Notification.TYPE_NEW_SHARED_HOMEWORK -> R.string.notification_type_new_shared_homework - Notification.TYPE_REMOVED_SHARED_EVENT -> R.string.notification_type_removed_shared_event - Notification.TYPE_NEW_MESSAGE -> R.string.notification_type_new_message - Notification.TYPE_NEW_NOTICE -> R.string.notification_type_notice - Notification.TYPE_NEW_ATTENDANCE -> R.string.notification_type_attendance - Notification.TYPE_SERVER_MESSAGE -> R.string.notification_type_server_message - Notification.TYPE_LUCKY_NUMBER -> R.string.notification_type_lucky_number - Notification.TYPE_FEEDBACK_MESSAGE -> R.string.notification_type_feedback_message - Notification.TYPE_NEW_ANNOUNCEMENT -> R.string.notification_type_new_announcement - Notification.TYPE_AUTO_ARCHIVING -> R.string.notification_type_auto_archiving - Notification.TYPE_TEACHER_ABSENCE -> R.string.notification_type_new_teacher_absence - Notification.TYPE_GENERAL -> R.string.notification_type_general - else -> R.string.notification_type_general - }) -} - -fun Cursor?.getString(columnName: String) = this?.getStringOrNull(getColumnIndex(columnName)) -fun Cursor?.getInt(columnName: String) = this?.getIntOrNull(getColumnIndex(columnName)) -fun Cursor?.getLong(columnName: String) = this?.getLongOrNull(getColumnIndex(columnName)) - -fun CharSequence.containsAll(list: List, ignoreCase: Boolean = false): Boolean { - for (i in list) { - if (!contains(i, ignoreCase)) - return false - } - return true -} - -inline fun RadioButton.setOnSelectedListener(crossinline listener: (buttonView: CompoundButton) -> Unit) - = setOnCheckedChangeListener { buttonView, isChecked -> if (isChecked) listener(buttonView) } - -fun Response.toErrorCode() = when (this.code()) { - 400 -> ERROR_REQUEST_HTTP_400 - 401 -> ERROR_REQUEST_HTTP_401 - 403 -> ERROR_REQUEST_HTTP_403 - 404 -> ERROR_REQUEST_HTTP_404 - 405 -> ERROR_REQUEST_HTTP_405 - 410 -> ERROR_REQUEST_HTTP_410 - 424 -> ERROR_REQUEST_HTTP_424 - 500 -> ERROR_REQUEST_HTTP_500 - 503 -> ERROR_REQUEST_HTTP_503 - else -> null -} -fun Throwable.toErrorCode() = when (this) { - is UnknownHostException -> ERROR_REQUEST_FAILURE_HOSTNAME_NOT_FOUND - is SSLException -> ERROR_REQUEST_FAILURE_SSL_ERROR - is SocketTimeoutException -> ERROR_REQUEST_FAILURE_TIMEOUT - is InterruptedIOException, is ConnectException -> ERROR_REQUEST_FAILURE_NO_INTERNET - is SzkolnyApiException -> this.error?.toErrorCode() - else -> null -} -fun ApiResponse.Error.toErrorCode() = when (this.code) { - "PdoError" -> ERROR_API_PDO_ERROR - "InvalidClient" -> ERROR_API_INVALID_CLIENT - "InvalidArgument" -> ERROR_API_INVALID_ARGUMENT - "InvalidSignature" -> ERROR_API_INVALID_SIGNATURE - "MissingScopes" -> ERROR_API_MISSING_SCOPES - "ResourceNotFound" -> ERROR_API_RESOURCE_NOT_FOUND - "InternalServerError" -> ERROR_API_INTERNAL_SERVER_ERROR - "PhpError" -> ERROR_API_PHP_E_ERROR - "PhpWarning" -> ERROR_API_PHP_E_WARNING - "PhpParse" -> ERROR_API_PHP_E_PARSE - "PhpNotice" -> ERROR_API_PHP_E_NOTICE - "PhpOther" -> ERROR_API_PHP_E_OTHER - "ApiMaintenance" -> ERROR_API_MAINTENANCE - "MissingArgument" -> ERROR_API_MISSING_ARGUMENT - "MissingPayload" -> ERROR_API_PAYLOAD_EMPTY - "InvalidAction" -> ERROR_API_INVALID_ACTION - "VersionNotFound" -> ERROR_API_UPDATE_NOT_FOUND - "InvalidDeviceIdUserCode" -> ERROR_API_INVALID_DEVICEID_USERCODE - "InvalidPairToken" -> ERROR_API_INVALID_PAIRTOKEN - "InvalidBrowserId" -> ERROR_API_INVALID_BROWSERID - "InvalidDeviceId" -> ERROR_API_INVALID_DEVICEID - "InvalidDeviceIdBrowserId" -> ERROR_API_INVALID_DEVICEID_BROWSERID - "HelpCategoryNotFound" -> ERROR_API_HELP_CATEGORY_NOT_FOUND - else -> ERROR_API_EXCEPTION -} -fun Throwable.toApiError(tag: String) = ApiError.fromThrowable(tag, this) - -inline fun ifNotNull(a: A?, b: B?, code: (A, B) -> R): R? { - if (a != null && b != null) { - return code(a, b) - } - return null -} - -@kotlin.jvm.JvmName("averageOrNullOfInt") -fun Iterable.averageOrNull() = this.average().let { if (it.isNaN()) null else it } -@kotlin.jvm.JvmName("averageOrNullOfFloat") -fun Iterable.averageOrNull() = this.average().let { if (it.isNaN()) null else it } - -fun String.copyToClipboard(context: Context) { - val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clipData = ClipData.newPlainText("Tekst", this) - clipboard.setPrimaryClip(clipData) -} - -fun TextView.getTextPosition(range: IntRange): Rect { - val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager - - // Initialize global value - var parentTextViewRect = Rect() - - // Initialize values for the computing of clickedText position - //val completeText = parentTextView.text as SpannableString - val textViewLayout = this.layout - - val startOffsetOfClickedText = range.first//completeText.getSpanStart(clickedText) - val endOffsetOfClickedText = range.last//completeText.getSpanEnd(clickedText) - var startXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal(startOffsetOfClickedText) - var endXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal(endOffsetOfClickedText) - - // Get the rectangle of the clicked text - val currentLineStartOffset = textViewLayout.getLineForOffset(startOffsetOfClickedText) - val currentLineEndOffset = textViewLayout.getLineForOffset(endOffsetOfClickedText) - val keywordIsInMultiLine = currentLineStartOffset != currentLineEndOffset - textViewLayout.getLineBounds(currentLineStartOffset, parentTextViewRect) - - // Update the rectangle position to his real position on screen - val parentTextViewLocation = intArrayOf(0, 0) - this.getLocationOnScreen(parentTextViewLocation) - - val parentTextViewTopAndBottomOffset = (parentTextViewLocation[1] - this.scrollY + this.compoundPaddingTop) - parentTextViewRect.top += parentTextViewTopAndBottomOffset - parentTextViewRect.bottom += parentTextViewTopAndBottomOffset - - // In the case of multi line text, we have to choose what rectangle take - if (keywordIsInMultiLine) { - val screenHeight = windowManager.defaultDisplay.height - val dyTop = parentTextViewRect.top - val dyBottom = screenHeight - parentTextViewRect.bottom - val onTop = dyTop > dyBottom - - if (onTop) { - endXCoordinatesOfClickedText = textViewLayout.getLineRight(currentLineStartOffset); - } else { - parentTextViewRect = Rect() - textViewLayout.getLineBounds(currentLineEndOffset, parentTextViewRect); - parentTextViewRect.top += parentTextViewTopAndBottomOffset; - parentTextViewRect.bottom += parentTextViewTopAndBottomOffset; - startXCoordinatesOfClickedText = textViewLayout.getLineLeft(currentLineEndOffset); - } - } - - parentTextViewRect.left += ( - parentTextViewLocation[0] + - startXCoordinatesOfClickedText + - this.compoundPaddingLeft - - this.scrollX - ).toInt() - parentTextViewRect.right = ( - parentTextViewRect.left + - endXCoordinatesOfClickedText - - startXCoordinatesOfClickedText - ).toInt() - - return parentTextViewRect -} - -inline fun ViewPager.addOnPageSelectedListener(crossinline block: (position: Int) -> Unit) = addOnPageChangeListener(object : ViewPager.OnPageChangeListener { - override fun onPageScrollStateChanged(state: Int) {} - override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} - override fun onPageSelected(position: Int) { block(position) } -}) - -val SwipeRefreshLayout.onScrollListener: RecyclerView.OnScrollListener - get() = object : RecyclerView.OnScrollListener() { - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { - if (recyclerView.canScrollVertically(-1)) - this@onScrollListener.isEnabled = false - if (!recyclerView.canScrollVertically(-1) && newState == RecyclerView.SCROLL_STATE_IDLE) - this@onScrollListener.isEnabled = true - } - } - -operator fun Iterable>.get(key: K): V? { - return firstOrNull { it.first == key }?.second -} - -fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) } - -fun MutableList.after(what: E, insert: E) { - val index = indexOf(what) - if (index != -1) - add(index + 1, insert) -} - -fun MutableList.before(what: E, insert: E) { - val index = indexOf(what) - if (index != -1) - add(index, insert) -} - -fun MutableList.after(what: E, insert: Collection) { - val index = indexOf(what) - if (index != -1) - addAll(index + 1, insert) -} - -fun MutableList.before(what: E, insert: Collection) { - val index = indexOf(what) - if (index != -1) - addAll(index, insert) -} - -fun Context.getSyncInterval(interval: Int): String { - val hours = interval / 60 / 60 - val minutes = interval / 60 % 60 - val hoursText = if (hours > 0) - plural(R.plurals.time_till_hours, hours) - else - null - val minutesText = if (minutes > 0) - plural(R.plurals.time_till_minutes, minutes) - else - "" - return hoursText?.plus(" $minutesText") ?: minutesText -} - -fun Profile.getSchoolYearConstrains(): CalendarConstraints { - return CalendarConstraints.Builder() - .setStart(dateSemester1Start.inMillisUtc) - .setEnd(dateYearEnd.inMillisUtc) - .build() -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt index 8dd83bf2..7965d17a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt @@ -17,78 +17,59 @@ import android.widget.Toast import androidx.appcompat.app.AppCompatActivity import androidx.core.graphics.ColorUtils import androidx.core.view.isVisible -import androidx.lifecycle.Observer import androidx.navigation.NavOptions import com.danimahardhika.cafebar.CafeBar import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.jetradarmobile.snowfall.SnowfallView 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.sizeDp -import com.mikepenz.materialdrawer.model.DividerDrawerItem -import com.mikepenz.materialdrawer.model.ProfileDrawerItem -import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem +import com.mikepenz.materialdrawer.model.* import com.mikepenz.materialdrawer.model.interfaces.* -import com.mikepenz.materialdrawer.model.utils.withIsHiddenInMiniDrawer -import eu.szkolny.font.SzkolnyFont +import com.mikepenz.materialdrawer.model.utils.hiddenInMiniDrawer import kotlinx.coroutines.* import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import pl.droidsonroids.gif.GifDrawable -import pl.szczodrzynski.edziennik.data.api.ERROR_API_INVALID_SIGNATURE import pl.szczodrzynski.edziennik.data.api.ERROR_VULCAN_API_DEPRECATED import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.events.* import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi -import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update -import pl.szczodrzynski.edziennik.data.db.entity.LoginStore +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 +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.sync.AppManagerDetectedEvent import pl.szczodrzynski.edziennik.sync.SyncWorker +import pl.szczodrzynski.edziennik.sync.UpdateStateEvent import pl.szczodrzynski.edziennik.sync.UpdateWorker -import pl.szczodrzynski.edziennik.ui.dialogs.RegisterUnavailableDialog -import pl.szczodrzynski.edziennik.ui.dialogs.ServerMessageDialog -import pl.szczodrzynski.edziennik.ui.dialogs.UpdateAvailableDialog -import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog -import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog -import pl.szczodrzynski.edziennik.ui.dialogs.profile.ProfileConfigDialog +import pl.szczodrzynski.edziennik.ui.base.MainSnackbar +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget +import pl.szczodrzynski.edziennik.ui.base.enums.NavTargetLocation +import pl.szczodrzynski.edziennik.ui.dialogs.ChangelogDialog +import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileConfigDialog +import pl.szczodrzynski.edziennik.ui.dialogs.sync.RegisterUnavailableDialog +import pl.szczodrzynski.edziennik.ui.dialogs.sync.ServerMessageDialog import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog -import pl.szczodrzynski.edziennik.ui.modules.agenda.AgendaFragment -import pl.szczodrzynski.edziennik.ui.modules.announcements.AnnouncementsFragment -import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceFragment -import pl.szczodrzynski.edziennik.ui.modules.base.MainSnackbar -import pl.szczodrzynski.edziennik.ui.modules.behaviour.BehaviourFragment -import pl.szczodrzynski.edziennik.ui.modules.debug.DebugFragment -import pl.szczodrzynski.edziennik.ui.modules.debug.LabFragment -import pl.szczodrzynski.edziennik.ui.modules.error.ErrorDetailsDialog -import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar -import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackFragment -import pl.szczodrzynski.edziennik.ui.modules.grades.GradesListFragment -import pl.szczodrzynski.edziennik.ui.modules.grades.editor.GradesEditorFragment -import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment -import pl.szczodrzynski.edziennik.ui.modules.homework.HomeworkFragment -import pl.szczodrzynski.edziennik.ui.modules.login.LoginActivity -import pl.szczodrzynski.edziennik.ui.modules.messages.MessageFragment -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment -import pl.szczodrzynski.edziennik.ui.modules.messages.compose.MessagesComposeFragment -import pl.szczodrzynski.edziennik.ui.modules.notifications.NotificationsListFragment -import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment -import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsFragment -import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment -import pl.szczodrzynski.edziennik.ui.modules.webpush.WebPushFragment -import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoTouch -import pl.szczodrzynski.edziennik.utils.Themes -import pl.szczodrzynski.edziennik.utils.Utils +import pl.szczodrzynski.edziennik.ui.dialogs.sync.UpdateAvailableDialog +import pl.szczodrzynski.edziennik.ui.dialogs.sync.UpdateProgressDialog +import pl.szczodrzynski.edziennik.ui.error.ErrorDetailsDialog +import pl.szczodrzynski.edziennik.ui.error.ErrorSnackbar +import pl.szczodrzynski.edziennik.ui.event.EventManualDialog +import pl.szczodrzynski.edziennik.ui.login.LoginActivity +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.appManagerIntentList +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.edziennik.utils.models.NavTarget import pl.szczodrzynski.navlib.* import pl.szczodrzynski.navlib.SystemBarsUtil.Companion.COLOR_HALF_TRANSPARENT import pl.szczodrzynski.navlib.bottomsheet.NavBottomSheet @@ -103,142 +84,7 @@ import kotlin.math.roundToInt class MainActivity : AppCompatActivity(), CoroutineScope { companion object { - - var useOldMessages = false - - const val TAG = "MainActivity" - - const val DRAWER_PROFILE_ADD_NEW = 200 - const val DRAWER_PROFILE_SYNC_ALL = 201 - const val DRAWER_PROFILE_EXPORT_DATA = 202 - const val DRAWER_PROFILE_MANAGE = 203 - const val DRAWER_PROFILE_MARK_ALL_AS_READ = 204 - const val DRAWER_ITEM_HOME = 1 - const val DRAWER_ITEM_TIMETABLE = 11 - const val DRAWER_ITEM_AGENDA = 12 - const val DRAWER_ITEM_GRADES = 13 - const val DRAWER_ITEM_MESSAGES = 17 - const val DRAWER_ITEM_HOMEWORK = 14 - const val DRAWER_ITEM_BEHAVIOUR = 15 - const val DRAWER_ITEM_ATTENDANCE = 16 - const val DRAWER_ITEM_ANNOUNCEMENTS = 18 - const val DRAWER_ITEM_NOTIFICATIONS = 20 - const val DRAWER_ITEM_SETTINGS = 101 - const val DRAWER_ITEM_DEBUG = 102 - - const val TARGET_GRADES_EDITOR = 501 - const val TARGET_FEEDBACK = 120 - const val TARGET_MESSAGES_DETAILS = 503 - const val TARGET_MESSAGES_COMPOSE = 504 - const val TARGET_WEB_PUSH = 140 - const val TARGET_LAB = 1000 - - const val HOME_ID = DRAWER_ITEM_HOME - - val navTargetList: List by lazy { - val list: MutableList = mutableListOf() - - // home item - list += NavTarget(DRAWER_ITEM_HOME, R.string.menu_home_page, HomeFragment::class) - .withTitle(R.string.app_name) - .withIcon(CommunityMaterial.Icon2.cmd_home_outline) - .isInDrawer(true) - .isStatic(true) - .withPopToHome(false) - - list += NavTarget(DRAWER_ITEM_TIMETABLE, R.string.menu_timetable, TimetableFragment::class) - .withIcon(CommunityMaterial.Icon3.cmd_timetable) - .withBadgeTypeId(TYPE_LESSON_CHANGE) - .isInDrawer(true) - - list += NavTarget(DRAWER_ITEM_AGENDA, R.string.menu_agenda, AgendaFragment::class) - .withIcon(CommunityMaterial.Icon.cmd_calendar_outline) - .withBadgeTypeId(TYPE_EVENT) - .isInDrawer(true) - - list += NavTarget(DRAWER_ITEM_GRADES, R.string.menu_grades, GradesListFragment::class) - .withIcon(CommunityMaterial.Icon3.cmd_numeric_5_box_outline) - .withBadgeTypeId(TYPE_GRADE) - .isInDrawer(true) - - list += NavTarget(DRAWER_ITEM_MESSAGES, R.string.menu_messages, MessagesFragment::class) - .withIcon(CommunityMaterial.Icon.cmd_email_outline) - .withBadgeTypeId(TYPE_MESSAGE) - .isInDrawer(true) - - list += NavTarget(DRAWER_ITEM_HOMEWORK, R.string.menu_homework, HomeworkFragment::class) - .withIcon(SzkolnyFont.Icon.szf_notebook_outline) - .withBadgeTypeId(TYPE_HOMEWORK) - .isInDrawer(true) - - list += NavTarget(DRAWER_ITEM_BEHAVIOUR, R.string.menu_notices, BehaviourFragment::class) - .withIcon(CommunityMaterial.Icon.cmd_emoticon_outline) - .withBadgeTypeId(TYPE_NOTICE) - .isInDrawer(true) - - list += NavTarget(DRAWER_ITEM_ATTENDANCE, R.string.menu_attendance, AttendanceFragment::class) - .withIcon(CommunityMaterial.Icon.cmd_calendar_remove_outline) - .withBadgeTypeId(TYPE_ATTENDANCE) - .isInDrawer(true) - - list += NavTarget(DRAWER_ITEM_ANNOUNCEMENTS, R.string.menu_announcements, AnnouncementsFragment::class) - .withIcon(CommunityMaterial.Icon.cmd_bullhorn_outline) - .withBadgeTypeId(TYPE_ANNOUNCEMENT) - .isInDrawer(true) - - - // static drawer items - list += NavTarget(DRAWER_ITEM_NOTIFICATIONS, R.string.menu_notifications, NotificationsListFragment::class) - .withIcon(CommunityMaterial.Icon.cmd_bell_ring_outline) - .isInDrawer(true) - .isStatic(true) - .isBelowSeparator(true) - - list += NavTarget(DRAWER_ITEM_SETTINGS, R.string.menu_settings, SettingsFragment::class) - .withIcon(CommunityMaterial.Icon.cmd_cog_outline) - .isInDrawer(true) - .isStatic(true) - .isBelowSeparator(true) - - - // profile settings items - list += NavTarget(DRAWER_PROFILE_ADD_NEW, R.string.menu_add_new_profile, null) - .withIcon(CommunityMaterial.Icon3.cmd_plus) - .withDescription(R.string.drawer_add_new_profile_desc) - .isInProfileList(true) - - list += NavTarget(DRAWER_PROFILE_MANAGE, R.string.menu_manage_profiles, ProfileManagerFragment::class) - .withTitle(R.string.title_profile_manager) - .withIcon(CommunityMaterial.Icon.cmd_account_group) - .withDescription(R.string.drawer_manage_profiles_desc) - .isInProfileList(false) - - list += NavTarget(DRAWER_PROFILE_MARK_ALL_AS_READ, R.string.menu_mark_everything_as_read, null) - .withIcon(CommunityMaterial.Icon.cmd_eye_check_outline) - .isInProfileList(true) - - list += NavTarget(DRAWER_PROFILE_SYNC_ALL, R.string.menu_sync_all, null) - .withIcon(CommunityMaterial.Icon.cmd_download_outline) - .isInProfileList(true) - - - // other target items, not directly navigated - list += NavTarget(TARGET_GRADES_EDITOR, R.string.menu_grades_editor, GradesEditorFragment::class) - list += NavTarget(TARGET_FEEDBACK, R.string.menu_feedback, FeedbackFragment::class) - list += NavTarget(TARGET_MESSAGES_DETAILS, R.string.menu_message, MessageFragment::class).withPopTo(DRAWER_ITEM_MESSAGES) - list += NavTarget(TARGET_MESSAGES_COMPOSE, R.string.menu_message_compose, MessagesComposeFragment::class) - list += NavTarget(TARGET_WEB_PUSH, R.string.menu_web_push, WebPushFragment::class) - if (App.devMode) { - list += NavTarget(DRAWER_ITEM_DEBUG, R.string.menu_debug, DebugFragment::class) - list += NavTarget(TARGET_LAB, R.string.menu_lab, LabFragment::class) - .withIcon(CommunityMaterial.Icon2.cmd_flask_outline) - .isInDrawer(true) - .isBelowSeparator(true) - .isStatic(true) - } - - list - } + private const val TAG = "MainActivity" } private var job = Job() @@ -255,15 +101,17 @@ class MainActivity : AppCompatActivity(), CoroutineScope { val swipeRefreshLayout: SwipeRefreshLayoutNoTouch by lazy { b.swipeRefreshLayout } + var onBeforeNavigate: (() -> Boolean)? = null + private var pausedNavigationData: PausedNavigationData? = null + val app: App by lazy { applicationContext as App } private val fragmentManager by lazy { supportFragmentManager } - private lateinit var navTarget: NavTarget + lateinit var navTarget: NavTarget + private set private var navArguments: Bundle? = null - val navTargetId - get() = navTarget.id private val navBackStack = mutableListOf>() private var navLoading = true @@ -326,8 +174,12 @@ class MainActivity : AppCompatActivity(), CoroutineScope { 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) { - window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR + 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 @@ -347,8 +199,8 @@ class MainActivity : AppCompatActivity(), CoroutineScope { fabGravity = Gravity.CENTER if (Themes.isDark) { setBackgroundColor(blendColors( - getColorFromAttr(context, R.attr.colorSurface), - getColorFromRes(R.color.colorSurface_4dp) + getColorFromAttr(context, R.attr.colorSurface), + getColorFromRes(R.color.colorSurface_4dp) )) elevation = dpToPx(4).toFloat() } @@ -370,13 +222,15 @@ class MainActivity : AppCompatActivity(), CoroutineScope { drawerProfileListEmptyListener = { onProfileListEmptyEvent(ProfileListEmptyEvent()) } - drawerItemSelectedListener = { id, position, drawerItem -> - loadTarget(id) - true + drawerItemSelectedListener = { id, _, item -> + if (item is ExpandableDrawerItem) + false + else + navigate(navTarget = id.asNavTargetOrNull()) } - drawerProfileSelectedListener = { id, profile, _, _ -> - loadProfile(id) - false + drawerProfileSelectedListener = { id, _, _, _ -> + // why is this negated -_- + !navigate(profileId = id) } drawerProfileLongClickListener = { _, profile, _, view -> if (view != null && profile is ProfileDrawerItem) { @@ -385,11 +239,10 @@ class MainActivity : AppCompatActivity(), CoroutineScope { App.db.profileDao().getByIdNow(profile.identifier.toInt()) } ?: return@launch drawer.close() - ProfileConfigDialog(this@MainActivity, appProfile) + ProfileConfigDialog(this@MainActivity, appProfile).show() } true - } - else { + } else { false } } @@ -401,48 +254,47 @@ class MainActivity : AppCompatActivity(), CoroutineScope { } } - navTarget = navTargetList[0] + navTarget = NavTarget.HOME if (savedInstanceState != null) { intent?.putExtras(savedInstanceState) savedInstanceState.clear() } - app.db.profileDao().all.observe(this, Observer { profiles -> + app.db.profileDao().all.observe(this) { profiles -> val allArchived = profiles.all { it.archived } - drawer.setProfileList(profiles.filter { it.id >= 0 && (!it.archived || allArchived) }.toMutableList()) + drawer.setProfileList(profiles.filter { + it.id >= 0 && (!it.archived || allArchived) + }.toMutableList()) //prepend the archived profile if loaded if (app.profile.archived && !allArchived) { drawer.prependProfile(Profile( - id = app.profile.id, - loginStoreId = app.profile.loginStoreId, - loginStoreType = app.profile.loginStoreType, - name = app.profile.name, - subname = "Archiwum - ${app.profile.subname}" + id = app.profile.id, + loginStoreId = app.profile.loginStoreId, + loginStoreType = app.profile.loginStoreType, + name = app.profile.name, + subname = "Archiwum - ${app.profile.subname}" ).also { it.archived = true }) } drawer.currentProfile = App.profileId - }) + } setDrawerItems() handleIntent(intent?.extras) - app.db.metadataDao().unreadCounts.observe(this, Observer { unreadCounters -> - unreadCounters.map { - it.type = it.thingType - } + app.db.metadataDao().unreadCounts.observe(this) { unreadCounters -> drawer.setUnreadCounterList(unreadCounters) - }) + } b.swipeRefreshLayout.isEnabled = true b.swipeRefreshLayout.setOnRefreshListener { launch { syncCurrentFeature() } } b.swipeRefreshLayout.setColorSchemeResources( - R.color.md_blue_500, - R.color.md_amber_500, - R.color.md_green_500 + R.color.md_blue_500, + R.color.md_amber_500, + R.color.md_green_500 ) SyncWorker.scheduleNext(app) @@ -456,11 +308,11 @@ class MainActivity : AppCompatActivity(), CoroutineScope { app.db.profileDao().getNotArchivedOf(app.profile.archiveId!!) } if (profile != null) - loadProfile(profile) + navigate(profile = profile) else - loadProfile(0) + navigate(profileId = 0) } else { - loadProfile(0) + navigate(profileId = 0) } } } @@ -470,15 +322,30 @@ class MainActivity : AppCompatActivity(), CoroutineScope { // IT'S WINTER MY DUDES val today = Date.getToday() - if ((today.month == 12 || today.month == 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( + 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) } // WHAT'S NEW DIALOG if (app.config.appVersion < BuildConfig.VERSION_CODE) { // force an AppSync after update app.config.sync.lastAppSync = 0L - ChangelogDialog(this) + ChangelogDialog(this).show() if (app.config.appVersion < 170) { //Intent intent = new Intent(this, ChangelogIntroActivity.class); //startActivity(intent); @@ -491,89 +358,90 @@ class MainActivity : AppCompatActivity(), CoroutineScope { if (app.config.appRateSnackbarTime != 0L && app.config.appRateSnackbarTime <= System.currentTimeMillis()) { navView.coordinator.postDelayed({ CafeBar.builder(this) - .content(R.string.rate_snackbar_text) - .icon(IconicsDrawable(this).apply { - icon = CommunityMaterial.Icon3.cmd_star_outline - sizeDp = 24 - colorInt = Themes.getPrimaryTextColor(this@MainActivity) - }) - .positiveText(R.string.rate_snackbar_positive) - .positiveColor(-0xb350b0) - .negativeText(R.string.rate_snackbar_negative) - .negativeColor(0xff666666.toInt()) - .neutralText(R.string.rate_snackbar_neutral) - .neutralColor(0xff666666.toInt()) - .onPositive { cafeBar -> - Utils.openGooglePlay(this) - cafeBar.dismiss() - app.config.appRateSnackbarTime = 0 - } - .onNegative { cafeBar -> - Toast.makeText(this, R.string.rate_snackbar_negative_message, Toast.LENGTH_LONG).show() - cafeBar.dismiss() - app.config.appRateSnackbarTime = 0 - } - .onNeutral { cafeBar -> - Toast.makeText(this, R.string.ok, Toast.LENGTH_LONG).show() - cafeBar.dismiss() - app.config.appRateSnackbarTime = System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000 - } - .autoDismiss(false) - .swipeToDismiss(true) - .floating(true) - .show() + .content(R.string.rate_snackbar_text) + .icon(IconicsDrawable(this).apply { + icon = CommunityMaterial.Icon3.cmd_star_outline + sizeDp = 24 + colorInt = Themes.getPrimaryTextColor(this@MainActivity) + }) + .positiveText(R.string.rate_snackbar_positive) + .positiveColor(-0xb350b0) + .negativeText(R.string.rate_snackbar_negative) + .negativeColor(0xff666666.toInt()) + .neutralText(R.string.rate_snackbar_neutral) + .neutralColor(0xff666666.toInt()) + .onPositive { cafeBar -> + Utils.openGooglePlay(this) + cafeBar.dismiss() + app.config.appRateSnackbarTime = 0 + } + .onNegative { cafeBar -> + Toast.makeText(this, + R.string.rate_snackbar_negative_message, + Toast.LENGTH_LONG).show() + cafeBar.dismiss() + app.config.appRateSnackbarTime = 0 + } + .onNeutral { cafeBar -> + Toast.makeText(this, R.string.ok, Toast.LENGTH_LONG).show() + cafeBar.dismiss() + app.config.appRateSnackbarTime = + System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000 + } + .autoDismiss(false) + .swipeToDismiss(true) + .floating(true) + .show() }, 10000) } // CONTEXT MENU ITEMS bottomSheet.removeAllItems() bottomSheet.appendItems( - BottomSheetPrimaryItem(false) - .withTitle(R.string.menu_sync) - .withIcon(CommunityMaterial.Icon.cmd_download_outline) - .withOnClickListener(View.OnClickListener { - bottomSheet.close() - SyncViewListDialog(this, navTargetId) - }), - BottomSheetSeparatorItem(false), - BottomSheetPrimaryItem(false) - .withTitle(R.string.menu_settings) - .withIcon(CommunityMaterial.Icon.cmd_cog_outline) - .withOnClickListener(View.OnClickListener { loadTarget(DRAWER_ITEM_SETTINGS) }), - BottomSheetPrimaryItem(false) - .withTitle(R.string.menu_feedback) - .withIcon(CommunityMaterial.Icon2.cmd_help_circle_outline) - .withOnClickListener(View.OnClickListener { loadTarget(TARGET_FEEDBACK) }) + BottomSheetPrimaryItem(false) + .withTitle(R.string.menu_sync) + .withIcon(CommunityMaterial.Icon.cmd_download_outline) + .withOnClickListener { + bottomSheet.close() + SyncViewListDialog(this, navTarget).show() + }, + BottomSheetSeparatorItem(false), ) - if (App.devMode) { - bottomSheet += BottomSheetPrimaryItem(false) - .withTitle(R.string.menu_debug) - .withIcon(CommunityMaterial.Icon.cmd_android_debug_bridge) - .withOnClickListener(View.OnClickListener { loadTarget(DRAWER_ITEM_DEBUG) }) + for (target in NavTarget.values()) { + if (target.location != NavTargetLocation.BOTTOM_SHEET) + continue + if (target.devModeOnly && !App.devMode) + continue + bottomSheet += target.toBottomSheetItem(this) } } - private var profileSettingClickListener = { id: Int, view: View? -> - when (id) { - DRAWER_PROFILE_ADD_NEW -> { + private var profileSettingClickListener = { itemId: Int, _: View? -> + when (val item = itemId.asNavTarget()) { + NavTarget.PROFILE_ADD -> { requestHandler.requestLogin() } - DRAWER_PROFILE_SYNC_ALL -> { + NavTarget.PROFILE_SYNC_ALL -> { EdziennikTask.sync().enqueue(this) } - DRAWER_PROFILE_MARK_ALL_AS_READ -> { launch { - withContext(Dispatchers.Default) { - app.db.profileDao().allNow.forEach { profile -> - if (profile.loginStoreType != LoginStore.LOGIN_TYPE_LIBRUS) - app.db.metadataDao().setAllSeenExceptMessagesAndAnnouncements(profile.id, true) - else - app.db.metadataDao().setAllSeenExceptMessages(profile.id, true) + NavTarget.PROFILE_MARK_AS_READ -> { + launch { + withContext(Dispatchers.Default) { + app.db.profileDao().allNow.forEach { profile -> + if (!profile.getAppData().uiConfig.enableMarkAsReadAnnouncements) + app.db.metadataDao() + .setAllSeenExceptMessagesAndAnnouncements(profile.id, true) + else + app.db.metadataDao().setAllSeenExceptMessages(profile.id, true) + } } + Toast.makeText(this@MainActivity, + R.string.main_menu_mark_as_read_success, + Toast.LENGTH_SHORT).show() } - Toast.makeText(this@MainActivity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show() - }} + } else -> { - loadTarget(id) + navigate(navTarget = item) } } false @@ -587,114 +455,106 @@ class MainActivity : AppCompatActivity(), CoroutineScope { |_____/ \__, |_| |_|\___| __/ | |__*/ - suspend fun syncCurrentFeature() { + private suspend fun syncCurrentFeature() { if (app.profile.archived) { MaterialAlertDialogBuilder(this) - .setTitle(R.string.profile_archived_title) - .setMessage( - R.string.profile_archived_text, - app.profile.studentSchoolYearStart, - app.profile.studentSchoolYearStart + 1 - ) - .setPositiveButton(R.string.ok, null) - .show() + .setTitle(R.string.profile_archived_title) + .setMessage( + R.string.profile_archived_text, + app.profile.studentSchoolYearStart, + app.profile.studentSchoolYearStart + 1 + ) + .setPositiveButton(R.string.ok, null) + .show() swipeRefreshLayout.isRefreshing = false return } if (app.profile.shouldArchive()) { MaterialAlertDialogBuilder(this) - .setTitle(R.string.profile_archiving_title) - .setMessage( - R.string.profile_archiving_format, - app.profile.dateYearEnd.formattedString - ) - .setPositiveButton(R.string.ok, null) - .show() + .setTitle(R.string.profile_archiving_title) + .setMessage( + R.string.profile_archiving_format, + app.profile.dateYearEnd.formattedString + ) + .setPositiveButton(R.string.ok, null) + .show() } if (app.profile.isBeforeYear()) { MaterialAlertDialogBuilder(this) - .setTitle(R.string.profile_year_not_started_title) - .setMessage( - R.string.profile_year_not_started_format, - app.profile.dateSemester1Start.formattedString - ) - .setPositiveButton(R.string.ok, null) - .show() + .setTitle(R.string.profile_year_not_started_title) + .setMessage( + R.string.profile_year_not_started_format, + app.profile.dateSemester1Start.formattedString + ) + .setPositiveButton(R.string.ok, null) + .show() swipeRefreshLayout.isRefreshing = false return } - app.profile.registerName?.let { registerName -> - var status = app.config.sync.registerAvailability[registerName] - if (status == null || status.nextCheckAt < currentTimeUnix()) { - val api = SzkolnyApi(app) - val result = withContext(Dispatchers.IO) { - return@withContext api.runCatching({ - val availability = getRegisterAvailability() - app.config.sync.registerAvailability = availability - availability[registerName] - }, onError = { - if (it.toErrorCode() == ERROR_API_INVALID_SIGNATURE) { - return@withContext false - } - return@withContext it - }) - } - - when (result) { - false -> { - Toast.makeText(this@MainActivity, R.string.error_no_api_access, Toast.LENGTH_SHORT).show() - return@let - } - is Throwable -> { - errorSnackbar.addError(result.toApiError(TAG)).show() - return - } - is RegisterAvailabilityStatus -> { - status = result - } - } - } - - if (status?.available != true || status.minVersionCode > BuildConfig.VERSION_CODE) { + val error = withContext(Dispatchers.IO) { + app.availabilityManager.check(app.profile) + } + when (error?.type) { + Type.NOT_AVAILABLE -> { swipeRefreshLayout.isRefreshing = false - loadTarget(DRAWER_ITEM_HOME) - if (status != null) - RegisterUnavailableDialog(this, status) + navigate(navTarget = NavTarget.HOME) + RegisterUnavailableDialog(this, error.status!!).show() return } + Type.API_ERROR -> { + errorSnackbar.addError(error.apiError!!).show() + return + } + Type.NO_API_ACCESS -> { + Toast.makeText(this, R.string.error_no_api_access, Toast.LENGTH_SHORT).show() + } + else -> {} } swipeRefreshLayout.isRefreshing = true - Toast.makeText(this, fragmentToSyncName(navTargetId), Toast.LENGTH_SHORT).show() - val fragmentParam = when (navTargetId) { - DRAWER_ITEM_MESSAGES -> MessagesFragment.pageSelection - else -> 0 + Toast.makeText(this, fragmentToSyncName(navTarget), Toast.LENGTH_SHORT).show() + val featureType = when (navTarget) { + NavTarget.MESSAGES -> when (MessagesFragment.pageSelection) { + Message.TYPE_SENT -> FeatureType.MESSAGES_SENT + else -> FeatureType.MESSAGES_INBOX + } + else -> navTarget.featureType } - val arguments = when (navTargetId) { - DRAWER_ITEM_TIMETABLE -> JsonObject("weekStart" to TimetableFragment.pageSelection?.weekStart?.stringY_m_d) + val arguments = when (navTarget) { + NavTarget.TIMETABLE -> JsonObject("weekStart" to TimetableFragment.pageSelection?.weekStart?.stringY_m_d) else -> null } EdziennikTask.syncProfile( - App.profileId, - listOf(navTargetId to fragmentParam), - arguments = arguments + App.profileId, + featureType?.let { setOf(it) }, + arguments = arguments ).enqueue(this) } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onUpdateEvent(event: Update) { EventBus.getDefault().removeStickyEvent(event) - UpdateAvailableDialog(this, event) + UpdateAvailableDialog(this, event).show() } + + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) + fun onUpdateStateEvent(event: UpdateStateEvent) { + if (!event.running) + return + EventBus.getDefault().removeStickyEvent(event) + UpdateProgressDialog(this, event.update ?: return, event.downloadId).show() + } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onRegisterAvailabilityEvent(event: RegisterAvailabilityEvent) { EventBus.getDefault().removeStickyEvent(event) - app.profile.registerName?.let { registerName -> - event.data[registerName]?.let { - RegisterUnavailableDialog(this, it) - } + val error = app.availabilityManager.check(app.profile, cacheOnly = true) + if (error != null) { + RegisterUnavailableDialog(this, error.status!!).show() } } + @Subscribe(threadMode = ThreadMode.MAIN) fun onApiTaskStartedEvent(event: ApiTaskStartedEvent) { swipeRefreshLayout.isRefreshing = true @@ -706,6 +566,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { } } } + @Subscribe(threadMode = ThreadMode.MAIN) fun onProfileListEmptyEvent(event: ProfileListEmptyEvent) { d(TAG, "Profile list is empty. Launch LoginActivity.") @@ -713,6 +574,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { startActivity(Intent(this, LoginActivity::class.java)) finish() } + @Subscribe(threadMode = ThreadMode.MAIN) fun onApiTaskProgressEvent(event: ApiTaskProgressEvent) { if (event.profileId == App.profileId) { @@ -722,11 +584,16 @@ class MainActivity : AppCompatActivity(), CoroutineScope { subtitle = if (event.progress < 0f) event.progressText ?: "" else - getString(R.string.toolbar_subtitle_syncing_format, event.progress.roundToInt(), event.progressText ?: "") + getString( + R.string.toolbar_subtitle_syncing_format, + event.progress.roundToInt(), + event.progressText ?: "", + ) } } } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onApiTaskFinishedEvent(event: ApiTaskFinishedEvent) { EventBus.getDefault().removeStickyEvent(event) @@ -738,18 +605,20 @@ class MainActivity : AppCompatActivity(), CoroutineScope { } } } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onApiTaskAllFinishedEvent(event: ApiTaskAllFinishedEvent) { EventBus.getDefault().removeStickyEvent(event) swipeRefreshLayout.isRefreshing = false } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onApiTaskErrorEvent(event: ApiTaskErrorEvent) { EventBus.getDefault().removeStickyEvent(event) if (event.error.errorCode == ERROR_VULCAN_API_DEPRECATED) { if (event.error.profileId != App.profileId) return - ErrorDetailsDialog(this, listOf(event.error)) + ErrorDetailsDialog(this, listOf(event.error)).show() } navView.toolbar.apply { subtitleFormat = R.string.toolbar_subtitle @@ -759,54 +628,59 @@ class MainActivity : AppCompatActivity(), CoroutineScope { mainSnackbar.dismiss() errorSnackbar.addError(event.error).show() } + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) fun onAppManagerDetectedEvent(event: AppManagerDetectedEvent) { EventBus.getDefault().removeStickyEvent(event) if (app.config.sync.dontShowAppManagerDialog) return MaterialAlertDialogBuilder(this) - .setTitle(R.string.app_manager_dialog_title) - .setMessage(R.string.app_manager_dialog_text) - .setPositiveButton(R.string.ok) { dialog, which -> - try { - for (intent in appManagerIntentList) { - if (packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) { - startActivity(intent) - } - } - } catch (e: Exception) { - try { - startActivity(Intent(Settings.ACTION_SETTINGS)) - } catch (e: Exception) { - e.printStackTrace() - Toast.makeText(this, R.string.app_manager_open_failed, Toast.LENGTH_SHORT).show() + .setTitle(R.string.app_manager_dialog_title) + .setMessage(R.string.app_manager_dialog_text) + .setPositiveButton(R.string.ok) { _, _ -> + try { + for (intent in appManagerIntentList) { + if (packageManager.resolveActivity(intent, + PackageManager.MATCH_DEFAULT_ONLY) != null + ) { + startActivity(intent) } } + } catch (e: Exception) { + try { + startActivity(Intent(Settings.ACTION_SETTINGS)) + } catch (e: Exception) { + e.printStackTrace() + Toast.makeText(this, R.string.app_manager_open_failed, Toast.LENGTH_SHORT) + .show() + } } - .setNeutralButton(R.string.dont_ask_again) { dialog, which -> - app.config.sync.dontShowAppManagerDialog = true - } - .setCancelable(false) - .show() - } - @Subscribe(threadMode = ThreadMode.MAIN) - fun onUserActionRequiredEvent(event: UserActionRequiredEvent) { - app.userActionManager.execute(this, event.profileId, event.type) + } + .setNeutralButton(R.string.dont_ask_again) { _, _ -> + app.config.sync.dontShowAppManagerDialog = true + } + .setCancelable(false) + .show() } - private fun fragmentToSyncName(currentFragment: Int): Int { - return when (currentFragment) { - DRAWER_ITEM_TIMETABLE -> R.string.sync_feature_timetable - DRAWER_ITEM_AGENDA -> R.string.sync_feature_agenda - DRAWER_ITEM_GRADES -> R.string.sync_feature_grades - DRAWER_ITEM_HOMEWORK -> R.string.sync_feature_homework - DRAWER_ITEM_BEHAVIOUR -> R.string.sync_feature_notices - DRAWER_ITEM_ATTENDANCE -> R.string.sync_feature_attendance - DRAWER_ITEM_MESSAGES -> when (MessagesFragment.pageSelection) { - 1 -> R.string.sync_feature_messages_outbox + @Subscribe(threadMode = ThreadMode.MAIN) + fun onUserActionRequiredEvent(event: UserActionRequiredEvent) { + app.userActionManager.execute(this, event, UserActionManager.UserActionCallback()) + } + + private fun fragmentToSyncName(navTarget: NavTarget): Int { + return when (navTarget) { + NavTarget.TIMETABLE -> R.string.sync_feature_timetable + NavTarget.AGENDA -> R.string.sync_feature_agenda + NavTarget.GRADES -> R.string.sync_feature_grades + NavTarget.HOMEWORK -> R.string.sync_feature_homework + NavTarget.BEHAVIOUR -> R.string.sync_feature_notices + NavTarget.ATTENDANCE -> R.string.sync_feature_attendance + NavTarget.MESSAGES -> when (MessagesFragment.pageSelection) { + Message.TYPE_SENT -> R.string.sync_feature_messages_outbox else -> R.string.sync_feature_messages_inbox } - DRAWER_ITEM_ANNOUNCEMENTS -> R.string.sync_feature_announcements + NavTarget.ANNOUNCEMENTS -> R.string.sync_feature_announcements else -> R.string.sync_feature_syncing_all } } @@ -822,46 +696,56 @@ class MainActivity : AppCompatActivity(), CoroutineScope { handleIntent(intent?.extras) } } - fun handleIntent(extras: Bundle?) { + fun handleIntent(extras: Bundle?) { d(TAG, "handleIntent() {") extras?.keySet()?.forEach { key -> - d(TAG, " \"$key\": "+extras.get(key)) + d(TAG, " \"$key\": " + extras.get(key)) } d(TAG, "}") - var intentProfileId = -1 - var intentTargetId = -1 + val intentProfileId = extras.getIntOrNull("profileId").takePositive() + var intentNavTarget = extras.getIntOrNull("fragmentId").asNavTargetOrNull() if (extras?.containsKey("action") == true) { val handled = when (extras.getString("action")) { + "updateRequest" -> { + UpdateAvailableDialog(this, app.config.update).show() + true + } "serverMessage" -> { ServerMessageDialog( - this, - extras.getString("serverMessageTitle") ?: getString(R.string.app_name), - extras.getString("serverMessageText") ?: "" - ) + this, + extras.getString("serverMessageTitle") ?: getString(R.string.app_name), + extras.getString("serverMessageText") ?: "" + ).show() true } "feedbackMessage" -> { - intentTargetId = TARGET_FEEDBACK + intentNavTarget = NavTarget.FEEDBACK false } "userActionRequired" -> { - app.userActionManager.execute( - this, - extras.getInt("profileId"), - extras.getInt("type") + val event = UserActionRequiredEvent( + profileId = extras.getInt("profileId"), + type = extras.getEnum("type") ?: return, + params = extras.getBundle("params") ?: return, + errorText = 0, ) + app.userActionManager.execute(this, + event, + UserActionManager.UserActionCallback()) true } "createManualEvent" -> { - val date = extras.getString("eventDate")?.let { Date.fromY_m_d(it) } ?: Date.getToday() + val date = extras.getString("eventDate") + ?.let { Date.fromY_m_d(it) } + ?: Date.getToday() EventManualDialog( - this, - App.profileId, - defaultDate = date - ) + this, + App.profileId, + defaultDate = date + ).show() true } else -> false @@ -872,70 +756,59 @@ class MainActivity : AppCompatActivity(), CoroutineScope { } if (extras?.containsKey("reloadProfileId") == true) { - val reloadProfileId = extras.getInt("reloadProfileId", -1) - extras.remove("reloadProfileId") - if (reloadProfileId == -1 || app.profile.id == reloadProfileId) { + val reloadProfileId = extras.getIntOrNull("reloadProfileId").takePositive() + if (reloadProfileId == null || app.profile.id == reloadProfileId) { reloadTarget() return } } - if (extras?.getInt("profileId", -1) != -1) { - intentProfileId = extras.getInt("profileId", -1) - extras?.remove("profileId") - } - - if (extras?.getInt("fragmentId", -1) != -1) { - intentTargetId = extras.getInt("fragmentId", -1) - extras?.remove("fragmentId") - } + extras?.remove("profileId") + extras?.remove("fragmentId") + extras?.remove("reloadProfileId") /*if (intentTargetId == -1 && navController.currentDestination?.id == R.id.loadingFragment) { intentTargetId = navTarget.id }*/ - if (navLoading) { + if (navLoading) b.fragment.removeAllViews() - if (intentTargetId == -1) - intentTargetId = HOME_ID - } when { - app.profile.id == 0 -> { - if (intentProfileId == -1) - intentProfileId = app.config.lastProfileId - loadProfile(intentProfileId, intentTargetId, extras) - } - intentProfileId != -1 -> { - if (app.profile.id != intentProfileId) - loadProfile(intentProfileId, intentTargetId, extras) - else - loadTarget(intentTargetId, extras) - } - intentTargetId != -1 -> { - drawer.currentProfile = app.profile.id - if (navTargetId != intentTargetId || navLoading) - loadTarget(intentTargetId, extras) - } - else -> { - drawer.currentProfile = app.profile.id - } + app.profile.id == 0 -> navigate( + profileId = intentProfileId ?: app.config.lastProfileId, + navTarget = intentNavTarget, + args = extras, + ) + intentProfileId != null -> navigate( + profileId = intentProfileId, + navTarget = intentNavTarget, + args = extras, + ) + intentNavTarget != null -> navigate( + navTarget = intentNavTarget, + args = extras, + ) + navLoading -> navigate() + else -> drawer.currentProfile = app.profile.id } navLoading = false } override fun recreate() { - recreate(navTargetId) + recreate(navTarget) } - fun recreate(targetId: Int) { - recreate(targetId, null) + + fun recreate(navTarget: NavTarget) { + recreate(navTarget, null) } - fun recreate(targetId: Int? = null, arguments: Bundle? = null) { + + fun recreate(navTarget: NavTarget? = null, arguments: Bundle? = null) { val intent = Intent(this, MainActivity::class.java) if (arguments != null) intent.putExtras(arguments) - if (targetId != null) { - intent.putExtra("fragmentId", targetId) + if (navTarget != null) { + intent.putExtra("fragmentId", navTarget.id) } finish() overridePendingTransition(R.anim.fade_in, R.anim.fade_out) @@ -946,10 +819,12 @@ class MainActivity : AppCompatActivity(), CoroutineScope { d(TAG, "Activity started") super.onStart() } + override fun onStop() { d(TAG, "Activity stopped") super.onStop() } + override fun onResume() { d(TAG, "Activity resumed") val filter = IntentFilter() @@ -958,12 +833,14 @@ class MainActivity : AppCompatActivity(), CoroutineScope { EventBus.getDefault().register(this) super.onResume() } + override fun onPause() { d(TAG, "Activity paused") unregisterReceiver(intentReceiver) EventBus.getDefault().unregister(this) super.onPause() } + override fun onDestroy() { d(TAG, "Activity destroyed") super.onDestroy() @@ -971,13 +848,15 @@ class MainActivity : AppCompatActivity(), CoroutineScope { override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) - outState.putInt("fragmentId", navTargetId) + outState.putExtras("fragmentId" to navTarget) } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) handleIntent(intent?.extras) } + + @Suppress("deprecation") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) requestHandler.handleResult(requestCode, resultCode, data) @@ -990,110 +869,139 @@ class MainActivity : AppCompatActivity(), CoroutineScope { | |___| (_) | (_| | (_| | | | | | | | __/ |_| | | | (_) | (_| \__ \ |______\___/ \__,_|\__,_| |_| |_| |_|\___|\__|_| |_|\___/ \__,_|__*/ val navOptions = NavOptions.Builder() - .setEnterAnim(R.anim.task_open_enter) // new fragment enter - .setExitAnim(R.anim.task_open_exit) // old fragment exit - .setPopEnterAnim(R.anim.task_close_enter) // old fragment enter back - .setPopExitAnim(R.anim.task_close_exit) // new fragment exit - .build() + .setEnterAnim(R.anim.task_open_enter) // new fragment enter + .setExitAnim(R.anim.task_open_exit) // old fragment exit + .setPopEnterAnim(R.anim.task_close_enter) // old fragment enter back + .setPopExitAnim(R.anim.task_close_exit) // new fragment exit + .build() - fun loadProfile(id: Int) = loadProfile(id, navTargetId) - fun loadProfile(id: Int, arguments: Bundle?) = loadProfile(id, navTargetId, arguments) - fun loadProfile(profile: Profile) = loadProfile( - profile, - navTargetId, - null, - if (app.profile.archived) app.profile.id else null - ) - private fun loadProfile(id: Int, drawerSelection: Int, arguments: Bundle? = null) { - if (App.profileId == id) { - drawer.currentProfile = app.profile.id - loadTarget(drawerSelection, arguments) + private fun canNavigate(): Boolean = onBeforeNavigate?.invoke() != false + + fun resumePausedNavigation(): Boolean { + val data = pausedNavigationData ?: return false + navigate( + profileId = data.profileId, + navTarget = data.navTarget, + args = data.args, + skipBeforeNavigate = true, + ) + pausedNavigationData = null + return true + } + + fun navigate( + profileId: Int? = null, + profile: Profile? = null, + navTarget: NavTarget? = null, + args: Bundle? = null, + skipBeforeNavigate: Boolean = false, + ): Boolean { + d(TAG, "navigate(profileId = ${profile?.id ?: profileId}, target = ${navTarget?.name}, args = $args)") + if (!(skipBeforeNavigate || navTarget == this.navTarget) && !canNavigate()) { + bottomSheet.close() + drawer.close() + // restore the previous profile if changing it with the drawer + // well, it still does not change the toolbar profile image, + // but that's now NavView's problem, not mine. + drawer.currentProfile = App.profile.id + pausedNavigationData = PausedNavigationData(profileId, navTarget, args) + return false + } + + val loadNavTarget = navTarget ?: this.navTarget + if (profile != null && profile.id != App.profileId) { + navigateImpl(profile, loadNavTarget, args, profileChanged = true) + return true + } + if (profileId != null && profileId != App.profileId) { + app.profileLoad(profileId) { + navigateImpl(it, loadNavTarget, args, profileChanged = true) + } + return true + } + navigateImpl(App.profile, loadNavTarget, args, profileChanged = false) + return true + } + + private fun navigateImpl( + profile: Profile, + navTarget: NavTarget, + args: Bundle?, + profileChanged: Boolean, + ) { + d(TAG, "navigateImpl(profileId = ${profile.id}, target = ${navTarget.name}, args = $args)") + + if (navTarget.featureType != null && !profile.hasUIFeature(navTarget.featureType)) { + navigateImpl(profile, NavTarget.HOME, args, profileChanged) return } - val previousArchivedId = if (app.profile.archived) app.profile.id else null - app.profileLoad(id) { - loadProfile(it, drawerSelection, arguments, previousArchivedId) - } - } - private fun loadProfile(profile: Profile, drawerSelection: Int, arguments: Bundle?, previousArchivedId: Int?) { - App.profile = profile - MessagesFragment.pageSelection = -1 - setDrawerItems() + if (profileChanged) { + if (App.profileId != profile.id) + app.profileLoad(profile) + MessagesFragment.pageSelection = -1 + // set new drawer items for this profile + setDrawerItems() - if (previousArchivedId != null) { - // prevents accidentally removing the first item if the archived profile is not shown - drawer.removeProfileById(previousArchivedId) - } - if (profile.archived) { - drawer.prependProfile(Profile( + val previousArchivedId = if (app.profile.archived) app.profile.id else null + if (previousArchivedId != null) { + // prevents accidentally removing the first item if the archived profile is not shown + drawer.removeProfileById(previousArchivedId) + } + if (profile.archived) { + // add the same profile but with a different name + // (other fields are not needed by the drawer) + drawer.prependProfile(Profile( id = profile.id, loginStoreId = profile.loginStoreId, loginStoreType = profile.loginStoreType, name = profile.name, subname = "Archiwum - ${profile.subname}" - ).also { - it.archived = true - }) + ).also { + it.archived = true + }) + } + + // the drawer profile is updated automatically when the drawer item is clicked + // update it manually when switching profiles from other source + //if (drawer.currentProfile != app.profile.id) + drawer.currentProfile = App.profileId } - // the drawer profile is updated automatically when the drawer item is clicked - // update it manually when switching profiles from other source - //if (drawer.currentProfile != app.profile.id) - drawer.currentProfile = app.profileId - loadTarget(drawerSelection, arguments) - } - fun loadTarget(id: Int, arguments: Bundle? = null) { - var loadId = id - if (loadId == -1) { - loadId = DRAWER_ITEM_HOME - } - val target = navTargetList - .firstOrNull { it.id == loadId } - if (target == null) { - Toast.makeText(this, getString(R.string.error_invalid_fragment, id), Toast.LENGTH_LONG).show() - loadTarget(navTargetList.first(), arguments) - } - else { - loadTarget(target, arguments) - } - } - private fun loadTarget(target: NavTarget, args: Bundle? = null) { - d("NavDebug", "loadTarget(target = $target, args = $args)") - - val arguments = args ?: navBackStack.firstOrNull { it.first.id == target.id }?.second ?: Bundle() + val arguments = args + ?: navBackStack.firstOrNull { it.first == navTarget }?.second + ?: Bundle() bottomSheet.close() bottomSheet.removeAllContextual() bottomSheet.toggleGroupEnabled = false drawer.close() - if (drawer.getSelection() != target.id) - drawer.setSelection(target.id, fireOnClick = false) - navView.toolbar.setTitle(target.title ?: target.name) + 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) - d("NavDebug", "Navigating from ${navTarget.fragmentClass?.java?.simpleName} to ${target.fragmentClass?.java?.simpleName}") + d("NavDebug", "Navigating from ${this.navTarget.name} to ${navTarget.name}") - val fragment = target.fragmentClass?.java?.newInstance() ?: return + val fragment = navTarget.fragmentClass?.newInstance() ?: return fragment.arguments = arguments val transaction = fragmentManager.beginTransaction() - if (navTarget == target) { + if (navTarget == this.navTarget) { // just reload the current target transaction.setCustomAnimations( - R.anim.fade_in, - R.anim.fade_out + R.anim.fade_in, + R.anim.fade_out ) - } - else { - navBackStack.keys().lastIndexOf(target).let { + } else { + navBackStack.keys().lastIndexOf(navTarget).let { if (it == -1) - return@let target + return@let navTarget // pop the back stack up until that target transaction.setCustomAnimations( - R.anim.task_close_enter, - R.anim.task_close_exit + R.anim.task_close_enter, + R.anim.task_close_exit ) // navigating grades_add -> grades @@ -1108,24 +1016,24 @@ class MainActivity : AppCompatActivity(), CoroutineScope { for (i in 0 until popCount) { navBackStack.removeAt(navBackStack.lastIndex) } - navTarget = target - navArguments = arguments + this.navTarget = navTarget + this.navArguments = arguments return@let null }?.let { // target is neither current nor in the back stack // so navigate to it transaction.setCustomAnimations( - R.anim.task_open_enter, - R.anim.task_open_exit + R.anim.task_open_enter, + R.anim.task_open_exit ) - navBackStack.add(navTarget to navArguments) - navTarget = target - navArguments = arguments + navBackStack.add(this.navTarget to this.navArguments) + this.navTarget = navTarget + this.navArguments = arguments } } - if (navTarget.popToHome) { + if (navTarget.popTo == NavTarget.HOME) { // if the current has popToHome, let only home be in the back stack // probably `if (navTarget.popToHome)` in popBackStack() is not needed now val popCount = navBackStack.size - 1 @@ -1134,9 +1042,9 @@ class MainActivity : AppCompatActivity(), CoroutineScope { } } - d("NavDebug", "Current fragment ${navTarget.fragmentClass?.java?.simpleName}, pop to home ${navTarget.popToHome}, back stack:") - navBackStack.forEachIndexed { index, target2 -> - d("NavDebug", " - $index: ${target2.first.fragmentClass?.java?.simpleName}") + d("NavDebug", "Current fragment ${navTarget.name}, back stack:") + navBackStack.forEachIndexed { index, item -> + d("NavDebug", " - $index: ${item.first.name}") } transaction.replace(R.id.fragment, fragment) @@ -1145,39 +1053,47 @@ class MainActivity : AppCompatActivity(), CoroutineScope { // TASK DESCRIPTION if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { val bm = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher) + + @Suppress("deprecation") val taskDesc = ActivityManager.TaskDescription( - if (target.id == HOME_ID) getString(R.string.app_name) else getString(R.string.app_task_format, getString(target.name)), - bm, - getColorFromAttr(this, R.attr.colorSurface) + 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 } - fun reloadTarget() = loadTarget(navTarget) - private fun popBackStack(): Boolean { + fun reloadTarget() = navigate() + + private fun popBackStack(skipBeforeNavigate: Boolean = false): Boolean { if (navBackStack.size == 0) { return false } // TODO back stack argument support - when { - navTarget.popToHome -> { - loadTarget(HOME_ID) - } - navTarget.popTo != null -> { - loadTarget(navTarget.popTo ?: HOME_ID) - } - else -> { - navBackStack.last().let { - loadTarget(it.first, it.second) - } + if (navTarget.popTo != null) { + navigate( + navTarget = navTarget.popTo, + skipBeforeNavigate = skipBeforeNavigate, + ) + } else { + navBackStack.last().let { + navigate( + navTarget = it.first, + args = it.second, + skipBeforeNavigate = skipBeforeNavigate, + ) } } return true } - fun navigateUp() { - if (!popBackStack()) { + + fun navigateUp(skipBeforeNavigate: Boolean = false) { + if (!popBackStack(skipBeforeNavigate)) { super.onBackPressed() } } @@ -1226,25 +1142,31 @@ class MainActivity : AppCompatActivity(), CoroutineScope { | |__| | | | (_| |\ V V / __/ | | | || __/ | | | | \__ \ |_____/|_| \__,_| \_/\_/ \___|_| |_|\__\___|_| |_| |_|__*/ private fun createDrawerItem(target: NavTarget, level: Int = 1): IDrawerItem<*> { - val item = DrawerPrimaryItem() - .withIdentifier(target.id.toLong()) - .withName(target.name) - .withIsHiddenInMiniDrawer(!app.config.ui.miniMenuButtons.contains(target.id)) - .also { if (target.description != null) it.withDescription(target.description!!) } - .also { if (target.icon != null) it.withIcon(target.icon!!) } - .also { if (target.title != null) it.withAppTitle(getString(target.title!!)) } - .also { if (target.badgeTypeId != null) it.withBadgeStyle(drawer.badgeStyle)} - .withSelectedBackgroundAnimated(false) + val item = when { + // target.subItems != null -> ExpandableDrawerItem() + level > 1 -> SecondaryDrawerItem() + else -> DrawerPrimaryItem() + } - if (target.badgeTypeId != null) - drawer.addUnreadCounterType(target.badgeTypeId!!, target.id) - // TODO sub items - /* - if (target.subItems != null) { - for (subItem in target.subItems!!) { - item.subItems += createDrawerItem(subItem, level+1) - } - }*/ + item.also { + it.identifier = target.id.toLong() + it.nameRes = target.nameRes + it.descriptionRes = target.descriptionRes ?: -1 + it.icon = target.icon?.toImageHolder() + it.hiddenInMiniDrawer = !app.config.ui.miniMenuButtons.contains(target) + if (it is DrawerPrimaryItem) + it.appTitle = target.titleRes?.resolveString(this) + if (/* it is ColorfulBadgeable && */ target.badgeType != null) + it.badgeStyle = drawer.badgeStyle + it.isSelectedBackgroundAnimated = false + it.level = level + } + if (target.badgeType != null) + drawer.addUnreadCounterType(target.badgeType.id, target.id) + + /* item.subItems = target.subItems?.map { + createDrawerItem(it, level + 1) + }?.toMutableList() ?: mutableListOf() */ return item } @@ -1252,62 +1174,75 @@ class MainActivity : AppCompatActivity(), CoroutineScope { fun setDrawerItems() { d("NavDebug", "setDrawerItems() app.profile = ${app.profile}") val drawerItems = arrayListOf>() + val drawerItemsMore = arrayListOf>() + val drawerItemsBottom = arrayListOf>() val drawerProfiles = arrayListOf() - val supportedFragments = app.profile.supportedFragments + for (target in NavTarget.values()) { + if (target.devModeOnly && !App.devMode) + continue + if (target.featureType != null && !app.profile.hasUIFeature(target.featureType)) + continue - targetPopToHomeList.clear() - - var separatorAdded = false - - for (target in navTargetList) { - if (target.isInDrawer && target.isBelowSeparator && !separatorAdded) { - separatorAdded = true - drawerItems += DividerDrawerItem() - } - - if (target.popToHome) - targetPopToHomeList += target.id - - if (target.isInDrawer && (target.isStatic || supportedFragments.isEmpty() || supportedFragments.contains(target.id))) { - drawerItems += createDrawerItem(target) - if (target.id == 1) { - targetHomeId = target.id + when (target.location) { + NavTargetLocation.DRAWER -> { + drawerItems += createDrawerItem(target, level = 1) } - } - - if (target.isInProfileList) { - drawerProfiles += ProfileSettingDrawerItem() - .withIdentifier(target.id.toLong()) - .withName(target.name) - .also { if (target.description != null) it.withDescription(target.description!!) } - .also { if (target.icon != null) it.withIcon(target.icon!!) } + NavTargetLocation.DRAWER_MORE -> { + drawerItemsMore += createDrawerItem(target, level = 2) + } + NavTargetLocation.DRAWER_BOTTOM -> { + drawerItemsBottom += createDrawerItem(target, level = 1) + } + NavTargetLocation.PROFILE_LIST -> { + drawerProfiles += ProfileSettingDrawerItem().also { + it.identifier = target.id.toLong() + it.nameRes = target.nameRes + it.descriptionRes = target.descriptionRes ?: -1 + it.icon = target.icon?.toImageHolder() + } + } + else -> continue } } + drawerItems += ExpandableDrawerItem().also { + it.identifier = -1L + it.nameRes = R.string.menu_more + it.icon = CommunityMaterial.Icon.cmd_dots_horizontal.toImageHolder() + it.subItems = drawerItemsMore.toMutableList() + it.isSelectedBackgroundAnimated = false + it.isSelectable = false + } + drawerItems += DividerDrawerItem() + drawerItems += drawerItemsBottom + // seems that this cannot be open, because the itemAdapter has Profile items // instead of normal Drawer items... drawer.profileSelectionClose() - drawer.setItems(*drawerItems.toTypedArray()) drawer.removeAllProfileSettings() drawer.addProfileSettings(*drawerProfiles.toTypedArray()) } - private val targetPopToHomeList = arrayListOf() - private var targetHomeId: Int = -1 override fun onBackPressed() { - if (!b.navView.onBackPressed()) { - if (App.config.ui.openDrawerOnBackPressed && ((navTarget.popTo == null && navTarget.popToHome) - || navTarget.id == DRAWER_ITEM_HOME)) { - b.navView.drawer.toggle() - } else { + if (App.config.ui.openDrawerOnBackPressed) { + if (drawer.isOpen) + navigateUp() + else if (!navView.onBackPressed()) + drawer.open() + } else { + if (!navView.onBackPressed()) navigateUp() - } } } fun error(error: ApiError) = errorSnackbar.addError(error).show() - fun snackbar(text: String, actionText: String? = null, onClick: (() -> Unit)? = null) = mainSnackbar.snackbar(text, actionText, onClick) + fun snackbar( + text: String, + actionText: String? = null, + onClick: (() -> Unit)? = null, + ) = mainSnackbar.snackbar(text, actionText, onClick) + fun snackbarDismiss() = mainSnackbar.dismiss() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivityRequestHandler.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivityRequestHandler.kt index a39a5872..ff165a46 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivityRequestHandler.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivityRequestHandler.kt @@ -11,7 +11,7 @@ import android.provider.OpenableColumns import com.canhub.cropper.CropImage import com.canhub.cropper.CropImageView import pl.szczodrzynski.edziennik.data.db.entity.Profile -import pl.szczodrzynski.edziennik.ui.modules.login.LoginActivity +import pl.szczodrzynski.edziennik.ui.login.LoginActivity import java.io.File import java.io.FileOutputStream diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/AbstractConfig.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/AbstractConfig.kt deleted file mode 100644 index d597a119..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/AbstractConfig.kt +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2019-11-27. - */ - -package pl.szczodrzynski.edziennik.config - -interface AbstractConfig { - fun set(key: String, value: String?) -} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/AppData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/AppData.kt new file mode 100644 index 00000000..a0fa7c27 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/AppData.kt @@ -0,0 +1,70 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-21. + */ + +package pl.szczodrzynski.edziennik.config + +import com.google.gson.Gson +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import com.google.gson.stream.JsonReader +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.enums.LoginType +import pl.szczodrzynski.edziennik.ext.getJsonObject +import pl.szczodrzynski.edziennik.ext.mergeWith +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.HtmlMode + +data class AppData( + val configOverrides: Map, + val messagesConfig: MessagesConfig, + val uiConfig: UIConfig, + val eventTypes: List, +) { + companion object { + private var data: JsonObject? = null + private val appData = mutableMapOf() + + fun read(app: App) { + val res = app.resources.openRawResource(R.raw.app_data) + data = JsonParser.parseReader(JsonReader(res.reader())).asJsonObject + } + + fun get(loginType: LoginType): AppData { + if (loginType in appData) + return appData.getValue(loginType) + val json = data?.getJsonObject("base")?.deepCopy() + ?: throw NoSuchElementException("Base data not found") + val overrides = setOf(loginType, loginType.schoolType) + for (overrideType in overrides) { + val override = data?.getJsonObject(overrideType.name.lowercase()) ?: continue + json.mergeWith(override) + } + val value = Gson().fromJson(json, AppData::class.java) + appData[loginType] = value + return value + } + } + + data class MessagesConfig( + val subjectLength: Int?, + val bodyLength: Int?, + val textStyling: Boolean, + val syncRecipientList: Boolean, + val htmlMode: HtmlMode, + val needsReadStatus: Boolean, + ) + + data class UIConfig( + val lessonHeight: Int, + val enableMarkAsReadAnnouncements: Boolean, + val enableNoticePoints: Boolean, + val eventManualShowSubjectDropdown: Boolean, + ) + + data class EventType( + val id: Long, + val color: String, + val name: String, + ) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/BaseConfig.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/BaseConfig.kt new file mode 100644 index 00000000..76d95b0b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/BaseConfig.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-27. + */ + +package pl.szczodrzynski.edziennik.config + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import pl.szczodrzynski.edziennik.config.db.ConfigEntry +import pl.szczodrzynski.edziennik.data.db.AppDb +import pl.szczodrzynski.edziennik.ext.takePositive +import kotlin.coroutines.CoroutineContext + +abstract class BaseConfig( + @Transient + val db: AppDb, + val profileId: Int? = null, + protected var entries: List? = null, +) : CoroutineScope { + + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Default + + val values = hashMapOf() + + init { + if (entries == null) + entries = db.configDao().getAllNow() + values.clear() + for ((profileId, key, value) in entries!!) { + if (profileId.takePositive() != this.profileId) + continue + values[key] = value + } + } + + fun set(key: String, value: String?) { + values[key] = value + launch(Dispatchers.IO) { + db.configDao().add(ConfigEntry(profileId ?: -1, key, value)) + } + } + + fun has(key: String) = values.containsKey(key) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/Config.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/Config.kt index 160deb23..2042e8b2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/Config.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/Config.kt @@ -5,144 +5,59 @@ package pl.szczodrzynski.edziennik.config import com.google.gson.JsonObject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.BuildConfig -import pl.szczodrzynski.edziennik.config.db.ConfigEntry -import pl.szczodrzynski.edziennik.config.utils.ConfigMigration -import pl.szczodrzynski.edziennik.config.utils.get -import pl.szczodrzynski.edziennik.config.utils.set -import pl.szczodrzynski.edziennik.config.utils.toHashMap +import pl.szczodrzynski.edziennik.config.utils.* import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update import pl.szczodrzynski.edziennik.data.db.AppDb -import kotlin.coroutines.CoroutineContext -class Config(val db: AppDb) : CoroutineScope, AbstractConfig { +@Suppress("RemoveExplicitTypeArguments") +class Config(db: AppDb) : BaseConfig(db) { companion object { const val DATA_VERSION = 12 } - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Default - - val values: HashMap = hashMapOf() + private val profileConfigs: HashMap = hashMapOf() val ui by lazy { ConfigUI(this) } val sync by lazy { ConfigSync(this) } val timetable by lazy { ConfigTimetable(this) } val grades by lazy { ConfigGrades(this) } - private var mDataVersion: Int? = null - var dataVersion: Int - get() { mDataVersion = mDataVersion ?: values.get("dataVersion", 0); return mDataVersion ?: 0 } - set(value) { set("dataVersion", value); mDataVersion = value } + var dataVersion by config(DATA_VERSION) + var hash by config("") - private var mHash: String? = null - var hash: String - get() { mHash = mHash ?: values.get("hash", ""); return mHash ?: "" } - set(value) { set("hash", value); mHash = value } + var lastProfileId by config(0) + var loginFinished by config(false) + var privacyPolicyAccepted by config(false) + var update by config(null) + var updatesChannel by config("release") - private var mLastProfileId: Int? = null - var lastProfileId: Int - get() { mLastProfileId = mLastProfileId ?: values.get("lastProfileId", 0); return mLastProfileId ?: 0 } - set(value) { set("lastProfileId", value); mLastProfileId = value } + var devMode by config("debugMode", null) + var devModePassword by config(null) + var enableChucker by config(null) - private var mUpdatesChannel: String? = null - var updatesChannel: String - get() { mUpdatesChannel = mUpdatesChannel ?: values.get("updatesChannel", "release"); return mUpdatesChannel ?: "release" } - set(value) { set("updatesChannel", value); mUpdatesChannel = value } - private var mUpdate: Update? = null - var update: Update? - get() { mUpdate = mUpdate ?: values.get("update", null as Update?); return mUpdate ?: null as Update? } - set(value) { set("update", value); mUpdate = value } + var apiAvailabilityCheck by config(true) + var apiInvalidCert by config(null) + var apiKeyCustom by config(null) + var appInstalledTime by config(0L) + var appRateSnackbarTime by config(0L) + var appVersion by config(BuildConfig.VERSION_CODE) + var validation by config(null, "buildValidation") - private var mAppVersion: Int? = null - var appVersion: Int - get() { mAppVersion = mAppVersion ?: values.get("appVersion", BuildConfig.VERSION_CODE); return mAppVersion ?: BuildConfig.VERSION_CODE } - set(value) { set("appVersion", value); mAppVersion = value } + var archiverEnabled by config(true) + var runSync by config(false) + var widgetConfigs by config { JsonObject() } - private var mLoginFinished: Boolean? = null - var loginFinished: Boolean - get() { mLoginFinished = mLoginFinished ?: values.get("loginFinished", false); return mLoginFinished ?: false } - set(value) { set("loginFinished", value); mLoginFinished = value } - - private var mPrivacyPolicyAccepted: Boolean? = null - var privacyPolicyAccepted: Boolean - get() { mPrivacyPolicyAccepted = mPrivacyPolicyAccepted ?: values.get("privacyPolicyAccepted", false); return mPrivacyPolicyAccepted ?: false } - set(value) { set("privacyPolicyAccepted", value); mPrivacyPolicyAccepted = value } - - private var mDebugMode: Boolean? = null - var debugMode: Boolean - get() { mDebugMode = mDebugMode ?: values.get("debugMode", false); return mDebugMode ?: false } - set(value) { set("debugMode", value); mDebugMode = value } - - private var mDevModePassword: String? = null - var devModePassword: String? - get() { mDevModePassword = mDevModePassword ?: values.get("devModePassword", null as String?); return mDevModePassword } - set(value) { set("devModePassword", value); mDevModePassword = value } - - private var mAppInstalledTime: Long? = null - var appInstalledTime: Long - get() { mAppInstalledTime = mAppInstalledTime ?: values.get("appInstalledTime", 0L); return mAppInstalledTime ?: 0L } - set(value) { set("appInstalledTime", value); mAppInstalledTime = value } - - private var mAppRateSnackbarTime: Long? = null - var appRateSnackbarTime: Long - get() { mAppRateSnackbarTime = mAppRateSnackbarTime ?: values.get("appRateSnackbarTime", 0L); return mAppRateSnackbarTime ?: 0L } - set(value) { set("appRateSnackbarTime", value); mAppRateSnackbarTime = value } - - private var mRunSync: Boolean? = null - var runSync: Boolean - get() { mRunSync = mRunSync ?: values.get("runSync", false); return mRunSync ?: false } - set(value) { set("runSync", value); mRunSync = value } - - private var mWidgetConfigs: JsonObject? = null - var widgetConfigs: JsonObject - get() { mWidgetConfigs = mWidgetConfigs ?: values.get("widgetConfigs", JsonObject()); return mWidgetConfigs ?: JsonObject() } - set(value) { set("widgetConfigs", value); mWidgetConfigs = value } - - private var mArchiverEnabled: Boolean? = null - var archiverEnabled: Boolean - get() { mArchiverEnabled = mArchiverEnabled ?: values.get("archiverEnabled", true); return mArchiverEnabled ?: true } - set(value) { set("archiverEnabled", value); mArchiverEnabled = value } - - private var mValidation: String? = null - var validation: String? - get() { mValidation = mValidation ?: values["buildValidation"]; return mValidation } - set(value) { set("buildValidation", value); mValidation = value } - - private var mApiInvalidCert: String? = null - var apiInvalidCert: String? - get() { mApiInvalidCert = mApiInvalidCert ?: values["apiInvalidCert"]; return mApiInvalidCert } - set(value) { set("apiInvalidCert", value); mApiInvalidCert = value } - - private var rawEntries: List = db.configDao().getAllNow() - private val profileConfigs: HashMap = hashMapOf() - init { - rawEntries.toHashMap(-1, values) - } fun migrate(app: App) { - if (dataVersion < DATA_VERSION) + if (dataVersion < DATA_VERSION || hash == "") + // migrate old data version OR freshly installed app (or updated from 3.x) ConfigMigration(app, this) } - fun getFor(profileId: Int): ProfileConfig { - return profileConfigs[profileId] ?: ProfileConfig(db, profileId, db.configDao().getAllNow(profileId)).also { + + operator fun get(profileId: Int): ProfileConfig { + return profileConfigs[profileId] ?: ProfileConfig(db, profileId, entries).also { profileConfigs[profileId] = it } } - fun forProfile() = getFor(App.profileId) - - fun setProfile(profileId: Int) { - } - - override fun set(key: String, value: String?) { - values[key] = value - launch { - db.configDao().add(ConfigEntry(-1, key, value)) - } - } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigGrades.kt index 267ec322..a28dcc3d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigGrades.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigGrades.kt @@ -4,13 +4,10 @@ package pl.szczodrzynski.edziennik.config -import pl.szczodrzynski.edziennik.config.utils.get -import pl.szczodrzynski.edziennik.config.utils.set -import pl.szczodrzynski.edziennik.utils.managers.GradesManager +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.ORDER_BY_DATE_DESC -class ConfigGrades(private val config: Config) { - private var mOrderBy: Int? = null - var orderBy: Int - get() { mOrderBy = mOrderBy ?: config.values.get("gradesOrderBy", 0); return mOrderBy ?: GradesManager.ORDER_BY_DATE_DESC } - set(value) { config.set("gradesOrderBy", value); mOrderBy = value } +@Suppress("RemoveExplicitTypeArguments") +class ConfigGrades(base: Config) { + + var orderBy by base.config("gradesOrderBy", ORDER_BY_DATE_DESC) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt index 068400a1..6688838b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigSync.kt @@ -4,125 +4,53 @@ package pl.szczodrzynski.edziennik.config -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import pl.szczodrzynski.edziennik.config.utils.get -import pl.szczodrzynski.edziennik.config.utils.getIntList -import pl.szczodrzynski.edziennik.config.utils.set -import pl.szczodrzynski.edziennik.config.utils.setMap +import pl.szczodrzynski.edziennik.BuildConfig import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus +import pl.szczodrzynski.edziennik.ext.HOUR import pl.szczodrzynski.edziennik.utils.models.Time -class ConfigSync(private val config: Config) { - private val gson = Gson() +@Suppress("RemoveExplicitTypeArguments") +class ConfigSync(base: Config) { - private var mDontShowAppManagerDialog: Boolean? = null - var dontShowAppManagerDialog: Boolean - get() { mDontShowAppManagerDialog = mDontShowAppManagerDialog ?: config.values.get("dontShowAppManagerDialog", false); return mDontShowAppManagerDialog ?: false } - set(value) { config.set("dontShowAppManagerDialog", value); mDontShowAppManagerDialog = value } + var enabled by base.config("syncEnabled", true) + var interval by base.config("syncInterval", 1 * HOUR.toInt()) + var onlyWifi by base.config("syncOnlyWifi", false) - private var mSyncEnabled: Boolean? = null - var enabled: Boolean - get() { mSyncEnabled = mSyncEnabled ?: config.values.get("syncEnabled", true); return mSyncEnabled ?: true } - set(value) { config.set("syncEnabled", value); mSyncEnabled = value } + var dontShowAppManagerDialog by base.config(false) + var lastAppSync by base.config(0L) + var notifyAboutUpdates by base.config(true) + var webPushEnabled by base.config(true) - private var mWebPushEnabled: Boolean? = null - var webPushEnabled: Boolean - get() { mWebPushEnabled = mWebPushEnabled ?: config.values.get("webPushEnabled", true); return mWebPushEnabled ?: true } - set(value) { config.set("webPushEnabled", value); mWebPushEnabled = value } + // Quiet Hours + var quietHoursEnabled by base.config(false) + var quietHoursStart by base.config(null) + var quietHoursEnd by base.config(null) + var quietDuringLessons by base.config(false) - private var mSyncOnlyWifi: Boolean? = null - var onlyWifi: Boolean - get() { mSyncOnlyWifi = mSyncOnlyWifi ?: config.values.get("syncOnlyWifi", false); return mSyncOnlyWifi ?: notifyAboutUpdates } - set(value) { config.set("syncOnlyWifi", value); mSyncOnlyWifi = value } + // FCM Tokens + var tokenApp by base.config(null) + var tokenMobidziennik by base.config(null) + var tokenLibrus by base.config(null) + var tokenVulcan by base.config(null) + var tokenVulcanHebe by base.config(null) - private var mSyncInterval: Int? = null - var interval: Int - get() { mSyncInterval = mSyncInterval ?: config.values.get("syncInterval", 60*60); return mSyncInterval ?: 60*60 } - set(value) { config.set("syncInterval", value); mSyncInterval = value } + var tokenMobidziennikList by base.config> { listOf() } + var tokenLibrusList by base.config> { listOf() } + var tokenVulcanList by base.config> { listOf() } + var tokenVulcanHebeList by base.config> { listOf() } - private var mNotifyAboutUpdates: Boolean? = null - var notifyAboutUpdates: Boolean - get() { mNotifyAboutUpdates = mNotifyAboutUpdates ?: config.values.get("notifyAboutUpdates", true); return mNotifyAboutUpdates ?: true } - set(value) { config.set("notifyAboutUpdates", value); mNotifyAboutUpdates = value } + // Register Availability + private var registerAvailabilityMap by base.config>("registerAvailability") { mapOf() } + private var registerAvailabilityFlavor by base.config(null) - private var mLastAppSync: Long? = null - var lastAppSync: Long - get() { mLastAppSync = mLastAppSync ?: config.values.get("lastAppSync", 0L); return mLastAppSync ?: 0L } - set(value) { config.set("lastAppSync", value); mLastAppSync = value } - - /* ____ _ _ _ - / __ \ (_) | | | | - | | | |_ _ _ ___| |_ | |__ ___ _ _ _ __ ___ - | | | | | | | |/ _ \ __| | '_ \ / _ \| | | | '__/ __| - | |__| | |_| | | __/ |_ | | | | (_) | |_| | | \__ \ - \___\_\\__,_|_|\___|\__| |_| |_|\___/ \__,_|_| |__*/ - private var mQuietHoursEnabled: Boolean? = null - var quietHoursEnabled: Boolean - get() { mQuietHoursEnabled = mQuietHoursEnabled ?: config.values.get("quietHoursEnabled", false); return mQuietHoursEnabled ?: false } - set(value) { config.set("quietHoursEnabled", value); mQuietHoursEnabled = value } - - private var mQuietHoursStart: Time? = null - var quietHoursStart: Time? - get() { mQuietHoursStart = mQuietHoursStart ?: config.values.get("quietHoursStart", null as Time?); return mQuietHoursStart } - set(value) { config.set("quietHoursStart", value); mQuietHoursStart = value } - - private var mQuietHoursEnd: Time? = null - var quietHoursEnd: Time? - get() { mQuietHoursEnd = mQuietHoursEnd ?: config.values.get("quietHoursEnd", null as Time?); return mQuietHoursEnd } - set(value) { config.set("quietHoursEnd", value); mQuietHoursEnd = value } - - private var mQuietDuringLessons: Boolean? = null - var quietDuringLessons: Boolean - get() { mQuietDuringLessons = mQuietDuringLessons ?: config.values.get("quietDuringLessons", false); return mQuietDuringLessons ?: false } - set(value) { config.set("quietDuringLessons", value); mQuietDuringLessons = value } - - /* ______ _____ __ __ _______ _ - | ____/ ____| \/ | |__ __| | | - | |__ | | | \ / | | | ___ | | _____ _ __ ___ - | __|| | | |\/| | | |/ _ \| |/ / _ \ '_ \/ __| - | | | |____| | | | | | (_) | < __/ | | \__ \ - |_| \_____|_| |_| |_|\___/|_|\_\___|_| |_|__*/ - private var mTokenApp: String? = null - var tokenApp: String? - get() { mTokenApp = mTokenApp ?: config.values.get("tokenApp", null as String?); return mTokenApp } - set(value) { config.set("tokenApp", value); mTokenApp = value } - private var mTokenMobidziennik: String? = null - var tokenMobidziennik: String? - get() { mTokenMobidziennik = mTokenMobidziennik ?: config.values.get("tokenMobidziennik", null as String?); return mTokenMobidziennik } - set(value) { config.set("tokenMobidziennik", value); mTokenMobidziennik = value } - private var mTokenLibrus: String? = null - var tokenLibrus: String? - get() { mTokenLibrus = mTokenLibrus ?: config.values.get("tokenLibrus", null as String?); return mTokenLibrus } - set(value) { config.set("tokenLibrus", value); mTokenLibrus = value } - private var mTokenVulcan: String? = null - var tokenVulcan: String? - get() { mTokenVulcan = mTokenVulcan ?: config.values.get("tokenVulcan", null as String?); return mTokenVulcan } - set(value) { config.set("tokenVulcan", value); mTokenVulcan = value } - private var mTokenVulcanHebe: String? = null - var tokenVulcanHebe: String? - get() { mTokenVulcanHebe = mTokenVulcanHebe ?: config.values.get("tokenVulcanHebe", null as String?); return mTokenVulcanHebe } - set(value) { config.set("tokenVulcanHebe", value); mTokenVulcanHebe = value } - - private var mTokenMobidziennikList: List? = null - var tokenMobidziennikList: List - get() { mTokenMobidziennikList = mTokenMobidziennikList ?: config.values.getIntList("tokenMobidziennikList", listOf()); return mTokenMobidziennikList ?: listOf() } - set(value) { config.set("tokenMobidziennikList", value); mTokenMobidziennikList = value } - private var mTokenLibrusList: List? = null - var tokenLibrusList: List - get() { mTokenLibrusList = mTokenLibrusList ?: config.values.getIntList("tokenLibrusList", listOf()); return mTokenLibrusList ?: listOf() } - set(value) { config.set("tokenLibrusList", value); mTokenLibrusList = value } - private var mTokenVulcanList: List? = null - var tokenVulcanList: List - get() { mTokenVulcanList = mTokenVulcanList ?: config.values.getIntList("tokenVulcanList", listOf()); return mTokenVulcanList ?: listOf() } - set(value) { config.set("tokenVulcanList", value); mTokenVulcanList = value } - private var mTokenVulcanHebeList: List? = null - var tokenVulcanHebeList: List - get() { mTokenVulcanHebeList = mTokenVulcanHebeList ?: config.values.getIntList("tokenVulcanHebeList", listOf()); return mTokenVulcanHebeList ?: listOf() } - set(value) { config.set("tokenVulcanHebeList", value); mTokenVulcanHebeList = value } - - private var mRegisterAvailability: Map? = null var registerAvailability: Map - get() { mRegisterAvailability = mRegisterAvailability ?: config.values.get("registerAvailability", null as String?)?.let { it -> gson.fromJson>(it, object: TypeToken>(){}.type) }; return mRegisterAvailability ?: mapOf() } - set(value) { config.setMap("registerAvailability", value); mRegisterAvailability = value } + get() { + if (BuildConfig.FLAVOR != registerAvailabilityFlavor) + return mapOf() + return registerAvailabilityMap + } + set(value) { + registerAvailabilityMap = value + registerAvailabilityFlavor = BuildConfig.FLAVOR + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigTimetable.kt index 2418f1d3..9f60534b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigTimetable.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigTimetable.kt @@ -4,23 +4,12 @@ package pl.szczodrzynski.edziennik.config -import pl.szczodrzynski.edziennik.config.utils.get -import pl.szczodrzynski.edziennik.config.utils.set import pl.szczodrzynski.edziennik.utils.models.Time -class ConfigTimetable(private val config: Config) { - private var mBellSyncMultiplier: Int? = null - var bellSyncMultiplier: Int - get() { mBellSyncMultiplier = mBellSyncMultiplier ?: config.values.get("bellSyncMultiplier", 0); return mBellSyncMultiplier ?: 0 } - set(value) { config.set("bellSyncMultiplier", value); mBellSyncMultiplier = value } +@Suppress("RemoveExplicitTypeArguments") +class ConfigTimetable(base: Config) { - private var mBellSyncDiff: Time? = null - var bellSyncDiff: Time? - get() { mBellSyncDiff = mBellSyncDiff ?: config.values.get("bellSyncDiff", null as Time?); return mBellSyncDiff } - set(value) { config.set("bellSyncDiff", value); mBellSyncDiff = value } - - private var mCountInSeconds: Boolean? = null - var countInSeconds: Boolean - get() { mCountInSeconds = mCountInSeconds ?: config.values.get("countInSeconds", false); return mCountInSeconds ?: false } - set(value) { config.set("countInSeconds", value); mCountInSeconds = value } -} \ No newline at end of file + var bellSyncMultiplier by base.config(0) + var bellSyncDiff by base.config(null) + var countInSeconds by base.config(false) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigUI.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigUI.kt index 7b39383e..a8a6ba33 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigUI.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ConfigUI.kt @@ -4,53 +4,32 @@ package pl.szczodrzynski.edziennik.config -import pl.szczodrzynski.edziennik.config.utils.get -import pl.szczodrzynski.edziennik.config.utils.getIntList -import pl.szczodrzynski.edziennik.config.utils.set +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget -class ConfigUI(private val config: Config) { - private var mTheme: Int? = null - var theme: Int - get() { mTheme = mTheme ?: config.values.get("theme", 1); return mTheme ?: 1 } - set(value) { config.set("theme", value); mTheme = value } +@Suppress("RemoveExplicitTypeArguments") +class ConfigUI(base: Config) { - private var mLanguage: String? = null - var language: String? - get() { mLanguage = mLanguage ?: config.values.get("language", null as String?); return mLanguage } - set(value) { config.set("language", value); mLanguage = value } + var theme by base.config(1) + var language by base.config(null) - private var mHeaderBackground: String? = null - var headerBackground: String? - get() { mHeaderBackground = mHeaderBackground ?: config.values.get("headerBg", null as String?); return mHeaderBackground } - set(value) { config.set("headerBg", value); mHeaderBackground = value } + var appBackground by base.config("appBg", null) + var headerBackground by base.config("headerBg", null) - private var mAppBackground: String? = null - var appBackground: String? - get() { mAppBackground = mAppBackground ?: config.values.get("appBg", null as String?); return mAppBackground } - set(value) { config.set("appBg", value); mAppBackground = value } + var miniMenuVisible by base.config(false) + var miniMenuButtons by base.config> { + setOf( + NavTarget.HOME, + NavTarget.TIMETABLE, + NavTarget.AGENDA, + NavTarget.GRADES, + NavTarget.MESSAGES, + NavTarget.HOMEWORK, + NavTarget.SETTINGS + ) + } + var openDrawerOnBackPressed by base.config(false) - private var mMiniMenuVisible: Boolean? = null - var miniMenuVisible: Boolean - get() { mMiniMenuVisible = mMiniMenuVisible ?: config.values.get("miniMenuVisible", false); return mMiniMenuVisible ?: false } - set(value) { config.set("miniMenuVisible", value); mMiniMenuVisible = value } - - private var mMiniMenuButtons: List? = null - var miniMenuButtons: List - get() { mMiniMenuButtons = mMiniMenuButtons ?: config.values.getIntList("miniMenuButtons", listOf()); return mMiniMenuButtons ?: listOf() } - set(value) { config.set("miniMenuButtons", value); mMiniMenuButtons = value } - - private var mOpenDrawerOnBackPressed: Boolean? = null - var openDrawerOnBackPressed: Boolean - get() { mOpenDrawerOnBackPressed = mOpenDrawerOnBackPressed ?: config.values.get("openDrawerOnBackPressed", false); return mOpenDrawerOnBackPressed ?: false } - set(value) { config.set("openDrawerOnBackPressed", value); mOpenDrawerOnBackPressed = value } - - private var mSnowfall: Boolean? = null - var snowfall: Boolean - get() { mSnowfall = mSnowfall ?: config.values.get("snowfall", false); return mSnowfall ?: false } - set(value) { config.set("snowfall", value); mSnowfall = value } - - private var mBottomSheetOpened: Boolean? = null - var bottomSheetOpened: Boolean - get() { mBottomSheetOpened = mBottomSheetOpened ?: config.values.get("bottomSheetOpened", false); return mBottomSheetOpened ?: false } - set(value) { config.set("bottomSheetOpened", value); mBottomSheetOpened = value } + var bottomSheetOpened by base.config(false) + var snowfall by base.config(false) + var eggfall by base.config(false) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/DelegateConfig.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/DelegateConfig.kt new file mode 100644 index 00000000..91dac668 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/DelegateConfig.kt @@ -0,0 +1,175 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-21. + */ + +package pl.szczodrzynski.edziennik.config + +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.gson.reflect.TypeToken +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time +import java.lang.reflect.ParameterizedType +import java.lang.reflect.WildcardType +import kotlin.reflect.KProperty + +private val gson = Gson() + +inline fun BaseConfig.config(name: String? = null, noinline default: () -> T) = ConfigDelegate( + config = this, + type = T::class.java, + nullable = null is T, + typeToken = object : TypeToken() {}, + defaultFunc = default, + defaultValue = null, + fieldName = name, +) + +inline fun BaseConfig.config(default: T) = ConfigDelegate( + config = this, + type = T::class.java, + nullable = null is T, + typeToken = object : TypeToken() {}, + defaultFunc = null, + defaultValue = default, + fieldName = null, +) + +inline fun BaseConfig.config(name: String? = null, default: T) = ConfigDelegate( + config = this, + type = T::class.java, + nullable = null is T, + typeToken = object : TypeToken() {}, + defaultFunc = null, + defaultValue = default, + fieldName = name, +) + +@Suppress("UNCHECKED_CAST") +class ConfigDelegate( + private val config: BaseConfig, + private val type: Class, + private val nullable: Boolean, + private val typeToken: TypeToken, + private val defaultFunc: (() -> T)?, + private val defaultValue: T?, + private val fieldName: String?, +) { + private var value: T? = null + private var isInitialized = false + + private fun getDefault(): T = when { + defaultFunc != null -> defaultFunc.invoke() + else -> defaultValue as T + } + + private fun getGenericType(index: Int = 0): Class<*> { + val parameterizedType = typeToken.type as ParameterizedType + val typeArgument = parameterizedType.actualTypeArguments[index] as WildcardType + return typeArgument.upperBounds[0] as Class<*> + } + + operator fun setValue(_thisRef: Any, property: KProperty<*>, newValue: T) { + value = newValue + isInitialized = true + config.set(fieldName ?: property.name, serialize(newValue)?.toString()) + } + + operator fun getValue(_thisRef: Any, property: KProperty<*>): T { + if (isInitialized) + return value as T + val key = fieldName ?: property.name + + if (key !in config.values) { + value = getDefault() + isInitialized = true + return value as T + } + val str = config.values[key] + + value = if (str == null && nullable) + null as T + else if (str == null) + getDefault() + else + deserialize(str) + + isInitialized = true + return value as T + } + + private fun serialize(value: I?, serializeObjects: Boolean = true): Any? { + if (value == null) + return null + + return when (value) { + is String -> value + is Date -> value.stringY_m_d + is Time -> value.stringValue + is JsonObject -> value + is JsonArray -> value + // primitives + is Number -> value + is Boolean -> value + // enums, maps & collections + is Enum<*> -> value.toInt() + is Collection<*> -> value.map { + if (it is Number || it is Boolean) it else serialize(it, serializeObjects = false) + }.toJsonElement() + is Map<*, *> -> gson.toJson(value.mapValues { (_, it) -> + if (it is Number || it is Boolean) it else serialize(it, serializeObjects = false) + }) + // objects or else + else -> if (serializeObjects) gson.toJson(value) else value + } + } + + private fun deserialize(value: String?, type: Class<*> = this.type): I? { + if (value == null) + return null + + @Suppress("TYPE_MISMATCH_WARNING") + return when (type) { + String::class.java -> value + Date::class.java -> Date.fromY_m_d(value) + Time::class.java -> Time.fromHms(value) + JsonObject::class.java -> value.toJsonObject() + JsonArray::class.java -> value.toJsonArray() + // primitives + java.lang.Integer::class.java -> value.toIntOrNull() + java.lang.Boolean::class.java -> value.toBooleanStrictOrNull() + java.lang.Long::class.java -> value.toLongOrNull() + java.lang.Float::class.java -> value.toFloatOrNull() + // enums, maps & collections + else -> when { + Enum::class.java.isAssignableFrom(type) -> value.toIntOrNull()?.toEnum(type) as Enum<*> + Collection::class.java.isAssignableFrom(type) -> { + val array = value.toJsonArray() + val genericType = getGenericType() + val list = array?.map { + val str = if (it.isJsonPrimitive) it.asString else it.toString() + deserialize(str, genericType) + } + when { + List::class.java.isAssignableFrom(type) -> list + Set::class.java.isAssignableFrom(type) -> list?.toSet() + else -> list?.toTypedArray() + } + } + Map::class.java.isAssignableFrom(type) -> { + val obj = value.toJsonObject() + val genericType = getGenericType(index = 1) + val map = obj?.entrySet()?.associate { (key, it) -> + val str = if (it.isJsonPrimitive) it.asString else it.toString() + key to deserialize(str, genericType) + } + map + } + // objects or else + else -> gson.fromJson(value, type) + } + } as? I + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt index c4c65ac3..c36b6fae 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt @@ -4,29 +4,20 @@ package pl.szczodrzynski.edziennik.config -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch import pl.szczodrzynski.edziennik.config.db.ConfigEntry import pl.szczodrzynski.edziennik.config.utils.ProfileConfigMigration -import pl.szczodrzynski.edziennik.config.utils.get -import pl.szczodrzynski.edziennik.config.utils.set -import pl.szczodrzynski.edziennik.config.utils.toHashMap import pl.szczodrzynski.edziennik.data.db.AppDb -import kotlin.coroutines.CoroutineContext -class ProfileConfig(val db: AppDb, val profileId: Int, rawEntries: List) : CoroutineScope, AbstractConfig { +@Suppress("RemoveExplicitTypeArguments") +class ProfileConfig( + db: AppDb, + profileId: Int, + entries: List?, +) : BaseConfig(db, profileId, entries) { companion object { - const val DATA_VERSION = 2 + const val DATA_VERSION = 5 } - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Default - - val values: HashMap = hashMapOf() - val grades by lazy { ProfileConfigGrades(this) } val ui by lazy { ProfileConfigUI(this) } val sync by lazy { ProfileConfigSync(this) } @@ -35,26 +26,13 @@ class ProfileConfig(val db: AppDb, val profileId: Int, rawEntries: List(DATA_VERSION) + var hash by config("") - private var mHash: String? = null - var hash: String - get() { mHash = mHash ?: values.get("hash", ""); return mHash ?: "" } - set(value) { set("hash", value); mHash = value } + var shareByDefault by config(false) init { - rawEntries.toHashMap(profileId, values) if (dataVersion < DATA_VERSION) ProfileConfigMigration(this) } - - override fun set(key: String, value: String?) { - values[key] = value - launch { - db.configDao().add(ConfigEntry(profileId, key, value)) - } - } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigAttendance.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigAttendance.kt index 326f5b16..ba4f9deb 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigAttendance.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigAttendance.kt @@ -4,27 +4,11 @@ package pl.szczodrzynski.edziennik.config -import pl.szczodrzynski.edziennik.config.utils.get -import pl.szczodrzynski.edziennik.config.utils.set +@Suppress("RemoveExplicitTypeArguments") +class ProfileConfigAttendance(base: ProfileConfig) { -class ProfileConfigAttendance(private val config: ProfileConfig) { - private var mAttendancePageSelection: Int? = null - var attendancePageSelection: Int - get() { mAttendancePageSelection = mAttendancePageSelection ?: config.values.get("attendancePageSelection", 1); return mAttendancePageSelection ?: 1 } - set(value) { config.set("attendancePageSelection", value); mAttendancePageSelection = value } - - private var mUseSymbols: Boolean? = null - var useSymbols: Boolean - get() { mUseSymbols = mUseSymbols ?: config.values.get("useSymbols", false); return mUseSymbols ?: false } - set(value) { config.set("useSymbols", value); mUseSymbols = value } - - private var mGroupConsecutiveDays: Boolean? = null - var groupConsecutiveDays: Boolean - get() { mGroupConsecutiveDays = mGroupConsecutiveDays ?: config.values.get("groupConsecutiveDays", true); return mGroupConsecutiveDays ?: true } - set(value) { config.set("groupConsecutiveDays", value); mGroupConsecutiveDays = value } - - private var mShowPresenceInMonth: Boolean? = null - var showPresenceInMonth: Boolean - get() { mShowPresenceInMonth = mShowPresenceInMonth ?: config.values.get("showPresenceInMonth", false); return mShowPresenceInMonth ?: false } - set(value) { config.set("showPresenceInMonth", value); mShowPresenceInMonth = value } + var attendancePageSelection by base.config(1) + var groupConsecutiveDays by base.config(true) + var showPresenceInMonth by base.config(false) + var useSymbols by base.config(false) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigGrades.kt index 6434aac7..cfe4bc3e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigGrades.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigGrades.kt @@ -4,54 +4,19 @@ package pl.szczodrzynski.edziennik.config -import pl.szczodrzynski.edziennik.config.utils.get -import pl.szczodrzynski.edziennik.config.utils.getFloat -import pl.szczodrzynski.edziennik.config.utils.set import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_ALL_GRADES -class ProfileConfigGrades(private val config: ProfileConfig) { - private var mColorMode: Int? = null - var colorMode: Int - get() { mColorMode = mColorMode ?: config.values.get("gradesColorMode", COLOR_MODE_WEIGHTED); return mColorMode ?: COLOR_MODE_WEIGHTED } - set(value) { config.set("gradesColorMode", value); mColorMode = value } +@Suppress("RemoveExplicitTypeArguments") +class ProfileConfigGrades(base: ProfileConfig) { - private var mYearAverageMode: Int? = null - var yearAverageMode: Int - get() { mYearAverageMode = mYearAverageMode ?: config.values.get("yearAverageMode", YEAR_ALL_GRADES); return mYearAverageMode ?: YEAR_ALL_GRADES } - set(value) { config.set("yearAverageMode", value); mYearAverageMode = value } - - private var mHideImproved: Boolean? = null - var hideImproved: Boolean - get() { mHideImproved = mHideImproved ?: config.values.get("hideImproved", false); return mHideImproved ?: false } - set(value) { config.set("hideImproved", value); mHideImproved = value } - - private var mAverageWithoutWeight: Boolean? = null - var averageWithoutWeight: Boolean - get() { mAverageWithoutWeight = mAverageWithoutWeight ?: config.values.get("averageWithoutWeight", true); return mAverageWithoutWeight ?: true } - set(value) { config.set("averageWithoutWeight", value); mAverageWithoutWeight = value } - - private var mPlusValue: Float? = null - var plusValue: Float? - get() { mPlusValue = mPlusValue ?: config.values.getFloat("plusValue"); return mPlusValue } - set(value) { config.set("plusValue", value); mPlusValue = value } - private var mMinusValue: Float? = null - var minusValue: Float? - get() { mMinusValue = mMinusValue ?: config.values.getFloat("minusValue"); return mMinusValue } - set(value) { config.set("minusValue", value); mMinusValue = value } - - private var mDontCountEnabled: Boolean? = null - var dontCountEnabled: Boolean - get() { mDontCountEnabled = mDontCountEnabled ?: config.values.get("dontCountEnabled", false); return mDontCountEnabled ?: false } - set(value) { config.set("dontCountEnabled", value); mDontCountEnabled = value } - - private var mDontCountGrades: List? = null - var dontCountGrades: List - get() { mDontCountGrades = mDontCountGrades ?: config.values.get("dontCountGrades", listOf()); return mDontCountGrades ?: listOf() } - set(value) { config.set("dontCountGrades", value); mDontCountGrades = value } - - private var mHideSticksFromOld: Boolean? = null - var hideSticksFromOld: Boolean - get() { mHideSticksFromOld = mHideSticksFromOld ?: config.values.get("hideSticksFromOld", false); return mHideSticksFromOld ?: false } - set(value) { config.set("hideSticksFromOld", value); mHideSticksFromOld = value } + var averageWithoutWeight by base.config(true) + var colorMode by base.config(COLOR_MODE_WEIGHTED) + var dontCountEnabled by base.config(false) + var dontCountGrades by base.config> { listOf() } + var hideImproved by base.config(false) + var hideSticksFromOld by base.config(false) + var minusValue by base.config(null) + var plusValue by base.config(null) + var yearAverageMode by base.config(YEAR_ALL_GRADES) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigSync.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigSync.kt index 6d8f95e3..91b6394e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigSync.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigSync.kt @@ -4,12 +4,14 @@ package pl.szczodrzynski.edziennik.config -import pl.szczodrzynski.edziennik.config.utils.get -import pl.szczodrzynski.edziennik.config.utils.set +import pl.szczodrzynski.edziennik.data.db.enums.NotificationType -class ProfileConfigSync(private val config: ProfileConfig) { - private var mNotificationFilter: List? = null - var notificationFilter: List - get() { mNotificationFilter = mNotificationFilter ?: config.values.get("notificationFilter", listOf()); return mNotificationFilter ?: listOf() } - set(value) { config.set("notificationFilter", value); mNotificationFilter = value } +@Suppress("RemoveExplicitTypeArguments") +class ProfileConfigSync(base: ProfileConfig) { + + var notificationFilter by base.config> { + NotificationType.values() + .filter { it.enabledByDefault == false } + .toSet() + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt index 5ce237a0..35951082 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfigUI.kt @@ -4,19 +4,30 @@ package pl.szczodrzynski.edziennik.config -import pl.szczodrzynski.edziennik.config.utils.get -import pl.szczodrzynski.edziennik.config.utils.set import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.AGENDA_DEFAULT -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardModel +import pl.szczodrzynski.edziennik.ui.home.HomeCardModel -class ProfileConfigUI(private val config: ProfileConfig) { - private var mAgendaViewType: Int? = null - var agendaViewType: Int - get() { mAgendaViewType = mAgendaViewType ?: config.values.get("agendaViewType", 0); return mAgendaViewType ?: AGENDA_DEFAULT } - set(value) { config.set("agendaViewType", value); mAgendaViewType = value } +@Suppress("RemoveExplicitTypeArguments") +class ProfileConfigUI(base: ProfileConfig) { - private var mHomeCards: List? = null - var homeCards: List - get() { mHomeCards = mHomeCards ?: config.values.get("homeCards", listOf(), HomeCardModel::class.java); return mHomeCards ?: listOf() } - set(value) { config.set("homeCards", value); mHomeCards = value } + var agendaViewType by base.config(AGENDA_DEFAULT) + var agendaCompactMode by base.config(false) + var agendaGroupByType by base.config(false) + var agendaLessonChanges by base.config(true) + var agendaTeacherAbsence by base.config(true) + var agendaSubjectImportant by base.config(false) + var agendaElearningMark by base.config(false) + var agendaElearningGroup by base.config(true) + + var homeCards by base.config> { listOf() } + + var messagesGreetingOnCompose by base.config(true) + var messagesGreetingOnReply by base.config(true) + var messagesGreetingOnForward by base.config(false) + var messagesGreetingText by base.config(null) + + var timetableShowAttendance by base.config(true) + var timetableShowEvents by base.config(true) + var timetableTrimHourRange by base.config(false) + var timetableColorSubjectName by base.config(false) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/db/ConfigDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/db/ConfigDao.kt index f55df857..21c6dc7c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/db/ConfigDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/db/ConfigDao.kt @@ -17,7 +17,7 @@ interface ConfigDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun addAll(list: List) - @Query("SELECT * FROM config WHERE profileId = -1") + @Query("SELECT * FROM config") fun getAllNow(): List @Query("SELECT * FROM config WHERE profileId = :profileId") @@ -25,4 +25,4 @@ interface ConfigDao { @Query("DELETE FROM config WHERE profileId = :profileId") fun clear(profileId: Int) -} \ No newline at end of file +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/AppConfigMigrationV3.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/AppConfigMigrationV3.kt index 78a1d8a6..d99de89d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/AppConfigMigrationV3.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/AppConfigMigrationV3.kt @@ -8,38 +8,35 @@ import android.content.SharedPreferences import com.google.gson.Gson import com.google.gson.reflect.TypeToken import pl.szczodrzynski.edziennik.BuildConfig -import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.config.Config -import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_LIBRUS -import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_MOBIDZIENNIK -import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_VULCAN +import pl.szczodrzynski.edziennik.data.db.enums.LoginType +import pl.szczodrzynski.edziennik.ext.asNavTargetOrNull +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget import pl.szczodrzynski.edziennik.utils.models.Time import kotlin.math.abs class AppConfigMigrationV3(p: SharedPreferences, config: Config) { - init { config.apply { + init { val s = "app.appConfig" - if (dataVersion < 1) { + config.apply { ui.theme = p.getString("$s.appTheme", null)?.toIntOrNull() ?: 1 sync.enabled = p.getString("$s.registerSyncEnabled", null)?.toBoolean() ?: true sync.interval = p.getString("$s.registerSyncInterval", null)?.toIntOrNull() ?: 3600 val oldButtons = p.getString("$s.miniDrawerButtonIds", null)?.let { str -> str.replace("[\\[\\]]*".toRegex(), "") .split(",\\s?".toRegex()) - .mapNotNull { it.toIntOrNull() } + .mapNotNull { it.toIntOrNull().asNavTargetOrNull() } + .toSet() } - ui.miniMenuButtons = oldButtons ?: listOf( - MainActivity.DRAWER_ITEM_HOME, - MainActivity.DRAWER_ITEM_TIMETABLE, - MainActivity.DRAWER_ITEM_AGENDA, - MainActivity.DRAWER_ITEM_GRADES, - MainActivity.DRAWER_ITEM_MESSAGES, - MainActivity.DRAWER_ITEM_HOMEWORK, - MainActivity.DRAWER_ITEM_SETTINGS + ui.miniMenuButtons = oldButtons ?: setOf( + NavTarget.HOME, + NavTarget.TIMETABLE, + NavTarget.AGENDA, + NavTarget.GRADES, + NavTarget.MESSAGES, + NavTarget.HOMEWORK, + NavTarget.SETTINGS ) - dataVersion = 1 - } - if (dataVersion < 2) { devModePassword = p.getString("$s.devModePassword", null).fix() sync.tokenApp = p.getString("$s.fcmToken", null).fix() timetable.bellSyncMultiplier = p.getString("$s.bellSyncMultiplier", null)?.toIntOrNull() ?: 0 @@ -81,14 +78,13 @@ class AppConfigMigrationV3(p: SharedPreferences, config: Config) { tokens?.forEach { val token = it.value.first when (it.key) { - LOGIN_TYPE_MOBIDZIENNIK -> sync.tokenMobidziennik = token - LOGIN_TYPE_VULCAN -> sync.tokenVulcan = token - LOGIN_TYPE_LIBRUS -> sync.tokenLibrus = token + LoginType.MOBIDZIENNIK.id -> sync.tokenMobidziennik = token + LoginType.VULCAN.id -> sync.tokenVulcan = token + LoginType.LIBRUS.id -> sync.tokenLibrus = token } } - dataVersion = 2 } - }} + } private fun String?.fix(): String? { return this?.replace("\"", "")?.let { if (it == "null") null else it } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigExtensions.kt deleted file mode 100644 index c98cf832..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigExtensions.kt +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2019-11-27. - */ - -package pl.szczodrzynski.edziennik.config.utils - -import com.google.gson.* -import com.google.gson.reflect.TypeToken -import pl.szczodrzynski.edziennik.config.AbstractConfig -import pl.szczodrzynski.edziennik.config.db.ConfigEntry -import pl.szczodrzynski.edziennik.utils.models.Date -import pl.szczodrzynski.edziennik.utils.models.Time - -private val gson = Gson() - -fun AbstractConfig.set(key: String, value: Int) { - set(key, value.toString()) -} -fun AbstractConfig.set(key: String, value: Boolean) { - set(key, value.toString()) -} -fun AbstractConfig.set(key: String, value: Long) { - set(key, value.toString()) -} -fun AbstractConfig.set(key: String, value: Float) { - set(key, value.toString()) -} -fun AbstractConfig.set(key: String, value: Date?) { - set(key, value?.stringY_m_d) -} -fun AbstractConfig.set(key: String, value: Time?) { - set(key, value?.stringValue) -} -fun AbstractConfig.set(key: String, value: JsonElement?) { - set(key, value?.toString()) -} -fun AbstractConfig.set(key: String, value: List?) { - set(key, value?.let { gson.toJson(it) }) -} -fun AbstractConfig.set(key: String, value: Any?) { - set(key, value?.let { gson.toJson(it) }) -} -fun AbstractConfig.setStringList(key: String, value: List?) { - set(key, value?.let { gson.toJson(it) }) -} -fun AbstractConfig.setIntList(key: String, value: List?) { - set(key, value?.let { gson.toJson(it) }) -} -fun AbstractConfig.setLongList(key: String, value: List?) { - set(key, value?.let { gson.toJson(it) }) -} -fun AbstractConfig.setMap(key: String, value: Map?) { - set(key, value?.let { gson.toJson(it) }) -} - -fun HashMap.get(key: String, default: String?): String? { - return this[key] ?: default -} -fun HashMap.get(key: String, default: Boolean): Boolean { - return this[key]?.toBoolean() ?: default -} -fun HashMap.get(key: String, default: Int): Int { - return this[key]?.toIntOrNull() ?: default -} -fun HashMap.get(key: String, default: Long): Long { - return this[key]?.toLongOrNull() ?: default -} -fun HashMap.get(key: String, default: Float): Float { - return this[key]?.toFloatOrNull() ?: default -} -fun HashMap.get(key: String, default: Date?): Date? { - return this[key]?.let { Date.fromY_m_d(it) } ?: default -} -fun HashMap.get(key: String, default: Time?): Time? { - return this[key]?.let { Time.fromHms(it) } ?: default -} -fun HashMap.get(key: String, default: JsonObject?): JsonObject? { - return this[key]?.let { JsonParser().parse(it)?.asJsonObject } ?: default -} -fun HashMap.get(key: String, default: JsonArray?): JsonArray? { - return this[key]?.let { JsonParser().parse(it)?.asJsonArray } ?: default -} -inline fun HashMap.get(key: String, default: T?): T? { - return this[key]?.let { Gson().fromJson(it, T::class.java) } ?: default -} -/* !!! cannot use mutable list here - modifying it will not update the DB */ -fun HashMap.get(key: String, default: List?, classOfT: Class): List? { - return this[key]?.let { ConfigGsonUtils().deserializeList(gson, it, classOfT) } ?: default -} -fun HashMap.getStringList(key: String, default: List?): List? { - return this[key]?.let { gson.fromJson>(it, object: TypeToken>(){}.type) } ?: default -} -fun HashMap.getIntList(key: String, default: List?): List? { - return this[key]?.let { gson.fromJson>(it, object: TypeToken>(){}.type) } ?: default -} -fun HashMap.getLongList(key: String, default: List?): List? { - return this[key]?.let { gson.fromJson>(it, object: TypeToken>(){}.type) } ?: default -} - -fun HashMap.getFloat(key: String): Float? { - return this[key]?.toFloatOrNull() -} - -fun List.toHashMap(profileId: Int, map: HashMap) { - map.clear() - forEach { - if (it.profileId == profileId) - map[it.key] = it.value - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigGsonUtils.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigGsonUtils.kt deleted file mode 100644 index eb04578d..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigGsonUtils.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2019-12-2. - */ -package pl.szczodrzynski.edziennik.config.utils - -import com.google.gson.Gson -import com.google.gson.JsonParser -import pl.szczodrzynski.edziennik.getInt -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardModel -import pl.szczodrzynski.edziennik.utils.models.Time - -class ConfigGsonUtils { - fun deserializeList(gson: Gson, str: String?, classOfT: Class): List { - val json = JsonParser().parse(str) - val list: MutableList = mutableListOf() - if (!json.isJsonArray) - return list - - json.asJsonArray.forEach { e -> - when (classOfT) { - String::class.java -> { - list += e.asString as T - } - HomeCardModel::class.java -> { - val o = e.asJsonObject - list += HomeCardModel( - o.getInt("profileId", 0), - o.getInt("cardId", 0) - ) as T - } - Time::class.java -> { - val o = e.asJsonObject - list += Time( - o.getInt("hour", 0), - o.getInt("minute", 0), - o.getInt("second", 0) - ) as T - } - } - } - - return list - } -} \ No newline at end of file diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigMigration.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigMigration.kt index 1095f511..f44d6e56 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigMigration.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ConfigMigration.kt @@ -5,12 +5,9 @@ package pl.szczodrzynski.edziennik.config.utils import android.content.Context +import androidx.core.content.edit import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.BuildConfig -import pl.szczodrzynski.edziennik.HOUR -import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.config.Config -import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.ORDER_BY_DATE_DESC import pl.szczodrzynski.edziennik.utils.models.Time import kotlin.math.abs @@ -22,74 +19,14 @@ class ConfigMigration(app: App, config: Config) { // migrate appConfig from app version 3.x and lower. // Updates dataVersion to level 2. AppConfigMigrationV3(p, config) - } - - if (dataVersion < 2) { - appVersion = BuildConfig.VERSION_CODE - loginFinished = false - ui.language = null - ui.theme = 1 - ui.appBackground = null - ui.headerBackground = null - ui.miniMenuVisible = false - ui.miniMenuButtons = listOf( - MainActivity.DRAWER_ITEM_HOME, - MainActivity.DRAWER_ITEM_TIMETABLE, - MainActivity.DRAWER_ITEM_AGENDA, - MainActivity.DRAWER_ITEM_GRADES, - MainActivity.DRAWER_ITEM_MESSAGES, - MainActivity.DRAWER_ITEM_HOMEWORK, - MainActivity.DRAWER_ITEM_SETTINGS - ) - sync.enabled = true - sync.interval = 1*HOUR.toInt() - sync.notifyAboutUpdates = true - sync.onlyWifi = false - sync.quietHoursEnabled = false - sync.quietHoursStart = null - sync.quietHoursEnd = null - sync.quietDuringLessons = false - sync.tokenApp = null - sync.tokenMobidziennik = null - sync.tokenMobidziennikList = listOf() - sync.tokenLibrus = null - sync.tokenLibrusList = listOf() - sync.tokenVulcan = null - sync.tokenVulcanList = listOf() - timetable.bellSyncMultiplier = 0 - timetable.bellSyncDiff = null - timetable.countInSeconds = false - grades.orderBy = ORDER_BY_DATE_DESC - - dataVersion = 2 - } - - if (dataVersion < 3) { - update = null - privacyPolicyAccepted = false - debugMode = false - devModePassword = null - appInstalledTime = 0L - appRateSnackbarTime = 0L - - dataVersion = 3 - } - - if (dataVersion < 10) { - ui.openDrawerOnBackPressed = false - ui.snowfall = false - ui.bottomSheetOpened = false - sync.dontShowAppManagerDialog = false - sync.webPushEnabled = true - sync.lastAppSync = 0L - - - dataVersion = 10 + p.edit { + remove("app.appConfig.appTheme") + } } if (dataVersion < 11) { - val startMillis = config.values.get("quietHoursStart", 0L) - val endMillis = config.values.get("quietHoursEnd", 0L) + val startMillis = config.values["quietHoursStart"]?.toLongOrNull() ?: 0L + val endMillis = config.values["quietHoursEnd"]?.toLongOrNull() ?: 0L if (startMillis > 0) { try { sync.quietHoursStart = Time.fromMillis(abs(startMillis)) @@ -106,5 +43,7 @@ class ConfigMigration(app: App, config: Config) { dataVersion = 11 } + + hash = "invalid" }} } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ProfileConfigMigration.kt b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ProfileConfigMigration.kt index 17f071f2..ab915b1d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ProfileConfigMigration.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/utils/ProfileConfigMigration.kt @@ -5,33 +5,57 @@ package pl.szczodrzynski.edziennik.config.utils import pl.szczodrzynski.edziennik.config.ProfileConfig -import pl.szczodrzynski.edziennik.data.db.entity.Notification import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.AGENDA_DEFAULT +import pl.szczodrzynski.edziennik.data.db.enums.NotificationType +import pl.szczodrzynski.edziennik.data.db.enums.SchoolType +import pl.szczodrzynski.edziennik.ui.home.HomeCard +import pl.szczodrzynski.edziennik.ui.home.HomeCardModel import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_ALL_GRADES class ProfileConfigMigration(config: ProfileConfig) { init { config.apply { - if (dataVersion < 1) { - grades.colorMode = COLOR_MODE_WEIGHTED - grades.yearAverageMode = YEAR_ALL_GRADES - grades.hideImproved = false - grades.averageWithoutWeight = true - grades.plusValue = null - grades.minusValue = null - grades.dontCountEnabled = false - grades.dontCountGrades = listOf() - ui.agendaViewType = AGENDA_DEFAULT - // no migration for ui.homeCards - - dataVersion = 1 - } + val profile = db.profileDao().getByIdNow(profileId ?: -1) if (dataVersion < 2) { - sync.notificationFilter = sync.notificationFilter + Notification.TYPE_TEACHER_ABSENCE + sync.notificationFilter = sync.notificationFilter + NotificationType.TEACHER_ABSENCE dataVersion = 2 } + + if (dataVersion < 3) { + if (ui.homeCards.isNotEmpty()) { + ui.homeCards = ui.homeCards + HomeCardModel( + profileId = config.profileId ?: -1, + cardId = HomeCard.CARD_NOTES, + ) + } + + dataVersion = 3 + } + + if (dataVersion < 4) { + // switch to new event types (USOS) + dataVersion = 4 + + 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;") + } + } }} } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt index ad9c4296..0d9fe92c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/ApiService.kt @@ -22,7 +22,7 @@ import pl.szczodrzynski.edziennik.data.api.task.ErrorReportTask import pl.szczodrzynski.edziennik.data.api.task.IApiTask import pl.szczodrzynski.edziennik.data.api.task.SzkolnyTask import pl.szczodrzynski.edziennik.data.db.entity.Profile -import pl.szczodrzynski.edziennik.toApiError +import pl.szczodrzynski.edziennik.ext.toApiError import pl.szczodrzynski.edziennik.utils.Utils.d import kotlin.math.min import kotlin.math.roundToInt @@ -84,19 +84,21 @@ class ApiService : Service() { runTask() } + override fun onRequiresUserAction(event: UserActionRequiredEvent) { + app.userActionManager.sendToUser(event) + taskRunning?.cancel() + clearTask() + runTask() + } + override fun onError(apiError: ApiError) { lastEventTime = System.currentTimeMillis() d(TAG, "Task $taskRunningId threw an error - $apiError") apiError.profileId = taskProfileId - if (app.userActionManager.requiresUserAction(apiError)) { - app.userActionManager.sendToUser(apiError) - } - else { - EventBus.getDefault().postSticky(ApiTaskErrorEvent(apiError)) - errorList.add(apiError) - apiError.throwable?.printStackTrace() - } + EventBus.getDefault().postSticky(ApiTaskErrorEvent(apiError)) + errorList.add(apiError) + apiError.throwable?.printStackTrace() if (apiError.isCritical) { taskRunning?.cancel() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt index f06ef354..31fabaea 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Constants.kt @@ -24,11 +24,12 @@ const val FAKE_LIBRUS_ACCOUNTS = "/synergia_accounts.php" 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 = "0RbsDOkV9tyKEQYzlLv5hs3DM1ukrynFI4p6C1Yc" +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" @@ -43,7 +44,7 @@ const val LIBRUS_API_TOKEN_URL = "https://api.librus.pl/OAuth/Token" const val LIBRUS_API_TOKEN_JST_URL = "https://api.librus.pl/OAuth/TokenJST" const val LIBRUS_API_AUTHORIZATION = "Mjg6ODRmZGQzYTg3YjAzZDNlYTZmZmU3NzdiNThiMzMyYjE=" const val LIBRUS_API_SECRET_JST = "18b7c1ee08216f636a1b1a2440e68398" -const val LIBRUS_API_CLIENT_ID_JST = "49" +const val LIBRUS_API_CLIENT_ID_JST = "59" //const val LIBRUS_API_CLIENT_ID_JST_REFRESH = "42" const val LIBRUS_JST_DEMO_CODE = "68656A21" @@ -59,40 +60,12 @@ 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 IDZIENNIK_USER_AGENT = SYNERGIA_USER_AGENT -const val IDZIENNIK_WEB_URL = "https://iuczniowie.progman.pl/idziennik" -const val IDZIENNIK_WEB_LOGIN = "login.aspx" -const val IDZIENNIK_WEB_SETTINGS = "mod_panelRodzica/Ustawienia.aspx" -const val IDZIENNIK_WEB_HOME = "mod_panelRodzica/StronaGlowna.aspx" -const val IDZIENNIK_WEB_TIMETABLE = "mod_panelRodzica/plan/WS_Plan.asmx/pobierzPlanZajec" -const val IDZIENNIK_WEB_GRADES = "mod_panelRodzica/oceny/WS_ocenyUcznia.asmx/pobierzOcenyUcznia" -const val IDZIENNIK_WEB_MISSING_GRADES = "mod_panelRodzica/brak_ocen/WS_BrakOcenUcznia.asmx/pobierzBrakujaceOcenyUcznia" -const val IDZIENNIK_WEB_EXAMS = "mod_panelRodzica/sprawdziany/mod_sprawdzianyPanel.asmx/pobierzListe" -const val IDZIENNIK_WEB_HOMEWORK = "mod_panelRodzica/pracaDomowa/WS_pracaDomowa.asmx/pobierzPraceDomowe" -const val IDZIENNIK_WEB_NOTICES = "mod_panelRodzica/uwagi/WS_uwagiUcznia.asmx/pobierzUwagiUcznia" -const val IDZIENNIK_WEB_ATTENDANCE = "mod_panelRodzica/obecnosci/WS_obecnosciUcznia.asmx/pobierzObecnosciUcznia" -const val IDZIENNIK_WEB_ANNOUNCEMENTS = "mod_panelRodzica/tabOgl/WS_tablicaOgloszen.asmx/GetOgloszenia" -const val IDZIENNIK_WEB_MESSAGES_LIST = "mod_komunikator/WS_wiadomosci.asmx/PobierzListeWiadomosci" -const val IDZIENNIK_WEB_GET_MESSAGE = "mod_komunikator/WS_wiadomosci.asmx/PobierzWiadomosc" -const val IDZIENNIK_WEB_GET_RECIPIENT_LIST = "mod_komunikator/WS_wiadomosci.asmx/pobierzListeOdbiorcowPanelRodzic" -const val IDZIENNIK_WEB_SEND_MESSAGE = "mod_komunikator/WS_wiadomosci.asmx/WyslijWiadomosc" -const val IDZIENNIK_WEB_GET_ATTACHMENT = "mod_komunikator/Download.ashx" -const val IDZIENNIK_WEB_GET_HOMEWORK = "mod_panelRodzica/pracaDomowa/WS_pracaDomowa.asmx/pobierzJednaPraceDomowa" -const val IDZIENNIK_WEB_GET_HOMEWORK_ATTACHMENT = "mod_panelRodzica/pracaDomowa.aspx" - -val IDZIENNIK_API_USER_AGENT = SYSTEM_USER_AGENT -const val IDZIENNIK_API_URL = "https://iuczniowie.progman.pl/idziennik/api" -const val IDZIENNIK_API_CURRENT_REGISTER = "Uczniowie/\$STUDENT_ID/AktualnyDziennik" -const val IDZIENNIK_API_GRADES = "Uczniowie/\$STUDENT_ID/Oceny/" /* + semester */ -const val IDZIENNIK_API_MESSAGES_INBOX = "Wiadomosci/Odebrane" -const val IDZIENNIK_API_MESSAGES_SENT = "Wiadomosci/Wyslane" - val MOBIDZIENNIK_USER_AGENT = SYSTEM_USER_AGENT const val VULCAN_HEBE_USER_AGENT = "Dart/2.10 (dart:io)" const val VULCAN_HEBE_APP_NAME = "DzienniczekPlus 2.0" -const val VULCAN_HEBE_APP_VERSION = "21.02.09 (G)" +const val VULCAN_HEBE_APP_VERSION = "22.09.02 (G)" private const val VULCAN_API_DEVICE_NAME_PREFIX = "Szkolny.eu " private const val VULCAN_API_DEVICE_NAME_SUFFIX = " - nie usuwać" val VULCAN_API_DEVICE_NAME by lazy { @@ -109,20 +82,30 @@ const val VULCAN_HEBE_ENDPOINT_PUSH_ALL = "api/mobile/push/all" const val VULCAN_HEBE_ENDPOINT_TIMETABLE = "api/mobile/schedule" const val VULCAN_HEBE_ENDPOINT_TIMETABLE_CHANGES = "api/mobile/schedule/changes" const val VULCAN_HEBE_ENDPOINT_ADDRESSBOOK = "api/mobile/addressbook" +const val VULCAN_HEBE_ENDPOINT_TEACHERS = "api/mobile/teacher" const val VULCAN_HEBE_ENDPOINT_EXAMS = "api/mobile/exam" const val VULCAN_HEBE_ENDPOINT_GRADES = "api/mobile/grade" const val VULCAN_HEBE_ENDPOINT_GRADE_SUMMARY = "api/mobile/grade/summary" const val VULCAN_HEBE_ENDPOINT_HOMEWORK = "api/mobile/homework" const val VULCAN_HEBE_ENDPOINT_NOTICES = "api/mobile/note" const val VULCAN_HEBE_ENDPOINT_ATTENDANCE = "api/mobile/lesson" -const val VULCAN_HEBE_ENDPOINT_MESSAGES = "api/mobile/message" -const val VULCAN_HEBE_ENDPOINT_MESSAGES_STATUS = "api/mobile/message/status" -const val VULCAN_HEBE_ENDPOINT_MESSAGES_SEND = "api/mobile/message" +const val VULCAN_HEBE_ENDPOINT_MESSAGEBOX = "api/mobile/messagebox" +const val VULCAN_HEBE_ENDPOINT_MESSAGEBOX_ADDRESSBOOK = "api/mobile/messagebox/addressbook" +const val VULCAN_HEBE_ENDPOINT_MESSAGEBOX_MESSAGES = "api/mobile/messagebox/message" +const val VULCAN_HEBE_ENDPOINT_MESSAGEBOX_STATUS = "api/mobile/messagebox/message/status" +const val VULCAN_HEBE_ENDPOINT_MESSAGEBOX_SEND = "api/mobile/messagebox/message" const val VULCAN_HEBE_ENDPOINT_LUCKY_NUMBER = "api/mobile/school/lucky" -const val EDUDZIENNIK_USER_AGENT = "Szkolny.eu/${BuildConfig.VERSION_NAME}" - const val PODLASIE_API_VERSION = "1.0.62" const val PODLASIE_API_URL = "https://cpdklaser.zeto.bialystok.pl/api" const val PODLASIE_API_USER_ENDPOINT = "/pobierzDaneUcznia" const val PODLASIE_API_LOGOUT_DEVICES_ENDPOINT = "/wyczyscUrzadzenia" + +const val USOS_API_OAUTH_REDIRECT_URL = "szkolny://redirect/usos" + +val USOS_API_SCOPES by lazy { listOf( + "offline_access", + "studies", + "grades", + "events", +) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EdziennikNotification.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EdziennikNotification.kt index 605b644c..6b2b7533 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EdziennikNotification.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EdziennikNotification.kt @@ -11,8 +11,9 @@ import android.content.Context import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat.PRIORITY_MIN import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.Bundle import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.ext.Bundle +import pl.szczodrzynski.edziennik.ext.pendingIntentFlag import pl.szczodrzynski.edziennik.receivers.SzkolnyReceiver import kotlin.math.roundToInt @@ -40,14 +41,14 @@ class EdziennikNotification(val app: App) { "task" to "TaskCancelRequest", "taskId" to taskId )) - return PendingIntent.getBroadcast(app, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) as PendingIntent + return PendingIntent.getBroadcast(app, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or pendingIntentFlag()) as PendingIntent } private val closePendingIntent: PendingIntent get() { val intent = SzkolnyReceiver.getIntent(app, Bundle( "task" to "ServiceCloseRequest" )) - return PendingIntent.getBroadcast(app, 0, intent, 0) as PendingIntent + return PendingIntent.getBroadcast(app, 0, intent, pendingIntentFlag()) as PendingIntent } private fun errorCountText(): String? { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EndpointChooser.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EndpointChooser.kt index 7f882123..de834344 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EndpointChooser.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/EndpointChooser.kt @@ -2,115 +2,116 @@ package pl.szczodrzynski.edziennik.data.api import pl.szczodrzynski.edziennik.data.api.models.Data import pl.szczodrzynski.edziennik.data.api.models.Feature -import pl.szczodrzynski.edziennik.data.api.models.LoginMethod import pl.szczodrzynski.edziennik.data.db.entity.EndpointTimer import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS import pl.szczodrzynski.edziennik.data.db.entity.SYNC_NEVER +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.ext.getFeatureTypesNecessary +import pl.szczodrzynski.edziennik.ext.getFeatureTypesUnnecessary +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty -fun Data.prepare(loginMethods: List, features: List, featureIds: List, viewId: Int?, onlyEndpoints: List?) { - val data = this - - val possibleLoginMethods = data.loginMethods.toMutableList() - - for (loginMethod in loginMethods) { - if (loginMethod.isPossible(profile, loginStore)) - possibleLoginMethods += loginMethod.loginMethodId +fun Data.prepare( + features: List, + featureTypes: Set?, + onlyEndpoints: Set?, +) { + val loginType = this.loginStore.type + val possibleLoginMethods = this.loginMethods.toMutableList() + possibleLoginMethods += LoginMethod.values().filter { + it.loginType == loginType && it.isPossible?.invoke(profile, loginStore) != false } //var highestLoginMethod = 0 - var endpointList = mutableListOf() - val requiredLoginMethods = mutableListOf() + var possibleFeatures = mutableListOf() + val requiredLoginMethods = mutableListOf() - data.targetEndpointIds.clear() - data.targetLoginMethodIds.clear() + val syncFeatureTypes = when { + featureTypes.isNotNullNorEmpty() -> featureTypes!! + else -> getFeatureTypesUnnecessary() + } + getFeatureTypesNecessary() + val forceFeatureType = featureTypes?.singleOrNull() + + this.targetEndpoints.clear() + this.targetLoginMethods.clear() // get all endpoints for every feature, only if possible to login and possible/necessary to sync - for (featureId in featureIds) { - features.filter { - it.featureId == featureId // feature ID matches + for (featureId in syncFeatureTypes) { + possibleFeatures += features.filter { + it.featureType == featureId // feature ID matches && possibleLoginMethods.containsAll(it.requiredLoginMethods) // is possible to login - && it.shouldSync?.invoke(data) ?: true // is necessary/possible to sync - }.let { - endpointList.addAll(it) + && it.shouldSync?.invoke(this) ?: true // is necessary/possible to sync } } val timestamp = System.currentTimeMillis() - endpointList = endpointList - // sort the endpoint list by feature ID and priority - .sortedWith(compareBy(Feature::featureId, Feature::priority)) - // select only the most important endpoint for each feature - .distinctBy { it.featureId } - .toMutableList() - // add all endpoint IDs and required login methods, filtering using timers - .onEach { feature -> - feature.endpointIds.forEach { endpoint -> - if (onlyEndpoints?.contains(endpoint.first) == false) - return@forEach - (data.endpointTimers - .singleOrNull { it.endpointId == endpoint.first } ?: EndpointTimer(data.profile?.id - ?: -1, endpoint.first)) - .let { timer -> - if ( - onlyEndpoints?.contains(endpoint.first) == true || - timer.nextSync == SYNC_ALWAYS || - viewId != null && timer.viewId == viewId || - timer.nextSync != SYNC_NEVER && timer.nextSync < timestamp - ) { - data.targetEndpointIds[endpoint.first] = timer.lastSync - requiredLoginMethods.add(endpoint.second) - } - } - } + possibleFeatures = possibleFeatures + // sort the endpoint list by feature ID and priority + .sortedWith(compareBy(Feature::featureType, Feature::priority)) + // select only the most important endpoint for each feature + .distinctBy { it.featureType } + .toMutableList() + + for (feature in possibleFeatures) { + // add all endpoint IDs and required login methods, filtering using timers + feature.endpoints.forEach { endpoint -> + if (onlyEndpoints?.contains(endpoint.first) == false) + return@forEach + val timer = this.endpointTimers + .singleOrNull { it.endpointId == endpoint.first } + ?: EndpointTimer(this.profileId, endpoint.first) + if ( + onlyEndpoints?.contains(endpoint.first) == true || + timer.nextSync == SYNC_ALWAYS || + forceFeatureType != null && timer.featureType == forceFeatureType || + timer.nextSync != SYNC_NEVER && timer.nextSync < timestamp + ) { + this.targetEndpoints[endpoint.first] = timer.lastSync + requiredLoginMethods += endpoint.second } + } + } // check every login method for any dependencies - for (loginMethodId in requiredLoginMethods) { - var requiredLoginMethod: Int? = loginMethodId - while (requiredLoginMethod != LOGIN_METHOD_NOT_NEEDED) { - loginMethods.singleOrNull { it.loginMethodId == requiredLoginMethod }?.let { loginMethod -> - if (requiredLoginMethod != null) - data.targetLoginMethodIds.add(requiredLoginMethod!!) - requiredLoginMethod = loginMethod.requiredLoginMethod(data.profile, data.loginStore) - } + for (loginMethod in requiredLoginMethods) { + var requiredLoginMethod: LoginMethod? = loginMethod + while (requiredLoginMethod != null) { + this.targetLoginMethods += requiredLoginMethod + requiredLoginMethod = requiredLoginMethod.requiredLoginMethod?.invoke(this.profile, this.loginStore) } } // sort and distinct every login method and endpoint - data.targetLoginMethodIds = data.targetLoginMethodIds.toHashSet().toMutableList() - data.targetLoginMethodIds.sort() + this.targetLoginMethods = this.targetLoginMethods.toHashSet().toMutableList() + this.targetLoginMethods.sort() //data.targetEndpointIds = data.targetEndpointIds.toHashSet().toMutableList() //data.targetEndpointIds.sort() - progressCount = targetLoginMethodIds.size + targetEndpointIds.size + progressCount = targetLoginMethods.size + targetEndpoints.size progressStep = if (progressCount <= 0) 0f else 100f / progressCount.toFloat() } -fun Data.prepareFor(loginMethods: List, loginMethodId: Int) { +fun Data.prepareFor(loginMethod: LoginMethod) { + val loginType = loginStore.type val possibleLoginMethods = this.loginMethods.toMutableList() - - loginMethods.forEach { - if (it.isPossible(profile, loginStore)) - possibleLoginMethods += it.loginMethodId + possibleLoginMethods += LoginMethod.values().filter { + it.loginType == loginType && it.isPossible?.invoke(profile, loginStore) != false } - targetLoginMethodIds.clear() + this.targetLoginMethods.clear() // check the login method for any dependencies - var requiredLoginMethod: Int? = loginMethodId - while (requiredLoginMethod != LOGIN_METHOD_NOT_NEEDED) { - loginMethods.singleOrNull { it.loginMethodId == requiredLoginMethod }?.let { - if (requiredLoginMethod != null) - targetLoginMethodIds.add(requiredLoginMethod!!) - requiredLoginMethod = it.requiredLoginMethod(profile, loginStore) - } + var requiredLoginMethod: LoginMethod? = loginMethod + while (requiredLoginMethod != null) { + this.targetLoginMethods += requiredLoginMethod + requiredLoginMethod = requiredLoginMethod.requiredLoginMethod?.invoke(this.profile, this.loginStore) } // sort and distinct every login method - targetLoginMethodIds = targetLoginMethodIds.toHashSet().toMutableList() - targetLoginMethodIds.sort() + this.targetLoginMethods = this.targetLoginMethods.toHashSet().toMutableList() + this.targetLoginMethods.sort() progressCount = 0 progressStep = 0f diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt index 5738064c..93a7228f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Errors.kt @@ -58,11 +58,7 @@ const val ERROR_INVALID_LOGIN_MODE = 110 const val ERROR_LOGIN_METHOD_NOT_SATISFIED = 111 const val ERROR_NOT_IMPLEMENTED = 112 const val ERROR_FILE_DOWNLOAD = 113 - -const val ERROR_NO_STUDENTS_IN_ACCOUNT = 115 - -const val ERROR_CAPTCHA_NEEDED = 3000 -const val ERROR_CAPTCHA_LIBRUS_PORTAL = 3001 +const val ERROR_REQUIRES_USER_ACTION = 114 const val ERROR_API_PDO_ERROR = 5000 const val ERROR_API_INVALID_CLIENT = 5001 @@ -195,21 +191,21 @@ const val ERROR_VULCAN_HEBE_FIREBASE_ERROR = 362 const val ERROR_VULCAN_HEBE_CERTIFICATE_GONE = 363 const val ERROR_VULCAN_HEBE_SERVER_ERROR = 364 const val ERROR_VULCAN_HEBE_ENTITY_NOT_FOUND = 365 +const val ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY = 366 const val ERROR_VULCAN_API_DEPRECATED = 390 -const val ERROR_LOGIN_EDUDZIENNIK_WEB_INVALID_LOGIN = 501 -const val ERROR_LOGIN_EDUDZIENNIK_WEB_OTHER = 510 -const val ERROR_LOGIN_EDUDZIENNIK_WEB_NO_SESSION_ID = 511 -const val ERROR_EDUDZIENNIK_WEB_LIMITED_ACCESS = 521 -const val ERROR_EDUDZIENNIK_WEB_SESSION_EXPIRED = 522 -const val ERROR_EDUDZIENNIK_WEB_TEAM_MISSING = 530 - const val ERROR_LOGIN_PODLASIE_API_INVALID_TOKEN = 601 const val ERROR_LOGIN_PODLASIE_API_DEVICE_LIMIT = 602 const val ERROR_PODLASIE_API_NO_TOKEN = 630 const val ERROR_PODLASIE_API_OTHER = 631 const val ERROR_PODLASIE_API_DATA_MISSING = 632 +const val ERROR_USOS_OAUTH_GOT_DIFFERENT_TOKEN = 702 +const val ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE = 703 +const val ERROR_USOS_NO_STUDENT_PROGRAMMES = 704 +const val ERROR_USOS_API_INCOMPLETE_RESPONSE = 705 +const val ERROR_USOS_API_MISSING_RESPONSE = 706 + const val ERROR_TEMPLATE_WEB_OTHER = 801 const val EXCEPTION_API_TASK = 900 @@ -223,8 +219,6 @@ const val EXCEPTION_MOBIDZIENNIK_WEB_FILE_REQUEST = 908 const val EXCEPTION_LIBRUS_MESSAGES_FILE_REQUEST = 909 const val EXCEPTION_NOTIFY = 910 const val EXCEPTION_LIBRUS_MESSAGES_REQUEST = 911 -const val EXCEPTION_EDUDZIENNIK_WEB_REQUEST = 920 -const val EXCEPTION_EDUDZIENNIK_FILE_REQUEST = 921 const val ERROR_ONEDRIVE_DOWNLOAD = 930 const val EXCEPTION_VULCAN_WEB_LOGIN = 931 const val EXCEPTION_VULCAN_WEB_REQUEST = 932 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Features.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Features.kt deleted file mode 100644 index e38b37b5..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Features.kt +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2019-9-29. - */ - -package pl.szczodrzynski.edziennik.data.api - -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_AGENDA -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_ANNOUNCEMENTS -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_ATTENDANCE -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_BEHAVIOUR -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_GRADES -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_HOME -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_HOMEWORK -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_TIMETABLE -import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED -import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT - -internal const val FEATURE_TIMETABLE = 1 -internal const val FEATURE_AGENDA = 2 -internal const val FEATURE_GRADES = 3 -internal const val FEATURE_HOMEWORK = 4 -internal const val FEATURE_BEHAVIOUR = 5 -internal const val FEATURE_ATTENDANCE = 6 -internal const val FEATURE_MESSAGES_INBOX = 7 -internal const val FEATURE_MESSAGES_SENT = 8 -internal const val FEATURE_ANNOUNCEMENTS = 9 - -internal const val FEATURE_ALWAYS_NEEDED = 100 -internal const val FEATURE_STUDENT_INFO = 101 -internal const val FEATURE_STUDENT_NUMBER = 109 -internal const val FEATURE_SCHOOL_INFO = 102 -internal const val FEATURE_CLASS_INFO = 103 -internal const val FEATURE_TEAM_INFO = 104 -internal const val FEATURE_LUCKY_NUMBER = 105 -internal const val FEATURE_TEACHERS = 106 -internal const val FEATURE_SUBJECTS = 107 -internal const val FEATURE_CLASSROOMS = 108 -internal const val FEATURE_PUSH_CONFIG = 120 - -object Features { - private fun getAllNecessary(): List = listOf( - FEATURE_ALWAYS_NEEDED, - FEATURE_PUSH_CONFIG, - FEATURE_STUDENT_INFO, - FEATURE_STUDENT_NUMBER, - FEATURE_SCHOOL_INFO, - FEATURE_CLASS_INFO, - FEATURE_TEAM_INFO, - FEATURE_LUCKY_NUMBER, - FEATURE_TEACHERS, - FEATURE_SUBJECTS, - FEATURE_CLASSROOMS) - - private fun getAllFeatures(): List = listOf( - FEATURE_TIMETABLE, - FEATURE_AGENDA, - FEATURE_GRADES, - FEATURE_HOMEWORK, - FEATURE_BEHAVIOUR, - FEATURE_ATTENDANCE, - FEATURE_MESSAGES_INBOX, - FEATURE_MESSAGES_SENT, - FEATURE_ANNOUNCEMENTS) - - fun getAllIds(): List = getAllFeatures() + getAllNecessary() - - fun getIdsByView(targetId: Int, targetType: Int): List { - return (when (targetId) { - DRAWER_ITEM_HOME -> getAllFeatures() - DRAWER_ITEM_TIMETABLE -> listOf(FEATURE_TIMETABLE) - DRAWER_ITEM_AGENDA -> listOf(FEATURE_AGENDA) - DRAWER_ITEM_GRADES -> listOf(FEATURE_GRADES) - DRAWER_ITEM_MESSAGES -> when (targetType) { - TYPE_RECEIVED -> listOf(FEATURE_MESSAGES_INBOX) - TYPE_SENT -> listOf(FEATURE_MESSAGES_SENT) - else -> listOf(FEATURE_MESSAGES_INBOX, FEATURE_MESSAGES_SENT) - } - DRAWER_ITEM_HOMEWORK -> listOf(FEATURE_HOMEWORK) - DRAWER_ITEM_BEHAVIOUR -> listOf(FEATURE_BEHAVIOUR) - DRAWER_ITEM_ATTENDANCE -> listOf(FEATURE_ATTENDANCE) - DRAWER_ITEM_ANNOUNCEMENTS -> listOf(FEATURE_ANNOUNCEMENTS) - else -> getAllFeatures() - } + getAllNecessary()).sorted() - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt deleted file mode 100644 index b3db9f71..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/LoginMethods.kt +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2019-9-20. - */ - -package pl.szczodrzynski.edziennik.data.api - -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login.EdudziennikLoginWeb -import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginApi -import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginMessages -import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginPortal -import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginSynergia -import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.MobidziennikLoginApi2 -import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.MobidziennikLoginWeb -import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login.PodlasieLoginApi -import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLoginApi -import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLoginWeb -import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginHebe -import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginWebMain -import pl.szczodrzynski.edziennik.data.api.models.LoginMethod - -// librus -// mobidziennik -// idziennik [*] -// vulcan -// mobireg - -const val SYNERGIA_API_ENABLED = false - - -const val LOGIN_TYPE_IDZIENNIK = 3 - -const val LOGIN_TYPE_TEMPLATE = 21 - -// LOGIN MODES -const val LOGIN_MODE_TEMPLATE_WEB = 0 - -// LOGIN METHODS -const val LOGIN_METHOD_NOT_NEEDED = -1 -const val LOGIN_METHOD_TEMPLATE_WEB = 100 -const val LOGIN_METHOD_TEMPLATE_API = 200 - -const val LOGIN_TYPE_LIBRUS = 2 -const val LOGIN_MODE_LIBRUS_EMAIL = 0 -const val LOGIN_MODE_LIBRUS_SYNERGIA = 1 -const val LOGIN_MODE_LIBRUS_JST = 2 -const val LOGIN_METHOD_LIBRUS_PORTAL = 100 -const val LOGIN_METHOD_LIBRUS_API = 200 -const val LOGIN_METHOD_LIBRUS_SYNERGIA = 300 -const val LOGIN_METHOD_LIBRUS_MESSAGES = 400 -val librusLoginMethods = listOf( - LoginMethod(LOGIN_TYPE_LIBRUS, LOGIN_METHOD_LIBRUS_PORTAL, LibrusLoginPortal::class.java) - .withIsPossible { _, loginStore -> - loginStore.mode == LOGIN_MODE_LIBRUS_EMAIL - } - .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }, - - LoginMethod(LOGIN_TYPE_LIBRUS, LOGIN_METHOD_LIBRUS_API, LibrusLoginApi::class.java) - .withIsPossible { _, loginStore -> - loginStore.mode != LOGIN_MODE_LIBRUS_SYNERGIA || SYNERGIA_API_ENABLED - } - .withRequiredLoginMethod { _, loginStore -> - if (loginStore.mode == LOGIN_MODE_LIBRUS_EMAIL) LOGIN_METHOD_LIBRUS_PORTAL else LOGIN_METHOD_NOT_NEEDED - }, - - LoginMethod(LOGIN_TYPE_LIBRUS, LOGIN_METHOD_LIBRUS_SYNERGIA, LibrusLoginSynergia::class.java) - .withIsPossible { _, loginStore -> !loginStore.hasLoginData("fakeLogin") } - .withRequiredLoginMethod { profile, _ -> - if (profile?.hasStudentData("accountPassword") == false || true) LOGIN_METHOD_LIBRUS_API else LOGIN_METHOD_NOT_NEEDED - }, - - LoginMethod(LOGIN_TYPE_LIBRUS, LOGIN_METHOD_LIBRUS_MESSAGES, LibrusLoginMessages::class.java) - .withIsPossible { _, loginStore -> !loginStore.hasLoginData("fakeLogin") } - .withRequiredLoginMethod { profile, _ -> - if (profile?.hasStudentData("accountPassword") == false || true) LOGIN_METHOD_LIBRUS_SYNERGIA else LOGIN_METHOD_NOT_NEEDED - } -) - -const val LOGIN_TYPE_MOBIDZIENNIK = 1 -const val LOGIN_MODE_MOBIDZIENNIK_WEB = 0 -const val LOGIN_METHOD_MOBIDZIENNIK_WEB = 100 -const val LOGIN_METHOD_MOBIDZIENNIK_API2 = 300 -val mobidziennikLoginMethods = listOf( - LoginMethod(LOGIN_TYPE_MOBIDZIENNIK, LOGIN_METHOD_MOBIDZIENNIK_WEB, MobidziennikLoginWeb::class.java) - .withIsPossible { _, _ -> true } - .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }, - - LoginMethod(LOGIN_TYPE_MOBIDZIENNIK, LOGIN_METHOD_MOBIDZIENNIK_API2, MobidziennikLoginApi2::class.java) - .withIsPossible { profile, _ -> profile?.getStudentData("email", null) != null } - .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED } -) - -const val LOGIN_TYPE_VULCAN = 4 -const val LOGIN_MODE_VULCAN_API = 0 -const val LOGIN_MODE_VULCAN_WEB = 1 -const val LOGIN_MODE_VULCAN_HEBE = 2 -const val LOGIN_METHOD_VULCAN_WEB_MAIN = 100 -const val LOGIN_METHOD_VULCAN_WEB_NEW = 200 -const val LOGIN_METHOD_VULCAN_WEB_OLD = 300 -const val LOGIN_METHOD_VULCAN_WEB_MESSAGES = 400 -const val LOGIN_METHOD_VULCAN_HEBE = 600 -val vulcanLoginMethods = listOf( - LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_WEB_MAIN, VulcanLoginWebMain::class.java) - .withIsPossible { _, loginStore -> loginStore.hasLoginData("webHost") } - .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }, - - /*LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_WEB_NEW, VulcanLoginWebNew::class.java) - .withIsPossible { _, _ -> false } - .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_VULCAN_WEB_MAIN }, - - LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_WEB_OLD, VulcanLoginWebOld::class.java) - .withIsPossible { _, _ -> false } - .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_VULCAN_WEB_MAIN },*/ - - LoginMethod(LOGIN_TYPE_VULCAN, LOGIN_METHOD_VULCAN_HEBE, VulcanLoginHebe::class.java) - .withIsPossible { _, loginStore -> - loginStore.mode != LOGIN_MODE_VULCAN_API - } - .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED } -) - -const val LOGIN_TYPE_EDUDZIENNIK = 5 -const val LOGIN_MODE_EDUDZIENNIK_WEB = 0 -const val LOGIN_METHOD_EDUDZIENNIK_WEB = 100 -val edudziennikLoginMethods = listOf( - LoginMethod(LOGIN_TYPE_EDUDZIENNIK, LOGIN_METHOD_EDUDZIENNIK_WEB, EdudziennikLoginWeb::class.java) - .withIsPossible { _, _ -> true } - .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED } -) - -const val LOGIN_TYPE_PODLASIE = 6 -const val LOGIN_MODE_PODLASIE_API = 0 -const val LOGIN_METHOD_PODLASIE_API = 100 -val podlasieLoginMethods = listOf( - LoginMethod(LOGIN_TYPE_PODLASIE, LOGIN_METHOD_PODLASIE_API, PodlasieLoginApi::class.java) - .withIsPossible { _, _ -> true } - .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED } -) - -val templateLoginMethods = listOf( - LoginMethod(LOGIN_TYPE_TEMPLATE, LOGIN_METHOD_TEMPLATE_WEB, TemplateLoginWeb::class.java) - .withIsPossible { _, _ -> true } - .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_NOT_NEEDED }, - - LoginMethod(LOGIN_TYPE_TEMPLATE, LOGIN_METHOD_TEMPLATE_API, TemplateLoginApi::class.java) - .withIsPossible { _, _ -> true } - .withRequiredLoginMethod { _, _ -> LOGIN_METHOD_TEMPLATE_WEB } -) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt index 5d847e3f..70388abf 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/Regexes.kt @@ -16,6 +16,33 @@ object Regexes { """[^0-9]""".toRegex() } + val HTML_BR by lazy { + """""".toRegex() + } + + val MESSAGE_META by lazy { + """^\[META:([A-z0-9-&=]+)]""".toRegex() + } + + val HTML_INPUT_HIDDEN by lazy { + """""".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 { + """
(.+?)(?: )?""".toRegex() + } + val MOBIDZIENNIK_MESSAGE_READ_DATE by lazy { """czas przeczytania:.+?,\s([0-9]+)\s(.+?)\s([0-9]{4}),\sgodzina\s([0-9:]+)""".toRegex(DOT_MATCHES_ALL) } val MOBIDZIENNIK_MESSAGE_SENT_READ_DATE by lazy { """.+?,\s([0-9]+)\s(.+?)\s([0-9]{4}),\sgodzina\s([0-9:]+)""".toRegex(DOT_MATCHES_ALL) } - val MOBIDZIENNIK_MESSAGE_ATTACHMENT by lazy { - """href="https://.+?\.mobidziennik.pl/.+?&(?:amp;)?zalacznik=([0-9]+)"(?:.+?(.+?)\s*\s*\((.+?),\s*(.+?)\)""".toRegex(DOT_MATCHES_ALL) } - val MOBIDZIENNIK_HOMEWORK_ROW by lazy { + val MOBIDZIENNIK_MOBILE_HOMEWORK_ROW by lazy { """class="rowRolling">(.+?\s*)""".toRegex(DOT_MATCHES_ALL) } - val MOBIDZIENNIK_HOMEWORK_ITEM by lazy { + val MOBIDZIENNIK_MOBILE_HOMEWORK_ITEM by lazy { """

(.+?):\s*(.+?)\s*

""".toRegex(DOT_MATCHES_ALL) } - val MOBIDZIENNIK_HOMEWORK_BODY by lazy { + val MOBIDZIENNIK_MOBILE_HOMEWORK_BODY by lazy { """Treść:(.+?)

""".toRegex(DOT_MATCHES_ALL) } - val MOBIDZIENNIK_HOMEWORK_ID by lazy { - """zadanieFormularz\(([0-9]+),""".toRegex(DOT_MATCHES_ALL) + val MOBIDZIENNIK_MOBILE_HOMEWORK_ID by lazy { + """name="id_zadania" value="([0-9]+)"""".toRegex(DOT_MATCHES_ALL) } - val MOBIDZIENNIK_HOMEWORK_ATTACHMENT by lazy { + val MOBIDZIENNIK_MOBILE_HOMEWORK_ATTACHMENT by lazy { """zalacznik(_zadania)?=([0-9]+)'.+?word-break">(.+?)""".toRegex(DOT_MATCHES_ALL) } - - - val IDZIENNIK_LOGIN_HIDDEN_FIELDS by lazy { - """""".toRegex(DOT_MATCHES_ALL) - } - val IDZIENNIK_LOGIN_ERROR by lazy { - """id="spanErrorMessage">(.*?)(.+?)""".toRegex(DOT_MATCHES_ALL) - } - val IDZIENNIK_LOGIN_FIRST_IS_PARENT by lazy { - """id="ctl00_CzyRodzic" value="([01])" />""".toRegex() - } - val IDZIENNIK_LOGIN_FIRST_SCHOOL_YEAR by lazy { - """name="ctl00\${"$"}dxComboRokSzkolny".+?selected="selected".*?value="([0-9]+)">([0-9]+)/([0-9]+)<""".toRegex(DOT_MATCHES_ALL) - } - val IDZIENNIK_LOGIN_FIRST_STUDENT_SELECT by lazy { - """""".toRegex(DOT_MATCHES_ALL) - } - val IDZIENNIK_LOGIN_FIRST_STUDENT by lazy { - """(.+?)\s(.+?)\s*\((.+?),\s*(.+?)\)""".toRegex(DOT_MATCHES_ALL) - } - val IDZIENNIK_MESSAGES_RECIPIENT_PARENT by lazy { - """(.+?)\s\((.+)\)""".toRegex() - } - /*Szczęśliwy los na dzisiaj to 19. Los na jutro to 22*/ - val IDZIENNIK_WEB_LUCKY_NUMBER by lazy { - """dzisiaj to ([0-9]+)""".toRegex() - } - val IDZIENNIK_WEB_SELECTED_REGISTER by lazy { - """selected="selected" value="([0-9]+)" data-id-ucznia""".toRegex() + val MOBIDZIENNIK_WEB_HOMEWORK_ADDED_DATE by lazy { + """Wpisał\(a\):\s+\s+(.+?), (.+?), ([0-9]{1,2}) (.+?) ([0-9]{4}), godzina ([0-9:]+)""".toRegex() } - - val VULCAN_SHIFT_ANNOTATION by lazy { - """\(przeniesiona (z|na) lekcj[ię] ([0-9]+), (.+)\)""".toRegex() + val MOBIDZIENNIK_TIMETABLE_TOP by lazy { + """

.+?
""".toRegex(DOT_MATCHES_ALL) } + val MOBIDZIENNIK_TIMETABLE_CELL by lazy { + """
.+?style="(.+?)".+?title="(.+?)".+?>\s+(.+?)\s+
""".toRegex(DOT_MATCHES_ALL) + } + val MOBIDZIENNIK_TIMETABLE_LEFT by lazy { + """
.+?
""".toRegex(DOT_MATCHES_ALL) + } + + + val MOBIDZIENNIK_EVENT_CONTENT by lazy { + """

(.+?) \(wpisał\(a\) (.+?) w dniu ([0-9-]{10})\).+?(.+?)(.*?)""".toRegex() - } - val EDUDZIENNIK_ACCOUNT_NAME_START by lazy { - """(.*?)""".toRegex() - } - val EDUDZIENNIK_SUBJECTS_START by lazy { - """(.+?)""".toRegex() - } - - val EDUDZIENNIK_ATTENDANCE_ENTRIES by lazy { - """(.+?)""".toRegex() - } - val EDUDZIENNIK_ATTENDANCE_TYPES by lazy { - """
.*?

(.*?)

""".toRegex(DOT_MATCHES_ALL) - } - val EDUDZIENNIK_ATTENDANCE_TYPE by lazy { - """\((.+?)\) (.+)""".toRegex() - } - - val EDUDZIENNIK_ANNOUNCEMENT_DESCRIPTION by lazy { - """
.*?

(.*?)

""".toRegex(DOT_MATCHES_ALL) - } - val EDUDZIENNIK_HOMEWORK_DESCRIPTION by lazy { - """
(.*?)
""".toRegex(DOT_MATCHES_ALL) - } - - val EDUDZIENNIK_SUBJECT_ID by lazy { - """/Courses/([\w-_]+?)/""".toRegex() - } - val EDUDZIENNIK_GRADE_ID by lazy { - """/Grades/([\w-_]+?)/""".toRegex() - } - val EDUDZIENNIK_EXAM_ID by lazy { - """/Evaluations/([\w-_]+?)/""".toRegex() - } - val EDUDZIENNIK_EVENT_TYPE_ID by lazy { - """/GradeLabels/([\w-_]+?)/""".toRegex() - } - val EDUDZIENNIK_ANNOUNCEMENT_ID by lazy { - """/Announcement/([\w-_]+?)/""".toRegex() - } - val EDUDZIENNIK_HOMEWORK_ID by lazy { - """/Homework/([\w-_]+?)/""".toRegex() - } - val EDUDZIENNIK_TEACHER_ID by lazy { - """/Teachers/([\w-_]+?)/""".toRegex() - } - val EDUDZIENNIK_EVENT_ID by lazy { - """/KlassEvent/([\w-_]+?)/""".toRegex() - } - val EDUDZIENNIK_NOTE_ID by lazy { - """/RegistryNotes/([\w-_]+?)/""".toRegex() - } - - val EDUDZIENNIK_SCHOOL_DETAIL_ID by lazy { - """.*?

(.*?)

.*?
  • """.toRegex(DOT_MATCHES_ALL) - } - val EDUDZIENNIK_CLASS_DETAIL_ID by lazy { - """(.*?)""".toRegex(DOT_MATCHES_ALL) - } - - val EDUDZIENNIK_TEACHERS by lazy { - """
    .*?

    (.+?) (.+?)

    """.toRegex(DOT_MATCHES_ALL) - } - - - - val LINKIFY_DATE_YMD by lazy { """(1\d{3}|20\d{2})[\-./](1[0-2]|0?\d)[\-./]([1-2]\d|3[0-1]|0?\d)""".toRegex() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt index e3692c33..e90813f2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/EdziennikTask.kt @@ -6,27 +6,32 @@ package pl.szczodrzynski.edziennik.data.api.edziennik import com.google.gson.JsonObject import org.greenrobot.eventbus.EventBus -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.api.* -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.Edudziennik +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.ERROR_PROFILE_ARCHIVED import pl.szczodrzynski.edziennik.data.api.edziennik.librus.Librus import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.Mobidziennik import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.Podlasie import pl.szczodrzynski.edziennik.data.api.edziennik.template.Template +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.Usos import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.Vulcan import pl.szczodrzynski.edziennik.data.api.events.RegisterAvailabilityEvent import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.data.api.task.IApiTask import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.LoginType import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.MessageFull +import pl.szczodrzynski.edziennik.ext.isBeforeYear +import pl.szczodrzynski.edziennik.ext.shouldArchive import pl.szczodrzynski.edziennik.utils.Utils.d +import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTask(profileId) { companion object { @@ -37,10 +42,10 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa fun firstLogin(loginStore: LoginStore) = EdziennikTask(-1, FirstLoginRequest(loginStore)) fun sync() = EdziennikTask(-1, SyncRequest()) - fun syncProfile(profileId: Int, viewIds: List>? = null, onlyEndpoints: List? = null, arguments: JsonObject? = null) = EdziennikTask(profileId, SyncProfileRequest(viewIds, onlyEndpoints, arguments)) - fun syncProfileList(profileList: List) = EdziennikTask(-1, SyncProfileListRequest(profileList)) + fun syncProfile(profileId: Int, featureTypes: Set? = null, onlyEndpoints: Set? = null, arguments: JsonObject? = null) = EdziennikTask(profileId, SyncProfileRequest(featureTypes, onlyEndpoints, arguments)) + fun syncProfileList(profileList: Set) = EdziennikTask(-1, SyncProfileListRequest(profileList)) fun messageGet(profileId: Int, message: MessageFull) = EdziennikTask(profileId, MessageGetRequest(message)) - fun messageSend(profileId: Int, recipients: List, subject: String, text: String) = EdziennikTask(profileId, MessageSendRequest(recipients, subject, text)) + fun messageSend(profileId: Int, recipients: Set, subject: String, text: String) = EdziennikTask(profileId, MessageSendRequest(recipients, subject, text)) fun announcementsRead(profileId: Int) = EdziennikTask(profileId, AnnouncementsReadRequest()) fun announcementGet(profileId: Int, announcement: AnnouncementFull) = EdziennikTask(profileId, AnnouncementGetRequest(announcement)) fun attachmentGet(profileId: Int, owner: Any, attachmentId: Long, attachmentName: String) = EdziennikTask(profileId, AttachmentGetRequest(owner, attachmentId, attachmentName)) @@ -90,45 +95,31 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa return } - profile.registerName?.also { registerName -> - var status = app.config.sync.registerAvailability[registerName] - if (status == null || status.nextCheckAt < currentTimeUnix()) { - val api = SzkolnyApi(app) - api.runCatching({ - val availability = getRegisterAvailability() - app.config.sync.registerAvailability = availability - status = availability[registerName] - }, onError = { - val apiError = it.toApiError(TAG) - if (apiError.errorCode == ERROR_API_INVALID_SIGNATURE) { - return@also - } - taskCallback.onError(apiError) - return - }) - } - - if (status?.available != true - || status?.minVersionCode ?: BuildConfig.VERSION_CODE > BuildConfig.VERSION_CODE) { + val error = app.availabilityManager.check(profile) + when (error?.type) { + Type.NOT_AVAILABLE -> { if (EventBus.getDefault().hasSubscriberForEvent(RegisterAvailabilityEvent::class.java)) { - EventBus.getDefault().postSticky( - RegisterAvailabilityEvent(app.config.sync.registerAvailability) - ) + EventBus.getDefault().postSticky(RegisterAvailabilityEvent()) } cancel() taskCallback.onCompleted() return } + Type.API_ERROR -> { + taskCallback.onError(error.apiError!!) + return + } + else -> return@let } } edziennikInterface = when (loginStore.type) { - LOGIN_TYPE_LIBRUS -> Librus(app, profile, loginStore, taskCallback) - LOGIN_TYPE_MOBIDZIENNIK -> Mobidziennik(app, profile, loginStore, taskCallback) - LOGIN_TYPE_VULCAN -> Vulcan(app, profile, loginStore, taskCallback) - LOGIN_TYPE_EDUDZIENNIK -> Edudziennik(app, profile, loginStore, taskCallback) - LOGIN_TYPE_PODLASIE -> Podlasie(app, profile, loginStore, taskCallback) - LOGIN_TYPE_TEMPLATE -> Template(app, profile, loginStore, taskCallback) + LoginType.LIBRUS -> Librus(app, profile, loginStore, taskCallback) + LoginType.MOBIDZIENNIK -> Mobidziennik(app, profile, loginStore, taskCallback) + LoginType.VULCAN -> Vulcan(app, profile, loginStore, taskCallback) + LoginType.PODLASIE -> Podlasie(app, profile, loginStore, taskCallback) + LoginType.TEMPLATE -> Template(app, profile, loginStore, taskCallback) + LoginType.USOS -> Usos(app, profile, loginStore, taskCallback) else -> null } if (edziennikInterface == null) { @@ -137,9 +128,7 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa when (request) { is SyncProfileRequest -> edziennikInterface?.sync( - featureIds = request.viewIds?.flatMap { Features.getIdsByView(it.first, it.second) } - ?: Features.getAllIds(), - viewId = request.viewIds?.get(0)?.first, + featureTypes = request.featureTypes, onlyEndpoints = request.onlyEndpoints, arguments = request.arguments) is MessageGetRequest -> edziennikInterface?.getMessage(request.message) @@ -164,10 +153,10 @@ open class EdziennikTask(override val profileId: Int, val request: Any) : IApiTa data class FirstLoginRequest(val loginStore: LoginStore) class SyncRequest - data class SyncProfileRequest(val viewIds: List>? = null, val onlyEndpoints: List? = null, val arguments: JsonObject? = null) - data class SyncProfileListRequest(val profileList: List) + data class SyncProfileRequest(val featureTypes: Set? = null, val onlyEndpoints: Set? = null, val arguments: JsonObject? = null) + data class SyncProfileListRequest(val profileList: Set) data class MessageGetRequest(val message: MessageFull) - data class MessageSendRequest(val recipients: List, val subject: String, val text: String) + data class MessageSendRequest(val recipients: Set, val subject: String, val text: String) class AnnouncementsReadRequest data class AnnouncementGetRequest(val announcement: AnnouncementFull) data class AttachmentGetRequest(val owner: Any, val attachmentId: Long, val attachmentName: String) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/ProfileArchiver.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/ProfileArchiver.kt index 15da9765..f4f8db2a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/ProfileArchiver.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/ProfileArchiver.kt @@ -6,9 +6,9 @@ package pl.szczodrzynski.edziennik.data.api.edziennik import android.content.Intent import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.Intent -import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.enums.LoginType +import pl.szczodrzynski.edziennik.ext.Intent import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.models.Date @@ -51,35 +51,32 @@ class ProfileArchiver(val app: App, val profile: Profile) { d(TAG, "New profile ID for ${profile.name}: ${profile.id}") when (profile.loginStoreType) { - LOGIN_TYPE_LIBRUS -> { - profile.removeStudentData("isPremium") - profile.removeStudentData("pushDeviceId") - profile.removeStudentData("startPointsSemester1") - profile.removeStudentData("startPointsSemester2") - profile.removeStudentData("enablePointGrades") - profile.removeStudentData("enableDescriptiveGrades") + LoginType.LIBRUS -> { + profile.studentData.remove("isPremium") + profile.studentData.remove("pushDeviceId") + profile.studentData.remove("startPointsSemester1") + profile.studentData.remove("startPointsSemester2") + profile.studentData.remove("enablePointGrades") + profile.studentData.remove("enableDescriptiveGrades") } - LOGIN_TYPE_MOBIDZIENNIK -> { - - } - LOGIN_TYPE_VULCAN -> { + LoginType.MOBIDZIENNIK -> {} + LoginType.VULCAN -> { // DataVulcan.isApiLoginValid() returns false so it will update the semester - profile.removeStudentData("currentSemesterEndDate") - profile.removeStudentData("studentSemesterId") - profile.removeStudentData("studentSemesterNumber") - profile.removeStudentData("semester1Id") - profile.removeStudentData("semester2Id") - profile.removeStudentData("studentClassId") + profile.studentData.remove("currentSemesterEndDate") + profile.studentData.remove("studentSemesterId") + profile.studentData.remove("studentSemesterNumber") + profile.studentData.remove("semester1Id") + profile.studentData.remove("semester2Id") + profile.studentData.remove("studentClassId") } - LOGIN_TYPE_IDZIENNIK -> { - profile.removeStudentData("schoolYearId") - } - LOGIN_TYPE_EDUDZIENNIK -> { - - } - LOGIN_TYPE_PODLASIE -> { - + LoginType.IDZIENNIK -> { + profile.studentData.remove("schoolYearId") } + LoginType.EDUDZIENNIK -> {} + LoginType.PODLASIE -> {} + LoginType.USOS -> {} + LoginType.DEMO -> {} + LoginType.TEMPLATE -> {} } d(TAG, "Processed student data: ${profile.studentData}") diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/DataEdudziennik.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/DataEdudziennik.kt deleted file mode 100644 index 40601249..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/DataEdudziennik.kt +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-12-22 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik - -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_EDUDZIENNIK_WEB -import pl.szczodrzynski.edziennik.data.api.models.Data -import pl.szczodrzynski.edziennik.data.db.entity.* - -/** - * Use http://patorjk.com/software/taag/#p=display&f=Big for the ascii art - * - * Use https://codepen.io/kubasz/pen/RwwwbGN to easily generate the student data getters/setters - */ -class DataEdudziennik(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) { - - fun isWebLoginValid() = webSessionIdExpiryTime-30 > currentTimeUnix() && webSessionId.isNotNullNorEmpty() - - override fun satisfyLoginMethods() { - loginMethods.clear() - if (isWebLoginValid()) { - loginMethods += LOGIN_METHOD_EDUDZIENNIK_WEB - } - } - - override fun generateUserCode() = "$schoolName:$loginEmail:${studentId?.crc32()}" - - private var mLoginEmail: String? = null - var loginEmail: String? - get() { mLoginEmail = mLoginEmail ?: loginStore.getLoginData("email", null); return mLoginEmail } - set(value) { loginStore.putLoginData("email", value); mLoginEmail = value } - - private var mLoginPassword: String? = null - var loginPassword: String? - get() { mLoginPassword = mLoginPassword ?: loginStore.getLoginData("password", null); return mLoginPassword } - set(value) { loginStore.putLoginData("password", value); mLoginPassword = value } - - private var mStudentId: String? = null - var studentId: String? - get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", null); return mStudentId } - set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value } - - private var mSchoolId: String? = null - var schoolId: String? - get() { mSchoolId = mSchoolId ?: profile?.getStudentData("schoolId", null); return mSchoolId } - set(value) { profile?.putStudentData("schoolId", value) ?: return; mSchoolId = value } - - private var mClassId: String? = null - var classId: String? - get() { mClassId = mClassId ?: profile?.getStudentData("classId", null); return mClassId } - set(value) { profile?.putStudentData("classId", value) ?: return; mClassId = value } - - /* __ __ _ - \ \ / / | | - \ \ /\ / /__| |__ - \ \/ \/ / _ \ '_ \ - \ /\ / __/ |_) | - \/ \/ \___|_._*/ - private var mWebSessionId: String? = null - var webSessionId: String? - get() { mWebSessionId = mWebSessionId ?: loginStore.getLoginData("webSessionId", null); return mWebSessionId } - set(value) { loginStore.putLoginData("webSessionId", value); mWebSessionId = value } - - private var mWebSessionIdExpiryTime: Long? = null - var webSessionIdExpiryTime: Long - get() { mWebSessionIdExpiryTime = mWebSessionIdExpiryTime ?: loginStore.getLoginData("webSessionIdExpiryTime", 0L); return mWebSessionIdExpiryTime ?: 0L } - set(value) { loginStore.putLoginData("webSessionIdExpiryTime", value); mWebSessionIdExpiryTime = value } - - /* ____ _ _ - / __ \| | | | - | | | | |_| |__ ___ _ __ - | | | | __| '_ \ / _ \ '__| - | |__| | |_| | | | __/ | - \____/ \__|_| |_|\___|*/ - private var mCurrentSemester: Int? = null - var currentSemester: Int - get() { mCurrentSemester = mCurrentSemester ?: profile?.getStudentData("currentSemester", 1); return mCurrentSemester ?: 1 } - set(value) { profile?.putStudentData("currentSemester", value) ?: return; mCurrentSemester = value } - - private var mSchoolName: String? = null - var schoolName: String? - get() { mSchoolName = mSchoolName ?: profile?.getStudentData("schoolName", null); return mSchoolName } - set(value) { profile?.putStudentData("schoolName", value) ?: return; mSchoolName = value } - - val studentEndpoint: String - get() = "Students/$studentId/" - - val schoolEndpoint: String - get() = "Schools/$schoolId/" - - val classStudentEndpoint: String - get() = "Class/$studentId/" - - val schoolClassEndpoint: String - get() = "Schools/$classId/" - - val studentAndClassEndpoint: String - get() = "Students/$studentId/Klass/$classId/" - - val studentAndClassesEndpoint: String - get() = "Students/$studentId/Classes/$classId/" - - val timetableEndpoint: String - get() = "Plan/$studentId/" - - val studentAndTeacherClassEndpoint: String - get() = "Students/$studentId/Teachers/$classId/" - - val courseStudentEndpoint: String - get() = "Course/$studentId/" - - fun getSubject(longId: String, name: String): Subject { - val id = longId.crc32() - return subjectList.singleOrNull { it.id == id } ?: run { - val subject = Subject(profileId, id, name, name) - subjectList.put(id, subject) - subject - } - } - - fun getTeacher(firstName: String, lastName: String, longId: String? = null): Teacher { - val name = "$firstName $lastName".fixName() - val id = name.crc32() - return teacherList.singleOrNull { it.id == id }?.also { - if (longId != null && it.loginId == null) it.loginId = longId - } ?: run { - val teacher = Teacher(profileId, id, firstName, lastName, longId) - teacherList.put(id, teacher) - teacher - } - } - - fun getTeacherByFirstLast(nameFirstLast: String, longId: String? = null): Teacher { - val nameParts = nameFirstLast.split(" ") - return getTeacher(nameParts[0], nameParts[1], longId) - } - - fun getTeacherByLastFirst(nameLastFirst: String, longId: String? = null): Teacher { - val nameParts = nameLastFirst.split(" ") - return getTeacher(nameParts[1], nameParts[0], longId) - } - - fun getEventType(longId: String, name: String): EventType { - val id = longId.crc16().toLong() - return eventTypes.singleOrNull { it.id == id } ?: run { - val eventType = EventType(profileId, id, name, colorFromName(name)) - eventTypes.put(id, eventType) - eventType - } - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/Edudziennik.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/Edudziennik.kt deleted file mode 100644 index 23d9d3b1..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/Edudziennik.kt +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-12-22 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik - -import com.google.gson.JsonObject -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.data.api.* -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikData -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web.EdudziennikWebGetAnnouncement -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web.EdudziennikWebGetHomework -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.firstlogin.EdudziennikFirstLogin -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login.EdudziennikLogin -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login.EdudziennikLoginWeb -import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback -import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface -import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.data.db.entity.LoginStore -import pl.szczodrzynski.edziennik.data.db.entity.Profile -import pl.szczodrzynski.edziennik.data.db.entity.Teacher -import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull -import pl.szczodrzynski.edziennik.data.db.full.EventFull -import pl.szczodrzynski.edziennik.data.db.full.MessageFull -import pl.szczodrzynski.edziennik.utils.Utils.d - -class Edudziennik(val app: App, val profile: Profile?, val loginStore: LoginStore, val callback: EdziennikCallback) : EdziennikInterface { - companion object { - private const val TAG = "Edudziennik" - } - - val internalErrorList = mutableListOf() - val data: DataEdudziennik - private var afterLogin: (() -> Unit)? = null - - init { - data = DataEdudziennik(app, profile, loginStore).apply { - callback = wrapCallback(this@Edudziennik.callback) - satisfyLoginMethods() - } - } - - private fun completed() { - data.saveData() - callback.onCompleted() - } - - /* _______ _ _ _ _ _ - |__ __| | /\ | | (_) | | | - | | | |__ ___ / \ | | __ _ ___ _ __ _| |_| |__ _ __ ___ - | | | '_ \ / _ \ / /\ \ | |/ _` |/ _ \| '__| | __| '_ \| '_ ` _ \ - | | | | | | __/ / ____ \| | (_| | (_) | | | | |_| | | | | | | | | - |_| |_| |_|\___| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_| - __/ | - |__*/ - override fun sync(featureIds: List, viewId: Int?, onlyEndpoints: List?, arguments: JsonObject?) { - data.arguments = arguments - data.prepare(edudziennikLoginMethods, EdudziennikFeatures, featureIds, viewId, onlyEndpoints) - login() - } - - private fun login(loginMethodId: Int? = null, afterLogin: (() -> Unit)? = null) { - d(TAG, "Trying to login with ${data.targetLoginMethodIds}") - if (internalErrorList.isNotEmpty()) { - d(TAG, " - Internal errors:") - internalErrorList.forEach { d(TAG, " - code $it") } - } - loginMethodId?.let { data.prepareFor(edudziennikLoginMethods, it) } - afterLogin?.let { this.afterLogin = it } - EdudziennikLogin(data) { - data() - } - } - - private fun data() { - d(TAG, "Endpoint IDs: ${data.targetEndpointIds}") - if (internalErrorList.isNotEmpty()) { - d(TAG, " - Internal errors:") - internalErrorList.forEach { d(TAG, " - code $it") } - } - afterLogin?.invoke() ?: EdudziennikData(data) { - completed() - } - } - - override fun getMessage(message: MessageFull) {} - override fun sendMessage(recipients: List, subject: String, text: String) {} - override fun markAllAnnouncementsAsRead() {} - - override fun getAnnouncement(announcement: AnnouncementFull) { - EdudziennikLoginWeb(data) { - EdudziennikWebGetAnnouncement(data, announcement) { - completed() - } - } - } - - override fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) {} - override fun getRecipientList() {} - - override fun getEvent(eventFull: EventFull) { - EdudziennikLoginWeb(data) { - EdudziennikWebGetHomework(data, eventFull) { - completed() - } - } - } - - override fun firstLogin() { EdudziennikFirstLogin(data) { completed() } } - override fun cancel() { - d(TAG, "Cancelled") - data.cancel() - } - - private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { - return object : EdziennikCallback { - override fun onCompleted() { callback.onCompleted() } - override fun onProgress(step: Float) { callback.onProgress(step) } - override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) } - override fun onError(apiError: ApiError) { - if (apiError.errorCode in internalErrorList) { - // finish immediately if the same error occurs twice during the same sync - callback.onError(apiError) - return - } - internalErrorList.add(apiError.errorCode) - when (apiError.errorCode) { - ERROR_EDUDZIENNIK_WEB_SESSION_EXPIRED -> { - login() - } - ERROR_LOGIN_EDUDZIENNIK_WEB_NO_SESSION_ID -> { - login() - } - ERROR_EDUDZIENNIK_WEB_LIMITED_ACCESS -> { - data() - } - else -> callback.onError(apiError) - } - } - } - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/EdudziennikFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/EdudziennikFeatures.kt deleted file mode 100644 index c3fd17ed..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/EdudziennikFeatures.kt +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-12-23 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik - -import pl.szczodrzynski.edziennik.data.api.* -import pl.szczodrzynski.edziennik.data.api.models.Feature - -const val ENDPOINT_EDUDZIENNIK_WEB_START = 1000 -const val ENDPOINT_EDUDZIENNIK_WEB_TEACHERS = 1001 -const val ENDPOINT_EDUDZIENNIK_WEB_GRADES = 1011 -const val ENDPOINT_EDUDZIENNIK_WEB_TIMETABLE = 1012 -const val ENDPOINT_EDUDZIENNIK_WEB_EXAMS = 1013 -const val ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE = 1014 -const val ENDPOINT_EDUDZIENNIK_WEB_ANNOUNCEMENTS = 1015 -const val ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK = 1016 -const val ENDPOINT_EDUDZIENNIK_WEB_EVENTS = 1017 -const val ENDPOINT_EDUDZIENNIK_WEB_NOTES = 1018 -const val ENDPOINT_EDUDZIENNIK_WEB_LUCKY_NUMBER = 1030 - -val EdudziennikFeatures = listOf( - /* School and team info and subjects */ - Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_STUDENT_INFO, listOf( - ENDPOINT_EDUDZIENNIK_WEB_START to LOGIN_METHOD_EDUDZIENNIK_WEB - ), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)), - - /* Teachers */ - Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_TEACHERS, listOf( - ENDPOINT_EDUDZIENNIK_WEB_TEACHERS to LOGIN_METHOD_EDUDZIENNIK_WEB - ), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)), - - /* Timetable */ - Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_TIMETABLE, listOf( - ENDPOINT_EDUDZIENNIK_WEB_TIMETABLE to LOGIN_METHOD_EDUDZIENNIK_WEB - ), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)), - - /* Grades */ - Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_GRADES, listOf( - ENDPOINT_EDUDZIENNIK_WEB_GRADES to LOGIN_METHOD_EDUDZIENNIK_WEB - ), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)), - - /* Agenda */ - Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_AGENDA, listOf( - ENDPOINT_EDUDZIENNIK_WEB_EXAMS to LOGIN_METHOD_EDUDZIENNIK_WEB, - ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK to LOGIN_METHOD_EDUDZIENNIK_WEB, - ENDPOINT_EDUDZIENNIK_WEB_EVENTS to LOGIN_METHOD_EDUDZIENNIK_WEB - ), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)), - - /* Homework */ - Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_HOMEWORK, listOf( - ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK to LOGIN_METHOD_EDUDZIENNIK_WEB - ), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)), - - /* Behaviour */ - Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_BEHAVIOUR, listOf( - ENDPOINT_EDUDZIENNIK_WEB_NOTES to LOGIN_METHOD_EDUDZIENNIK_WEB - ), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)), - - /* Attendance */ - Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_ATTENDANCE, listOf( - ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE to LOGIN_METHOD_EDUDZIENNIK_WEB - ), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)), - - /* Announcements */ - Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_ANNOUNCEMENTS, listOf( - ENDPOINT_EDUDZIENNIK_WEB_ANNOUNCEMENTS to LOGIN_METHOD_EDUDZIENNIK_WEB - ), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)), - - /* Lucky number */ - Feature(LOGIN_TYPE_EDUDZIENNIK, FEATURE_LUCKY_NUMBER, listOf( - ENDPOINT_EDUDZIENNIK_WEB_LUCKY_NUMBER to LOGIN_METHOD_EDUDZIENNIK_WEB - ), listOf(LOGIN_METHOD_EDUDZIENNIK_WEB)) -) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/EdudziennikData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/EdudziennikData.kt deleted file mode 100644 index 7eb58d1c..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/EdudziennikData.kt +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-12-22 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data - -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.* -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web.* -import pl.szczodrzynski.edziennik.utils.Utils - -class EdudziennikData(val data: DataEdudziennik, val onSuccess: () -> Unit) { - companion object { - private const val TAG = "EdudziennikData" - } - - init { - nextEndpoint(onSuccess) - } - - private fun nextEndpoint(onSuccess: () -> Unit) { - if (data.targetEndpointIds.isEmpty()) { - onSuccess() - return - } - if (data.cancelled) { - onSuccess() - return - } - val id = data.targetEndpointIds.firstKey() - val lastSync = data.targetEndpointIds.remove(id) - useEndpoint(id, lastSync) { endpointId -> - data.progress(data.progressStep) - nextEndpoint(onSuccess) - } - } - - private fun useEndpoint(endpointId: Int, lastSync: Long?, onSuccess: (endpointId: Int) -> Unit) { - Utils.d(TAG, "Using endpoint $endpointId. Last sync time = $lastSync") - when (endpointId) { - ENDPOINT_EDUDZIENNIK_WEB_START -> { - data.startProgress(R.string.edziennik_progress_endpoint_data) - EdudziennikWebStart(data, lastSync, onSuccess) - } - ENDPOINT_EDUDZIENNIK_WEB_TEACHERS -> { - data.startProgress(R.string.edziennik_progress_endpoint_teachers) - EdudziennikWebTeachers(data, lastSync, onSuccess) - } - ENDPOINT_EDUDZIENNIK_WEB_GRADES -> { - data.startProgress(R.string.edziennik_progress_endpoint_grades) - EdudziennikWebGrades(data, lastSync, onSuccess) - } - ENDPOINT_EDUDZIENNIK_WEB_TIMETABLE -> { - data.startProgress(R.string.edziennik_progress_endpoint_timetable) - EdudziennikWebTimetable(data, lastSync, onSuccess) - } - ENDPOINT_EDUDZIENNIK_WEB_EXAMS -> { - data.startProgress(R.string.edziennik_progress_endpoint_exams) - EdudziennikWebExams(data, lastSync, onSuccess) - } - ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE -> { - data.startProgress(R.string.edziennik_progress_endpoint_attendance) - EdudziennikWebAttendance(data, lastSync, onSuccess) - } - ENDPOINT_EDUDZIENNIK_WEB_ANNOUNCEMENTS -> { - data.startProgress(R.string.edziennik_progress_endpoint_announcements) - EdudziennikWebAnnouncements(data, lastSync, onSuccess) - } - ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK -> { - data.startProgress(R.string.edziennik_progress_endpoint_homework) - EdudziennikWebHomework(data, lastSync, onSuccess) - } - ENDPOINT_EDUDZIENNIK_WEB_EVENTS -> { - data.startProgress(R.string.edziennik_progress_endpoint_events) - EdudziennikWebEvents(data, lastSync, onSuccess) - } - ENDPOINT_EDUDZIENNIK_WEB_NOTES -> { - data.startProgress(R.string.edziennik_progress_endpoint_notices) - EdudziennikWebNotes(data, lastSync, onSuccess) - } - ENDPOINT_EDUDZIENNIK_WEB_LUCKY_NUMBER -> { - data.startProgress(R.string.edziennik_progress_endpoint_lucky_number) - EdudziennikWebLuckyNumber(data, lastSync, onSuccess) - } - else -> onSuccess(endpointId) - } - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/EdudziennikWeb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/EdudziennikWeb.kt deleted file mode 100644 index f5adfc5d..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/EdudziennikWeb.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-12-22 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data - -import im.wangchao.mhttp.Request -import im.wangchao.mhttp.Response -import im.wangchao.mhttp.callback.TextCallbackHandler -import pl.szczodrzynski.edziennik.data.api.* -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik -import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.utils.Utils.d -import pl.szczodrzynski.edziennik.utils.models.Date - -open class EdudziennikWeb(open val data: DataEdudziennik, open val lastSync: Long?) { - companion object { - private const val TAG = "EdudziennikWeb" - } - - val profileId - get() = data.profile?.id ?: -1 - - val profile - get() = data.profile - - fun webGet(tag: String, endpoint: String, xhr: Boolean = false, semester: Int? = null, onSuccess: (text: String) -> Unit) { - val url = "https://dziennikel.appspot.com/" + when (endpoint.endsWith('/') || endpoint.contains('?') || endpoint.isEmpty()) { - true -> endpoint - else -> "$endpoint/" - } + (semester?.let { "?semester=" + if(it == -1) "all" else it } ?: "") - - d(tag, "Request: Edudziennik/Web - $url") - - val callback = object : TextCallbackHandler() { - override fun onSuccess(text: String?, response: Response?) { - if (text == null || response == null) { - data.error(ApiError(tag, ERROR_RESPONSE_EMPTY) - .withResponse(response)) - return - } - - if (semester == null && url.contains("start")) { - profile?.also { profile -> - val cookies = data.app.cookieJar.getAll("dziennikel.appspot.com") - val semesterCookie = cookies["semester"]?.toIntOrNull() - - semesterCookie?.let { data.currentSemester = it } - - if (semesterCookie == 2 && profile.dateSemester2Start > Date.getToday()) - profile.dateSemester2Start = Date.getToday().stepForward(0, 0, -1) - } - } - - try { - onSuccess(text) - } catch (e: Exception) { - data.error(ApiError(tag, EXCEPTION_EDUDZIENNIK_WEB_REQUEST) - .withThrowable(e) - .withResponse(response) - .withApiResponse(text)) - } - } - - override fun onFailure(response: Response?, throwable: Throwable?) { - val error = when (response?.code()) { - 402 -> ERROR_EDUDZIENNIK_WEB_LIMITED_ACCESS - 403 -> ERROR_EDUDZIENNIK_WEB_SESSION_EXPIRED - else -> ERROR_REQUEST_FAILURE - } - data.error(ApiError(tag, error) - .withResponse(response) - .withThrowable(throwable)) - } - } - - data.app.cookieJar.set("dziennikel.appspot.com", "sessionid", data.webSessionId) - - Request.builder() - .url(url) - .userAgent(EDUDZIENNIK_USER_AGENT) - .apply { - if (xhr) header("X-Requested-With", "XMLHttpRequest") - } - .get() - .callback(callback) - .build() - .enqueue() - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebAnnouncements.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebAnnouncements.kt deleted file mode 100644 index 32ca353c..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebAnnouncements.kt +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-12-26 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web - -import org.jsoup.Jsoup -import pl.szczodrzynski.edziennik.crc32 -import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_ANNOUNCEMENT_ID -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_ANNOUNCEMENTS -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb -import pl.szczodrzynski.edziennik.data.db.entity.Announcement -import pl.szczodrzynski.edziennik.data.db.entity.Metadata -import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.utils.models.Date - -class EdudziennikWebAnnouncements(override val data: DataEdudziennik, - override val lastSync: Long?, - val onSuccess: (endpointId: Int) -> Unit -) : EdudziennikWeb(data, lastSync) { - companion object { - const val TAG = "EdudziennikWebAnnouncements" - } - - init { data.profile?.also { profile -> - webGet(TAG, data.schoolClassEndpoint + "Announcements") { text -> - val doc = Jsoup.parse(text) - - if (doc.getElementsByClass("message").text().trim() != "Brak ogłoszeń.") { - doc.select("table.list tbody tr").forEach { announcementElement -> - val titleElement = announcementElement.child(0).child(0) - - val longId = EDUDZIENNIK_ANNOUNCEMENT_ID.find(titleElement.attr("href"))?.get(1) - ?: return@forEach - val id = longId.crc32() - val subject = titleElement.text() - - val teacherName = announcementElement.child(1).text() - val teacher = data.getTeacherByFirstLast(teacherName) - - val dateString = announcementElement.getElementsByClass("datetime").first().text() - val startDate = Date.fromY_m_d(dateString) - val addedDate = Date.fromIsoHm(dateString) - - val announcementObject = Announcement( - profileId = profileId, - id = id, - subject = subject, - text = null, - startDate = startDate, - endDate = null, - teacherId = teacher.id, - addedDate = addedDate - ).also { - it.idString = longId - } - - data.announcementList.add(announcementObject) - data.metadataList.add(Metadata( - profileId, - Metadata.TYPE_ANNOUNCEMENT, - id, - profile.empty, - profile.empty - )) - } - } - - data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_ANNOUNCEMENTS, SYNC_ALWAYS) - onSuccess(ENDPOINT_EDUDZIENNIK_WEB_ANNOUNCEMENTS) - } - } ?: onSuccess(ENDPOINT_EDUDZIENNIK_WEB_ANNOUNCEMENTS) } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebAttendance.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebAttendance.kt deleted file mode 100644 index 7fe18171..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebAttendance.kt +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-12-24 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web - -import pl.szczodrzynski.edziennik.crc32 -import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_ATTENDANCE_ENTRIES -import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_ATTENDANCE_TYPE -import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_ATTENDANCE_TYPES -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb -import pl.szczodrzynski.edziennik.data.db.entity.Attendance -import pl.szczodrzynski.edziennik.data.db.entity.Metadata -import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.singleOrNull -import pl.szczodrzynski.edziennik.utils.models.Date -import java.util.* - -class EdudziennikWebAttendance(override val data: DataEdudziennik, - override val lastSync: Long?, - val onSuccess: (endpointId: Int) -> Unit -) : EdudziennikWeb(data, lastSync) { - companion object { - private const val TAG = "EdudziennikWebAttendance" - } - - private var requestSemester: Int? = null - - init { - if (profile?.empty == true && data.currentSemester == 2) requestSemester = 1 - getAttendances() - } - - private fun getAttendances() { data.profile?.also { profile -> - webGet(TAG, data.studentEndpoint + "Presence", semester = requestSemester) { text -> - - val attendanceTypes = EDUDZIENNIK_ATTENDANCE_TYPES.find(text)?.get(1)?.split(',')?.map { - val type = EDUDZIENNIK_ATTENDANCE_TYPE.find(it.trim()) - val symbol = type?.get(1)?.trim() ?: "?" - val name = type?.get(2)?.trim() ?: "nieznany rodzaj" - return@map Triple( - symbol, - name, - when (name.toLowerCase(Locale.ROOT)) { - "obecność" -> Attendance.TYPE_PRESENT - "nieobecność" -> Attendance.TYPE_ABSENT - "spóźnienie" -> Attendance.TYPE_BELATED - "nieobecność usprawiedliwiona" -> Attendance.TYPE_ABSENT_EXCUSED - "dzień wolny" -> Attendance.TYPE_DAY_FREE - "brak zajęć" -> Attendance.TYPE_DAY_FREE - "oddelegowany" -> Attendance.TYPE_RELEASED - else -> Attendance.TYPE_UNKNOWN - } - ) - } ?: emptyList() - - EDUDZIENNIK_ATTENDANCE_ENTRIES.findAll(text).forEach { attendanceElement -> - val date = Date.fromY_m_d(attendanceElement[1]) - val lessonNumber = attendanceElement[2].toInt() - val attendanceSymbol = attendanceElement[3] - - val lessons = data.app.db.timetableDao().getAllForDateNow(profileId, date) - val lesson = lessons.firstOrNull { it.lessonNumber == lessonNumber } - - val id = "${date.stringY_m_d}:$lessonNumber:$attendanceSymbol".crc32() - - val (typeSymbol, typeName, baseType) = attendanceTypes.firstOrNull { (symbol, _, _) -> symbol == attendanceSymbol } - ?: return@forEach - - val startTime = data.lessonRanges.singleOrNull { it.lessonNumber == lessonNumber }?.startTime - ?: return@forEach - - val attendanceObject = Attendance( - profileId = profileId, - id = id, - baseType = baseType, - typeName = typeName, - typeShort = data.app.attendanceManager.getTypeShort(baseType), - typeSymbol = typeSymbol, - typeColor = null, - date = date, - startTime = lesson?.displayStartTime ?: startTime, - semester = profile.currentSemester, - teacherId = lesson?.displayTeacherId ?: -1, - subjectId = lesson?.displaySubjectId ?: -1 - ).also { - it.lessonNumber = lessonNumber - } - - data.attendanceList.add(attendanceObject) - if (baseType != Attendance.TYPE_PRESENT) { - data.metadataList.add(Metadata( - profileId, - Metadata.TYPE_ATTENDANCE, - id, - profile.empty || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == Attendance.TYPE_UNKNOWN, - profile.empty || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == Attendance.TYPE_UNKNOWN - )) - } - } - - if (profile.empty && requestSemester == 1 && data.currentSemester == 2) { - requestSemester = null - getAttendances() - } else { - data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE, SYNC_ALWAYS) - onSuccess(ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE) - } - } - } ?: onSuccess(ENDPOINT_EDUDZIENNIK_WEB_ATTENDANCE) } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebEvents.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebEvents.kt deleted file mode 100644 index 35c68478..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebEvents.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2020-1-1 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web - -import org.jsoup.Jsoup -import pl.szczodrzynski.edziennik.crc32 -import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_EVENT_ID -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_EVENTS -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb -import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel -import pl.szczodrzynski.edziennik.data.db.entity.Event -import pl.szczodrzynski.edziennik.data.db.entity.Metadata -import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.utils.models.Date - -class EdudziennikWebEvents(override val data: DataEdudziennik, - override val lastSync: Long?, - val onSuccess: (endpointId: Int) -> Unit -) : EdudziennikWeb(data, lastSync) { - companion object { - const val TAG = "EdudziennikWebEvents" - } - - init { data.profile?.also { profile -> - webGet(TAG, data.studentAndClassesEndpoint + "KlassEvent", xhr = true) { text -> - val doc = Jsoup.parseBodyFragment("" + text.trim() + "
    ") - - doc.getElementsByTag("tr").forEach { eventElement -> - val date = Date.fromY_m_d(eventElement.child(1).text()) - - val titleElement = eventElement.child(2).child(0) - val title = titleElement.text().trim() - - val id = EDUDZIENNIK_EVENT_ID.find(titleElement.attr("href"))?.get(1)?.crc32() - ?: return@forEach - - val eventObject = Event( - profileId = profileId, - id = id, - date = date, - time = null, - topic = title, - color = null, - type = Event.TYPE_CLASS_EVENT, - teacherId = -1, - subjectId = -1, - teamId = data.teamClass?.id ?: -1 - ) - - data.eventList.add(eventObject) - data.metadataList.add(Metadata( - profileId, - Metadata.TYPE_EVENT, - id, - profile.empty, - profile.empty - )) - } - - data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_CLASS_EVENT)) - - data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_EVENTS, SYNC_ALWAYS) - onSuccess(ENDPOINT_EDUDZIENNIK_WEB_EVENTS) - } - } ?: onSuccess(ENDPOINT_EDUDZIENNIK_WEB_EVENTS) } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebExams.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebExams.kt deleted file mode 100644 index f6abf637..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebExams.kt +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-12-24 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web - -import org.jsoup.Jsoup -import pl.szczodrzynski.edziennik.crc32 -import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_EVENT_TYPE_ID -import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_EXAM_ID -import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_SUBJECT_ID -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_EXAMS -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb -import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel -import pl.szczodrzynski.edziennik.data.db.entity.Event -import pl.szczodrzynski.edziennik.data.db.entity.Metadata -import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.utils.models.Date - -class EdudziennikWebExams(override val data: DataEdudziennik, - override val lastSync: Long?, - val onSuccess: (endpointId: Int) -> Unit -) : EdudziennikWeb(data, lastSync) { - companion object { - const val TAG = "EdudziennikWebExams" - } - - init { profile?.also { profile -> - webGet(TAG, data.studentAndClassEndpoint + "Evaluations", xhr = true) { text -> - val doc = Jsoup.parseBodyFragment("" + text.trim() + "
    ") - - doc.select("tr").forEach { examElement -> - val id = EDUDZIENNIK_EXAM_ID.find(examElement.child(0).child(0).attr("href")) - ?.get(1)?.crc32() ?: return@forEach - val topic = examElement.child(0).text().trim() - - val subjectElement = examElement.child(1).child(0) - val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1) - ?: return@forEach - val subjectName = subjectElement.text().trim() - val subject = data.getSubject(subjectId, subjectName) - - val dateString = examElement.child(2).text().trim() - if (dateString.isBlank()) return@forEach - val date = Date.fromY_m_d(dateString) - - val lessons = data.app.db.timetableDao().getAllForDateNow(profileId, date) - val startTime = lessons.firstOrNull { it.displaySubjectId == subject.id }?.displayStartTime - - val eventTypeElement = examElement.child(3).child(0) - val eventTypeId = EDUDZIENNIK_EVENT_TYPE_ID.find(eventTypeElement.attr("href"))?.get(1) - ?: return@forEach - val eventTypeName = eventTypeElement.text() - val eventType = data.getEventType(eventTypeId, eventTypeName) - - val eventObject = Event( - profileId = profileId, - id = id, - date = date, - time = startTime, - topic = topic, - color = null, - type = eventType.id, - teacherId = -1, - subjectId = subject.id, - teamId = data.teamClass?.id ?: -1 - ) - - data.eventList.add(eventObject) - data.metadataList.add(Metadata( - profileId, - Metadata.TYPE_EVENT, - id, - profile.empty, - profile.empty - )) - } - - data.toRemove.add(DataRemoveModel.Events.futureExceptTypes(listOf( - Event.TYPE_HOMEWORK, - Event.TYPE_CLASS_EVENT - ))) - - data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_EXAMS, SYNC_ALWAYS) - onSuccess(ENDPOINT_EDUDZIENNIK_WEB_EXAMS) - } - }} -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGetAnnouncement.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGetAnnouncement.kt deleted file mode 100644 index 5d915e64..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGetAnnouncement.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-12-26 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web - -import org.greenrobot.eventbus.EventBus -import pl.szczodrzynski.edziennik.data.api.Regexes -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb -import pl.szczodrzynski.edziennik.data.api.events.AnnouncementGetEvent -import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull -import pl.szczodrzynski.edziennik.get - -class EdudziennikWebGetAnnouncement(override val data: DataEdudziennik, - private val announcement: AnnouncementFull, - val onSuccess: () -> Unit -) : EdudziennikWeb(data, null) { - companion object { - const val TAG = "EdudziennikWebGetAnnouncement" - } - - init { - webGet(TAG, "Announcement/${announcement.idString}") { text -> - val description = Regexes.EDUDZIENNIK_ANNOUNCEMENT_DESCRIPTION.find(text)?.get(1)?.trim() ?: "" - - announcement.text = description - - EventBus.getDefault().postSticky(AnnouncementGetEvent(announcement)) - - data.announcementList.add(announcement) - onSuccess() - } - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGetHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGetHomework.kt deleted file mode 100644 index 84171699..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGetHomework.kt +++ /dev/null @@ -1,45 +0,0 @@ -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web - -import android.text.Html -import org.greenrobot.eventbus.EventBus -import pl.szczodrzynski.edziennik.data.api.Regexes -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb -import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent -import pl.szczodrzynski.edziennik.data.db.full.EventFull -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.isNotNullNorEmpty - -class EdudziennikWebGetHomework( - override val data: DataEdudziennik, - val event: EventFull, - val onSuccess: () -> Unit -) : EdudziennikWeb(data, null) { - companion object { - const val TAG = "EdudziennikWebGetHomework" - } - - init { - if (event.attachmentNames.isNotNullNorEmpty()) { - val id = event.attachmentNames!![0] - - webGet(TAG, "Homework/$id") { text -> - val description = Regexes.EDUDZIENNIK_HOMEWORK_DESCRIPTION.find(text)?.get(1)?.trim() - - if (description != null) event.topic = Html.fromHtml(description).toString() - - event.homeworkBody = "" - event.attachmentNames = null - - data.eventList += event - data.eventListReplace = true - - EventBus.getDefault().postSticky(EventGetEvent(event)) - onSuccess() - } - } else { - EventBus.getDefault().postSticky(EventGetEvent(event)) - onSuccess() - } - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGrades.kt deleted file mode 100644 index 459f13aa..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebGrades.kt +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-12-25 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web - -import android.graphics.Color -import org.jsoup.Jsoup -import pl.szczodrzynski.edziennik.colorFromCssName -import pl.szczodrzynski.edziennik.crc32 -import pl.szczodrzynski.edziennik.data.api.Regexes -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_GRADES -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb -import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel -import pl.szczodrzynski.edziennik.data.db.entity.Grade -import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NORMAL -import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_POINT_SUM -import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_SEMESTER1_FINAL -import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_SEMESTER1_PROPOSED -import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_SEMESTER2_FINAL -import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_SEMESTER2_PROPOSED -import pl.szczodrzynski.edziennik.data.db.entity.Metadata -import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.utils.Utils -import pl.szczodrzynski.edziennik.utils.models.Date - -class EdudziennikWebGrades(override val data: DataEdudziennik, - override val lastSync: Long?, - val onSuccess: (endpointId: Int) -> Unit -) : EdudziennikWeb(data, lastSync) { - companion object { - private const val TAG = "EdudziennikWebGrades" - } - - private var requestSemester: Int? = null - - init { - if (profile?.empty == true && data.currentSemester == 2) requestSemester = 1 - getGrades() - } - - private fun getGrades() { data.profile?.also { profile -> - webGet(TAG, data.studentEndpoint + "start", semester = requestSemester) { text -> - val semester = requestSemester ?: data.currentSemester - - val doc = Jsoup.parse(text) - val subjects = doc.select("#student_grades tbody").firstOrNull()?.children() - - subjects?.forEach { subjectElement -> - if (subjectElement.id().isBlank()) return@forEach - - val subjectId = subjectElement.id().trim() - val subjectName = subjectElement.child(0).text().trim() - val subject = data.getSubject(subjectId, subjectName) - - val gradeType = when { - subjectElement.select("#sum").text().isNotBlank() -> TYPE_POINT_SUM - else -> TYPE_NORMAL - } - - val gradeCountToAverage = subjectElement.select("#avg").text().isNotBlank() - - val grades = subjectElement.select(".grade[data-edited]") - val gradesInfo = subjectElement.select(".grade-tip") - - val gradeValues = if (grades.isNotEmpty()) { - subjects.select(".avg-$subjectId .grade-tip > p").first() - .text().split('+').map { - val split = it.split('*') - val value = split[1].trim().toFloatOrNull() - val weight = value?.let { split[0].trim().toFloatOrNull() } ?: 0f - - Pair(value ?: 0f, weight) - } - } else emptyList() - - grades.forEachIndexed { index, gradeElement -> - val id = Regexes.EDUDZIENNIK_GRADE_ID.find(gradeElement.attr("href"))?.get(1)?.crc32() - ?: return@forEachIndexed - val (value, weight) = gradeValues[index] - val name = gradeElement.text().trim().let { - if (it.contains(',') || it.contains('.')) { - val replaced = it.replace(',', '.') - val float = replaced.toFloatOrNull() - - if (float != null && float % 1 == 0f) float.toInt().toString() - else it - } else it - } - - val info = gradesInfo[index] - val fullName = info.child(0).text().trim() - val columnName = info.child(4).text().trim() - val comment = info.ownText() - - val description = columnName + if (comment.isNotBlank()) " - $comment" else null - - val teacherName = info.child(1).text() - val teacher = data.getTeacherByLastFirst(teacherName) - - val addedDate = info.child(2).text().split(' ').let { - val day = it[0].toInt() - val month = Utils.monthFromName(it[1]) - val year = it[2].toInt() - - Date(year, month, day).inMillis - } - - val color = Regexes.STYLE_CSS_COLOR.find(gradeElement.attr("style"))?.get(1)?.let { - if (it.startsWith('#')) Color.parseColor(it) - else colorFromCssName(it) - } ?: -1 - - val gradeObject = Grade( - profileId = profileId, - id = id, - name = name, - type = gradeType, - value = value, - weight = if (gradeCountToAverage) weight else 0f, - color = color, - category = fullName, - description = description, - comment = null, - semester = semester, - teacherId = teacher.id, - subjectId = subject.id, - addedDate = addedDate - ) - - data.gradeList.add(gradeObject) - data.metadataList.add(Metadata( - profileId, - Metadata.TYPE_GRADE, - id, - profile.empty, - profile.empty - )) - } - - val proposed = subjectElement.select(".proposal").firstOrNull()?.text()?.trim() - - if (proposed != null && proposed.isNotBlank()) { - val proposedGradeObject = Grade( - profileId = profileId, - id = (-1 * subject.id) - 1, - name = proposed, - type = when (semester) { - 1 -> TYPE_SEMESTER1_PROPOSED - else -> TYPE_SEMESTER2_PROPOSED - }, - value = proposed.toFloatOrNull() ?: 0f, - weight = 0f, - color = -1, - category = null, - description = null, - comment = null, - semester = semester, - teacherId = -1, - subjectId = subject.id - ) - - data.gradeList.add(proposedGradeObject) - data.metadataList.add(Metadata( - profileId, - Metadata.TYPE_GRADE, - proposedGradeObject.id, - profile.empty, - profile.empty - )) - } - - val final = subjectElement.select(".final").firstOrNull()?.text()?.trim() - - if (final != null && final.isNotBlank()) { - val finalGradeObject = Grade( - profileId = profileId, - id = (-1 * subject.id) - 2, - name = final, - type = when (semester) { - 1 -> TYPE_SEMESTER1_FINAL - else -> TYPE_SEMESTER2_FINAL - }, - value = final.toFloatOrNull() ?: 0f, - weight = 0f, - color = -1, - category = null, - description = null, - comment = null, - semester = semester, - teacherId = -1, - subjectId = subject.id - ) - - data.gradeList.add(finalGradeObject) - data.metadataList.add(Metadata( - data.profileId, - Metadata.TYPE_GRADE, - finalGradeObject.id, - profile.empty, - profile.empty - )) - } - } - - if (!subjects.isNullOrEmpty()) { - data.toRemove.addAll(listOf( - TYPE_NORMAL, - TYPE_POINT_SUM, - TYPE_SEMESTER1_PROPOSED, - TYPE_SEMESTER2_PROPOSED, - TYPE_SEMESTER1_FINAL, - TYPE_SEMESTER2_FINAL - ).map { - DataRemoveModel.Grades.semesterWithType(semester, it) - }) - } - - if (profile.empty && requestSemester == 1 && data.currentSemester == 2) { - requestSemester = null - getGrades() - } else { - data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_GRADES, SYNC_ALWAYS) - onSuccess(ENDPOINT_EDUDZIENNIK_WEB_GRADES) - } - } - } ?: onSuccess(ENDPOINT_EDUDZIENNIK_WEB_GRADES) } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebHomework.kt deleted file mode 100644 index 169a5127..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebHomework.kt +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-12-29 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web - -import org.jsoup.Jsoup -import pl.szczodrzynski.edziennik.crc32 -import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_HOMEWORK_ID -import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_SUBJECT_ID -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb -import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel -import pl.szczodrzynski.edziennik.data.db.entity.Event -import pl.szczodrzynski.edziennik.data.db.entity.Metadata -import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.utils.models.Date - -class EdudziennikWebHomework(override val data: DataEdudziennik, - override val lastSync: Long?, - val onSuccess: (endpointId: Int) -> Unit -) : EdudziennikWeb(data, lastSync) { - companion object { - const val TAG = "EdudziennikWebHomework" - } - - init { data.profile?.also { profile -> - webGet(TAG, data.courseStudentEndpoint + "Homework", xhr = true) { text -> - val doc = Jsoup.parseBodyFragment("" + text.trim() + "
    ") - - if (doc.getElementsByClass("message").text().trim() != "Brak prac domowych") { - doc.getElementsByTag("tr").forEach { homeworkElement -> - val dateElement = homeworkElement.getElementsByClass("date").first().child(0) - val idStr = EDUDZIENNIK_HOMEWORK_ID.find(dateElement.attr("href"))?.get(1) ?: return@forEach - val id = idStr.crc32() - val date = Date.fromY_m_d(dateElement.text()) - - val subjectElement = homeworkElement.child(1).child(0) - val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1) - ?: return@forEach - val subjectName = subjectElement.text() - val subject = data.getSubject(subjectId, subjectName) - - val lessons = data.app.db.timetableDao().getAllForDateNow(profileId, date) - val startTime = lessons.firstOrNull { it.subjectId == subject.id }?.displayStartTime - - val teacherName = homeworkElement.child(2).text() - val teacher = data.getTeacherByFirstLast(teacherName) - - val topic = homeworkElement.child(4).text()?.trim() - - val eventObject = Event( - profileId = profileId, - id = id, - date = date, - time = startTime, - topic = topic ?: "", - color = null, - type = Event.TYPE_HOMEWORK, - teacherId = teacher.id, - subjectId = subject.id, - teamId = data.teamClass?.id ?: -1 - ) - - eventObject.attachmentNames = mutableListOf(idStr) - - data.eventList.add(eventObject) - data.metadataList.add(Metadata( - profileId, - Metadata.TYPE_HOMEWORK, - id, - profile.empty, - profile.empty - )) - } - } - - data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_HOMEWORK)) - - data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK, SYNC_ALWAYS) - onSuccess(ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK) - } - } ?: onSuccess(ENDPOINT_EDUDZIENNIK_WEB_HOMEWORK) } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebLuckyNumber.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebLuckyNumber.kt deleted file mode 100644 index db8164c8..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebLuckyNumber.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-12-23 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web - -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_LUCKY_NUMBER -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb -import pl.szczodrzynski.edziennik.data.db.entity.LuckyNumber -import pl.szczodrzynski.edziennik.data.db.entity.Metadata -import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS -import pl.szczodrzynski.edziennik.utils.models.Date - -class EdudziennikWebLuckyNumber(override val data: DataEdudziennik, - override val lastSync: Long?, - val onSuccess: (endpointId: Int) -> Unit -) : EdudziennikWeb(data, lastSync) { - companion object { - private const val TAG = "EdudziennikWebLuckyNumber" - } - - init { data.profile?.also { profile -> - webGet(TAG, data.schoolEndpoint + "Lucky", xhr = true) { text -> - text.toIntOrNull()?.also { luckyNumber -> - val luckyNumberObject = LuckyNumber( - profileId = profileId, - date = Date.getToday(), - number = luckyNumber - ) - - data.luckyNumberList.add(luckyNumberObject) - data.metadataList.add(Metadata( - profileId, - Metadata.TYPE_LUCKY_NUMBER, - luckyNumberObject.date.value.toLong(), - true, - profile.empty - )) - } - - data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_LUCKY_NUMBER, SYNC_ALWAYS) - onSuccess(ENDPOINT_EDUDZIENNIK_WEB_LUCKY_NUMBER) - } - } ?: onSuccess(ENDPOINT_EDUDZIENNIK_WEB_LUCKY_NUMBER) } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebNotes.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebNotes.kt deleted file mode 100644 index 35a78168..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebNotes.kt +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2020-1-1 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web - -import org.jsoup.Jsoup -import pl.szczodrzynski.edziennik.crc32 -import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_NOTE_ID -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_NOTES -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb -import pl.szczodrzynski.edziennik.data.db.entity.Metadata -import pl.szczodrzynski.edziennik.data.db.entity.Notice -import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.utils.models.Date - -class EdudziennikWebNotes(override val data: DataEdudziennik, - override val lastSync: Long?, - val onSuccess: (endpointId: Int) -> Unit -) : EdudziennikWeb(data, lastSync) { - companion object { - const val TAG = "EdudziennikWebNotes" - } - - init { data.profile?.also { profile -> - webGet(TAG, data.classStudentEndpoint + "RegistryNotesStudent", xhr = true) { text -> - val doc = Jsoup.parseBodyFragment("" + text.trim() + "
    ") - - doc.getElementsByTag("tr").forEach { noteElement -> - val dateElement = noteElement.getElementsByClass("date").first().child(0) - val addedDate = Date.fromY_m_d(dateElement.text()).inMillis - - val id = EDUDZIENNIK_NOTE_ID.find(dateElement.attr("href"))?.get(0)?.crc32() - ?: return@forEach - - val teacherName = noteElement.child(1).text() - val teacher = data.getTeacherByFirstLast(teacherName) - - val description = noteElement.child(3).text() - - val noticeObject = Notice( - profileId = profileId, - id = id, - type = Notice.TYPE_NEUTRAL, - semester = profile.currentSemester, - text = description, - category = null, - points = null, - teacherId = teacher.id, - addedDate = addedDate - ) - - data.noticeList.add(noticeObject) - data.metadataList.add(Metadata( - profileId, - Metadata.TYPE_NOTICE, - id, - profile.empty, - profile.empty - )) - } - - data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_NOTES, SYNC_ALWAYS) - onSuccess(ENDPOINT_EDUDZIENNIK_WEB_NOTES) - } - } ?: onSuccess(ENDPOINT_EDUDZIENNIK_WEB_NOTES) } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebStart.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebStart.kt deleted file mode 100644 index 66cb7a59..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebStart.kt +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-12-23 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web - -import pl.szczodrzynski.edziennik.MONTH -import pl.szczodrzynski.edziennik.crc32 -import pl.szczodrzynski.edziennik.data.api.ERROR_EDUDZIENNIK_WEB_TEAM_MISSING -import pl.szczodrzynski.edziennik.data.api.Regexes -import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_SUBJECTS_START -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_START -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb -import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.data.db.entity.Team -import pl.szczodrzynski.edziennik.firstLettersName -import pl.szczodrzynski.edziennik.get - -class EdudziennikWebStart(override val data: DataEdudziennik, - override val lastSync: Long?, - val onSuccess: (endpointId: Int) -> Unit -) : EdudziennikWeb(data, lastSync) { - companion object { - private const val TAG = "EdudziennikWebStart" - } - - init { - webGet(TAG, data.studentEndpoint + "start") { text -> - getSchoolAndTeam(text) - getSubjects(text) - - data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_START, MONTH) - onSuccess(ENDPOINT_EDUDZIENNIK_WEB_START) - } - } - - private fun getSchoolAndTeam(text: String) { - val schoolId = Regexes.EDUDZIENNIK_SCHOOL_DETAIL_ID.find(text)?.get(1)?.trim() - val schoolLongName = Regexes.EDUDZIENNIK_SCHOOL_DETAIL_NAME.find(text)?.get(1)?.trim() - data.schoolId = schoolId - - val classId = Regexes.EDUDZIENNIK_CLASS_DETAIL_ID.find(text)?.get(1)?.trim() - val className = Regexes.EDUDZIENNIK_CLASS_DETAIL_NAME.find(text)?.get(1)?.trim() - data.classId = classId - - if (classId == null || className == null || schoolId == null || schoolLongName == null) { - data.error(ApiError(TAG, ERROR_EDUDZIENNIK_WEB_TEAM_MISSING) - .withApiResponse(text)) - return - } - - val schoolName = schoolId.crc32().toString() + schoolLongName.firstLettersName + "_edu" - data.schoolName = schoolName - - val teamId = classId.crc32() - val teamCode = "$schoolName:$className" - - val teamObject = Team( - data.profileId, - teamId, - className, - Team.TYPE_CLASS, - teamCode, - -1 - ) - - data.teamClass = teamObject - data.teamList.put(teamObject.id, teamObject) - } - - private fun getSubjects(text: String) { - EDUDZIENNIK_SUBJECTS_START.findAll(text).forEach { - val id = it[1].trim() - val name = it[2].trim() - data.getSubject(id, name) - } - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebTeachers.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebTeachers.kt deleted file mode 100644 index d2f3ba15..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebTeachers.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-12-25 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web - -import pl.szczodrzynski.edziennik.MONTH -import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_TEACHERS -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_TEACHERS -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb -import pl.szczodrzynski.edziennik.get - -class EdudziennikWebTeachers(override val data: DataEdudziennik, - override val lastSync: Long?, - val onSuccess: (endpointId: Int) -> Unit -) : EdudziennikWeb(data, lastSync) { - companion object { - private const val TAG = "EdudziennikWebTeachers" - } - - init { - webGet(TAG, data.studentAndTeacherClassEndpoint + "grid") { text -> - EDUDZIENNIK_TEACHERS.findAll(text).forEach { - val lastName = it[1].trim() - val firstName = it[2].trim() - data.getTeacher(firstName, lastName) - } - - data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_TEACHERS, MONTH) - onSuccess(ENDPOINT_EDUDZIENNIK_WEB_TEACHERS) - } - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebTimetable.kt deleted file mode 100644 index 94db15ef..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/data/web/EdudziennikWebTimetable.kt +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-12-23 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.web - -import org.jsoup.Jsoup -import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_SUBJECT_ID -import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_TEACHER_ID -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.ENDPOINT_EDUDZIENNIK_WEB_TIMETABLE -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb -import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel -import pl.szczodrzynski.edziennik.data.db.entity.Lesson -import pl.szczodrzynski.edziennik.data.db.entity.LessonRange -import pl.szczodrzynski.edziennik.data.db.entity.Metadata -import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.getString -import pl.szczodrzynski.edziennik.singleOrNull -import pl.szczodrzynski.edziennik.utils.Utils.d -import pl.szczodrzynski.edziennik.utils.models.Date -import pl.szczodrzynski.edziennik.utils.models.Time -import pl.szczodrzynski.edziennik.utils.models.Week - -class EdudziennikWebTimetable(override val data: DataEdudziennik, - override val lastSync: Long?, - val onSuccess: (endpointId: Int) -> Unit -) : EdudziennikWeb(data, lastSync) { - companion object { - private const val TAG = "EdudziennikWebTimetable" - } - - init { data.profile?.also { profile -> - - val currentWeekStart = Week.getWeekStart() - - if (Date.getToday().weekDay > 4) { - currentWeekStart.stepForward(0, 0, 7) - } - - val getDate = data.arguments?.getString("weekStart") ?: currentWeekStart.stringY_m_d - - val weekStart = Date.fromY_m_d(getDate) - val weekEnd = weekStart.clone().stepForward(0, 0, 6) - - webGet(TAG, data.timetableEndpoint + "print?date=$getDate") { text -> - val doc = Jsoup.parse(text) - - val dataDays = mutableListOf() - val dataStart = weekStart.clone() - while (dataStart <= weekEnd) { - dataDays += dataStart.value - dataStart.stepForward(0, 0, 1) - } - - val table = doc.select("#Schedule tbody").first() - - if (!table.text().contains("Brak planu lekcji.")) { - table.children().forEach { row -> - val rowElements = row.children() - - val lessonNumber = rowElements[0].text().toInt() - - val times = rowElements[1].text().split('-') - val startTime = Time.fromH_m(times[0].trim()) - val endTime = Time.fromH_m(times[1].trim()) - - data.lessonRanges.singleOrNull { - it.lessonNumber == lessonNumber && it.startTime == startTime && it.endTime == endTime - } ?: run { - data.lessonRanges.put(lessonNumber, LessonRange(profileId, lessonNumber, startTime, endTime)) - } - - rowElements.subList(2, rowElements.size).forEachIndexed { index, lesson -> - val course = lesson.select(".course").firstOrNull() ?: return@forEachIndexed - val info = course.select("span > span") - - if (info.isEmpty()) return@forEachIndexed - - val type = when (course.hasClass("substitute")) { - true -> Lesson.TYPE_CHANGE - else -> Lesson.TYPE_NORMAL - } - - /* Getting subject */ - - val subjectElement = info[0].child(0) - val subjectId = EDUDZIENNIK_SUBJECT_ID.find(subjectElement.attr("href"))?.get(1) - ?: return@forEachIndexed - val subjectName = subjectElement.text().trim() - val subject = data.getSubject(subjectId, subjectName) - - /* Getting teacher */ - - val teacherId = if (info.size >= 2) { - val teacherElement = info[1].child(0) - val teacherLongId = EDUDZIENNIK_TEACHER_ID.find(teacherElement.attr("href"))?.get(1) - val teacherName = teacherElement.text().trim() - data.getTeacherByLastFirst(teacherName, teacherLongId).id - } else null - - val lessonObject = Lesson(profileId, -1).also { - it.type = type - it.date = weekStart.clone().stepForward(0, 0, index) - it.lessonNumber = lessonNumber - it.startTime = startTime - it.endTime = endTime - it.subjectId = subject.id - it.teacherId = teacherId - it.teamId = data.teamClass?.id - - it.id = it.buildId() - } - - data.lessonList.add(lessonObject) - dataDays.remove(lessonObject.date!!.value) - - if (type != Lesson.TYPE_NORMAL) { - val seen = profile.empty || lessonObject.date!! < Date.getToday() - - data.metadataList.add(Metadata( - profileId, - Metadata.TYPE_LESSON_CHANGE, - lessonObject.id, - seen, - seen - )) - } - } - } - } - - for (day in dataDays) { - val lessonDate = Date.fromValue(day) - data.lessonList += Lesson(profileId, lessonDate.value.toLong()).apply { - type = Lesson.TYPE_NO_LESSONS - date = lessonDate - } - } - - d(TAG, "Clearing lessons between ${weekStart.stringY_m_d} and ${weekEnd.stringY_m_d} - timetable downloaded for $getDate") - - data.toRemove.add(DataRemoveModel.Timetable.between(weekStart, weekEnd)) - data.setSyncNext(ENDPOINT_EDUDZIENNIK_WEB_TIMETABLE, SYNC_ALWAYS) - onSuccess(ENDPOINT_EDUDZIENNIK_WEB_TIMETABLE) - } - } ?: onSuccess(ENDPOINT_EDUDZIENNIK_WEB_TIMETABLE) } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/firstlogin/EdudziennikFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/firstlogin/EdudziennikFirstLogin.kt deleted file mode 100644 index a93632b1..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/firstlogin/EdudziennikFirstLogin.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-12-22 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.firstlogin - -import org.greenrobot.eventbus.EventBus -import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_EDUDZIENNIK -import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_ACCOUNT_NAME_START -import pl.szczodrzynski.edziennik.data.api.Regexes.EDUDZIENNIK_STUDENTS_START -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.data.EdudziennikWeb -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login.EdudziennikLoginWeb -import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent -import pl.szczodrzynski.edziennik.data.db.entity.Profile -import pl.szczodrzynski.edziennik.fixName -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.getShortName -import pl.szczodrzynski.edziennik.set - -class EdudziennikFirstLogin(val data: DataEdudziennik, val onSuccess: () -> Unit) { - companion object { - private const val TAG = "EdudziennikFirstLogin" - } - - private val web = EdudziennikWeb(data, null) - private val profileList = mutableListOf() - - init { - val loginStoreId = data.loginStore.id - val loginStoreType = LOGIN_TYPE_EDUDZIENNIK - var firstProfileId = loginStoreId - - EdudziennikLoginWeb(data) { - web.webGet(TAG, "") { text -> - val accountNameLong = EDUDZIENNIK_ACCOUNT_NAME_START.find(text)?.get(1)?.fixName() - - EDUDZIENNIK_STUDENTS_START.findAll(text).forEach { - val studentId = it[1] - val studentNameLong = it[2].fixName() - - if (studentId.isBlank() || studentNameLong.isBlank()) return@forEach - - val studentNameShort = studentNameLong.getShortName() - val accountName = if (accountNameLong == studentNameLong) null else accountNameLong - - val profile = Profile( - firstProfileId++, - loginStoreId, - loginStoreType, - studentNameLong, - data.loginEmail, - studentNameLong, - studentNameShort, - accountName - ).apply { - studentData["studentId"] = studentId - } - profileList.add(profile) - } - - EventBus.getDefault().postSticky(FirstLoginFinishedEvent(profileList, data.loginStore)) - onSuccess() - } - } - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/login/EdudziennikLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/login/EdudziennikLogin.kt deleted file mode 100644 index 1cb2fd07..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/login/EdudziennikLogin.kt +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-12-22 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login - -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_EDUDZIENNIK_WEB -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik -import pl.szczodrzynski.edziennik.utils.Utils - -class EdudziennikLogin(val data: DataEdudziennik, val onSuccess: () -> Unit) { - companion object { - private const val TAG = "EdudziennikLogin" - } - - private var cancelled = false - - init { - nextLoginMethod(onSuccess) - } - - private fun nextLoginMethod(onSuccess: () -> Unit) { - if (data.targetLoginMethodIds.isEmpty()) { - onSuccess() - return - } - if (cancelled) { - onSuccess() - return - } - useLoginMethod(data.targetLoginMethodIds.removeAt(0)) { usedMethodId -> - data.progress(data.progressStep) - if (usedMethodId != -1) - data.loginMethods.add(usedMethodId) - nextLoginMethod(onSuccess) - } - } - - private fun useLoginMethod(loginMethodId: Int, onSuccess: (usedMethodId: Int) -> Unit) { - // this should never be true - if (data.loginMethods.contains(loginMethodId)) { - onSuccess(-1) - return - } - Utils.d(TAG, "Using login method $loginMethodId") - when (loginMethodId) { - LOGIN_METHOD_EDUDZIENNIK_WEB -> { - data.startProgress(R.string.edziennik_progress_login_edudziennik_web) - EdudziennikLoginWeb(data) { onSuccess(loginMethodId) } - } - } - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/login/EdudziennikLoginWeb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/login/EdudziennikLoginWeb.kt deleted file mode 100644 index 6b1615a2..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/edudziennik/login/EdudziennikLoginWeb.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-12-22 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.login - -import im.wangchao.mhttp.Request -import im.wangchao.mhttp.Response -import im.wangchao.mhttp.callback.TextCallbackHandler -import pl.szczodrzynski.edziennik.data.api.* -import pl.szczodrzynski.edziennik.data.api.edziennik.edudziennik.DataEdudziennik -import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.getUnixDate -import pl.szczodrzynski.edziennik.isNotNullNorEmpty -import pl.szczodrzynski.edziennik.utils.Utils.d - -class EdudziennikLoginWeb(val data: DataEdudziennik, val onSuccess: () -> Unit) { - companion object { - private const val TAG = "EdudziennikLoginWeb" - } - - init { run { - if (data.isWebLoginValid()) { - onSuccess() - } - else { - data.app.cookieJar.clear("dziennikel.appspot.com") - if (data.loginEmail.isNotNullNorEmpty() && data.loginPassword.isNotNullNorEmpty()) { - loginWithCredentials() - } - else { - data.error(ApiError(TAG, ERROR_LOGIN_DATA_MISSING)) - } - } - }} - - private fun loginWithCredentials() { - d(TAG, "Request: Edudziennik/Login/Web - https://dziennikel.appspot.com/login/?next=/") - - val callback = object : TextCallbackHandler() { - override fun onSuccess(text: String?, response: Response?) { - if (text == null || response == null) { - data.error(ApiError(TAG, ERROR_RESPONSE_EMPTY) - .withResponse(response)) - return - } - - val url = response.raw().request().url().toString() - - if (!url.contains("Student")) { - when { - text.contains("Wprowadzono nieprawidłową nazwę użytkownika lub hasło.") -> ERROR_LOGIN_EDUDZIENNIK_WEB_INVALID_LOGIN - else -> ERROR_LOGIN_EDUDZIENNIK_WEB_OTHER - }.let { errorCode -> - data.error(ApiError(TAG, errorCode) - .withApiResponse(text) - .withResponse(response)) - return - } - } - - val cookies = data.app.cookieJar.getAll("dziennikel.appspot.com") - val sessionId = cookies["sessionid"] - - if (sessionId == null) { - data.error(ApiError(TAG, ERROR_LOGIN_EDUDZIENNIK_WEB_NO_SESSION_ID) - .withResponse(response) - .withApiResponse(text)) - return - } - - data.webSessionId = sessionId - data.webSessionIdExpiryTime = response.getUnixDate() + 45 * 60 /* 45 min */ - onSuccess() - } - - override fun onFailure(response: Response?, throwable: Throwable?) { - data.error(ApiError(TAG, ERROR_REQUEST_FAILURE) - .withResponse(response) - .withThrowable(throwable)) - } - } - - Request.builder() - .url("https://dziennikel.appspot.com/login/?next=/") - .userAgent(EDUDZIENNIK_USER_AGENT) - .contentType("application/x-www-form-urlencoded") - .addParameter("email", data.loginEmail) - .addParameter("password", data.loginPassword) - .addParameter("auth_method", "password") - .addParameter("next", "/") - .post() - .callback(callback) - .build() - .enqueue() - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/DataLibrus.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/DataLibrus.kt index e77dd50d..0702c203 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/DataLibrus.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/DataLibrus.kt @@ -5,15 +5,14 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.currentTimeUnix -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_LIBRUS_API -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_LIBRUS_MESSAGES -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_LIBRUS_PORTAL -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_LIBRUS_SYNERGIA import pl.szczodrzynski.edziennik.data.api.models.Data import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.Profile -import pl.szczodrzynski.edziennik.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.ext.currentTimeUnix +import pl.szczodrzynski.edziennik.ext.getStudentData +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.ext.set class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) { @@ -25,15 +24,15 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app override fun satisfyLoginMethods() { loginMethods.clear() if (isPortalLoginValid()) - loginMethods += LOGIN_METHOD_LIBRUS_PORTAL + loginMethods += LoginMethod.LIBRUS_PORTAL if (isApiLoginValid()) - loginMethods += LOGIN_METHOD_LIBRUS_API + loginMethods += LoginMethod.LIBRUS_API if (isSynergiaLoginValid()) { - loginMethods += LOGIN_METHOD_LIBRUS_SYNERGIA + loginMethods += LoginMethod.LIBRUS_SYNERGIA app.cookieJar.set("synergia.librus.pl", "DZIENNIKSID", synergiaSessionId) } if (isMessagesLoginValid()) { - loginMethods += LOGIN_METHOD_LIBRUS_MESSAGES + loginMethods += LoginMethod.LIBRUS_MESSAGES app.cookieJar.set("wiadomosci.librus.pl", "DZIENNIKSID", messagesSessionId) } } @@ -120,7 +119,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mApiLogin: String? = null var apiLogin: String? get() { mApiLogin = mApiLogin ?: profile?.getStudentData("accountLogin", null); return mApiLogin } - set(value) { profile?.putStudentData("accountLogin", value); mApiLogin = value } + set(value) { profile["accountLogin"] = value; mApiLogin = value } /** * A Synergia password. * Used: for login (API Login Method) in Synergia mode. @@ -129,7 +128,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mApiPassword: String? = null var apiPassword: String? get() { mApiPassword = mApiPassword ?: profile?.getStudentData("accountPassword", null); return mApiPassword } - set(value) { profile?.putStudentData("accountPassword", value); mApiPassword = value } + set(value) { profile["accountPassword"] = value; mApiPassword = value } /** * A JST login Code. @@ -138,7 +137,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mApiCode: String? = null var apiCode: String? get() { mApiCode = mApiCode ?: loginStore.getLoginData("accountCode", null); return mApiCode } - set(value) { profile?.putStudentData("accountCode", value); mApiCode = value } + set(value) { profile["accountCode"] = value; mApiCode = value } /** * A JST login PIN. * Used only during first login in JST mode. @@ -146,7 +145,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mApiPin: String? = null var apiPin: String? get() { mApiPin = mApiPin ?: loginStore.getLoginData("accountPin", null); return mApiPin } - set(value) { profile?.putStudentData("accountPin", value); mApiPin = value } + set(value) { profile["accountPin"] = value; mApiPin = value } /** * A Synergia API access token. @@ -157,7 +156,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mApiAccessToken: String? = null var apiAccessToken: String? get() { mApiAccessToken = mApiAccessToken ?: profile?.getStudentData("accountToken", null); return mApiAccessToken } - set(value) { mApiAccessToken = value; profile?.putStudentData("accountToken", value) ?: return; } + set(value) { mApiAccessToken = value; profile["accountToken"] = value ?: return; } /** * A Synergia API refresh token. * Used when refreshing the [apiAccessToken] in JST, Synergia modes. @@ -165,7 +164,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mApiRefreshToken: String? = null var apiRefreshToken: String? get() { mApiRefreshToken = mApiRefreshToken ?: profile?.getStudentData("accountRefreshToken", null); return mApiRefreshToken } - set(value) { mApiRefreshToken = value; profile?.putStudentData("accountRefreshToken", value) ?: return; } + set(value) { mApiRefreshToken = value; profile["accountRefreshToken"] = value ?: return; } /** * The expiry time for [apiAccessToken], as a UNIX timestamp. * Used when refreshing the [apiAccessToken] in JST, Synergia modes. @@ -174,7 +173,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mApiTokenExpiryTime: Long? = null var apiTokenExpiryTime: Long get() { mApiTokenExpiryTime = mApiTokenExpiryTime ?: profile?.getStudentData("accountTokenTime", 0L); return mApiTokenExpiryTime ?: 0L } - set(value) { mApiTokenExpiryTime = value; profile?.putStudentData("accountTokenTime", value) ?: return; } + set(value) { mApiTokenExpiryTime = value; profile["accountTokenTime"] = value; } /** * A push device ID, generated by Librus when registering @@ -184,7 +183,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mPushDeviceId: Int? = null var pushDeviceId: Int get() { mPushDeviceId = mPushDeviceId ?: profile?.getStudentData("pushDeviceId", 0); return mPushDeviceId ?: 0 } - set(value) { mPushDeviceId = value; profile?.putStudentData("pushDeviceId", value) ?: return; } + set(value) { mPushDeviceId = value; profile["pushDeviceId"] = value; } /* _____ _ / ____| (_) @@ -201,7 +200,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mSynergiaSessionId: String? = null var synergiaSessionId: String? get() { mSynergiaSessionId = mSynergiaSessionId ?: profile?.getStudentData("accountSID", null); return mSynergiaSessionId } - set(value) { profile?.putStudentData("accountSID", value) ?: return; mSynergiaSessionId = value } + set(value) { profile["accountSID"] = value; mSynergiaSessionId = value } /** * The expiry time for [synergiaSessionId], as a UNIX timestamp. * Used in endpoints with Synergia login method. @@ -210,7 +209,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mSynergiaSessionIdExpiryTime: Long? = null var synergiaSessionIdExpiryTime: Long get() { mSynergiaSessionIdExpiryTime = mSynergiaSessionIdExpiryTime ?: profile?.getStudentData("accountSIDTime", 0L); return mSynergiaSessionIdExpiryTime ?: 0L } - set(value) { profile?.putStudentData("accountSIDTime", value) ?: return; mSynergiaSessionIdExpiryTime = value } + set(value) { profile["accountSIDTime"] = value; mSynergiaSessionIdExpiryTime = value } /** @@ -220,7 +219,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mMessagesSessionId: String? = null var messagesSessionId: String? get() { mMessagesSessionId = mMessagesSessionId ?: profile?.getStudentData("messagesSID", null); return mMessagesSessionId } - set(value) { profile?.putStudentData("messagesSID", value) ?: return; mMessagesSessionId = value } + set(value) { profile["messagesSID"] = value; mMessagesSessionId = value } /** * The expiry time for [messagesSessionId], as a UNIX timestamp. * Used in endpoints with Messages login method. @@ -229,7 +228,7 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mMessagesSessionIdExpiryTime: Long? = null var messagesSessionIdExpiryTime: Long get() { mMessagesSessionIdExpiryTime = mMessagesSessionIdExpiryTime ?: profile?.getStudentData("messagesSIDTime", 0L); return mMessagesSessionIdExpiryTime ?: 0L } - set(value) { profile?.putStudentData("messagesSIDTime", value) ?: return; mMessagesSessionIdExpiryTime = value } + set(value) { profile["messagesSIDTime"] = value; mMessagesSessionIdExpiryTime = value } /* ____ _ _ / __ \| | | | @@ -239,42 +238,42 @@ class DataLibrus(app: App, profile: Profile?, loginStore: LoginStore) : Data(app \____/ \__|_| |_|\___|*/ var isPremium get() = profile?.getStudentData("isPremium", false) ?: false - set(value) { profile?.putStudentData("isPremium", value) } + set(value) { profile["isPremium"] = value } private var mSchoolName: String? = null var schoolName: String? get() { mSchoolName = mSchoolName ?: profile?.getStudentData("schoolName", null); return mSchoolName } - set(value) { profile?.putStudentData("schoolName", value) ?: return; mSchoolName = value } + set(value) { profile["schoolName"] = value; mSchoolName = value } private var mUnitId: Long? = null var unitId: Long get() { mUnitId = mUnitId ?: profile?.getStudentData("unitId", 0L); return mUnitId ?: 0L } - set(value) { profile?.putStudentData("unitId", value) ?: return; mUnitId = value } + set(value) { profile["unitId"] = value; mUnitId = value } private var mStartPointsSemester1: Int? = null var startPointsSemester1: Int get() { mStartPointsSemester1 = mStartPointsSemester1 ?: profile?.getStudentData("startPointsSemester1", 0); return mStartPointsSemester1 ?: 0 } - set(value) { profile?.putStudentData("startPointsSemester1", value) ?: return; mStartPointsSemester1 = value } + set(value) { profile["startPointsSemester1"] = value; mStartPointsSemester1 = value } private var mStartPointsSemester2: Int? = null var startPointsSemester2: Int get() { mStartPointsSemester2 = mStartPointsSemester2 ?: profile?.getStudentData("startPointsSemester2", 0); return mStartPointsSemester2 ?: 0 } - set(value) { profile?.putStudentData("startPointsSemester2", value) ?: return; mStartPointsSemester2 = value } + set(value) { profile["startPointsSemester2"] = value; mStartPointsSemester2 = value } private var mEnablePointGrades: Boolean? = null var enablePointGrades: Boolean get() { mEnablePointGrades = mEnablePointGrades ?: profile?.getStudentData("enablePointGrades", true); return mEnablePointGrades ?: true } - set(value) { profile?.putStudentData("enablePointGrades", value) ?: return; mEnablePointGrades = value } + set(value) { profile["enablePointGrades"] = value; mEnablePointGrades = value } private var mEnableDescriptiveGrades: Boolean? = null var enableDescriptiveGrades: Boolean get() { mEnableDescriptiveGrades = mEnableDescriptiveGrades ?: profile?.getStudentData("enableDescriptiveGrades", true); return mEnableDescriptiveGrades ?: true } - set(value) { profile?.putStudentData("enableDescriptiveGrades", value) ?: return; mEnableDescriptiveGrades = value } + set(value) { profile["enableDescriptiveGrades"] = value; mEnableDescriptiveGrades = value } private var mTimetableNotPublic: Boolean? = null var timetableNotPublic: Boolean get() { mTimetableNotPublic = mTimetableNotPublic ?: profile?.getStudentData("timetableNotPublic", false); return mTimetableNotPublic ?: false } - set(value) { profile?.putStudentData("timetableNotPublic", value) ?: return; mTimetableNotPublic = value } + set(value) { profile["timetableNotPublic"] = value; mTimetableNotPublic = value } /** * Set to false when Recaptcha helper doesn't provide a working token. diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/Librus.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/Librus.kt index abf3ac5f..5b29b9fe 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/Librus.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/Librus.kt @@ -16,6 +16,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages.Librus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia.* import pl.szczodrzynski.edziennik.data.api.edziennik.librus.firstlogin.LibrusFirstLogin import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLogin +import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface import pl.szczodrzynski.edziennik.data.api.models.ApiError @@ -23,6 +24,8 @@ import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.MessageFull @@ -57,19 +60,19 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va |_| |_| |_|\___| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_| __/ | |__*/ - override fun sync(featureIds: List, viewId: Int?, onlyEndpoints: List?, arguments: JsonObject?) { + override fun sync(featureTypes: Set?, onlyEndpoints: Set?, arguments: JsonObject?) { data.arguments = arguments - data.prepare(librusLoginMethods, LibrusFeatures, featureIds, viewId, onlyEndpoints) + data.prepare(LibrusFeatures, featureTypes, onlyEndpoints) login() } - private fun login(loginMethodId: Int? = null, afterLogin: (() -> Unit)? = null) { - d(TAG, "Trying to login with ${data.targetLoginMethodIds}") + private fun login(loginMethod: LoginMethod? = null, afterLogin: (() -> Unit)? = null) { + d(TAG, "Trying to login with ${data.targetLoginMethods}") if (internalErrorList.isNotEmpty()) { d(TAG, " - Internal errors:") internalErrorList.forEach { d(TAG, " - code $it") } } - loginMethodId?.let { data.prepareFor(librusLoginMethods, it) } + loginMethod?.let { data.prepareFor(it) } afterLogin?.let { this.afterLogin = it } LibrusLogin(data) { data() @@ -77,7 +80,7 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va } private fun data() { - d(TAG, "Endpoint IDs: ${data.targetEndpointIds}") + d(TAG, "Endpoint IDs: ${data.targetEndpoints}") if (internalErrorList.isNotEmpty()) { d(TAG, " - Internal errors:") internalErrorList.forEach { d(TAG, " - code $it") } @@ -88,14 +91,14 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va } override fun getMessage(message: MessageFull) { - login(LOGIN_METHOD_LIBRUS_MESSAGES) { + login(LoginMethod.LIBRUS_MESSAGES) { if (data.messagesLoginSuccessful) LibrusMessagesGetMessage(data, message) { completed() } else LibrusSynergiaGetMessage(data, message) { completed() } } } - override fun sendMessage(recipients: List, subject: String, text: String) { - login(LOGIN_METHOD_LIBRUS_MESSAGES) { + override fun sendMessage(recipients: Set, subject: String, text: String) { + login(LoginMethod.LIBRUS_MESSAGES) { LibrusMessagesSendMessage(data, recipients, subject, text) { completed() } @@ -103,7 +106,7 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va } override fun markAllAnnouncementsAsRead() { - login(LOGIN_METHOD_LIBRUS_SYNERGIA) { + login(LoginMethod.LIBRUS_SYNERGIA) { LibrusSynergiaMarkAllAnnouncementsAsRead(data) { completed() } @@ -111,7 +114,7 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va } override fun getAnnouncement(announcement: AnnouncementFull) { - login(LOGIN_METHOD_LIBRUS_API) { + login(LoginMethod.LIBRUS_API) { LibrusApiAnnouncementMarkAsRead(data, announcement) { completed() } @@ -121,13 +124,13 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va override fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) { when (owner) { is Message -> { - login(LOGIN_METHOD_LIBRUS_SYNERGIA) { + login(LoginMethod.LIBRUS_SYNERGIA) { if (data.messagesLoginSuccessful) LibrusMessagesGetAttachment(data, owner, attachmentId, attachmentName) { completed() } LibrusSynergiaGetAttachment(data, owner, attachmentId, attachmentName) { completed() } } } is EventFull -> { - login(LOGIN_METHOD_LIBRUS_SYNERGIA) { + login(LoginMethod.LIBRUS_SYNERGIA) { LibrusSynergiaHomeworkGetAttachment(data, owner, attachmentId, attachmentName) { completed() } @@ -138,7 +141,7 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va } override fun getRecipientList() { - login(LOGIN_METHOD_LIBRUS_MESSAGES) { + login(LoginMethod.LIBRUS_MESSAGES) { LibrusMessagesGetRecipientList(data) { completed() } @@ -146,7 +149,7 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va } override fun getEvent(eventFull: EventFull) { - login(LOGIN_METHOD_LIBRUS_SYNERGIA) { + login(LoginMethod.LIBRUS_SYNERGIA) { LibrusSynergiaGetHomework(data, eventFull) { completed() } @@ -162,6 +165,7 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { return object : EdziennikCallback { override fun onCompleted() { callback.onCompleted() } + override fun onRequiresUserAction(event: UserActionRequiredEvent) { callback.onRequiresUserAction(event) } override fun onProgress(step: Float) { callback.onProgress(step) } override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) } override fun onError(apiError: ApiError) { @@ -173,27 +177,27 @@ class Librus(val app: App, val profile: Profile?, val loginStore: LoginStore, va internalErrorList.add(apiError.errorCode) when (apiError.errorCode) { ERROR_LIBRUS_PORTAL_ACCESS_DENIED -> { - data.loginMethods.remove(LOGIN_METHOD_LIBRUS_PORTAL) - data.prepareFor(librusLoginMethods, LOGIN_METHOD_LIBRUS_PORTAL) + data.loginMethods.remove(LoginMethod.LIBRUS_PORTAL) + data.prepareFor(LoginMethod.LIBRUS_PORTAL) data.portalTokenExpiryTime = 0 login() } ERROR_LIBRUS_API_ACCESS_DENIED, ERROR_LIBRUS_API_TOKEN_EXPIRED -> { - data.loginMethods.remove(LOGIN_METHOD_LIBRUS_API) - data.prepareFor(librusLoginMethods, LOGIN_METHOD_LIBRUS_API) + data.loginMethods.remove(LoginMethod.LIBRUS_API) + data.prepareFor(LoginMethod.LIBRUS_API) data.apiTokenExpiryTime = 0 login() } ERROR_LIBRUS_SYNERGIA_ACCESS_DENIED -> { - data.loginMethods.remove(LOGIN_METHOD_LIBRUS_SYNERGIA) - data.prepareFor(librusLoginMethods, LOGIN_METHOD_LIBRUS_SYNERGIA) + data.loginMethods.remove(LoginMethod.LIBRUS_SYNERGIA) + data.prepareFor(LoginMethod.LIBRUS_SYNERGIA) data.synergiaSessionIdExpiryTime = 0 login() } ERROR_LIBRUS_MESSAGES_ACCESS_DENIED -> { - data.loginMethods.remove(LOGIN_METHOD_LIBRUS_MESSAGES) - data.prepareFor(librusLoginMethods, LOGIN_METHOD_LIBRUS_MESSAGES) + data.loginMethods.remove(LoginMethod.LIBRUS_MESSAGES) + data.prepareFor(LoginMethod.LIBRUS_MESSAGES) data.messagesSessionIdExpiryTime = 0 login() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/LibrusFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/LibrusFeatures.kt index c14a00df..eaa05b99 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/LibrusFeatures.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/LibrusFeatures.kt @@ -4,8 +4,10 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus -import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.models.Feature +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.data.db.enums.LoginType const val ENDPOINT_LIBRUS_API_ME = 1001 const val ENDPOINT_LIBRUS_API_SCHOOLS = 1002 @@ -58,14 +60,14 @@ const val ENDPOINT_LIBRUS_MESSAGES_TRASH = 3030 val LibrusFeatures = listOf( - Feature(LOGIN_TYPE_LIBRUS, FEATURE_ALWAYS_NEEDED, listOf( - ENDPOINT_LIBRUS_API_LESSONS to LOGIN_METHOD_LIBRUS_API - ), listOf(LOGIN_METHOD_LIBRUS_API)), + Feature(LoginType.LIBRUS, FeatureType.ALWAYS_NEEDED, listOf( + ENDPOINT_LIBRUS_API_LESSONS to LoginMethod.LIBRUS_API + )), // push config - Feature(LOGIN_TYPE_LIBRUS, FEATURE_PUSH_CONFIG, listOf( - ENDPOINT_LIBRUS_API_PUSH_CONFIG to LOGIN_METHOD_LIBRUS_API - ), listOf(LOGIN_METHOD_LIBRUS_API)).withShouldSync { data -> + Feature(LoginType.LIBRUS, FeatureType.PUSH_CONFIG, listOf( + ENDPOINT_LIBRUS_API_PUSH_CONFIG to LoginMethod.LIBRUS_API + )).withShouldSync { data -> (data as DataLibrus).isPremium && !data.app.config.sync.tokenLibrusList.contains(data.profileId) }, @@ -76,72 +78,72 @@ val LibrusFeatures = listOf( /** * Timetable - using API. */ - Feature(LOGIN_TYPE_LIBRUS, FEATURE_TIMETABLE, listOf( - ENDPOINT_LIBRUS_API_TIMETABLES to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_SUBSTITUTIONS to LOGIN_METHOD_LIBRUS_API - ), listOf(LOGIN_METHOD_LIBRUS_API)), + Feature(LoginType.LIBRUS, FeatureType.TIMETABLE, listOf( + ENDPOINT_LIBRUS_API_TIMETABLES to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_SUBSTITUTIONS to LoginMethod.LIBRUS_API + )), /** * Agenda - using API. * Events, Parent-teacher meetings, free days (teacher/school/class). */ - Feature(LOGIN_TYPE_LIBRUS, FEATURE_AGENDA, listOf( - ENDPOINT_LIBRUS_API_EVENTS to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_EVENT_TYPES to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_PT_MEETINGS to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_TEACHER_FREE_DAY_TYPES to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_TEACHER_FREE_DAYS to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_SCHOOL_FREE_DAYS to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_CLASS_FREE_DAYS to LOGIN_METHOD_LIBRUS_API - ), listOf(LOGIN_METHOD_LIBRUS_API)), + Feature(LoginType.LIBRUS, FeatureType.AGENDA, listOf( + ENDPOINT_LIBRUS_API_EVENTS to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_EVENT_TYPES to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_PT_MEETINGS to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_TEACHER_FREE_DAY_TYPES to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_TEACHER_FREE_DAYS to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_SCHOOL_FREE_DAYS to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_CLASS_FREE_DAYS to LoginMethod.LIBRUS_API + )), /** * Grades - using API. * All grades + categories. */ - Feature(LOGIN_TYPE_LIBRUS, FEATURE_GRADES, listOf( - ENDPOINT_LIBRUS_API_NORMAL_GRADE_CATEGORIES to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_POINT_GRADE_CATEGORIES to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADE_CATEGORIES to LOGIN_METHOD_LIBRUS_API, + Feature(LoginType.LIBRUS, FeatureType.GRADES, listOf( + ENDPOINT_LIBRUS_API_NORMAL_GRADE_CATEGORIES to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_POINT_GRADE_CATEGORIES to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADE_CATEGORIES to LoginMethod.LIBRUS_API, // Commented out, because TextGrades/Categories is the same as Grades/Categories - /* ENDPOINT_LIBRUS_API_TEXT_GRADE_CATEGORIES to LOGIN_METHOD_LIBRUS_API, */ - ENDPOINT_LIBRUS_API_DESCRIPTIVE_TEXT_GRADE_CATEGORIES to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_CATEGORIES to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_COMMENTS to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_NORMAL_GRADES to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_POINT_GRADES to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADES to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_TEXT_GRADES to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_DESCRIPTIVE_TEXT_GRADES to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADES to LOGIN_METHOD_LIBRUS_API - ), listOf(LOGIN_METHOD_LIBRUS_API)), + /* ENDPOINT_LIBRUS_API_TEXT_GRADE_CATEGORIES to LoginMethod.LIBRUS_API, */ + ENDPOINT_LIBRUS_API_DESCRIPTIVE_TEXT_GRADE_CATEGORIES to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_CATEGORIES to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_COMMENTS to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_NORMAL_GRADES to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_POINT_GRADES to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADES to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_TEXT_GRADES to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_DESCRIPTIVE_TEXT_GRADES to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADES to LoginMethod.LIBRUS_API + )), /** * Homework - using API. * Sync only if account has premium access. */ - /*Feature(LOGIN_TYPE_LIBRUS, FEATURE_HOMEWORK, listOf( - ENDPOINT_LIBRUS_API_HOMEWORK to LOGIN_METHOD_LIBRUS_API - ), listOf(LOGIN_METHOD_LIBRUS_API)).withShouldSync { data -> + /*Feature(LoginType.LIBRUS, FeatureType.HOMEWORK, listOf( + ENDPOINT_LIBRUS_API_HOMEWORK to LoginMethod.LIBRUS_API + )).withShouldSync { data -> (data as DataLibrus).isPremium },*/ /** * Behaviour - using API. */ - Feature(LOGIN_TYPE_LIBRUS, FEATURE_BEHAVIOUR, listOf( - ENDPOINT_LIBRUS_API_NOTICES to LOGIN_METHOD_LIBRUS_API - ), listOf(LOGIN_METHOD_LIBRUS_API)), + Feature(LoginType.LIBRUS, FeatureType.BEHAVIOUR, listOf( + ENDPOINT_LIBRUS_API_NOTICES to LoginMethod.LIBRUS_API + )), /** * Attendance - using API. */ - Feature(LOGIN_TYPE_LIBRUS, FEATURE_ATTENDANCE, listOf( - ENDPOINT_LIBRUS_API_ATTENDANCE_TYPES to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_ATTENDANCES to LOGIN_METHOD_LIBRUS_API - ), listOf(LOGIN_METHOD_LIBRUS_API)), + Feature(LoginType.LIBRUS, FeatureType.ATTENDANCE, listOf( + ENDPOINT_LIBRUS_API_ATTENDANCE_TYPES to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_ATTENDANCES to LoginMethod.LIBRUS_API + )), /** * Announcements - using API. */ - Feature(LOGIN_TYPE_LIBRUS, FEATURE_ANNOUNCEMENTS, listOf( - ENDPOINT_LIBRUS_API_ANNOUNCEMENTS to LOGIN_METHOD_LIBRUS_API - ), listOf(LOGIN_METHOD_LIBRUS_API)), + Feature(LoginType.LIBRUS, FeatureType.ANNOUNCEMENTS, listOf( + ENDPOINT_LIBRUS_API_ANNOUNCEMENTS to LoginMethod.LIBRUS_API + )), @@ -150,99 +152,99 @@ val LibrusFeatures = listOf( /** * Student info - using API. */ - Feature(LOGIN_TYPE_LIBRUS, FEATURE_STUDENT_INFO, listOf( - ENDPOINT_LIBRUS_API_ME to LOGIN_METHOD_LIBRUS_API - ), listOf(LOGIN_METHOD_LIBRUS_API)), + Feature(LoginType.LIBRUS, FeatureType.STUDENT_INFO, listOf( + ENDPOINT_LIBRUS_API_ME to LoginMethod.LIBRUS_API + )), /** * School info - using API. */ - Feature(LOGIN_TYPE_LIBRUS, FEATURE_SCHOOL_INFO, listOf( - ENDPOINT_LIBRUS_API_SCHOOLS to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_UNITS to LOGIN_METHOD_LIBRUS_API - ), listOf(LOGIN_METHOD_LIBRUS_API)), + Feature(LoginType.LIBRUS, FeatureType.SCHOOL_INFO, listOf( + ENDPOINT_LIBRUS_API_SCHOOLS to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_UNITS to LoginMethod.LIBRUS_API + )), /** * Class info - using API. */ - Feature(LOGIN_TYPE_LIBRUS, FEATURE_CLASS_INFO, listOf( - ENDPOINT_LIBRUS_API_CLASSES to LOGIN_METHOD_LIBRUS_API - ), listOf(LOGIN_METHOD_LIBRUS_API)), + Feature(LoginType.LIBRUS, FeatureType.CLASS_INFO, listOf( + ENDPOINT_LIBRUS_API_CLASSES to LoginMethod.LIBRUS_API + )), /** * Team info - using API. */ - Feature(LOGIN_TYPE_LIBRUS, FEATURE_TEAM_INFO, listOf( - ENDPOINT_LIBRUS_API_VIRTUAL_CLASSES to LOGIN_METHOD_LIBRUS_API - ), listOf(LOGIN_METHOD_LIBRUS_API)), + Feature(LoginType.LIBRUS, FeatureType.TEAM_INFO, listOf( + ENDPOINT_LIBRUS_API_VIRTUAL_CLASSES to LoginMethod.LIBRUS_API + )), /** * Lucky number - using API. */ - Feature(LOGIN_TYPE_LIBRUS, FEATURE_LUCKY_NUMBER, listOf( - ENDPOINT_LIBRUS_API_LUCKY_NUMBER to LOGIN_METHOD_LIBRUS_API - ), listOf(LOGIN_METHOD_LIBRUS_API)).withShouldSync { data -> data.shouldSyncLuckyNumber() }, + Feature(LoginType.LIBRUS, FeatureType.LUCKY_NUMBER, listOf( + ENDPOINT_LIBRUS_API_LUCKY_NUMBER to LoginMethod.LIBRUS_API + )).withShouldSync { data -> data.shouldSyncLuckyNumber() }, /** * Teacher list - using API. */ - Feature(LOGIN_TYPE_LIBRUS, FEATURE_TEACHERS, listOf( - ENDPOINT_LIBRUS_API_USERS to LOGIN_METHOD_LIBRUS_API - ), listOf(LOGIN_METHOD_LIBRUS_API)), + Feature(LoginType.LIBRUS, FeatureType.TEACHERS, listOf( + ENDPOINT_LIBRUS_API_USERS to LoginMethod.LIBRUS_API + )), /** * Subject list - using API. */ - Feature(LOGIN_TYPE_LIBRUS, FEATURE_SUBJECTS, listOf( - ENDPOINT_LIBRUS_API_SUBJECTS to LOGIN_METHOD_LIBRUS_API - ), listOf(LOGIN_METHOD_LIBRUS_API)), + Feature(LoginType.LIBRUS, FeatureType.SUBJECTS, listOf( + ENDPOINT_LIBRUS_API_SUBJECTS to LoginMethod.LIBRUS_API + )), /** * Classroom list - using API. */ - Feature(LOGIN_TYPE_LIBRUS, FEATURE_CLASSROOMS, listOf( - ENDPOINT_LIBRUS_API_CLASSROOMS to LOGIN_METHOD_LIBRUS_API - ), listOf(LOGIN_METHOD_LIBRUS_API)), + Feature(LoginType.LIBRUS, FeatureType.CLASSROOMS, listOf( + ENDPOINT_LIBRUS_API_CLASSROOMS to LoginMethod.LIBRUS_API + )), /** * Student info - using synergia scrapper. */ - Feature(LOGIN_TYPE_LIBRUS, FEATURE_STUDENT_INFO, listOf( - ENDPOINT_LIBRUS_SYNERGIA_INFO to LOGIN_METHOD_LIBRUS_SYNERGIA - ), listOf(LOGIN_METHOD_LIBRUS_SYNERGIA)), + Feature(LoginType.LIBRUS, FeatureType.STUDENT_INFO, listOf( + ENDPOINT_LIBRUS_SYNERGIA_INFO to LoginMethod.LIBRUS_SYNERGIA + )), /** * Student number - using synergia scrapper. */ - Feature(LOGIN_TYPE_LIBRUS, FEATURE_STUDENT_NUMBER, listOf( - ENDPOINT_LIBRUS_SYNERGIA_INFO to LOGIN_METHOD_LIBRUS_SYNERGIA - ), listOf(LOGIN_METHOD_LIBRUS_SYNERGIA)), + Feature(LoginType.LIBRUS, FeatureType.STUDENT_NUMBER, listOf( + ENDPOINT_LIBRUS_SYNERGIA_INFO to LoginMethod.LIBRUS_SYNERGIA + )), /** * Grades - using API + synergia scrapper. */ - /*Feature(LOGIN_TYPE_LIBRUS, FEATURE_GRADES, listOf( - ENDPOINT_LIBRUS_API_NORMAL_GC to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_API_NORMAL_GRADES to LOGIN_METHOD_LIBRUS_API, - ENDPOINT_LIBRUS_SYNERGIA_GRADES to LOGIN_METHOD_LIBRUS_SYNERGIA - ), listOf(LOGIN_METHOD_LIBRUS_API, LOGIN_METHOD_LIBRUS_SYNERGIA)),*/ - /*Endpoint(LOGIN_TYPE_LIBRUS, FEATURE_GRADES, listOf( - ENDPOINT_LIBRUS_SYNERGIA_GRADES to LOGIN_METHOD_LIBRUS_SYNERGIA - ), listOf(LOGIN_METHOD_LIBRUS_SYNERGIA)),*/ + /*Feature(LoginType.LIBRUS, FeatureType.GRADES, listOf( + ENDPOINT_LIBRUS_API_NORMAL_GC to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_API_NORMAL_GRADES to LoginMethod.LIBRUS_API, + ENDPOINT_LIBRUS_SYNERGIA_GRADES to LoginMethod.LIBRUS_SYNERGIA + )),*/ + /*Endpoint(LoginType.LIBRUS, FeatureType.GRADES, listOf( + ENDPOINT_LIBRUS_SYNERGIA_GRADES to LoginMethod.LIBRUS_SYNERGIA + )),*/ /** * Homework - using scrapper. * Sync only if account has not premium access. */ - Feature(LOGIN_TYPE_LIBRUS, FEATURE_HOMEWORK, listOf( - ENDPOINT_LIBRUS_SYNERGIA_HOMEWORK to LOGIN_METHOD_LIBRUS_SYNERGIA - ), listOf(LOGIN_METHOD_LIBRUS_SYNERGIA))/*.withShouldSync { data -> + Feature(LoginType.LIBRUS, FeatureType.HOMEWORK, listOf( + ENDPOINT_LIBRUS_SYNERGIA_HOMEWORK to LoginMethod.LIBRUS_SYNERGIA + ))/*.withShouldSync { data -> !(data as DataLibrus).isPremium }*/, /** * Messages inbox - using messages website. */ - Feature(LOGIN_TYPE_LIBRUS, FEATURE_MESSAGES_INBOX, listOf( - ENDPOINT_LIBRUS_MESSAGES_RECEIVED to LOGIN_METHOD_LIBRUS_MESSAGES - ), listOf(LOGIN_METHOD_LIBRUS_MESSAGES)), + Feature(LoginType.LIBRUS, FeatureType.MESSAGES_INBOX, listOf( + ENDPOINT_LIBRUS_MESSAGES_RECEIVED to LoginMethod.LIBRUS_MESSAGES + )), /** * Messages sent - using messages website. */ - Feature(LOGIN_TYPE_LIBRUS, FEATURE_MESSAGES_SENT, listOf( - ENDPOINT_LIBRUS_MESSAGES_SENT to LOGIN_METHOD_LIBRUS_MESSAGES - ), listOf(LOGIN_METHOD_LIBRUS_MESSAGES)) + Feature(LoginType.LIBRUS, FeatureType.MESSAGES_SENT, listOf( + ENDPOINT_LIBRUS_MESSAGES_SENT to LoginMethod.LIBRUS_MESSAGES + )) ) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/LibrusRecaptchaHelper.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/LibrusRecaptchaHelper.kt index 3652f18a..a44076b4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/LibrusRecaptchaHelper.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/LibrusRecaptchaHelper.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import pl.szczodrzynski.edziennik.startCoroutineTimer +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer import kotlin.coroutines.CoroutineContext class LibrusRecaptchaHelper( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusApi.kt index d06cc219..2b736a0f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusApi.kt @@ -11,7 +11,7 @@ import im.wangchao.mhttp.callback.JsonCallbackHandler import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.utils.Utils.d import java.net.HttpURLConnection.* diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusData.kt index 877d803b..a5d9256e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusData.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusData.kt @@ -24,7 +24,7 @@ class LibrusData(val data: DataLibrus, val onSuccess: () -> Unit) { } private fun nextEndpoint(onSuccess: () -> Unit) { - if (data.targetEndpointIds.isEmpty()) { + if (data.targetEndpoints.isEmpty()) { onSuccess() return } @@ -32,8 +32,8 @@ class LibrusData(val data: DataLibrus, val onSuccess: () -> Unit) { onSuccess() return } - val id = data.targetEndpointIds.firstKey() - val lastSync = data.targetEndpointIds.remove(id) + val id = data.targetEndpoints.firstKey() + val lastSync = data.targetEndpoints.remove(id) useEndpoint(id, lastSync) { endpointId -> data.progress(data.progressStep) nextEndpoint(onSuccess) @@ -182,10 +182,6 @@ class LibrusData(val data: DataLibrus, val onSuccess: () -> Unit) { data.startProgress(R.string.edziennik_progress_endpoint_pt_meetings) LibrusApiPtMeetings(data, lastSync, onSuccess) } - ENDPOINT_LIBRUS_API_TEACHER_FREE_DAY_TYPES -> { - data.startProgress(R.string.edziennik_progress_endpoint_teacher_free_day_types) - LibrusApiTeacherFreeDayTypes(data, lastSync, onSuccess) - } ENDPOINT_LIBRUS_API_TEACHER_FREE_DAYS -> { data.startProgress(R.string.edziennik_progress_endpoint_teacher_free_days) LibrusApiTeacherFreeDays(data, lastSync, onSuccess) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusPortal.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusPortal.kt index 4a5e0af6..d1d34226 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusPortal.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/LibrusPortal.kt @@ -7,7 +7,7 @@ import im.wangchao.mhttp.callback.JsonCallbackHandler import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.utils.Utils.d import java.net.HttpURLConnection diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAnnouncementMarkAsRead.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAnnouncementMarkAsRead.kt index 23cfceb4..8f45f9df 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAnnouncementMarkAsRead.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAnnouncementMarkAsRead.kt @@ -12,6 +12,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.api.events.AnnouncementGetEvent import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull class LibrusApiAnnouncementMarkAsRead(override val data: DataLibrus, @@ -34,7 +35,7 @@ class LibrusApiAnnouncementMarkAsRead(override val data: DataLibrus, data.setSeenMetadataList.add(Metadata( profileId, - Metadata.TYPE_ANNOUNCEMENT, + MetadataType.ANNOUNCEMENT, announcement.id, announcement.seen, announcement.notified diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAnnouncements.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAnnouncements.kt index d19be231..ad7c0df8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAnnouncements.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAnnouncements.kt @@ -11,6 +11,8 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.Announcement import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.models.Date class LibrusApiAnnouncements(override val data: DataLibrus, @@ -53,7 +55,7 @@ class LibrusApiAnnouncements(override val data: DataLibrus, data.announcementList.add(announcementObject) data.setSeenMetadataList.add(Metadata( profileId, - Metadata.TYPE_ANNOUNCEMENT, + MetadataType.ANNOUNCEMENT, id, read, profile.empty || read diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAttendanceTypes.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAttendanceTypes.kt index c620d13d..417a1de0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAttendanceTypes.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAttendanceTypes.kt @@ -11,6 +11,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.Attendance import pl.szczodrzynski.edziennik.data.db.entity.AttendanceType +import pl.szczodrzynski.edziennik.ext.* class LibrusApiAttendanceTypes(override val data: DataLibrus, override val lastSync: Long?, @@ -58,7 +59,7 @@ class LibrusApiAttendanceTypes(override val data: DataLibrus, )) } - data.setSyncNext(ENDPOINT_LIBRUS_API_ATTENDANCE_TYPES, 2*DAY) + data.setSyncNext(ENDPOINT_LIBRUS_API_ATTENDANCE_TYPES, 2* DAY) onSuccess(ENDPOINT_LIBRUS_API_ATTENDANCE_TYPES) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAttendances.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAttendances.kt index 7857dc2a..b9bb6152 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAttendances.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiAttendances.kt @@ -12,6 +12,8 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.Attendance import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.models.Date class LibrusApiAttendances(override val data: DataLibrus, @@ -75,7 +77,7 @@ class LibrusApiAttendances(override val data: DataLibrus, if(type?.baseType != Attendance.TYPE_PRESENT) { data.metadataList.add(Metadata( profileId, - Metadata.TYPE_ATTENDANCE, + MetadataType.ATTENDANCE, id, profile?.empty ?: false || type?.baseType == Attendance.TYPE_PRESENT_CUSTOM || type?.baseType == Attendance.TYPE_UNKNOWN, profile?.empty ?: false || type?.baseType == Attendance.TYPE_PRESENT_CUSTOM || type?.baseType == Attendance.TYPE_UNKNOWN diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGradeCategories.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGradeCategories.kt index 650dfbb5..24992be1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGradeCategories.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGradeCategories.kt @@ -10,6 +10,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_CATEGORIES import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.GradeCategory +import pl.szczodrzynski.edziennik.ext.* class LibrusApiBehaviourGradeCategories(override val data: DataLibrus, override val lastSync: Long?, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGradeComments.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGradeComments.kt index eedb1d1d..4f44c0f0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGradeComments.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGradeComments.kt @@ -10,6 +10,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.GradeCategory import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.ext.* class LibrusApiBehaviourGradeComments(override val data: DataLibrus, override val lastSync: Long?, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGrades.kt index fae79a50..9d9e3015 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGrades.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiBehaviourGrades.kt @@ -14,6 +14,8 @@ import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_POINT_SUM import pl.szczodrzynski.edziennik.data.db.entity.GradeCategory import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.models.Date import java.text.DecimalFormat @@ -62,7 +64,7 @@ class LibrusApiBehaviourGrades(override val data: DataLibrus, data.gradeList.add(semester1StartGradeObject) data.metadataList.add(Metadata( profileId, - Metadata.TYPE_GRADE, + MetadataType.GRADE, semester1StartGradeObject.id, true, true @@ -90,7 +92,7 @@ class LibrusApiBehaviourGrades(override val data: DataLibrus, data.gradeList.add(semester2StartGradeObject) data.metadataList.add(Metadata( profileId, - Metadata.TYPE_GRADE, + MetadataType.GRADE, semester2StartGradeObject.id, true, true @@ -164,7 +166,7 @@ class LibrusApiBehaviourGrades(override val data: DataLibrus, data.gradeList.add(gradeObject) data.metadataList.add(Metadata( profileId, - Metadata.TYPE_GRADE, + MetadataType.GRADE, id, profile.empty, profile.empty diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiClasses.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiClasses.kt index 171e80e1..43baf873 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiClasses.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiClasses.kt @@ -4,14 +4,14 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api -import pl.szczodrzynski.edziennik.DAY import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_CLASSES import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.Team -import pl.szczodrzynski.edziennik.getJsonObject -import pl.szczodrzynski.edziennik.getLong -import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.ext.DAY +import pl.szczodrzynski.edziennik.ext.getJsonObject +import pl.szczodrzynski.edziennik.ext.getLong +import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.utils.models.Date class LibrusApiClasses(override val data: DataLibrus, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiClassrooms.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiClassrooms.kt index 80a11105..6fbebca5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiClassrooms.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiClassrooms.kt @@ -9,6 +9,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_CLASSROOMS import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.Classroom +import pl.szczodrzynski.edziennik.ext.* import java.util.* class LibrusApiClassrooms(override val data: DataLibrus, @@ -25,8 +26,8 @@ class LibrusApiClassrooms(override val data: DataLibrus, classrooms?.forEach { classroom -> val id = classroom.getLong("Id") ?: return@forEach - val name = classroom.getString("Name")?.toLowerCase(Locale.getDefault()) ?: "" - val symbol = classroom.getString("Symbol")?.toLowerCase(Locale.getDefault()) ?: "" + val name = classroom.getString("Name")?.lowercase() ?: "" + val symbol = classroom.getString("Symbol")?.lowercase() ?: "" val nameShort = name.fixWhiteSpaces().split(" ").onEach { it[0] }.joinToString() val symbolParts = symbol.fixWhiteSpaces().split(" ") @@ -40,7 +41,7 @@ class LibrusApiClassrooms(override val data: DataLibrus, data.classrooms.put(id, Classroom(profileId, id, friendlyName)) } - data.setSyncNext(ENDPOINT_LIBRUS_API_CLASSROOMS, 4*DAY) + data.setSyncNext(ENDPOINT_LIBRUS_API_CLASSROOMS, 4* DAY) onSuccess(ENDPOINT_LIBRUS_API_CLASSROOMS) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiDescriptiveGradeCategories.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiDescriptiveGradeCategories.kt index 94ce6972..c48d34f8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiDescriptiveGradeCategories.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiDescriptiveGradeCategories.kt @@ -10,6 +10,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADE_CATEGORIES import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.GradeCategory +import pl.szczodrzynski.edziennik.ext.* class LibrusApiDescriptiveGradeCategories(override val data: DataLibrus, override val lastSync: Long?, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiDescriptiveGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiDescriptiveGrades.kt index 8cc4f1c1..d77d1bf4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiDescriptiveGrades.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiDescriptiveGrades.kt @@ -15,6 +15,8 @@ import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_TEXT import pl.szczodrzynski.edziennik.data.db.entity.GradeCategory import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.models.Date class LibrusApiDescriptiveGrades(override val data: DataLibrus, @@ -72,7 +74,7 @@ class LibrusApiDescriptiveGrades(override val data: DataLibrus, data.gradeList.add(gradeObject) data.metadataList.add(Metadata( profileId, - Metadata.TYPE_GRADE, + MetadataType.GRADE, id, profile.empty, profile.empty diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiEventTypes.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiEventTypes.kt index 331b8249..c2129ba6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiEventTypes.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiEventTypes.kt @@ -9,6 +9,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_EVENT_TYPES import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.EventType +import pl.szczodrzynski.edziennik.ext.* class LibrusApiEventTypes(override val data: DataLibrus, override val lastSync: Long?, @@ -30,7 +31,7 @@ class LibrusApiEventTypes(override val data: DataLibrus, data.eventTypes.put(id, EventType(profileId, id, name, color)) } - data.setSyncNext(ENDPOINT_LIBRUS_API_EVENT_TYPES, 4*DAY) + data.setSyncNext(ENDPOINT_LIBRUS_API_EVENT_TYPES, 4* DAY) onSuccess(ENDPOINT_LIBRUS_API_EVENT_TYPES) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiEvents.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiEvents.kt index 7a76f88e..79722a8b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiEvents.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiEvents.kt @@ -13,6 +13,8 @@ import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -70,7 +72,7 @@ class LibrusApiEvents(override val data: DataLibrus, data.metadataList.add( Metadata( profileId, - Metadata.TYPE_EVENT, + MetadataType.EVENT, id, profile?.empty ?: false, profile?.empty ?: false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGradeCategories.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGradeCategories.kt index 518e643f..c6c4fecc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGradeCategories.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGradeCategories.kt @@ -11,6 +11,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.GradeCategory import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.ext.* class LibrusApiGradeCategories(override val data: DataLibrus, override val lastSync: Long?, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGradeComments.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGradeComments.kt index 971ac35b..0e9ea16c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGradeComments.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGradeComments.kt @@ -4,12 +4,12 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api -import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.GradeCategory import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.ext.* class LibrusApiGradeComments(override val data: DataLibrus, override val lastSync: Long?, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGrades.kt index c2e2d0ce..7c2d60f3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGrades.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiGrades.kt @@ -16,6 +16,8 @@ import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_YEAR_PROPO import pl.szczodrzynski.edziennik.data.db.entity.GradeCategory import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.models.Date @@ -96,7 +98,7 @@ class LibrusApiGrades(override val data: DataLibrus, data.metadataList.add( Metadata( profileId, - Metadata.TYPE_GRADE, + MetadataType.GRADE, id, profile.empty, profile.empty diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiHomework.kt index c1183c19..1c7741ce 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiHomework.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiHomework.kt @@ -12,6 +12,8 @@ import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.models.Date class LibrusApiHomework(override val data: DataLibrus, @@ -50,7 +52,7 @@ class LibrusApiHomework(override val data: DataLibrus, data.eventList.add(eventObject) data.metadataList.add(Metadata( profileId, - Metadata.TYPE_HOMEWORK, + MetadataType.HOMEWORK, id, profile?.empty ?: false, profile?.empty ?: false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiLessons.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiLessons.kt index e32790c9..72605991 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiLessons.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiLessons.kt @@ -4,11 +4,11 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api -import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_LESSONS import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.LibrusLesson +import pl.szczodrzynski.edziennik.ext.* class LibrusApiLessons(override val data: DataLibrus, override val lastSync: Long?, @@ -39,7 +39,7 @@ class LibrusApiLessons(override val data: DataLibrus, data.librusLessons.put(id, librusLesson) } - data.setSyncNext(ENDPOINT_LIBRUS_API_LESSONS, 4*DAY) + data.setSyncNext(ENDPOINT_LIBRUS_API_LESSONS, 4* DAY) onSuccess(ENDPOINT_LIBRUS_API_LESSONS) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiLuckyNumber.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiLuckyNumber.kt index 80e6e299..f93a06a3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiLuckyNumber.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiLuckyNumber.kt @@ -4,12 +4,13 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api -import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_LUCKY_NUMBER import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.LuckyNumber import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -22,7 +23,7 @@ class LibrusApiLuckyNumber(override val data: DataLibrus, } init { - var nextSync = System.currentTimeMillis() + 2*DAY*1000 + var nextSync = System.currentTimeMillis() + 2* DAY *1000 apiGet(TAG, "LuckyNumbers") { json -> if (json.isJsonNull) { @@ -41,13 +42,13 @@ class LibrusApiLuckyNumber(override val data: DataLibrus, if (luckyNumberDate >= Date.getToday()) nextSync = luckyNumberDate.combineWith(Time(15, 0, 0)) else - nextSync = System.currentTimeMillis() + 6*HOUR*1000 + nextSync = System.currentTimeMillis() + 6* HOUR *1000 data.luckyNumberList.add(luckyNumberObject) data.metadataList.add( Metadata( profileId, - Metadata.TYPE_LUCKY_NUMBER, + MetadataType.LUCKY_NUMBER, luckyNumberObject.date.value.toLong(), true, profile?.empty ?: false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiMe.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiMe.kt index 9d5ce1d4..ea46971f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiMe.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiMe.kt @@ -4,10 +4,10 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api -import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_ME import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.ext.* class LibrusApiMe(override val data: DataLibrus, override val lastSync: Long?, @@ -34,7 +34,7 @@ class LibrusApiMe(override val data: DataLibrus, data.profile?.studentNameLong = buildFullName(user?.getString("FirstName"), user?.getString("LastName")) - data.setSyncNext(ENDPOINT_LIBRUS_API_ME, 2*DAY) + data.setSyncNext(ENDPOINT_LIBRUS_API_ME, 2* DAY) onSuccess(ENDPOINT_LIBRUS_API_ME) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiNoticeTypes.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiNoticeTypes.kt index 55e753ee..7e90fbb2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiNoticeTypes.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiNoticeTypes.kt @@ -9,6 +9,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_NOTICE_TYPES import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.NoticeType +import pl.szczodrzynski.edziennik.ext.* class LibrusApiNoticeTypes(override val data: DataLibrus, override val lastSync: Long?, @@ -29,7 +30,7 @@ class LibrusApiNoticeTypes(override val data: DataLibrus, data.noticeTypes.put(id, NoticeType(profileId, id, name)) } - data.setSyncNext(ENDPOINT_LIBRUS_API_NOTICE_TYPES, 4*DAY) + data.setSyncNext(ENDPOINT_LIBRUS_API_NOTICE_TYPES, 4* DAY) onSuccess(ENDPOINT_LIBRUS_API_NOTICE_TYPES) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiNotices.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiNotices.kt index ba531afe..83536977 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiNotices.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiNotices.kt @@ -12,6 +12,8 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Notice import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.models.Date class LibrusApiNotices(override val data: DataLibrus, @@ -34,7 +36,7 @@ class LibrusApiNotices(override val data: DataLibrus, val id = note.getLong("Id") ?: return@forEach val text = note.getString("Text") ?: "" val categoryId = note.getJsonObject("Category")?.getLong("Id") ?: -1 - val teacherId = note.getJsonObject("AddedBy")?.getLong("Id") ?: -1 + val teacherId = note.getJsonObject("Teacher")?.getLong("Id") ?: -1 val addedDate = note.getString("Date")?.let { Date.fromY_m_d(it) } ?: return@forEach val type = when (note.getInt("Positive")) { @@ -61,7 +63,7 @@ class LibrusApiNotices(override val data: DataLibrus, data.metadataList.add( Metadata( profileId, - Metadata.TYPE_NOTICE, + MetadataType.NOTICE, id, profile?.empty ?: false, profile?.empty ?: false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPointGradeCategories.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPointGradeCategories.kt index bba6ead9..775f0897 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPointGradeCategories.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPointGradeCategories.kt @@ -10,6 +10,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_POINT_GRADE_CATEGORIES import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.GradeCategory +import pl.szczodrzynski.edziennik.ext.* class LibrusApiPointGradeCategories(override val data: DataLibrus, override val lastSync: Long?, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPointGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPointGrades.kt index 9da2c435..ee9fdfdf 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPointGrades.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPointGrades.kt @@ -14,6 +14,8 @@ import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_POINT_AVG import pl.szczodrzynski.edziennik.data.db.entity.GradeCategory import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.models.Date class LibrusApiPointGrades(override val data: DataLibrus, @@ -65,7 +67,7 @@ class LibrusApiPointGrades(override val data: DataLibrus, data.gradeList.add(gradeObject) data.metadataList.add(Metadata( profileId, - Metadata.TYPE_GRADE, + MetadataType.GRADE, id, profile.empty, profile.empty diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPtMeetings.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPtMeetings.kt index 6c0a9dd7..35818ac0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPtMeetings.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPtMeetings.kt @@ -11,6 +11,8 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -55,7 +57,7 @@ class LibrusApiPtMeetings(override val data: DataLibrus, data.metadataList.add( Metadata( profileId, - Metadata.TYPE_EVENT, + MetadataType.EVENT, id, profile?.empty ?: false, profile?.empty ?: false @@ -64,7 +66,7 @@ class LibrusApiPtMeetings(override val data: DataLibrus, data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_PT_MEETING)) - data.setSyncNext(ENDPOINT_LIBRUS_API_PT_MEETINGS, 12*HOUR) + data.setSyncNext(ENDPOINT_LIBRUS_API_PT_MEETINGS, 12* HOUR) onSuccess(ENDPOINT_LIBRUS_API_PT_MEETINGS) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPushConfig.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPushConfig.kt index 9073d03f..5dac1841 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPushConfig.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiPushConfig.kt @@ -4,13 +4,13 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api -import pl.szczodrzynski.edziennik.JsonObject import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_PUSH_CONFIG import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS -import pl.szczodrzynski.edziennik.getInt -import pl.szczodrzynski.edziennik.getJsonObject +import pl.szczodrzynski.edziennik.ext.JsonObject +import pl.szczodrzynski.edziennik.ext.getInt +import pl.szczodrzynski.edziennik.ext.getJsonObject class LibrusApiPushConfig(override val data: DataLibrus, override val lastSync: Long?, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiSchools.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiSchools.kt index d405f9bf..c5581e4d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiSchools.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiSchools.kt @@ -4,13 +4,12 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api -import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_SCHOOLS import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.LessonRange +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.models.Time -import java.util.* class LibrusApiSchools(override val data: DataLibrus, override val lastSync: Long?, @@ -29,7 +28,7 @@ class LibrusApiSchools(override val data: DataLibrus, // create the school's short name using first letters of each long name's word // append the town name and save to student data val schoolNameShort = schoolNameLong?.firstLettersName - val schoolTown = school?.getString("Town")?.toLowerCase(Locale.getDefault()) + val schoolTown = school?.getString("Town")?.lowercase() data.schoolName = schoolId.toString() + schoolNameShort + "_" + schoolTown school?.getJsonArray("LessonsRange")?.let { ranges -> diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiSubjects.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiSubjects.kt index cfd9bd31..b615d379 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiSubjects.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiSubjects.kt @@ -4,11 +4,11 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api -import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_SUBJECTS import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.Subject +import pl.szczodrzynski.edziennik.ext.* class LibrusApiSubjects(override val data: DataLibrus, override val lastSync: Long?, @@ -32,7 +32,7 @@ class LibrusApiSubjects(override val data: DataLibrus, data.subjectList.put(1, Subject(profileId, 1, "Zachowanie", "zach")) - data.setSyncNext(ENDPOINT_LIBRUS_API_SUBJECTS, 4*DAY) + data.setSyncNext(ENDPOINT_LIBRUS_API_SUBJECTS, 4* DAY) onSuccess(ENDPOINT_LIBRUS_API_SUBJECTS) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTeacherFreeDayTypes.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTeacherFreeDayTypes.kt deleted file mode 100644 index e09504bf..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTeacherFreeDayTypes.kt +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-10-19 - */ - -package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api - -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus -import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_TEACHER_FREE_DAY_TYPES -import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi -import pl.szczodrzynski.edziennik.data.db.entity.TeacherAbsenceType - -class LibrusApiTeacherFreeDayTypes(override val data: DataLibrus, - override val lastSync: Long?, - val onSuccess: (endpointId: Int) -> Unit -) : LibrusApi(data, lastSync) { - companion object { - const val TAG = "LibrusApiTeacherFreeDayTypes" - } - - init { - apiGet(TAG, "TeacherFreeDays/Types") { json -> - val teacherAbsenceTypes = json.getJsonArray("Types")?.asJsonObjectList() - - teacherAbsenceTypes?.forEach { teacherAbsenceType -> - val id = teacherAbsenceType.getLong("Id") ?: return@forEach - val name = teacherAbsenceType.getString("Name") ?: return@forEach - - val teacherAbsenceTypeObject = TeacherAbsenceType( - profileId, - id, - name - ) - - data.teacherAbsenceTypes.put(id, teacherAbsenceTypeObject) - } - - data.setSyncNext(ENDPOINT_LIBRUS_API_TEACHER_FREE_DAY_TYPES, 7 * DAY) - onSuccess(ENDPOINT_LIBRUS_API_TEACHER_FREE_DAY_TYPES) - } - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTeacherFreeDays.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTeacherFreeDays.kt index 973ada46..68053aeb 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTeacherFreeDays.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTeacherFreeDays.kt @@ -5,13 +5,14 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.api import androidx.core.util.isEmpty -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_AGENDA import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_TEACHER_FREE_DAYS import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.TeacherAbsence +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -35,8 +36,6 @@ class LibrusApiTeacherFreeDays(override val data: DataLibrus, val id = teacherAbsence.getLong("Id") ?: return@forEach val teacherId = teacherAbsence.getJsonObject("Teacher")?.getLong("Id") ?: return@forEach - val type = teacherAbsence.getJsonObject("Type").getLong("Id") ?: return@forEach - val name = data.teacherAbsenceTypes.singleOrNull { it.id == type }?.name val dateFrom = Date.fromY_m_d(teacherAbsence.getString("DateFrom")) val dateTo = Date.fromY_m_d(teacherAbsence.getString("DateTo")) val timeFrom = teacherAbsence.getString("TimeFrom")?.let { Time.fromH_m_s(it) } @@ -45,8 +44,8 @@ class LibrusApiTeacherFreeDays(override val data: DataLibrus, val teacherAbsenceObject = TeacherAbsence( profileId = profileId, id = id, - type = type, - name = name, + type = -1L, + name = null, dateFrom = dateFrom, dateTo = dateTo, timeFrom = timeFrom, @@ -57,14 +56,14 @@ class LibrusApiTeacherFreeDays(override val data: DataLibrus, data.teacherAbsenceList.add(teacherAbsenceObject) data.metadataList.add(Metadata( profileId, - Metadata.TYPE_TEACHER_ABSENCE, + MetadataType.TEACHER_ABSENCE, id, true, profile?.empty ?: false )) } - data.setSyncNext(ENDPOINT_LIBRUS_API_TEACHER_FREE_DAYS, 6*HOUR, DRAWER_ITEM_AGENDA) + data.setSyncNext(ENDPOINT_LIBRUS_API_TEACHER_FREE_DAYS, 6* HOUR, FeatureType.AGENDA) onSuccess(ENDPOINT_LIBRUS_API_TEACHER_FREE_DAYS) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTextGradeCategories.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTextGradeCategories.kt index 45b6b069..441e3b60 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTextGradeCategories.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTextGradeCategories.kt @@ -10,6 +10,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_TEXT_GRADE_CATEGORIES import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.GradeCategory +import pl.szczodrzynski.edziennik.ext.* class LibrusApiTextGradeCategories(override val data: DataLibrus, override val lastSync: Long?, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTextGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTextGrades.kt index 7b7c8c6f..ce4cbcc2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTextGrades.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTextGrades.kt @@ -14,6 +14,8 @@ import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_DESCRIPTIV import pl.szczodrzynski.edziennik.data.db.entity.GradeCategory import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.models.Date class LibrusApiTextGrades(override val data: DataLibrus, @@ -67,7 +69,7 @@ class LibrusApiTextGrades(override val data: DataLibrus, data.gradeList.add(gradeObject) data.metadataList.add(Metadata( profileId, - Metadata.TYPE_GRADE, + MetadataType.GRADE, id, profile.empty, profile.empty diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTimetables.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTimetables.kt index 9a463a6b..abd62272 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTimetables.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiTimetables.kt @@ -14,6 +14,8 @@ import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel import pl.szczodrzynski.edziennik.data.db.entity.Lesson import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -188,6 +190,7 @@ class LibrusApiTimetables(override val data: DataLibrus, } lessonObject.id = lessonObject.buildId() + lessonObject.ownerId = lessonObject.buildOwnerId() val seen = profile.empty || lessonDate < Date.getToday() @@ -195,7 +198,7 @@ class LibrusApiTimetables(override val data: DataLibrus, data.metadataList.add( Metadata( profileId, - Metadata.TYPE_LESSON_CHANGE, + MetadataType.LESSON_CHANGE, lessonObject.id, seen, seen diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiUnits.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiUnits.kt index 791283ed..03fcf4a5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiUnits.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiUnits.kt @@ -8,6 +8,7 @@ import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_UNITS import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi +import pl.szczodrzynski.edziennik.ext.* class LibrusApiUnits(override val data: DataLibrus, override val lastSync: Long?, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiUsers.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiUsers.kt index 0ac9a5ea..fb82849f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiUsers.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiUsers.kt @@ -9,6 +9,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_USERS import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.ext.* class LibrusApiUsers(override val data: DataLibrus, override val lastSync: Long?, @@ -37,7 +38,7 @@ class LibrusApiUsers(override val data: DataLibrus, data.teacherList.put(id, teacher) } - data.setSyncNext(ENDPOINT_LIBRUS_API_USERS, 4*DAY) + data.setSyncNext(ENDPOINT_LIBRUS_API_USERS, 4* DAY) onSuccess(ENDPOINT_LIBRUS_API_USERS) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiVirtualClasses.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiVirtualClasses.kt index 9937f808..2d6a7958 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiVirtualClasses.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/api/LibrusApiVirtualClasses.kt @@ -9,6 +9,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_VIRTUAL_CLASSES import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.db.entity.Team +import pl.szczodrzynski.edziennik.ext.* class LibrusApiVirtualClasses(override val data: DataLibrus, override val lastSync: Long?, @@ -31,7 +32,7 @@ class LibrusApiVirtualClasses(override val data: DataLibrus, data.teamList.put(id, Team(profileId, id, name, 2, code, teacherId)) } - data.setSyncNext(ENDPOINT_LIBRUS_API_VIRTUAL_CLASSES, 4*DAY) + data.setSyncNext(ENDPOINT_LIBRUS_API_VIRTUAL_CLASSES, 4* DAY) onSuccess(ENDPOINT_LIBRUS_API_VIRTUAL_CLASSES) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetList.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetList.kt index 9f962bf9..1535e759 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetList.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetList.kt @@ -4,8 +4,6 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages -import pl.szczodrzynski.edziennik.DAY -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES import pl.szczodrzynski.edziennik.data.api.ERROR_NOT_IMPLEMENTED import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_MESSAGES_RECEIVED @@ -13,8 +11,11 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_MESS import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusMessages import pl.szczodrzynski.edziennik.data.db.entity.* import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED -import pl.szczodrzynski.edziennik.fixName -import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.DAY +import pl.szczodrzynski.edziennik.ext.fixName +import pl.szczodrzynski.edziennik.ext.singleOrNull import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.models.Date @@ -65,16 +66,17 @@ class LibrusMessagesGetList(override val data: DataLibrus, val recipientId = data.teacherList.singleOrNull { it.name == recipientFirstName && it.surname == recipientLastName - }?.id ?: { + }?.id ?: run { val teacherObject = Teacher( - profileId, - -1 * Utils.crc16("$recipientFirstName $recipientLastName".toByteArray()).toLong(), - recipientFirstName, - recipientLastName + profileId, + -1 * Utils.crc16("$recipientFirstName $recipientLastName".toByteArray()) + .toLong(), + recipientFirstName, + recipientLastName ) data.teacherList.put(teacherObject.id, teacherObject) teacherObject.id - }.invoke() + } val senderId = when (type) { TYPE_RECEIVED -> recipientId @@ -109,7 +111,7 @@ class LibrusMessagesGetList(override val data: DataLibrus, id ) - element.select("isAnyFileAttached")?.text()?.let { + element.select("isAnyFileAttached").text()?.let { if (it == "1") messageObject.hasAttachments = true } @@ -118,7 +120,7 @@ class LibrusMessagesGetList(override val data: DataLibrus, data.messageRecipientList.add(messageRecipientObject) data.setSeenMetadataList.add(Metadata( profileId, - Metadata.TYPE_MESSAGE, + MetadataType.MESSAGE, id, notified, notified @@ -127,7 +129,7 @@ class LibrusMessagesGetList(override val data: DataLibrus, when (type) { TYPE_RECEIVED -> data.setSyncNext(ENDPOINT_LIBRUS_MESSAGES_RECEIVED, SYNC_ALWAYS) - Message.TYPE_SENT -> data.setSyncNext(ENDPOINT_LIBRUS_MESSAGES_SENT, DAY, DRAWER_ITEM_MESSAGES) + Message.TYPE_SENT -> data.setSyncNext(ENDPOINT_LIBRUS_MESSAGES_SENT, DAY, FeatureType.MESSAGES_SENT) } onSuccess(endpointId) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetMessage.kt index 49ee0d52..e8a1e1a6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetMessage.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetMessage.kt @@ -13,12 +13,13 @@ import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.data.db.full.MessageRecipientFull -import pl.szczodrzynski.edziennik.fixName -import pl.szczodrzynski.edziennik.isNotNullNorEmpty -import pl.szczodrzynski.edziennik.notEmptyOrNull -import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.ext.fixName +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.ext.notEmptyOrNull +import pl.szczodrzynski.edziennik.ext.singleOrNull import pl.szczodrzynski.edziennik.utils.models.Date import java.nio.charset.Charset @@ -35,7 +36,7 @@ class LibrusMessagesGetMessage(override val data: DataLibrus, "messageId" to messageObject.id, "archive" to 0 )) { doc -> - val message = doc.select("response GetMessage data").first() + val message = doc.select("response GetMessage data").first() ?: return@messagesGet val body = Base64.decode(message.select("Message").text(), Base64.DEFAULT) .toString(Charset.defaultCharset()) @@ -108,7 +109,7 @@ class LibrusMessagesGetMessage(override val data: DataLibrus, readDate = readDate ) - messageRecipientObject.fullName = profile.accountName ?: profile.studentNameLong ?: "" + messageRecipientObject.fullName = profile.accountName ?: profile.studentNameLong messageRecipientList.add(messageRecipientObject) } @@ -124,7 +125,7 @@ class LibrusMessagesGetMessage(override val data: DataLibrus, val receiverId = teacher?.id ?: -1 teacher?.loginId = receiverLoginId - val readDateText = message.select("readed").text() + val readDateText = receiver.select("readed").text() val readDate = when (readDateText.isNotNullNorEmpty()) { true -> Date.fromIso(readDateText) else -> 0 @@ -147,7 +148,7 @@ class LibrusMessagesGetMessage(override val data: DataLibrus, if (!messageObject.seen) { data.setSeenMetadataList.add(Metadata( messageObject.profileId, - Metadata.TYPE_MESSAGE, + MetadataType.MESSAGE, messageObject.id, true, true diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetRecipientList.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetRecipientList.kt index 053258d8..b6881f17 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetRecipientList.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesGetRecipientList.kt @@ -14,6 +14,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusMessages import pl.szczodrzynski.edziennik.data.api.events.RecipientListGetEvent import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.ext.* class LibrusMessagesGetRecipientList(override val data: DataLibrus, val onSuccess: () -> Unit @@ -28,9 +29,9 @@ class LibrusMessagesGetRecipientList(override val data: DataLibrus, messagesGet(TAG, "Receivers/action/GetTypes", parameters = mapOf( "includeClass" to 1 )) { doc -> - doc.select("response GetTypes data list ArrayItem")?.forEach { - val id = it.getElementsByTag("id")?.firstOrNull()?.ownText() ?: return@forEach - val name = it.getElementsByTag("name")?.firstOrNull()?.ownText() ?: return@forEach + doc.select("response GetTypes data list ArrayItem").forEach { + val id = it.getElementsByTag("id").firstOrNull()?.ownText() ?: return@forEach + val name = it.getElementsByTag("name").firstOrNull()?.ownText() ?: return@forEach listTypes += id to name } @@ -55,7 +56,7 @@ class LibrusMessagesGetRecipientList(override val data: DataLibrus, if (dataEl is JsonObject) { val listEl = dataEl.get("ArrayItem") if (listEl is JsonArray) { - listEl.asJsonObjectList()?.forEach { item -> + listEl.asJsonObjectList().forEach { item -> processElement(item, type.first, type.second) } } @@ -71,7 +72,7 @@ class LibrusMessagesGetRecipientList(override val data: DataLibrus, private fun processElement(element: JsonObject, typeId: String, typeName: String, listName: String? = null) { val listEl = element.getJsonObject("list")?.get("ArrayItem") if (listEl is JsonArray) { - listEl.asJsonObjectList()?.let { list -> + listEl.asJsonObjectList().let { list -> val label = element.getString("label") ?: "" list.forEach { item -> processElement(item, typeId, typeName, label) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesSendMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesSendMessage.kt index d568135e..bcd01311 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesSendMessage.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusMessagesSendMessage.kt @@ -5,19 +5,18 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.messages import org.greenrobot.eventbus.EventBus -import pl.szczodrzynski.edziennik.base64Encode import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusMessages import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent import pl.szczodrzynski.edziennik.data.db.entity.Message -import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Teacher -import pl.szczodrzynski.edziennik.getJsonObject -import pl.szczodrzynski.edziennik.getLong -import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.ext.base64Encode +import pl.szczodrzynski.edziennik.ext.getJsonObject +import pl.szczodrzynski.edziennik.ext.getLong +import pl.szczodrzynski.edziennik.ext.getString class LibrusMessagesSendMessage(override val data: DataLibrus, - val recipients: List, + val recipients: Set, val subject: String, val text: String, val onSuccess: () -> Unit @@ -42,14 +41,14 @@ class LibrusMessagesSendMessage(override val data: DataLibrus, val id = response.getLong("data") if (response.getString("status") != "ok" || id == null) { - val message = response.getString("message") + // val message = response.getString("message") // TODO error return@messagesGetJson } LibrusMessagesGetList(data, type = Message.TYPE_SENT, lastSync = null) { - val message = data.messageList.firstOrNull { it.type == Message.TYPE_SENT && it.id == id } - val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == message?.id } + val message = data.messageList.firstOrNull { it.isSent && it.id == id } + // val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == message?.id } val event = MessageSentEvent(data.profileId, message, message?.addedDate) EventBus.getDefault().postSticky(event) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusSandboxDownloadAttachment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusSandboxDownloadAttachment.kt index 567071aa..c58c185f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusSandboxDownloadAttachment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/messages/LibrusSandboxDownloadAttachment.kt @@ -7,8 +7,8 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusMessages import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.ext.get +import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.utils.Utils import java.io.File import kotlin.coroutines.CoroutineContext diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaGetHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaGetHomework.kt index 379bd42d..4ba502f0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaGetHomework.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaGetHomework.kt @@ -1,12 +1,12 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia -import android.text.Html import org.greenrobot.eventbus.EventBus import org.jsoup.Jsoup import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusSynergia import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.utils.html.BetterHtml class LibrusSynergiaGetHomework(override val data: DataLibrus, val event: EventFull, @@ -23,7 +23,11 @@ class LibrusSynergiaGetHomework(override val data: DataLibrus, val table = doc.select("table.decorated tbody > tr") event.topic = table[1].select("td")[1].text() - event.homeworkBody = Html.fromHtml(table[5].select("td")[1].html()).toString() + event.homeworkBody = BetterHtml.fromHtml( + context = null, + html = table[5].select("td")[1].html(), + ).toString() + event.isDownloaded = true event.attachmentIds = mutableListOf() event.attachmentNames = mutableListOf() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaGetMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaGetMessage.kt index 4cb1e246..36cadfaa 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaGetMessage.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaGetMessage.kt @@ -8,12 +8,13 @@ import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.data.db.full.MessageRecipientFull -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.isNotNullNorEmpty -import pl.szczodrzynski.edziennik.singleOrNull -import pl.szczodrzynski.edziennik.swapFirstLastName +import pl.szczodrzynski.edziennik.ext.get +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.ext.singleOrNull +import pl.szczodrzynski.edziennik.ext.swapFirstLastName import pl.szczodrzynski.edziennik.utils.models.Date class LibrusSynergiaGetMessage(override val data: DataLibrus, @@ -87,7 +88,7 @@ class LibrusSynergiaGetMessage(override val data: DataLibrus, }) } - val readDateText = readElement.select(".left").text() + val readDateText = readElement?.select(".left")?.text() val readDate = when (readDateText.isNotNullNorEmpty()) { true -> Date.fromIso(readDateText) else -> 0 @@ -108,7 +109,7 @@ class LibrusSynergiaGetMessage(override val data: DataLibrus, Message.TYPE_SENT -> { - readElement.select("tr").forEachIndexed { i, receiver -> + readElement?.select("tr")?.forEachIndexed { i, receiver -> if (i == 0) return@forEachIndexed // Skip the header val receiverFullName = receiver.child(0).text() @@ -139,7 +140,7 @@ class LibrusSynergiaGetMessage(override val data: DataLibrus, if (!messageObject.seen) { data.setSeenMetadataList.add(Metadata( messageObject.profileId, - Metadata.TYPE_MESSAGE, + MetadataType.MESSAGE, messageObject.id, true, true diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaGetMessages.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaGetMessages.kt index ffc8133a..626aec61 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaGetMessages.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaGetMessages.kt @@ -1,12 +1,14 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia import org.jsoup.Jsoup -import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.ERROR_NOT_IMPLEMENTED import pl.szczodrzynski.edziennik.data.api.Regexes import pl.szczodrzynski.edziennik.data.api.edziennik.librus.* import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusSynergia import pl.szczodrzynski.edziennik.data.db.entity.* +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.models.Date @@ -36,19 +38,19 @@ class LibrusSynergiaGetMessages(override val data: DataLibrus, fun getRecipientId(name: String): Long = data.teacherList.singleOrNull { it.fullNameLastFirst == name - }?.id ?: { + }?.id ?: run { val teacherObject = Teacher( - profileId, - -1 * Utils.crc16(name.swapFirstLastName().toByteArray()).toLong(), - name.splitName()?.second!!, - name.splitName()?.first!! + profileId, + -1 * Utils.crc16(name.swapFirstLastName().toByteArray()).toLong(), + name.splitName()?.second!!, + name.splitName()?.first!! ) data.teacherList.put(teacherObject.id, teacherObject) teacherObject.id - }.invoke() + } doc.select(".decorated.stretch tbody > tr").forEach { messageElement -> - val url = messageElement.select("a").first().attr("href") + val url = messageElement.select("a").first()?.attr("href") ?: return@forEach val id = Regexes.LIBRUS_MESSAGE_ID.find(url)?.get(1)?.toLong() ?: return@forEach val subject = messageElement.child(3).text() val sentDate = Date.fromIso(messageElement.child(4).text()) @@ -95,7 +97,7 @@ class LibrusSynergiaGetMessages(override val data: DataLibrus, data.messageRecipientList.add(messageRecipientObject) data.setSeenMetadataList.add(Metadata( profileId, - Metadata.TYPE_MESSAGE, + MetadataType.MESSAGE, id, notified, notified @@ -104,7 +106,7 @@ class LibrusSynergiaGetMessages(override val data: DataLibrus, when (type) { Message.TYPE_RECEIVED -> data.setSyncNext(ENDPOINT_LIBRUS_MESSAGES_RECEIVED, SYNC_ALWAYS) - Message.TYPE_SENT -> data.setSyncNext(ENDPOINT_LIBRUS_MESSAGES_SENT, DAY, MainActivity.DRAWER_ITEM_MESSAGES) + Message.TYPE_SENT -> data.setSyncNext(ENDPOINT_LIBRUS_MESSAGES_SENT, DAY, FeatureType.MESSAGES_SENT) } onSuccess(endpointId) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaHomework.kt index d33f6911..be9b4efc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaHomework.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaHomework.kt @@ -5,8 +5,6 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia import org.jsoup.Jsoup -import pl.szczodrzynski.edziennik.HOUR -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_HOMEWORK import pl.szczodrzynski.edziennik.data.api.POST import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_SYNERGIA_HOMEWORK @@ -14,8 +12,12 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusSynergia import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Metadata -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.HOUR +import pl.szczodrzynski.edziennik.ext.get +import pl.szczodrzynski.edziennik.ext.getSemesterStart +import pl.szczodrzynski.edziennik.ext.singleOrNull import pl.szczodrzynski.edziennik.utils.models.Date class LibrusSynergiaHomework(override val data: DataLibrus, @@ -42,7 +44,7 @@ class LibrusSynergiaHomework(override val data: DataLibrus, doc.select("table.myHomeworkTable > tbody").firstOrNull()?.also { homeworkTable -> val homeworkElements = homeworkTable.children() - homeworkElements.forEachIndexed { i, el -> + homeworkElements.forEach { el -> val elements = el.children() val subjectName = elements[0].text().trim() @@ -56,7 +58,7 @@ class LibrusSynergiaHomework(override val data: DataLibrus, val eventDate = Date.fromY_m_d(elements[6].text().trim()) val id = "/podglad/([0-9]+)'".toRegex().find( elements[9].select("input").attr("onclick") - )?.get(1)?.toLong() ?: return@forEachIndexed + )?.get(1)?.toLong() ?: return@forEach val lessons = data.db.timetableDao().getAllForDateNow(profileId, eventDate) val startTime = lessons.firstOrNull { it.subjectId == subjectId }?.startTime @@ -79,11 +81,12 @@ class LibrusSynergiaHomework(override val data: DataLibrus, teamId = data.teamClass?.id ?: -1, addedDate = addedDate.inMillis ) + eventObject.isDownloaded = false data.eventList.add(eventObject) data.metadataList.add(Metadata( profileId, - Metadata.TYPE_HOMEWORK, + MetadataType.HOMEWORK, id, seen, seen @@ -94,7 +97,7 @@ class LibrusSynergiaHomework(override val data: DataLibrus, data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_HOMEWORK)) // because this requires a synergia login (2 more requests!!!) sync this every few hours or if explicit :D - data.setSyncNext(ENDPOINT_LIBRUS_SYNERGIA_HOMEWORK, 5 * HOUR, DRAWER_ITEM_HOMEWORK) + data.setSyncNext(ENDPOINT_LIBRUS_SYNERGIA_HOMEWORK, 5 * HOUR, FeatureType.HOMEWORK) onSuccess(ENDPOINT_LIBRUS_SYNERGIA_HOMEWORK) } } ?: onSuccess(ENDPOINT_LIBRUS_SYNERGIA_HOMEWORK) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaInfo.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaInfo.kt index 66e11785..74458fb5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaInfo.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaInfo.kt @@ -5,10 +5,10 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia import org.jsoup.Jsoup -import pl.szczodrzynski.edziennik.MONTH import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_SYNERGIA_INFO import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusSynergia +import pl.szczodrzynski.edziennik.ext.MONTH class LibrusSynergiaInfo(override val data: DataLibrus, override val lastSync: Long?, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaMarkAllAnnouncementsAsRead.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaMarkAllAnnouncementsAsRead.kt index 04a7f5b6..2cb06942 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaMarkAllAnnouncementsAsRead.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/data/synergia/LibrusSynergiaMarkAllAnnouncementsAsRead.kt @@ -7,6 +7,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.synergia import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusSynergia import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType class LibrusSynergiaMarkAllAnnouncementsAsRead(override val data: DataLibrus, val onSuccess: () -> Unit @@ -17,7 +18,7 @@ class LibrusSynergiaMarkAllAnnouncementsAsRead(override val data: DataLibrus, init { synergiaGet(TAG, "ogloszenia") { - data.app.db.metadataDao().setAllSeen(profileId, Metadata.TYPE_ANNOUNCEMENT, true) + data.app.db.metadataDao().setAllSeen(profileId, MetadataType.ANNOUNCEMENT, true) onSuccess() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/firstlogin/LibrusFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/firstlogin/LibrusFirstLogin.kt index 68f44269..c29d3a85 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/firstlogin/LibrusFirstLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/firstlogin/LibrusFirstLogin.kt @@ -11,6 +11,9 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.librus.login.LibrusLoginPor import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.enums.LoginMode +import pl.szczodrzynski.edziennik.data.db.enums.LoginType +import pl.szczodrzynski.edziennik.ext.* class LibrusFirstLogin(val data: DataLibrus, val onSuccess: () -> Unit) { companion object { @@ -22,11 +25,9 @@ class LibrusFirstLogin(val data: DataLibrus, val onSuccess: () -> Unit) { private val profileList = mutableListOf() init { - val loginStoreId = data.loginStore.id - val loginStoreType = LOGIN_TYPE_LIBRUS - var firstProfileId = loginStoreId + var firstProfileId = data.loginStore.id - if (data.loginStore.mode == LOGIN_MODE_LIBRUS_EMAIL) { + if (data.loginStore.mode == LoginMode.LIBRUS_EMAIL) { // email login: use Portal for account list LibrusLoginPortal(data) { portal.portalGet(TAG, if (data.fakeLogin) FAKE_LIBRUS_ACCOUNTS else LIBRUS_ACCOUNTS_URL) { json, response -> @@ -65,8 +66,8 @@ class LibrusFirstLogin(val data: DataLibrus, val onSuccess: () -> Unit) { val profile = Profile( firstProfileId++, - loginStoreId, - loginStoreType, + data.loginStore.id, + LoginType.LIBRUS, studentNameLong, data.portalEmail, studentNameLong, @@ -106,8 +107,8 @@ class LibrusFirstLogin(val data: DataLibrus, val onSuccess: () -> Unit) { val profile = Profile( firstProfileId++, - loginStoreId, - loginStoreType, + data.loginStore.id, + LoginType.LIBRUS, studentNameLong, login, studentNameLong, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLogin.kt index c79acd2e..6af1398b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLogin.kt @@ -5,11 +5,8 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.librus.login import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_LIBRUS_API -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_LIBRUS_MESSAGES -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_LIBRUS_PORTAL -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_LIBRUS_SYNERGIA import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod import pl.szczodrzynski.edziennik.utils.Utils class LibrusLogin(val data: DataLibrus, val onSuccess: () -> Unit) { @@ -24,7 +21,7 @@ class LibrusLogin(val data: DataLibrus, val onSuccess: () -> Unit) { } private fun nextLoginMethod(onSuccess: () -> Unit) { - if (data.targetLoginMethodIds.isEmpty()) { + if (data.targetLoginMethods.isEmpty()) { onSuccess() return } @@ -32,38 +29,39 @@ class LibrusLogin(val data: DataLibrus, val onSuccess: () -> Unit) { onSuccess() return } - useLoginMethod(data.targetLoginMethodIds.removeAt(0)) { usedMethodId -> + useLoginMethod(data.targetLoginMethods.removeAt(0)) { usedMethod -> data.progress(data.progressStep) - if (usedMethodId != -1) - data.loginMethods.add(usedMethodId) + if (usedMethod != null) + data.loginMethods.add(usedMethod) nextLoginMethod(onSuccess) } } - private fun useLoginMethod(loginMethodId: Int, onSuccess: (usedMethodId: Int) -> Unit) { + private fun useLoginMethod(loginMethod: LoginMethod, onSuccess: (usedMethod: LoginMethod?) -> Unit) { // this should never be true - if (data.loginMethods.contains(loginMethodId)) { - onSuccess(-1) + if (data.loginMethods.contains(loginMethod)) { + onSuccess(null) return } - Utils.d(TAG, "Using login method $loginMethodId") - when (loginMethodId) { - LOGIN_METHOD_LIBRUS_PORTAL -> { + Utils.d(TAG, "Using login method $loginMethod") + when (loginMethod) { + LoginMethod.LIBRUS_PORTAL -> { data.startProgress(R.string.edziennik_progress_login_librus_portal) - LibrusLoginPortal(data) { onSuccess(loginMethodId) } + LibrusLoginPortal(data) { onSuccess(loginMethod) } } - LOGIN_METHOD_LIBRUS_API -> { + LoginMethod.LIBRUS_API -> { data.startProgress(R.string.edziennik_progress_login_librus_api) - LibrusLoginApi(data) { onSuccess(loginMethodId) } + LibrusLoginApi(data) { onSuccess(loginMethod) } } - LOGIN_METHOD_LIBRUS_SYNERGIA -> { + LoginMethod.LIBRUS_SYNERGIA -> { data.startProgress(R.string.edziennik_progress_login_librus_synergia) - LibrusLoginSynergia(data) { onSuccess(loginMethodId) } + LibrusLoginSynergia(data) { onSuccess(loginMethod) } } - LOGIN_METHOD_LIBRUS_MESSAGES -> { + LoginMethod.LIBRUS_MESSAGES -> { data.startProgress(R.string.edziennik_progress_login_librus_messages) - LibrusLoginMessages(data) { onSuccess(loginMethodId) } + LibrusLoginMessages(data) { onSuccess(loginMethod) } } + else -> {} } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginApi.kt index 7151cf29..d2ef2336 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginApi.kt @@ -12,9 +12,11 @@ import im.wangchao.mhttp.callback.JsonCallbackHandler import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.getInt -import pl.szczodrzynski.edziennik.getString -import pl.szczodrzynski.edziennik.getUnixDate +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.data.db.enums.LoginMode +import pl.szczodrzynski.edziennik.ext.getInt +import pl.szczodrzynski.edziennik.ext.getString +import pl.szczodrzynski.edziennik.ext.getUnixDate import pl.szczodrzynski.edziennik.utils.Utils.d import java.net.HttpURLConnection.* @@ -32,7 +34,7 @@ class LibrusLoginApi { this.data = data this.onSuccess = onSuccess - if (data.loginStore.mode == LOGIN_MODE_LIBRUS_EMAIL && data.profile == null) { + if (data.loginStore.mode == LoginMode.LIBRUS_EMAIL && data.profile == null) { data.error(ApiError(TAG, ERROR_PROFILE_MISSING)) return } @@ -42,9 +44,9 @@ class LibrusLoginApi { } else { when (data.loginStore.mode) { - LOGIN_MODE_LIBRUS_EMAIL -> loginWithPortal() - LOGIN_MODE_LIBRUS_SYNERGIA -> loginWithSynergia() - LOGIN_MODE_LIBRUS_JST -> loginWithJst() + LoginMode.LIBRUS_EMAIL -> loginWithPortal() + LoginMode.LIBRUS_SYNERGIA -> loginWithSynergia() + LoginMode.LIBRUS_JST -> loginWithJst() else -> { data.error(ApiError(TAG, ERROR_INVALID_LOGIN_MODE)) } @@ -53,7 +55,7 @@ class LibrusLoginApi { } private fun loginWithPortal() { - if (!data.loginMethods.contains(LOGIN_METHOD_LIBRUS_PORTAL)) { + if (!data.loginMethods.contains(LoginMethod.LIBRUS_PORTAL)) { data.error(ApiError(TAG, ERROR_LOGIN_METHOD_NOT_SATISFIED)) return } @@ -63,7 +65,7 @@ class LibrusLoginApi { } private fun copyFromLoginStore() { - data.loginStore.data?.apply { + data.loginStore.data.apply { if (has("accountLogin")) { data.apiLogin = getString("accountLogin") remove("accountLogin") diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginMessages.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginMessages.kt index 9bfad99f..eeefe0f8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginMessages.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginMessages.kt @@ -12,7 +12,8 @@ import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.LibrusRecaptchaHelper import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.getUnixDate +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.ext.getUnixDate import pl.szczodrzynski.edziennik.utils.Utils.d import java.io.StringWriter import javax.xml.parsers.DocumentBuilderFactory @@ -91,7 +92,7 @@ class LibrusLoginMessages(val data: DataLibrus, val onSuccess: () -> Unit) { } else { data.app.cookieJar.clear("wiadomosci.librus.pl") - if (data.loginMethods.contains(LOGIN_METHOD_LIBRUS_SYNERGIA)) { + if (data.loginMethods.contains(LoginMethod.LIBRUS_SYNERGIA)) { loginWithSynergia() } else if (data.apiLogin != null && data.apiPassword != null && false) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginPortal.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginPortal.kt index f164e3ed..92741bb0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginPortal.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginPortal.kt @@ -10,7 +10,10 @@ import im.wangchao.mhttp.callback.TextCallbackHandler import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus +import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.enums.LoginMode +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.Utils.d import java.net.HttpURLConnection.* import java.util.* @@ -21,8 +24,11 @@ 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 != LOGIN_MODE_LIBRUS_EMAIL) { + if (data.loginStore.mode != LoginMode.LIBRUS_EMAIL) { data.error(ApiError(TAG, ERROR_INVALID_LOGIN_MODE)) return@run } @@ -30,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()) { @@ -55,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) @@ -80,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() + 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) { @@ -102,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, + ) { + 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) @@ -113,62 +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 - 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 - // this doesn't work anyway: `errors` is an object with `g-recaptcha-response` set - code.contains("robotem") -> ERROR_CAPTCHA_LIBRUS_PORTAL - 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 - } - } - if (json.getBoolean("captchaRequired") == true) { - data.error(ApiError(TAG, ERROR_CAPTCHA_LIBRUS_PORTAL) - .withResponse(response) - .withApiResponse(json)) - 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) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginSynergia.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginSynergia.kt index 4a292cfe..5f53890c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginSynergia.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/LibrusLoginSynergia.kt @@ -12,8 +12,9 @@ import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusApi import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.getString -import pl.szczodrzynski.edziennik.getUnixDate +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.ext.getString +import pl.szczodrzynski.edziennik.ext.getUnixDate import pl.szczodrzynski.edziennik.utils.Utils.d import java.net.HttpURLConnection @@ -34,7 +35,7 @@ class LibrusLoginSynergia(override val data: DataLibrus, val onSuccess: () -> Un } else { data.app.cookieJar.clear("synergia.librus.pl") - if (data.loginMethods.contains(LOGIN_METHOD_LIBRUS_API)) { + if (data.loginMethods.contains(LoginMethod.LIBRUS_API)) { loginWithApi() } else if (data.apiLogin != null && data.apiPassword != null && false) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/SynergiaTokenExtractor.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/SynergiaTokenExtractor.kt index 5072e5a2..7e1dc012 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/SynergiaTokenExtractor.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/librus/login/SynergiaTokenExtractor.kt @@ -7,6 +7,8 @@ import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.librus.DataLibrus import pl.szczodrzynski.edziennik.data.api.edziennik.librus.data.LibrusPortal import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.enums.LoginMode +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.Utils.d class SynergiaTokenExtractor(override val data: DataLibrus, val onSuccess: () -> Unit) : LibrusPortal(data) { @@ -15,7 +17,7 @@ class SynergiaTokenExtractor(override val data: DataLibrus, val onSuccess: () -> } init { run { - if (data.loginStore.mode != LOGIN_MODE_LIBRUS_EMAIL) { + if (data.loginStore.mode != LoginMode.LIBRUS_EMAIL) { data.error(ApiError(TAG, ERROR_INVALID_LOGIN_MODE)) return@run } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/DataMobidziennik.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/DataMobidziennik.kt index ddfb0113..64507362 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/DataMobidziennik.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/DataMobidziennik.kt @@ -6,12 +6,14 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik import android.util.LongSparseArray import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.currentTimeUnix -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_MOBIDZIENNIK_WEB import pl.szczodrzynski.edziennik.data.api.models.Data import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.Profile -import pl.szczodrzynski.edziennik.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.ext.currentTimeUnix +import pl.szczodrzynski.edziennik.ext.getStudentData +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.ext.set import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -29,12 +31,37 @@ class DataMobidziennik(app: App, profile: Profile?, loginStore: LoginStore) : Da override fun satisfyLoginMethods() { loginMethods.clear() if (isWebLoginValid()) { - loginMethods += LOGIN_METHOD_MOBIDZIENNIK_WEB + loginMethods += LoginMethod.MOBIDZIENNIK_WEB } } override fun generateUserCode() = "$loginServerName:$loginUsername:$studentId" + fun parseDateTime(dateStr: String): Pair { + // pt, 4 lut, 09:11 + val dateParts = dateStr.split(',', ' ').filter { it.isNotEmpty() } + // [pt], [4], [lut], [09:11] + val date = Date.getToday() + date.day = dateParts[1].toIntOrNull() ?: 1 + date.month = when (dateParts[2]) { + "sty" -> 1 + "lut" -> 2 + "mar" -> 3 + "kwi" -> 4 + "maj" -> 5 + "cze" -> 6 + "lip" -> 7 + "sie" -> 8 + "wrz" -> 9 + "paź" -> 10 + "lis" -> 11 + "gru" -> 12 + else -> 1 + } + val time = Time.fromH_m(dateParts[3]) + return date to time + } + val teachersMap = LongSparseArray() val subjectsMap = LongSparseArray() @@ -60,7 +87,7 @@ class DataMobidziennik(app: App, profile: Profile?, loginStore: LoginStore) : Da private var mStudentId: Int? = null var studentId: Int get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 } - set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value } + set(value) { profile["studentId"] = value; mStudentId = value } /* __ __ _ \ \ / / | | @@ -100,7 +127,7 @@ class DataMobidziennik(app: App, profile: Profile?, loginStore: LoginStore) : Da */ var globalId: String? get() { mGlobalId = mGlobalId ?: profile?.getStudentData("globalId", null); return mGlobalId } - set(value) { profile?.putStudentData("globalId", value) ?: return; mGlobalId = value } + set(value) { profile["globalId"] = value; mGlobalId = value } private var mGlobalId: String? = null /** @@ -110,7 +137,7 @@ class DataMobidziennik(app: App, profile: Profile?, loginStore: LoginStore) : Da */ var loginEmail: String? get() { mLoginEmail = mLoginEmail ?: profile?.getStudentData("email", null); return mLoginEmail } - set(value) { profile?.putStudentData("email", value); mLoginEmail = value } + set(value) { profile["email"] = value; mLoginEmail = value } private var mLoginEmail: String? = null /** @@ -119,7 +146,7 @@ class DataMobidziennik(app: App, profile: Profile?, loginStore: LoginStore) : Da */ var loginId: String? get() { mLoginId = mLoginId ?: profile?.getStudentData("loginId", null); return mLoginId } - set(value) { profile?.putStudentData("loginId", value) ?: return; mLoginId = value } + set(value) { profile["loginId"] = value; mLoginId = value } private var mLoginId: String? = null /** @@ -127,7 +154,7 @@ class DataMobidziennik(app: App, profile: Profile?, loginStore: LoginStore) : Da */ var ciasteczkoAutoryzacji: String? get() { mCiasteczkoAutoryzacji = mCiasteczkoAutoryzacji ?: profile?.getStudentData("ciasteczkoAutoryzacji", null); return mCiasteczkoAutoryzacji } - set(value) { profile?.putStudentData("ciasteczkoAutoryzacji", value) ?: return; mCiasteczkoAutoryzacji = value } + set(value) { profile["ciasteczkoAutoryzacji"] = value; mCiasteczkoAutoryzacji = value } private var mCiasteczkoAutoryzacji: String? = null diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/Mobidziennik.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/Mobidziennik.kt index 0e002576..6d53a6c7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/Mobidziennik.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/Mobidziennik.kt @@ -11,12 +11,15 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.Mobidzien import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web.* import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.firstlogin.MobidziennikFirstLogin import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.MobidziennikLogin +import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.MessageFull @@ -53,19 +56,19 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto |_| |_| |_|\___| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_| __/ | |__*/ - override fun sync(featureIds: List, viewId: Int?, onlyEndpoints: List?, arguments: JsonObject?) { + override fun sync(featureTypes: Set?, onlyEndpoints: Set?, arguments: JsonObject?) { data.arguments = arguments - data.prepare(mobidziennikLoginMethods, MobidziennikFeatures, featureIds, viewId, onlyEndpoints) + data.prepare(MobidziennikFeatures, featureTypes, onlyEndpoints) login() } - private fun login(loginMethodId: Int? = null, afterLogin: (() -> Unit)? = null) { - d(TAG, "Trying to login with ${data.targetLoginMethodIds}") + private fun login(loginMethod: LoginMethod? = null, afterLogin: (() -> Unit)? = null) { + d(TAG, "Trying to login with ${data.targetLoginMethods}") if (internalErrorList.isNotEmpty()) { d(TAG, " - Internal errors:") internalErrorList.forEach { d(TAG, " - code $it") } } - loginMethodId?.let { data.prepareFor(mobidziennikLoginMethods, it) } + loginMethod?.let { data.prepareFor(it) } afterLogin?.let { this.afterLogin = it } MobidziennikLogin(data) { data() @@ -73,7 +76,7 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto } private fun data() { - d(TAG, "Endpoint IDs: ${data.targetEndpointIds}") + d(TAG, "Endpoint IDs: ${data.targetEndpoints}") if (internalErrorList.isNotEmpty()) { d(TAG, " - Internal errors:") internalErrorList.forEach { d(TAG, " - code $it") } @@ -84,15 +87,15 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto } override fun getMessage(message: MessageFull) { - login(LOGIN_METHOD_MOBIDZIENNIK_WEB) { + login(LoginMethod.MOBIDZIENNIK_WEB) { MobidziennikWebGetMessage(data, message) { completed() } } } - override fun sendMessage(recipients: List, subject: String, text: String) { - login(LOGIN_METHOD_MOBIDZIENNIK_WEB) { + override fun sendMessage(recipients: Set, subject: String, text: String) { + login(LoginMethod.MOBIDZIENNIK_WEB) { MobidziennikWebSendMessage(data, recipients, subject, text) { completed() } @@ -103,7 +106,7 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto override fun getAnnouncement(announcement: AnnouncementFull) {} override fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) { - login(LOGIN_METHOD_MOBIDZIENNIK_WEB) { + login(LoginMethod.MOBIDZIENNIK_WEB) { MobidziennikWebGetAttachment(data, owner, attachmentId, attachmentName) { completed() } @@ -111,7 +114,7 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto } override fun getRecipientList() { - login(LOGIN_METHOD_MOBIDZIENNIK_WEB) { + login(LoginMethod.MOBIDZIENNIK_WEB) { MobidziennikWebGetRecipientList(data) { completed() } @@ -119,9 +122,16 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto } override fun getEvent(eventFull: EventFull) { - login(LOGIN_METHOD_MOBIDZIENNIK_WEB) { - MobidziennikWebGetHomework(data, eventFull) { - completed() + login(LoginMethod.MOBIDZIENNIK_WEB) { + if (eventFull.isHomework) { + MobidziennikWebGetHomework(data, eventFull) { + completed() + } + } + else { + MobidziennikWebGetEvent(data, eventFull) { + completed() + } } } } @@ -135,6 +145,7 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { return object : EdziennikCallback { override fun onCompleted() { callback.onCompleted() } + override fun onRequiresUserAction(event: UserActionRequiredEvent) { callback.onRequiresUserAction(event) } override fun onProgress(step: Float) { callback.onProgress(step) } override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) } override fun onError(apiError: ApiError) { @@ -149,8 +160,8 @@ class Mobidziennik(val app: App, val profile: Profile?, val loginStore: LoginSto ERROR_MOBIDZIENNIK_WEB_NO_SESSION_KEY, ERROR_MOBIDZIENNIK_WEB_NO_SESSION_VALUE, ERROR_MOBIDZIENNIK_WEB_NO_SERVER_ID -> { - data.loginMethods.remove(LOGIN_METHOD_MOBIDZIENNIK_WEB) - data.prepareFor(mobidziennikLoginMethods, LOGIN_METHOD_MOBIDZIENNIK_WEB) + data.loginMethods.remove(LoginMethod.MOBIDZIENNIK_WEB) + data.prepareFor(LoginMethod.MOBIDZIENNIK_WEB) data.webSessionIdExpiryTime = 0 login() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/MobidziennikFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/MobidziennikFeatures.kt index 7396ac52..cd2c99ec 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/MobidziennikFeatures.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/MobidziennikFeatures.kt @@ -4,8 +4,10 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik -import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.models.Feature +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.data.db.enums.LoginType const val ENDPOINT_MOBIDZIENNIK_API_MAIN = 1000 const val ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_INBOX = 2011 @@ -18,19 +20,20 @@ const val ENDPOINT_MOBIDZIENNIK_WEB_ATTENDANCE = 2050 const val ENDPOINT_MOBIDZIENNIK_WEB_MANUALS = 2100 const val ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL = 2200 const val ENDPOINT_MOBIDZIENNIK_WEB_HOMEWORK = 2300 // not used as an endpoint +const val ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE = 2400 const val ENDPOINT_MOBIDZIENNIK_API2_MAIN = 3000 val MobidziennikFeatures = listOf( // always synced - Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_ALWAYS_NEEDED, listOf( - ENDPOINT_MOBIDZIENNIK_API_MAIN to LOGIN_METHOD_MOBIDZIENNIK_WEB, - ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL to LOGIN_METHOD_MOBIDZIENNIK_WEB - ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB)), // TODO divide features into separate view IDs (all with API_MAIN) + Feature(LoginType.MOBIDZIENNIK, FeatureType.ALWAYS_NEEDED, listOf( + ENDPOINT_MOBIDZIENNIK_API_MAIN to LoginMethod.MOBIDZIENNIK_WEB, + ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL to LoginMethod.MOBIDZIENNIK_WEB + )), // TODO divide features into separate view IDs (all with API_MAIN) // push config - Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_PUSH_CONFIG, listOf( - ENDPOINT_MOBIDZIENNIK_API2_MAIN to LOGIN_METHOD_MOBIDZIENNIK_API2 - ), listOf(LOGIN_METHOD_MOBIDZIENNIK_API2)).withShouldSync { data -> + Feature(LoginType.MOBIDZIENNIK, FeatureType.PUSH_CONFIG, listOf( + ENDPOINT_MOBIDZIENNIK_API2_MAIN to LoginMethod.MOBIDZIENNIK_API2 + )).withShouldSync { data -> !data.app.config.sync.tokenMobidziennikList.contains(data.profileId) }, @@ -38,33 +41,39 @@ val MobidziennikFeatures = listOf( + /** + * Timetable - web scraping - does nothing if the API_MAIN timetable is enough. + */ + Feature(LoginType.MOBIDZIENNIK, FeatureType.TIMETABLE, listOf( + ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE to LoginMethod.MOBIDZIENNIK_WEB + )), /** * Agenda - "API" + web scraping. */ - Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_AGENDA, listOf( - ENDPOINT_MOBIDZIENNIK_API_MAIN to LOGIN_METHOD_MOBIDZIENNIK_WEB, - ENDPOINT_MOBIDZIENNIK_WEB_CALENDAR to LOGIN_METHOD_MOBIDZIENNIK_WEB - ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB, LOGIN_METHOD_MOBIDZIENNIK_WEB)), + Feature(LoginType.MOBIDZIENNIK, FeatureType.AGENDA, listOf( + ENDPOINT_MOBIDZIENNIK_API_MAIN to LoginMethod.MOBIDZIENNIK_WEB, + ENDPOINT_MOBIDZIENNIK_WEB_CALENDAR to LoginMethod.MOBIDZIENNIK_WEB + )), /** * Grades - "API" + web scraping. */ - Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_GRADES, listOf( - ENDPOINT_MOBIDZIENNIK_API_MAIN to LOGIN_METHOD_MOBIDZIENNIK_WEB, - ENDPOINT_MOBIDZIENNIK_WEB_GRADES to LOGIN_METHOD_MOBIDZIENNIK_WEB - ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB, LOGIN_METHOD_MOBIDZIENNIK_WEB)), + Feature(LoginType.MOBIDZIENNIK, FeatureType.GRADES, listOf( + ENDPOINT_MOBIDZIENNIK_API_MAIN to LoginMethod.MOBIDZIENNIK_WEB, + ENDPOINT_MOBIDZIENNIK_WEB_GRADES to LoginMethod.MOBIDZIENNIK_WEB + )), /** * Behaviour - "API" + web scraping. */ - Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_BEHAVIOUR, listOf( - ENDPOINT_MOBIDZIENNIK_API_MAIN to LOGIN_METHOD_MOBIDZIENNIK_WEB, - ENDPOINT_MOBIDZIENNIK_WEB_NOTICES to LOGIN_METHOD_MOBIDZIENNIK_WEB - ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB, LOGIN_METHOD_MOBIDZIENNIK_WEB)), + Feature(LoginType.MOBIDZIENNIK, FeatureType.BEHAVIOUR, listOf( + ENDPOINT_MOBIDZIENNIK_API_MAIN to LoginMethod.MOBIDZIENNIK_WEB, + ENDPOINT_MOBIDZIENNIK_WEB_NOTICES to LoginMethod.MOBIDZIENNIK_WEB + )), /** * Attendance - only web scraping. */ - Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_ATTENDANCE, listOf( - ENDPOINT_MOBIDZIENNIK_WEB_ATTENDANCE to LOGIN_METHOD_MOBIDZIENNIK_WEB - ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB)), + Feature(LoginType.MOBIDZIENNIK, FeatureType.ATTENDANCE, listOf( + ENDPOINT_MOBIDZIENNIK_WEB_ATTENDANCE to LoginMethod.MOBIDZIENNIK_WEB + )), @@ -73,38 +82,38 @@ val MobidziennikFeatures = listOf( /** * Messages inbox - using web scraper. */ - Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_MESSAGES_INBOX, listOf( - ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_INBOX to LOGIN_METHOD_MOBIDZIENNIK_WEB, - ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL to LOGIN_METHOD_MOBIDZIENNIK_WEB - ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB)), + Feature(LoginType.MOBIDZIENNIK, FeatureType.MESSAGES_INBOX, listOf( + ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_INBOX to LoginMethod.MOBIDZIENNIK_WEB, + ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL to LoginMethod.MOBIDZIENNIK_WEB + )), /** * Messages sent - using web scraper. */ - Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_MESSAGES_SENT, listOf( - ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_SENT to LOGIN_METHOD_MOBIDZIENNIK_WEB, - ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL to LOGIN_METHOD_MOBIDZIENNIK_WEB - ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB)) + Feature(LoginType.MOBIDZIENNIK, FeatureType.MESSAGES_SENT, listOf( + ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_SENT to LoginMethod.MOBIDZIENNIK_WEB, + ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL to LoginMethod.MOBIDZIENNIK_WEB + )) // lucky number possibilities // all endpoints that may supply the lucky number - /*Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_LUCKY_NUMBER, listOf( - ENDPOINT_MOBIDZIENNIK_WEB_MANUALS to LOGIN_METHOD_MOBIDZIENNIK_WEB - ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB)).apply { priority = 10 }, + /*Feature(LoginType.MOBIDZIENNIK, FeatureType.LUCKY_NUMBER, listOf( + ENDPOINT_MOBIDZIENNIK_WEB_MANUALS to LoginMethod.MOBIDZIENNIK_WEB + )).apply { priority = 10 }, - Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_LUCKY_NUMBER, listOf( - ENDPOINT_MOBIDZIENNIK_WEB_CALENDAR to LOGIN_METHOD_MOBIDZIENNIK_WEB - ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB)).apply { priority = 3 }, + Feature(LoginType.MOBIDZIENNIK, FeatureType.LUCKY_NUMBER, listOf( + ENDPOINT_MOBIDZIENNIK_WEB_CALENDAR to LoginMethod.MOBIDZIENNIK_WEB + )).apply { priority = 3 }, - Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_LUCKY_NUMBER, listOf( - ENDPOINT_MOBIDZIENNIK_WEB_GRADES to LOGIN_METHOD_MOBIDZIENNIK_WEB - ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB)).apply { priority = 2 }, + Feature(LoginType.MOBIDZIENNIK, FeatureType.LUCKY_NUMBER, listOf( + ENDPOINT_MOBIDZIENNIK_WEB_GRADES to LoginMethod.MOBIDZIENNIK_WEB + )).apply { priority = 2 }, - Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_LUCKY_NUMBER, listOf( - ENDPOINT_MOBIDZIENNIK_WEB_NOTICES to LOGIN_METHOD_MOBIDZIENNIK_WEB - ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB)).apply { priority = 1 }, + Feature(LoginType.MOBIDZIENNIK, FeatureType.LUCKY_NUMBER, listOf( + ENDPOINT_MOBIDZIENNIK_WEB_NOTICES to LoginMethod.MOBIDZIENNIK_WEB + )).apply { priority = 1 }, - Feature(LOGIN_TYPE_MOBIDZIENNIK, FEATURE_LUCKY_NUMBER, listOf( - ENDPOINT_MOBIDZIENNIK_WEB_ATTENDANCE to LOGIN_METHOD_MOBIDZIENNIK_WEB - ), listOf(LOGIN_METHOD_MOBIDZIENNIK_WEB)).apply { priority = 4 }*/ + Feature(LoginType.MOBIDZIENNIK, FeatureType.LUCKY_NUMBER, listOf( + ENDPOINT_MOBIDZIENNIK_WEB_ATTENDANCE to LoginMethod.MOBIDZIENNIK_WEB + )).apply { priority = 4 }*/ ) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/MobidziennikData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/MobidziennikData.kt index 09e1e9fb..4b58d203 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/MobidziennikData.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/MobidziennikData.kt @@ -21,7 +21,7 @@ class MobidziennikData(val data: DataMobidziennik, val onSuccess: () -> Unit) { } private fun nextEndpoint(onSuccess: () -> Unit) { - if (data.targetEndpointIds.isEmpty()) { + if (data.targetEndpoints.isEmpty()) { onSuccess() return } @@ -29,8 +29,8 @@ class MobidziennikData(val data: DataMobidziennik, val onSuccess: () -> Unit) { onSuccess() return } - val id = data.targetEndpointIds.firstKey() - val lastSync = data.targetEndpointIds.remove(id) + val id = data.targetEndpoints.firstKey() + val lastSync = data.targetEndpoints.remove(id) useEndpoint(id, lastSync) { endpointId -> data.progress(data.progressStep) nextEndpoint(onSuccess) @@ -84,6 +84,10 @@ class MobidziennikData(val data: DataMobidziennik, val onSuccess: () -> Unit) { data.startProgress(R.string.edziennik_progress_endpoint_lucky_number) MobidziennikWebManuals(data, lastSync, onSuccess) }*/ + ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE-> { + data.startProgress(R.string.edziennik_progress_endpoint_timetable) + MobidziennikWebTimetable(data, lastSync, onSuccess) + } else -> onSuccess(endpointId) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiAttendance.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiAttendance.kt index 473b3691..26079f44 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiAttendance.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiAttendance.kt @@ -11,6 +11,8 @@ import pl.szczodrzynski.edziennik.data.db.entity.Attendance.Companion.TYPE_ABSEN import pl.szczodrzynski.edziennik.data.db.entity.Attendance.Companion.TYPE_PRESENT import pl.szczodrzynski.edziennik.data.db.entity.Attendance.Companion.TYPE_RELEASED import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.dateToSemester class MobidziennikApiAttendance(val data: DataMobidziennik, rows: List) { init { run { @@ -70,7 +72,7 @@ class MobidziennikApiAttendance(val data: DataMobidziennik, rows: List) data.metadataList.add( Metadata( data.profileId, - Metadata.TYPE_ATTENDANCE, + MetadataType.ATTENDANCE, id, data.profile?.empty ?: false || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == Attendance.TYPE_UNKNOWN, data.profile?.empty ?: false || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == Attendance.TYPE_UNKNOWN diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiEvents.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiEvents.kt index 7a72d3de..d8d9d7c7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiEvents.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiEvents.kt @@ -10,6 +10,7 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidzienn import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time import java.text.ParseException @@ -68,7 +69,7 @@ class MobidziennikApiEvents(val data: DataMobidziennik, rows: List) { data.metadataList.add( Metadata( data.profileId, - Metadata.TYPE_EVENT, + MetadataType.EVENT, id, data.profile?.empty ?: false, data.profile?.empty ?: false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiGrades.kt index c3c1b449..91ddedf0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiGrades.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiGrades.kt @@ -15,6 +15,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_SEMESTER2_ import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_YEAR_FINAL import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_YEAR_PROPOSED import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType class MobidziennikApiGrades(val data: DataMobidziennik, rows: List) { init { data.profile?.also { profile -> run { @@ -91,7 +92,7 @@ class MobidziennikApiGrades(val data: DataMobidziennik, rows: List) { data.metadataList.add( Metadata( data.profileId, - Metadata.TYPE_GRADE, + MetadataType.GRADE, id, data.profile?.empty ?: false, data.profile?.empty ?: false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiHomework.kt index 683a8820..52f7cdc4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiHomework.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiHomework.kt @@ -4,12 +4,13 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api -import android.text.Html import androidx.core.util.contains import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.utils.html.BetterHtml import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -26,7 +27,7 @@ class MobidziennikApiHomework(val data: DataMobidziennik, rows: List) { val id = cols[0].toLong() val teacherId = cols[7].toLong() val subjectId = cols[6].toLong() - val topic = Html.fromHtml(cols[1])?.toString()?.trim() ?: "" + val topic = BetterHtml.fromHtml(context = null, cols[1]).toString().trim() val eventDate = Date.fromYmd(cols[2]) val startTime = Time.fromYmdHm(cols[3]) @@ -47,7 +48,7 @@ class MobidziennikApiHomework(val data: DataMobidziennik, rows: List) { data.metadataList.add( Metadata( data.profileId, - Metadata.TYPE_HOMEWORK, + MetadataType.HOMEWORK, id, data.profile?.empty ?: false, data.profile?.empty ?: false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiNotices.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiNotices.kt index 66bfe1fd..c765e1ec 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiNotices.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiNotices.kt @@ -7,6 +7,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Notice +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.utils.models.Date class MobidziennikApiNotices(val data: DataMobidziennik, rows: List) { @@ -48,7 +49,7 @@ class MobidziennikApiNotices(val data: DataMobidziennik, rows: List) { data.metadataList.add( Metadata( data.profileId, - Metadata.TYPE_NOTICE, + MetadataType.NOTICE, id, data.profile?.empty ?: false, data.profile?.empty ?: false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTeams.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTeams.kt index 6440b04e..dbd1c03d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTeams.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTeams.kt @@ -6,8 +6,8 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik import pl.szczodrzynski.edziennik.data.db.entity.Team -import pl.szczodrzynski.edziennik.getById -import pl.szczodrzynski.edziennik.values +import pl.szczodrzynski.edziennik.ext.getById +import pl.szczodrzynski.edziennik.ext.values class MobidziennikApiTeams(val data: DataMobidziennik, tableTeams: List?, tableRelations: List?) { init { @@ -35,7 +35,6 @@ class MobidziennikApiTeams(val data: DataMobidziennik, tableTeams: List? } if (tableRelations != null) { val allTeams = data.teamList.values() - data.teamList.clear() for (row in tableRelations) { if (row.isEmpty()) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTimetable.kt index aa07b1c3..8aeca3f9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTimetable.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiTimetable.kt @@ -11,9 +11,10 @@ import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel import pl.szczodrzynski.edziennik.data.db.entity.Lesson import pl.szczodrzynski.edziennik.data.db.entity.LessonRange import pl.szczodrzynski.edziennik.data.db.entity.Metadata -import pl.szczodrzynski.edziennik.fixName -import pl.szczodrzynski.edziennik.keys -import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.fixName +import pl.szczodrzynski.edziennik.ext.keys +import pl.szczodrzynski.edziennik.ext.singleOrNull import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -24,7 +25,7 @@ class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List) { val dataStart = Date.getToday() val dataEnd = dataStart.clone().stepForward(0, 0, 7 + (6 - dataStart.weekDay)) - data.toRemove.add(DataRemoveModel.Timetable.between(dataStart.clone(), dataEnd)) + data.toRemove.add(DataRemoveModel.Timetable.between(dataStart.clone(), dataEnd, isExtra = false)) val dataDays = mutableListOf() while (dataStart <= dataEnd) { @@ -87,6 +88,7 @@ class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List) { } it.id = it.buildId() + it.ownerId = it.buildOwnerId() val seen = profile.empty || date < Date.getToday() @@ -94,7 +96,7 @@ class MobidziennikApiTimetable(val data: DataMobidziennik, rows: List) { data.metadataList.add( Metadata( data.profileId, - Metadata.TYPE_LESSON_CHANGE, + MetadataType.LESSON_CHANGE, it.id, seen, seen diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiUsers.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiUsers.kt index b95a55fe..943983d4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiUsers.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api/MobidziennikApiUsers.kt @@ -6,7 +6,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.api import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik import pl.szczodrzynski.edziennik.data.db.entity.Teacher -import pl.szczodrzynski.edziennik.fixName +import pl.szczodrzynski.edziennik.ext.fixName class MobidziennikApiUsers(val data: DataMobidziennik, rows: List) { init { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api2/MobidziennikApi2Main.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api2/MobidziennikApi2Main.kt index 70d32b18..9c231849 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api2/MobidziennikApi2Main.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/api2/MobidziennikApi2Main.kt @@ -14,8 +14,8 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBID import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.MobidziennikLoginApi2 import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS -import pl.szczodrzynski.edziennik.getJsonObject -import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.ext.getJsonObject +import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.utils.Utils class MobidziennikApi2Main(val data: DataMobidziennik, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikLuckyNumberExtractor.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikLuckyNumberExtractor.kt index ece23d7f..2ad8a61f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikLuckyNumberExtractor.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikLuckyNumberExtractor.kt @@ -8,6 +8,7 @@ import pl.szczodrzynski.edziennik.data.api.Regexes import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik import pl.szczodrzynski.edziennik.data.db.entity.LuckyNumber import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.utils.models.Date class MobidziennikLuckyNumberExtractor(val data: DataMobidziennik, text: String) { @@ -26,7 +27,7 @@ class MobidziennikLuckyNumberExtractor(val data: DataMobidziennik, text: String) data.metadataList.add( Metadata( data.profileId, - Metadata.TYPE_LUCKY_NUMBER, + MetadataType.LUCKY_NUMBER, luckyNumberObject.date.value.toLong(), true, data.profile?.empty ?: false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebAccountEmail.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebAccountEmail.kt index ef4c2847..6c252678 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebAccountEmail.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebAccountEmail.kt @@ -4,12 +4,13 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web -import pl.szczodrzynski.edziennik.DAY import pl.szczodrzynski.edziennik.data.api.Regexes import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb -import pl.szczodrzynski.edziennik.get +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,9 +25,10 @@ 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) + data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL, if (email == null) 3* DAY else 7* DAY) onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_ACCOUNT_EMAIL) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebAttendance.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebAttendance.kt index f23478b6..08ce6457 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebAttendance.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebAttendance.kt @@ -20,9 +20,11 @@ import pl.szczodrzynski.edziennik.data.db.entity.Attendance.Companion.TYPE_RELEA import pl.szczodrzynski.edziennik.data.db.entity.Attendance.Companion.TYPE_UNKNOWN import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS -import pl.szczodrzynski.edziennik.fixName -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.dateToSemester +import pl.szczodrzynski.edziennik.ext.fixName +import pl.szczodrzynski.edziennik.ext.get +import pl.szczodrzynski.edziennik.ext.singleOrNull import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -48,7 +50,7 @@ class MobidziennikWebAttendance(override val data: DataMobidziennik, //syncWeeks.clear() //syncWeeks += Date.fromY_m_d("2019-12-19") - syncWeeks.minBy { it.value }?.let { + syncWeeks.minByOrNull { it.value }?.let { data.toRemove.add(DataRemoveModel.Attendance.from(it)) } @@ -131,11 +133,11 @@ class MobidziennikWebAttendance(override val data: DataMobidziennik, // verify the lesson count is the same as dates & entries if (count != lessonDates.count() || count != entries.count()) return@forEach - ranges.forEach { range -> + ranges.onEach { range -> val lessonDate = dateIterator.next() val entry = entriesIterator.next() if (range == null || entry.isBlank()) - return@forEach + return@onEach val startTime = Time.fromH_m(range[1]) range[2].split(" / ").mapNotNull { @@ -186,7 +188,7 @@ class MobidziennikWebAttendance(override val data: DataMobidziennik, if (entry.startsWith(symbol) && symbol.length > typeSymbol.length) typeSymbol = symbol } - entry = entry.removePrefix(typeSymbol) + // entry = entry.removePrefix(typeSymbol) var isCustom = false val baseType = when (typeSymbol) { @@ -246,7 +248,7 @@ class MobidziennikWebAttendance(override val data: DataMobidziennik, data.metadataList.add( Metadata( data.profileId, - Metadata.TYPE_ATTENDANCE, + MetadataType.ATTENDANCE, id, data.profile?.empty ?: false || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == TYPE_UNKNOWN, data.profile?.empty ?: false || baseType == Attendance.TYPE_PRESENT_CUSTOM || baseType == TYPE_UNKNOWN diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebCalendar.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebCalendar.kt index 95c1f6df..11cfa69d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebCalendar.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebCalendar.kt @@ -12,10 +12,10 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.Mobidzien import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS -import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.utils.Utils.crc16 import pl.szczodrzynski.edziennik.utils.models.Date -import java.util.* class MobidziennikWebCalendar(override val data: DataMobidziennik, override val lastSync: Long?, @@ -30,7 +30,7 @@ class MobidziennikWebCalendar(override val data: DataMobidziennik, MobidziennikLuckyNumberExtractor(data, text) Regexes.MOBIDZIENNIK_CLASS_CALENDAR.find(text)?.let { - val events = JsonParser().parse(it.groupValues[1]).asJsonArray + val events = JsonParser.parseString(it.groupValues[1]).asJsonArray for (eventEl in events) { val event = eventEl.asJsonObject @@ -50,7 +50,7 @@ class MobidziennikWebCalendar(override val data: DataMobidziennik, val dateString = event.getString("start") ?: continue val eventDate = Date.fromY_m_d(dateString) - val eventType = when (event.getString("color")?.toLowerCase(Locale.getDefault())) { + val eventType = when (event.getString("color")?.lowercase()) { "#c54449" -> Event.TYPE_SHORT_QUIZ "#ab0001" -> Event.TYPE_EXAM "#008928" -> Event.TYPE_CLASS_EVENT @@ -81,12 +81,13 @@ class MobidziennikWebCalendar(override val data: DataMobidziennik, subjectId = -1, teamId = data.teamClass?.id ?: -1 ) + eventObject.isDownloaded = false data.eventList.add(eventObject) data.metadataList.add( Metadata( profileId, - Metadata.TYPE_EVENT, + MetadataType.EVENT, eventObject.id, profile?.empty ?: false, profile?.empty ?: false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetAttachment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetAttachment.kt index 52561a81..b8c6644f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetAttachment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetAttachment.kt @@ -28,7 +28,7 @@ class MobidziennikWebGetAttachment(override val data: DataMobidziennik, val targetFile = File(Utils.getStorageDir(), attachmentName) val typeUrl = when (owner) { - is Message -> if (owner.type == Message.TYPE_SENT) + is Message -> if (owner.isSent) "dziennik/wiadwyslana/?id=" else "dziennik/wiadodebrana/?id=" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetEvent.kt new file mode 100644 index 00000000..69a2dce0 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetEvent.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-1. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web + +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.data.api.POST +import pl.szczodrzynski.edziennik.data.api.Regexes +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb +import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent +import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.ext.get +import pl.szczodrzynski.edziennik.utils.models.Date + +class MobidziennikWebGetEvent( + override val data: DataMobidziennik, + val event: EventFull, + val onSuccess: () -> Unit +) : MobidziennikWeb(data, null) { + companion object { + private const val TAG = "MobidziennikWebGetEvent" + } + + init { + val params = listOf( + "typ" to "kalendarz", + "uczen" to data.studentId, + "id" to event.id, + ) + + webGet(TAG, "/dziennik/ajaxkalendarzklasowy", method = POST, parameters = params) { text -> + Regexes.MOBIDZIENNIK_EVENT_CONTENT.find(text)?.let { + val topic = it[1] + val teacherName = it[2] + val teacher = data.getTeacherByLastFirst(teacherName) + val addedDate = Date.fromY_m_d(it[3]) + val body = it[4] + .replace("\n", "") + .replace(Regexes.HTML_BR, "\n") + + event.topic = topic + event.homeworkBody = body + event.isDownloaded = true + event.teacherId = teacher.id + event.addedDate = addedDate.inMillis + } + + data.eventList.add(event) + data.eventListReplace = true + + EventBus.getDefault().postSticky(EventGetEvent(event)) + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetHomework.kt index b0699b4b..9dbdd439 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetHomework.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetHomework.kt @@ -10,43 +10,60 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidzienn import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent import pl.szczodrzynski.edziennik.data.db.full.EventFull -import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.ext.get import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time class MobidziennikWebGetHomework(override val data: DataMobidziennik, val event: EventFull, val onSuccess: () -> Unit ) : MobidziennikWeb(data, null) { companion object { - private const val TAG = "MobidziennikWebHomework" + private const val TAG = "MobidziennikWebGetHomework" } init { - val endpoint = if (event.date >= Date.getToday()) - "zadaniadomowe" - else - "zadaniadomowearchiwalne" - - webGet(TAG, "/mobile/$endpoint") { text -> + webGet(TAG, "/dziennik/wyslijzadanie/?id_zadania=${event.id}&uczen=${data.studentId}") { text -> MobidziennikLuckyNumberExtractor(data, text) - Regexes.MOBIDZIENNIK_HOMEWORK_ROW.findAll(text).forEach { homeworkMatch -> - val tableRow = homeworkMatch[1].ifBlank { return@forEach } - - val id = Regexes.MOBIDZIENNIK_HOMEWORK_ID.find(tableRow)?.get(1)?.toLongOrNull() ?: return@forEach - if (event.id != id) + event.clearAttachments() + Regexes.MOBIDZIENNIK_WEB_ATTACHMENT.findAll(text).forEach { match -> + if (match[1].isNotEmpty()) return@forEach - - event.attachmentIds = mutableListOf() - event.attachmentNames = mutableListOf() - Regexes.MOBIDZIENNIK_HOMEWORK_ATTACHMENT.findAll(tableRow).forEach { - event.attachmentIds?.add(it[2].toLongOrNull() ?: return@forEach) - event.attachmentNames?.add(it[3]) - } - - event.homeworkBody = "" + val attachmentId = match[2].toLong() + val attachmentName = match[3] + event.addAttachment(attachmentId, attachmentName) } + Regexes.MOBIDZIENNIK_WEB_HOMEWORK_ADDED_DATE.find(text)?.let { + // (Kowalski Jan), (wtorek), (2) (stycznia) (2019), godzina (12:34:56) + val month = when (it[4]) { + "stycznia" -> 1 + "lutego" -> 2 + "marca" -> 3 + "kwietnia" -> 4 + "maja" -> 5 + "czerwca" -> 6 + "lipca" -> 7 + "sierpnia" -> 8 + "września" -> 9 + "października" -> 10 + "listopada" -> 11 + "grudnia" -> 12 + else -> 1 + } + val addedDate = Date( + it[5].toInt(), + month, + it[3].toInt() + ) + val time = Time.fromH_m_s(it[6]) + event.addedDate = addedDate.combineWith(time) + } + + event.homeworkBody = "" + event.isDownloaded = true + data.eventList.add(event) data.eventListReplace = true diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetMessage.kt index 7282cc2d..23650982 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetMessage.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetMessage.kt @@ -10,14 +10,13 @@ import pl.szczodrzynski.edziennik.data.api.Regexes import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent -import pl.szczodrzynski.edziennik.data.db.entity.Message -import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.data.db.full.MessageRecipientFull -import pl.szczodrzynski.edziennik.fixName -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.ext.fixName +import pl.szczodrzynski.edziennik.ext.get +import pl.szczodrzynski.edziennik.ext.singleOrNull import pl.szczodrzynski.edziennik.utils.Utils.monthFromName import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -31,7 +30,7 @@ class MobidziennikWebGetMessage(override val data: DataMobidziennik, } init { - val typeUrl = if (message.type == Message.TYPE_SENT) + val typeUrl = if (message.isSent) "wiadwyslana" else "wiadodebrana" @@ -42,13 +41,14 @@ class MobidziennikWebGetMessage(override val data: DataMobidziennik, val doc = Jsoup.parse(text) - val content = doc.select("#content").first() + val content = doc.select("#content").first() ?: return@webGet val body = content.select(".wiadomosc_tresc").first() + val bodyHtml = body?.html() ?: "" - if (message.type == TYPE_RECEIVED) { + if (message.isReceived) { var readDate = System.currentTimeMillis() - Regexes.MOBIDZIENNIK_MESSAGE_READ_DATE.find(body.html())?.let { + Regexes.MOBIDZIENNIK_MESSAGE_READ_DATE.find(bodyHtml)?.let { val date = Date( it[3].toIntOrNull() ?: 2019, monthFromName(it[2]), @@ -73,35 +73,35 @@ class MobidziennikWebGetMessage(override val data: DataMobidziennik, } else { message.senderId = null - content.select("table.spis tr:has(td)")?.forEach { recipientEl -> - val senderEl = recipientEl.select("td:eq(1)")?.first() ?: return@forEach + content.select("table.spis tr:has(td)").forEach { recipientEl -> + val senderEl = recipientEl.select("td:eq(1)").first() ?: return@forEach val senderName = senderEl.text().fixName() val teacher = data.teacherList.singleOrNull { it.fullNameLastFirst == senderName } val receiverId = teacher?.id ?: -1 var readDate = 0L - val isReadEl = recipientEl.select("td:eq(4)")?.first() ?: return@forEach + val isReadEl = recipientEl.select("td:eq(4)").first() ?: return@forEach if (isReadEl.html().contains("tak")) { - val readDateEl = recipientEl.select("td:eq(5) small")?.first() ?: return@forEach + val readDateEl = recipientEl.select("td:eq(5) small").first() ?: return@forEach Regexes.MOBIDZIENNIK_MESSAGE_SENT_READ_DATE.find(readDateEl.ownText())?.let { val date = Date( - it[3].toIntOrNull() ?: 2019, - monthFromName(it[2]), - it[1].toIntOrNull() ?: 1 + it[3].toIntOrNull() ?: 2019, + monthFromName(it[2]), + it[1].toIntOrNull() ?: 1 ) val time = Time.fromH_m_s( - it[4] // TODO blank string safety + it[4] // TODO blank string safety ) readDate = date.combineWith(time) } } val recipient = MessageRecipientFull( - profileId = profileId, - id = receiverId, - messageId = message.id, - readDate = readDate + profileId = profileId, + id = receiverId, + messageId = message.id, + readDate = readDate ) recipient.fullName = teacher?.fullName ?: "?" @@ -111,32 +111,30 @@ class MobidziennikWebGetMessage(override val data: DataMobidziennik, } // this line removes the sender and read date details - body.select("div").remove() + body?.select("div")?.remove() // this needs to be at the end message.apply { - this.body = body.html() + this.body = body?.html() clearAttachments() - content.select("ul li").map { it.select("a").first() }.forEach { - val attachmentName = it.ownText() - Regexes.MOBIDZIENNIK_MESSAGE_ATTACHMENT.find(it.outerHtml())?.let { match -> - val attachmentId = match[1].toLong() - var size = match[2].toFloatOrNull() ?: -1f - when (match[3]) { - "K" -> size *= 1024f - "M" -> size *= 1024f * 1024f - "G" -> size *= 1024f * 1024f * 1024f - } - message.addAttachment(attachmentId, attachmentName, size.toLong()) + Regexes.MOBIDZIENNIK_WEB_ATTACHMENT.findAll(text).forEach { match -> + val attachmentId = match[2].toLong() + val attachmentName = match[3] + var size = match[4].toFloatOrNull() ?: -1f + when (match[5]) { + "K" -> size *= 1024f + "M" -> size *= 1024f * 1024f + "G" -> size *= 1024f * 1024f * 1024f } + message.addAttachment(attachmentId, attachmentName, size.toLong()) } } if (!message.seen) { // TODO discover why this monstrosity instead of MetadataDao.setSeen data.setSeenMetadataList.add(Metadata( message.profileId, - Metadata.TYPE_MESSAGE, + MetadataType.MESSAGE, message.id, true, true diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetRecipientList.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetRecipientList.kt index 0ed1e4a6..b6f2afa7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetRecipientList.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGetRecipientList.kt @@ -9,12 +9,12 @@ import androidx.room.OnConflictStrategy import com.google.gson.JsonObject import com.google.gson.JsonParser import org.greenrobot.eventbus.EventBus -import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.Regexes import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb import pl.szczodrzynski.edziennik.data.api.events.RecipientListGetEvent import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.ext.* class MobidziennikWebGetRecipientList(override val data: DataMobidziennik, val onSuccess: () -> Unit @@ -26,7 +26,7 @@ class MobidziennikWebGetRecipientList(override val data: DataMobidziennik, init { webGet(TAG, "/mobile/dodajwiadomosc") { text -> Regexes.MOBIDZIENNIK_MESSAGE_RECIPIENTS_JSON.find(text)?.let { match -> - val recipientLists = JsonParser().parse(match[1]).asJsonArray + val recipientLists = JsonParser.parseString(match[1]).asJsonArray recipientLists?.asJsonObjectList()?.forEach { list -> val listType = list.getString("typ")?.toIntOrNull() ?: -1 val listName = list.getString("nazwa") ?: "" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGrades.kt index 43b67d23..dcf77fee 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGrades.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebGrades.kt @@ -14,9 +14,11 @@ import pl.szczodrzynski.edziennik.data.db.entity.Grade import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NORMAL import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS -import pl.szczodrzynski.edziennik.fixWhiteSpaces -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.dateToSemester +import pl.szczodrzynski.edziennik.ext.fixWhiteSpaces +import pl.szczodrzynski.edziennik.ext.get +import pl.szczodrzynski.edziennik.ext.singleOrNull import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -135,7 +137,7 @@ class MobidziennikWebGrades(override val data: DataMobidziennik, data.metadataList.add( Metadata( profileId, - Metadata.TYPE_GRADE, + MetadataType.GRADE, gradeObject.id, profile.empty, profile.empty diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebHomework.kt index 944b21ea..f9638ac1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebHomework.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebHomework.kt @@ -11,7 +11,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.data.api.events.EventGetEvent import pl.szczodrzynski.edziennik.data.db.full.EventFull -import pl.szczodrzynski.edziennik.get +import pl.szczodrzynski.edziennik.ext.get class MobidziennikWebHomework(override val data: DataMobidziennik, override val lastSync: Long?, @@ -33,14 +33,14 @@ class MobidziennikWebHomework(override val data: DataMobidziennik, webGet(TAG, "/mobile/$endpoint") { text -> MobidziennikLuckyNumberExtractor(data, text) - Regexes.MOBIDZIENNIK_HOMEWORK_ROW.findAll(text).forEach { homeworkMatch -> + Regexes.MOBIDZIENNIK_MOBILE_HOMEWORK_ROW.findAll(text).forEach { homeworkMatch -> val tableRow = homeworkMatch[1].ifBlank { return@forEach } /*val items = Regexes.MOBIDZIENNIK_HOMEWORK_ITEM.findAll(tableRow).map { match -> match[1] to match[2].fixWhiteSpaces() }.toList()*/ - val id = Regexes.MOBIDZIENNIK_HOMEWORK_ID.find(tableRow)?.get(1)?.toLongOrNull() ?: return@forEach + val id = Regexes.MOBIDZIENNIK_MOBILE_HOMEWORK_ID.find(tableRow)?.get(1)?.toLongOrNull() ?: return@forEach if (event.id != id) return@forEach @@ -48,12 +48,13 @@ class MobidziennikWebHomework(override val data: DataMobidziennik, event.attachmentIds = mutableListOf() event.attachmentNames = mutableListOf() - Regexes.MOBIDZIENNIK_HOMEWORK_ATTACHMENT.findAll(tableRow).forEach { - event.attachmentIds?.add(it[1].toLongOrNull() ?: return@forEach) + Regexes.MOBIDZIENNIK_MOBILE_HOMEWORK_ATTACHMENT.findAll(tableRow).onEach { + event.attachmentIds?.add(it[1].toLongOrNull() ?: return@onEach) event.attachmentNames?.add(it[2]) } event.homeworkBody = "" + event.isDownloaded = true } //data.eventList.add(eventObject) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesAll.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesAll.kt index 4e8f900a..99715831 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesAll.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesAll.kt @@ -5,7 +5,6 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web import org.jsoup.Jsoup -import pl.szczodrzynski.edziennik.DAY import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb @@ -14,9 +13,12 @@ import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient import pl.szczodrzynski.edziennik.data.db.entity.Metadata -import pl.szczodrzynski.edziennik.fixName -import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.DAY +import pl.szczodrzynski.edziennik.ext.fixName +import pl.szczodrzynski.edziennik.ext.singleOrNull import pl.szczodrzynski.edziennik.utils.models.Date +import java.net.URLEncoder class MobidziennikWebMessagesAll(override val data: DataMobidziennik, override val lastSync: Long?, @@ -27,30 +29,31 @@ class MobidziennikWebMessagesAll(override val data: DataMobidziennik, } init { - webGet(TAG, "/dziennik/wyszukiwarkawiadomosci?q=+") { text -> + val query = URLEncoder.encode(data.profile?.studentNameLong ?: "a", "UTF-8") + webGet(TAG, "/dziennik/wyszukiwarkawiadomosci?q=$query") { text -> MobidziennikLuckyNumberExtractor(data, text) val doc = Jsoup.parse(text) - val listElement = doc.getElementsByClass("spis")?.first() + val listElement = doc.getElementsByClass("spis").first() if (listElement == null) { - data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL, 7*DAY) + data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL, 7* DAY) onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL) return@webGet } val list = listElement.getElementsByClass("podswietl") - list?.forEach { item -> + list.forEach { item -> val id = item.attr("rel").replace("[^\\d]".toRegex(), "").toLongOrNull() ?: return@forEach val subjectEl = item.select("td:eq(0) div").first() - val subject = subjectEl.text() + val subject = subjectEl?.text() ?: "" val addedDateEl = item.select("td:eq(1)").first() - val addedDate = Date.fromIsoHm(addedDateEl.text()) + val addedDate = Date.fromIsoHm(addedDateEl?.text()) val typeEl = item.select("td:eq(2) img").first() var type = TYPE_RECEIVED - if (typeEl.outerHtml().contains("mail_send.png")) + if (typeEl?.outerHtml()?.contains("mail_send.png") == true) type = TYPE_SENT val senderEl = item.select("td:eq(3) div").first() @@ -58,13 +61,13 @@ class MobidziennikWebMessagesAll(override val data: DataMobidziennik, if (type == TYPE_RECEIVED) { // search sender teacher - val senderName = senderEl.text().fixName() + val senderName = senderEl?.text().fixName() senderId = data.teacherList.singleOrNull { it.fullNameLastFirst == senderName }?.id data.messageRecipientList.add(MessageRecipient(profileId, -1, id)) } else { // TYPE_SENT, so multiple recipients possible - val recipientNames = senderEl.text().split(", ") - for (recipientName in recipientNames) { + val recipientNames = senderEl?.text()?.split(", ") + recipientNames?.forEach { recipientName -> val name = recipientName.fixName() val recipientId = data.teacherList.singleOrNull { it.fullNameLastFirst == name }?.id ?: -1 data.messageRecipientIgnoreList.add(MessageRecipient(profileId, recipientId, id)) @@ -72,22 +75,22 @@ class MobidziennikWebMessagesAll(override val data: DataMobidziennik, } val message = Message( - profileId = profileId, - id = id, - type = type, - subject = subject, - body = null, - senderId = senderId, - addedDate = addedDate + profileId = profileId, + id = id, + type = type, + subject = subject, + body = null, + senderId = senderId, + addedDate = addedDate ) data.messageList.add(message) - data.metadataList.add(Metadata(profileId, Metadata.TYPE_MESSAGE, message.id, true, true)) + data.metadataList.add(Metadata(profileId, MetadataType.MESSAGE, message.id, true, true)) } // sync every 7 days as we probably don't expect more than // 30 received messages during a week, without any normal sync - data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL, 7*DAY) + data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL, 7* DAY) onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_ALL) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesInbox.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesInbox.kt index be762228..698f125f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesInbox.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesInbox.kt @@ -12,8 +12,9 @@ import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS -import pl.szczodrzynski.edziennik.fixName -import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.fixName +import pl.szczodrzynski.edziennik.ext.singleOrNull import pl.szczodrzynski.edziennik.utils.models.Date class MobidziennikWebMessagesInbox(override val data: DataMobidziennik, @@ -36,26 +37,35 @@ class MobidziennikWebMessagesInbox(override val data: DataMobidziennik, val doc = Jsoup.parse(text) - val list = doc.getElementsByClass("spis")?.first()?.getElementsByClass("podswietl") + val today = Date.getToday() + var currentYear = today.year + var currentMonth = today.month + + val list = doc.getElementsByClass("spis").first()?.getElementsByClass("podswietl") list?.forEach { item -> val id = item.attr("rel").toLongOrNull() ?: return@forEach val subjectEl = item.select("td:eq(0)").first() var hasAttachments = false - if (subjectEl.getElementsByTag("a").size != 0) { + if (subjectEl?.getElementsByTag("a")?.size ?: 0 > 0) { hasAttachments = true } - val subject = subjectEl.ownText() + val subject = subjectEl?.ownText() ?: "" - val addedDateEl = item.select("td:eq(1) small").first() - val addedDate = Date.fromIsoHm(addedDateEl.text()) + val addedDateEl = item.select("td:eq(4)").first() + val (date, time) = data.parseDateTime(addedDateEl?.text()?.trim() ?: "") + if (date.month > currentMonth) { + currentYear-- + } + currentMonth = date.month + date.year = currentYear - val senderEl = item.select("td:eq(2)").first() - val senderName = senderEl.ownText().fixName() + val senderEl = item.select("td:eq(3)").first() + val senderName = senderEl?.ownText().fixName() val senderId = data.teacherList.singleOrNull { it.fullNameLastFirst == senderName }?.id data.messageRecipientIgnoreList.add(MessageRecipient(profileId, -1, id)) - val isRead = item.select("td:eq(3) span").first().hasClass("wiadomosc_przeczytana") + val isRead = item.select("td:eq(5) span").first()?.hasClass("wiadomosc_przeczytana") == true val message = Message( profileId = profileId, @@ -64,7 +74,7 @@ class MobidziennikWebMessagesInbox(override val data: DataMobidziennik, subject = subject, body = null, senderId = senderId, - addedDate = addedDate + addedDate = date.combineWith(time) ) if (hasAttachments) @@ -74,7 +84,7 @@ class MobidziennikWebMessagesInbox(override val data: DataMobidziennik, data.setSeenMetadataList.add( Metadata( profileId, - Metadata.TYPE_MESSAGE, + MetadataType.MESSAGE, message.id, isRead, isRead || profile?.empty ?: false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesSent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesSent.kt index 543df329..2e15751c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesSent.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebMessagesSent.kt @@ -5,8 +5,6 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web import org.jsoup.Jsoup -import pl.szczodrzynski.edziennik.DAY -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES import pl.szczodrzynski.edziennik.data.api.Regexes import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_SENT @@ -15,9 +13,12 @@ import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS -import pl.szczodrzynski.edziennik.fixName -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.singleOrNull +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.DAY +import pl.szczodrzynski.edziennik.ext.fixName +import pl.szczodrzynski.edziennik.ext.get +import pl.szczodrzynski.edziennik.ext.singleOrNull import pl.szczodrzynski.edziennik.utils.models.Date class MobidziennikWebMessagesSent(override val data: DataMobidziennik, @@ -40,37 +41,48 @@ class MobidziennikWebMessagesSent(override val data: DataMobidziennik, val doc = Jsoup.parse(text) - val list = doc.getElementsByClass("spis")?.first()?.getElementsByClass("podswietl") + val today = Date.getToday() + var currentYear = today.year + var currentMonth = today.month + + val list = doc.getElementsByClass("spis").first()?.getElementsByClass("podswietl") list?.forEach { item -> val id = item.attr("rel").toLongOrNull() ?: return@forEach val subjectEl = item.select("td:eq(0)").first() + val subject = subjectEl?.ownText() ?: "" + + val attachmentsEl = item.select("td:eq(1)").first() var hasAttachments = false - if (subjectEl.getElementsByTag("a").size != 0) { + if (attachmentsEl?.getElementsByTag("a")?.size ?: 0 > 0) { hasAttachments = true } - val subject = subjectEl.ownText() - val readByString = item.select("td:eq(2)").first().text() + val readByString = item.select("td:eq(4)").first()?.text() ?: "" val (readBy, sentTo) = Regexes.MOBIDZIENNIK_MESSAGE_SENT_READ_BY.find(readByString).let { (it?.get(1)?.toIntOrNull() ?: 0) to (it?.get(2)?.toIntOrNull() ?: 0) } - val recipientEl = item.select("td:eq(1) a span").first() - val recipientNames = recipientEl.ownText().split(", ") + val recipientEl = item.select("td:eq(2) a span").first() + val recipientNames = recipientEl?.ownText()?.split(", ") val readState = when (readBy) { 0 -> 0 sentTo -> 1 else -> -1 }.toLong() - for (recipientName in recipientNames) { + recipientNames?.forEach { recipientName -> val name = recipientName.fixName() val recipientId = data.teacherList.singleOrNull { it.fullNameLastFirst == name }?.id ?: -1 data.messageRecipientIgnoreList.add(MessageRecipient(profileId, recipientId, -1, readState, id)) } - val addedDateEl = item.select("td:eq(3) small").first() - val addedDate = Date.fromIsoHm(addedDateEl.text()) + val addedDateEl = item.select("td:eq(3)").first() + val (date, time) = data.parseDateTime(addedDateEl?.text()?.trim() ?: "") + if (date.month > currentMonth) { + currentYear-- + } + currentMonth = date.month + date.year = currentYear val message = Message( profileId = profileId, @@ -79,7 +91,7 @@ class MobidziennikWebMessagesSent(override val data: DataMobidziennik, subject = subject, body = null, senderId = null, - addedDate = addedDate + addedDate = date.combineWith(time) ) if (hasAttachments) @@ -89,14 +101,14 @@ class MobidziennikWebMessagesSent(override val data: DataMobidziennik, data.setSeenMetadataList.add( Metadata( profileId, - Metadata.TYPE_MESSAGE, + MetadataType.MESSAGE, message.id, true, true )) } - data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_SENT, 1*DAY, DRAWER_ITEM_MESSAGES) + data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_SENT, 1* DAY, FeatureType.MESSAGES_SENT) onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_MESSAGES_SENT) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebSendMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebSendMessage.kt index 89178b8b..d4965b36 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebSendMessage.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebSendMessage.kt @@ -9,12 +9,12 @@ import pl.szczodrzynski.edziennik.data.api.POST import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent -import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType class MobidziennikWebSendMessage(override val data: DataMobidziennik, - val recipients: List, + val recipients: Set, val subject: String, val text: String, val onSuccess: () -> Unit @@ -43,8 +43,8 @@ class MobidziennikWebSendMessage(override val data: DataMobidziennik, // TODO create MobidziennikWebMessagesSent and replace this MobidziennikWebMessagesAll(data, null) { - val message = data.messageList.firstOrNull { it.type == Message.TYPE_SENT && it.subject == subject } - val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == message?.id } + val message = data.messageList.firstOrNull { it.isSent && it.subject == subject } + val metadata = data.metadataList.firstOrNull { it.thingType == MetadataType.MESSAGE && it.thingId == message?.id } val event = MessageSentEvent(data.profileId, message, message?.addedDate) EventBus.getDefault().postSticky(event) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebTimetable.kt new file mode 100644 index 00000000..206c517e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/data/web/MobidziennikWebTimetable.kt @@ -0,0 +1,367 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-9-8. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.web + +import android.annotation.SuppressLint +import org.jsoup.Jsoup +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.Regexes +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE +import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel.Timetable.Companion.between +import pl.szczodrzynski.edziennik.data.db.entity.Lesson +import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.DAY +import pl.szczodrzynski.edziennik.ext.MS +import pl.szczodrzynski.edziennik.ext.get +import pl.szczodrzynski.edziennik.ext.getString +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time +import pl.szczodrzynski.edziennik.utils.models.Week +import kotlin.collections.set + +class MobidziennikWebTimetable( + override val data: DataMobidziennik, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit +) : MobidziennikWeb(data, lastSync) { + companion object { + private const val TAG = "MobidziennikWebTimetable" + } + + private val rangesH = mutableMapOf, Date>() + private val hoursV = mutableMapOf>() + private var startDate: Date + + private fun parseCss(css: String): Map { + return css.split(";").mapNotNull { + val spl = it.split(":") + if (spl.size != 2) + return@mapNotNull null + return@mapNotNull spl[0].trim() to spl[1].trim() + }.toMap() + } + + private fun getRangeH(h: Float): Date? { + return rangesH.entries.firstOrNull { + h in it.key + }?.value + } + + private fun stringToDate(date: String): Date? { + val items = date.split(" ") + val day = items.getOrNull(0)?.toIntOrNull() ?: return null + val year = items.getOrNull(2)?.toIntOrNull() ?: return null + val month = when (items.getOrNull(1)) { + "stycznia" -> 1 + "lutego" -> 2 + "marca" -> 3 + "kwietnia" -> 4 + "maja" -> 5 + "czerwca" -> 6 + "lipca" -> 7 + "sierpnia" -> 8 + "września" -> 9 + "października" -> 10 + "listopada" -> 11 + "grudnia" -> 12 + else -> return null + } + return Date(year, month, day) + } + + init { + val currentWeekStart = Week.getWeekStart() + val nextWeekEnd = Week.getWeekEnd().stepForward(0, 0, 7) + if (Date.getToday().weekDay > 4) { + currentWeekStart.stepForward(0, 0, 7) + } + startDate = data.arguments?.getString("weekStart")?.let { + Date.fromY_m_d(it) + } ?: currentWeekStart + + val syncFutureDate = startDate > nextWeekEnd + val syncPastDate = startDate < currentWeekStart + val syncExtraLessons = System.currentTimeMillis() - (lastSync ?: 0) > 2 * DAY * MS + // sync not needed - everything present in the "API" + if (!syncFutureDate && !syncPastDate && !syncExtraLessons) { + onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE) + } + else { + val types = when { + syncFutureDate || syncPastDate -> mutableListOf("podstawowy", "pozalekcyjny") + syncExtraLessons -> mutableListOf("pozalekcyjny") + else -> mutableListOf() + } + + val syncingExtra = types.contains("pozalekcyjny") + + syncTypes(types, startDate) { + if (syncingExtra) { + val endDate = startDate.clone().stepForward(0, 0, 7) + data.toRemove.add(between(startDate, endDate, isExtra = true)) + } + + // set as synced now only when not syncing future/past date + // (to avoid waiting 2 days for normal sync after future/past sync) + if (!syncFutureDate && !syncPastDate) + data.setSyncNext(ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE, SYNC_ALWAYS) + onSuccess(ENDPOINT_MOBIDZIENNIK_WEB_TIMETABLE) + } + } + } + + private fun syncTypes(types: MutableList, startDate: Date, onSuccess: () -> Unit) { + if (types.isEmpty()) { + onSuccess() + return + } + val type = types.removeAt(0) + webGet(TAG, "/dziennik/planlekcji?typ=$type&tydzien=${startDate.stringY_m_d}") { html -> + MobidziennikLuckyNumberExtractor(data, html) + readRangesH(html) + readRangesV(html) + readLessons(html, isExtra = type == "pozalekcyjny") + syncTypes(types, startDate, onSuccess) + } + } + + private fun readRangesH(html: String) { + val htmlH = Regexes.MOBIDZIENNIK_TIMETABLE_TOP.find(html) ?: return + val docH = Jsoup.parse(htmlH.value) + + var posH = 0f + for (el in docH.select("div > div")) { + val css = parseCss(el.attr("style")) + val width = css["width"] + ?.trimEnd('%') + ?.toFloatOrNull() + ?: continue + val value = stringToDate(el.attr("title")) + ?: continue + + val range = posH.rangeTo(posH + width) + posH += width + + rangesH[range] = value + } + } + + private fun readRangesV(html: String) { + val htmlV = Regexes.MOBIDZIENNIK_TIMETABLE_LEFT.find(html) ?: return + val docV = Jsoup.parse(htmlV.value) + + for (el in docV.select("div > div")) { + val css = parseCss(el.attr("style")) + val top = css["top"] + ?.trimEnd('%') + ?.toFloatOrNull() + ?: continue + val values = el.text().split(" ") + + val time = values.getOrNull(0)?.let { + Time.fromH_m(it) + } ?: continue + val num = values.getOrNull(1)?.toIntOrNull() + + hoursV[(top * 100).toInt()] = time to num + } + } + + private val whitespaceRegex = "\\s+".toRegex() + private val classroomRegex = "\\((.*)\\)".toRegex() + private fun cleanup(str: String): List { + return str + .replace(whitespaceRegex, " ") + .replace("\n", "") + .replace("<small>", "$") + .replace("</small>", "$") + .replace("<br />", "\n") + .replace("<br/>", "\n") + .replace("<br>", "\n") + .replace("
    ", "\n") + .replace("
    ", "\n") + .replace("
    ", "\n") + .replace("", "%") + .replace("", "%") + .replace("", "") + .replace("", "") + .split("\n") + .map { it.trim() } + } + + @SuppressLint("LongLogTag", "LogNotTimber") + private fun readLessons(html: String, isExtra: Boolean) { + val matches = Regexes.MOBIDZIENNIK_TIMETABLE_CELL.findAll(html) + + val noLessonDays = mutableListOf() + for (i in 0..6) { + noLessonDays.add(startDate.clone().stepForward(0, 0, i)) + } + + for (match in matches) { + val css = parseCss("${match[1]};${match[2]}") + val left = css["left"]?.trimEnd('%')?.toFloatOrNull() ?: continue + val top = css["top"]?.trimEnd('%')?.toFloatOrNull() ?: continue + val width = css["width"]?.trimEnd('%')?.toFloatOrNull() ?: continue + val height = css["height"]?.trimEnd('%')?.toFloatOrNull() ?: continue + + val posH = left + width / 2f + val topInt = (top * 100).toInt() + val bottomInt = ((top + height) * 100).toInt() + + val lessonDate = getRangeH(posH) ?: continue + val (startTime, lessonNumber) = hoursV[topInt] ?: continue + val endTime = hoursV[bottomInt]?.first ?: continue + + noLessonDays.remove(lessonDate) + + var typeName: String? = null + var subjectName: String? = null + var teacherName: String? = null + var classroomName: String? = null + var teamName: String? = null + val items = (cleanup(match[3]) + cleanup(match[4])).toMutableList() + + // comparing items size before and after the iteration + var length = 0 + while (items.isNotEmpty() && length != items.size) { + length = items.size + var i = 0 + while (i < items.size) { + // just to remain safe - I have no idea how all of this works. + if (i < 0) + break + val item = items[i] + when { + // remove empty items + item.isEmpty() -> { + items.remove(item) + i-- + } + // remove HH:MM items - it's calculated from the block position + item.contains(":") && item.contains(" - ") -> { + items.remove(item) + i-- + } + + item.startsWith("%") -> { + // the one wrapped in % is the short subject name + items.remove(item) + // remove the first remaining item + subjectName = items.removeAt(0) + // decrement the index counter + i -= 2 + } + + item.startsWith("$") -> { + typeName = item.trim('$') + items.remove(item) + i-- + } + typeName != null && (item.contains(typeName) || item.contains("
    ")) -> { + items.remove(item) + i-- + } + + item.contains("(") && item.contains(")") -> { + classroomName = classroomRegex.find(item)?.get(1) + items[i] = item.replace("($classroomName)", "").trim() + } + classroomName != null && item.contains(classroomName) -> { + items[i] = item.replace("($classroomName)", "").trim() + } + + item.contains("class=\"wyjatek tooltip\"") -> { + items.remove(item) + i-- + } + } + // finally advance to the next item + i++ + } + } + + if (items.size == 2 && items[0].contains(" - ")) { + val parts = items[0].split(" - ") + teamName = parts[0] + teacherName = parts[1] + } + else if (items.size == 2 && typeName?.contains("odwołana") == true) { + teamName = items[0] + } + else if (items.size == 4) { + teamName = items[0] + teacherName = items[1] + } + + val type = when (typeName) { + "zastępstwo" -> Lesson.TYPE_CHANGE + "lekcja odwołana", "odwołana" -> Lesson.TYPE_CANCELLED + else -> Lesson.TYPE_NORMAL + } + val subject = subjectName?.let { data.getSubject(null, it) } + val teacher = teacherName?.let { data.getTeacherByLastFirst(it) } + val team = teamName?.let { data.getTeam( + id = null, + name = it, + schoolCode = data.loginServerName ?: return@let null, + isTeamClass = false + ) } + + Lesson(data.profileId, -1).also { + it.type = type + if (type == Lesson.TYPE_CANCELLED) { + it.oldDate = lessonDate + it.oldLessonNumber = lessonNumber + it.oldStartTime = startTime + it.oldEndTime = endTime + it.oldSubjectId = subject?.id ?: -1 + it.oldTeamId = team?.id ?: -1 + } + else { + it.date = lessonDate + it.lessonNumber = lessonNumber + it.startTime = startTime + it.endTime = endTime + it.subjectId = subject?.id ?: -1 + it.teacherId = teacher?.id ?: -1 + it.teamId = team?.id ?: -1 + it.classroom = classroomName + } + + it.id = it.buildId() + it.ownerId = it.buildOwnerId() + it.isExtra = isExtra + + val seen = profile?.empty == false || lessonDate < Date.getToday() + + if (it.type != Lesson.TYPE_NORMAL) { + data.metadataList.add( + Metadata( + data.profileId, + MetadataType.LESSON_CHANGE, + it.id, + seen, + seen + ) + ) + } + data.lessonList += it + } + } + + for (date in noLessonDays) { + data.lessonList += Lesson(data.profileId, date.value.toLong()).also { + it.type = Lesson.TYPE_NO_LESSONS + it.date = date + } + } + } +} + diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/firstlogin/MobidziennikFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/firstlogin/MobidziennikFirstLogin.kt index 11b668f9..f1e67115 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/firstlogin/MobidziennikFirstLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/firstlogin/MobidziennikFirstLogin.kt @@ -1,14 +1,14 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.firstlogin import org.greenrobot.eventbus.EventBus -import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_MOBIDZIENNIK import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.data.MobidziennikWeb import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login.MobidziennikLoginWeb import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent import pl.szczodrzynski.edziennik.data.db.entity.Profile -import pl.szczodrzynski.edziennik.fixName -import pl.szczodrzynski.edziennik.set +import pl.szczodrzynski.edziennik.data.db.enums.LoginType +import pl.szczodrzynski.edziennik.ext.fixName +import pl.szczodrzynski.edziennik.ext.set import pl.szczodrzynski.edziennik.utils.models.Date class MobidziennikFirstLogin(val data: DataMobidziennik, val onSuccess: () -> Unit) { @@ -20,9 +20,7 @@ class MobidziennikFirstLogin(val data: DataMobidziennik, val onSuccess: () -> Un private val profileList = mutableListOf() init { - val loginStoreId = data.loginStore.id - val loginStoreType = LOGIN_TYPE_MOBIDZIENNIK - var firstProfileId = loginStoreId + var firstProfileId = data.loginStore.id MobidziennikLoginWeb(data) { web.webGet(TAG, "/api/zrzutbazy") { text -> @@ -66,8 +64,8 @@ class MobidziennikFirstLogin(val data: DataMobidziennik, val onSuccess: () -> Un val profile = Profile( firstProfileId++, - loginStoreId, - loginStoreType, + data.loginStore.id, + LoginType.MOBIDZIENNIK, studentNameLong, data.loginUsername, studentNameLong, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/login/MobidziennikLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/login/MobidziennikLogin.kt index 7bc79a7b..308087fb 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/login/MobidziennikLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/login/MobidziennikLogin.kt @@ -5,9 +5,8 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.login import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_MOBIDZIENNIK_API2 -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_MOBIDZIENNIK_WEB import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod import pl.szczodrzynski.edziennik.utils.Utils class MobidziennikLogin(val data: DataMobidziennik, val onSuccess: () -> Unit) { @@ -22,7 +21,7 @@ class MobidziennikLogin(val data: DataMobidziennik, val onSuccess: () -> Unit) { } private fun nextLoginMethod(onSuccess: () -> Unit) { - if (data.targetLoginMethodIds.isEmpty()) { + if (data.targetLoginMethods.isEmpty()) { onSuccess() return } @@ -30,30 +29,31 @@ class MobidziennikLogin(val data: DataMobidziennik, val onSuccess: () -> Unit) { onSuccess() return } - useLoginMethod(data.targetLoginMethodIds.removeAt(0)) { usedMethodId -> + useLoginMethod(data.targetLoginMethods.removeAt(0)) { usedMethod -> data.progress(data.progressStep) - if (usedMethodId != -1) - data.loginMethods.add(usedMethodId) + if (usedMethod != null) + data.loginMethods.add(usedMethod) nextLoginMethod(onSuccess) } } - private fun useLoginMethod(loginMethodId: Int, onSuccess: (usedMethodId: Int) -> Unit) { + private fun useLoginMethod(loginMethod: LoginMethod, onSuccess: (usedMethod: LoginMethod?) -> Unit) { // this should never be true - if (data.loginMethods.contains(loginMethodId)) { - onSuccess(-1) + if (data.loginMethods.contains(loginMethod)) { + onSuccess(null) return } - Utils.d(TAG, "Using login method $loginMethodId") - when (loginMethodId) { - LOGIN_METHOD_MOBIDZIENNIK_WEB -> { + Utils.d(TAG, "Using login method $loginMethod") + when (loginMethod) { + LoginMethod.MOBIDZIENNIK_WEB -> { data.startProgress(R.string.edziennik_progress_login_mobidziennik_web) - MobidziennikLoginWeb(data) { onSuccess(loginMethodId) } + MobidziennikLoginWeb(data) { onSuccess(loginMethod) } } - LOGIN_METHOD_MOBIDZIENNIK_API2 -> { + LoginMethod.MOBIDZIENNIK_API2 -> { data.startProgress(R.string.edziennik_progress_login_mobidziennik_api2) - MobidziennikLoginApi2(data) { onSuccess(loginMethodId) } + MobidziennikLoginApi2(data) { onSuccess(loginMethod) } } + else -> {} } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/login/MobidziennikLoginApi2.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/login/MobidziennikLoginApi2.kt index db750981..01d7b9b7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/login/MobidziennikLoginApi2.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/login/MobidziennikLoginApi2.kt @@ -9,10 +9,15 @@ import com.google.gson.JsonObject import im.wangchao.mhttp.Request import im.wangchao.mhttp.Response import im.wangchao.mhttp.callback.JsonCallbackHandler -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik 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 class MobidziennikLoginApi2(val data: DataMobidziennik, val onSuccess: () -> Unit) { @@ -73,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() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/login/MobidziennikLoginWeb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/login/MobidziennikLoginWeb.kt index 78526b0c..ce801a73 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/login/MobidziennikLoginWeb.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/mobidziennik/login/MobidziennikLoginWeb.kt @@ -11,8 +11,8 @@ import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.DataMobidziennik import pl.szczodrzynski.edziennik.data.api.edziennik.mobidziennik.Mobidziennik import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.getUnixDate -import pl.szczodrzynski.edziennik.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.ext.getUnixDate +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty import pl.szczodrzynski.edziennik.utils.Utils.d class MobidziennikLoginWeb(val data: DataMobidziennik, val onSuccess: () -> Unit) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/DataPodlasie.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/DataPodlasie.kt index d117c139..afbe53eb 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/DataPodlasie.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/DataPodlasie.kt @@ -4,11 +4,15 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_PODLASIE_API +import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.data.api.models.Data -import pl.szczodrzynski.edziennik.data.db.entity.* -import kotlin.text.replace +import pl.szczodrzynski.edziennik.data.db.entity.LoginStore +import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.ext.crc32 +import pl.szczodrzynski.edziennik.ext.getStudentData +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.ext.set class DataPodlasie(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) { @@ -17,7 +21,7 @@ class DataPodlasie(app: App, profile: Profile?, loginStore: LoginStore) : Data(a override fun satisfyLoginMethods() { loginMethods.clear() if (isApiLoginValid()) - loginMethods += LOGIN_METHOD_PODLASIE_API + loginMethods += LoginMethod.PODLASIE_API } override fun generateUserCode(): String = "$schoolShortName:$loginShort:${studentId?.crc32()}" @@ -38,7 +42,7 @@ class DataPodlasie(app: App, profile: Profile?, loginStore: LoginStore) : Data(a private var mApiUrl: String? = null var apiUrl: String? get() { mApiUrl = mApiUrl ?: profile?.getStudentData("apiUrl", null); return mApiUrl } - set(value) { profile?.putStudentData("apiUrl", value) ?: return; mApiUrl = value } + set(value) { profile["apiUrl"] = value; mApiUrl = value } /* ____ _ _ / __ \| | | | @@ -49,71 +53,36 @@ class DataPodlasie(app: App, profile: Profile?, loginStore: LoginStore) : Data(a private var mStudentId: String? = null var studentId: String? get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", null); return mStudentId } - set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value } + set(value) { profile["studentId"] = value; mStudentId = value } private var mStudentLogin: String? = null var studentLogin: String? get() { mStudentLogin = mStudentLogin ?: profile?.getStudentData("studentLogin", null); return mStudentLogin } - set(value) { profile?.putStudentData("studentLogin", value) ?: return; mStudentLogin = value } + set(value) { profile["studentLogin"] = value; mStudentLogin = value } private var mSchoolName: String? = null var schoolName: String? get() { mSchoolName = mSchoolName ?: profile?.getStudentData("schoolName", null); return mSchoolName } - set(value) { profile?.putStudentData("schoolName", value) ?: return; mSchoolName = value } + set(value) { profile["schoolName"] = value; mSchoolName = value } private var mClassName: String? = null var className: String? get() { mClassName = mClassName ?: profile?.getStudentData("className", null); return mClassName } - set(value) { profile?.putStudentData("className", value) ?: return; mClassName = value } + set(value) { profile["className"] = value; mClassName = value } private var mSchoolYear: String? = null var schoolYear: String? get() { mSchoolYear = mSchoolYear ?: profile?.getStudentData("schoolYear", null); return mSchoolYear } - set(value) { profile?.putStudentData("schoolYear", value) ?: return; mSchoolYear = value } + set(value) { profile["schoolYear"] = value; mSchoolYear = value } private var mCurrentSemester: Int? = null var currentSemester: Int get() { mCurrentSemester = mCurrentSemester ?: profile?.getStudentData("currentSemester", 0); return mCurrentSemester ?: 0 } - set(value) { profile?.putStudentData("currentSemester", value) ?: return; mCurrentSemester = value } + set(value) { profile["currentSemester"] = value; mCurrentSemester = value } val schoolShortName: String? get() = studentLogin?.split('@')?.get(1)?.replace(".podlaskie.pl", "") val loginShort: String? get() = studentLogin?.split('@')?.get(0) - - fun getSubject(name: String): Subject { - val id = name.crc32() - return subjectList.singleOrNull { it.id == id } ?: run { - val subject = Subject(profileId, id, name, name) - subjectList.put(id, subject) - subject - } - } - - fun getTeacher(firstName: String, lastName: String): Teacher { - val name = "$firstName $lastName".fixName() - return teacherList.singleOrNull { it.fullName == name } ?: run { - val id = name.crc32() - val teacher = Teacher(profileId, id, firstName, lastName) - teacherList.put(id, teacher) - teacher - } - } - - fun getTeam(name: String? = null): Team { - if (name == "cała klasa" || name == null) return teamClass ?: run { - val id = className!!.crc32() - val teamCode = "$schoolShortName:$className" - val team = Team(profileId, id, className, Team.TYPE_CLASS, teamCode, -1) - teamList.put(id, team) - return team - } else { - val id = name.crc32() - val teamCode = "$schoolShortName:$name" - val team = Team(profileId, id, name, Team.TYPE_VIRTUAL, teamCode, -1) - teamList.put(id, team) - return team - } - } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/Podlasie.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/Podlasie.kt index d7ec441f..660ba2e9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/Podlasie.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/Podlasie.kt @@ -12,14 +12,15 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.PodlasieData import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.firstlogin.PodlasieFirstLogin import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login.PodlasieLogin import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent +import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.data.api.podlasieLoginMethods import pl.szczodrzynski.edziennik.data.api.prepare import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.MessageFull @@ -54,11 +55,11 @@ class Podlasie(val app: App, val profile: Profile?, val loginStore: LoginStore, |_| |_| |_|\___| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_| __/ | |__*/ - override fun sync(featureIds: List, viewId: Int?, onlyEndpoints: List?, arguments: JsonObject?) { + override fun sync(featureTypes: Set?, onlyEndpoints: Set?, arguments: JsonObject?) { data.arguments = arguments - data.prepare(podlasieLoginMethods, PodlasieFeatures, featureIds, viewId, onlyEndpoints) - Utils.d(TAG, "LoginMethod IDs: ${data.targetLoginMethodIds}") - Utils.d(TAG, "Endpoint IDs: ${data.targetEndpointIds}") + data.prepare(PodlasieFeatures, featureTypes, onlyEndpoints) + Utils.d(TAG, "LoginMethod IDs: ${data.targetLoginMethods}") + Utils.d(TAG, "Endpoint IDs: ${data.targetEndpoints}") PodlasieLogin(data) { PodlasieData(data) { completed() @@ -70,7 +71,7 @@ class Podlasie(val app: App, val profile: Profile?, val loginStore: LoginStore, } - override fun sendMessage(recipients: List, subject: String, text: String) { + override fun sendMessage(recipients: Set, subject: String, text: String) { } @@ -142,6 +143,10 @@ class Podlasie(val app: App, val profile: Profile?, val loginStore: LoginStore, callback.onCompleted() } + override fun onRequiresUserAction(event: UserActionRequiredEvent) { + callback.onRequiresUserAction(event) + } + override fun onProgress(step: Float) { callback.onProgress(step) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/PodlasieFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/PodlasieFeatures.kt index 82c6f659..6d131990 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/PodlasieFeatures.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/PodlasieFeatures.kt @@ -4,15 +4,15 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie -import pl.szczodrzynski.edziennik.data.api.FEATURE_ALWAYS_NEEDED -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_PODLASIE_API -import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_PODLASIE import pl.szczodrzynski.edziennik.data.api.models.Feature +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.data.db.enums.LoginType const val ENDPOINT_PODLASIE_API_MAIN = 1001 val PodlasieFeatures = listOf( - Feature(LOGIN_TYPE_PODLASIE, FEATURE_ALWAYS_NEEDED, listOf( - ENDPOINT_PODLASIE_API_MAIN to LOGIN_METHOD_PODLASIE_API - ), listOf(LOGIN_METHOD_PODLASIE_API)) + Feature(LoginType.PODLASIE, FeatureType.ALWAYS_NEEDED, listOf( + ENDPOINT_PODLASIE_API_MAIN to LoginMethod.PODLASIE_API + )) ) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/PodlasieApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/PodlasieApi.kt index 985b9899..726b851a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/PodlasieApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/PodlasieApi.kt @@ -12,9 +12,9 @@ import im.wangchao.mhttp.callback.JsonCallbackHandler import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.getInt -import pl.szczodrzynski.edziennik.getJsonObject -import pl.szczodrzynski.edziennik.toHexString +import pl.szczodrzynski.edziennik.ext.getInt +import pl.szczodrzynski.edziennik.ext.getJsonObject +import pl.szczodrzynski.edziennik.ext.toHexString import pl.szczodrzynski.edziennik.utils.Utils import java.security.MessageDigest import java.text.SimpleDateFormat diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/PodlasieData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/PodlasieData.kt index ce1197b8..f1117537 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/PodlasieData.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/PodlasieData.kt @@ -20,7 +20,7 @@ class PodlasieData(val data: DataPodlasie, val onSuccess: () -> Unit) { } private fun nextEndpoint(onSuccess: () -> Unit) { - if (data.targetEndpointIds.isEmpty()) { + if (data.targetEndpoints.isEmpty()) { onSuccess() return } @@ -28,8 +28,8 @@ class PodlasieData(val data: DataPodlasie, val onSuccess: () -> Unit) { onSuccess() return } - val id = data.targetEndpointIds.firstKey() - val lastSync = data.targetEndpointIds.remove(id) + val id = data.targetEndpoints.firstKey() + val lastSync = data.targetEndpoints.remove(id) useEndpoint(id, lastSync) { data.progress(data.progressStep) nextEndpoint(onSuccess) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiEvents.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiEvents.kt index 8a169ee7..25735d3d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiEvents.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiEvents.kt @@ -9,8 +9,9 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Metadata -import pl.szczodrzynski.edziennik.getLong -import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.getLong +import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time import java.util.* @@ -26,7 +27,7 @@ class PodlasieApiEvents(val data: DataPodlasie, val rows: List) { val name = event.getString("Name")?.replace(""", "\"") ?: "" val description = event.getString("Description")?.replace(""", "\"") ?: "" - val type = when (event.getString("Category")?.toLowerCase(Locale.getDefault())) { + val type = when (event.getString("Category")?.lowercase()) { "klasówka" -> Event.TYPE_EXAM "praca domowa" -> Event.TYPE_HOMEWORK "wycieczka" -> Event.TYPE_EXCURSION @@ -57,13 +58,14 @@ class PodlasieApiEvents(val data: DataPodlasie, val rows: List) { addedDate = addedDate ).apply { homeworkBody = description + isDownloaded = true } data.eventList.add(eventObject) data.metadataList.add( Metadata( data.profileId, - if (type == Event.TYPE_HOMEWORK) Metadata.TYPE_HOMEWORK else Metadata.TYPE_EVENT, + if (type == Event.TYPE_HOMEWORK) MetadataType.HOMEWORK else MetadataType.EVENT, id, data.profile?.empty ?: false, data.profile?.empty ?: false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiFinalGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiFinalGrades.kt index 7c4575aa..04023f5f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiFinalGrades.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiFinalGrades.kt @@ -15,8 +15,10 @@ import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_SEMESTER2_ import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_YEAR_FINAL import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_YEAR_PROPOSED import pl.szczodrzynski.edziennik.data.db.entity.Metadata -import pl.szczodrzynski.edziennik.getLong -import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.getLong +import pl.szczodrzynski.edziennik.ext.getSemesterStart +import pl.szczodrzynski.edziennik.ext.getString class PodlasieApiFinalGrades(val data: DataPodlasie, val rows: List) { init { data.profile?.also { profile -> @@ -36,7 +38,7 @@ class PodlasieApiFinalGrades(val data: DataPodlasie, val rows: List) } val subjectName = grade.getString("SchoolSubject") ?: return@forEach - val subject = data.getSubject(subjectName) + val subject = data.getSubject(null, subjectName) val addedDate = if (profile.empty) profile.getSemesterStart(semester).inMillis else System.currentTimeMillis() @@ -62,7 +64,7 @@ class PodlasieApiFinalGrades(val data: DataPodlasie, val rows: List) data.metadataList.add( Metadata( data.profileId, - Metadata.TYPE_GRADE, + MetadataType.GRADE, id, profile.empty, profile.empty @@ -99,7 +101,7 @@ class PodlasieApiFinalGrades(val data: DataPodlasie, val rows: List) data.metadataList.add( Metadata( data.profileId, - Metadata.TYPE_GRADE, + MetadataType.GRADE, proposedGradeObject.id, profile.empty, profile.empty diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiGrades.kt index d5696567..9a049313 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiGrades.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiGrades.kt @@ -10,10 +10,11 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel import pl.szczodrzynski.edziennik.data.db.entity.Grade import pl.szczodrzynski.edziennik.data.db.entity.Metadata -import pl.szczodrzynski.edziennik.getFloat -import pl.szczodrzynski.edziennik.getInt -import pl.szczodrzynski.edziennik.getLong -import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.getFloat +import pl.szczodrzynski.edziennik.ext.getInt +import pl.szczodrzynski.edziennik.ext.getLong +import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.utils.models.Date class PodlasieApiGrades(val data: DataPodlasie, val rows: List) { @@ -34,7 +35,7 @@ class PodlasieApiGrades(val data: DataPodlasie, val rows: List) { val teacher = data.getTeacher(teacherFirstName, teacherLastName) val subjectName = grade.getString("SchoolSubject") ?: return@forEach - val subject = data.getSubject(subjectName) + val subject = data.getSubject(null, subjectName) val addedDate = grade.getString("ReceivedDate")?.let { Date.fromY_m_d(it).inMillis } ?: System.currentTimeMillis() @@ -60,7 +61,7 @@ class PodlasieApiGrades(val data: DataPodlasie, val rows: List) { data.metadataList.add( Metadata( data.profileId, - Metadata.TYPE_GRADE, + MetadataType.GRADE, id, data.profile?.empty ?: false, data.profile?.empty ?: false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiHomework.kt index 6ab53328..8a8e851b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiHomework.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiHomework.kt @@ -5,13 +5,14 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.api import com.google.gson.JsonObject -import pl.szczodrzynski.edziennik.crc32 import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Metadata -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.crc32 +import pl.szczodrzynski.edziennik.ext.get +import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.utils.models.Date class PodlasieApiHomework(val data: DataPodlasie, val rows: List) { @@ -37,6 +38,7 @@ class PodlasieApiHomework(val data: DataPodlasie, val rows: List) { addedDate = addedDate ).apply { homeworkBody = description + isDownloaded = true } eventObject.attachmentIds = mutableListOf() @@ -52,7 +54,7 @@ class PodlasieApiHomework(val data: DataPodlasie, val rows: List) { data.metadataList.add( Metadata( data.profileId, - Metadata.TYPE_HOMEWORK, + MetadataType.HOMEWORK, id, data.profile?.empty ?: false, data.profile?.empty ?: false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiLuckyNumber.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiLuckyNumber.kt index 725ec727..87b8dcc8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiLuckyNumber.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiLuckyNumber.kt @@ -7,6 +7,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.api import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie import pl.szczodrzynski.edziennik.data.db.entity.LuckyNumber import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.utils.models.Date class PodlasieApiLuckyNumber(val data: DataPodlasie, val luckyNumber: Int) { @@ -21,7 +22,7 @@ class PodlasieApiLuckyNumber(val data: DataPodlasie, val luckyNumber: Int) { data.metadataList.add( Metadata( data.profileId, - Metadata.TYPE_LUCKY_NUMBER, + MetadataType.LUCKY_NUMBER, luckyNumberObject.date.value.toLong(), true, data.profile?.empty ?: false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiMain.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiMain.kt index 6fe77b5c..bf9379b3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiMain.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiMain.kt @@ -4,14 +4,14 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.api -import pl.szczodrzynski.edziennik.asJsonObjectList import pl.szczodrzynski.edziennik.data.api.PODLASIE_API_USER_ENDPOINT import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.ENDPOINT_PODLASIE_API_MAIN import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.PodlasieApi import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS -import pl.szczodrzynski.edziennik.getInt -import pl.szczodrzynski.edziennik.getJsonArray +import pl.szczodrzynski.edziennik.ext.asJsonObjectList +import pl.szczodrzynski.edziennik.ext.getInt +import pl.szczodrzynski.edziennik.ext.getJsonArray class PodlasieApiMain(override val data: DataPodlasie, override val lastSync: Long?, @@ -22,7 +22,13 @@ class PodlasieApiMain(override val data: DataPodlasie, init { apiGet(TAG, PODLASIE_API_USER_ENDPOINT) { json -> - data.getTeam() // Save the class team when it doesn't exist. + // Save the class team when it doesn't exist. + data.getTeam( + id = null, + name = data.className ?: "", + schoolCode = data.schoolShortName ?: "", + isTeamClass = true + ) json.getInt("LuckyNumber")?.let { PodlasieApiLuckyNumber(data, it) } json.getJsonArray("Teacher")?.asJsonObjectList()?.let { PodlasieApiTeachers(data, it) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiTeachers.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiTeachers.kt index ca1fa7b4..f4769169 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiTeachers.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiTeachers.kt @@ -7,9 +7,9 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.api import com.google.gson.JsonObject import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie import pl.szczodrzynski.edziennik.data.db.entity.Teacher -import pl.szczodrzynski.edziennik.getInt -import pl.szczodrzynski.edziennik.getLong -import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.ext.getInt +import pl.szczodrzynski.edziennik.ext.getLong +import pl.szczodrzynski.edziennik.ext.getString class PodlasieApiTeachers(val data: DataPodlasie, val rows: List) { init { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiTimetable.kt index a9a12d5d..9abb47fe 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiTimetable.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/data/api/PodlasieApiTimetable.kt @@ -8,8 +8,8 @@ import com.google.gson.JsonObject import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel import pl.szczodrzynski.edziennik.data.db.entity.Lesson -import pl.szczodrzynski.edziennik.getInt -import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.ext.getInt +import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Week @@ -43,14 +43,21 @@ class PodlasieApiTimetable(val data: DataPodlasie, rows: List) { val startTime = lesson.getString("TimeFrom")?.let { Time.fromH_m_s(it) } ?: return@forEach val endTime = lesson.getString("TimeTo")?.let { Time.fromH_m_s(it) } ?: return@forEach - val subject = lesson.getString("SchoolSubject")?.let { data.getSubject(it) } + val subject = lesson.getString("SchoolSubject")?.let { data.getSubject(null, it) } ?: return@forEach val teacherFirstName = lesson.getString("TeacherFirstName") ?: return@forEach val teacherLastName = lesson.getString("TeacherLastName") ?: return@forEach val teacher = data.getTeacher(teacherFirstName, teacherLastName) - val team = lesson.getString("Group")?.let { data.getTeam(it) } ?: return@forEach + val team = lesson.getString("Group")?.let { + data.getTeam( + id = null, + name = it, + schoolCode = data.schoolShortName ?: "", + isTeamClass = it == "cała klasa" + ) + } ?: return@forEach val classroom = lesson.getString("Room") Lesson(data.profileId, -1).also { @@ -65,6 +72,7 @@ class PodlasieApiTimetable(val data: DataPodlasie, rows: List) { it.classroom = classroom it.id = it.buildId() + it.ownerId = it.buildOwnerId() data.lessonList += it } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/firstlogin/PodlasieFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/firstlogin/PodlasieFirstLogin.kt index a1e9d0eb..3b978cae 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/firstlogin/PodlasieFirstLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/firstlogin/PodlasieFirstLogin.kt @@ -5,8 +5,6 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.firstlogin import org.greenrobot.eventbus.EventBus -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_PODLASIE import pl.szczodrzynski.edziennik.data.api.PODLASIE_API_LOGOUT_DEVICES_ENDPOINT import pl.szczodrzynski.edziennik.data.api.PODLASIE_API_USER_ENDPOINT import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie @@ -14,6 +12,11 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.data.PodlasieApi import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login.PodlasieLoginApi import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.enums.LoginType +import pl.szczodrzynski.edziennik.ext.fixName +import pl.szczodrzynski.edziennik.ext.getShortName +import pl.szczodrzynski.edziennik.ext.getString +import pl.szczodrzynski.edziennik.ext.set class PodlasieFirstLogin(val data: DataPodlasie, val onSuccess: () -> Unit) { companion object { @@ -29,9 +32,6 @@ class PodlasieFirstLogin(val data: DataPodlasie, val onSuccess: () -> Unit) { } private fun doLogin() { - val loginStoreId = data.loginStore.id - val loginStoreType = LOGIN_TYPE_PODLASIE - if (data.loginStore.getLoginData("logoutDevices", false)) { data.loginStore.removeLoginData("logoutDevices") api.apiGet(TAG, PODLASIE_API_LOGOUT_DEVICES_ENDPOINT) { @@ -54,9 +54,9 @@ class PodlasieFirstLogin(val data: DataPodlasie, val onSuccess: () -> Unit) { val apiUrl = json.getString("URL") val profile = Profile( - loginStoreId, - loginStoreId, - loginStoreType, + data.loginStore.id, + data.loginStore.id, + LoginType.PODLASIE, studentNameLong, login, studentNameLong, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/login/PodlasieLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/login/PodlasieLogin.kt index 406bb997..73594d41 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/login/PodlasieLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/podlasie/login/PodlasieLogin.kt @@ -5,8 +5,8 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.login import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_PODLASIE_API import pl.szczodrzynski.edziennik.data.api.edziennik.podlasie.DataPodlasie +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod import pl.szczodrzynski.edziennik.utils.Utils class PodlasieLogin(val data: DataPodlasie, val onSuccess: () -> Unit) { @@ -21,7 +21,7 @@ class PodlasieLogin(val data: DataPodlasie, val onSuccess: () -> Unit) { } private fun nextLoginMethod(onSuccess: () -> Unit) { - if (data.targetLoginMethodIds.isEmpty()) { + if (data.targetLoginMethods.isEmpty()) { onSuccess() return } @@ -29,26 +29,27 @@ class PodlasieLogin(val data: DataPodlasie, val onSuccess: () -> Unit) { onSuccess() return } - useLoginMethod(data.targetLoginMethodIds.removeAt(0)) { usedMethodId -> + useLoginMethod(data.targetLoginMethods.removeAt(0)) { usedMethod -> data.progress(data.progressStep) - if (usedMethodId != -1) - data.loginMethods.add(usedMethodId) + if (usedMethod != null) + data.loginMethods.add(usedMethod) nextLoginMethod(onSuccess) } } - private fun useLoginMethod(loginMethodId: Int, onSuccess: (usedMethodId: Int) -> Unit) { + private fun useLoginMethod(loginMethod: LoginMethod, onSuccess: (usedMethod: LoginMethod?) -> Unit) { // this should never be true - if (data.loginMethods.contains(loginMethodId)) { - onSuccess(-1) + if (data.loginMethods.contains(loginMethod)) { + onSuccess(null) return } - Utils.d(TAG, "Using login method $loginMethodId") - when (loginMethodId) { - LOGIN_METHOD_PODLASIE_API -> { + Utils.d(TAG, "Using login method $loginMethod") + when (loginMethod) { + LoginMethod.PODLASIE_API -> { data.startProgress(R.string.edziennik_progress_login_podlasie_api) - PodlasieLoginApi(data) { onSuccess(loginMethodId) } + PodlasieLoginApi(data) { onSuccess(loginMethod) } } + else -> {} } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/DataTemplate.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/DataTemplate.kt index dcf569cf..f0a1785e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/DataTemplate.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/DataTemplate.kt @@ -5,13 +5,14 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.template import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.currentTimeUnix -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_TEMPLATE_API -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_TEMPLATE_WEB import pl.szczodrzynski.edziennik.data.api.models.Data import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.Profile -import pl.szczodrzynski.edziennik.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.ext.currentTimeUnix +import pl.szczodrzynski.edziennik.ext.getStudentData +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.ext.set /** * Use http://patorjk.com/software/taag/#p=display&f=Big for the ascii art @@ -26,11 +27,11 @@ class DataTemplate(app: App, profile: Profile?, loginStore: LoginStore) : Data(a override fun satisfyLoginMethods() { loginMethods.clear() if (isWebLoginValid()) { - loginMethods += LOGIN_METHOD_TEMPLATE_WEB + loginMethods += LoginMethod.TEMPLATE_WEB app.cookieJar.set("eregister.example.com", "AuthCookie", webCookie) } if (isApiLoginValid()) - loginMethods += LOGIN_METHOD_TEMPLATE_API + loginMethods += LoginMethod.TEMPLATE_API } override fun generateUserCode() = "TEMPLATE:DO_NOT_USE" @@ -44,12 +45,12 @@ class DataTemplate(app: App, profile: Profile?, loginStore: LoginStore) : Data(a private var mWebCookie: String? = null var webCookie: String? get() { mWebCookie = mWebCookie ?: profile?.getStudentData("webCookie", null); return mWebCookie } - set(value) { profile?.putStudentData("webCookie", value) ?: return; mWebCookie = value } + set(value) { profile["webCookie"] = value; mWebCookie = value } private var mWebExpiryTime: Long? = null var webExpiryTime: Long get() { mWebExpiryTime = mWebExpiryTime ?: profile?.getStudentData("webExpiryTime", 0L); return mWebExpiryTime ?: 0L } - set(value) { profile?.putStudentData("webExpiryTime", value) ?: return; mWebExpiryTime = value } + set(value) { profile["webExpiryTime"] = value; mWebExpiryTime = value } /* _ /\ (_) @@ -62,10 +63,10 @@ class DataTemplate(app: App, profile: Profile?, loginStore: LoginStore) : Data(a private var mApiToken: String? = null var apiToken: String? get() { mApiToken = mApiToken ?: profile?.getStudentData("apiToken", null); return mApiToken } - set(value) { profile?.putStudentData("apiToken", value) ?: return; mApiToken = value } + set(value) { profile["apiToken"] = value; mApiToken = value } private var mApiExpiryTime: Long? = null var apiExpiryTime: Long get() { mApiExpiryTime = mApiExpiryTime ?: profile?.getStudentData("apiExpiryTime", 0L); return mApiExpiryTime ?: 0L } - set(value) { profile?.putStudentData("apiExpiryTime", value) ?: return; mApiExpiryTime = value } + set(value) { profile["apiExpiryTime"] = value; mApiExpiryTime = value } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/Template.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/Template.kt index 843d36e9..7b0ddfd6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/Template.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/Template.kt @@ -10,14 +10,15 @@ import pl.szczodrzynski.edziennik.data.api.CODE_INTERNAL_LIBRUS_ACCOUNT_410 import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.TemplateData import pl.szczodrzynski.edziennik.data.api.edziennik.template.firstlogin.TemplateFirstLogin import pl.szczodrzynski.edziennik.data.api.edziennik.template.login.TemplateLogin +import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.api.prepare -import pl.szczodrzynski.edziennik.data.api.templateLoginMethods import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.MessageFull @@ -51,11 +52,11 @@ class Template(val app: App, val profile: Profile?, val loginStore: LoginStore, |_| |_| |_|\___| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_| __/ | |__*/ - override fun sync(featureIds: List, viewId: Int?, onlyEndpoints: List?, arguments: JsonObject?) { + override fun sync(featureTypes: Set?, onlyEndpoints: Set?, arguments: JsonObject?) { data.arguments = arguments - data.prepare(templateLoginMethods, TemplateFeatures, featureIds, viewId, onlyEndpoints) - d(TAG, "LoginMethod IDs: ${data.targetLoginMethodIds}") - d(TAG, "Endpoint IDs: ${data.targetEndpointIds}") + data.prepare(TemplateFeatures, featureTypes, onlyEndpoints) + d(TAG, "LoginMethod IDs: ${data.targetLoginMethods}") + d(TAG, "Endpoint IDs: ${data.targetEndpoints}") TemplateLogin(data) { TemplateData(data) { completed() @@ -67,7 +68,7 @@ class Template(val app: App, val profile: Profile?, val loginStore: LoginStore, } - override fun sendMessage(recipients: List, subject: String, text: String) { + override fun sendMessage(recipients: Set, subject: String, text: String) { } @@ -108,6 +109,10 @@ class Template(val app: App, val profile: Profile?, val loginStore: LoginStore, callback.onCompleted() } + override fun onRequiresUserAction(event: UserActionRequiredEvent) { + callback.onRequiresUserAction(event) + } + override fun onProgress(step: Float) { callback.onProgress(step) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/TemplateFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/TemplateFeatures.kt index c12e2bcc..b389ea59 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/TemplateFeatures.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/TemplateFeatures.kt @@ -4,21 +4,23 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.template -import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.models.Feature +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.data.db.enums.LoginType const val ENDPOINT_TEMPLATE_WEB_SAMPLE = 9991 const val ENDPOINT_TEMPLATE_WEB_SAMPLE_2 = 9992 const val ENDPOINT_TEMPLATE_API_SAMPLE = 9993 val TemplateFeatures = listOf( - Feature(LOGIN_TYPE_TEMPLATE, FEATURE_STUDENT_INFO, listOf( - ENDPOINT_TEMPLATE_WEB_SAMPLE to LOGIN_METHOD_TEMPLATE_WEB - ), listOf(LOGIN_METHOD_TEMPLATE_WEB)), - Feature(LOGIN_TYPE_TEMPLATE, FEATURE_SCHOOL_INFO, listOf( - ENDPOINT_TEMPLATE_WEB_SAMPLE_2 to LOGIN_METHOD_TEMPLATE_WEB - ), listOf(LOGIN_METHOD_TEMPLATE_WEB)), - Feature(LOGIN_TYPE_TEMPLATE, FEATURE_GRADES, listOf( - ENDPOINT_TEMPLATE_API_SAMPLE to LOGIN_METHOD_TEMPLATE_API - ), listOf(LOGIN_METHOD_TEMPLATE_API)) + Feature(LoginType.TEMPLATE, FeatureType.STUDENT_INFO, listOf( + ENDPOINT_TEMPLATE_WEB_SAMPLE to LoginMethod.TEMPLATE_WEB + )), + Feature(LoginType.TEMPLATE, FeatureType.SCHOOL_INFO, listOf( + ENDPOINT_TEMPLATE_WEB_SAMPLE_2 to LoginMethod.TEMPLATE_WEB + )), + Feature(LoginType.TEMPLATE, FeatureType.GRADES, listOf( + ENDPOINT_TEMPLATE_API_SAMPLE to LoginMethod.TEMPLATE_API + )) ) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateApi.kt index d3d4c607..69e8f654 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateApi.kt @@ -5,11 +5,11 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.template.data import com.google.gson.JsonObject -import pl.szczodrzynski.edziennik.currentTimeUnix import pl.szczodrzynski.edziennik.data.api.ERROR_TEMPLATE_WEB_OTHER import pl.szczodrzynski.edziennik.data.api.GET import pl.szczodrzynski.edziennik.data.api.edziennik.template.DataTemplate import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.ext.currentTimeUnix open class TemplateApi(open val data: DataTemplate, open val lastSync: Long?) { companion object { @@ -28,6 +28,7 @@ open class TemplateApi(open val data: DataTemplate, open val lastSync: Long?) { * You can customize this method's parameters to best fit the implemented e-register. * Just make sure that [tag] and [onSuccess] is present. */ + @Suppress("UNUSED_PARAMETER") fun apiGet(tag: String, endpoint: String, method: Int = GET, payload: JsonObject? = null, onSuccess: (json: JsonObject?) -> Unit) { val json = JsonObject() json.addProperty("foo", "bar") diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateData.kt index 3489555f..4b759e34 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateData.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateData.kt @@ -24,7 +24,7 @@ class TemplateData(val data: DataTemplate, val onSuccess: () -> Unit) { } private fun nextEndpoint(onSuccess: () -> Unit) { - if (data.targetEndpointIds.isEmpty()) { + if (data.targetEndpoints.isEmpty()) { onSuccess() return } @@ -32,8 +32,8 @@ class TemplateData(val data: DataTemplate, val onSuccess: () -> Unit) { onSuccess() return } - val id = data.targetEndpointIds.firstKey() - val lastSync = data.targetEndpointIds.remove(id) + val id = data.targetEndpoints.firstKey() + val lastSync = data.targetEndpoints.remove(id) useEndpoint(id, lastSync) { endpointId -> data.progress(data.progressStep) nextEndpoint(onSuccess) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateWeb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateWeb.kt index 0db508c7..2db8c41b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateWeb.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/TemplateWeb.kt @@ -5,11 +5,11 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.template.data import com.google.gson.JsonObject -import pl.szczodrzynski.edziennik.currentTimeUnix import pl.szczodrzynski.edziennik.data.api.ERROR_TEMPLATE_WEB_OTHER import pl.szczodrzynski.edziennik.data.api.GET import pl.szczodrzynski.edziennik.data.api.edziennik.template.DataTemplate import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.ext.currentTimeUnix open class TemplateWeb(open val data: DataTemplate, open val lastSync: Long?) { companion object { @@ -28,6 +28,7 @@ open class TemplateWeb(open val data: DataTemplate, open val lastSync: Long?) { * You can customize this method's parameters to best fit the implemented e-register. * Just make sure that [tag] and [onSuccess] is present. */ + @Suppress("UNUSED_PARAMETER") fun webGet(tag: String, endpoint: String, method: Int = GET, payload: JsonObject? = null, onSuccess: (json: JsonObject?) -> Unit) { val json = JsonObject() json.addProperty("foo", "bar") diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/api/TemplateApiSample.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/api/TemplateApiSample.kt index 632a8e74..62df262e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/api/TemplateApiSample.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/api/TemplateApiSample.kt @@ -4,12 +4,12 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.template.data.api -import pl.szczodrzynski.edziennik.DAY -import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.data.api.edziennik.template.DataTemplate import pl.szczodrzynski.edziennik.data.api.edziennik.template.ENDPOINT_TEMPLATE_API_SAMPLE import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.TemplateApi import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.ext.DAY class TemplateApiSample(override val data: DataTemplate, override val lastSync: Long?, @@ -20,7 +20,7 @@ class TemplateApiSample(override val data: DataTemplate, } init { - apiGet(TAG, "/api/v3/getData.php") { json -> + apiGet(TAG, "/api/v3/getData.php") { _ -> // here you can access and update any fields of the `data` object // ================ @@ -29,9 +29,7 @@ class TemplateApiSample(override val data: DataTemplate, // not sooner than two days later data.setSyncNext(ENDPOINT_TEMPLATE_API_SAMPLE, 2 * DAY) // in two days OR on explicit "grades" sync - data.setSyncNext(ENDPOINT_TEMPLATE_API_SAMPLE, 2 * DAY, MainActivity.DRAWER_ITEM_GRADES) - // only if sync is executed on Home view - data.setSyncNext(ENDPOINT_TEMPLATE_API_SAMPLE, syncIn = null, viewId = MainActivity.DRAWER_ITEM_HOME) + data.setSyncNext(ENDPOINT_TEMPLATE_API_SAMPLE, 2 * DAY, FeatureType.GRADES) // always, in every sync data.setSyncNext(ENDPOINT_TEMPLATE_API_SAMPLE, SYNC_ALWAYS) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/web/TemplateWebSample.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/web/TemplateWebSample.kt index f0ba882e..5d6d457b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/web/TemplateWebSample.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/web/TemplateWebSample.kt @@ -4,13 +4,12 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web -import pl.szczodrzynski.edziennik.DAY -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_GRADES -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_HOME import pl.szczodrzynski.edziennik.data.api.edziennik.template.DataTemplate import pl.szczodrzynski.edziennik.data.api.edziennik.template.ENDPOINT_TEMPLATE_WEB_SAMPLE import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.TemplateWeb import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.ext.DAY class TemplateWebSample(override val data: DataTemplate, override val lastSync: Long?, @@ -21,7 +20,7 @@ class TemplateWebSample(override val data: DataTemplate, } init { - webGet(TAG, "/api/v3/getData.php") { json -> + webGet(TAG, "/api/v3/getData.php") { // here you can access and update any fields of the `data` object // ================ @@ -30,9 +29,7 @@ class TemplateWebSample(override val data: DataTemplate, // not sooner than two days later data.setSyncNext(ENDPOINT_TEMPLATE_WEB_SAMPLE, 2 * DAY) // in two days OR on explicit "grades" sync - data.setSyncNext(ENDPOINT_TEMPLATE_WEB_SAMPLE, 2 * DAY, DRAWER_ITEM_GRADES) - // only if sync is executed on Home view - data.setSyncNext(ENDPOINT_TEMPLATE_WEB_SAMPLE, syncIn = null, viewId = DRAWER_ITEM_HOME) + data.setSyncNext(ENDPOINT_TEMPLATE_WEB_SAMPLE, 2 * DAY, FeatureType.GRADES) // always, in every sync data.setSyncNext(ENDPOINT_TEMPLATE_WEB_SAMPLE, SYNC_ALWAYS) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/web/TemplateWebSample2.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/web/TemplateWebSample2.kt index 02d58231..0125635d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/web/TemplateWebSample2.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/data/web/TemplateWebSample2.kt @@ -4,12 +4,12 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web -import pl.szczodrzynski.edziennik.DAY -import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.data.api.edziennik.template.DataTemplate import pl.szczodrzynski.edziennik.data.api.edziennik.template.ENDPOINT_TEMPLATE_WEB_SAMPLE_2 import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.TemplateWeb import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.ext.DAY class TemplateWebSample2(override val data: DataTemplate, override val lastSync: Long?, @@ -20,7 +20,7 @@ class TemplateWebSample2(override val data: DataTemplate, } init { - webGet(TAG, "/api/v3/getData.php") { json -> + webGet(TAG, "/api/v3/getData.php") { // here you can access and update any fields of the `data` object // ================ @@ -29,9 +29,7 @@ class TemplateWebSample2(override val data: DataTemplate, // not sooner than two days later data.setSyncNext(ENDPOINT_TEMPLATE_WEB_SAMPLE_2, 2 * DAY) // in two days OR on explicit "grades" sync - data.setSyncNext(ENDPOINT_TEMPLATE_WEB_SAMPLE_2, 2 * DAY, MainActivity.DRAWER_ITEM_GRADES) - // only if sync is executed on Home view - data.setSyncNext(ENDPOINT_TEMPLATE_WEB_SAMPLE_2, syncIn = null, viewId = MainActivity.DRAWER_ITEM_HOME) + data.setSyncNext(ENDPOINT_TEMPLATE_WEB_SAMPLE_2, 2 * DAY, FeatureType.GRADES) // always, in every sync data.setSyncNext(ENDPOINT_TEMPLATE_WEB_SAMPLE_2, SYNC_ALWAYS) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLogin.kt index b11eef13..82ee669a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLogin.kt @@ -5,9 +5,8 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.template.login import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_TEMPLATE_API -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_TEMPLATE_WEB import pl.szczodrzynski.edziennik.data.api.edziennik.template.DataTemplate +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod import pl.szczodrzynski.edziennik.utils.Utils class TemplateLogin(val data: DataTemplate, val onSuccess: () -> Unit) { @@ -22,7 +21,7 @@ class TemplateLogin(val data: DataTemplate, val onSuccess: () -> Unit) { } private fun nextLoginMethod(onSuccess: () -> Unit) { - if (data.targetLoginMethodIds.isEmpty()) { + if (data.targetLoginMethods.isEmpty()) { onSuccess() return } @@ -30,30 +29,31 @@ class TemplateLogin(val data: DataTemplate, val onSuccess: () -> Unit) { onSuccess() return } - useLoginMethod(data.targetLoginMethodIds.removeAt(0)) { usedMethodId -> + useLoginMethod(data.targetLoginMethods.removeAt(0)) { usedMethod -> data.progress(data.progressStep) - if (usedMethodId != -1) - data.loginMethods.add(usedMethodId) + if (usedMethod != null) + data.loginMethods.add(usedMethod) nextLoginMethod(onSuccess) } } - private fun useLoginMethod(loginMethodId: Int, onSuccess: (usedMethodId: Int) -> Unit) { + private fun useLoginMethod(loginMethod: LoginMethod, onSuccess: (usedMethod: LoginMethod?) -> Unit) { // this should never be true - if (data.loginMethods.contains(loginMethodId)) { - onSuccess(-1) + if (data.loginMethods.contains(loginMethod)) { + onSuccess(null) return } - Utils.d(TAG, "Using login method $loginMethodId") - when (loginMethodId) { - LOGIN_METHOD_TEMPLATE_WEB -> { + Utils.d(TAG, "Using login method $loginMethod") + when (loginMethod) { + LoginMethod.TEMPLATE_WEB -> { data.startProgress(R.string.edziennik_progress_login_template_web) - TemplateLoginWeb(data) { onSuccess(loginMethodId) } + TemplateLoginWeb(data) { onSuccess(loginMethod) } } - LOGIN_METHOD_TEMPLATE_API -> { + LoginMethod.TEMPLATE_API -> { data.startProgress(R.string.edziennik_progress_login_template_api) - TemplateLoginApi(data) { onSuccess(loginMethodId) } + TemplateLoginApi(data) { onSuccess(loginMethod) } } + else -> {} } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLoginApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLoginApi.kt index fc1fc20e..f49e433a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLoginApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLoginApi.kt @@ -4,12 +4,12 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.template.login -import pl.szczodrzynski.edziennik.HOUR import pl.szczodrzynski.edziennik.data.api.ERROR_LOGIN_DATA_MISSING import pl.szczodrzynski.edziennik.data.api.ERROR_PROFILE_MISSING import pl.szczodrzynski.edziennik.data.api.edziennik.template.DataTemplate import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.currentTimeUnix +import pl.szczodrzynski.edziennik.ext.HOUR +import pl.szczodrzynski.edziennik.ext.currentTimeUnix class TemplateLoginApi(val data: DataTemplate, val onSuccess: () -> Unit) { companion object { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLoginWeb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLoginWeb.kt index d7a24ef0..2f5b72c6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLoginWeb.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/template/login/TemplateLoginWeb.kt @@ -4,11 +4,11 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.template.login -import pl.szczodrzynski.edziennik.currentTimeUnix import pl.szczodrzynski.edziennik.data.api.ERROR_LOGIN_DATA_MISSING import pl.szczodrzynski.edziennik.data.api.ERROR_PROFILE_MISSING import pl.szczodrzynski.edziennik.data.api.edziennik.template.DataTemplate import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.ext.currentTimeUnix class TemplateLoginWeb(val data: DataTemplate, val onSuccess: () -> Unit) { companion object { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt new file mode 100644 index 00000000..8ebafb51 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/DataUsos.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos + +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.models.Data +import pl.szczodrzynski.edziennik.data.db.entity.LoginStore +import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.ext.getStudentData +import pl.szczodrzynski.edziennik.ext.set + +class DataUsos( + app: App, + profile: Profile?, + loginStore: LoginStore, +) : Data(app, profile, loginStore) { + + fun isApiLoginValid() = oauthTokenKey != null && oauthTokenSecret != null && oauthTokenIsUser + + override fun satisfyLoginMethods() { + loginMethods.clear() + if (isApiLoginValid()) { + loginMethods += LoginMethod.USOS_API + } + } + + override fun generateUserCode() = "$schoolId:${profile?.studentNumber ?: studentId}" + + var schoolId: String? + get() { mSchoolId = mSchoolId ?: loginStore.getLoginData("schoolId", null); return mSchoolId } + set(value) { loginStore.putLoginData("schoolId", value); mSchoolId = value } + private var mSchoolId: String? = null + + var instanceUrl: String? + get() { mInstanceUrl = mInstanceUrl ?: loginStore.getLoginData("instanceUrl", null); return mInstanceUrl } + set(value) { loginStore.putLoginData("instanceUrl", value); mInstanceUrl = value } + private var mInstanceUrl: String? = null + + var oauthLoginResponse: String? + get() { mOauthLoginResponse = mOauthLoginResponse ?: loginStore.getLoginData("oauthLoginResponse", null); return mOauthLoginResponse } + set(value) { loginStore.putLoginData("oauthLoginResponse", value); mOauthLoginResponse = value } + private var mOauthLoginResponse: String? = null + + var oauthConsumerKey: String? + get() { mOauthConsumerKey = mOauthConsumerKey ?: loginStore.getLoginData("oauthConsumerKey", null); return mOauthConsumerKey } + set(value) { loginStore.putLoginData("oauthConsumerKey", value); mOauthConsumerKey = value } + private var mOauthConsumerKey: String? = null + + var oauthConsumerSecret: String? + get() { mOauthConsumerSecret = mOauthConsumerSecret ?: loginStore.getLoginData("oauthConsumerSecret", null); return mOauthConsumerSecret } + set(value) { loginStore.putLoginData("oauthConsumerSecret", value); mOauthConsumerSecret = value } + private var mOauthConsumerSecret: String? = null + + var oauthTokenKey: String? + get() { mOauthTokenKey = mOauthTokenKey ?: loginStore.getLoginData("oauthTokenKey", null); return mOauthTokenKey } + set(value) { loginStore.putLoginData("oauthTokenKey", value); mOauthTokenKey = value } + private var mOauthTokenKey: String? = null + + var oauthTokenSecret: String? + get() { mOauthTokenSecret = mOauthTokenSecret ?: loginStore.getLoginData("oauthTokenSecret", null); return mOauthTokenSecret } + set(value) { loginStore.putLoginData("oauthTokenSecret", value); mOauthTokenSecret = value } + private var mOauthTokenSecret: String? = null + + var oauthTokenIsUser: Boolean + get() { mOauthTokenIsUser = mOauthTokenIsUser ?: loginStore.getLoginData("oauthTokenIsUser", false); return mOauthTokenIsUser ?: false } + set(value) { loginStore.putLoginData("oauthTokenIsUser", value); mOauthTokenIsUser = value } + private var mOauthTokenIsUser: Boolean? = null + + var studentId: Int + get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 } + set(value) { profile["studentId"] = value; mStudentId = value } + private var mStudentId: Int? = null +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt new file mode 100644 index 00000000..61cc0fda --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/Usos.kt @@ -0,0 +1,115 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosData +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.firstlogin.UsosFirstLogin +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.login.UsosLogin +import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent +import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback +import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.api.prepare +import pl.szczodrzynski.edziennik.data.db.entity.LoginStore +import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull +import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.data.db.full.MessageFull +import pl.szczodrzynski.edziennik.utils.Utils.d + +class Usos( + val app: App, + val profile: Profile?, + val loginStore: LoginStore, + val callback: EdziennikCallback, +) : EdziennikInterface { + companion object { + private const val TAG = "Usos" + } + + val internalErrorList = mutableListOf() + val data: DataUsos + + init { + data = DataUsos(app, profile, loginStore).apply { + callback = wrapCallback(this@Usos.callback) + satisfyLoginMethods() + } + } + + private fun completed() { + data.saveData() + callback.onCompleted() + } + + override fun sync( + featureTypes: Set?, + onlyEndpoints: Set?, + arguments: JsonObject?, + ) { + data.arguments = arguments + data.prepare(UsosFeatures, featureTypes, onlyEndpoints) + d(TAG, "LoginMethod IDs: ${data.targetLoginMethods}") + d(TAG, "Endpoint IDs: ${data.targetEndpoints}") + UsosLogin(data) { + UsosData(data) { + completed() + } + } + } + + override fun getMessage(message: MessageFull) {} + override fun sendMessage(recipients: Set, subject: String, text: String) {} + override fun markAllAnnouncementsAsRead() {} + override fun getAnnouncement(announcement: AnnouncementFull) {} + override fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) {} + override fun getRecipientList() {} + override fun getEvent(eventFull: EventFull) {} + + override fun firstLogin() { + UsosFirstLogin(data) { + completed() + } + } + + override fun cancel() { + d(TAG, "Cancelled") + data.cancel() + } + + private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { + return object : EdziennikCallback { + override fun onCompleted() { + callback.onCompleted() + } + + override fun onRequiresUserAction(event: UserActionRequiredEvent) { + callback.onRequiresUserAction(event) + } + + override fun onProgress(step: Float) { + callback.onProgress(step) + } + + override fun onStartProgress(stringRes: Int) { + callback.onStartProgress(stringRes) + } + + override fun onError(apiError: ApiError) { + when (apiError.errorCode) { + in internalErrorList -> { + // finish immediately if the same error occurs twice during the same sync + callback.onError(apiError) + } + else -> callback.onError(apiError) + } + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt new file mode 100644 index 00000000..f36580fb --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/UsosFeatures.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos + +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.models.Feature +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.data.db.enums.LoginType + +const val ENDPOINT_USOS_API_USER = 7000 +const val ENDPOINT_USOS_API_TERMS = 7010 +const val ENDPOINT_USOS_API_COURSES = 7020 +const val ENDPOINT_USOS_API_TIMETABLE = 7030 + +val UsosFeatures = listOf( + /* + * Student information + */ + Feature(LoginType.USOS, FeatureType.STUDENT_INFO, listOf( + ENDPOINT_USOS_API_USER to LoginMethod.USOS_API, + )), + + /* + * Terms & courses + */ + Feature(LoginType.USOS, FeatureType.SCHOOL_INFO, listOf( + ENDPOINT_USOS_API_TERMS to LoginMethod.USOS_API, + )), + Feature(LoginType.USOS, FeatureType.TEAM_INFO, listOf( + ENDPOINT_USOS_API_COURSES to LoginMethod.USOS_API, + )), + + /* + * Timetable + */ + Feature(LoginType.USOS, FeatureType.TIMETABLE, listOf( + ENDPOINT_USOS_API_TIMETABLE to LoginMethod.USOS_API, + )), +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt new file mode 100644 index 00000000..9f716d50 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosApi.kt @@ -0,0 +1,209 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-13. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data + +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import im.wangchao.mhttp.Request +import im.wangchao.mhttp.Response +import im.wangchao.mhttp.body.MediaTypeUtils +import im.wangchao.mhttp.callback.JsonArrayCallbackHandler +import im.wangchao.mhttp.callback.JsonCallbackHandler +import im.wangchao.mhttp.callback.TextCallbackHandler +import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE +import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_MISSING_RESPONSE +import pl.szczodrzynski.edziennik.data.api.SERVER_USER_AGENT +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.login.UsosLoginApi +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.utils.Utils.d +import java.net.HttpURLConnection.* +import java.util.UUID + +open class UsosApi(open val data: DataUsos, open val lastSync: Long?) { + companion object { + private const val TAG = "UsosApi" + } + + enum class ResponseType { + OBJECT, + ARRAY, + PLAIN, + } + + val profileId + get() = data.profile?.id ?: -1 + + val profile + get() = data.profile + + protected fun JsonObject.getLangString(key: String) = + this.getJsonObject(key)?.getString("pl") + + protected fun JsonObject.getLecturerIds(key: String) = + this.getJsonArray(key)?.asJsonObjectList()?.mapNotNull { + val id = it.getLong("id") ?: return@mapNotNull null + val firstName = it.getString("first_name") ?: return@mapNotNull null + val lastName = it.getString("last_name") ?: return@mapNotNull null + data.getTeacher(firstName, lastName, id = id).id + } ?: listOf() + + private fun valueToString(value: Any) = when (value) { + is String -> value + is Number -> value.toString() + is List<*> -> listToString(value) + else -> value.toString() + } + + private fun listToString(list: List<*>): String { + return list.map { + if (it is Pair<*, *> && it.first is String && it.second is List<*>) + return@map "${it.first}[${listToString(it.second as List<*>)}]" + return@map valueToString(it ?: "") + }.joinToString("|") + } + + private fun buildSignature(method: String, url: String, params: Map): String { + val query = params.toQueryString() + val signatureString = listOf( + method.uppercase(), + url.urlEncode(), + query.urlEncode(), + ).joinToString("&") + val signingKey = listOf( + data.oauthConsumerSecret ?: "", + data.oauthTokenSecret ?: "", + ).joinToString("&") { it.urlEncode() } + return signatureString.hmacSHA1(signingKey) + } + + fun apiRequest( + tag: String, + service: String, + params: Map? = null, + fields: List? = null, + responseType: ResponseType, + onSuccess: (data: T, response: Response?) -> Unit, + ) { + val url = "${data.instanceUrl}services/$service" + d(tag, "Request: Usos/Api - $url") + + val formData = mutableMapOf() + if (params != null) + formData.putAll(params.mapValues { + valueToString(it.value) + }) + if (fields != null) + formData["fields"] = valueToString(fields) + + val auth = mutableMapOf( + "realm" to url, + "oauth_consumer_key" to (data.oauthConsumerKey ?: ""), + "oauth_nonce" to UUID.randomUUID().toString(), + "oauth_signature_method" to "HMAC-SHA1", + "oauth_timestamp" to currentTimeUnix().toString(), + "oauth_token" to (data.oauthTokenKey ?: ""), + "oauth_version" to "1.0", + ) + val signature = buildSignature( + method = "POST", + url = url, + params = formData + auth.filterKeys { it.startsWith("oauth_") }, + ) + auth["oauth_signature"] = signature + + val authString = auth.map { + """${it.key}="${it.value.urlEncode()}"""" + }.joinToString(", ") + + Request.builder() + .url(url) + .userAgent(SERVER_USER_AGENT) + .addHeader("Authorization", "OAuth $authString") + .post() + .setTextBody(formData.toQueryString(), MediaTypeUtils.APPLICATION_FORM) + .allowErrorCode(HTTP_BAD_REQUEST) + .allowErrorCode(HTTP_UNAUTHORIZED) + .allowErrorCode(HTTP_FORBIDDEN) + .allowErrorCode(HTTP_NOT_FOUND) + .allowErrorCode(HTTP_UNAVAILABLE) + .callback(getCallback(tag, responseType, onSuccess)) + .build() + .enqueue() + } + + @Suppress("UNCHECKED_CAST") + private fun getCallback( + tag: String, + responseType: ResponseType, + onSuccess: (data: T, response: Response?) -> Unit, + ) = when (responseType) { + ResponseType.OBJECT -> object : JsonCallbackHandler() { + override fun onSuccess(data: JsonObject?, response: Response) { + processResponse(tag, response, data as T?, onSuccess) + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + processError(tag, response, throwable) + } + } + ResponseType.ARRAY -> object : JsonArrayCallbackHandler() { + override fun onSuccess(data: JsonArray?, response: Response) { + processResponse(tag, response, data as T?, onSuccess) + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + processError(tag, response, throwable) + } + } + ResponseType.PLAIN -> object : TextCallbackHandler() { + override fun onSuccess(data: String?, response: Response) { + processResponse(tag, response, data as T?, onSuccess) + } + + override fun onFailure(response: Response?, throwable: Throwable?) { + processError(tag, response, throwable) + } + } + } + + private fun processResponse( + tag: String, + response: Response, + value: T?, + onSuccess: (data: T, response: Response?) -> Unit, + ) { + val errorCode = when { + response.code() == HTTP_UNAUTHORIZED -> { + data.oauthTokenKey = null + data.oauthTokenSecret = null + data.oauthTokenIsUser = false + data.oauthLoginResponse = null + UsosLoginApi(data) { } + return + } + value == null -> ERROR_USOS_API_MISSING_RESPONSE + response.code() == HTTP_OK -> { + onSuccess(value, response) + null + } + else -> response.toErrorCode() + } + if (errorCode != null) { + data.error(tag, errorCode, response, value.toString()) + } + } + + private fun processError( + tag: String, + response: Response?, + throwable: Throwable?, + ) { + data.error(ApiError(tag, ERROR_REQUEST_FAILURE) + .withResponse(response) + .withThrowable(throwable)) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt new file mode 100644 index 00000000..dbccb16b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/UsosData.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-13. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data + +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.edziennik.template.data.web.TemplateWebSample +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.* +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiCourses +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTerms +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiTimetable +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api.UsosApiUser +import pl.szczodrzynski.edziennik.utils.Utils.d + +class UsosData(val data: DataUsos, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "UsosData" + } + + init { + nextEndpoint(onSuccess) + } + + private fun nextEndpoint(onSuccess: () -> Unit) { + if (data.targetEndpoints.isEmpty()) { + onSuccess() + return + } + if (data.cancelled) { + onSuccess() + return + } + val id = data.targetEndpoints.firstKey() + val lastSync = data.targetEndpoints.remove(id) + useEndpoint(id, lastSync) { endpointId -> + data.progress(data.progressStep) + nextEndpoint(onSuccess) + } + } + + private fun useEndpoint(endpointId: Int, lastSync: Long?, onSuccess: (endpointId: Int) -> Unit) { + d(TAG, "Using endpoint $endpointId. Last sync time = $lastSync") + when (endpointId) { + ENDPOINT_USOS_API_USER -> { + data.startProgress(R.string.edziennik_progress_endpoint_student_info) + UsosApiUser(data, lastSync, onSuccess) + } + ENDPOINT_USOS_API_TERMS -> { + data.startProgress(R.string.edziennik_progress_endpoint_school_info) + UsosApiTerms(data, lastSync, onSuccess) + } + ENDPOINT_USOS_API_COURSES -> { + data.startProgress(R.string.edziennik_progress_endpoint_teams) + UsosApiCourses(data, lastSync, onSuccess) + } + ENDPOINT_USOS_API_TIMETABLE -> { + data.startProgress(R.string.edziennik_progress_endpoint_timetable) + UsosApiTimetable(data, lastSync, onSuccess) + } + else -> onSuccess(endpointId) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt new file mode 100644 index 00000000..e93c63dd --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiCourses.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-15. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_COURSES +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi +import pl.szczodrzynski.edziennik.data.db.entity.Team +import pl.szczodrzynski.edziennik.ext.* + +class UsosApiCourses( + override val data: DataUsos, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit, +) : UsosApi(data, lastSync) { + companion object { + const val TAG = "UsosApiCourses" + } + + init { + apiRequest( + tag = TAG, + service = "courses/user", + fields = listOf( + // "terms" to listOf("id", "name", "start_date", "end_date"), + "course_editions" to listOf( + "course_id", + "course_name", + // "term_id", + "user_groups" to listOf( + "course_unit_id", + "group_number", + // "class_type", + "class_type_id", + "lecturers", + ), + ), + ), + responseType = ResponseType.OBJECT, + ) { json, response -> + if (!processResponse(json)) { + data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) + return@apiRequest + } + + data.setSyncNext(ENDPOINT_USOS_API_COURSES, 2 * DAY) + onSuccess(ENDPOINT_USOS_API_COURSES) + } + } + + private fun processResponse(json: JsonObject): Boolean { + // val term = json.getJsonArray("terms")?.firstOrNull() ?: return false + val courseEditions = json.getJsonObject("course_editions") + ?.entrySet() + ?.flatMap { it.value.asJsonArray } + ?.map { it.asJsonObject } ?: return false + + var hasValidTeam = false + for (courseEdition in courseEditions) { + val courseId = courseEdition.getString("course_id") ?: continue + val courseName = courseEdition.getLangString("course_name") ?: continue + val userGroups = courseEdition.getJsonArray("user_groups")?.asJsonObjectList() ?: continue + for (userGroup in userGroups) { + val courseUnitId = userGroup.getLong("course_unit_id") ?: continue + val groupNumber = userGroup.getInt("group_number") ?: continue + // val classType = userGroup.getLangString("class_type") ?: continue + val classTypeId = userGroup.getString("class_type_id") ?: continue + val lecturers = userGroup.getLecturerIds("lecturers") + + data.teamList.put(courseUnitId, Team( + profileId, + courseUnitId, + "${profile?.studentClassName} $classTypeId$groupNumber - $courseName", + 2, + "${data.schoolId}:${courseId} $classTypeId$groupNumber", + lecturers.firstOrNull() ?: -1L, + )) + hasValidTeam = true + } + } + return hasValidTeam + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt new file mode 100644 index 00000000..b1b2fd88 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTerms.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-15. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api + +import com.google.gson.JsonArray +import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TERMS +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.utils.models.Date + +class UsosApiTerms( + override val data: DataUsos, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit, +) : UsosApi(data, lastSync) { + companion object { + const val TAG = "UsosApiTerms" + } + + init { + apiRequest( + tag = TAG, + service = "terms/search", + params = mapOf( + "query" to Date.getToday().year.toString(), + ), + responseType = ResponseType.ARRAY, + ) { json, response -> + if (!processResponse(json)) { + data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) + return@apiRequest + } + + data.setSyncNext(ENDPOINT_USOS_API_TERMS, 7 * DAY) + onSuccess(ENDPOINT_USOS_API_TERMS) + } + } + + private fun processResponse(json: JsonArray): Boolean { + val today = Date.getToday() + for (term in json.asJsonObjectList()) { + if (!term.getBoolean("is_active", false)) + continue + val startDate = term.getString("start_date")?.let { Date.fromY_m_d(it) } ?: continue + val finishDate = term.getString("finish_date")?.let { Date.fromY_m_d(it) } ?: continue + if (today in startDate..finishDate) { + profile?.studentSchoolYearStart = startDate.year + profile?.dateSemester1Start = startDate + profile?.dateSemester2Start = finishDate + } + } + return true + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTimetable.kt new file mode 100644 index 00000000..251b2f4b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiTimetable.kt @@ -0,0 +1,143 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-16. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api + +import com.google.gson.JsonArray +import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_API_INCOMPLETE_RESPONSE +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_TIMETABLE +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel +import pl.szczodrzynski.edziennik.data.db.entity.Lesson +import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time +import pl.szczodrzynski.edziennik.utils.models.Week + +class UsosApiTimetable( + override val data: DataUsos, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit, +) : UsosApi(data, lastSync) { + companion object { + const val TAG = "UsosApiTimetable" + } + + init { + val currentWeekStart = Week.getWeekStart() + if (Date.getToday().weekDay > 4) + currentWeekStart.stepForward(0, 0, 7) + + val weekStart = data.arguments + ?.getString("weekStart") + ?.let { Date.fromY_m_d(it) } + ?: currentWeekStart + val weekEnd = weekStart.clone().stepForward(0, 0, 6) + + apiRequest( + tag = TAG, + service = "tt/user", + params = mapOf( + "start" to weekStart.stringY_m_d, + "days" to 7, + ), + fields = listOf( + "type", + "start_time", + "end_time", + "unit_id", + "course_id", + "course_name", + "lecturer_ids", + "building_id", + "room_number", + "classtype_id", + "group_number", + ), + responseType = ResponseType.ARRAY, + ) { json, response -> + if (!processResponse(json, weekStart..weekEnd)) { + data.error(TAG, ERROR_USOS_API_INCOMPLETE_RESPONSE, response) + return@apiRequest + } + + data.toRemove.add(DataRemoveModel.Timetable.between(weekStart, weekEnd)) + data.setSyncNext(ENDPOINT_USOS_API_TIMETABLE, SYNC_ALWAYS) + onSuccess(ENDPOINT_USOS_API_TIMETABLE) + } + } + + private fun processResponse(json: JsonArray, syncRange: ClosedRange): Boolean { + val foundDates = mutableSetOf() + + for (activity in json.asJsonObjectList()) { + val type = activity.getString("type") + if (type !in listOf("classgroup", "classgroup2")) + continue + + val startTime = activity.getString("start_time") ?: continue + val endTime = activity.getString("end_time") ?: continue + val unitId = activity.getLong("unit_id", -1) + val courseName = activity.getLangString("course_name") ?: continue + val courseId = activity.getString("course_id") ?: continue + val lecturerIds = activity.getJsonArray("lecturer_ids")?.map { it.asLong } + val buildingId = activity.getString("building_id") + val roomNumber = activity.getString("room_number") + val classTypeId = activity.getString("classtype_id") + val groupNumber = activity.getString("group_number") + + val lesson = Lesson(profileId, -1).also { + it.type = Lesson.TYPE_NORMAL + it.date = Date.fromY_m_d(startTime) + it.startTime = Time.fromY_m_d_H_m_s(startTime) + it.endTime = Time.fromY_m_d_H_m_s(endTime) + it.subjectId = data.getSubject( + id = null, + name = courseName, + shortName = courseId, + ).id + it.teacherId = lecturerIds?.firstOrNull() ?: -1L + it.teamId = unitId + val groupName = classTypeId?.plus(groupNumber)?.let { s -> "($s)" } + it.classroom = "Sala $roomNumber / bud. $buildingId ${groupName ?: ""}" + it.id = it.buildId() + it.ownerId = it.buildOwnerId() + + it.color = when (classTypeId) { + "WYK" -> 0xff0d6091 + "CW" -> 0xff54306e + "LAB" -> 0xff772747 + "KON" -> 0xff1e5128 + "^P?SEM" -> 0xff1e5128 // TODO make it regex + else -> 0xff08534c + }.toInt() + } + lesson.date?.let { foundDates += it } + + val seen = profile?.empty != false || lesson.date!! < Date.getToday() + data.lessonList.add(lesson) + if (lesson.type != Lesson.TYPE_NORMAL) + data.metadataList += Metadata( + profileId, + MetadataType.LESSON_CHANGE, + lesson.id, + seen, + seen, + ) + } + + val notFoundDates = syncRange.asSequence() - foundDates + for (date in notFoundDates) { + data.lessonList += Lesson(profileId, date.value.toLong()).also { + it.type = Lesson.TYPE_NO_LESSONS + it.date = date + } + } + return true + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiUser.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiUser.kt new file mode 100644 index 00000000..29a3e22b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/data/api/UsosApiUser.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-16. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.api + +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_NO_STUDENT_PROGRAMMES +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.ENDPOINT_USOS_API_USER +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.ext.* + +class UsosApiUser( + override val data: DataUsos, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit, +) : UsosApi(data, lastSync) { + companion object { + const val TAG = "UsosApiUser" + } + + init { + apiRequest( + tag = TAG, + service = "users/user", + params = mapOf( + "fields" to listOf( + "id", + "first_name", + "last_name", + "student_number", + "student_programmes" to listOf( + "programme" to listOf("id"), + ), + ), + ), + responseType = ResponseType.OBJECT, + ) { json, response -> + val programmes = json.getJsonArray("student_programmes") + if (programmes.isNullOrEmpty()) { + data.error(ApiError(TAG, ERROR_USOS_NO_STUDENT_PROGRAMMES) + .withApiResponse(json) + .withResponse(response)) + return@apiRequest + } + + val firstName = json.getString("first_name") + val lastName = json.getString("last_name") + val studentName = buildFullName(firstName, lastName) + + data.studentId = json.getInt("id") ?: data.studentId + profile?.studentNameLong = studentName + profile?.studentNameShort = studentName.getShortName() + profile?.studentNumber = json.getInt("student_number", -1) + profile?.studentClassName = programmes.getJsonObject(0).getJsonObject("programme").getString("id") + + profile?.studentClassName?.let { + data.getTeam( + id = null, + name = it, + schoolCode = data.schoolId ?: "", + isTeamClass = true, + ) + } + + data.setSyncNext(ENDPOINT_USOS_API_USER, 4 * DAY) + onSuccess(ENDPOINT_USOS_API_USER) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt new file mode 100644 index 00000000..f839511c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/firstlogin/UsosFirstLogin.kt @@ -0,0 +1,82 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-14. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.firstlogin + +import com.google.gson.JsonObject +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.data.api.ERROR_USOS_NO_STUDENT_PROGRAMMES +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.login.UsosLoginApi +import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.enums.LoginType +import pl.szczodrzynski.edziennik.ext.* + +class UsosFirstLogin(val data: DataUsos, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "UsosFirstLogin" + } + + private val api = UsosApi(data, null) + + init { + var firstProfileId = data.loginStore.id + + UsosLoginApi(data) { + api.apiRequest( + tag = TAG, + service = "users/user", + params = mapOf( + "fields" to listOf( + "id", + "first_name", + "last_name", + "student_number", + "student_programmes" to listOf( + "programme" to listOf("id"), + ), + ), + ), + responseType = UsosApi.ResponseType.OBJECT, + ) { json, response -> + val programmes = json.getJsonArray("student_programmes") + if (programmes.isNullOrEmpty()) { + data.error(ApiError(TAG, ERROR_USOS_NO_STUDENT_PROGRAMMES) + .withApiResponse(json) + .withResponse(response)) + return@apiRequest + } + + val firstName = json.getString("first_name") + val lastName = json.getString("last_name") + val studentName = buildFullName(firstName, lastName) + + val profile = Profile( + id = firstProfileId++, + loginStoreId = data.loginStore.id, + loginStoreType = LoginType.USOS, + name = studentName, + subname = data.schoolId, + studentNameLong = studentName, + studentNameShort = studentName.getShortName(), + accountName = null, // student account + studentData = JsonObject( + "studentId" to json.getInt("id"), + ), + ).also { + it.studentNumber = json.getInt("student_number", -1) + it.studentClassName = programmes.getJsonObject(0).getJsonObject("programme").getString("id") + } + + EventBus.getDefault().postSticky( + FirstLoginFinishedEvent(listOf(profile), data.loginStore), + ) + onSuccess() + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLogin.kt new file mode 100644 index 00000000..3f745b34 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLogin.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.login + +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.utils.Utils.d + +class UsosLogin(val data: DataUsos, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "UsosLogin" + } + + private var cancelled = false + + init { + nextLoginMethod(onSuccess) + } + + private fun nextLoginMethod(onSuccess: () -> Unit) { + if (data.targetLoginMethods.isEmpty()) { + onSuccess() + return + } + if (cancelled) { + onSuccess() + return + } + useLoginMethod(data.targetLoginMethods.removeAt(0)) { usedMethod -> + data.progress(data.progressStep) + if (usedMethod != null) + data.loginMethods.add(usedMethod) + nextLoginMethod(onSuccess) + } + } + + private fun useLoginMethod(loginMethod: LoginMethod, onSuccess: (usedMethod: LoginMethod?) -> Unit) { + // this should never be true + if (data.loginMethods.contains(loginMethod)) { + onSuccess(null) + return + } + d(TAG, "Using login method $loginMethod") + when (loginMethod) { + LoginMethod.USOS_API -> { + data.startProgress(R.string.edziennik_progress_login_usos_api) + UsosLoginApi(data) { onSuccess(loginMethod) } + } + else -> {} + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt new file mode 100644 index 00000000..06651477 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/usos/login/UsosLoginApi.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-11. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.usos.login + +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.DataUsos +import pl.szczodrzynski.edziennik.data.api.edziennik.usos.data.UsosApi +import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.utils.Utils.d + +class UsosLoginApi(val data: DataUsos, val onSuccess: () -> Unit) { + companion object { + private const val TAG = "UsosLoginApi" + } + + private val api = UsosApi(data, null) + + init { + run { + data.arguments?.getString("oauthLoginResponse")?.let { + data.oauthLoginResponse = it + } + if (data.isApiLoginValid()) { + onSuccess() + } else if (data.oauthLoginResponse != null) { + login() + } else { + authorize() + } + } + } + + private fun authorize() { + data.oauthTokenKey = null + data.oauthTokenSecret = null + api.apiRequest( + tag = TAG, + service = "oauth/request_token", + params = mapOf( + "oauth_callback" to USOS_API_OAUTH_REDIRECT_URL, + "scopes" to USOS_API_SCOPES, + ), + responseType = UsosApi.ResponseType.PLAIN, + ) { text, _ -> + val authorizeData = text.fromQueryString() + data.oauthTokenKey = authorizeData["oauth_token"] + data.oauthTokenSecret = authorizeData["oauth_token_secret"] + data.oauthTokenIsUser = false + + val authUrl = "${data.instanceUrl}services/oauth/authorize" + val authParams = mapOf( + "interactivity" to "confirm_user", + "oauth_token" to (data.oauthTokenKey ?: ""), + ) + data.requireUserAction( + type = UserActionRequiredEvent.Type.OAUTH, + params = Bundle( + "authorizeUrl" to "$authUrl?${authParams.toQueryString()}", + "redirectUrl" to USOS_API_OAUTH_REDIRECT_URL, + "responseStoreKey" to "oauthLoginResponse", + "extras" to data.loginStore.data.toBundle(), + ), + errorText = R.string.notification_user_action_required_oauth_usos, + ) + } + } + + private fun login() { + d(TAG, "Login to ${data.schoolId} with ${data.oauthLoginResponse}") + + val authorizeResponse = data.oauthLoginResponse?.fromQueryString() + ?: return // checked in init {} + if (authorizeResponse["oauth_token"] != data.oauthTokenKey) { + // got different token + data.error(ApiError(TAG, ERROR_USOS_OAUTH_GOT_DIFFERENT_TOKEN) + .withApiResponse(data.oauthLoginResponse)) + return + } + val verifier = authorizeResponse["oauth_verifier"] + if (verifier.isNullOrBlank()) { + data.error(ApiError(TAG, ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE) + .withApiResponse(data.oauthLoginResponse)) + return + } + + api.apiRequest( + tag = TAG, + service = "oauth/access_token", + params = mapOf( + "oauth_verifier" to verifier, + ), + responseType = UsosApi.ResponseType.PLAIN, + ) { text, response -> + val accessData = text.fromQueryString() + data.oauthTokenKey = accessData["oauth_token"] + data.oauthTokenSecret = accessData["oauth_token_secret"] + data.oauthTokenIsUser = data.oauthTokenKey != null && data.oauthTokenSecret != null + data.loginStore.removeLoginData("oauthLoginResponse") + + if (!data.oauthTokenIsUser) + data.error(ApiError(TAG, ERROR_USOS_OAUTH_INCOMPLETE_RESPONSE) + .withApiResponse(text) + .withResponse(response)) + else + onSuccess() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/DataVulcan.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/DataVulcan.kt index 55185745..b4e1610b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/DataVulcan.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/DataVulcan.kt @@ -6,14 +6,15 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan import com.google.gson.JsonObject import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.crc16 -import pl.szczodrzynski.edziennik.currentTimeUnix -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_HEBE -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_WEB_MAIN import pl.szczodrzynski.edziennik.data.api.models.Data import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.Profile -import pl.szczodrzynski.edziennik.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.ext.crc16 +import pl.szczodrzynski.edziennik.ext.currentTimeUnix +import pl.szczodrzynski.edziennik.ext.getStudentData +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.ext.set import pl.szczodrzynski.fslogin.realm.RealmData class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app, profile, loginStore) { @@ -28,10 +29,10 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app override fun satisfyLoginMethods() { loginMethods.clear() if (isWebMainLoginValid()) { - loginMethods += LOGIN_METHOD_VULCAN_WEB_MAIN + loginMethods += LoginMethod.VULCAN_WEB_MAIN } if (isHebeLoginValid()) { - loginMethods += LOGIN_METHOD_VULCAN_HEBE + loginMethods += LoginMethod.VULCAN_HEBE } } @@ -58,7 +59,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mSymbol: String? = null var symbol: String? get() { mSymbol = mSymbol ?: profile?.getStudentData("symbol", null); return mSymbol } - set(value) { profile?.putStudentData("symbol", value); mSymbol = value } + set(value) { profile["symbol"] = value; mSymbol = value } /** * Group symbol/number of the student's school. @@ -70,7 +71,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mSchoolSymbol: String? = null var schoolSymbol: String? get() { mSchoolSymbol = mSchoolSymbol ?: profile?.getStudentData("schoolSymbol", null); return mSchoolSymbol } - set(value) { profile?.putStudentData("schoolSymbol", value) ?: return; mSchoolSymbol = value } + set(value) { profile["schoolSymbol"] = value; mSchoolSymbol = value } /** * Short name of the school, used in some places. @@ -80,7 +81,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mSchoolShort: String? = null var schoolShort: String? get() { mSchoolShort = mSchoolShort ?: profile?.getStudentData("schoolShort", null); return mSchoolShort } - set(value) { profile?.putStudentData("schoolShort", value) ?: return; mSchoolShort = value } + set(value) { profile["schoolShort"] = value; mSchoolShort = value } /** * A school code consisting of the [symbol] and [schoolSymbol]. @@ -92,7 +93,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mSchoolCode: String? = null var schoolCode: String? get() { mSchoolCode = mSchoolCode ?: profile?.getStudentData("schoolName", null); return mSchoolCode } - set(value) { profile?.putStudentData("schoolName", value) ?: return; mSchoolCode = value } + set(value) { profile["schoolName"] = value; mSchoolCode = value } /** * ID of the student. @@ -102,7 +103,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mStudentId: Int? = null var studentId: Int get() { mStudentId = mStudentId ?: profile?.getStudentData("studentId", 0); return mStudentId ?: 0 } - set(value) { profile?.putStudentData("studentId", value) ?: return; mStudentId = value } + set(value) { profile["studentId"] = value; mStudentId = value } /** * ID of the student's account. @@ -112,7 +113,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mStudentLoginId: Int? = null var studentLoginId: Int get() { mStudentLoginId = mStudentLoginId ?: profile?.getStudentData("studentLoginId", 0); return mStudentLoginId ?: 0 } - set(value) { profile?.putStudentData("studentLoginId", value) ?: return; mStudentLoginId = value } + set(value) { profile["studentLoginId"] = value; mStudentLoginId = value } /** * ID of the student's class. @@ -122,7 +123,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mStudentClassId: Int? = null var studentClassId: Int get() { mStudentClassId = mStudentClassId ?: profile?.getStudentData("studentClassId", 0); return mStudentClassId ?: 0 } - set(value) { profile?.putStudentData("studentClassId", value) ?: return; mStudentClassId = value } + set(value) { profile["studentClassId"] = value; mStudentClassId = value } /** * ListaUczniow/IdOkresKlasyfikacyjny, e.g. 321 @@ -130,26 +131,26 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mStudentSemesterId: Int? = null var studentSemesterId: Int get() { mStudentSemesterId = mStudentSemesterId ?: profile?.getStudentData("studentSemesterId", 0); return mStudentSemesterId ?: 0 } - set(value) { profile?.putStudentData("studentSemesterId", value) ?: return; mStudentSemesterId = value } + set(value) { profile["studentSemesterId"] = value; mStudentSemesterId = value } private var mStudentUnitId: Int? = null var studentUnitId: Int get() { mStudentUnitId = mStudentUnitId ?: profile?.getStudentData("studentUnitId", 0); return mStudentUnitId ?: 0 } - set(value) { profile?.putStudentData("studentUnitId", value) ?: return; mStudentUnitId = value } + set(value) { profile["studentUnitId"] = value; mStudentUnitId = value } private var mStudentConstituentId: Int? = null var studentConstituentId: Int get() { mStudentConstituentId = mStudentConstituentId ?: profile?.getStudentData("studentConstituentId", 0); return mStudentConstituentId ?: 0 } - set(value) { profile?.putStudentData("studentConstituentId", value) ?: return; mStudentConstituentId = value } + set(value) { profile["studentConstituentId"] = value; mStudentConstituentId = value } private var mSemester1Id: Int? = null var semester1Id: Int get() { mSemester1Id = mSemester1Id ?: profile?.getStudentData("semester1Id", 0); return mSemester1Id ?: 0 } - set(value) { profile?.putStudentData("semester1Id", value) ?: return; mSemester1Id = value } + set(value) { profile["semester1Id"] = value; mSemester1Id = value } private var mSemester2Id: Int? = null var semester2Id: Int get() { mSemester2Id = mSemester2Id ?: profile?.getStudentData("semester2Id", 0); return mSemester2Id ?: 0 } - set(value) { profile?.putStudentData("semester2Id", value) ?: return; mSemester2Id = value } + set(value) { profile["semester2Id"] = value; mSemester2Id = value } /** * ListaUczniow/OkresNumer, e.g. 1 or 2 @@ -157,7 +158,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mStudentSemesterNumber: Int? = null var studentSemesterNumber: Int get() { mStudentSemesterNumber = mStudentSemesterNumber ?: profile?.getStudentData("studentSemesterNumber", 0); return mStudentSemesterNumber ?: 0 } - set(value) { profile?.putStudentData("studentSemesterNumber", value) ?: return; mStudentSemesterNumber = value } + set(value) { profile["studentSemesterNumber"] = value; mStudentSemesterNumber = value } /** * Date of the end of the current semester ([studentSemesterNumber]). @@ -167,7 +168,7 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mCurrentSemesterEndDate: Long? = null var currentSemesterEndDate: Long get() { mCurrentSemesterEndDate = mCurrentSemesterEndDate ?: profile?.getStudentData("currentSemesterEndDate", 0L); return mCurrentSemesterEndDate ?: 0L } - set(value) { profile?.putStudentData("currentSemesterEndDate", value) ?: return; mCurrentSemesterEndDate = value } + set(value) { profile["currentSemesterEndDate"] = value; mCurrentSemesterEndDate = value } /* _____ _____ ____ /\ | __ \_ _| |___ \ @@ -220,7 +221,17 @@ class DataVulcan(app: App, profile: Profile?, loginStore: LoginStore) : Data(app private var mHebeContext: String? = null var hebeContext: String? get() { mHebeContext = mHebeContext ?: profile?.getStudentData("hebeContext", null); return mHebeContext } - set(value) { profile?.putStudentData("hebeContext", value) ?: return; mHebeContext = value } + set(value) { profile["hebeContext"] = value; mHebeContext = value } + + private var mMessageBoxKey: String? = null + var messageBoxKey: String? + get() { mMessageBoxKey = mMessageBoxKey ?: profile?.getStudentData("messageBoxKey", null); return mMessageBoxKey } + set(value) { profile["messageBoxKey"] = value; mMessageBoxKey = value } + + private var mMessageBoxName: String? = null + var messageBoxName: String? + get() { mMessageBoxName = mMessageBoxName ?: profile?.getStudentData("messageBoxName", null); return mMessageBoxName } + set(value) { profile["messageBoxName"] = value; mMessageBoxName = value } val apiUrl: String? get() { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/Vulcan.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/Vulcan.kt index d066aca0..ca7a2d21 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/Vulcan.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/Vulcan.kt @@ -7,7 +7,8 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan import com.google.gson.JsonObject import org.greenrobot.eventbus.EventBus import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.ERROR_ONEDRIVE_DOWNLOAD +import pl.szczodrzynski.edziennik.data.api.ERROR_VULCAN_API_DEPRECATED import pl.szczodrzynski.edziennik.data.api.edziennik.helper.OneDriveDownloadAttachment import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanData import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.VulcanHebeMessagesChangeStatus @@ -17,12 +18,18 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLogin import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent +import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikInterface import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.api.prepare +import pl.szczodrzynski.edziennik.data.api.prepareFor import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.data.db.enums.LoginMode import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.MessageFull @@ -59,24 +66,24 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va |_| |_| |_|\___| /_/ \_\_|\__, |\___/|_| |_|\__|_| |_|_| |_| |_| __/ | |__*/ - override fun sync(featureIds: List, viewId: Int?, onlyEndpoints: List?, arguments: JsonObject?) { + override fun sync(featureTypes: Set?, onlyEndpoints: Set?, arguments: JsonObject?) { data.arguments = arguments - data.prepare(vulcanLoginMethods, VulcanFeatures, featureIds, viewId, onlyEndpoints) + data.prepare(VulcanFeatures, featureTypes, onlyEndpoints) login() } - private fun login(loginMethodId: Int? = null, afterLogin: (() -> Unit)? = null) { - if (data.loginStore.mode == LOGIN_MODE_VULCAN_API) { + private fun login(loginMethod: LoginMethod? = null, afterLogin: (() -> Unit)? = null) { + if (data.loginStore.mode == LoginMode.VULCAN_API) { data.error(TAG, ERROR_VULCAN_API_DEPRECATED) return } - d(TAG, "Trying to login with ${data.targetLoginMethodIds}") + d(TAG, "Trying to login with ${data.targetLoginMethods}") if (internalErrorList.isNotEmpty()) { d(TAG, " - Internal errors:") internalErrorList.forEach { d(TAG, " - code $it") } } - loginMethodId?.let { data.prepareFor(vulcanLoginMethods, it) } + loginMethod?.let { data.prepareFor(it) } afterLogin?.let { this.afterLogin = it } VulcanLogin(data) { data() @@ -84,7 +91,7 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va } private fun data() { - d(TAG, "Endpoint IDs: ${data.targetEndpointIds}") + d(TAG, "Endpoint IDs: ${data.targetEndpoints}") if (internalErrorList.isNotEmpty()) { d(TAG, " - Internal errors:") internalErrorList.forEach { d(TAG, " - code $it") } @@ -95,7 +102,7 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va } override fun getMessage(message: MessageFull) { - login(LOGIN_METHOD_VULCAN_HEBE) { + login(LoginMethod.VULCAN_HEBE) { if (message.seen) { EventBus.getDefault().postSticky(MessageGetEvent(message)) completed() @@ -107,8 +114,8 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va } } - override fun sendMessage(recipients: List, subject: String, text: String) { - login(LOGIN_METHOD_VULCAN_HEBE) { + override fun sendMessage(recipients: Set, subject: String, text: String) { + login(LoginMethod.VULCAN_HEBE) { VulcanHebeSendMessage(data, recipients, subject, text) { completed() } @@ -164,6 +171,7 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va override fun getEvent(eventFull: EventFull) { eventFull.homeworkBody = "" + eventFull.isDownloaded = true EventBus.getDefault().postSticky(EventGetEvent(eventFull)) completed() @@ -178,6 +186,7 @@ class Vulcan(val app: App, val profile: Profile?, val loginStore: LoginStore, va private fun wrapCallback(callback: EdziennikCallback): EdziennikCallback { return object : EdziennikCallback { override fun onCompleted() { callback.onCompleted() } + override fun onRequiresUserAction(event: UserActionRequiredEvent) { callback.onRequiresUserAction(event) } override fun onProgress(step: Float) { callback.onProgress(step) } override fun onStartProgress(stringRes: Int) { callback.onStartProgress(stringRes) } override fun onError(apiError: ApiError) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/VulcanFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/VulcanFeatures.kt index 2e239a7e..206983a1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/VulcanFeatures.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/VulcanFeatures.kt @@ -4,14 +4,17 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan -import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.models.Feature +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.data.db.enums.LoginType const val ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS = 2010 const val ENDPOINT_VULCAN_HEBE_MAIN = 3000 const val ENDPOINT_VULCAN_HEBE_PUSH_CONFIG = 3005 const val ENDPOINT_VULCAN_HEBE_ADDRESSBOOK = 3010 +const val ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2 = 3501 // after message boxes (3500) const val ENDPOINT_VULCAN_HEBE_TIMETABLE = 3020 const val ENDPOINT_VULCAN_HEBE_EXAMS = 3030 const val ENDPOINT_VULCAN_HEBE_GRADES = 3040 @@ -19,70 +22,75 @@ const val ENDPOINT_VULCAN_HEBE_GRADE_SUMMARY = 3050 const val ENDPOINT_VULCAN_HEBE_HOMEWORK = 3060 const val ENDPOINT_VULCAN_HEBE_NOTICES = 3070 const val ENDPOINT_VULCAN_HEBE_ATTENDANCE = 3080 -const val ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX = 3090 -const val ENDPOINT_VULCAN_HEBE_MESSAGES_SENT = 3100 +const val ENDPOINT_VULCAN_HEBE_TEACHERS = 3110 const val ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER = 3200 +const val ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES = 3500 +const val ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX = 3510 +const val ENDPOINT_VULCAN_HEBE_MESSAGES_SENT = 3520 val VulcanFeatures = listOf( // timetable - Feature(LOGIN_TYPE_VULCAN, FEATURE_TIMETABLE, listOf( - ENDPOINT_VULCAN_HEBE_TIMETABLE to LOGIN_METHOD_VULCAN_HEBE - ), listOf(LOGIN_METHOD_VULCAN_HEBE)), + Feature(LoginType.VULCAN, FeatureType.TIMETABLE, listOf( + ENDPOINT_VULCAN_HEBE_TIMETABLE to LoginMethod.VULCAN_HEBE + )), // agenda - Feature(LOGIN_TYPE_VULCAN, FEATURE_AGENDA, listOf( - ENDPOINT_VULCAN_HEBE_EXAMS to LOGIN_METHOD_VULCAN_HEBE - ), listOf(LOGIN_METHOD_VULCAN_HEBE)), + Feature(LoginType.VULCAN, FeatureType.AGENDA, listOf( + ENDPOINT_VULCAN_HEBE_EXAMS to LoginMethod.VULCAN_HEBE + )), // grades - Feature(LOGIN_TYPE_VULCAN, FEATURE_GRADES, listOf( - ENDPOINT_VULCAN_HEBE_GRADES to LOGIN_METHOD_VULCAN_HEBE, - ENDPOINT_VULCAN_HEBE_GRADE_SUMMARY to LOGIN_METHOD_VULCAN_HEBE - ), listOf(LOGIN_METHOD_VULCAN_HEBE)), + Feature(LoginType.VULCAN, FeatureType.GRADES, listOf( + ENDPOINT_VULCAN_HEBE_GRADES to LoginMethod.VULCAN_HEBE, + ENDPOINT_VULCAN_HEBE_GRADE_SUMMARY to LoginMethod.VULCAN_HEBE + )), // homework - Feature(LOGIN_TYPE_VULCAN, FEATURE_HOMEWORK, listOf( - ENDPOINT_VULCAN_HEBE_HOMEWORK to LOGIN_METHOD_VULCAN_HEBE - ), listOf(LOGIN_METHOD_VULCAN_HEBE)), + Feature(LoginType.VULCAN, FeatureType.HOMEWORK, listOf( + ENDPOINT_VULCAN_HEBE_HOMEWORK to LoginMethod.VULCAN_HEBE + )), // behaviour - Feature(LOGIN_TYPE_VULCAN, FEATURE_BEHAVIOUR, listOf( - ENDPOINT_VULCAN_HEBE_NOTICES to LOGIN_METHOD_VULCAN_HEBE - ), listOf(LOGIN_METHOD_VULCAN_HEBE)), + Feature(LoginType.VULCAN, FeatureType.BEHAVIOUR, listOf( + ENDPOINT_VULCAN_HEBE_NOTICES to LoginMethod.VULCAN_HEBE + )), // attendance - Feature(LOGIN_TYPE_VULCAN, FEATURE_ATTENDANCE, listOf( - ENDPOINT_VULCAN_HEBE_ATTENDANCE to LOGIN_METHOD_VULCAN_HEBE - ), listOf(LOGIN_METHOD_VULCAN_HEBE)), + Feature(LoginType.VULCAN, FeatureType.ATTENDANCE, listOf( + ENDPOINT_VULCAN_HEBE_ATTENDANCE to LoginMethod.VULCAN_HEBE + )), // messages - Feature(LOGIN_TYPE_VULCAN, FEATURE_MESSAGES_INBOX, listOf( - ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX to LOGIN_METHOD_VULCAN_HEBE - ), listOf(LOGIN_METHOD_VULCAN_HEBE)), - Feature(LOGIN_TYPE_VULCAN, FEATURE_MESSAGES_SENT, listOf( - ENDPOINT_VULCAN_HEBE_MESSAGES_SENT to LOGIN_METHOD_VULCAN_HEBE - ), listOf(LOGIN_METHOD_VULCAN_HEBE)), + Feature(LoginType.VULCAN, FeatureType.MESSAGES_INBOX, listOf( + ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX to LoginMethod.VULCAN_HEBE + )), + Feature(LoginType.VULCAN, FeatureType.MESSAGES_SENT, listOf( + ENDPOINT_VULCAN_HEBE_MESSAGES_SENT to LoginMethod.VULCAN_HEBE + )), // push config - Feature(LOGIN_TYPE_VULCAN, FEATURE_PUSH_CONFIG, listOf( - ENDPOINT_VULCAN_HEBE_PUSH_CONFIG to LOGIN_METHOD_VULCAN_HEBE - ), listOf(LOGIN_METHOD_VULCAN_HEBE)).withShouldSync { data -> + Feature(LoginType.VULCAN, FeatureType.PUSH_CONFIG, listOf( + ENDPOINT_VULCAN_HEBE_PUSH_CONFIG to LoginMethod.VULCAN_HEBE + )).withShouldSync { data -> !data.app.config.sync.tokenVulcanList.contains(data.profileId) }, /** * Lucky number - using WEB Main. */ - Feature(LOGIN_TYPE_VULCAN, FEATURE_LUCKY_NUMBER, listOf( - ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS to LOGIN_METHOD_VULCAN_WEB_MAIN - ), listOf(LOGIN_METHOD_VULCAN_WEB_MAIN)) + Feature(LoginType.VULCAN, FeatureType.LUCKY_NUMBER, listOf( + ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS to LoginMethod.VULCAN_WEB_MAIN + )) .withShouldSync { data -> data.shouldSyncLuckyNumber() } .withPriority(2), /** * Lucky number - using Hebe API */ - Feature(LOGIN_TYPE_VULCAN, FEATURE_LUCKY_NUMBER, listOf( - ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER to LOGIN_METHOD_VULCAN_HEBE - ), listOf(LOGIN_METHOD_VULCAN_HEBE)) + Feature(LoginType.VULCAN, FeatureType.LUCKY_NUMBER, listOf( + ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER to LoginMethod.VULCAN_HEBE + )) .withShouldSync { data -> data.shouldSyncLuckyNumber() } .withPriority(1), - Feature(LOGIN_TYPE_VULCAN, FEATURE_ALWAYS_NEEDED, listOf( - ENDPOINT_VULCAN_HEBE_MAIN to LOGIN_METHOD_VULCAN_HEBE, - ENDPOINT_VULCAN_HEBE_ADDRESSBOOK to LOGIN_METHOD_VULCAN_HEBE - ), listOf(LOGIN_METHOD_VULCAN_HEBE)) + Feature(LoginType.VULCAN, FeatureType.ALWAYS_NEEDED, listOf( + ENDPOINT_VULCAN_HEBE_MAIN to LoginMethod.VULCAN_HEBE, + ENDPOINT_VULCAN_HEBE_ADDRESSBOOK to LoginMethod.VULCAN_HEBE, + ENDPOINT_VULCAN_HEBE_TEACHERS to LoginMethod.VULCAN_HEBE, + ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES to LoginMethod.VULCAN_HEBE, + ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2 to LoginMethod.VULCAN_HEBE, + )) ) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanData.kt index 0724ca46..41014919 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanData.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanData.kt @@ -21,12 +21,15 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) { ENDPOINT_VULCAN_HEBE_MAIN, ENDPOINT_VULCAN_HEBE_PUSH_CONFIG, ENDPOINT_VULCAN_HEBE_ADDRESSBOOK, + ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2, ENDPOINT_VULCAN_HEBE_TIMETABLE, ENDPOINT_VULCAN_HEBE_EXAMS, ENDPOINT_VULCAN_HEBE_HOMEWORK, ENDPOINT_VULCAN_HEBE_NOTICES, + ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES, ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX, ENDPOINT_VULCAN_HEBE_MESSAGES_SENT, + ENDPOINT_VULCAN_HEBE_TEACHERS, ENDPOINT_VULCAN_HEBE_LUCKY_NUMBER ) @@ -48,7 +51,7 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) { } private fun nextEndpoint(onSuccess: () -> Unit) { - if (data.targetEndpointIds.isEmpty()) { + if (data.targetEndpoints.isEmpty()) { onSuccess() return } @@ -56,8 +59,8 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) { onSuccess() return } - val id = data.targetEndpointIds.firstKey() - val lastSync = data.targetEndpointIds.remove(id) + val id = data.targetEndpoints.firstKey() + val lastSync = data.targetEndpoints.remove(id) useEndpoint(id, lastSync) { if (firstSemesterSync && id !in firstSemesterSyncExclude) { // sync 2nd semester after every endpoint @@ -103,9 +106,17 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) { VulcanHebePushConfig(data, lastSync, onSuccess) } ENDPOINT_VULCAN_HEBE_ADDRESSBOOK -> { - data.startProgress(R.string.edziennik_progress_endpoint_teachers) + data.startProgress(R.string.edziennik_progress_endpoint_addressbook) VulcanHebeAddressbook(data, lastSync, onSuccess) } + ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2 -> { + data.startProgress(R.string.edziennik_progress_endpoint_addressbook) + VulcanHebeAddressbook2(data, lastSync, onSuccess) + } + ENDPOINT_VULCAN_HEBE_TEACHERS -> { + data.startProgress(R.string.edziennik_progress_endpoint_teachers) + VulcanHebeTeachers(data, lastSync, onSuccess) + } ENDPOINT_VULCAN_HEBE_TIMETABLE -> { data.startProgress(R.string.edziennik_progress_endpoint_timetable) VulcanHebeTimetable(data, lastSync, onSuccess) @@ -134,6 +145,10 @@ class VulcanData(val data: DataVulcan, val onSuccess: () -> Unit) { data.startProgress(R.string.edziennik_progress_endpoint_attendance) VulcanHebeAttendance(data, lastSync, onSuccess) } + ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES -> { + data.startProgress(R.string.edziennik_progress_endpoint_messages) + VulcanHebeMessageBoxes(data, lastSync, onSuccess) + } ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX -> { data.startProgress(R.string.edziennik_progress_endpoint_messages_inbox) VulcanHebeMessages(data, lastSync, onSuccess).getMessages(Message.TYPE_RECEIVED) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanHebe.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanHebe.kt index a34e0a1b..83963340 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanHebe.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanHebe.kt @@ -14,7 +14,6 @@ import im.wangchao.mhttp.Response import im.wangchao.mhttp.body.MediaTypeUtils import im.wangchao.mhttp.callback.JsonCallbackHandler import io.github.wulkanowy.signer.hebe.getSignatureHeaders -import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe.HebeFilterType @@ -22,11 +21,12 @@ import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.db.entity.LessonRange import pl.szczodrzynski.edziennik.data.db.entity.Subject import pl.szczodrzynski.edziennik.data.db.entity.Teacher -import pl.szczodrzynski.edziennik.data.db.entity.Team +import pl.szczodrzynski.edziennik.ext.* 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 @@ -55,6 +55,15 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) { return date.getLong("Timestamp") ?: return default } + fun buildDateTime(): JsonObject { + return JsonObject( + "Timestamp" to System.currentTimeMillis(), + "Date" to Date.getToday().stringY_m_d, + "DateDisplay" to Date.getToday().stringDmy, + "Time" to Time.getNow().stringHMS, + ) + } + fun getDate(json: JsonObject?, key: String): Date? { val date = json.getJsonObject(key) return date.getString("Date")?.let { Date.fromY_m_d(it) } @@ -74,6 +83,22 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) { return teacherId } + fun getTeacherRecipient(json: JsonObject): Teacher? { + val globalKey = json.getString("GlobalKey") ?: return null + if (globalKey == data.messageBoxKey) + return null + var name = json.getString("Name") ?: return null + val group = json.getString("Group", "P") + val loginId = "${globalKey};${group};${name}" + val pattern = " - $group - (${data.schoolShort})" + if (name.endsWith(pattern)) + name = name.substringBefore(pattern) + val teacher = data.getTeacherByFirstLast(name, loginId) + if (teacher.type == 0) + teacher.type = Teacher.TYPE_OTHER + return teacher + } + fun getSubjectId(json: JsonObject?, key: String): Long? { val subject = json.getJsonObject(key) val subjectId = subject.getLong("Id") ?: return null @@ -89,42 +114,32 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) { } fun getTeamId(json: JsonObject?, key: String): Long? { - val team = json.getJsonObject(key) - val teamId = team.getLong("Id") ?: return null - if (data.teamList[teamId] == null) { - var name = team.getString("Shortcut") - ?: team.getString("Name") - ?: "" - name = "${profile?.studentClassName ?: ""} $name" - data.teamList[teamId] = Team( - data.profileId, - teamId, - name, - Team.TYPE_VIRTUAL, - "${data.schoolCode}:$name", - -1 - ) - } - return teamId + val team = json.getJsonObject(key) ?: return null + val teamId = team.getLong("Id") + var teamName = team.getString("Shortcut") + ?: team.getString("Name") + ?: "" + teamName = "${profile?.studentClassName ?: ""} $teamName" + return data.getTeam( + id = teamId, + name = teamName, + schoolCode = data.schoolCode ?: "", + isTeamClass = false, + ).id } fun getClassId(json: JsonObject?, key: String): Long? { - val team = json.getJsonObject(key) - val teamId = team.getLong("Id") ?: return null - if (data.teamList[teamId] == null) { - val name = data.profile?.studentClassName - ?: team.getString("Name") - ?: "" - data.teamList[teamId] = Team( - data.profileId, - teamId, - name, - Team.TYPE_CLASS, - "${data.schoolCode}:$name", - -1 - ) - } - return teamId + val team = json.getJsonObject(key) ?: return null + val teamId = team.getLong("Id") + val teamName = data.profile?.studentClassName + ?: team.getString("Name") + ?: "" + return data.getTeam( + id = teamId, + name = teamName, + schoolCode = data.schoolCode ?: "", + isTeamClass = true, + ).id } fun getLessonRange(json: JsonObject?, key: String): LessonRange? { @@ -158,7 +173,7 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) { fun isCurrentYear(dateTime: Long): Boolean { return profile?.let { profile -> - return@let dateTime >= profile.dateSemester1Start.inMillis + return@let dateTime >= profile.dateSemester1Start.inMillis - WEEK * MS } ?: false } @@ -169,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" @@ -281,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) @@ -324,6 +353,7 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) { query: Map = mapOf(), baseUrl: Boolean = false, firebaseToken: String? = null, + allow404: Boolean = false, crossinline onSuccess: (json: T, response: Response?) -> Unit ) { val queryPath = query.map { @@ -334,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 ) } @@ -365,8 +396,10 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) { dateTo: Date? = null, lastSync: Long? = null, folder: Int? = null, + messageBox: String? = null, params: Map = mapOf(), includeFilterType: Boolean = true, + allow404: Boolean = false, onSuccess: (data: List, response: Response?) -> Unit ) { val url = if (includeFilterType && filterType != null) @@ -388,6 +421,9 @@ open class VulcanHebe(open val data: DataVulcan, open val lastSync: Long?) { query["periodId"] = data.studentSemesterId.toString() query["pupilId"] = data.studentId.toString() } + HebeFilterType.BY_MESSAGEBOX -> { + query["box"] = messageBox ?: data.messageBoxKey ?: "" + } } if (dateFrom != null) @@ -409,8 +445,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) } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanWebMain.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanWebMain.kt index d32161a6..e1cc52e6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanWebMain.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/VulcanWebMain.kt @@ -11,11 +11,11 @@ import im.wangchao.mhttp.Request import im.wangchao.mhttp.Response import im.wangchao.mhttp.callback.TextCallbackHandler import pl.droidsonroids.jspoon.Jspoon -import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.CufsCertificate import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.models.Date import java.io.File @@ -137,15 +137,50 @@ open class VulcanWebMain(open val data: DataVulcan, open val lastSync: Long?) { } } + val schoolSymbols = mutableListOf() + val clientUrl = "://uonetplus-uczen.${data.webHost}/$symbol/" + var clientIndex = text.indexOf(clientUrl) + var count = 0 + while (clientIndex != -1 && count < 100) { + val startIndex = clientIndex + clientUrl.length + val endIndex = text.indexOfAny(charArrayOf('"', '/'), startIndex = startIndex) + val schoolSymbol = text.substring(startIndex, endIndex) + schoolSymbols += schoolSymbol + clientIndex = text.indexOf(clientUrl, startIndex = endIndex) + count++ + } + schoolSymbols.removeAll { + it.lowercase() == "default" + || !it.matches(Regexes.VULCAN_WEB_SYMBOL_VALIDATE) + } + + if (postErrors && schoolSymbols.isEmpty()) { + data.error(ApiError(TAG, ERROR_VULCAN_WEB_NO_SCHOOLS) + .withResponse(response) + .withApiResponse(text)) + return + } + data.webPermissions = data.webPermissions.toMutableMap().also { map -> val permissions = Regexes.VULCAN_WEB_PERMISSIONS.find(text)?.let { it[1] } if (permissions?.isNotBlank() == true) { - val studentId = permissions.split("|") + val json = permissions.split("|") .getOrNull(0) ?.base64DecodeToString() ?.toJsonObject() + val unitIds = json + ?.getJsonArray("Units") + ?.asJsonObjectList() + ?.filter { unit -> + unit.getString("Symbol") in schoolSymbols + } + ?.mapNotNull { it.getInt("Id") } ?: emptyList() + val studentId = json ?.getJsonArray("AuthInfos") ?.asJsonObjectList() + ?.filter { authInfo -> + authInfo.getInt("JednostkaSprawozdawczaId") in unitIds + } ?.flatMap { authInfo -> authInfo.getJsonArray("UczenIds") ?.map { it.asInt } @@ -162,30 +197,6 @@ open class VulcanWebMain(open val data: DataVulcan, open val lastSync: Long?) { map[symbol] = permissions } - val schoolSymbols = mutableListOf() - val clientUrl = "://uonetplus-uczen.${data.webHost}/$symbol/" - var clientIndex = text.indexOf(clientUrl) - var count = 0 - while (clientIndex != -1 && count < 100) { - val startIndex = clientIndex + clientUrl.length - val endIndex = text.indexOfAny(charArrayOf('"', '/'), startIndex = startIndex) - val schoolSymbol = text.substring(startIndex, endIndex) - schoolSymbols += schoolSymbol - clientIndex = text.indexOf(clientUrl, startIndex = endIndex) - count++ - } - schoolSymbols.removeAll { - it.toLowerCase() == "default" - || !it.matches(Regexes.VULCAN_WEB_SYMBOL_VALIDATE) - } - - if (postErrors && schoolSymbols.isEmpty()) { - data.error(ApiError(TAG, ERROR_VULCAN_WEB_NO_SCHOOLS) - .withResponse(response) - .withApiResponse(text)) - return - } - onSuccess(text, schoolSymbols) } @@ -280,7 +291,7 @@ open class VulcanWebMain(open val data: DataVulcan, open val lastSync: Long?) { return try { - val json = JsonParser().parse(text).asJsonObject + val json = JsonParser.parseString(text).asJsonObject onSuccess(json, response) } catch (e: Exception) { data.error(ApiError(tag, EXCEPTION_VULCAN_WEB_REQUEST) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/HebeFilterType.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/HebeFilterType.kt index a4ea84b1..bd8b58e5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/HebeFilterType.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/HebeFilterType.kt @@ -5,6 +5,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe enum class HebeFilterType(val endpoint: String) { + BY_MESSAGEBOX("byBox"), BY_PUPIL("byPupil"), BY_PERSON("byPerson"), BY_PERIOD("byPeriod") diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAddressbook.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAddressbook.kt index 5069838e..5893486b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAddressbook.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAddressbook.kt @@ -5,6 +5,7 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe import androidx.core.util.set +import androidx.room.OnConflictStrategy import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_ADDRESSBOOK import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan @@ -17,7 +18,8 @@ import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_PARENT import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_PARENTS_COUNCIL import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_STUDENT import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_TEACHER -import kotlin.text.replace +import pl.szczodrzynski.edziennik.ext.* +import java.net.HttpURLConnection.HTTP_NOT_FOUND class VulcanHebeAddressbook( override val data: DataVulcan, @@ -40,11 +42,17 @@ 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 - val loginId = person.getString("LoginId") ?: return@forEach val idType = id.split("-") .getOrNull(0) @@ -69,7 +77,7 @@ class VulcanHebeAddressbook( idLong, name, surname, - loginId + null ).also { data.teacherList[idLong] = it } @@ -108,13 +116,14 @@ class VulcanHebeAddressbook( } teacher.setTeacherType(personType) - teacher.typeDescription = roleText + if (roleText != null) + teacher.typeDescription = roleText } if (teacher.type == 0) teacher.setTeacherType(typeBase) } - + data.teacherOnConflictStrategy = OnConflictStrategy.REPLACE data.setSyncNext(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK, 2 * DAY) onSuccess(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAddressbook2.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAddressbook2.kt new file mode 100644 index 00000000..2ac843a2 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAddressbook2.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-2-21. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe + +import androidx.room.OnConflictStrategy +import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGEBOX_ADDRESSBOOK +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2 +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe +import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_OTHER +import pl.szczodrzynski.edziennik.data.db.entity.Teacher.Companion.TYPE_PARENT +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.DAY +import pl.szczodrzynski.edziennik.ext.MINUTE +import pl.szczodrzynski.edziennik.ext.getString + +class VulcanHebeAddressbook2( + override val data: DataVulcan, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit +) : VulcanHebe(data, lastSync) { + companion object { + const val TAG = "VulcanHebeAddressbook2" + } + + init { let { + if (data.messageBoxKey == null) { + data.setSyncNext(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2, 30 * MINUTE) + onSuccess(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2) + return@let + } + + apiGetList( + TAG, + VULCAN_HEBE_ENDPOINT_MESSAGEBOX_ADDRESSBOOK, + HebeFilterType.BY_MESSAGEBOX, + messageBox = data.messageBoxKey, + lastSync = lastSync, + includeFilterType = false + ) { list, _ -> + list.forEach { person -> + val teacher = getTeacherRecipient(person) ?: return@forEach + val group = person.getString("Group", "P") + if (teacher.type == TYPE_OTHER) { + teacher.type = when (group) { + "P" -> TYPE_TEACHER // Pracownik + "O" -> TYPE_PARENT // Opiekun + "U" -> TYPE_STUDENT // Uczeń + else -> TYPE_OTHER + } + } + } + data.teacherOnConflictStrategy = OnConflictStrategy.REPLACE + data.setSyncNext(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2, 2 * DAY) + onSuccess(ENDPOINT_VULCAN_HEBE_ADDRESSBOOK_2) + } + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAttendance.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAttendance.kt index 9e41bb6d..d2a17039 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAttendance.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeAttendance.kt @@ -13,6 +13,8 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe import pl.szczodrzynski.edziennik.data.db.entity.Attendance import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.* class VulcanHebeAttendance( override val data: DataVulcan, @@ -38,7 +40,7 @@ class VulcanHebeAttendance( lastSync = lastSync ) { list, _ -> list.forEach { attendance -> - val id = attendance.getLong("AuxPresenceId") ?: return@forEach + val id = attendance.getLong("Id") ?: return@forEach val type = attendance.getJsonObject("PresenceType") ?: return@forEach val baseType = getBaseType(type) val typeName = type.getString("Name") ?: return@forEach @@ -95,7 +97,7 @@ class VulcanHebeAttendance( data.metadataList.add( Metadata( profileId, - Metadata.TYPE_ATTENDANCE, + MetadataType.ATTENDANCE, attendanceObject.id, profile?.empty ?: true || baseType == Attendance.TYPE_PRESENT_CUSTOM diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeExams.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeExams.kt index 64b52e2b..70ccc1f9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeExams.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeExams.kt @@ -12,6 +12,9 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.getLong +import pl.szczodrzynski.edziennik.ext.getString class VulcanHebeExams( override val data: DataVulcan, @@ -69,7 +72,7 @@ class VulcanHebeExams( data.metadataList.add( Metadata( profileId, - Metadata.TYPE_EVENT, + MetadataType.EVENT, id, profile?.empty ?: true, profile?.empty ?: true diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeGradeSummary.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeGradeSummary.kt index e38ac393..a2f561dc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeGradeSummary.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeGradeSummary.kt @@ -4,14 +4,15 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe -import pl.szczodrzynski.edziennik.DAY import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_GRADE_SUMMARY import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_GRADE_SUMMARY import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe import pl.szczodrzynski.edziennik.data.db.entity.Grade import pl.szczodrzynski.edziennik.data.db.entity.Metadata -import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.DAY +import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.utils.Utils class VulcanHebeGradeSummary( @@ -72,7 +73,7 @@ class VulcanHebeGradeSummary( data.metadataList.add( Metadata( profileId, - Metadata.TYPE_GRADE, + MetadataType.GRADE, id, profile?.empty ?: true, profile?.empty ?: true diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeGrades.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeGrades.kt index 9c0591de..fc53d8b2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeGrades.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeGrades.kt @@ -12,6 +12,8 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe import pl.szczodrzynski.edziennik.data.db.entity.Grade import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.* import java.text.DecimalFormat import kotlin.math.roundToInt @@ -110,7 +112,7 @@ class VulcanHebeGrades( data.metadataList.add( Metadata( profileId, - Metadata.TYPE_GRADE, + MetadataType.GRADE, id, profile?.empty ?: true, profile?.empty ?: true diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeHomework.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeHomework.kt index cb792e2c..6b336b47 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeHomework.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeHomework.kt @@ -4,7 +4,6 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe -import pl.szczodrzynski.edziennik.asJsonObjectList import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_HOMEWORK import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_HOMEWORK @@ -12,9 +11,11 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS -import pl.szczodrzynski.edziennik.getJsonArray -import pl.szczodrzynski.edziennik.getLong -import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.asJsonObjectList +import pl.szczodrzynski.edziennik.ext.getJsonArray +import pl.szczodrzynski.edziennik.ext.getLong +import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.utils.Utils class VulcanHebeHomework( @@ -79,7 +80,7 @@ class VulcanHebeHomework( data.metadataList.add( Metadata( profileId, - Metadata.TYPE_HOMEWORK, + MetadataType.HOMEWORK, id, profile?.empty ?: true, profile?.empty ?: true diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeLuckyNumber.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeLuckyNumber.kt index 8ff1631b..993c1e30 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeLuckyNumber.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeLuckyNumber.kt @@ -12,8 +12,9 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe import pl.szczodrzynski.edziennik.data.db.entity.LuckyNumber import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS -import pl.szczodrzynski.edziennik.getInt -import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.getInt +import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Week @@ -57,7 +58,7 @@ class VulcanHebeLuckyNumber( data.metadataList.add( Metadata( profileId, - Metadata.TYPE_LUCKY_NUMBER, + MetadataType.LUCKY_NUMBER, luckyNumberObject.date.value.toLong(), true, profile?.empty ?: false diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMain.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMain.kt index c017e3c9..b729fd9c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMain.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMain.kt @@ -5,13 +5,13 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe import com.google.gson.JsonArray -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_VULCAN import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MAIN import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_MAIN import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.enums.LoginType +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.models.Date class VulcanHebeMain( @@ -97,6 +97,10 @@ class VulcanHebeMain( val studentSemesterId = period.getInt("Id") ?: return@forEach val studentSemesterNumber = period.getInt("Number") ?: return@forEach + val senderEntry = student.getJsonObject("SenderEntry") + val senderAddressName = senderEntry.getString("Address") + val senderAddressHash = senderEntry.getString("AddressHash") + val hebeContext = student.getString("Context") val isParent = login.getString("LoginRole").equals("opiekun", ignoreCase = true) @@ -120,7 +124,7 @@ class VulcanHebeMain( val newProfile = profile ?: Profile( profileId++, loginStoreId!!, - LOGIN_TYPE_VULCAN, + LoginType.VULCAN, studentNameLong, userLogin, studentNameLong, @@ -143,7 +147,18 @@ class VulcanHebeMain( studentData["schoolSymbol"] = schoolSymbol studentData["schoolShort"] = schoolShort studentData["schoolName"] = schoolCode + studentData["senderAddressName"] = senderAddressName + studentData["senderAddressHash"] = senderAddressHash studentData["hebeContext"] = hebeContext + + // create the default TeamClass + data.getTeam( + id = null, + name = studentClassName, + schoolCode = schoolCode, + isTeamClass = true, + profileId = this.id, + ) } dateSemester1Start?.let { newProfile.dateSemester1Start = it diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessageBoxes.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessageBoxes.kt new file mode 100644 index 00000000..df6613ba --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessageBoxes.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-9-16. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe + +import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGEBOX +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe +import pl.szczodrzynski.edziennik.ext.DAY +import pl.szczodrzynski.edziennik.ext.getString + +class VulcanHebeMessageBoxes( + override val data: DataVulcan, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit +) : VulcanHebe(data, lastSync) { + companion object { + const val TAG = "VulcanHebeMessageBoxes" + } + + init { + apiGetList( + TAG, + VULCAN_HEBE_ENDPOINT_MESSAGEBOX, + lastSync = lastSync + ) { list, _ -> + var found = false + for (messageBox in list) { + val name = messageBox.getString("Name") ?: continue + val studentName = profile?.studentNameLong ?: continue + if (!name.contains(studentName)) + continue + + data.messageBoxKey = messageBox.getString("GlobalKey") + data.messageBoxName = name + found = true + break + } + if (!found && list.isNotEmpty()) { + list.firstOrNull()?.let { messageBox -> + data.messageBoxKey = messageBox.getString("GlobalKey") + data.messageBoxName = messageBox.getString("Name") + } + } + data.setSyncNext(ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES, 7 * DAY) + onSuccess(ENDPOINT_VULCAN_HEBE_MESSAGE_BOXES) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessages.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessages.kt index eb4091e0..8a48f65c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessages.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessages.kt @@ -4,22 +4,22 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe -import androidx.core.util.set -import com.google.gson.JsonObject -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES -import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES +import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGEBOX_MESSAGES import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_MESSAGES_SENT import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe -import pl.szczodrzynski.edziennik.data.db.entity.* +import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_DELETED import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT +import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient +import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.Utils -import pl.szczodrzynski.navlib.crc16 -import kotlin.text.replace class VulcanHebeMessages( override val data: DataVulcan, @@ -27,29 +27,7 @@ class VulcanHebeMessages( val onSuccess: (endpointId: Int) -> Unit ) : VulcanHebe(data, lastSync) { companion object { - const val TAG = "VulcanHebeMessagesInbox" - } - - private fun getPersonId(json: JsonObject): Long { - val senderLoginId = json.getInt("LoginId") ?: return -1 - /*if (senderLoginId == data.studentLoginId) - return -1*/ - - val senderName = json.getString("Address") ?: return -1 - val senderNameSplit = senderName.splitName() - val senderLoginIdStr = senderLoginId.toString() - val teacher = data.teacherList.singleOrNull { it.loginId == senderLoginIdStr } - ?: Teacher( - profileId, - -1 * crc16(senderName).toLong(), - senderNameSplit?.second ?: "", - senderNameSplit?.first ?: "", - senderLoginIdStr - ).also { - it.setTeacherType(Teacher.TYPE_OTHER) - data.teacherList[it.id] = it - } - return teacher.id + const val TAG = "VulcanHebeMessages" } fun getMessages(messageType: Int) { @@ -64,17 +42,28 @@ class VulcanHebeMessages( TYPE_SENT -> ENDPOINT_VULCAN_HEBE_MESSAGES_SENT else -> ENDPOINT_VULCAN_HEBE_MESSAGES_INBOX } + + val messageBox = data.messageBoxKey + if (messageBox == null) { + onSuccess(endpointId) + return + } + apiGetList( TAG, - VULCAN_HEBE_ENDPOINT_MESSAGES, - HebeFilterType.BY_PERSON, + VULCAN_HEBE_ENDPOINT_MESSAGEBOX_MESSAGES, + HebeFilterType.BY_MESSAGEBOX, + messageBox = data.messageBoxKey, folder = folder, lastSync = lastSync ) { list, _ -> list.forEach { message -> - val id = message.getLong("Id") ?: return@forEach + val uuid = message.getString("Id") ?: return@forEach + val id = Utils.crc32(uuid.toByteArray()) + val globalKey = message.getString("GlobalKey", "") + val threadKey = message.getString("ThreadKey", "") val subject = message.getString("Subject") ?: return@forEach - val body = message.getString("Content") ?: return@forEach + var body = message.getString("Content") ?: return@forEach val sender = message.getJsonObject("Sender") ?: return@forEach @@ -83,27 +72,45 @@ class VulcanHebeMessages( if (!isCurrentYear(sentDate)) return@forEach + val senderId = if (messageType == TYPE_RECEIVED) + getTeacherRecipient(sender)?.id + else + null + + val meta = mutableMapOf( + "uuid" to uuid, + "globalKey" to globalKey, + "threadKey" to threadKey, + ) + val metaString = meta.map { "${it.key}=${it.value}" }.join("&") + body = "[META:${metaString}]" + body + body = body.replace("\n", "
    ") + val messageObject = Message( profileId = profileId, id = id, type = messageType, subject = subject, - body = body.replace("\n", "
    "), - senderId = if (messageType == TYPE_RECEIVED) getPersonId(sender) else null, + body = body, + senderId = senderId, addedDate = sentDate ) val receivers = message.getJsonArray("Receiver") ?.asJsonObjectList() ?: return@forEach - val receiverReadDate = - if (receivers.size == 1) readDate - else -1 for (receiver in receivers) { + val recipientId = if (messageType == TYPE_SENT) + getTeacherRecipient(receiver)?.id ?: -1 + else + -1 + + val receiverReadDate = receiver.getLong("HasRead", -1) + val messageRecipientObject = MessageRecipient( profileId, - if (messageType == TYPE_SENT) getPersonId(receiver) else -1, + recipientId, -1, receiverReadDate, id @@ -115,6 +122,9 @@ class VulcanHebeMessages( ?.asJsonObjectList() ?: return@forEach + messageObject.attachmentIds = mutableListOf() + messageObject.attachmentNames = mutableListOf() + messageObject.attachmentSizes = mutableListOf() for (attachment in attachments) { val fileName = attachment.getString("Name") ?: continue val url = attachment.getString("Link") ?: continue @@ -132,7 +142,7 @@ class VulcanHebeMessages( data.setSeenMetadataList.add( Metadata( profileId, - Metadata.TYPE_MESSAGE, + MetadataType.MESSAGE, id, readDate > 0 || messageType == TYPE_SENT, readDate > 0 || messageType == TYPE_SENT @@ -143,7 +153,7 @@ class VulcanHebeMessages( data.setSyncNext( endpointId, if (messageType == TYPE_RECEIVED) SYNC_ALWAYS else 1 * DAY, - if (messageType == TYPE_RECEIVED) null else DRAWER_ITEM_MESSAGES + if (messageType == TYPE_RECEIVED) null else FeatureType.MESSAGES_SENT ) onSuccess(endpointId) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessagesChangeStatus.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessagesChangeStatus.kt index ff5dbe4c..bc8e9e9c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessagesChangeStatus.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeMessagesChangeStatus.kt @@ -5,15 +5,15 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe import org.greenrobot.eventbus.EventBus -import pl.szczodrzynski.edziennik.JsonObject -import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES_STATUS +import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGEBOX_STATUS import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent -import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.data.db.full.MessageFull +import pl.szczodrzynski.edziennik.ext.JsonObject class VulcanHebeMessagesChangeStatus( override val data: DataVulcan, @@ -24,13 +24,19 @@ class VulcanHebeMessagesChangeStatus( const val TAG = "VulcanHebeMessagesChangeStatus" } - init { + init { let { + val messageKey = messageObject.body?.let { data.parseMessageMeta(it) }?.get("uuid") ?: run { + EventBus.getDefault().postSticky(MessageGetEvent(messageObject)) + onSuccess() + return@let + } + apiPost( TAG, - VULCAN_HEBE_ENDPOINT_MESSAGES_STATUS, + VULCAN_HEBE_ENDPOINT_MESSAGEBOX_STATUS, payload = JsonObject( - "MessageId" to messageObject.id, - "LoginId" to data.studentLoginId, + "BoxKey" to data.messageBoxKey, + "MessageKey" to messageKey, "Status" to 1 ) ) { _: Boolean, _ -> @@ -39,7 +45,7 @@ class VulcanHebeMessagesChangeStatus( data.setSeenMetadataList.add( Metadata( profileId, - Metadata.TYPE_MESSAGE, + MetadataType.MESSAGE, messageObject.id, true, true @@ -48,7 +54,7 @@ class VulcanHebeMessagesChangeStatus( messageObject.seen = true } - if (messageObject.type != Message.TYPE_SENT) { + if (!messageObject.isSent) { val messageRecipientObject = MessageRecipient( profileId, -1, @@ -62,5 +68,5 @@ class VulcanHebeMessagesChangeStatus( EventBus.getDefault().postSticky(MessageGetEvent(messageObject)) onSuccess() } - } + }} } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeNotices.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeNotices.kt index 5b67b272..6e8d9176 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeNotices.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeNotices.kt @@ -12,6 +12,8 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Notice import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.* class VulcanHebeNotices( override val data: DataVulcan, @@ -62,7 +64,7 @@ class VulcanHebeNotices( data.metadataList.add( Metadata( profileId, - Metadata.TYPE_NOTICE, + MetadataType.NOTICE, id, profile?.empty ?: true, profile?.empty ?: true diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeSendMessage.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeSendMessage.kt index 8f5f5afa..19d8f0a2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeSendMessage.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeSendMessage.kt @@ -4,20 +4,24 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe +import com.google.gson.JsonArray import com.google.gson.JsonObject import org.greenrobot.eventbus.EventBus import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGES_SEND +import pl.szczodrzynski.edziennik.data.api.ERROR_MESSAGE_NOT_SENT +import pl.szczodrzynski.edziennik.data.api.ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY +import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_MESSAGEBOX_SEND import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent import pl.szczodrzynski.edziennik.data.db.entity.Message -import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.ext.* +import java.util.UUID class VulcanHebeSendMessage( override val data: DataVulcan, - val recipients: List, + val recipients: Set, val subject: String, val text: String, val onSuccess: () -> Unit @@ -27,51 +31,84 @@ class VulcanHebeSendMessage( } init { + if (data.messageBoxKey == null || data.messageBoxName == null) { + VulcanHebeMessageBoxes(data, 0) { + if (data.messageBoxKey == null || data.messageBoxName == null) { + data.error(TAG, ERROR_VULCAN_HEBE_MISSING_SENDER_ENTRY) + } + else { + sendMessage() + } + } + } + else { + sendMessage() + } + } + + private fun sendMessage() { + val uuid = UUID.randomUUID().toString() + val globalKey = UUID.randomUUID().toString() + val partition = "${data.symbol}-${data.schoolSymbol}" + val recipientsArray = JsonArray() recipients.forEach { teacher -> + val loginId = teacher.loginId?.split(";", limit = 3) ?: return@forEach + val key = loginId.getOrNull(0) ?: teacher.loginId + val group = loginId.getOrNull(1) + val name = loginId.getOrNull(2) + if (key?.toIntOrNull() != null) { + // raise error for old-format (non-UUID) login IDs + data.error(TAG, ERROR_MESSAGE_NOT_SENT) + return + } recipientsArray += JsonObject( - "Address" to teacher.fullNameLastFirst, - "LoginId" to (teacher.loginId?.toIntOrNull() ?: return@forEach), - "Initials" to teacher.initialsLastFirst, - "AddressHash" to teacher.fullNameLastFirst.sha1Hex() + "Id" to "${data.messageBoxKey}-${key}", + "Partition" to partition, + "Owner" to data.messageBoxKey, + "GlobalKey" to key, + "Name" to name, + "Group" to group, + "Initials" to "", + "HasRead" to 0, ) } - val senderName = (profile?.accountName ?: profile?.studentNameLong) - ?.swapFirstLastName() ?: "" val sender = JsonObject( - "Address" to senderName, - "LoginId" to data.studentLoginId.toString(), - "Initials" to senderName.getNameInitials(), - "AddressHash" to senderName.sha1Hex() + "Id" to "0", + "Partition" to partition, + "Owner" to data.messageBoxKey, + "GlobalKey" to data.messageBoxKey, + "Name" to data.messageBoxName, + "Group" to "", + "Initials" to "", + "HasRead" to 0, ) apiPost( TAG, - VULCAN_HEBE_ENDPOINT_MESSAGES_SEND, + VULCAN_HEBE_ENDPOINT_MESSAGEBOX_SEND, payload = JsonObject( - "Status" to 1, - "Sender" to sender, - "DateSent" to null, - "DateRead" to null, - "Content" to text, - "Receiver" to recipientsArray, - "Id" to 0, + "Id" to uuid, + "GlobalKey" to globalKey, + "Partition" to partition, + "ThreadKey" to globalKey, // TODO correct threadKey for reply messages "Subject" to subject, - "Attachments" to null, - "Self" to null + "Content" to text, + "Status" to 1, + "Owner" to data.messageBoxKey, + "DateSent" to buildDateTime(), + "DateRead" to null, + "Sender" to sender, + "Receiver" to recipientsArray, + "Attachments" to JsonArray(), ) - ) { json: JsonObject, _ -> - val messageId = json.getLong("Id") - - if (messageId == null) { - // TODO error - return@apiPost - } + ) { _: JsonObject, _ -> + // TODO handle errors VulcanHebeMessages(data, null) { - val message = data.messageList.firstOrNull { it.type == Message.TYPE_SENT && it.subject == subject } - val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == messageId } + val message = data.messageList.firstOrNull { it.isSent && it.subject == subject } + // val metadata = data.metadataList.firstOrNull { it.thingType == Metadata.TYPE_MESSAGE && it.thingId == messageId } val event = MessageSentEvent(data.profileId, message, message?.addedDate) EventBus.getDefault().postSticky(event) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeTeachers.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeTeachers.kt new file mode 100644 index 00000000..7be65e34 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeTeachers.kt @@ -0,0 +1,61 @@ +/* + * Copyright (c) Antoni Czaplicki 2021-10-15. + */ + +package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.hebe + +import androidx.room.OnConflictStrategy +import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_TEACHERS +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_TEACHERS +import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe +import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.ext.DAY +import pl.szczodrzynski.edziennik.ext.getString + +class VulcanHebeTeachers( + override val data: DataVulcan, + override val lastSync: Long?, + val onSuccess: (endpointId: Int) -> Unit, +) : VulcanHebe(data, lastSync) { + companion object { + const val TAG = "VulcanHebeTeachers" + } + + init { + apiGetList( + TAG, + VULCAN_HEBE_ENDPOINT_TEACHERS, + HebeFilterType.BY_PERIOD, + lastSync = 0L, + ) { list, _ -> + list.forEach { person -> + val name = person.getString("Name") + val surname = person.getString("Surname") + val displayName = person.getString("DisplayName") + val subjectName = person.getString("Description") ?: return@forEach + + if (subjectName.isBlank()) { + return@forEach + } + + val teacher = data.getTeacherByFirstLast( + name?.plus(" ")?.plus(surname) ?: displayName ?: return@forEach + ) + + when (subjectName) { + "Pedagog" -> teacher.setTeacherType(Teacher.TYPE_PEDAGOGUE) + "Dyrektor" -> teacher.setTeacherType(Teacher.TYPE_PRINCIPAL) + else -> { + val subjectId = data.getSubject(null, subjectName).id + if (!teacher.subjects.contains(subjectId)) + teacher.addSubject(subjectId) + } + } + } + data.teacherOnConflictStrategy = OnConflictStrategy.REPLACE + data.setSyncNext(ENDPOINT_VULCAN_HEBE_TEACHERS, 2 * DAY) + onSuccess(ENDPOINT_VULCAN_HEBE_TEACHERS) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeTimetable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeTimetable.kt index 03a28a9e..e3c3348b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeTimetable.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/hebe/VulcanHebeTimetable.kt @@ -11,6 +11,7 @@ import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_TIMETABLE_CHANGE import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_HEBE_TIMETABLE import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe +import pl.szczodrzynski.edziennik.data.api.models.DataRemoveModel import pl.szczodrzynski.edziennik.data.db.entity.Lesson import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_CANCELLED import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_CHANGE @@ -19,6 +20,11 @@ import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_SHIFTED_S import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_SHIFTED_TARGET import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.getBoolean +import pl.szczodrzynski.edziennik.ext.getInt +import pl.szczodrzynski.edziennik.ext.getJsonObject +import pl.szczodrzynski.edziennik.ext.getString import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Week @@ -47,7 +53,7 @@ class VulcanHebeTimetable( ?: previousWeekStart val dateTo = dateFrom.clone().stepForward(0, 0, 13) - val lastSync = null + val lastSync = 0L apiGetList( TAG, @@ -106,6 +112,8 @@ class VulcanHebeTimetable( "Clearing lessons between ${dateFrom.stringY_m_d} and ${dateTo.stringY_m_d}" ) + data.toRemove.add(DataRemoveModel.Timetable.between(dateFrom, dateTo)) + data.lessonList.addAll(lessonList) data.setSyncNext(ENDPOINT_VULCAN_HEBE_TIMETABLE, SYNC_ALWAYS) @@ -230,7 +238,7 @@ class VulcanHebeTimetable( data.metadataList.add( Metadata( profileId, - Metadata.TYPE_LESSON_CHANGE, + MetadataType.LESSON_CHANGE, lesson.id, seen, seen @@ -239,7 +247,9 @@ class VulcanHebeTimetable( } lessonObject.id = lessonObject.buildId() + lessonObject.ownerId = lessonObject.buildOwnerId() lessonShift?.id = lessonShift?.buildId() ?: -1 + lessonShift?.ownerId = lessonShift?.buildOwnerId() ?: -1 lessonList.add(lessonObject) lessonShift?.let { lessonList.add(it) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/web/VulcanWebLuckyNumber.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/web/VulcanWebLuckyNumber.kt index cbf24c2a..9dc707f5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/web/VulcanWebLuckyNumber.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/data/web/VulcanWebLuckyNumber.kt @@ -4,7 +4,6 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.web -import pl.szczodrzynski.edziennik.DAY import pl.szczodrzynski.edziennik.data.api.VULCAN_WEB_ENDPOINT_LUCKY_NUMBER import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS @@ -12,7 +11,9 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanWebMain import pl.szczodrzynski.edziennik.data.db.entity.LuckyNumber import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.SYNC_ALWAYS -import pl.szczodrzynski.edziennik.getJsonArray +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.DAY +import pl.szczodrzynski.edziennik.ext.getJsonArray import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Week @@ -56,18 +57,18 @@ class VulcanWebLuckyNumber(override val data: DataVulcan, data.metadataList.add( Metadata( profileId, - Metadata.TYPE_LUCKY_NUMBER, + MetadataType.LUCKY_NUMBER, luckyNumberObject.date.value.toLong(), true, profile?.empty ?: false )) } - } ?: { + } ?: run { // no lucky number if (Date.getToday().weekDay <= Week.FRIDAY && Time.getNow().hour >= 22) { // working days, after 10PM // consider the lucky number is disabled; sync in 4 days - nextSync = System.currentTimeMillis() + 4*DAY*1000 + nextSync = System.currentTimeMillis() + 4* DAY *1000 } else if (Date.getToday().weekDay <= Week.FRIDAY && Time.getNow().hour < 22) { // working days, before 10PM @@ -77,7 +78,7 @@ class VulcanWebLuckyNumber(override val data: DataVulcan, // weekends nextSync = Week.getNearestWeekDayDate(Week.MONDAY).combineWith(Time(5, 0, 0)) } - }() + } data.setSyncNext(ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS, SYNC_ALWAYS) onSuccess(ENDPOINT_VULCAN_WEB_LUCKY_NUMBERS) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/firstlogin/VulcanFirstLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/firstlogin/VulcanFirstLogin.kt index 2bf633f8..16d7a569 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/firstlogin/VulcanFirstLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/firstlogin/VulcanFirstLogin.kt @@ -17,6 +17,9 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login.VulcanLoginWeb import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.enums.LoginMode +import pl.szczodrzynski.edziennik.ext.getJsonObject +import pl.szczodrzynski.edziennik.ext.getString class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) { companion object { @@ -31,7 +34,7 @@ class VulcanFirstLogin(val data: DataVulcan, val onSuccess: () -> Unit) { private val tryingSymbols = mutableListOf() init { - if (data.loginStore.mode == LOGIN_MODE_VULCAN_WEB) { + if (data.loginStore.mode == LoginMode.VULCAN_WEB) { VulcanLoginWebMain(data) { val xml = web.readCertificate() ?: run { data.error(ApiError(TAG, ERROR_VULCAN_WEB_NO_CERTIFICATE)) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLogin.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLogin.kt index 3cbb7ea7..dc57acc9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLogin.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLogin.kt @@ -5,9 +5,8 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_HEBE -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_VULCAN_WEB_MAIN import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod import pl.szczodrzynski.edziennik.utils.Utils class VulcanLogin(val data: DataVulcan, val onSuccess: () -> Unit) { @@ -22,7 +21,7 @@ class VulcanLogin(val data: DataVulcan, val onSuccess: () -> Unit) { } private fun nextLoginMethod(onSuccess: () -> Unit) { - if (data.targetLoginMethodIds.isEmpty()) { + if (data.targetLoginMethods.isEmpty()) { onSuccess() return } @@ -30,30 +29,31 @@ class VulcanLogin(val data: DataVulcan, val onSuccess: () -> Unit) { onSuccess() return } - useLoginMethod(data.targetLoginMethodIds.removeAt(0)) { usedMethodId -> + useLoginMethod(data.targetLoginMethods.removeAt(0)) { usedMethod -> data.progress(data.progressStep) - if (usedMethodId != -1) - data.loginMethods.add(usedMethodId) + if (usedMethod != null) + data.loginMethods.add(usedMethod) nextLoginMethod(onSuccess) } } - private fun useLoginMethod(loginMethodId: Int, onSuccess: (usedMethodId: Int) -> Unit) { + private fun useLoginMethod(loginMethod: LoginMethod, onSuccess: (usedMethod: LoginMethod?) -> Unit) { // this should never be true - if (data.loginMethods.contains(loginMethodId)) { - onSuccess(-1) + if (data.loginMethods.contains(loginMethod)) { + onSuccess(null) return } - Utils.d(TAG, "Using login method $loginMethodId") - when (loginMethodId) { - LOGIN_METHOD_VULCAN_WEB_MAIN -> { + Utils.d(TAG, "Using login method $loginMethod") + when (loginMethod) { + LoginMethod.VULCAN_WEB_MAIN -> { data.startProgress(R.string.edziennik_progress_login_vulcan_web_main) - VulcanLoginWebMain(data) { onSuccess(loginMethodId) } + VulcanLoginWebMain(data) { onSuccess(loginMethod) } } - LOGIN_METHOD_VULCAN_HEBE -> { + LoginMethod.VULCAN_HEBE -> { data.startProgress(R.string.edziennik_progress_login_vulcan_api) - VulcanLoginHebe(data) { onSuccess(loginMethodId) } + VulcanLoginHebe(data) { onSuccess(loginMethod) } } + else -> {} } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginHebe.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginHebe.kt index ded6e937..241f249a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginHebe.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginHebe.kt @@ -6,7 +6,6 @@ package pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.login import com.google.gson.JsonObject import io.github.wulkanowy.signer.hebe.generateKeyPair -import pl.szczodrzynski.edziennik.JsonObject import pl.szczodrzynski.edziennik.data.api.ERROR_LOGIN_DATA_MISSING import pl.szczodrzynski.edziennik.data.api.VULCAN_API_DEVICE_NAME import pl.szczodrzynski.edziennik.data.api.VULCAN_HEBE_ENDPOINT_REGISTER_NEW @@ -14,8 +13,9 @@ import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanHebe import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi -import pl.szczodrzynski.edziennik.getString -import pl.szczodrzynski.edziennik.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.ext.JsonObject +import pl.szczodrzynski.edziennik.ext.getString +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty class VulcanLoginHebe(val data: DataVulcan, val onSuccess: () -> Unit) { companion object { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginWebMain.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginWebMain.kt index 2b62eb14..3160ceb2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginWebMain.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/edziennik/vulcan/login/VulcanLoginWebMain.kt @@ -9,8 +9,8 @@ import pl.szczodrzynski.edziennik.data.api.* import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.DataVulcan import pl.szczodrzynski.edziennik.data.api.edziennik.vulcan.data.VulcanWebMain import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.getString -import pl.szczodrzynski.edziennik.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.ext.getString +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.fslogin.FSLogin import pl.szczodrzynski.fslogin.realm.toRealm diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/RegisterAvailabilityEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/RegisterAvailabilityEvent.kt index fe66c4b3..2a3531df 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/RegisterAvailabilityEvent.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/RegisterAvailabilityEvent.kt @@ -4,8 +4,4 @@ package pl.szczodrzynski.edziennik.data.api.events -import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus - -data class RegisterAvailabilityEvent( - val data: Map< String, RegisterAvailabilityStatus> -) +class RegisterAvailabilityEvent() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/UserActionRequiredEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/UserActionRequiredEvent.kt index d88fa652..3995842a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/UserActionRequiredEvent.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/events/UserActionRequiredEvent.kt @@ -4,13 +4,16 @@ package pl.szczodrzynski.edziennik.data.api.events -data class UserActionRequiredEvent(val profileId: Int, val type: Int) { - companion object { - const val LOGIN_DATA_MOBIDZIENNIK = 101 - const val LOGIN_DATA_LIBRUS = 102 - const val LOGIN_DATA_IDZIENNIK = 103 - const val LOGIN_DATA_VULCAN = 104 - const val LOGIN_DATA_EDUDZIENNIK = 105 - const val CAPTCHA_LIBRUS = 202 +import android.os.Bundle + +data class UserActionRequiredEvent( + val profileId: Int?, + val type: Type, + val params: Bundle, + val errorText: Int, +) { + enum class Type { + RECAPTCHA, + OAUTH, } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikCallback.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikCallback.kt index c1c878b4..47c85096 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikCallback.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikCallback.kt @@ -4,8 +4,8 @@ package pl.szczodrzynski.edziennik.data.api.interfaces +import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.models.Feature -import pl.szczodrzynski.edziennik.data.api.models.LoginMethod /** * A callback passed only to an e-register class. @@ -14,4 +14,5 @@ import pl.szczodrzynski.edziennik.data.api.models.LoginMethod */ interface EdziennikCallback : EndpointCallback { fun onCompleted() + fun onRequiresUserAction(event: UserActionRequiredEvent) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikInterface.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikInterface.kt index 153bbe18..617e3336 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikInterface.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EdziennikInterface.kt @@ -6,14 +6,15 @@ package pl.szczodrzynski.edziennik.data.api.interfaces import com.google.gson.JsonObject import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.MessageFull interface EdziennikInterface { - fun sync(featureIds: List, viewId: Int? = null, onlyEndpoints: List? = null, arguments: JsonObject? = null) + fun sync(featureTypes: Set? = null, onlyEndpoints: Set? = null, arguments: JsonObject? = null) fun getMessage(message: MessageFull) - fun sendMessage(recipients: List, subject: String, text: String) + fun sendMessage(recipients: Set, subject: String, text: String) fun markAllAnnouncementsAsRead() fun getAnnouncement(announcement: AnnouncementFull) fun getAttachment(owner: Any, attachmentId: Long, attachmentName: String) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EndpointCallback.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EndpointCallback.kt index b44820c5..f8a65ac4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EndpointCallback.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/interfaces/EndpointCallback.kt @@ -6,7 +6,6 @@ package pl.szczodrzynski.edziennik.data.api.interfaces import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.api.models.Feature -import pl.szczodrzynski.edziennik.data.api.models.LoginMethod /** * A callback passed to all [Feature]s and [LoginMethod]s diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/ApiError.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/ApiError.kt index d99e16a2..b631ed7d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/ApiError.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/ApiError.kt @@ -5,6 +5,7 @@ package pl.szczodrzynski.edziennik.data.api.models import android.content.Context +import android.os.Bundle import com.google.gson.JsonObject import im.wangchao.mhttp.Request import im.wangchao.mhttp.Response @@ -13,8 +14,8 @@ import pl.szczodrzynski.edziennik.data.api.ERROR_API_EXCEPTION import pl.szczodrzynski.edziennik.data.api.ERROR_EXCEPTION import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApiException import pl.szczodrzynski.edziennik.data.api.szkolny.request.ErrorReportRequest -import pl.szczodrzynski.edziennik.stackTraceString -import pl.szczodrzynski.edziennik.toErrorCode +import pl.szczodrzynski.edziennik.ext.stackTraceString +import pl.szczodrzynski.edziennik.ext.toErrorCode class ApiError(val tag: String, var errorCode: Int) { companion object { @@ -30,6 +31,7 @@ class ApiError(val tag: String, var errorCode: Int) { var request: Request? = null var response: Response? = null var isCritical = true + var params: Bundle? = null fun withThrowable(throwable: Throwable?): ApiError { this.throwable = throwable @@ -58,6 +60,11 @@ class ApiError(val tag: String, var errorCode: Int) { return this } + fun withParams(bundle: Bundle): ApiError { + this.params = bundle + return this + } + fun getStringText(context: Context): String { return context.resources.getIdentifier("error_${errorCode}", "string", context.packageName).let { if (it != 0) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt index 8dda9543..2198c46c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Data.kt @@ -1,16 +1,25 @@ package pl.szczodrzynski.edziennik.data.api.models +import android.os.Bundle import android.util.LongSparseArray import android.util.SparseArray +import androidx.core.util.set import androidx.core.util.size import androidx.room.OnConflictStrategy import com.google.gson.JsonObject import im.wangchao.mhttp.Response -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.BuildConfig +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.ERROR_REQUEST_FAILURE -import pl.szczodrzynski.edziennik.data.api.interfaces.EndpointCallback +import pl.szczodrzynski.edziennik.data.api.Regexes.MESSAGE_META +import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent +import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.db.AppDb import pl.szczodrzynski.edziennik.data.db.entity.* +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.models.Date @@ -32,7 +41,7 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt /** * A callback passed to all [Feature]s and [LoginMethod]s */ - lateinit var callback: EndpointCallback + lateinit var callback: EdziennikCallback /** * A list of [LoginMethod]s *already fulfilled* during this sync. @@ -40,7 +49,7 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt * A [LoginMethod] may add elements to this list only after a successful login * with that method. */ - val loginMethods = mutableListOf() + val loginMethods = mutableListOf() /** * A method which may be overridden in child Data* classes. @@ -54,12 +63,12 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt * A list of Login method IDs that are still pending * to run. */ - var targetLoginMethodIds = mutableListOf() + var targetLoginMethods = mutableListOf() /** * A map of endpoint ID to last sync time, that are still pending * to run. */ - var targetEndpointIds = sortedMapOf() + var targetEndpoints = sortedMapOf() /** * A count of all network requests to do. */ @@ -306,7 +315,7 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt d("Total save time: ${System.currentTimeMillis()-totalStart} ms") } - fun setSyncNext(endpointId: Int, syncIn: Long? = null, viewId: Int? = null, syncAt: Long? = null) { + fun setSyncNext(endpointId: Int, syncIn: Long? = null, forceFeatureType: FeatureType? = null, syncAt: Long? = null) { EndpointTimer(profile?.id ?: -1, endpointId).apply { syncedNow() @@ -320,8 +329,8 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt if (syncAt != null) { nextSync = syncAt } - if (viewId != null) - syncWhenView(viewId) + if (forceFeatureType != null) + syncWithFeature(forceFeatureType) endpointTimers.add(this) } @@ -369,6 +378,15 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt callback.onError(apiError) } + fun requireUserAction(type: UserActionRequiredEvent.Type, params: Bundle, errorText: Int) { + callback.onRequiresUserAction(UserActionRequiredEvent( + profileId = profile?.id, + type = type, + params = params, + errorText = errorText, + )) + } + fun progress(step: Float) { callback.onProgress(step) } @@ -376,4 +394,134 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt fun startProgress(stringRes: Int) { callback.onStartProgress(stringRes) } + + /* _ _ _ _ _ + | | | | | (_) | + | | | | |_ _| |___ + | | | | __| | / __| + | |__| | |_| | \__ \ + \____/ \__|_|_|__*/ + fun getSubject(id: Long?, name: String, shortName: String = name): Subject { + var subject = subjectList.singleOrNull { it.id == id } + if (subject == null) + subject = subjectList.singleOrNull { it.longName == name } + if (subject == null) + subject = subjectList.singleOrNull { it.shortName == name } + + if (subject == null) { + subject = Subject( + profileId, + id ?: name.crc32(), + name, + shortName + ) + subjectList[subject.id] = subject + } + return subject + } + + fun getTeam( + id: Long?, + name: String, + schoolCode: String, + isTeamClass: Boolean = false, + profileId: Int? = null, + ): Team { + if (isTeamClass && teamClass != null) + return teamClass as Team + var team = teamList.singleOrNull { it.id == id } + + val namePlain = name.replace(" ", "") + if (team == null) + team = teamList.singleOrNull { it.name.replace(" ", "") == namePlain } + + if (team == null) { + team = Team( + profileId ?: this.profileId, + id ?: name.crc32(), + name, + if (isTeamClass) Team.TYPE_CLASS else Team.TYPE_VIRTUAL, + "$schoolCode:$name", + -1 + ) + teamList[team.id] = team + } else if (id != null) { + team.id = id + } + return team + } + + fun getTeacher(firstName: String, lastName: String, loginId: String? = null, id: Long? = null): Teacher { + val teacher = teacherList.singleOrNull { it.fullName == "$firstName $lastName" } + return validateTeacher(teacher, firstName, lastName, loginId, id) + } + + fun getTeacher(firstNameChar: Char, lastName: String, loginId: String? = null): Teacher { + val teacher = teacherList.singleOrNull { it.shortName == "$firstNameChar.$lastName" } + return validateTeacher(teacher, firstNameChar.toString(), lastName, loginId, null) + } + + fun getTeacherByLastFirst(nameLastFirst: String, loginId: String? = null): Teacher { + // comparing full name is safer than splitting and swapping + val teacher = teacherList.singleOrNull { it.fullNameLastFirst == nameLastFirst } + val nameParts = nameLastFirst.split(" ", limit = 2) + return if (nameParts.size == 1) + validateTeacher(teacher, nameParts[0], "", loginId, null) + else + validateTeacher(teacher, nameParts[1], nameParts[0], loginId, null) + } + + fun getTeacherByFirstLast(nameFirstLast: String, loginId: String? = null): Teacher { + // comparing full name is safer than splitting and swapping + val teacher = teacherList.singleOrNull { it.fullName == nameFirstLast } + val nameParts = nameFirstLast.split(" ", limit = 2) + return if (nameParts.size == 1) + validateTeacher(teacher, nameParts[0], "", loginId, null) + else + validateTeacher(teacher, nameParts[0], nameParts[1], loginId, null) + } + + fun getTeacherByFDotLast(nameFDotLast: String, loginId: String? = null): Teacher { + val nameParts = nameFDotLast.split(".") + return if (nameParts.size == 1) + getTeacher(nameParts[0], "", loginId) + else + getTeacher(nameParts[0][0], nameParts[1], loginId) + } + + fun getTeacherByFDotSpaceLast(nameFDotSpaceLast: String, loginId: String? = null): Teacher { + val nameParts = nameFDotSpaceLast.split(".") + return if (nameParts.size == 1) + getTeacher(nameParts[0], "", loginId) + else + getTeacher(nameParts[0][0], nameParts[1], loginId) + } + + private fun validateTeacher( + teacher: Teacher?, + firstName: String, + lastName: String, + loginId: String?, + id: Long? + ): Teacher { + val obj = teacher ?: Teacher(profileId, -1, firstName, lastName, loginId).also { + it.id = id ?: it.fullName.crc32() + teacherList[it.id] = it + } + return obj.also { + if (loginId != null) + it.loginId = loginId + if (firstName.length > 1) + it.name = firstName + it.surname = lastName + } + } + + fun parseMessageMeta(body: String): Map? { + val match = MESSAGE_META.find(body) ?: return null + return match[1].split("&").associateBy( + { it.substringBefore("=") }, + { it.substringAfter("=") }, + ) + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/DataRemoveModel.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/DataRemoveModel.kt index 49b57999..73096f28 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/DataRemoveModel.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/DataRemoveModel.kt @@ -11,19 +11,19 @@ import pl.szczodrzynski.edziennik.data.db.dao.TimetableDao import pl.szczodrzynski.edziennik.utils.models.Date open class DataRemoveModel { - data class Timetable(private val dateFrom: Date?, private val dateTo: Date?) : DataRemoveModel() { + data class Timetable(private val dateFrom: Date?, private val dateTo: Date?, private val isExtra: Boolean?) : DataRemoveModel() { companion object { - fun from(dateFrom: Date) = Timetable(dateFrom, null) - fun to(dateTo: Date) = Timetable(null, dateTo) - fun between(dateFrom: Date, dateTo: Date) = Timetable(dateFrom, dateTo) + fun from(dateFrom: Date, isExtra: Boolean? = null) = Timetable(dateFrom, null, isExtra) + fun to(dateTo: Date, isExtra: Boolean? = null) = Timetable(null, dateTo, isExtra) + fun between(dateFrom: Date, dateTo: Date, isExtra: Boolean? = null) = Timetable(dateFrom, dateTo, isExtra) } fun commit(profileId: Int, dao: TimetableDao) { if (dateFrom != null && dateTo != null) { - dao.dontKeepBetweenDates(profileId, dateFrom, dateTo) + dao.dontKeepBetweenDates(profileId, dateFrom, dateTo, isExtra ?: false) } else { - dateFrom?.let { dateFrom -> dao.dontKeepFromDate(profileId, dateFrom) } - dateTo?.let { dateTo -> dao.dontKeepToDate(profileId, dateTo) } + dateFrom?.let { dateFrom -> dao.dontKeepFromDate(profileId, dateFrom, isExtra ?: false) } + dateTo?.let { dateTo -> dao.dontKeepToDate(profileId, dateTo, isExtra ?: false) } } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Feature.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Feature.kt index fd7c7873..2007135b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Feature.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/Feature.kt @@ -1,25 +1,27 @@ package pl.szczodrzynski.edziennik.data.api.models +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.data.db.enums.LoginType + /** * A Endpoint descriptor class. * * The API runs appropriate endpoints in order to fulfill its * feature list. * An endpoint may have its [LoginMethod] dependencies which will be - * satisfied by the API before the [endpointClass]'s constructor is invoked. + * satisfied by the API before the endpoint class is invoked. * * @param loginType type of the e-register this endpoint handles - * @param featureId a feature ID - * @param endpointIds a [List] of [Feature]s that satisfy this feature ID - * @param requiredLoginMethod a required login method, which will have to be executed before this endpoint. + * @param featureType type of the feature + * @param endpoints a [List] of endpoints and their required login methods that satisfy this feature type */ data class Feature( - val loginType: Int, - val featureId: Int, - val endpointIds: List>, - val requiredLoginMethods: List + val loginType: LoginType, + val featureType: FeatureType, + val endpoints: List>, ) { - var priority = endpointIds.size + var priority = endpoints.size fun withPriority(priority: Int): Feature { this.priority = priority return this @@ -30,4 +32,6 @@ data class Feature( this.shouldSync = shouldSync return this } + + val requiredLoginMethods by lazy { endpoints.map { it.second } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/LoginMethod.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/LoginMethod.kt deleted file mode 100644 index cf899cff..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/models/LoginMethod.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2019-9-20. - */ - -package pl.szczodrzynski.edziennik.data.api.models - -import pl.szczodrzynski.edziennik.data.api.LOGIN_METHOD_NOT_NEEDED -import pl.szczodrzynski.edziennik.data.db.entity.LoginStore -import pl.szczodrzynski.edziennik.data.db.entity.Profile - -/** - * A Login Method descriptor class. - * - * This is used by the API to satisfy all [Feature]s' dependencies. - * A login method may have its own dependencies which need to be - * satisfied before the [loginMethodClass]'s constructor is invoked. - * - * @param loginType type of the e-register this login method handles - * @param loginMethodId a unique ID of this login method - * @param loginMethodClass a [Class] which constructor will be invoked when a log in is needed - * @param requiredLoginMethod a required login method (which will be called before this). May differ depending on the [Profile] and/or [LoginStore]. - */ -class LoginMethod( - val loginType: Int, - val loginMethodId: Int, - val loginMethodClass: Class<*>, - private var mIsPossible: ((profile: Profile?, loginStore: LoginStore) -> Boolean)? = null, - private var mRequiredLoginMethod: ((profile: Profile?, loginStore: LoginStore) -> Int)? = null -) { - - fun withIsPossible(isPossible: (profile: Profile?, loginStore: LoginStore) -> Boolean): LoginMethod { - this.mIsPossible = isPossible - return this - } - fun withRequiredLoginMethod(requiredLoginMethod: (profile: Profile?, loginStore: LoginStore) -> Int): LoginMethod { - this.mRequiredLoginMethod = requiredLoginMethod - return this - } - - fun isPossible(profile: Profile?, loginStore: LoginStore): Boolean { - return mIsPossible?.invoke(profile, loginStore) ?: false - } - fun requiredLoginMethod(profile: Profile?, loginStore: LoginStore): Int { - return mRequiredLoginMethod?.invoke(profile, loginStore) ?: LOGIN_METHOD_NOT_NEEDED - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt index 95d61224..954d6bb1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyApi.kt @@ -13,7 +13,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.withContext import okhttp3.OkHttpClient -import org.greenrobot.eventbus.EventBus import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.ERROR_API_INVALID_SIGNATURE import pl.szczodrzynski.edziennik.data.api.szkolny.adapter.DateAdapter @@ -22,18 +21,16 @@ import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.ApiCacheIntercept import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.SignatureInterceptor import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing import pl.szczodrzynski.edziennik.data.api.szkolny.request.* -import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse -import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus -import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update -import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse -import pl.szczodrzynski.edziennik.data.db.entity.Event -import pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage -import pl.szczodrzynski.edziennik.data.db.entity.Notification -import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.api.szkolny.response.* +import pl.szczodrzynski.edziennik.data.db.entity.* import pl.szczodrzynski.edziennik.data.db.full.EventFull -import pl.szczodrzynski.edziennik.ui.modules.error.ErrorDetailsDialog -import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar -import pl.szczodrzynski.edziennik.ui.modules.login.LoginInfo +import pl.szczodrzynski.edziennik.ext.keys +import pl.szczodrzynski.edziennik.ext.md5 +import pl.szczodrzynski.edziennik.ext.toApiError +import pl.szczodrzynski.edziennik.ext.toErrorCode +import pl.szczodrzynski.edziennik.ui.error.ErrorDetailsDialog +import pl.szczodrzynski.edziennik.ui.error.ErrorSnackbar +import pl.szczodrzynski.edziennik.ui.login.LoginInfo import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time import retrofit2.Response @@ -103,7 +100,7 @@ class SzkolnyApi(val app: App) : CoroutineScope { activity, listOf(apiError), R.string.error_occured - ) + ).show() null } null @@ -126,17 +123,14 @@ class SzkolnyApi(val app: App) : CoroutineScope { * or null if it's a HTTP call error. */ @Throws(Exception::class) - private inline fun parseResponse(response: Response>): T { - app.config.update = response.body()?.update?.let { update -> - if (update.versionCode > BuildConfig.VERSION_CODE) { - if (update.updateMandatory - && EventBus.getDefault().hasSubscriberForEvent(update::class.java)) { - EventBus.getDefault().postSticky(update) - } - update - } - else - null + private inline fun parseResponse( + response: Response>, + updateDeviceHash: Boolean = false, + ): T { + response.body()?.update?.let { update -> + // do not process "null" update, as it might not mean there's no update + // do not notify; silently check and show the home update card + app.updateManager.process(update, notify = false) } response.body()?.registerAvailability?.let { registerAvailability -> @@ -144,6 +138,14 @@ class SzkolnyApi(val app: App) : CoroutineScope { } if (response.isSuccessful && response.body()?.success == true) { + // update the device's hash on success + if (updateDeviceHash) { + val hash = getDevice()?.toString()?.md5() + if (hash != null) { + app.config.hash = hash + } + } + if (Unit is T) { return Unit } @@ -168,7 +170,6 @@ class SzkolnyApi(val app: App) : CoroutineScope { throw SzkolnyApiException(body?.errors?.firstOrNull()) } - @Throws(Exception::class) private fun getDevice() = run { val config = app.config val device = Device( @@ -181,52 +182,56 @@ class SzkolnyApi(val app: App) : CoroutineScope { appVersionCode = BuildConfig.VERSION_CODE, syncInterval = app.config.sync.interval ) - device.toString().md5().let { - if (it == config.hash) - null - else { - config.hash = it - device - } - } + val hash = device.toString().md5() + if (hash == config.hash) + return@run null + return@run device } @Throws(Exception::class) - fun getEvents(profiles: List, notifications: List, blacklistedIds: List, lastSyncTime: Long): List { + fun getEvents( + profiles: List, + notifications: List, + blacklistedIds: List, + lastSyncTime: Long, + ): Pair, List> { val teams = app.db.teamDao().allNow + val users = profiles.mapNotNull { profile -> + val user = ServerSyncRequest.User( + profile.userCode, + profile.studentNameLong, + profile.studentNameShort, + profile.loginStoreType.id, + teams.filter { it.profileId == profile.id }.map { it.code } + ) + val hash = user.toString().md5() + if (hash == profile.config.hash) + return@mapNotNull null + return@mapNotNull user to profile.config + } + val response = api.serverSync(ServerSyncRequest( deviceId = app.deviceId, device = getDevice(), userCodes = profiles.map { it.userCode }, - users = profiles.mapNotNull { profile -> - val config = app.config.getFor(profile.id) - val user = ServerSyncRequest.User( - profile.userCode, - profile.studentNameLong, - profile.studentNameShort, - profile.loginStoreType, - teams.filter { it.profileId == profile.id }.map { it.code } - ) - user.toString().md5().let { - if (it == config.hash) - null - else { - config.hash = it - user - } - } - }, + users = users.keys(), lastSync = lastSyncTime, - notifications = notifications.map { ServerSyncRequest.Notification(it.profileName ?: "", it.type, it.text) } + notifications = notifications.map { ServerSyncRequest.Notification(it.profileName ?: "", it.type.id, it.text) } )).execute() - val (events, hasBrowsers) = parseResponse(response) + val (events, notes, hasBrowsers) = parseResponse(response, updateDeviceHash = true) hasBrowsers?.let { app.config.sync.webPushEnabled = it } + // update users' hashes on success + users.forEach { (user, config) -> + config.hash = user.toString().md5() + } + val eventList = mutableListOf() + val noteList = mutableListOf() events.forEach { event -> // skip blacklisted events @@ -237,9 +242,13 @@ class SzkolnyApi(val app: App) : CoroutineScope { if (event.color == -1) event.color = null + val eventSharedBy = event.sharedBy + // create the event for every matching team and profile teams.filter { it.code == event.teamCode }.onEach { team -> val profile = profiles.firstOrNull { it.id == team.profileId } ?: return@onEach + if (!profile.canShare) + return@forEach eventList += EventFull(event).apply { profileId = team.profileId @@ -248,38 +257,107 @@ class SzkolnyApi(val app: App) : CoroutineScope { seen = profile.empty notified = profile.empty - if (profile.userCode == event.sharedBy) sharedBy = "self" + sharedBy = if (profile.userCode == event.sharedBy) + "self" + else + eventSharedBy } } } - return eventList + notes.forEach { note -> + val noteSharedBy = note.sharedBy + + // create the note for every matching team and profile + teams.filter { it.code == note.teamCode }.onEach { team -> + val profile = profiles.firstOrNull { it.id == team.profileId } ?: return@onEach + if (!profile.canShare) + return@forEach + note.profileId = team.profileId + if (profile.userCode == note.sharedBy) { + note.sharedBy = "self" + } else { + note.sharedBy = noteSharedBy + } + + if (app.noteManager.hasValidOwner(note)) + noteList += note + } + } + return eventList to noteList } @Throws(Exception::class) fun shareEvent(event: EventFull) { + val profile = app.db.profileDao().getByIdNow(event.profileId) + ?: throw NullPointerException("Profile is not found") val team = app.db.teamDao().getByIdNow(event.profileId, event.teamId) + ?: throw NullPointerException("Team is not found") val response = api.shareEvent(EventShareRequest( - deviceId = app.deviceId, - device = getDevice(), - sharedByName = event.sharedByName, - shareTeamCode = team.code, - event = event + deviceId = app.deviceId, + device = getDevice(), + userCode = profile.userCode, + studentNameLong = profile.studentNameLong, + shareTeamCode = team.code, + event = event + )).execute() + parseResponse(response, updateDeviceHash = true) + } + + @Throws(Exception::class) + fun unshareEvent(event: Event) { + val profile = app.db.profileDao().getByIdNow(event.profileId) + ?: throw NullPointerException("Profile is not found") + val team = app.db.teamDao().getByIdNow(event.profileId, event.teamId) + ?: throw NullPointerException("Team is not found") + + val response = api.shareEvent(EventShareRequest( + deviceId = app.deviceId, + device = getDevice(), + userCode = profile.userCode, + studentNameLong = profile.studentNameLong, + unshareTeamCode = team.code, + eventId = event.id + )).execute() + parseResponse(response, updateDeviceHash = true) + } + + @Throws(Exception::class) + fun shareNote(note: Note, teamId: Long? = null) { + val profile = app.db.profileDao().getByIdNow(note.profileId) + ?: throw NullPointerException("Profile is not found") + val team = if (teamId == null) + app.db.teamDao().getClassNow(note.profileId) + else + app.db.teamDao().getByIdNow(note.profileId, teamId) + team ?: throw NullPointerException("TeamClass is not found") + + val response = api.shareNote(NoteShareRequest( + deviceId = app.deviceId, + device = getDevice(), + userCode = profile.userCode, + studentNameLong = profile.studentNameLong, + shareTeamCode = team.code, + note = note, )).execute() parseResponse(response) } @Throws(Exception::class) - fun unshareEvent(event: Event) { - val team = app.db.teamDao().getByIdNow(event.profileId, event.teamId) + fun unshareNote(note: Note) { + val profile = app.db.profileDao().getByIdNow(note.profileId) + ?: throw NullPointerException("Profile is not found") + val team = app.db.teamDao().getClassNow(note.profileId) + ?: throw NullPointerException("TeamClass is not found") - val response = api.shareEvent(EventShareRequest( - deviceId = app.deviceId, - device = getDevice(), - sharedByName = event.sharedByName, - unshareTeamCode = team.code, - eventId = event.id + val response = api.shareNote(NoteShareRequest( + deviceId = app.deviceId, + device = getDevice(), + userCode = profile.userCode, + studentNameLong = profile.studentNameLong, + unshareTeamCode = team.code, + noteId = note.id, )).execute() parseResponse(response) } @@ -298,7 +376,7 @@ class SzkolnyApi(val app: App) : CoroutineScope { pairToken = pairToken )).execute() - return parseResponse(response).browsers + return parseResponse(response, updateDeviceHash = true).browsers } @Throws(Exception::class) @@ -309,7 +387,7 @@ class SzkolnyApi(val app: App) : CoroutineScope { action = "listBrowsers" )).execute() - return parseResponse(response).browsers + return parseResponse(response, updateDeviceHash = true).browsers } @Throws(Exception::class) @@ -321,7 +399,7 @@ class SzkolnyApi(val app: App) : CoroutineScope { browserId = browserId )).execute() - return parseResponse(response).browsers + return parseResponse(response, updateDeviceHash = true).browsers } @Throws(Exception::class) @@ -332,7 +410,7 @@ class SzkolnyApi(val app: App) : CoroutineScope { appVersion = BuildConfig.VERSION_NAME, errors = errors )).execute() - parseResponse(response) + parseResponse(response, updateDeviceHash = true) } @Throws(Exception::class) @@ -342,12 +420,12 @@ class SzkolnyApi(val app: App) : CoroutineScope { device = getDevice(), userCode = userCode )).execute() - parseResponse(response) + parseResponse(response, updateDeviceHash = true) } @Throws(Exception::class) - fun getUpdate(channel: String): List { - val response = api.updates(channel).execute() + fun getUpdate(channel: Update.Type): List { + val response = api.updates(channel.name.lowercase()).execute() return parseResponse(response) } @@ -361,14 +439,23 @@ class SzkolnyApi(val app: App) : CoroutineScope { text = text )).execute() - return parseResponse(response).message + return parseResponse(response, updateDeviceHash = true).message } @Throws(Exception::class) fun getRealms(registerName: String): List { - val response = api.fsLoginRealms(registerName).execute() + val response = api.platforms(registerName).execute() if (response.isSuccessful && response.body() != null) { - return response.body()!! + return parseResponse(response) + } + throw SzkolnyApiException(null) + } + + @Throws(Exception::class) + fun getContributors(): ContributorsResponse { + val response = api.contributors().execute() + if (response.isSuccessful && response.body() != null) { + return parseResponse(response) } throw SzkolnyApiException(null) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt index c4198130..cca9475a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/SzkolnyService.kt @@ -6,7 +6,7 @@ package pl.szczodrzynski.edziennik.data.api.szkolny import pl.szczodrzynski.edziennik.data.api.szkolny.request.* import pl.szczodrzynski.edziennik.data.api.szkolny.response.* -import pl.szczodrzynski.edziennik.ui.modules.login.LoginInfo +import pl.szczodrzynski.edziennik.ui.login.LoginInfo import retrofit2.Call import retrofit2.http.* @@ -18,6 +18,9 @@ interface SzkolnyService { @POST("share") fun shareEvent(@Body request: EventShareRequest): Call> + @POST("share") + fun shareNote(@Body request: NoteShareRequest): Call> + @POST("webPush") fun webPush(@Body request: WebPushRequest): Call> @@ -27,6 +30,9 @@ interface SzkolnyService { @POST("appUser") fun appUser(@Body request: AppUserRequest): Call> + @GET("contributors/android") + fun contributors(): Call> + @GET("updates/app") fun updates(@Query("channel") channel: String = "release"): Call>> @@ -39,6 +45,6 @@ interface SzkolnyService { @GET("registerAvailability") fun registerAvailability(): Call>> - @GET("https://szkolny-eu.github.io/FSLogin/realms/{registerName}.json") - fun fsLoginRealms(@Path("registerName") registerName: String): Call> + @GET("platforms/{registerName}") + fun platforms(@Path("registerName") registerName: String): Call>> } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/ApiCacheInterceptor.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/ApiCacheInterceptor.kt index 19a84546..f1f8e341 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/ApiCacheInterceptor.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/ApiCacheInterceptor.kt @@ -7,7 +7,7 @@ package pl.szczodrzynski.edziennik.data.api.szkolny.interceptor import okhttp3.* import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse -import pl.szczodrzynski.edziennik.md5 +import pl.szczodrzynski.edziennik.ext.md5 class ApiCacheInterceptor(val app: App) : Interceptor { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/SignatureInterceptor.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/SignatureInterceptor.kt index 30d0ec7e..275a1909 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/SignatureInterceptor.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/SignatureInterceptor.kt @@ -6,11 +6,17 @@ package pl.szczodrzynski.edziennik.data.api.szkolny.interceptor import okhttp3.Interceptor import okhttp3.Response -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.BuildConfig +import pl.szczodrzynski.edziennik.ext.bodyToString +import pl.szczodrzynski.edziennik.ext.currentTimeUnix +import pl.szczodrzynski.edziennik.ext.hmacSHA1 +import pl.szczodrzynski.edziennik.ext.md5 +import pl.szczodrzynski.edziennik.ext.takeValue class SignatureInterceptor(val app: App) : Interceptor { companion object { - private const val API_KEY = "szkolny_android_42a66f0842fc7da4e37c66732acf705a" + const val API_KEY = "szkolny_android_42a66f0842fc7da4e37c66732acf705a" } override fun intercept(chain: Interceptor.Chain): Response { @@ -22,12 +28,13 @@ class SignatureInterceptor(val app: App) : Interceptor { return chain.proceed( request.newBuilder() - .header("X-ApiKey", API_KEY) - .header("X-AppVersion", BuildConfig.VERSION_CODE.toString()) - .header("X-Timestamp", timestamp.toString()) - .header("X-Signature", sign(timestamp, body, url)) + .header("X-ApiKey", app.config.apiKeyCustom?.takeValue() ?: API_KEY) .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()) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt index 819194d8..f520ac62 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/interceptor/Signing.kt @@ -9,7 +9,7 @@ import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.util.Base64 import pl.szczodrzynski.edziennik.BuildConfig -import pl.szczodrzynski.edziennik.sha256 +import pl.szczodrzynski.edziennik.ext.sha256 import java.security.MessageDigest object Signing { @@ -39,13 +39,13 @@ object Signing { val appPassword by lazy { iLoveApple( "ThisIsOurHardWorkPleaseDoNotCopyOrSteal(c)2019.KubaSz".sha256(), - BuildConfig.VERSION_NAME.substringBeforeLast('+'), + BuildConfig.VERSION_BASE.substringBeforeLast('+'), BuildConfig.VERSION_CODE.toLong() ) } /*fun provideKey(param1: String, param2: Long): ByteArray {*/ fun pleaseStopRightNow(param1: String, param2: Long): ByteArray { - return "$param1.MTIzNDU2Nzg5MDLPrcQX7M===.$param2".sha256() + return "$param1.MTIzNDU2Nzg5MD01uMP7oW===.$param2".sha256() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/request/EventShareRequest.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/request/EventShareRequest.kt index 1977d1b2..aba026a4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/request/EventShareRequest.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/request/EventShareRequest.kt @@ -12,8 +12,9 @@ data class EventShareRequest ( val action: String = "event", - /* If null, the server shows an error */ - val sharedByName: String?, + val userCode: String, + val studentNameLong: String, + val shareTeamCode: String? = null, val unshareTeamCode: String? = null, val requesterName: String? = null, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/request/NoteShareRequest.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/request/NoteShareRequest.kt new file mode 100644 index 00000000..122ea542 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/request/NoteShareRequest.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-26. + */ + +package pl.szczodrzynski.edziennik.data.api.szkolny.request + +import pl.szczodrzynski.edziennik.data.db.entity.Note + +data class NoteShareRequest ( + val deviceId: String, + val device: Device? = null, + + val action: String = "note", + + val userCode: String, + val studentNameLong: String, + + val shareTeamCode: String? = null, + val unshareTeamCode: String? = null, + val requesterName: String? = null, + + val noteId: Long? = null, + val note: Note? = null +) + diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/ContributorsResponse.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/ContributorsResponse.kt new file mode 100644 index 00000000..f824280a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/ContributorsResponse.kt @@ -0,0 +1,20 @@ +package pl.szczodrzynski.edziennik.data.api.szkolny.response + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +data class ContributorsResponse( + val contributors: List, + val translators: List +) { + + @Parcelize + data class Item( + val login: String, + val name: String?, + val avatarUrl: String, + val profileUrl: String, + val itemUrl: String, + val contributions: Int? + ) : Parcelable +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/RegisterAvailabilityStatus.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/RegisterAvailabilityStatus.kt index 82f2e050..617a7167 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/RegisterAvailabilityStatus.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/RegisterAvailabilityStatus.kt @@ -5,15 +5,15 @@ package pl.szczodrzynski.edziennik.data.api.szkolny.response import pl.szczodrzynski.edziennik.BuildConfig -import pl.szczodrzynski.edziennik.DAY -import pl.szczodrzynski.edziennik.currentTimeUnix +import pl.szczodrzynski.edziennik.ext.DAY +import pl.szczodrzynski.edziennik.ext.currentTimeUnix data class RegisterAvailabilityStatus( - val available: Boolean, - val name: String?, - val userMessage: Message?, - val nextCheckAt: Long = currentTimeUnix() + 7 * DAY, - val minVersionCode: Int = BuildConfig.VERSION_CODE + val available: Boolean, + val name: String?, + val userMessage: Message?, + val nextCheckAt: Long = currentTimeUnix() + 7 * DAY, + val minVersionCode: Int = BuildConfig.VERSION_CODE ) { data class Message( val title: String, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/ServerSyncResponse.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/ServerSyncResponse.kt index 0ed11f9c..e7158754 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/ServerSyncResponse.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/ServerSyncResponse.kt @@ -4,9 +4,11 @@ package pl.szczodrzynski.edziennik.data.api.szkolny.response +import pl.szczodrzynski.edziennik.data.db.entity.Note import pl.szczodrzynski.edziennik.data.db.full.EventFull data class ServerSyncResponse( val events: List, + val notes: List, val hasBrowsers: Boolean? = null ) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/UpdateResponse.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/UpdateResponse.kt index 63a8ce9d..0b0433f3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/UpdateResponse.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/response/UpdateResponse.kt @@ -5,12 +5,21 @@ package pl.szczodrzynski.edziennik.data.api.szkolny.response data class Update( - val versionCode: Int, - val versionName: String, - val releaseDate: String, - val releaseNotes: String?, - val releaseType: String, - val isOnGooglePlay: Boolean, - val downloadUrl: String?, - val updateMandatory: Boolean -) + val versionCode: Int, + val versionName: String, + val releaseDate: String, + val releaseNotes: String?, + val releaseType: String, + val isOnGooglePlay: Boolean, + val downloadUrl: String?, + val updateMandatory: Boolean, +) { + + enum class Type { + NIGHTLY, + DEV, + BETA, + RC, + RELEASE, + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/AppSync.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/AppSync.kt index 15482c53..f076503a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/AppSync.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/AppSync.kt @@ -11,7 +11,8 @@ import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApiException import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Notification import pl.szczodrzynski.edziennik.data.db.entity.Profile -import pl.szczodrzynski.edziennik.toErrorCode +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ext.toErrorCode import pl.szczodrzynski.edziennik.utils.models.Date class AppSync(val app: App, val notifications: MutableList, val profiles: List, val api: SzkolnyApi) { @@ -30,7 +31,7 @@ class AppSync(val app: App, val notifications: MutableList, val pr */ fun run(lastSyncTime: Long, markAsSeen: Boolean = false): Int { val blacklistedIds = app.db.eventDao().blacklistedIds - val events = try { + val (events, notes) = try { api.getEvents(profiles, notifications, blacklistedIds, lastSyncTime) } catch (e: SzkolnyApiException) { if (e.toErrorCode() == ERROR_API_INVALID_SIGNATURE) @@ -40,13 +41,17 @@ class AppSync(val app: App, val notifications: MutableList, val pr app.config.sync.lastAppSync = System.currentTimeMillis() + if (notes.isNotEmpty()) { + app.db.noteDao().addAll(notes) + } + if (events.isNotEmpty()) { val today = Date.getToday() app.db.metadataDao().addAllIgnore(events.map { event -> val isPast = event.date < today Metadata( event.profileId, - Metadata.TYPE_EVENT, + MetadataType.EVENT, event.id, isPast || markAsSeen || event.seen, isPast || markAsSeen || event.notified @@ -54,6 +59,6 @@ class AppSync(val app: App, val notifications: MutableList, val pr }) return app.db.eventDao().upsertAll(events).size } - return 0; + return 0 } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/Notifications.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/Notifications.kt index 21a5c112..bb4f0cb1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/Notifications.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/Notifications.kt @@ -5,11 +5,13 @@ package pl.szczodrzynski.edziennik.data.api.task import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.* -import pl.szczodrzynski.edziennik.getNotificationTitle +import pl.szczodrzynski.edziennik.data.db.enums.NotificationType +import pl.szczodrzynski.edziennik.ext.resolveString +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Week class Notifications(val app: App, val notifications: MutableList, val profiles: List) { companion object { @@ -42,76 +44,116 @@ class Notifications(val app: App, val notifications: MutableList, val text = app.getString( R.string.notification_lesson_change_format, lesson.getDisplayChangeType(app), - if (lesson.displayDate == null) "" else lesson.displayDate!!.formattedString, + lesson.displayDate?.formattedString ?: "", lesson.changeSubjectName ) + val textLong = app.getString( + R.string.notification_lesson_change_long_format, + lesson.getDisplayChangeType(app), + lesson.displayDate?.formattedString ?: "-", + lesson.displayDate?.weekDay?.let { Week.getFullDayName(it) } ?: "-", + lesson.changeSubjectName, + lesson.changeTeacherName + ) notifications += Notification( - id = Notification.buildId(lesson.profileId, Notification.TYPE_TIMETABLE_LESSON_CHANGE, lesson.id), - title = app.getNotificationTitle(Notification.TYPE_TIMETABLE_LESSON_CHANGE), + id = Notification.buildId(lesson.profileId, NotificationType.TIMETABLE_LESSON_CHANGE, lesson.id), + title = NotificationType.TIMETABLE_LESSON_CHANGE.titleRes.resolveString(app), text = text, - type = Notification.TYPE_TIMETABLE_LESSON_CHANGE, + textLong = textLong, + type = NotificationType.TIMETABLE_LESSON_CHANGE, profileId = lesson.profileId, profileName = profiles.singleOrNull { it.id == lesson.profileId }?.name, - viewId = MainActivity.DRAWER_ITEM_TIMETABLE, + navTarget = NavTarget.TIMETABLE, addedDate = System.currentTimeMillis() ).addExtra("timetableDate", lesson.displayDate?.stringY_m_d ?: "") } } private fun eventNotifications() { - for (event in app.db.eventDao().getNotNotifiedNow().filter { it.date >= today }) { - val text = if (event.type == Event.TYPE_HOMEWORK) + app.db.eventDao().getNotNotifiedNow().filter { + it.date >= today + }.forEach { event -> + val text = if (event.isHomework) app.getString( - if (event.subjectLongName.isNullOrEmpty()) - R.string.notification_homework_no_subject_format - else - R.string.notification_homework_format, - event.subjectLongName, - event.date.formattedString + if (event.subjectLongName.isNullOrEmpty()) + R.string.notification_homework_no_subject_format + else + R.string.notification_homework_format, + event.subjectLongName, + event.date.formattedString ) else app.getString( - if (event.subjectLongName.isNullOrEmpty()) - R.string.notification_event_no_subject_format - else - R.string.notification_event_format, - event.typeName ?: "wydarzenie", - event.date.formattedString, - event.subjectLongName + if (event.subjectLongName.isNullOrEmpty()) + R.string.notification_event_no_subject_format + else + R.string.notification_event_format, + event.typeName ?: "wydarzenie", + event.date.formattedString, + event.subjectLongName ) - val type = if (event.type == Event.TYPE_HOMEWORK) Notification.TYPE_NEW_HOMEWORK else Notification.TYPE_NEW_EVENT + val textLong = app.getString( + R.string.notification_event_long_format, + event.typeName ?: "-", + event.subjectLongName ?: "-", + event.date.formattedString, + Week.getFullDayName(event.date.weekDay), + event.time?.stringHM ?: app.getString(R.string.event_all_day), + event.topic.take(200) + ) + val type = if (event.isHomework) + NotificationType.HOMEWORK + else + NotificationType.EVENT notifications += Notification( - id = Notification.buildId(event.profileId, type, event.id), - title = app.getNotificationTitle(type), - text = text, - type = type, - profileId = event.profileId, - profileName = profiles.singleOrNull { it.id == event.profileId }?.name, - viewId = if (event.type == Event.TYPE_HOMEWORK) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA, - addedDate = event.addedDate + id = Notification.buildId(event.profileId, type, event.id), + title = type.titleRes.resolveString(app), + text = text, + textLong = textLong, + type = type, + profileId = event.profileId, + profileName = profiles.singleOrNull { it.id == event.profileId }?.name, + navTarget = if (event.isHomework) NavTarget.HOMEWORK else NavTarget.AGENDA, + addedDate = event.addedDate ).addExtra("eventId", event.id).addExtra("eventDate", event.date.value.toLong()) } } fun sharedEventNotifications() { - for (event in app.db.eventDao().getNotNotifiedNow().filter { it.date >= today && it.sharedBy != null }) { + app.db.eventDao().getNotNotifiedNow().filter { + it.date >= today && it.sharedBy != null && it.sharedBy != "self" + }.forEach { event -> val text = app.getString( - R.string.notification_shared_event_format, - event.sharedByName, - event.typeName ?: "wydarzenie", - event.date.formattedString, - event.topic + R.string.notification_shared_event_format, + event.sharedByName, + event.typeName ?: "wydarzenie", + event.date.formattedString, + event.topicHtml ) - val type = if (event.type == Event.TYPE_HOMEWORK) Notification.TYPE_NEW_HOMEWORK else Notification.TYPE_NEW_EVENT + val textLong = app.getString( + R.string.notification_shared_event_long_format, + event.sharedByName, + event.typeName ?: "-", + event.subjectLongName ?: "-", + event.date.formattedString, + Week.getFullDayName(event.date.weekDay), + event.time?.stringHM ?: app.getString(R.string.event_all_day), + event.topicHtml.take(200) + ) + val type = if (event.isHomework) + NotificationType.HOMEWORK + else + NotificationType.EVENT notifications += Notification( - id = Notification.buildId(event.profileId, type, event.id), - title = app.getNotificationTitle(type), - text = text, - type = type, - profileId = event.profileId, - profileName = profiles.singleOrNull { it.id == event.profileId }?.name, - viewId = if (event.type == Event.TYPE_HOMEWORK) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA, - addedDate = event.addedDate + id = Notification.buildId(event.profileId, type, event.id), + title = type.titleRes.resolveString(app), + text = text, + textLong = textLong, + type = type, + profileId = event.profileId, + profileName = profiles.singleOrNull { it.id == event.profileId }?.name, + navTarget = if (event.isHomework) NavTarget.HOMEWORK else NavTarget.AGENDA, + addedDate = event.addedDate ).addExtra("eventId", event.id).addExtra("eventDate", event.date.value.toLong()) } } @@ -130,14 +172,24 @@ class Notifications(val app: App, val notifications: MutableList, gradeName, grade.subjectLongName ) + val textLong = app.getString( + R.string.notification_grade_long_format, + gradeName, + grade.weight.toString(), + grade.subjectLongName ?: "-", + grade.category ?: "-", + grade.description ?: "-", + grade.teacherName ?: "-" + ) notifications += Notification( - id = Notification.buildId(grade.profileId, Notification.TYPE_NEW_GRADE, grade.id), - title = app.getNotificationTitle(Notification.TYPE_NEW_GRADE), + id = Notification.buildId(grade.profileId, NotificationType.GRADE, grade.id), + title = NotificationType.GRADE.titleRes.resolveString(app), text = text, - type = Notification.TYPE_NEW_GRADE, + textLong = textLong, + type = NotificationType.GRADE, profileId = grade.profileId, profileName = profiles.singleOrNull { it.id == grade.profileId }?.name, - viewId = MainActivity.DRAWER_ITEM_GRADES, + navTarget = NavTarget.GRADES, addedDate = grade.addedDate ).addExtra("gradeId", grade.id).addExtra("gradesSubjectId", grade.subjectId) } @@ -158,14 +210,21 @@ class Notifications(val app: App, val notifications: MutableList, notice.teacherName, Date.fromMillis(notice.addedDate).formattedString ) + val textLong = app.getString( + R.string.notification_notice_long_format, + noticeTypeStr, + notice.teacherName ?: "-", + notice.text.take(200) + ) notifications += Notification( - id = Notification.buildId(notice.profileId, Notification.TYPE_NEW_NOTICE, notice.id), - title = app.getNotificationTitle(Notification.TYPE_NEW_NOTICE), + id = Notification.buildId(notice.profileId, NotificationType.NOTICE, notice.id), + title = NotificationType.NOTICE.titleRes.resolveString(app), text = text, - type = Notification.TYPE_NEW_NOTICE, + textLong = textLong, + type = NotificationType.NOTICE, profileId = notice.profileId, profileName = profiles.singleOrNull { it.id == notice.profileId }?.name, - viewId = MainActivity.DRAWER_ITEM_BEHAVIOUR, + navTarget = NavTarget.BEHAVIOUR, addedDate = notice.addedDate ).addExtra("noticeId", notice.id) } @@ -193,14 +252,25 @@ class Notifications(val app: App, val notifications: MutableList, attendance.subjectLongName, attendance.date.formattedString ) + val textLong = app.getString( + R.string.notification_attendance_long_format, + attendanceTypeStr, + attendance.date.formattedString, + attendance.startTime?.stringHM ?: "-", + attendance.lessonNumber ?: "-", + attendance.subjectLongName ?: "-", + attendance.teacherName ?: "-", + attendance.lessonTopic ?: "-" + ) notifications += Notification( - id = Notification.buildId(attendance.profileId, Notification.TYPE_NEW_ATTENDANCE, attendance.id), - title = app.getNotificationTitle(Notification.TYPE_NEW_ATTENDANCE), + id = Notification.buildId(attendance.profileId, NotificationType.ATTENDANCE, attendance.id), + title = NotificationType.ATTENDANCE.titleRes.resolveString(app), text = text, - type = Notification.TYPE_NEW_ATTENDANCE, + textLong = textLong, + type = NotificationType.ATTENDANCE, profileId = attendance.profileId, profileName = profiles.singleOrNull { it.id == attendance.profileId }?.name, - viewId = MainActivity.DRAWER_ITEM_ATTENDANCE, + navTarget = NavTarget.ATTENDANCE, addedDate = attendance.addedDate ).addExtra("attendanceId", attendance.id).addExtra("attendanceSubjectId", attendance.subjectId) } @@ -214,13 +284,13 @@ class Notifications(val app: App, val notifications: MutableList, announcement.subject ) notifications += Notification( - id = Notification.buildId(announcement.profileId, Notification.TYPE_NEW_ANNOUNCEMENT, announcement.id), - title = app.getNotificationTitle(Notification.TYPE_NEW_ANNOUNCEMENT), + id = Notification.buildId(announcement.profileId, NotificationType.ANNOUNCEMENT, announcement.id), + title = NotificationType.ANNOUNCEMENT.titleRes.resolveString(app), text = text, - type = Notification.TYPE_NEW_ANNOUNCEMENT, + type = NotificationType.ANNOUNCEMENT, profileId = announcement.profileId, profileName = profiles.singleOrNull { it.id == announcement.profileId }?.name, - viewId = MainActivity.DRAWER_ITEM_ANNOUNCEMENTS, + navTarget = NavTarget.ANNOUNCEMENTS, addedDate = announcement.addedDate ).addExtra("announcementId", announcement.id) } @@ -234,13 +304,13 @@ class Notifications(val app: App, val notifications: MutableList, message.subject ) notifications += Notification( - id = Notification.buildId(message.profileId, Notification.TYPE_NEW_MESSAGE, message.id), - title = app.getNotificationTitle(Notification.TYPE_NEW_MESSAGE), + id = Notification.buildId(message.profileId, NotificationType.MESSAGE, message.id), + title = NotificationType.MESSAGE.titleRes.resolveString(app), text = text, - type = Notification.TYPE_NEW_MESSAGE, + type = NotificationType.MESSAGE, profileId = message.profileId, profileName = profiles.singleOrNull { it.id == message.profileId }?.name, - viewId = MainActivity.DRAWER_ITEM_MESSAGES, + navTarget = NavTarget.MESSAGES, addedDate = message.addedDate ).addExtra("messageType", Message.TYPE_RECEIVED.toLong()).addExtra("messageId", message.id) } @@ -264,13 +334,13 @@ class Notifications(val app: App, val notifications: MutableList, } } notifications += Notification( - id = Notification.buildId(luckyNumber.profileId, Notification.TYPE_LUCKY_NUMBER, luckyNumber.date.value.toLong()), - title = app.getNotificationTitle(Notification.TYPE_LUCKY_NUMBER), + id = Notification.buildId(luckyNumber.profileId, NotificationType.LUCKY_NUMBER, luckyNumber.date.value.toLong()), + title = NotificationType.LUCKY_NUMBER.titleRes.resolveString(app), text = app.getString(text, luckyNumber.date.formattedString, luckyNumber.number), - type = Notification.TYPE_LUCKY_NUMBER, + type = NotificationType.LUCKY_NUMBER, profileId = luckyNumber.profileId, profileName = profile.name, - viewId = MainActivity.DRAWER_ITEM_HOME, + navTarget = NavTarget.HOME, addedDate = System.currentTimeMillis() ) } @@ -283,13 +353,13 @@ class Notifications(val app: App, val notifications: MutableList, teacherAbsence.teacherName ) notifications += Notification( - id = Notification.buildId(teacherAbsence.profileId, Notification.TYPE_TEACHER_ABSENCE, teacherAbsence.id), - title = app.getNotificationTitle(Notification.TYPE_TEACHER_ABSENCE), + id = Notification.buildId(teacherAbsence.profileId, NotificationType.TEACHER_ABSENCE, teacherAbsence.id), + title = NotificationType.TEACHER_ABSENCE.titleRes.resolveString(app), text = message, - type = Notification.TYPE_TEACHER_ABSENCE, + type = NotificationType.TEACHER_ABSENCE, profileId = teacherAbsence.profileId, profileName = profiles.singleOrNull { it.id == teacherAbsence.profileId }?.name, - viewId = MainActivity.DRAWER_ITEM_AGENDA, + navTarget = NavTarget.AGENDA, addedDate = teacherAbsence.addedDate ).addExtra("eventDate", teacherAbsence.dateFrom.value.toLong()) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/PostNotifications.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/PostNotifications.kt index 843f2174..9dac8928 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/PostNotifications.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/PostNotifications.kt @@ -4,12 +4,19 @@ import android.app.NotificationManager import android.app.PendingIntent import android.content.Context import android.os.Build -import android.util.SparseIntArray import androidx.core.app.NotificationCompat -import androidx.core.util.forEach -import androidx.core.util.set -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.db.entity.Notification.Companion.TYPE_SERVER_MESSAGE +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import com.mikepenz.iconics.utils.colorRes +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.enums.NotificationType +import pl.szczodrzynski.edziennik.ext.Intent +import pl.szczodrzynski.edziennik.ext.asBoldSpannable +import pl.szczodrzynski.edziennik.ext.concat +import pl.szczodrzynski.edziennik.ext.pendingIntentFlag +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.data.db.entity.Notification as AppNotification @@ -47,26 +54,12 @@ class PostNotifications(val app: App, nList: List) { .setGroup(if (quiet) app.notificationChannelsManager.dataQuiet.key else app.notificationChannelsManager.data.key) } - private fun buildSummaryText(summaryCounts: SparseIntArray): CharSequence { + private fun buildSummaryText(summaryCounts: Map): CharSequence { val summaryTexts = mutableListOf() - summaryCounts.forEach { key, value -> + summaryCounts.forEach { (key, value) -> if (value <= 0) return@forEach - val pluralRes = when (key) { - AppNotification.TYPE_TIMETABLE_LESSON_CHANGE -> R.plurals.notification_new_timetable_change_format - AppNotification.TYPE_NEW_GRADE -> R.plurals.notification_new_grades_format - AppNotification.TYPE_NEW_EVENT -> R.plurals.notification_new_events_format - AppNotification.TYPE_NEW_HOMEWORK -> R.plurals.notification_new_homework_format - AppNotification.TYPE_NEW_SHARED_EVENT -> R.plurals.notification_new_shared_events_format - AppNotification.TYPE_NEW_SHARED_HOMEWORK -> R.plurals.notification_new_shared_homework_format - AppNotification.TYPE_NEW_MESSAGE -> R.plurals.notification_new_messages_format - AppNotification.TYPE_NEW_NOTICE -> R.plurals.notification_new_notices_format - AppNotification.TYPE_NEW_ATTENDANCE -> R.plurals.notification_new_attendance_format - AppNotification.TYPE_LUCKY_NUMBER -> R.plurals.notification_new_lucky_number_format - AppNotification.TYPE_NEW_ANNOUNCEMENT -> R.plurals.notification_new_announcements_format - else -> R.plurals.notification_other_format - } - summaryTexts += app.resources.getQuantityString(pluralRes, value, value) + summaryTexts += app.resources.getQuantityString(key.pluralRes, value, value) } return summaryTexts.concat(", ") } @@ -76,7 +69,7 @@ class PostNotifications(val app: App, nList: List) { if (count == 0) return@run val notificationManager = app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val summaryCounts = SparseIntArray() + val summaryCounts = mutableMapOf() val newNotificationsText = app.resources.getQuantityString(R.plurals.notification_count_format, count, count) val newNotificationsShortText = app.resources.getQuantityString(R.plurals.notification_count_short_format, count, count) @@ -84,9 +77,9 @@ class PostNotifications(val app: App, nList: List) { val intent = Intent( app, MainActivity::class.java, - "fragmentId" to MainActivity.DRAWER_ITEM_NOTIFICATIONS + "fragmentId" to NavTarget.NOTIFICATIONS.id ) - val summaryIntent = PendingIntent.getActivity(app, app.notificationChannelsManager.data.id, intent, PendingIntent.FLAG_ONE_SHOT) + val summaryIntent = PendingIntent.getActivity(app, app.notificationChannelsManager.data.id, intent, PendingIntent.FLAG_ONE_SHOT or pendingIntentFlag()) // On Nougat or newer - show maximum 8 notifications // On Marshmallow or older - show maximum 4 notifications @@ -94,7 +87,7 @@ class PostNotifications(val app: App, nList: List) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N && count > 4 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && count > 8) { val summaryList = mutableListOf() nList.forEach { - summaryCounts[it.type]++ + summaryCounts[it.type] = summaryCounts.getOrDefault(it.type, 0) + 1 summaryList += listOf( it.profileName.asBoldSpannable(), it.text @@ -107,6 +100,10 @@ class PostNotifications(val app: App, nList: List) { .setContentText(buildSummaryText(summaryCounts)) .setTicker(newNotificationsText) .setSmallIcon(R.drawable.ic_notification) + .setLargeIcon(IconicsDrawable(app).apply { + icon = CommunityMaterial.Icon.cmd_bell_ring_outline + colorRes = R.color.colorPrimary + }.toBitmap()) .setStyle(NotificationCompat.InboxStyle() .also { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { @@ -130,15 +127,18 @@ class PostNotifications(val app: App, nList: List) { else { // Less than 8 notifications val notifications = nList.map { - summaryCounts[it.type]++ + summaryCounts[it.type] = summaryCounts.getOrDefault(it.type, 0) + 1 NotificationCompat.Builder(app, app.notificationChannelsManager.data.key) .setContentTitle(it.profileName ?: app.getString(R.string.app_name)) .setContentText(it.text) - .setSubText(if (it.type == TYPE_SERVER_MESSAGE) null else it.title) + .setSubText(if (it.type == NotificationType.SERVER_MESSAGE) null else it.title) .setTicker("${it.profileName}: ${it.title}") .setSmallIcon(R.drawable.ic_notification) + .setLargeIcon(IconicsDrawable(app, it.type.icon).apply { + colorRes = R.color.colorPrimary + }.toBitmap()) .setStyle(NotificationCompat.BigTextStyle() - .bigText(it.text)) + .bigText(it.textLong ?: it.text)) .setWhen(it.addedDate) .addDefaults() .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY) @@ -160,6 +160,10 @@ class PostNotifications(val app: App, nList: List) { .setContentText(buildSummaryText(summaryCounts)) .setTicker(newNotificationsText) .setSmallIcon(R.drawable.ic_notification) + .setLargeIcon(IconicsDrawable(app).apply { + icon = CommunityMaterial.Icon.cmd_bell_ring_outline + colorRes = R.color.colorPrimary + }.toBitmap()) .addDefaults() .setGroupSummary(true) .setContentIntent(summaryIntent) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/SzkolnyTask.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/SzkolnyTask.kt index 3ac80495..64f647e9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/SzkolnyTask.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/SzkolnyTask.kt @@ -5,12 +5,12 @@ package pl.szczodrzynski.edziennik.data.api.task import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.HOUR import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.interfaces.EdziennikCallback import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.data.db.entity.Notification import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.ext.HOUR import pl.szczodrzynski.edziennik.utils.Utils.d class SzkolnyTask(val app: App, val syncingProfiles: List) : IApiTask(-1) { @@ -31,12 +31,12 @@ class SzkolnyTask(val app: App, val syncingProfiles: List) : IApiTask(- val notifications = Notifications(app, notificationList, profiles) notifications.run() - val appSyncProfiles = profiles.filter { it.registration == Profile.REGISTRATION_ENABLED && !it.archived } + val appSyncProfiles = profiles.filter { it.canShare } // App Sync conditions: // - every 24 hours && any profile is registered // - if there are new notifications && any browser is paired val shouldAppSync = - System.currentTimeMillis() - app.config.sync.lastAppSync > 24*HOUR*1000 + System.currentTimeMillis() - app.config.sync.lastAppSync > 24* HOUR *1000 && appSyncProfiles.isNotEmpty() || notificationList.isNotEmpty() && app.config.sync.webPushEnabled @@ -55,7 +55,7 @@ class SzkolnyTask(val app: App, val syncingProfiles: List) : IApiTask(- notificationList .mapNotNull { it.profileId } .distinct() - .map { app.config.getFor(it).sync.notificationFilter } + .map { app.config[it].sync.notificationFilter } .forEach { filter -> filter.forEach { type -> notificationList.removeAll { it.type == type } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt index e6d7e40e..def90a37 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/AppDb.kt @@ -42,15 +42,17 @@ import pl.szczodrzynski.edziennik.data.db.migration.* ConfigEntry::class, LibrusLesson::class, TimetableManual::class, + Note::class, Metadata::class -], version = 91) +], version = 100) @TypeConverters( ConverterTime::class, ConverterDate::class, ConverterJsonObject::class, ConverterListLong::class, ConverterListString::class, - ConverterDateInt::class + ConverterDateInt::class, + ConverterEnums::class ) abstract class AppDb : RoomDatabase() { abstract fun gradeDao(): GradeDao @@ -82,6 +84,7 @@ abstract class AppDb : RoomDatabase() { abstract fun configDao(): ConfigDao abstract fun librusLessonDao(): LibrusLessonDao abstract fun timetableManualDao(): TimetableManualDao + abstract fun noteDao(): NoteDao abstract fun metadataDao(): MetadataDao companion object { @@ -176,7 +179,16 @@ abstract class AppDb : RoomDatabase() { Migration88(), Migration89(), Migration90(), - Migration91() + Migration91(), + Migration92(), + Migration93(), + Migration94(), + Migration95(), + Migration96(), + Migration97(), + Migration98(), + Migration99(), + Migration100(), ).allowMainThreadQueries().build() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/converter/ConverterEnums.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/converter/ConverterEnums.kt new file mode 100644 index 00000000..d9fc893b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/converter/ConverterEnums.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-17. + */ + +package pl.szczodrzynski.edziennik.data.db.converter + +import androidx.room.TypeConverter +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.LoginMethod +import pl.szczodrzynski.edziennik.data.db.enums.LoginMode +import pl.szczodrzynski.edziennik.data.db.enums.LoginType +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.data.db.enums.NotificationType +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget + +class ConverterEnums { + + @TypeConverter + fun fromFeatureType(value: FeatureType?) = value?.id + + @TypeConverter + fun fromLoginMethod(value: LoginMethod?) = value?.id + + @TypeConverter + fun fromLoginMode(value: LoginMode?) = value?.id + + @TypeConverter + fun fromLoginType(value: LoginType?) = value?.id + + @TypeConverter + fun fromMetadataType(value: MetadataType?) = value?.id + + @TypeConverter + fun fromNotificationType(value: NotificationType?) = value?.id + + @TypeConverter + fun fromNavTarget(value: NavTarget?) = value?.id + + @TypeConverter + fun toFeatureType(value: Int?) = value.asFeatureTypeOrNull() + + @TypeConverter + fun toLoginMethod(value: Int?) = value.asLoginMethodOrNull() + + @TypeConverter + fun toLoginMode(value: Int?) = value.asLoginModeOrNull() + + @TypeConverter + fun toLoginType(value: Int?) = value.asLoginTypeOrNull() + + @TypeConverter + fun toMetadataType(value: Int?) = value.asMetadataTypeOrNull() + + @TypeConverter + fun toNotificationType(value: Int?) = value.asNotificationTypeOrNull() + + @TypeConverter + fun toNavTarget(value: Int?) = value.asNavTargetOrNull() ?: NavTarget.HOME +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/AnnouncementDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/AnnouncementDao.kt index 88b3ccd1..aa35c810 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/AnnouncementDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/AnnouncementDao.kt @@ -14,6 +14,7 @@ import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.data.db.AppDb import pl.szczodrzynski.edziennik.data.db.entity.Announcement import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull @Dao @@ -26,7 +27,7 @@ abstract class AnnouncementDao : BaseDao { teachers.teacherName ||" "|| teachers.teacherSurname AS teacherName FROM announcements LEFT JOIN teachers USING(profileId, teacherId) - LEFT JOIN metadata ON announcementId = thingId AND thingType = ${Metadata.TYPE_ANNOUNCEMENT} AND metadata.profileId = announcements.profileId + LEFT JOIN metadata ON announcementId = thingId AND thingType = 7 AND metadata.profileId = announcements.profileId """ private const val ORDER_BY = """ORDER BY addedDate DESC""" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/AttendanceDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/AttendanceDao.kt index 3ffdc1b8..a420c5b5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/AttendanceDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/AttendanceDao.kt @@ -28,7 +28,7 @@ abstract class AttendanceDao : BaseDao { FROM attendances LEFT JOIN teachers USING(profileId, teacherId) LEFT JOIN subjects USING(profileId, subjectId) - LEFT JOIN metadata ON attendanceId = thingId AND thingType = ${Metadata.TYPE_ATTENDANCE} AND metadata.profileId = attendances.profileId + LEFT JOIN metadata ON attendanceId = thingId AND thingType = 3 AND metadata.profileId = attendances.profileId """ private const val ORDER_BY = """ORDER BY attendanceDate DESC, attendanceTime DESC""" @@ -64,6 +64,8 @@ abstract class AttendanceDao : BaseDao { getRawNow("$QUERY WHERE notified = 0 $ORDER_BY") fun getNotNotifiedNow(profileId: Int) = getRawNow("$QUERY WHERE attendances.profileId = $profileId AND notified = 0 $ORDER_BY") + fun getAllByDateNow(profileId: Int, date: Date) = + getRawNow("$QUERY WHERE attendances.profileId = $profileId AND attendanceDate = '${date.stringY_m_d}' $ORDER_BY") // GET ONE - NOW fun getByIdNow(profileId: Int, id: Long) = diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/BaseDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/BaseDao.kt index 0ae961db..60089423 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/BaseDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/BaseDao.kt @@ -12,15 +12,19 @@ import pl.szczodrzynski.edziennik.data.db.entity.Keepable @Dao interface BaseDao { + @Transaction @RawQuery fun getRaw(query: SupportSQLiteQuery): LiveData> fun getRaw(query: String) = getRaw(SimpleSQLiteQuery(query)) + @Transaction @RawQuery fun getOne(query: SupportSQLiteQuery): LiveData fun getOne(query: String) = getOne(SimpleSQLiteQuery(query)) + @Transaction @RawQuery fun getRawNow(query: SupportSQLiteQuery): List fun getRawNow(query: String) = getRawNow(SimpleSQLiteQuery(query)) + @Transaction @RawQuery fun getOneNow(query: SupportSQLiteQuery): F? fun getOneNow(query: String) = getOneNow(SimpleSQLiteQuery(query)) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventDao.kt index 897d53ff..09605429 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventDao.kt @@ -16,6 +16,7 @@ import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.data.db.AppDb import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -35,7 +36,7 @@ abstract class EventDao : BaseDao { LEFT JOIN subjects USING(profileId, subjectId) LEFT JOIN teams USING(profileId, teamId) LEFT JOIN eventTypes USING(profileId, eventType) - LEFT JOIN metadata ON eventId = thingId AND (thingType = ${Metadata.TYPE_EVENT} OR thingType = ${Metadata.TYPE_HOMEWORK}) AND metadata.profileId = events.profileId + LEFT JOIN metadata ON eventId = thingId AND (thingType = 4 OR thingType = 5) AND metadata.profileId = events.profileId """ private const val ORDER_BY = """GROUP BY eventId ORDER BY eventDate, eventTime, addedDate ASC""" @@ -84,6 +85,10 @@ abstract class EventDao : BaseDao { fun getAllByDateNow(profileId: Int, date: Date) = getRawNow("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId AND eventDate = '${date.stringY_m_d}' $ORDER_BY") + // GET ONE - LIVE DATA + fun getById(profileId: Int, id: Long) = + getOne("$QUERY WHERE events.profileId = $profileId AND eventId = $id") + // GET ONE - NOW fun getByIdNow(profileId: Int, id: Long) = getOneNow("$QUERY WHERE events.profileId = $profileId AND eventId = $id") @@ -105,7 +110,7 @@ abstract class EventDao : BaseDao { abstract fun removeNotManual(profileId: Int)*/ @RawQuery - abstract fun dontKeepFuture(query: SupportSQLiteQuery?): Long + abstract fun dontKeepFuture(query: SupportSQLiteQuery): Long @Transaction open fun dontKeepFuture(profileId: Int, todayDate: Date, filter: String) { @@ -128,7 +133,7 @@ abstract class EventDao : BaseDao { dontKeepFuture(profileId, todayDate, "eventType NOT IN " + exceptTypes.toString().replace('[', '(').replace(']', ')')) } - @Query("UPDATE metadata SET seen = :seen WHERE profileId = :profileId AND (thingType = " + Metadata.TYPE_EVENT + " OR thingType = " + Metadata.TYPE_LESSON_CHANGE + " OR thingType = " + Metadata.TYPE_HOMEWORK + ") AND thingId IN (SELECT eventId FROM events WHERE profileId = :profileId AND eventDate = :date)") + @Query("UPDATE metadata SET seen = :seen WHERE profileId = :profileId AND (thingType = 4 OR thingType = 6 OR thingType = 5) AND thingId IN (SELECT eventId FROM events WHERE profileId = :profileId AND eventDate = :date)") abstract fun setSeenByDate(profileId: Int, date: Date, seen: Boolean) @Query("UPDATE events SET eventBlacklisted = :blacklisted WHERE profileId = :profileId AND eventId = :eventId") @@ -138,12 +143,12 @@ abstract class EventDao : BaseDao { abstract fun remove(profileId: Int, id: Long) @Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = :thingType AND thingId = :thingId") - abstract fun removeMetadata(profileId: Int, thingType: Int, thingId: Long) + abstract fun removeMetadata(profileId: Int, thingType: MetadataType, thingId: Long) @Transaction open fun remove(profileId: Int, type: Long, id: Long) { remove(profileId, id) - removeMetadata(profileId, if (type == Event.TYPE_HOMEWORK) Metadata.TYPE_HOMEWORK else Metadata.TYPE_EVENT, id) + removeMetadata(profileId, if (type == Event.TYPE_HOMEWORK) MetadataType.HOMEWORK else MetadataType.EVENT, id) } @Transaction diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.kt index 28be6d65..2f3f5c94 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/EventTypeDao.kt @@ -3,36 +3,16 @@ */ package pl.szczodrzynski.edziennik.data.db.dao -import android.content.Context +import android.graphics.Color import androidx.lifecycle.LiveData import androidx.room.Dao import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query -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_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_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 +import pl.szczodrzynski.edziennik.config.AppData import pl.szczodrzynski.edziennik.data.db.entity.EventType +import pl.szczodrzynski.edziennik.data.db.entity.EventType.Companion.SOURCE_DEFAULT +import pl.szczodrzynski.edziennik.data.db.entity.Profile @Dao abstract class EventTypeDao { @@ -45,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? @@ -57,21 +40,37 @@ abstract class EventTypeDao { @get:Query("SELECT * FROM eventTypes") abstract val allNow: List - fun addDefaultTypes(context: Context, profileId: Int): List { - val typeList = listOf( - EventType(profileId, TYPE_HOMEWORK, context.getString(R.string.event_type_homework), COLOR_HOMEWORK), - EventType(profileId, TYPE_DEFAULT, context.getString(R.string.event_other), COLOR_DEFAULT), - EventType(profileId, TYPE_EXAM, context.getString(R.string.event_exam), COLOR_EXAM), - EventType(profileId, TYPE_SHORT_QUIZ, context.getString(R.string.event_short_quiz), COLOR_SHORT_QUIZ), - EventType(profileId, TYPE_ESSAY, context.getString(R.string.event_essay), COLOR_ESSAY), - EventType(profileId, TYPE_PROJECT, context.getString(R.string.event_project), COLOR_PROJECT), - EventType(profileId, TYPE_PT_MEETING, context.getString(R.string.event_pt_meeting), COLOR_PT_MEETING), - EventType(profileId, TYPE_EXCURSION, context.getString(R.string.event_excursion), COLOR_EXCURSION), - EventType(profileId, TYPE_READING, context.getString(R.string.event_reading), COLOR_READING), - EventType(profileId, TYPE_CLASS_EVENT, context.getString(R.string.event_class_event), COLOR_CLASS_EVENT), - EventType(profileId, TYPE_INFORMATION, context.getString(R.string.event_information), COLOR_INFORMATION) - ) + fun addDefaultTypes(profile: Profile): List { + val data = AppData.get(profile.loginStoreType) + var order = 100 + val typeList = data.eventTypes.map { + EventType( + profileId = profile.id, + id = it.id, + name = it.name, + color = Color.parseColor(it.color), + order = order++, + source = SOURCE_DEFAULT, + ) + } addAll(typeList) return typeList } + + fun getAllWithDefaults(profile: Profile): List { + 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 + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/GradeDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/GradeDao.kt index 6254f80b..4703eca9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/GradeDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/GradeDao.kt @@ -33,7 +33,7 @@ abstract class GradeDao : BaseDao { FROM grades LEFT JOIN teachers USING(profileId, teacherId) LEFT JOIN subjects USING(profileId, subjectId) - LEFT JOIN metadata ON gradeId = thingId AND thingType = ${Metadata.TYPE_GRADE} AND metadata.profileId = grades.profileId + LEFT JOIN metadata ON gradeId = thingId AND thingType = 1 AND metadata.profileId = grades.profileId """ private const val ORDER_BY = """ORDER BY addedDate DESC""" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/LuckyNumberDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/LuckyNumberDao.kt index 3c752124..b4d788c5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/LuckyNumberDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/LuckyNumberDao.kt @@ -25,7 +25,7 @@ abstract class LuckyNumberDao : BaseDao { SELECT * FROM luckyNumbers - LEFT JOIN metadata ON luckyNumberDate = thingId AND thingType = ${Metadata.TYPE_LUCKY_NUMBER} AND metadata.profileId = luckyNumbers.profileId + LEFT JOIN metadata ON luckyNumberDate = thingId AND thingType = 10 AND metadata.profileId = luckyNumbers.profileId """ private const val ORDER_BY = """ORDER BY luckyNumberDate DESC""" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageDao.kt index 51d56854..59e9ca82 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageDao.kt @@ -26,10 +26,10 @@ abstract class MessageDao : BaseDao { teachers.teacherName ||" "|| teachers.teacherSurname AS senderName FROM messages LEFT JOIN teachers ON teachers.profileId = messages.profileId AND teacherId = senderId - LEFT JOIN metadata ON messageId = thingId AND thingType = ${Metadata.TYPE_MESSAGE} AND metadata.profileId = messages.profileId + LEFT JOIN metadata ON messageId = thingId AND thingType = 8 AND metadata.profileId = messages.profileId """ - private const val ORDER_BY = """ORDER BY messageIsPinned, addedDate DESC""" + private const val ORDER_BY = """ORDER BY messageIsPinned DESC, addedDate DESC""" } private val selective by lazy { MessageDaoSelective(App.db) } @@ -51,6 +51,9 @@ abstract class MessageDao : BaseDao { @Query("DELETE FROM messages WHERE keep = 0") abstract override fun removeNotKept() + @Query("DELETE FROM messages WHERE profileId = :profileId AND messageId = :messageId") + abstract fun delete(profileId: Int, messageId: Long) + // GET ALL - LIVE DATA fun getAll(profileId: Int) = getRaw("$QUERY WHERE messages.profileId = $profileId $ORDER_BY") diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageRecipientDao.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageRecipientDao.java index 20e6fe90..b33adbff 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageRecipientDao.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MessageRecipientDao.java @@ -4,8 +4,6 @@ package pl.szczodrzynski.edziennik.data.db.dao; -import java.util.List; - import androidx.room.Dao; import androidx.room.Insert; import androidx.room.OnConflictStrategy; @@ -14,6 +12,8 @@ import androidx.room.RawQuery; import androidx.sqlite.db.SimpleSQLiteQuery; import androidx.sqlite.db.SupportSQLiteQuery; +import java.util.List; + import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient; import pl.szczodrzynski.edziennik.data.db.full.MessageRecipientFull; @@ -22,6 +22,9 @@ public abstract class MessageRecipientDao { @Query("DELETE FROM messageRecipients WHERE profileId = :profileId") public abstract void clear(int profileId); + @Query("DELETE FROM messageRecipients WHERE profileId = :profileId AND messageId = :messageId") + public abstract void clearFor(int profileId, long messageId); + @Insert(onConflict = OnConflictStrategy.REPLACE) public abstract long add(MessageRecipient messageRecipient); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MetadataDao.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MetadataDao.java index eac92f21..c8cb295b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MetadataDao.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/MetadataDao.java @@ -20,18 +20,10 @@ import pl.szczodrzynski.edziennik.data.db.entity.Grade; import pl.szczodrzynski.edziennik.data.db.entity.Message; import pl.szczodrzynski.edziennik.data.db.entity.Metadata; import pl.szczodrzynski.edziennik.data.db.entity.Notice; +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType; import pl.szczodrzynski.edziennik.data.db.full.LessonFull; import pl.szczodrzynski.edziennik.utils.models.UnreadCounter; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_ANNOUNCEMENT; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_ATTENDANCE; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_EVENT; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_GRADE; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_HOMEWORK; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_LESSON_CHANGE; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_MESSAGE; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_NOTICE; - @Dao public abstract class MetadataDao { @Insert(onConflict = OnConflictStrategy.IGNORE) @@ -44,10 +36,10 @@ public abstract class MetadataDao { public abstract void addAllReplace(List metadataList); @Query("UPDATE metadata SET seen = :seen WHERE thingId = :thingId AND thingType = :thingType AND profileId = :profileId") - abstract void updateSeen(int profileId, int thingType, long thingId, boolean seen); + abstract void updateSeen(int profileId, MetadataType thingType, long thingId, boolean seen); @Query("UPDATE metadata SET notified = :notified WHERE thingId = :thingId AND thingType = :thingType AND profileId = :profileId") - abstract void updateNotified(int profileId, int thingType, long thingId, boolean notified); + abstract void updateNotified(int profileId, MetadataType thingType, long thingId, boolean notified); @@ -63,38 +55,38 @@ public abstract class MetadataDao { @Transaction public void setSeen(int profileId, Object o, boolean seen) { if (o instanceof Grade) { - if (add(new Metadata(profileId, TYPE_GRADE, ((Grade) o).getId(), seen, false)) == -1) { - updateSeen(profileId, TYPE_GRADE, ((Grade) o).getId(), seen); + if (add(new Metadata(profileId, MetadataType.GRADE, ((Grade) o).getId(), seen, false)) == -1) { + updateSeen(profileId, MetadataType.GRADE, ((Grade) o).getId(), seen); } } if (o instanceof Attendance) { - if (add(new Metadata(profileId, TYPE_ATTENDANCE, ((Attendance) o).getId(), seen, false)) == -1) { - updateSeen(profileId, TYPE_ATTENDANCE, ((Attendance) o).getId(), seen); + if (add(new Metadata(profileId, MetadataType.ATTENDANCE, ((Attendance) o).getId(), seen, false)) == -1) { + updateSeen(profileId, MetadataType.ATTENDANCE, ((Attendance) o).getId(), seen); } } if (o instanceof Notice) { - if (add(new Metadata(profileId, TYPE_NOTICE, ((Notice) o).getId(), seen, false)) == -1) { - updateSeen(profileId, TYPE_NOTICE, ((Notice) o).getId(), seen); + if (add(new Metadata(profileId, MetadataType.NOTICE, ((Notice) o).getId(), seen, false)) == -1) { + updateSeen(profileId, MetadataType.NOTICE, ((Notice) o).getId(), seen); } } if (o instanceof Event) { - if (add(new Metadata(profileId, ((Event) o).getType() == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, ((Event) o).getId(), seen, false)) == -1) { - updateSeen(profileId, ((Event) o).getType() == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, ((Event) o).getId(), seen); + if (add(new Metadata(profileId, ((Event) o).isHomework() ? MetadataType.HOMEWORK : MetadataType.EVENT, ((Event) o).getId(), seen, false)) == -1) { + updateSeen(profileId, ((Event) o).isHomework() ? MetadataType.HOMEWORK : MetadataType.EVENT, ((Event) o).getId(), seen); } } if (o instanceof LessonFull) { - if (add(new Metadata(profileId, TYPE_LESSON_CHANGE, ((LessonFull) o).getId(), seen, false)) == -1) { - updateSeen(profileId, TYPE_LESSON_CHANGE, ((LessonFull) o).getId(), seen); + if (add(new Metadata(profileId, MetadataType.LESSON_CHANGE, ((LessonFull) o).getId(), seen, false)) == -1) { + updateSeen(profileId, MetadataType.LESSON_CHANGE, ((LessonFull) o).getId(), seen); } } if (o instanceof Announcement) { - if (add(new Metadata(profileId, TYPE_ANNOUNCEMENT, ((Announcement) o).getId(), seen, false)) == -1) { - updateSeen(profileId, TYPE_ANNOUNCEMENT, ((Announcement) o).getId(), seen); + if (add(new Metadata(profileId, MetadataType.ANNOUNCEMENT, ((Announcement) o).getId(), seen, false)) == -1) { + updateSeen(profileId, MetadataType.ANNOUNCEMENT, ((Announcement) o).getId(), seen); } } if (o instanceof Message) { - if (add(new Metadata(profileId, TYPE_MESSAGE, ((Message) o).getId(), seen, false)) == -1) { - updateSeen(profileId, TYPE_MESSAGE, ((Message) o).getId(), seen); + if (add(new Metadata(profileId, MetadataType.MESSAGE, ((Message) o).getId(), seen, false)) == -1) { + updateSeen(profileId, MetadataType.MESSAGE, ((Message) o).getId(), seen); } } } @@ -102,38 +94,38 @@ public abstract class MetadataDao { @Transaction public void setNotified(int profileId, Object o, boolean notified) { if (o instanceof Grade) { - if (add(new Metadata(profileId, TYPE_GRADE, ((Grade) o).getId(), false, notified)) == -1) { - updateNotified(profileId, TYPE_GRADE, ((Grade) o).getId(), notified); + if (add(new Metadata(profileId, MetadataType.GRADE, ((Grade) o).getId(), false, notified)) == -1) { + updateNotified(profileId, MetadataType.GRADE, ((Grade) o).getId(), notified); } } if (o instanceof Attendance) { - if (add(new Metadata(profileId, TYPE_ATTENDANCE, ((Attendance) o).getId(), false, notified)) == -1) { - updateNotified(profileId, TYPE_ATTENDANCE, ((Attendance) o).getId(), notified); + if (add(new Metadata(profileId, MetadataType.ATTENDANCE, ((Attendance) o).getId(), false, notified)) == -1) { + updateNotified(profileId, MetadataType.ATTENDANCE, ((Attendance) o).getId(), notified); } } if (o instanceof Notice) { - if (add(new Metadata(profileId, TYPE_NOTICE, ((Notice) o).getId(), false, notified)) == -1) { - updateNotified(profileId, TYPE_NOTICE, ((Notice) o).getId(), notified); + if (add(new Metadata(profileId, MetadataType.NOTICE, ((Notice) o).getId(), false, notified)) == -1) { + updateNotified(profileId, MetadataType.NOTICE, ((Notice) o).getId(), notified); } } if (o instanceof Event) { - if (add(new Metadata(profileId, ((Event) o).getType() == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, ((Event) o).getId(), false, notified)) == -1) { - updateNotified(profileId, ((Event) o).getType() == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, ((Event) o).getId(), notified); + if (add(new Metadata(profileId, ((Event) o).isHomework() ? MetadataType.HOMEWORK : MetadataType.EVENT, ((Event) o).getId(), false, notified)) == -1) { + updateNotified(profileId, ((Event) o).isHomework() ? MetadataType.HOMEWORK : MetadataType.EVENT, ((Event) o).getId(), notified); } } if (o instanceof LessonFull) { - if (add(new Metadata(profileId, TYPE_LESSON_CHANGE, ((LessonFull) o).getId(), false, notified)) == -1) { - updateNotified(profileId, TYPE_LESSON_CHANGE, ((LessonFull) o).getId(), notified); + if (add(new Metadata(profileId, MetadataType.LESSON_CHANGE, ((LessonFull) o).getId(), false, notified)) == -1) { + updateNotified(profileId, MetadataType.LESSON_CHANGE, ((LessonFull) o).getId(), notified); } } if (o instanceof Announcement) { - if (add(new Metadata(profileId, TYPE_ANNOUNCEMENT, ((Announcement) o).getId(), false, notified)) == -1) { - updateNotified(profileId, TYPE_ANNOUNCEMENT, ((Announcement) o).getId(), notified); + if (add(new Metadata(profileId, MetadataType.ANNOUNCEMENT, ((Announcement) o).getId(), false, notified)) == -1) { + updateNotified(profileId, MetadataType.ANNOUNCEMENT, ((Announcement) o).getId(), notified); } } if (o instanceof Message) { - if (add(new Metadata(profileId, TYPE_MESSAGE, ((Message) o).getId(), false, notified)) == -1) { - updateNotified(profileId, TYPE_MESSAGE, ((Message) o).getId(), notified); + if (add(new Metadata(profileId, MetadataType.MESSAGE, ((Message) o).getId(), false, notified)) == -1) { + updateNotified(profileId, MetadataType.MESSAGE, ((Message) o).getId(), notified); } } } @@ -141,9 +133,9 @@ public abstract class MetadataDao { @Transaction public void setBoth(int profileId, Event o, boolean seen, boolean notified, long addedDate) { if (o != null) { - if (add(new Metadata(profileId, o.getType() == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, o.getId(), seen, notified)) == -1) { - updateSeen(profileId, o.getType() == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, o.getId(), seen); - updateNotified(profileId, o.getType() == Event.TYPE_HOMEWORK ? TYPE_HOMEWORK : TYPE_EVENT, o.getId(), notified); + if (add(new Metadata(profileId, o.isHomework() ? MetadataType.HOMEWORK : MetadataType.EVENT, o.getId(), seen, notified)) == -1) { + updateSeen(profileId, o.isHomework() ? MetadataType.HOMEWORK : MetadataType.EVENT, o.getId(), seen); + updateNotified(profileId, o.isHomework() ? MetadataType.HOMEWORK : MetadataType.EVENT, o.getId(), notified); } } } @@ -151,18 +143,18 @@ public abstract class MetadataDao { @Query("UPDATE metadata SET seen = :seen WHERE profileId = :profileId AND thingType = :thingType") - public abstract void setAllSeen(int profileId, int thingType, boolean seen); + public abstract void setAllSeen(int profileId, MetadataType thingType, boolean seen); @Query("UPDATE metadata SET notified = :notified WHERE profileId = :profileId AND thingType = :thingType") - public abstract void setAllNotified(int profileId, int thingType, boolean notified); + public abstract void setAllNotified(int profileId, MetadataType thingType, boolean notified); @Query("UPDATE metadata SET seen = :seen WHERE profileId = :profileId") public abstract void setAllSeen(int profileId, boolean seen); - @Query("UPDATE metadata SET seen = :seen WHERE profileId = :profileId AND thingType != " + TYPE_MESSAGE) + @Query("UPDATE metadata SET seen = :seen WHERE profileId = :profileId AND thingType != 8") public abstract void setAllSeenExceptMessages(int profileId, boolean seen); - @Query("UPDATE metadata SET seen = :seen WHERE profileId = :profileId AND thingType != " + TYPE_MESSAGE + " AND thingType != " + TYPE_ANNOUNCEMENT) + @Query("UPDATE metadata SET seen = :seen WHERE profileId = :profileId AND thingType != 8 AND thingType != 7") public abstract void setAllSeenExceptMessagesAndAnnouncements(int profileId, boolean seen); @Query("UPDATE metadata SET notified = :notified WHERE profileId = :profileId") @@ -174,10 +166,10 @@ public abstract class MetadataDao { @Query("SELECT count() FROM metadata WHERE profileId = :profileId AND thingType = :thingType AND seen = 0") - public abstract LiveData countUnseen(int profileId, int thingType); + public abstract LiveData countUnseen(int profileId, MetadataType thingType); @Query("SELECT count() FROM metadata WHERE profileId = :profileId AND thingType = :thingType AND seen = 0") - public abstract Integer countUnseenNow(int profileId, int thingType); + public abstract Integer countUnseenNow(int profileId, MetadataType thingType); @Query("SELECT count() FROM metadata WHERE profileId = :profileId AND seen = 0") public abstract LiveData countUnseen(int profileId); @@ -191,7 +183,7 @@ public abstract class MetadataDao { @Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = :thingType AND thingId = :thingId") - public abstract void delete(int profileId, int thingType, long thingId); + public abstract void delete(int profileId, MetadataType thingType, long thingId); @Query("DELETE FROM metadata WHERE profileId = :profileId") public abstract void deleteAll(int profileId); @@ -203,25 +195,25 @@ public abstract class MetadataDao { - @Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = "+TYPE_GRADE+" AND thingId NOT IN (SELECT gradeId FROM grades WHERE profileId = :profileId);") + @Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = 1 AND thingId NOT IN (SELECT gradeId FROM grades WHERE profileId = :profileId);") public abstract void deleteUnusedGrades(int profileId); - @Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = "+TYPE_NOTICE+" AND thingId NOT IN (SELECT noticeId FROM notices WHERE profileId = :profileId);") + @Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = 2 AND thingId NOT IN (SELECT noticeId FROM notices WHERE profileId = :profileId);") public abstract void deleteUnusedNotices(int profileId); - @Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = "+TYPE_ATTENDANCE+" AND thingId NOT IN (SELECT attendanceId FROM attendances WHERE profileId = :profileId);") + @Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = 3 AND thingId NOT IN (SELECT attendanceId FROM attendances WHERE profileId = :profileId);") public abstract void deleteUnusedAttendance(int profileId); - @Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = "+TYPE_EVENT+" AND thingId NOT IN (SELECT eventId FROM events WHERE profileId = :profileId AND eventType != -1);") + @Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = 4 AND thingId NOT IN (SELECT eventId FROM events WHERE profileId = :profileId AND eventType != -1);") public abstract void deleteUnusedEvents(int profileId); - @Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = "+TYPE_HOMEWORK+" AND thingId NOT IN (SELECT eventId FROM events WHERE profileId = :profileId AND eventType = -1);") + @Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = 5 AND thingId NOT IN (SELECT eventId FROM events WHERE profileId = :profileId AND eventType = -1);") public abstract void deleteUnusedHomework(int profileId); - @Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = "+TYPE_ANNOUNCEMENT+" AND thingId NOT IN (SELECT announcementId FROM announcements WHERE profileId = :profileId);") + @Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = 7 AND thingId NOT IN (SELECT announcementId FROM announcements WHERE profileId = :profileId);") public abstract void deleteUnusedAnnouncements(int profileId); - @Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = "+TYPE_MESSAGE+" AND thingId NOT IN (SELECT messageId FROM messages WHERE profileId = :profileId);") + @Query("DELETE FROM metadata WHERE profileId = :profileId AND thingType = 8 AND thingId NOT IN (SELECT messageId FROM messages WHERE profileId = :profileId);") public abstract void deleteUnusedMessages(int profileId); @Transaction diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/NoteDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/NoteDao.kt new file mode 100644 index 00000000..4ad421bc --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/NoteDao.kt @@ -0,0 +1,43 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-23. + */ + +package pl.szczodrzynski.edziennik.data.db.dao + +import androidx.lifecycle.LiveData +import androidx.room.* +import pl.szczodrzynski.edziennik.data.db.entity.Note + +@Dao +interface NoteDao { + companion object { + private const val ORDER_BY = "ORDER BY addedDate DESC" + } + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun add(note: Note) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun addAll(noteList: List) + + @Delete + fun delete(note: Note) + + @Query("DELETE FROM notes WHERE profileId = :profileId AND noteId = :noteId") + fun remove(profileId: Int, noteId: Long) + + @Query("SELECT * FROM notes WHERE profileId = :profileId AND noteId = :noteId $ORDER_BY") + fun get(profileId: Int, noteId: Long): LiveData + + @Query("SELECT * FROM notes WHERE profileId = :profileId AND noteId = :noteId $ORDER_BY") + fun getNow(profileId: Int, noteId: Long): Note? + + @Query("SELECT * FROM notes WHERE profileId = :profileId $ORDER_BY") + fun getAll(profileId: Int): LiveData> + + @Query("SELECT * FROM notes WHERE profileId = :profileId AND noteOwnerType = :ownerType AND noteOwnerId = :ownerId $ORDER_BY") + fun getAllFor(profileId: Int, ownerType: Note.OwnerType, ownerId: Long): LiveData> + + @Query("SELECT * FROM notes WHERE profileId = :profileId AND noteOwnerType IS NULL $ORDER_BY") + fun getAllNoOwner(profileId: Int): LiveData> +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/NoticeDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/NoticeDao.kt index 51f3e839..20443c22 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/NoticeDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/NoticeDao.kt @@ -26,7 +26,7 @@ abstract class NoticeDao : BaseDao { teachers.teacherName ||" "|| teachers.teacherSurname AS teacherName FROM notices LEFT JOIN teachers USING(profileId, teacherId) - LEFT JOIN metadata ON noticeId = thingId AND thingType = ${Metadata.TYPE_NOTICE} AND metadata.profileId = notices.profileId + LEFT JOIN metadata ON noticeId = thingId AND thingType = 2 AND metadata.profileId = notices.profileId """ private const val ORDER_BY = """ORDER BY addedDate DESC""" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/ProfileDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/ProfileDao.kt index 6d1e799d..f83a8e76 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/ProfileDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/ProfileDao.kt @@ -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> @@ -49,7 +52,7 @@ interface ProfileDao { @get:Query("SELECT profileId FROM profiles WHERE profileId >= 0 ORDER BY profileId") val idsNow: List - @Query("SELECT profiles.* FROM teams JOIN profiles USING(profileId) WHERE teamCode = :teamCode AND registration = " + Profile.REGISTRATION_ENABLED + " AND enableSharedEvents = 1") + @Query("SELECT profiles.* FROM teams JOIN profiles USING(profileId) WHERE teamCode = :teamCode AND registration = " + Profile.REGISTRATION_ENABLED) fun getByTeamCodeNowWithRegistration(teamCode: String?): List @get:Query("SELECT profileId FROM profiles WHERE profileId > 0 ORDER BY profileId ASC LIMIT 1") diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TeacherAbsenceDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TeacherAbsenceDao.kt index f12248b8..de65f730 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TeacherAbsenceDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TeacherAbsenceDao.kt @@ -27,7 +27,7 @@ abstract class TeacherAbsenceDao : BaseDao { teachers.teacherName ||" "|| teachers.teacherSurname AS teacherName FROM teacherAbsence LEFT JOIN teachers USING(profileId, teacherId) - LEFT JOIN metadata ON teacherAbsenceId = thingId AND thingType = ${Metadata.TYPE_TEACHER_ABSENCE} AND metadata.profileId = teacherAbsence.profileId + LEFT JOIN metadata ON teacherAbsenceId = thingId AND thingType = 9 AND metadata.profileId = teacherAbsence.profileId """ private const val ORDER_BY = """ORDER BY teacherAbsenceDateFrom ASC""" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TimetableDao.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TimetableDao.kt index c7cbee12..701dabf6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TimetableDao.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/TimetableDao.kt @@ -38,7 +38,7 @@ abstract class TimetableDao : BaseDao { LEFT JOIN subjects AS oldS ON timetable.profileId = oldS.profileId AND timetable.oldSubjectId = oldS.subjectId LEFT JOIN teachers AS oldT ON timetable.profileId = oldT.profileId AND timetable.oldTeacherId = oldT.teacherId LEFT JOIN teams AS oldG ON timetable.profileId = oldG.profileId AND timetable.oldTeamId = oldG.teamId - LEFT JOIN metadata ON id = thingId AND thingType = ${Metadata.TYPE_LESSON_CHANGE} AND metadata.profileId = timetable.profileId + LEFT JOIN metadata ON id = thingId AND thingType = 6 AND metadata.profileId = timetable.profileId """ private const val ORDER_BY = """ORDER BY profileId, id, type""" @@ -84,6 +84,8 @@ abstract class TimetableDao : BaseDao { "LIMIT 1") fun getBetweenDates(dateFrom: Date, dateTo: Date) = getRaw("$QUERY WHERE (type != 3 AND date >= '${dateFrom.stringY_m_d}' AND date <= '${dateTo.stringY_m_d}') OR ((type = 3 OR type = 1) AND oldDate >= '${dateFrom.stringY_m_d}' AND oldDate <= '${dateTo.stringY_m_d}') $ORDER_BY") + fun getChanges(profileId: Int) = + getRaw("$QUERY WHERE timetable.profileId = $profileId AND $IS_CHANGED $ORDER_BY") // GET ALL - NOW fun getAllNow(profileId: Int) = @@ -105,12 +107,15 @@ abstract class TimetableDao : BaseDao { fun getByIdNow(profileId: Int, id: Long) = getOneNow("$QUERY WHERE timetable.profileId = $profileId AND timetable.id = $id") - @Query("UPDATE timetable SET keep = 0 WHERE profileId = :profileId AND type != -1 AND ((type != 3 AND date >= :dateFrom) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom))") - abstract fun dontKeepFromDate(profileId: Int, dateFrom: Date) + fun getByOwnerIdNow(profileId: Int, ownerId: Long) = + getOneNow("$QUERY WHERE timetable.profileId = $profileId AND timetable.ownerId = $ownerId") - @Query("UPDATE timetable SET keep = 0 WHERE profileId = :profileId AND type != -1 AND ((type != 3 AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate <= :dateTo))") - abstract fun dontKeepToDate(profileId: Int, dateTo: Date) + @Query("UPDATE timetable SET keep = 0 WHERE profileId = :profileId AND isExtra = :isExtra AND type != -1 AND ((type != 3 AND date >= :dateFrom) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom))") + abstract fun dontKeepFromDate(profileId: Int, dateFrom: Date, isExtra: Boolean) - @Query("UPDATE timetable SET keep = 0 WHERE profileId = :profileId AND type != -1 AND ((type != 3 AND date >= :dateFrom AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom AND oldDate <= :dateTo))") - abstract fun dontKeepBetweenDates(profileId: Int, dateFrom: Date, dateTo: Date) + @Query("UPDATE timetable SET keep = 0 WHERE profileId = :profileId AND isExtra = :isExtra AND type != -1 AND ((type != 3 AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate <= :dateTo))") + abstract fun dontKeepToDate(profileId: Int, dateTo: Date, isExtra: Boolean) + + @Query("UPDATE timetable SET keep = 0 WHERE profileId = :profileId AND isExtra = :isExtra AND type != -1 AND ((type != 3 AND date >= :dateFrom AND date <= :dateTo) OR ((type = 3 OR type = 1) AND oldDate >= :dateFrom AND oldDate <= :dateTo))") + abstract fun dontKeepBetweenDates(profileId: Int, dateFrom: Date, dateTo: Date, isExtra: Boolean) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EndpointTimer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EndpointTimer.kt index 05676131..a40df7da 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EndpointTimer.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EndpointTimer.kt @@ -6,6 +6,7 @@ package pl.szczodrzynski.edziennik.data.db.entity import androidx.room.ColumnInfo import androidx.room.Entity +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType const val SYNC_NEVER = 0L const val SYNC_ALWAYS = 1L @@ -26,7 +27,7 @@ data class EndpointTimer ( var nextSync: Long = SYNC_ALWAYS, @ColumnInfo(name = "endpointViewId") - var viewId: Int? = null + var featureType: FeatureType? = null ) { @@ -49,17 +50,15 @@ data class EndpointTimer ( } /** - * Set this timer to sync only if [viewId] is the only + * Set this timer to sync only if [featureType] is the only * selected feature during the current process. - * - * [viewId] may be [DRAWER_ITEM_HOME] to sync only if all features are selected. */ - fun syncWhenView(viewId: Int): EndpointTimer { + fun syncWithFeature(featureType: FeatureType): EndpointTimer { // set to never sync if nextSync is not already a timestamp if (nextSync < 10) { this.nextSync = SYNC_NEVER } - this.viewId = viewId + this.featureType = featureType return this } @@ -68,7 +67,7 @@ data class EndpointTimer ( */ fun syncAlways(): EndpointTimer { nextSync = SYNC_ALWAYS - viewId = null + featureType = null return this } @@ -77,7 +76,7 @@ data class EndpointTimer ( */ fun syncNever(): EndpointTimer { nextSync = SYNC_NEVER - viewId = null + featureType = null return this } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.kt index 0dcaa83e..505ed086 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Event.kt @@ -8,8 +8,9 @@ import androidx.room.Entity import androidx.room.Ignore import androidx.room.Index import com.google.gson.annotations.SerializedName -import pl.szczodrzynski.edziennik.MINUTE import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.ext.MINUTE +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time import java.util.* @@ -45,6 +46,7 @@ open class Event( var addedDate: Long = System.currentTimeMillis() ) : Keepable() { companion object { + const val TYPE_ELEARNING = -5L const val TYPE_UNDEFINED = -2L const val TYPE_HOMEWORK = -1L const val TYPE_DEFAULT = 0L @@ -57,7 +59,7 @@ open class Event( const val TYPE_READING = 7L const val TYPE_CLASS_EVENT = 8L const val TYPE_INFORMATION = 9L - const val TYPE_TEACHER_ABSENCE = 10L + const val COLOR_ELEARNING = 0xfff57f17.toInt() const val COLOR_HOMEWORK = 0xff795548.toInt() const val COLOR_DEFAULT = 0xffffc107.toInt() const val COLOR_EXAM = 0xfff44336.toInt() @@ -69,20 +71,36 @@ open class Event( const val COLOR_READING = 0xFFFFEB3B.toInt() const val COLOR_CLASS_EVENT = 0xff388e3c.toInt() const val COLOR_INFORMATION = 0xff039be5.toInt() - const val COLOR_TEACHER_ABSENCE = 0xff039be5.toInt() } + /** + * Added manually - added by self, shared by self, or shared by someone else. + */ @ColumnInfo(name = "eventAddedManually") var addedManually: Boolean = false + get() = field || isShared + + /** + * Shared by - user code who shared the event. Null if not shared. + * "Self" if shared by this app user. + */ @ColumnInfo(name = "eventSharedBy") var sharedBy: String? = null @ColumnInfo(name = "eventSharedByName") var sharedByName: String? = null + @ColumnInfo(name = "eventBlacklisted") var blacklisted: Boolean = false @ColumnInfo(name = "eventIsDone") var isDone: Boolean = false + /** + * Whether the full contents of the event are already stored locally. + * There may be a need to download the full topic or body. + */ + @ColumnInfo(name = "eventIsDownloaded") + var isDownloaded: Boolean = true + /** * Body/text of the event, if this is a [TYPE_HOMEWORK]. * May be null if the body is not downloaded yet, or the type is not [TYPE_HOMEWORK]. @@ -90,9 +108,32 @@ open class Event( * or the topic contains the body already. */ var homeworkBody: String? = null + val hasAttachments + get() = attachmentIds.isNotNullNorEmpty() var attachmentIds: MutableList? = null var attachmentNames: MutableList? = null + val isHomework + get() = type == TYPE_HOMEWORK + + /** + * Whether the event is shared by anyone. Note that this implies [addedManually]. + */ + val isShared + get() = sharedBy != null + + /** + * Whether the event is shared by "self" (this app user). + */ + val isSharedSent + get() = sharedBy == "self" + + /** + * Whether the event is shared by someone else from the class group. + */ + val isSharedReceived + get() = sharedBy != null && sharedBy != "self" + /** * Add an attachment * @param id attachment ID @@ -116,14 +157,7 @@ open class Event( var showAsUnseen: Boolean? = null val startTimeCalendar: Calendar - get() = Calendar.getInstance().also { it.set( - date.year, - date.month - 1, - date.day, - time?.hour ?: 0, - time?.minute ?: 0, - time?.second ?: 0 - ) } + get() = date.getAsCalendar(time) val endTimeCalendar: Calendar get() = startTimeCalendar.also { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.java deleted file mode 100644 index bf981e84..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2020-1-6 - */ - -package pl.szczodrzynski.edziennik.data.db.entity; - -import android.graphics.Color; - -import androidx.room.ColumnInfo; -import androidx.room.Entity; - -@Entity(tableName = "eventTypes", - primaryKeys = {"profileId", "eventType"}) -public class EventType { - public int profileId; - - @ColumnInfo(name = "eventType") - public long id; - - @ColumnInfo(name = "eventTypeName") - public String name; - @ColumnInfo(name = "eventTypeColor") - public int color; - - public EventType(int profileId, long id, String name, int color) { - this.profileId = profileId; - this.id = id; - this.name = name; - this.color = color; - } - - public EventType(int profileId, int id, String name, String color) { - this(profileId, id, name, Color.parseColor(color)); - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.kt new file mode 100644 index 00000000..00ba4c04 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/EventType.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-19. + */ +package pl.szczodrzynski.edziennik.data.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity + +@Entity( + tableName = "eventTypes", + primaryKeys = ["profileId", "eventType"] +) +class EventType( + val profileId: Int, + + @ColumnInfo(name = "eventType") + val id: Long, + + @ColumnInfo(name = "eventTypeName") + val name: String, + @ColumnInfo(name = "eventTypeColor") + val color: Int, + @ColumnInfo(name = "eventTypeOrder") + var order: Int = id.toInt(), + @ColumnInfo(name = "eventTypeSource") + val source: Int = SOURCE_REGISTER +) { + companion object { + const val SOURCE_DEFAULT = 0 + const val SOURCE_REGISTER = 1 + const val SOURCE_CUSTOM = 2 + const val SOURCE_SHARED = 3 + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Lesson.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Lesson.kt index 0c97ba24..8827993b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Lesson.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Lesson.kt @@ -48,6 +48,15 @@ open class Lesson( var oldTeamId: Long? = null var oldClassroom: String? = null + var isExtra: Boolean = false + + /** + * Stable ID denoting this lesson, used for note sharing (i.e. [profileId]-independent). + * + * This is simply the Unix timestamp of the lesson (in seconds). + */ + var ownerId: Long = id + val displayDate: Date? get() { if (type == TYPE_SHIFTED_SOURCE) @@ -67,11 +76,18 @@ open class Lesson( val isChange get() = type == TYPE_CHANGE || type == TYPE_SHIFTED_TARGET - fun buildId(): Long = (displayDate?.combineWith(displayStartTime) ?: 0L) / 6L * 10L + (hashCode() and 0xFFFF) + fun buildId(): Long = + (displayDate?.combineWith(displayStartTime) ?: 0L) / 6L * 10L + + (hashCode() and 0xFFFF) + + fun buildOwnerId(): Long = + (displayDate?.combineWith(displayStartTime) ?: 0L) @Ignore var showAsUnseen = false + var color: Int? = null + override fun toString(): String { return "Lesson(profileId=$profileId, " + "id=$id, " + @@ -121,11 +137,13 @@ open class Lesson( return true } - override fun hashCode(): Int { // intentionally ignoring ID and display* here + override fun hashCode(): Int { // intentionally ignoring ID, display* and isExtra here var result = profileId result = 31 * result + type result = 31 * result + (date?.hashCode() ?: 0) - result = 31 * result + (lessonNumber ?: 0) + // this creates problems in Mobidziennik with extra lessons + // ... and is not generally useful anyway + // result = 31 * result + (lessonNumber ?: 0) result = 31 * result + (startTime?.hashCode() ?: 0) result = 31 * result + (endTime?.hashCode() ?: 0) result = 31 * result + (subjectId?.hashCode() ?: 0) @@ -133,7 +151,7 @@ open class Lesson( result = 31 * result + (teamId?.hashCode() ?: 0) result = 31 * result + (classroom?.hashCode() ?: 0) result = 31 * result + (oldDate?.hashCode() ?: 0) - result = 31 * result + (oldLessonNumber ?: 0) + // result = 31 * result + (oldLessonNumber ?: 0) result = 31 * result + (oldStartTime?.hashCode() ?: 0) result = 31 * result + (oldEndTime?.hashCode() ?: 0) result = 31 * result + (oldSubjectId?.hashCode() ?: 0) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/LoginStore.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/LoginStore.kt index 192bd1f6..f9ba31da 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/LoginStore.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/LoginStore.kt @@ -8,7 +8,9 @@ import android.os.Bundle import androidx.room.ColumnInfo import androidx.room.Entity import com.google.gson.JsonObject -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.db.enums.LoginMode +import pl.szczodrzynski.edziennik.data.db.enums.LoginType +import pl.szczodrzynski.edziennik.ext.* @Entity(tableName = "loginStores", primaryKeys = ["loginStoreId"]) class LoginStore( @@ -16,25 +18,14 @@ class LoginStore( val id: Int, @ColumnInfo(name = "loginStoreType") - val type: Int, + val type: LoginType, @ColumnInfo(name = "loginStoreMode") - val mode: Int, + val mode: LoginMode, @ColumnInfo(name = "loginStoreData") val data: JsonObject = JsonObject() ) { - companion object { - const val LOGIN_TYPE_MOBIDZIENNIK = 1 - const val LOGIN_TYPE_LIBRUS = 2 - const val LOGIN_TYPE_VULCAN = 4 - const val LOGIN_TYPE_IDZIENNIK = 3 - const val LOGIN_TYPE_EDUDZIENNIK = 5 - const val LOGIN_TYPE_DEMO = 20 - const val LOGIN_MODE_LIBRUS_EMAIL = 0 - const val LOGIN_MODE_LIBRUS_SYNERGIA = 1 - const val LOGIN_MODE_LIBRUS_JST = 2 - } fun hasLoginData(key: String) = data.has(key) fun getLoginData(key: String, defaultValue: Boolean) = data.getBoolean(key) ?: defaultValue @@ -64,31 +55,11 @@ class LoginStore( } } - fun type(): String { - return when (type) { - LOGIN_TYPE_MOBIDZIENNIK -> "LOGIN_TYPE_MOBIDZIENNIK" - LOGIN_TYPE_LIBRUS -> "LOGIN_TYPE_LIBRUS" - LOGIN_TYPE_IDZIENNIK -> "LOGIN_TYPE_IDZIENNIK" - LOGIN_TYPE_VULCAN -> "LOGIN_TYPE_VULCAN" - LOGIN_TYPE_DEMO -> "LOGIN_TYPE_DEMO" - else -> "unknown" - } - } - - fun mode(): String { - return when (mode) { - LOGIN_MODE_LIBRUS_EMAIL -> "LOGIN_MODE_LIBRUS_EMAIL" - LOGIN_MODE_LIBRUS_SYNERGIA -> "LOGIN_MODE_LIBRUS_SYNERGIA" - LOGIN_MODE_LIBRUS_JST -> "LOGIN_MODE_LIBRUS_JST" - else -> "unknown" - } - } - override fun toString(): String { return "LoginStore{" + "id=" + id + - ", type=" + type() + - ", mode=" + mode() + + ", type=" + type + + ", mode=" + mode + ", data=" + data + '}' } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Message.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Message.kt index 98105057..18a87df7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Message.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Message.kt @@ -7,7 +7,7 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.Ignore import androidx.room.Index -import pl.szczodrzynski.edziennik.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty @Entity(tableName = "messages", primaryKeys = ["profileId", "messageId"], @@ -41,7 +41,16 @@ open class Message( } @ColumnInfo(name = "messageIsPinned") - var isPinned: Boolean = false + var isStarred: Boolean = false + + val isReceived + get() = type == TYPE_RECEIVED + val isSent + get() = type == TYPE_SENT + val isDeleted + get() = type == TYPE_DELETED + val isDraft + get() = type == TYPE_DRAFT var hasAttachments = false // if the attachments are not yet downloaded but we already know there are some get() = field || attachmentIds.isNotNullNorEmpty() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Metadata.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Metadata.java index 7374d915..7b3544df 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Metadata.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Metadata.java @@ -4,47 +4,32 @@ package pl.szczodrzynski.edziennik.data.db.entity; +import androidx.annotation.NonNull; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.Ignore; import androidx.room.Index; import androidx.room.PrimaryKey; +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType; @Entity(tableName = "metadata", indices = {@Index(value = {"profileId", "thingType", "thingId"}, unique = true)} ) public class Metadata { - public static final int TYPE_GRADE = 1; - public static final int TYPE_NOTICE = 2; - public static final int TYPE_ATTENDANCE = 3; - public static final int TYPE_EVENT = 4; - public static final int TYPE_HOMEWORK = 5; - public static final int TYPE_LESSON_CHANGE = 6; - public static final int TYPE_ANNOUNCEMENT = 7; - public static final int TYPE_MESSAGE = 8; - public static final int TYPE_TEACHER_ABSENCE = 9; - public static final int TYPE_LUCKY_NUMBER = 10; - public int profileId; @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "metadataId") public int id; - public int thingType; + @NonNull + public MetadataType thingType; public long thingId; public boolean seen; public boolean notified; - @Ignore - public Metadata() { - this.profileId = -1; - this.seen = false; - this.notified = false; - } - - public Metadata(int profileId, int thingType, long thingId, boolean seen, boolean notified) { + public Metadata(int profileId, @NonNull MetadataType thingType, long thingId, boolean seen, boolean notified) { this.profileId = profileId; this.thingType = thingType; this.thingId = thingId; @@ -52,35 +37,12 @@ public class Metadata { this.notified = notified; } - public String thingType() { - switch (thingType) { - case TYPE_GRADE: - return "TYPE_GRADE"; - case TYPE_NOTICE: - return "TYPE_NOTICE"; - case TYPE_ATTENDANCE: - return "TYPE_ATTENDANCE"; - case TYPE_EVENT: - return "TYPE_EVENT"; - case TYPE_HOMEWORK: - return "TYPE_HOMEWORK"; - case TYPE_LESSON_CHANGE: - return "TYPE_LESSON_CHANGE"; - case TYPE_ANNOUNCEMENT: - return "TYPE_ANNOUNCEMENT"; - case TYPE_MESSAGE: - return "TYPE_MESSAGE"; - default: - return "TYPE_UNKNOWN"; - } - } - @Override public String toString() { return "Metadata{" + "profileId=" + profileId + ", id=" + id + - ", thingType=" + thingType() + + ", thingType=" + thingType + ", thingId=" + thingId + ", seen=" + seen + ", notified=" + notified + diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Note.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Note.kt new file mode 100644 index 00000000..9b6ad908 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Note.kt @@ -0,0 +1,149 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-16. + */ + +package pl.szczodrzynski.edziennik.data.db.entity + +import androidx.room.* +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.ui.search.Searchable +import pl.szczodrzynski.edziennik.utils.html.BetterHtml + +@Entity( + tableName = "notes", + indices = [ + Index(value = ["profileId", "noteOwnerType", "noteOwnerId"]), + ], +) +data class Note( + var profileId: Int, + + @PrimaryKey + @ColumnInfo(name = "noteId") + val id: Long, + + @ColumnInfo(name = "noteOwnerType") + val ownerType: OwnerType?, + @ColumnInfo(name = "noteOwnerId") + val ownerId: Long?, + @ColumnInfo(name = "noteReplacesOriginal") + val replacesOriginal: Boolean = false, + + @ColumnInfo(name = "noteTopic") + val topic: String?, + @ColumnInfo(name = "noteBody") + val body: String, + @ColumnInfo(name = "noteColor") + val color: Long?, + + @ColumnInfo(name = "noteSharedBy") + var sharedBy: String? = null, + @ColumnInfo(name = "noteSharedByName") + val sharedByName: String? = null, + + val addedDate: Long = System.currentTimeMillis(), +) : Searchable { + enum class OwnerType( + val isShareable: Boolean, + val canReplace: Boolean, + ) { + /** + * The [NONE] type is only for usage in the UI and should not be saved. + */ + NONE(isShareable = true, canReplace = false), + EVENT(isShareable = true, canReplace = true), + DAY(isShareable = true, canReplace = false), + LESSON(isShareable = true, canReplace = true), + MESSAGE(isShareable = true, canReplace = false), + EVENT_SUBJECT(isShareable = true, canReplace = false), + LESSON_SUBJECT(isShareable = true, canReplace = false), + GRADE(isShareable = false, canReplace = true), + ATTENDANCE(isShareable = false, canReplace = true), + BEHAVIOR(isShareable = false, canReplace = false), + ANNOUNCEMENT(isShareable = true, canReplace = false), + } + + enum class Color(val value: Long?, val stringRes: Int) { + NONE(null, R.string.color_none), + RED(0xffff1744, R.string.color_red), + ORANGE(0xffff9100, R.string.color_orange), + YELLOW(0xffffea00, R.string.color_yellow), + GREEN(0xff00c853, R.string.color_green), + TEAL(0xff00bfa5, R.string.color_teal), + BLUE(0xff0091ea, R.string.color_blue), + DARK_BLUE(0xff304ffe, R.string.color_dark_blue), + PURPLE(0xff6200ea, R.string.color_purple), + PINK(0xffd500f9, R.string.color_pink), + BROWN(0xff795548, R.string.color_brown), + GREY(0xff9e9e9e, R.string.color_grey), + BLACK(0xff000000, R.string.color_black), + } + + val isShared + get() = sharedBy != null && sharedByName != null + val canEdit + get() = !isShared || sharedBy == "self" + + // used when receiving notes + @Ignore + var teamCode: String? = null + + @delegate:Ignore + @delegate:Transient + val topicHtml by lazy { + topic?.let { + BetterHtml.fromHtml(context = null, it, nl2br = true) + } + } + + @delegate:Ignore + @delegate:Transient + val bodyHtml by lazy { + BetterHtml.fromHtml(context = null, body, nl2br = true) + } + + @Ignore + @Transient + var isCategoryItem = false + + @Ignore + @Transient + override var searchPriority = 0 + + @Ignore + @Transient + override var searchHighlightText: String? = null + + @delegate:Ignore + @delegate:Transient + override val searchKeywords by lazy { + if (isCategoryItem) + return@lazy emptyList() + listOf( + listOf(topicHtml?.toString(), bodyHtml.toString()), + listOf(sharedByName), + ) + } + + override fun compareTo(other: Searchable<*>): Int { + if (other !is Note) + return 0 + val order = ownerType?.ordinal ?: -1 + val otherOrder = other.ownerType?.ordinal ?: -1 + return when { + // custom ascending sorting + order > otherOrder -> 1 + order < otherOrder -> -1 + // all category elements stay at their original position + isCategoryItem -> 0 + other.isCategoryItem -> 0 + // ascending sorting + searchPriority > other.searchPriority -> 1 + searchPriority < other.searchPriority -> -1 + // descending sorting + addedDate > other.addedDate -> -1 + addedDate < other.addedDate -> 1 + else -> 0 + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Noteable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Noteable.kt new file mode 100644 index 00000000..8881c1b4 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Noteable.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-17. + */ + +package pl.szczodrzynski.edziennik.data.db.entity + +interface Noteable { + + fun getNoteType(): Note.OwnerType + fun getNoteOwnerProfileId(): Int + fun getNoteOwnerId(): Long + fun getNoteShareTeamId(): Long? = null + + var notes: MutableList + + fun filterNotes() { + val type = getNoteType() + val profileId = getNoteOwnerProfileId() + notes.removeAll { + it.profileId != profileId || it.ownerType != type + } + } + + fun hasNotes() = notes.isNotEmpty() + fun hasReplacingNotes() = notes.any { it.replacesOriginal } + + fun getNoteSubstituteText(showNotes: Boolean): CharSequence? { + if (!showNotes) + return null + val note = notes.firstOrNull { + it.replacesOriginal + } + return note?.topicHtml ?: note?.bodyHtml + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Notification.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Notification.kt index 1e8b8e0e..6c3097a4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Notification.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Notification.kt @@ -7,10 +7,17 @@ package pl.szczodrzynski.edziennik.data.db.entity import android.app.PendingIntent import android.content.Context import android.content.Intent +import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey import com.google.gson.JsonObject +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.data.db.enums.NotificationType +import pl.szczodrzynski.edziennik.ext.pendingIntentFlag +import pl.szczodrzynski.edziennik.ext.putExtras +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget @Entity(tableName = "notifications") data class Notification( @@ -19,43 +26,24 @@ data class Notification( val title: String, val text: String, + val textLong: String? = null, - val type: Int, + val type: NotificationType, val profileId: Int?, val profileName: String?, var posted: Boolean = true, - var viewId: Int? = null, + @ColumnInfo(name = "viewId") + var navTarget: NavTarget? = null, var extras: JsonObject? = null, val addedDate: Long = System.currentTimeMillis() ) { companion object { - const val TYPE_GENERAL = 0 - const val TYPE_UPDATE = 1 - const val TYPE_ERROR = 2 - const val TYPE_TIMETABLE_CHANGED = 3 - const val TYPE_TIMETABLE_LESSON_CHANGE = 4 - const val TYPE_NEW_GRADE = 5 - const val TYPE_NEW_EVENT = 6 - const val TYPE_NEW_HOMEWORK = 10 - const val TYPE_NEW_SHARED_EVENT = 7 - const val TYPE_NEW_SHARED_HOMEWORK = 12 - const val TYPE_REMOVED_SHARED_EVENT = 18 - const val TYPE_NEW_MESSAGE = 8 - const val TYPE_NEW_NOTICE = 9 - const val TYPE_NEW_ATTENDANCE = 13 - const val TYPE_SERVER_MESSAGE = 11 - const val TYPE_LUCKY_NUMBER = 14 - const val TYPE_NEW_ANNOUNCEMENT = 15 - const val TYPE_FEEDBACK_MESSAGE = 16 - const val TYPE_AUTO_ARCHIVING = 17 - const val TYPE_TEACHER_ABSENCE = 19 - - fun buildId(profileId: Int, type: Int, itemId: Long): Long { - return 1000000000000 + profileId*10000000000 + type*100000000 + itemId; + fun buildId(profileId: Int, type: NotificationType, itemId: Long): Long { + return 1000000000000 + profileId*10000000000 + type.id*100000000 + itemId; } } @@ -73,8 +61,8 @@ data class Notification( fun fillIntent(intent: Intent) { if (profileId != -1) intent.putExtra("profileId", profileId) - if (viewId != -1) - intent.putExtra("fragmentId", viewId) + if (navTarget != null) + intent.putExtras("fragmentId" to navTarget) try { extras?.entrySet()?.forEach { (key, value) -> if (!value.isJsonPrimitive) @@ -94,6 +82,6 @@ data class Notification( fun getPendingIntent(context: Context): PendingIntent { val intent = Intent(context, MainActivity::class.java) fillIntent(intent) - return PendingIntent.getActivity(context, id.toInt(), intent, PendingIntent.FLAG_ONE_SHOT) + return PendingIntent.getActivity(context, id.toInt(), intent, PendingIntent.FLAG_ONE_SHOT or pendingIntentFlag()) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt index 9e52dc81..8e689883 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Profile.kt @@ -5,31 +5,25 @@ package pl.szczodrzynski.edziennik.data.db.entity import android.content.Context -import android.graphics.PorterDuff -import android.graphics.PorterDuffColorFilter -import android.graphics.drawable.Drawable import android.widget.ImageView -import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.Ignore import com.google.gson.JsonObject -import pl.droidsonroids.gif.GifDrawable -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.api.* -import pl.szczodrzynski.edziennik.utils.ProfileImageHolder +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.db.enums.LoginType +import pl.szczodrzynski.edziennik.ext.dateToSemester +import pl.szczodrzynski.edziennik.ext.getDrawable +import pl.szczodrzynski.edziennik.ext.getHolder import pl.szczodrzynski.edziennik.utils.models.Date -import pl.szczodrzynski.navlib.ImageHolder -import pl.szczodrzynski.navlib.R import pl.szczodrzynski.navlib.drawer.IDrawerProfile -import pl.szczodrzynski.navlib.getDrawableFromRes @Entity(tableName = "profiles", primaryKeys = ["profileId"]) open class Profile( @ColumnInfo(name = "profileId") override var id: Int, /* needs to be var for ProfileArchiver */ val loginStoreId: Int, - val loginStoreType: Int, + val loginStoreType: LoginType, override var name: String = "", override var subname: String? = null, @@ -59,9 +53,13 @@ open class Profile( } override var image: String? = null - var empty = true var archived = false + var syncEnabled = true + @ColumnInfo(name = "enableSharedEvents") + var unused1 = true + var registration = REGISTRATION_UNSPECIFIED + var userCode = "" /** * A unique ID matching [archived] profiles with current ones @@ -69,166 +67,36 @@ open class Profile( */ var archiveId: Int? = null - var syncEnabled = true - var enableSharedEvents = true - var registration = REGISTRATION_UNSPECIFIED - var userCode = "" - /** * The student's number in the class register. */ var studentNumber = -1 var studentClassName: String? = null var studentSchoolYearStart = Date.getToday().let { if (it.month < 9) it.year - 1 else it.year } - var dateSemester1Start = Date(studentSchoolYearStart, 9, 1) var dateSemester2Start = Date(studentSchoolYearStart + 1, 2, 1) var dateYearEnd = Date(studentSchoolYearStart + 1, 6, 30) - fun getSemesterStart(semester: Int) = if (semester == 1) dateSemester1Start else dateSemester2Start - fun getSemesterEnd(semester: Int) = if (semester == 1) dateSemester2Start.clone().stepForward(0, 0, -1) else dateYearEnd - fun dateToSemester(date: Date) = if (date >= dateSemester2Start) 2 else 1 - @delegate:Ignore - val currentSemester by lazy { dateToSemester(Date.getToday()) } - - fun shouldArchive(): Boolean { - // vulcan hotfix - if (dateYearEnd.month > 6) { - dateYearEnd.month = 6 - dateYearEnd.day = 30 - } - // fix for when versions <4.3 synced 2020/2021 year dates to older profiles during 2020 Jun-Aug - if (dateSemester1Start.year > studentSchoolYearStart) { - val diff = dateSemester1Start.year - studentSchoolYearStart - dateSemester1Start.year -= diff - dateSemester2Start.year -= diff - dateYearEnd.year -= diff - } - return App.config.archiverEnabled - && Date.getToday() >= dateYearEnd - && Date.getToday().year > studentSchoolYearStart - } - fun isBeforeYear() = false && Date.getToday() < dateSemester1Start - var disabledNotifications: List? = null - var lastReceiversSync: Long = 0 - fun hasStudentData(key: String) = studentData.has(key) - fun getStudentData(key: String, defaultValue: Boolean) = studentData.getBoolean(key) ?: defaultValue - fun getStudentData(key: String, defaultValue: String?) = studentData.getString(key) ?: defaultValue - fun getStudentData(key: String, defaultValue: Int) = studentData.getInt(key) ?: defaultValue - fun getStudentData(key: String, defaultValue: Long) = studentData.getLong(key) ?: defaultValue - fun getStudentData(key: String, defaultValue: Float) = studentData.getFloat(key) ?: defaultValue - fun getStudentData(key: String, defaultValue: Char) = studentData.getChar(key) ?: defaultValue - fun putStudentData(key: String, value: Boolean) { studentData[key] = value } - fun putStudentData(key: String, value: String?) { studentData[key] = value } - fun putStudentData(key: String, value: Number) { studentData[key] = value } - fun putStudentData(key: String, value: Char) { studentData[key] = value } - fun removeStudentData(key: String) { studentData.remove(key) } - + val currentSemester + get() = dateToSemester(Date.getToday()) val isParent get() = accountName != null - + val accountOwnerName + get() = accountName ?: studentNameLong val registerName - get() = when (loginStoreType) { - LOGIN_TYPE_LIBRUS -> "librus" - LOGIN_TYPE_VULCAN -> "vulcan" - LOGIN_TYPE_IDZIENNIK -> "idziennik" - LOGIN_TYPE_MOBIDZIENNIK -> "mobidziennik" - LOGIN_TYPE_PODLASIE -> "podlasie" - LOGIN_TYPE_EDUDZIENNIK -> "edudziennik" - else -> null - } + get() = loginStoreType.name.lowercase() + val canShare + get() = registration == REGISTRATION_ENABLED && !archived - override fun getImageDrawable(context: Context): Drawable { - if (archived) { - return context.getDrawableFromRes(pl.szczodrzynski.edziennik.R.drawable.profile_archived).also { - it.colorFilter = PorterDuffColorFilter(colorFromName(name), PorterDuff.Mode.DST_OVER) - } - } + @delegate:Ignore + @delegate:Transient + val config by lazy { App.config[this.id] } - if (!image.isNullOrEmpty()) { - try { - return if (image?.endsWith(".gif", true) == true) { - GifDrawable(image ?: "") - } else { - RoundedBitmapDrawableFactory.create(context.resources, image ?: "") - //return Drawable.createFromPath(image ?: "") ?: throw Exception() - } - } - catch (e: Exception) { - e.printStackTrace() - } - } - - return context.getDrawableFromRes(R.drawable.profile).also { - it.colorFilter = PorterDuffColorFilter(colorFromName(name), PorterDuff.Mode.DST_OVER) - } - } - - override fun getImageHolder(context: Context): ImageHolder { - if (archived) { - return ImageHolder(pl.szczodrzynski.edziennik.R.drawable.profile_archived, colorFromName(name)) - } - - return if (!image.isNullOrEmpty()) { - try { - ProfileImageHolder(image ?: "") - } catch (_: Exception) { - ImageHolder(R.drawable.profile, colorFromName(name)) - } - } - else { - ImageHolder(R.drawable.profile, colorFromName(name)) - } - } + override fun getImageDrawable(context: Context) = this.getDrawable(context) + override fun getImageHolder(context: Context) = this.getHolder() override fun applyImageTo(imageView: ImageView) { getImageHolder(imageView.context).applyTo(imageView) } - - val supportedFragments: List - get() = when (loginStoreType) { - LoginStore.LOGIN_TYPE_MOBIDZIENNIK, - LoginStore.LOGIN_TYPE_DEMO, - LoginStore.LOGIN_TYPE_VULCAN -> listOf( - MainActivity.DRAWER_ITEM_TIMETABLE, - MainActivity.DRAWER_ITEM_AGENDA, - MainActivity.DRAWER_ITEM_GRADES, - MainActivity.DRAWER_ITEM_MESSAGES, - MainActivity.DRAWER_ITEM_HOMEWORK, - MainActivity.DRAWER_ITEM_BEHAVIOUR, - MainActivity.DRAWER_ITEM_ATTENDANCE - ) - LoginStore.LOGIN_TYPE_LIBRUS, - LoginStore.LOGIN_TYPE_IDZIENNIK -> listOf( - MainActivity.DRAWER_ITEM_TIMETABLE, - MainActivity.DRAWER_ITEM_AGENDA, - MainActivity.DRAWER_ITEM_GRADES, - MainActivity.DRAWER_ITEM_MESSAGES, - MainActivity.DRAWER_ITEM_HOMEWORK, - MainActivity.DRAWER_ITEM_BEHAVIOUR, - MainActivity.DRAWER_ITEM_ATTENDANCE, - MainActivity.DRAWER_ITEM_ANNOUNCEMENTS - ) - LOGIN_TYPE_EDUDZIENNIK -> listOf( - MainActivity.DRAWER_ITEM_TIMETABLE, - MainActivity.DRAWER_ITEM_AGENDA, - MainActivity.DRAWER_ITEM_GRADES, - MainActivity.DRAWER_ITEM_HOMEWORK, - MainActivity.DRAWER_ITEM_BEHAVIOUR, - MainActivity.DRAWER_ITEM_ATTENDANCE, - MainActivity.DRAWER_ITEM_ANNOUNCEMENTS - ) - LOGIN_TYPE_PODLASIE -> listOf( - MainActivity.DRAWER_ITEM_TIMETABLE, - MainActivity.DRAWER_ITEM_AGENDA, - MainActivity.DRAWER_ITEM_GRADES, - MainActivity.DRAWER_ITEM_HOMEWORK - ) - else -> listOf( - MainActivity.DRAWER_ITEM_TIMETABLE, - MainActivity.DRAWER_ITEM_AGENDA, - MainActivity.DRAWER_ITEM_GRADES - ) - } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Subject.java b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Subject.java index a034c785..1a4c46e4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Subject.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Subject.java @@ -9,7 +9,7 @@ import androidx.room.Entity; import java.util.List; -import pl.szczodrzynski.edziennik.ExtensionsKt; +import pl.szczodrzynski.edziennik.ext.GraphicsExtensionsKt; @Entity(tableName = "subjects", primaryKeys = {"profileId", "subjectId"}) @@ -31,7 +31,7 @@ public class Subject { this.id = id; this.longName = longName; this.shortName = shortName; - this.color = ExtensionsKt.colorFromName(longName); + this.color = GraphicsExtensionsKt.colorFromName(longName); } @Override diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Teacher.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Teacher.kt index c41b8a7a..b192bc1d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Teacher.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Teacher.kt @@ -10,13 +10,14 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.Ignore import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.fixName -import pl.szczodrzynski.edziennik.getNameInitials -import pl.szczodrzynski.edziennik.join +import pl.szczodrzynski.edziennik.ext.fixName +import pl.szczodrzynski.edziennik.ext.getNameInitials +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.ext.join import java.util.* @Entity(tableName = "teachers", - primaryKeys = ["profileId", "teacherId"]) + primaryKeys = ["profileId", "teacherId"]) open class Teacher { companion object { const val TYPE_TEACHER = 0 // 1 @@ -26,6 +27,7 @@ open class Teacher { const val TYPE_SECRETARIAT = 4 // 16 const val TYPE_PRINCIPAL = 5 // 32 const val TYPE_SCHOOL_ADMIN = 6 // 64 + // not teachers const val TYPE_SPECIALIST = 7 // 128 const val TYPE_SUPER_ADMIN = 10 // 1024 @@ -36,7 +38,8 @@ open class Teacher { const val TYPE_OTHER = 24 // 16777216 const val IS_TEACHER_MASK = 127 - val types: List by lazy { listOf( + val types: List by lazy { + listOf( TYPE_TEACHER, TYPE_EDUCATOR, TYPE_PEDAGOGUE, @@ -51,7 +54,8 @@ open class Teacher { TYPE_PARENTS_COUNCIL, TYPE_SCHOOL_PARENTS_COUNCIL, TYPE_OTHER - ) } + ) + } fun typeName(c: Context, type: Int, typeDescription: String? = null): String { val suffix = typeDescription?.let { " ($typeDescription)" } ?: "" @@ -94,6 +98,9 @@ open class Teacher { @ColumnInfo(name = "teacherTypeDescription") var typeDescription: String? = null + @ColumnInfo(name = "teacherSubjects") + var subjects = mutableListOf() + fun isType(checkingType: Int): Boolean { return type and (1 shl checkingType) >= 1 } @@ -105,19 +112,29 @@ open class Teacher { type = type or (1 shl i) } + fun addSubject(subjectId: Long) = subjects.add(subjectId) + fun unsetTeacherType(i: Int) { type = type and (1 shl i).inv() } - fun getTypeText(c: Context): String { - val list = mutableListOf() + fun getTypeText(c: Context, subjectList: List? = null): String { + val roles = mutableListOf() types.forEach { if (isType(it)) - list += typeName(c, it, typeDescription) + roles += typeName(c, it, typeDescription) } - return list.join(", ") - } + if (subjectList != null && subjects.isNotEmpty()) { + return subjects.joinToString( + prefix = if (roles.isNotEmpty()) roles.joinToString(postfix = ": ") else "", + transform = { subjectId -> + subjectList.firstOrNull { it.id == subjectId }?.longName ?: "" + }, + ) + } + return roles.joinToString() + } @Ignore var image: Bitmap? = null @@ -128,6 +145,7 @@ open class Teacher { */ @Ignore var recipientDisplayName: CharSequence? = null + /** * Used in Message composing - determining the priority * of search result, based on the search phrase match @@ -142,8 +160,6 @@ open class Teacher { this.id = id } - - @Ignore constructor(profileId: Int, id: Long, name: String, surname: String) { this.profileId = profileId @@ -170,6 +186,7 @@ open class Teacher { this.surname = it.surname this.type = it.type this.typeDescription = it.typeDescription + this.subjects = it.subjects this.image = it.image this.recipientDisplayName = it.recipientDisplayName } @@ -195,6 +212,7 @@ open class Teacher { ", name='" + name + '\'' + ", surname='" + surname + '\'' + ", type=" + dumpType() + + ", subjects=" + subjects.joinToString() + ", typeDescription='" + typeDescription + '\'' + '}' } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/FeatureType.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/FeatureType.kt new file mode 100644 index 00000000..90209435 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/FeatureType.kt @@ -0,0 +1,36 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-17. + */ + +package pl.szczodrzynski.edziennik.data.db.enums + +import pl.szczodrzynski.edziennik.R + +enum class FeatureType( + val id: Int, + val isAlwaysNeeded: Boolean, + val nameRes: Int? = null, + val isUIAlwaysAvailable: Boolean = false, +) { + TIMETABLE(id = 1, isAlwaysNeeded = false, nameRes = R.string.menu_timetable), + AGENDA(id = 2, isAlwaysNeeded = false, nameRes = R.string.menu_agenda, isUIAlwaysAvailable = true), + GRADES(id = 3, isAlwaysNeeded = false, nameRes = R.string.menu_grades), + HOMEWORK(id = 4, isAlwaysNeeded = false, nameRes = R.string.menu_homework, isUIAlwaysAvailable = true), + BEHAVIOUR(id = 5, isAlwaysNeeded = false, nameRes = R.string.menu_notices), + ATTENDANCE(id = 6, isAlwaysNeeded = false, nameRes = R.string.menu_attendance), + MESSAGES_INBOX(id = 7, isAlwaysNeeded = false, nameRes = R.string.title_messages_inbox_single), + MESSAGES_SENT(id = 8, isAlwaysNeeded = false, nameRes = R.string.title_messages_sent_single), + ANNOUNCEMENTS(id = 9, isAlwaysNeeded = false, nameRes = R.string.menu_announcements), + + ALWAYS_NEEDED(id = 100, isAlwaysNeeded = true), + STUDENT_INFO(id = 101, isAlwaysNeeded = true), + STUDENT_NUMBER(id = 109, isAlwaysNeeded = true), + SCHOOL_INFO(id = 102, isAlwaysNeeded = true), + CLASS_INFO(id = 103, isAlwaysNeeded = true), + TEAM_INFO(id = 104, isAlwaysNeeded = true), + LUCKY_NUMBER(id = 105, isAlwaysNeeded = true), + TEACHERS(id = 106, isAlwaysNeeded = true), + SUBJECTS(id = 107, isAlwaysNeeded = true), + CLASSROOMS(id = 108, isAlwaysNeeded = true), + PUSH_CONFIG(id = 120, isAlwaysNeeded = true), +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/LoginMethod.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/LoginMethod.kt new file mode 100644 index 00000000..64b0b7ca --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/LoginMethod.kt @@ -0,0 +1,86 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-17. + */ + +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, + val id: Int, + val isPossible: (( + profile: Profile?, + loginStore: LoginStore, + ) -> Boolean)? = null, + val requiredLoginMethod: (( + profile: Profile?, + loginStore: LoginStore, + ) -> LoginMethod?)? = null, +) { + MOBIDZIENNIK_WEB( + loginType = LoginType.MOBIDZIENNIK, + id = 1100, + ), + MOBIDZIENNIK_API2( + loginType = LoginType.MOBIDZIENNIK, + id = 1300, + isPossible = { profile, _ -> profile?.studentData?.getString("email").isNotNullNorBlank() }, + ), + LIBRUS_PORTAL( + loginType = LoginType.LIBRUS, + id = 2100, + isPossible = { _, loginStore -> loginStore.mode == LoginMode.LIBRUS_EMAIL }, + ), + LIBRUS_API( + loginType = LoginType.LIBRUS, + id = 2200, + isPossible = { _, loginStore -> loginStore.mode != LoginMode.LIBRUS_SYNERGIA }, + requiredLoginMethod = { _, loginStore -> + if (loginStore.mode == LoginMode.LIBRUS_EMAIL) LIBRUS_PORTAL + else null + }, + ), + LIBRUS_SYNERGIA( + loginType = LoginType.LIBRUS, + id = 2300, + isPossible = { _, loginStore -> !loginStore.hasLoginData("fakeLogin") }, + requiredLoginMethod = { _, _ -> LIBRUS_API }, + ), + LIBRUS_MESSAGES( + loginType = LoginType.LIBRUS, + id = 2400, + isPossible = { _, loginStore -> !loginStore.hasLoginData("fakeLogin") }, + requiredLoginMethod = { _, _ -> LIBRUS_SYNERGIA }, + ), + VULCAN_WEB_MAIN( + loginType = LoginType.VULCAN, + id = 4100, + isPossible = { _, loginStore -> loginStore.getLoginData("webHost", null).isNotNullNorBlank() }, + ), + VULCAN_HEBE( + loginType = LoginType.VULCAN, + id = 4600, + isPossible = { _, loginStore -> loginStore.mode != LoginMode.VULCAN_API }, + ), + PODLASIE_API( + loginType = LoginType.PODLASIE, + id = 6100, + ), + USOS_API( + loginType = LoginType.USOS, + id = 7100, + ), + TEMPLATE_WEB( + loginType = LoginType.TEMPLATE, + id = 21100, + ), + TEMPLATE_API( + loginType = LoginType.TEMPLATE, + id = 21200, + requiredLoginMethod = { _, _ -> TEMPLATE_WEB }, + ), +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/LoginMode.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/LoginMode.kt new file mode 100644 index 00000000..9deb5310 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/LoginMode.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-17. + */ + +package pl.szczodrzynski.edziennik.data.db.enums + +enum class LoginMode( + val loginType: LoginType, + val id: Int, +) { + MOBIDZIENNIK_WEB(LoginType.MOBIDZIENNIK, id = 100), + LIBRUS_EMAIL(LoginType.LIBRUS, id = 200), + LIBRUS_SYNERGIA(LoginType.LIBRUS, id = 201), + LIBRUS_JST(LoginType.LIBRUS, id = 202), + VULCAN_API(LoginType.VULCAN, id = 400), + VULCAN_WEB(LoginType.VULCAN, id = 401), + VULCAN_HEBE(LoginType.VULCAN, id = 402), + PODLASIE_API(LoginType.PODLASIE, id = 600), + USOS_OAUTH(LoginType.USOS, id = 700), +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/LoginType.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/LoginType.kt new file mode 100644 index 00000000..de0ca2f8 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/LoginType.kt @@ -0,0 +1,23 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-17. + */ + +package pl.szczodrzynski.edziennik.data.db.enums + +enum class LoginType( + val id: Int, + val features: Set, + val schoolType: SchoolType = SchoolType.STANDARD, +) { + MOBIDZIENNIK(id = 1, features = FEATURES_MOBIDZIENNIK), + LIBRUS(id = 2, features = FEATURES_LIBRUS), + VULCAN(id = 4, features = FEATURES_VULCAN), + PODLASIE(id = 6, features = FEATURES_PODLASIE), + USOS(id = 7, features = FEATURES_USOS, schoolType = SchoolType.UNIVERSITY), + DEMO(id = 20, features = setOf()), + TEMPLATE(id = 21, features = setOf()), + + // the graveyard + EDUDZIENNIK(id = 5, features = FEATURES_EDUDZIENNIK), + IDZIENNIK(id = 3, features = FEATURES_IDZIENNIK), +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/LoginTypeFeatures.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/LoginTypeFeatures.kt new file mode 100644 index 00000000..b0d16002 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/LoginTypeFeatures.kt @@ -0,0 +1,145 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-20. + */ + +package pl.szczodrzynski.edziennik.data.db.enums + +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType.* + +internal val FEATURES_MOBIDZIENNIK = setOf( + TIMETABLE, + AGENDA, + GRADES, + HOMEWORK, + BEHAVIOUR, + ATTENDANCE, + MESSAGES_INBOX, + MESSAGES_SENT, + + STUDENT_INFO, + STUDENT_NUMBER, + SCHOOL_INFO, + CLASS_INFO, + TEAM_INFO, + LUCKY_NUMBER, + TEACHERS, + SUBJECTS, + CLASSROOMS, + PUSH_CONFIG, +) + +internal val FEATURES_LIBRUS = setOf( + TIMETABLE, + AGENDA, + GRADES, + HOMEWORK, + BEHAVIOUR, + ATTENDANCE, + MESSAGES_INBOX, + MESSAGES_SENT, + ANNOUNCEMENTS, + + STUDENT_INFO, + STUDENT_NUMBER, + SCHOOL_INFO, + CLASS_INFO, + TEAM_INFO, + LUCKY_NUMBER, + TEACHERS, + SUBJECTS, + CLASSROOMS, + PUSH_CONFIG, +) + +internal val FEATURES_VULCAN = setOf( + TIMETABLE, + AGENDA, + GRADES, + HOMEWORK, + BEHAVIOUR, + ATTENDANCE, + MESSAGES_INBOX, + MESSAGES_SENT, + + STUDENT_INFO, + STUDENT_NUMBER, + SCHOOL_INFO, + CLASS_INFO, + TEAM_INFO, + LUCKY_NUMBER, + TEACHERS, + SUBJECTS, + CLASSROOMS, + PUSH_CONFIG, +) + +internal val FEATURES_PODLASIE = setOf( + TIMETABLE, + AGENDA, + GRADES, + HOMEWORK, + + STUDENT_INFO, + STUDENT_NUMBER, + SCHOOL_INFO, + CLASS_INFO, + TEAM_INFO, + LUCKY_NUMBER, + TEACHERS, + SUBJECTS, + CLASSROOMS, +) + +internal val FEATURES_USOS = setOf( + TIMETABLE, + AGENDA, + + STUDENT_INFO, + STUDENT_NUMBER, + CLASS_INFO, + TEAM_INFO, + TEACHERS, + SUBJECTS, + CLASSROOMS, +) + +internal val FEATURES_EDUDZIENNIK = setOf( + TIMETABLE, + AGENDA, + GRADES, + HOMEWORK, + BEHAVIOUR, + ATTENDANCE, + + STUDENT_INFO, + STUDENT_NUMBER, + SCHOOL_INFO, + CLASS_INFO, + TEAM_INFO, + LUCKY_NUMBER, + TEACHERS, + SUBJECTS, + CLASSROOMS, +) + +internal val FEATURES_IDZIENNIK = setOf( + TIMETABLE, + AGENDA, + GRADES, + HOMEWORK, + BEHAVIOUR, + ATTENDANCE, + MESSAGES_INBOX, + MESSAGES_SENT, + ANNOUNCEMENTS, + + STUDENT_INFO, + STUDENT_NUMBER, + SCHOOL_INFO, + CLASS_INFO, + TEAM_INFO, + LUCKY_NUMBER, + TEACHERS, + SUBJECTS, + CLASSROOMS, +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/MetadataType.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/MetadataType.kt new file mode 100644 index 00000000..a993820f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/MetadataType.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-17. + */ + +package pl.szczodrzynski.edziennik.data.db.enums + +enum class MetadataType( + val id: Int, +) { + GRADE(id = 1), + NOTICE(id = 2), + ATTENDANCE(id = 3), + EVENT(id = 4), + HOMEWORK(id = 5), + LESSON_CHANGE(id = 6), + ANNOUNCEMENT(id = 7), + MESSAGE(id = 8), + TEACHER_ABSENCE(id = 9), + LUCKY_NUMBER(id = 10), +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/NotificationType.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/NotificationType.kt new file mode 100644 index 00000000..4b6842d9 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/NotificationType.kt @@ -0,0 +1,134 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-20. + */ + +package pl.szczodrzynski.edziennik.data.db.enums + +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import pl.szczodrzynski.edziennik.R + +enum class NotificationType( + val id: Int, + val icon: IIcon = CommunityMaterial.Icon.cmd_bell_ring_outline, + val titleRes: Int, + val pluralRes: Int = R.plurals.notification_other_format, + val enabledByDefault: Boolean? = true, +) { + TIMETABLE_LESSON_CHANGE( + id = 4, + icon = CommunityMaterial.Icon3.cmd_timetable, + titleRes = R.string.notification_type_timetable_lesson_change, + pluralRes = R.plurals.notification_new_timetable_change_format, + ), + GRADE( + id = 5, + icon = CommunityMaterial.Icon3.cmd_numeric_5_box_outline, + titleRes = R.string.notification_type_new_grade, + pluralRes = R.plurals.notification_new_grades_format, + ), + EVENT( + id = 6, + icon = CommunityMaterial.Icon.cmd_calendar_outline, + titleRes = R.string.notification_type_new_event, + pluralRes = R.plurals.notification_new_events_format, + ), + HOMEWORK( + id = 10, + icon = CommunityMaterial.Icon3.cmd_notebook_outline, + titleRes = R.string.notification_type_new_homework, + pluralRes = R.plurals.notification_new_homework_format, + ), + MESSAGE( + id = 8, + icon = CommunityMaterial.Icon.cmd_email_outline, + titleRes = R.string.notification_type_new_message, + pluralRes = R.plurals.notification_new_messages_format, + ), + LUCKY_NUMBER( + id = 14, + icon = CommunityMaterial.Icon.cmd_emoticon_excited_outline, + titleRes = R.string.notification_type_lucky_number, + pluralRes = R.plurals.notification_new_lucky_number_format, + ), + NOTICE( + id = 9, + icon = CommunityMaterial.Icon.cmd_emoticon_outline, + titleRes = R.string.notification_type_notice, + pluralRes = R.plurals.notification_new_notices_format, + ), + ATTENDANCE( + id = 13, + icon = CommunityMaterial.Icon.cmd_calendar_remove_outline, + titleRes = R.string.notification_type_attendance, + pluralRes = R.plurals.notification_new_attendance_format, + ), + ANNOUNCEMENT( + id = 15, + icon = CommunityMaterial.Icon.cmd_bullhorn_outline, + titleRes = R.string.notification_type_feedback_message, + pluralRes = R.plurals.notification_new_announcements_format, + ), + SHARED_EVENT( + id = 7, + icon = CommunityMaterial.Icon.cmd_calendar_outline, + titleRes = R.string.notification_type_new_shared_event, + pluralRes = R.plurals.notification_new_shared_events_format, + ), + SHARED_HOMEWORK( + id = 12, + icon = CommunityMaterial.Icon3.cmd_notebook_outline, + titleRes = R.string.notification_type_new_shared_homework, + pluralRes = R.plurals.notification_new_shared_homework_format, + ), + SHARED_NOTE( + id = 20, + icon = CommunityMaterial.Icon3.cmd_playlist_edit, + titleRes = R.string.notification_type_new_shared_note, + ), + REMOVED_SHARED_EVENT( + id = 18, + titleRes = R.string.notification_type_removed_shared_event, + ), + TEACHER_ABSENCE( + id = 19, + titleRes = R.string.notification_type_new_teacher_absence, + enabledByDefault = false, + ), + + GENERAL( + id = 0, + titleRes = R.string.notification_type_general, + enabledByDefault = null, + ), + UPDATE( + id = 1, + titleRes = R.string.notification_type_update, + enabledByDefault = null, + ), + ERROR( + id = 2, + titleRes = R.string.notification_type_error, + enabledByDefault = null, + ), + TIMETABLE_CHANGED( + id = 3, + titleRes = R.string.notification_type_timetable_change, + enabledByDefault = null, + ), + SERVER_MESSAGE( + id = 11, + titleRes = R.string.notification_type_server_message, + enabledByDefault = null, + ), + FEEDBACK_MESSAGE( + id = 16, + titleRes = R.string.notification_type_feedback_message, + enabledByDefault = null, + ), + AUTO_ARCHIVING( + id = 17, + titleRes = R.string.notification_type_auto_archiving, + enabledByDefault = null, + ), +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/SchoolType.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/SchoolType.kt new file mode 100644 index 00000000..593851e2 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/enums/SchoolType.kt @@ -0,0 +1,10 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-21. + */ + +package pl.szczodrzynski.edziennik.data.db.enums + +enum class SchoolType { + STANDARD, + UNIVERSITY, +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/AnnouncementFull.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/AnnouncementFull.kt index 5b4cb711..f82213dd 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/AnnouncementFull.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/AnnouncementFull.kt @@ -3,7 +3,10 @@ */ package pl.szczodrzynski.edziennik.data.db.full +import androidx.room.Relation import pl.szczodrzynski.edziennik.data.db.entity.Announcement +import pl.szczodrzynski.edziennik.data.db.entity.Note +import pl.szczodrzynski.edziennik.data.db.entity.Noteable import pl.szczodrzynski.edziennik.utils.models.Date class AnnouncementFull( @@ -16,10 +19,16 @@ class AnnouncementFull( subject, text, startDate, endDate, teacherId, addedDate -) { +), Noteable { var teacherName: String? = null // metadata var seen = false var notified = false + + @Relation(parentColumn = "announcementId", entityColumn = "noteOwnerId", entity = Note::class) + override lateinit var notes: MutableList + override fun getNoteType() = Note.OwnerType.ANNOUNCEMENT + override fun getNoteOwnerProfileId() = profileId + override fun getNoteOwnerId() = id } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/AttendanceFull.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/AttendanceFull.kt index f235fafd..c1440ab5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/AttendanceFull.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/AttendanceFull.kt @@ -3,7 +3,10 @@ */ package pl.szczodrzynski.edziennik.data.db.full +import androidx.room.Relation import pl.szczodrzynski.edziennik.data.db.entity.Attendance +import pl.szczodrzynski.edziennik.data.db.entity.Note +import pl.szczodrzynski.edziennik.data.db.entity.Noteable import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -17,7 +20,7 @@ class AttendanceFull( baseType, typeName, typeShort, typeSymbol, typeColor, date, startTime, semester, teacherId, subjectId, addedDate -) { +), Noteable { var teacherName: String? = null var subjectLongName: String? = null var subjectShortName: String? = null @@ -26,4 +29,10 @@ class AttendanceFull( var seen = false get() = field || baseType == TYPE_PRESENT var notified = false + + @Relation(parentColumn = "attendanceId", entityColumn = "noteOwnerId", entity = Note::class) + override lateinit var notes: MutableList + override fun getNoteType() = Note.OwnerType.ATTENDANCE + override fun getNoteOwnerProfileId() = profileId + override fun getNoteOwnerId() = id } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/EventFull.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/EventFull.kt index 01e85395..332b7ce5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/EventFull.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/EventFull.kt @@ -3,8 +3,15 @@ */ package pl.szczodrzynski.edziennik.data.db.full +import androidx.room.Ignore +import androidx.room.Relation import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.entity.Note +import pl.szczodrzynski.edziennik.data.db.entity.Noteable +import pl.szczodrzynski.edziennik.ext.takePositive +import pl.szczodrzynski.edziennik.ui.search.Searchable +import pl.szczodrzynski.edziennik.utils.html.BetterHtml import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -16,7 +23,7 @@ class EventFull( profileId, id, date, time, topic, color, type, teacherId, subjectId, teamId, addedDate -) { +), Searchable, Noteable { constructor(event: Event, metadata: Metadata? = null) : this( event.profileId, event.id, event.date, event.time, event.topic, event.color, event.type, @@ -26,6 +33,7 @@ class EventFull( sharedBy = it.sharedBy sharedByName = it.sharedByName blacklisted = it.blacklisted + isDownloaded = it.isDownloaded homeworkBody = it.homeworkBody attachmentIds = it.attachmentIds attachmentNames = it.attachmentNames @@ -45,10 +53,71 @@ class EventFull( var teamName: String? = null var teamCode: String? = null + @delegate:Ignore + @delegate:Transient + val topicHtml by lazy { + BetterHtml.fromHtml(context = null, topic, nl2br = true) + } + + @delegate:Ignore + @delegate:Transient + val bodyHtml by lazy { + homeworkBody?.let { + BetterHtml.fromHtml(context = null, it, nl2br = true) + } + } + + @Ignore + @Transient + override var searchPriority = 0 + + @Ignore + @Transient + override var searchHighlightText: String? = null + + @delegate:Ignore + @delegate:Transient + override val searchKeywords by lazy { + listOf( + listOf(topicHtml.toString(), bodyHtml?.toString()), + attachmentNames, + listOf(subjectLongName), + listOf(teacherName), + listOf(sharedByName), + ) + } + + override fun compareTo(other: Searchable<*>): Int { + if (other !is EventFull) + return 0 + return when { + // ascending sorting + searchPriority > other.searchPriority -> 1 + searchPriority < other.searchPriority -> -1 + // ascending sorting + date > other.date -> 1 + date < other.date -> -1 + // ascending sorting + (time?.value ?: 0) > (other.time?.value ?: 0) -> 1 + (time?.value ?: 0) < (other.time?.value ?: 0) -> -1 + // ascending sorting + addedDate > other.addedDate -> 1 + addedDate < other.addedDate -> -1 + else -> 0 + } + } + // metadata var seen = false var notified = false val eventColor get() = color ?: typeColor ?: 0xff2196f3.toInt() + + @Relation(parentColumn = "eventId", entityColumn = "noteOwnerId", entity = Note::class) + override lateinit var notes: MutableList + override fun getNoteType() = Note.OwnerType.EVENT + override fun getNoteOwnerProfileId() = profileId + override fun getNoteOwnerId() = id + override fun getNoteShareTeamId() = teamId.takePositive() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/GradeFull.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/GradeFull.kt index f8d90c05..fc6e7f19 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/GradeFull.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/GradeFull.kt @@ -3,7 +3,10 @@ */ package pl.szczodrzynski.edziennik.data.db.full +import androidx.room.Relation import pl.szczodrzynski.edziennik.data.db.entity.Grade +import pl.szczodrzynski.edziennik.data.db.entity.Note +import pl.szczodrzynski.edziennik.data.db.entity.Noteable class GradeFull( profileId: Int, id: Long, name: String, type: Int, @@ -15,7 +18,7 @@ class GradeFull( value, weight, color, category, description, comment, semester, teacherId, subjectId, addedDate -) { +), Noteable { var teacherName: String? = null var subjectLongName: String? = null var subjectShortName: String? = null @@ -23,4 +26,10 @@ class GradeFull( // metadata var seen = false var notified = false + + @Relation(parentColumn = "gradeId", entityColumn = "noteOwnerId", entity = Note::class) + override lateinit var notes: MutableList + override fun getNoteType() = Note.OwnerType.GRADE + override fun getNoteOwnerProfileId() = profileId + override fun getNoteOwnerId() = id } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/LessonFull.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/LessonFull.kt index 0e9542b6..b61bba30 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/LessonFull.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/LessonFull.kt @@ -4,15 +4,19 @@ package pl.szczodrzynski.edziennik.data.db.full import android.content.Context +import androidx.room.Relation import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.Lesson +import pl.szczodrzynski.edziennik.data.db.entity.Note +import pl.szczodrzynski.edziennik.data.db.entity.Noteable +import pl.szczodrzynski.edziennik.ext.takePositive import pl.szczodrzynski.edziennik.utils.models.Time class LessonFull( profileId: Int, id: Long ) : Lesson( profileId, id -) { +), Noteable { var subjectName: String? = null var teacherName: String? = null var teamName: String? = null @@ -133,4 +137,11 @@ class LessonFull( // metadata var seen: Boolean = false var notified: Boolean = false + + @Relation(parentColumn = "ownerId", entityColumn = "noteOwnerId", entity = Note::class) + override lateinit var notes: MutableList + override fun getNoteType() = Note.OwnerType.LESSON + override fun getNoteOwnerProfileId() = profileId + override fun getNoteOwnerId() = ownerId + override fun getNoteShareTeamId() = teamId.takePositive() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt index 3fdcdd0d..232095d8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/MessageFull.kt @@ -7,6 +7,10 @@ import androidx.room.Ignore import androidx.room.Relation import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient +import pl.szczodrzynski.edziennik.data.db.entity.Note +import pl.szczodrzynski.edziennik.data.db.entity.Noteable +import pl.szczodrzynski.edziennik.ui.search.Searchable +import pl.szczodrzynski.edziennik.utils.html.BetterHtml class MessageFull( profileId: Int, id: Long, type: Int, @@ -16,7 +20,7 @@ class MessageFull( profileId, id, type, subject, body, senderId, addedDate -) { +), Searchable, Noteable { var senderName: String? = null @Relation(parentColumn = "messageId", entityColumn = "messageId", entity = MessageRecipient::class) var recipients: MutableList? = null @@ -27,12 +31,64 @@ class MessageFull( return this } + @delegate:Ignore + @delegate:Transient + val bodyHtml by lazy { + body?.let { + BetterHtml.fromHtml(context = null, it) + } + } + @Ignore - var filterWeight = 0 + @Transient + override var searchPriority = 0 + @Ignore - var searchHighlightText: String? = null + @Transient + override var searchHighlightText: String? = null + + @delegate:Ignore + @delegate:Transient + override val searchKeywords by lazy { + listOf( + when { + isSent -> recipients?.map { it.fullName } + else -> listOf(senderName) + }, + listOf(subject), + listOf(bodyHtml?.toString()), + attachmentNames, + ) + } + + override fun compareTo(other: Searchable<*>): Int { + if (other !is MessageFull) + return 0 + return when { + // ascending sorting + searchPriority > other.searchPriority -> 1 + searchPriority < other.searchPriority -> -1 + // descending sorting (1. true, 2. false) + isStarred && !other.isStarred -> -1 + !isStarred && other.isStarred -> 1 + // descending sorting + addedDate > other.addedDate -> -1 + addedDate < other.addedDate -> 1 + else -> 0 + } + } + + @Ignore + @Transient + var readByEveryone = true // metadata var seen = false var notified = false + + @Relation(parentColumn = "messageId", entityColumn = "noteOwnerId", entity = Note::class) + override lateinit var notes: MutableList + override fun getNoteType() = Note.OwnerType.MESSAGE + override fun getNoteOwnerProfileId() = profileId + override fun getNoteOwnerId() = id } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/NoticeFull.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/NoticeFull.kt index 25aee14d..6f8d6fb0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/NoticeFull.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/full/NoticeFull.kt @@ -3,6 +3,9 @@ */ package pl.szczodrzynski.edziennik.data.db.full +import androidx.room.Relation +import pl.szczodrzynski.edziennik.data.db.entity.Note +import pl.szczodrzynski.edziennik.data.db.entity.Noteable import pl.szczodrzynski.edziennik.data.db.entity.Notice class NoticeFull( @@ -13,10 +16,16 @@ class NoticeFull( profileId, id, type, semester, text, category, points, teacherId, addedDate -) { +), Noteable { var teacherName: String? = null // metadata var seen = false var notified = false + + @Relation(parentColumn = "noticeId", entityColumn = "noteOwnerId", entity = Note::class) + override lateinit var notes: MutableList + override fun getNoteType() = Note.OwnerType.BEHAVIOR + override fun getNoteOwnerProfileId() = profileId + override fun getNoteOwnerId() = id } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration100.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration100.kt new file mode 100644 index 00000000..b1d05b1b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration100.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-25. + */ + +package pl.szczodrzynski.edziennik.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration100 : Migration(99, 100) { + override fun migrate(database: SupportSQLiteDatabase) { + // add Note Owner ID to Lesson, to make it profileId-independent + // calculate the new owner ID based on the old ID + database.execSQL("ALTER TABLE timetable ADD COLUMN ownerId INT NOT NULL DEFAULT 0;") + // set new ID for actual lessons + database.execSQL("UPDATE timetable SET ownerId = ROUND((id & ~65535) / 500000.0) * 300000;") + // copy the old ID (date value) for NO_LESSONS + database.execSQL("UPDATE timetable SET ownerId = id WHERE type = -1;") + // update ID for notes as well + database.execSQL("UPDATE notes SET noteOwnerId = ROUND((noteOwnerId & ~65535) / 500000.0) * 300000 WHERE noteOwnerType = 'LESSON' AND noteOwnerId > 2000000000000;") + // force full app sync to download notes with new IDs + database.execSQL("DELETE FROM config WHERE `key` = 'hash';") + database.execSQL("DELETE FROM config WHERE `key` = 'lastAppSync';") + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration58.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration58.kt index d0ebf548..ba1f26b7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration58.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration58.kt @@ -6,14 +6,13 @@ package pl.szczodrzynski.edziennik.data.db.migration import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase -import pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_EVENT -import pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_HOMEWORK +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType class Migration58 : Migration(57, 58) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE metadata RENAME TO _metadata_old;") database.execSQL("DROP INDEX index_metadata_profileId_thingType_thingId;") - database.execSQL("UPDATE _metadata_old SET thingType = $TYPE_HOMEWORK WHERE thingType = $TYPE_EVENT AND thingId IN (SELECT eventId FROM events WHERE eventType = -1)") + database.execSQL("UPDATE _metadata_old SET thingType = ${MetadataType.HOMEWORK.id} WHERE thingType = ${MetadataType.EVENT.id} AND thingId IN (SELECT eventId FROM events WHERE eventType = -1)") database.execSQL("""CREATE TABLE metadata ( profileId INTEGER NOT NULL, metadataId INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration68.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration68.kt index f7a079d0..6bbc8e86 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration68.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration68.kt @@ -6,12 +6,12 @@ package pl.szczodrzynski.edziennik.data.db.migration import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase -import pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_ANNOUNCEMENT +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType class Migration68 : Migration(67, 68) { override fun migrate(database: SupportSQLiteDatabase) { /* Migration from crc16 to crc32 id */ database.execSQL("DELETE FROM announcements") - database.execSQL("DELETE FROM metadata WHERE thingType=$TYPE_ANNOUNCEMENT") + database.execSQL("DELETE FROM metadata WHERE thingType=${MetadataType.ANNOUNCEMENT.id}") } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration71.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration71.kt index dabb2303..5166a0ce 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration71.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration71.kt @@ -6,7 +6,7 @@ package pl.szczodrzynski.edziennik.data.db.migration import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase -import pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_MESSAGE +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType class Migration71 : Migration(70, 71) { override fun migrate(database: SupportSQLiteDatabase) { @@ -14,7 +14,7 @@ class Migration71 : Migration(70, 71) { database.execSQL("DELETE FROM messageRecipients WHERE profileId IN (SELECT profileId FROM profiles WHERE archived = 0)") database.execSQL("DELETE FROM teachers WHERE profileId IN (SELECT profileId FROM profiles WHERE archived = 0)") database.execSQL("DELETE FROM endpointTimers WHERE profileId IN (SELECT profileId FROM profiles WHERE archived = 0)") - database.execSQL("DELETE FROM metadata WHERE profileId IN (SELECT profileId FROM profiles WHERE archived = 0) AND thingType = $TYPE_MESSAGE") + database.execSQL("DELETE FROM metadata WHERE profileId IN (SELECT profileId FROM profiles WHERE archived = 0) AND thingType = ${MetadataType.MESSAGE.id}") database.execSQL("UPDATE profiles SET empty = 1 WHERE archived = 0") database.execSQL("UPDATE profiles SET lastReceiversSync = 0 WHERE archived = 0") database.execSQL("INSERT INTO config (profileId, `key`, value) VALUES (-1, 'runSync', 'true')") diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration72.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration72.kt index 88567b47..65f9c4ce 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration72.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration72.kt @@ -6,7 +6,7 @@ package pl.szczodrzynski.edziennik.data.db.migration import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase -import pl.szczodrzynski.edziennik.crc32 +import pl.szczodrzynski.edziennik.ext.crc32 class Migration72 : Migration(71, 72) { override fun migrate(database: SupportSQLiteDatabase) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration81.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration81.kt index bf547244..5af06357 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration81.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration81.kt @@ -2,10 +2,10 @@ package pl.szczodrzynski.edziennik.data.db.migration import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase -import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType class Migration81 : Migration(80, 81) { override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("UPDATE metadata SET seen = 1, notified = 1 WHERE thingType = ${Metadata.TYPE_TEACHER_ABSENCE}") + database.execSQL("UPDATE metadata SET seen = 1, notified = 1 WHERE thingType = ${MetadataType.TEACHER_ABSENCE.id}") } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration85.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration85.kt index 6f1c79c0..b53c08e2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration85.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration85.kt @@ -2,11 +2,11 @@ package pl.szczodrzynski.edziennik.data.db.migration import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase -import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_EDUDZIENNIK import pl.szczodrzynski.edziennik.data.db.entity.Event +import pl.szczodrzynski.edziennik.data.db.enums.LoginType class Migration85 : Migration(84, 85) { override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("DELETE FROM events WHERE eventAddedManually = 0 AND eventType = ${Event.TYPE_HOMEWORK} AND profileId IN (SELECT profileId FROM (SELECT profileId FROM profiles WHERE loginStoreType = $LOGIN_TYPE_EDUDZIENNIK) x)") + database.execSQL("DELETE FROM events WHERE eventAddedManually = 0 AND eventType = ${Event.TYPE_HOMEWORK} AND profileId IN (SELECT profileId FROM (SELECT profileId FROM profiles WHERE loginStoreType = ${LoginType.EDUDZIENNIK.id}) x)") } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration92.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration92.kt new file mode 100644 index 00000000..3010a371 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration92.kt @@ -0,0 +1,60 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-15. + */ + +package pl.szczodrzynski.edziennik.data.db.migration + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.COLOR_ELEARNING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_ELEARNING +import pl.szczodrzynski.edziennik.data.db.entity.Event.Companion.TYPE_INFORMATION +import pl.szczodrzynski.edziennik.data.db.entity.EventType.Companion.SOURCE_DEFAULT +import pl.szczodrzynski.edziennik.data.db.entity.EventType.Companion.SOURCE_REGISTER +import pl.szczodrzynski.edziennik.ext.getInt + +class Migration92 : Migration(91, 92) { + override fun migrate(database: SupportSQLiteDatabase) { + // make eventTypeName not nullable + database.execSQL("ALTER TABLE eventTypes RENAME TO _eventTypes;") + database.execSQL("CREATE TABLE eventTypes (" + + "profileId INTEGER NOT NULL, " + + "eventType INTEGER NOT NULL, " + + "eventTypeName TEXT NOT NULL, " + + "eventTypeColor INTEGER NOT NULL, " + + "PRIMARY KEY(profileId,eventType)" + + ");") + database.execSQL("INSERT INTO eventTypes " + + "(profileId, eventType, eventTypeName, eventTypeColor) " + + "SELECT profileId, eventType, eventTypeName, eventTypeColor " + + "FROM _eventTypes;") + database.execSQL("DROP TABLE _eventTypes;") + + // add columns for order and source + database.execSQL("ALTER TABLE eventTypes ADD COLUMN eventTypeOrder INTEGER NOT NULL DEFAULT 0;") + database.execSQL("ALTER TABLE eventTypes ADD COLUMN eventTypeSource INTEGER NOT NULL DEFAULT 0;") + + // migrate existing types to show correct order and source + database.execSQL("UPDATE eventTypes SET eventTypeOrder = eventType + 102;") + database.execSQL("UPDATE eventTypes SET eventTypeSource = $SOURCE_REGISTER WHERE eventType > $TYPE_INFORMATION;") + + // add new e-learning type + val cursor = database.query("SELECT profileId FROM profiles;") + cursor.use { + while (it.moveToNext()) { + val values = ContentValues().apply { + put("profileId", it.getInt("profileId")) + put("eventType", TYPE_ELEARNING) + put("eventTypeName", "lekcja online") + put("eventTypeColor", COLOR_ELEARNING) + put("eventTypeOrder", 100) + put("eventTypeSource", SOURCE_DEFAULT) + } + + database.insert("eventTypes", SQLiteDatabase.CONFLICT_REPLACE, values) + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration93.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration93.kt new file mode 100644 index 00000000..9d5df5b3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration93.kt @@ -0,0 +1,15 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-5-26. + */ + +package pl.szczodrzynski.edziennik.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration93 : Migration(92, 93) { + override fun migrate(database: SupportSQLiteDatabase) { + // notifications - long text + database.execSQL("ALTER TABLE notifications ADD COLUMN textLong TEXT DEFAULT NULL;") + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration94.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration94.kt new file mode 100644 index 00000000..1facd564 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration94.kt @@ -0,0 +1,26 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-1. + */ + +package pl.szczodrzynski.edziennik.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import pl.szczodrzynski.edziennik.data.db.entity.Event +import pl.szczodrzynski.edziennik.data.db.enums.LoginType + +class Migration94 : Migration(93, 94) { + override fun migrate(database: SupportSQLiteDatabase) { + // events - is downloaded flag + + // get all profiles using Mobidziennik + database.execSQL("CREATE TABLE _94_ids (id INTEGER NOT NULL);") + database.execSQL("INSERT INTO _94_ids SELECT profileId FROM profiles JOIN loginStores USING(loginStoreId) WHERE loginStores.loginStoreType = ${LoginType.MOBIDZIENNIK};") + + database.execSQL("ALTER TABLE events ADD COLUMN eventIsDownloaded INT NOT NULL DEFAULT 1;") + // set isDownloaded = 0 for information events in Mobidziennik + database.execSQL("UPDATE events SET eventIsDownloaded = 0 WHERE profileId IN (SELECT id FROM _94_ids) AND eventType = ${Event.TYPE_INFORMATION};") + + database.execSQL("DROP TABLE _94_ids;") + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration95.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration95.kt new file mode 100644 index 00000000..0281de6b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration95.kt @@ -0,0 +1,15 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-1. + */ + +package pl.szczodrzynski.edziennik.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration95 : Migration(94, 95) { + override fun migrate(database: SupportSQLiteDatabase) { + // timetable - is extra flag + database.execSQL("ALTER TABLE timetable ADD COLUMN isExtra INT NOT NULL DEFAULT 0;") + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration96.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration96.kt new file mode 100644 index 00000000..e8315aa3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration96.kt @@ -0,0 +1,15 @@ +/* + * Copyright (c) Antoni Czaplicki 2021-10-17. + */ + +package pl.szczodrzynski.edziennik.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration96 : Migration(95, 96) { + override fun migrate(database: SupportSQLiteDatabase) { + // teachers - associated subjects list + database.execSQL("ALTER TABLE teachers ADD COLUMN teacherSubjects TEXT NOT NULL DEFAULT '[]';") + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration97.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration97.kt new file mode 100644 index 00000000..13014ecb --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration97.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-16. + */ + +package pl.szczodrzynski.edziennik.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration97 : Migration(96, 97) { + override fun migrate(database: SupportSQLiteDatabase) { + // notes + database.execSQL("""CREATE TABLE notes ( + profileId INTEGER NOT NULL, + noteId INTEGER NOT NULL, + noteOwnerType TEXT, + noteOwnerId INTEGER, + noteReplacesOriginal INTEGER NOT NULL, + noteTopic TEXT, + noteBody TEXT NOT NULL, + noteColor INTEGER, + noteSharedBy TEXT, + noteSharedByName TEXT, + addedDate INTEGER NOT NULL, + PRIMARY KEY(noteId) + );""") + database.execSQL("CREATE INDEX IF NOT EXISTS index_notes_profileId_noteOwnerType_noteOwnerId ON notes (profileId, noteOwnerType, noteOwnerId);") + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration98.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration98.kt new file mode 100644 index 00000000..91003fd9 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration98.kt @@ -0,0 +1,15 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-16. + */ + +package pl.szczodrzynski.edziennik.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration98 : Migration(97, 98) { + override fun migrate(database: SupportSQLiteDatabase) { + // timetable colors - override color in lesson object + database.execSQL("ALTER TABLE timetable ADD COLUMN color INT DEFAULT NULL;") + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration99.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration99.kt new file mode 100644 index 00000000..d321765c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration99.kt @@ -0,0 +1,15 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-19. + */ + +package pl.szczodrzynski.edziennik.data.db.migration + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration99 : Migration(98, 99) { + override fun migrate(database: SupportSQLiteDatabase) { + // enum refactor, part 1 - make LoginStore modes unique, even without type + database.execSQL("UPDATE loginStores SET loginStoreMode = loginStoreType * 100 + loginStoreMode;") + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/FirebaseService.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/FirebaseService.kt index 6396a87c..80dcd4ea 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/FirebaseService.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/FirebaseService.kt @@ -13,7 +13,7 @@ import com.google.firebase.iid.zzad import com.google.firebase.iid.zzaz import com.google.firebase.messaging.zzc import com.google.gson.JsonObject -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.ext.* import java.util.* import com.google.firebase.messaging.zzo.zza as logNotificationOpen import com.google.firebase.messaging.zzo.zza as logNotificationReceived diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt index 1d563c34..b92a38b5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt @@ -6,16 +6,31 @@ package pl.szczodrzynski.edziennik.data.firebase import com.google.gson.JsonParser import com.google.gson.reflect.TypeToken -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.data.api.events.FeedbackMessageEvent import pl.szczodrzynski.edziennik.data.api.events.RegisterAvailabilityEvent import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update import pl.szczodrzynski.edziennik.data.api.task.PostNotifications -import pl.szczodrzynski.edziennik.data.db.entity.* -import pl.szczodrzynski.edziennik.sync.UpdateWorker +import pl.szczodrzynski.edziennik.data.db.entity.Event +import pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage +import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.entity.Note +import pl.szczodrzynski.edziennik.data.db.entity.Notification +import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.data.db.enums.NotificationType +import pl.szczodrzynski.edziennik.ext.getInt +import pl.szczodrzynski.edziennik.ext.getLong +import pl.szczodrzynski.edziennik.ext.getString +import pl.szczodrzynski.edziennik.ext.resolveString +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time import kotlin.coroutines.CoroutineContext @@ -43,12 +58,24 @@ class SzkolnyAppFirebase(val app: App, val profiles: List, val message: message.data.getLong("eventId") ?: return@run, message.data.getString("message") ?: return@run ) + "sharedNote" -> sharedNote( + message.data.getString("shareTeamCode") ?: return@run, + message.data.getString("note") ?: return@run, + message.data.getString("message") ?: return@run, + ) + "unsharedNote" -> unsharedNote( + message.data.getString("unshareTeamCode") ?: return@run, + message.data.getLong("noteId") ?: return@run, + ) "serverMessage", "unpairedBrowser" -> serverMessage( message.data.getString("title") ?: "", message.data.getString("message") ?: "" ) - "appUpdate" -> launch { UpdateWorker.runNow(app, app.gson.fromJson(message.data.getString("update"), Update::class.java)) } + "appUpdate" -> { + val update = app.gson.fromJson(message.data.getString("update"), Update::class.java) + app.updateManager.process(update, notify = true) + } "feedbackMessage" -> launch { val message = app.gson.fromJson(message.data.getString("message"), FeedbackMessage::class.java) ?: return@launch feedbackMessage(message) @@ -60,7 +87,7 @@ class SzkolnyAppFirebase(val app: App, val profiles: List, val message: ) ?: return@launch app.config.sync.registerAvailability = data if (EventBus.getDefault().hasSubscriberForEvent(RegisterAvailabilityEvent::class.java)) { - EventBus.getDefault().postSticky(RegisterAvailabilityEvent(data)) + EventBus.getDefault().postSticky(RegisterAvailabilityEvent()) } } } @@ -72,7 +99,7 @@ class SzkolnyAppFirebase(val app: App, val profiles: List, val message: id = System.currentTimeMillis(), title = title, text = message, - type = Notification.TYPE_SERVER_MESSAGE, + type = NotificationType.SERVER_MESSAGE, profileId = null, profileName = title ).addExtra("action", "serverMessage").addExtra("serverMessageTitle", title).addExtra("serverMessageText", message) @@ -92,7 +119,7 @@ class SzkolnyAppFirebase(val app: App, val profiles: List, val message: id = System.currentTimeMillis(), title = "Wiadomość od ${message.senderName}", text = message.text, - type = Notification.TYPE_FEEDBACK_MESSAGE, + type = NotificationType.FEEDBACK_MESSAGE, profileId = null, profileName = "Wiadomość od ${message.senderName}" ).addExtra("action", "feedbackMessage").addExtra("feedbackMessageDeviceId", message.deviceId) @@ -104,7 +131,7 @@ class SzkolnyAppFirebase(val app: App, val profiles: List, val message: } private fun sharedEvent(teamCode: String, jsonStr: String, message: String) { - val json = JsonParser().parse(jsonStr).asJsonObject + val json = JsonParser.parseString(jsonStr).asJsonObject val teams = app.db.teamDao().allNow // not used, as the server provides a sharing message //val eventTypes = app.db.eventTypeDao().allNow @@ -115,14 +142,14 @@ class SzkolnyAppFirebase(val app: App, val profiles: List, val message: teams.filter { it.code == teamCode }.distinctBy { it.profileId }.forEach { team -> val profile = profiles.firstOrNull { it.id == team.profileId } ?: return@forEach - if (profile.registration != Profile.REGISTRATION_ENABLED) + if (!profile.canShare) return@forEach val event = Event( profileId = team.profileId, id = json.getLong("id") ?: return, date = json.getInt("eventDate")?.let { Date.fromValue(it) } ?: return, time = json.getInt("startTime")?.let { Time.fromValue(it) }, - topic = json.getString("topic") ?: "", + topic = json.getString("topicHtml") ?: json.getString("topic") ?: "", color = json.getInt("color"), type = json.getLong("type") ?: 0, teacherId = json.getLong("teacherId") ?: -1, @@ -133,30 +160,33 @@ class SzkolnyAppFirebase(val app: App, val profiles: List, val message: if (event.color == -1) event.color = null + event.addedManually = true event.sharedBy = json.getString("sharedBy") event.sharedByName = json.getString("sharedByName") - if (profile.userCode == event.sharedBy) event.sharedBy = "self" + if (profile.userCode == event.sharedBy) { + event.sharedBy = "self" + } val metadata = Metadata( event.profileId, - if (event.type == Event.TYPE_HOMEWORK) Metadata.TYPE_HOMEWORK else Metadata.TYPE_EVENT, + if (event.isHomework) MetadataType.HOMEWORK else MetadataType.EVENT, event.id, false, true ) - val type = if (event.type == Event.TYPE_HOMEWORK) Notification.TYPE_NEW_SHARED_HOMEWORK else Notification.TYPE_NEW_SHARED_EVENT - val notificationFilter = app.config.getFor(event.profileId).sync.notificationFilter + val type = if (event.isHomework) NotificationType.SHARED_HOMEWORK else NotificationType.SHARED_EVENT + val notificationFilter = app.config[event.profileId].sync.notificationFilter - if (!notificationFilter.contains(type)) { + if (!notificationFilter.contains(type) && event.sharedBy != "self" && event.date >= Date.getToday()) { val notification = Notification( id = Notification.buildId(event.profileId, type, event.id), - title = app.getNotificationTitle(type), + title = type.titleRes.resolveString(app), text = message, type = type, profileId = profile.id, profileName = profile.name, - viewId = if (event.type == Event.TYPE_HOMEWORK) MainActivity.DRAWER_ITEM_HOMEWORK else MainActivity.DRAWER_ITEM_AGENDA, + navTarget = if (event.isHomework) NavTarget.HOMEWORK else NavTarget.AGENDA, addedDate = event.addedDate ).addExtra("eventId", event.id).addExtra("eventDate", event.date.value.toLong()) notificationList += notification @@ -179,20 +209,19 @@ class SzkolnyAppFirebase(val app: App, val profiles: List, val message: teams.filter { it.code == teamCode }.distinctBy { it.profileId }.forEach { team -> val profile = profiles.firstOrNull { it.id == team.profileId } ?: return@forEach - if (profile.registration != Profile.REGISTRATION_ENABLED) + if (!profile.canShare) return@forEach - val notificationFilter = app.config.getFor(team.profileId).sync.notificationFilter + val notificationFilter = app.config[team.profileId].sync.notificationFilter - if (!notificationFilter.contains(Notification.TYPE_REMOVED_SHARED_EVENT)) { + if (!notificationFilter.contains(NotificationType.REMOVED_SHARED_EVENT)) { val notification = Notification( - id = Notification.buildId(profile.id - ?: 0, Notification.TYPE_REMOVED_SHARED_EVENT, eventId), - title = app.getNotificationTitle(Notification.TYPE_REMOVED_SHARED_EVENT), + id = Notification.buildId(profile.id, NotificationType.REMOVED_SHARED_EVENT, eventId), + title = NotificationType.REMOVED_SHARED_EVENT.titleRes.resolveString(app), text = message, - type = Notification.TYPE_REMOVED_SHARED_EVENT, + type = NotificationType.REMOVED_SHARED_EVENT, profileId = profile.id, profileName = profile.name, - viewId = MainActivity.DRAWER_ITEM_AGENDA + navTarget = NavTarget.AGENDA, ) notificationList += notification } @@ -203,4 +232,71 @@ class SzkolnyAppFirebase(val app: App, val profiles: List, val message: PostNotifications(app, notificationList) } } + + private fun sharedNote(teamCode: String, jsonStr: String, message: String) { + val note = app.gson.fromJson(jsonStr, Note::class.java) + val noteSharedBy = note.sharedBy + val teams = app.db.teamDao().allNow + // not used, as the server provides a sharing message + //val eventTypes = app.db.eventTypeDao().allNow + + val notes = mutableListOf() + val notificationList = mutableListOf() + + teams.filter { it.code == teamCode }.distinctBy { it.profileId }.forEach { team -> + val profile = profiles.firstOrNull { it.id == team.profileId } ?: return@forEach + if (!profile.canShare) + return@forEach + note.profileId = team.profileId + if (profile.userCode == note.sharedBy) { + note.sharedBy = "self" + } else { + note.sharedBy = noteSharedBy + } + + if (!app.noteManager.hasValidOwner(note)) + return@forEach + + notes += note + + val hadNote = app.db.noteDao().getNow(note.profileId, note.id) != null + // skip creating notifications + if (hadNote) + return@forEach + + val type = NotificationType.SHARED_NOTE + val notificationFilter = app.config[note.profileId].sync.notificationFilter + + if (!notificationFilter.contains(type) && note.sharedBy != "self") { + val notification = Notification( + id = Notification.buildId(note.profileId, type, note.id), + title = type.titleRes.resolveString(app), + text = message, + type = type, + profileId = profile.id, + profileName = profile.name, + navTarget = NavTarget.HOME, + addedDate = note.addedDate + ).addExtra("noteId", note.id) + notificationList += notification + } + } + app.db.noteDao().addAll(notes) + if (notificationList.isNotEmpty()) { + app.db.notificationDao().addAll(notificationList) + PostNotifications(app, notificationList) + } + } + + private fun unsharedNote(teamCode: String, noteId: Long) { + val teams = app.db.teamDao().allNow + + teams.filter { it.code == teamCode }.distinctBy { it.profileId }.forEach { team -> + val profile = profiles.firstOrNull { it.id == team.profileId } ?: return@forEach + if (!profile.canShare) + return@forEach + + app.db.noteDao().remove(team.profileId, noteId) + } + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyLibrusFirebase.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyLibrusFirebase.kt index cc59faf4..14c3f998 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyLibrusFirebase.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyLibrusFirebase.kt @@ -5,13 +5,38 @@ package pl.szczodrzynski.edziennik.data.firebase import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.MainActivity -import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_LIBRUS import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask -import pl.szczodrzynski.edziennik.data.api.edziennik.librus.* +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_ANNOUNCEMENTS +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_ATTENDANCES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_CATEGORIES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_COMMENTS +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_CLASS_FREE_DAYS +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADE_CATEGORIES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_DESCRIPTIVE_TEXT_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_EVENTS +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_HOMEWORK +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_LUCKY_NUMBER +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_NORMAL_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_NORMAL_GRADE_CATEGORIES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_NOTICES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_POINT_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_POINT_GRADE_CATEGORIES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_PT_MEETINGS +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_SCHOOL_FREE_DAYS +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_TEACHER_FREE_DAYS +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_TEXT_GRADES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_API_TIMETABLES +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_MESSAGES_RECEIVED +import pl.szczodrzynski.edziennik.data.api.edziennik.librus.ENDPOINT_LIBRUS_SYNERGIA_HOMEWORK import pl.szczodrzynski.edziennik.data.api.task.IApiTask import pl.szczodrzynski.edziennik.data.db.entity.Profile -import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.LoginType +import pl.szczodrzynski.edziennik.ext.getString +import pl.szczodrzynski.edziennik.ext.getStudentData class SzkolnyLibrusFirebase(val app: App, val profiles: List, val message: FirebaseService.Message) { /*{ @@ -31,27 +56,27 @@ class SzkolnyLibrusFirebase(val app: App, val profiles: List, val messa /* ./src/store/modules/helpers/change-processor.js */ val endpoints = when (type) { - "Notes" -> listOf(ENDPOINT_LIBRUS_API_NOTICES) - "Grades" -> listOf(ENDPOINT_LIBRUS_API_NORMAL_GRADES, ENDPOINT_LIBRUS_API_NORMAL_GRADE_CATEGORIES, ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS) - "PointGrades" -> listOf(ENDPOINT_LIBRUS_API_POINT_GRADES, ENDPOINT_LIBRUS_API_POINT_GRADE_CATEGORIES) - "DescriptiveGrades" -> listOf(ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADES) - "DescriptiveGrades/Text/Categories" -> listOf(ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADE_CATEGORIES) - "DescriptiveTextGrades" -> listOf(ENDPOINT_LIBRUS_API_DESCRIPTIVE_TEXT_GRADES) - "TextGrades" -> listOf(ENDPOINT_LIBRUS_API_TEXT_GRADES) - "BehaviourGrades/Points" -> listOf(ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADES, ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_CATEGORIES, ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_COMMENTS) - "BehaviourGrades" -> listOf() - "Attendances" -> listOf(ENDPOINT_LIBRUS_API_ATTENDANCES) - "HomeWorks" -> listOf(ENDPOINT_LIBRUS_API_EVENTS) - "ParentTeacherConferences" -> listOf(ENDPOINT_LIBRUS_API_PT_MEETINGS) - "Calendars/ClassFreeDays" -> listOf(ENDPOINT_LIBRUS_API_CLASS_FREE_DAYS) - "Calendars/TeacherFreeDays" -> listOf(ENDPOINT_LIBRUS_API_TEACHER_FREE_DAYS) - "Calendars/SchoolFreeDays" -> listOf(ENDPOINT_LIBRUS_API_SCHOOL_FREE_DAYS) - "Calendars/Substitutions" -> listOf(ENDPOINT_LIBRUS_API_TIMETABLES) - "HomeWorkAssignments" -> listOf(ENDPOINT_LIBRUS_API_HOMEWORK, ENDPOINT_LIBRUS_SYNERGIA_HOMEWORK) - "SchoolNotices" -> listOf(ENDPOINT_LIBRUS_API_ANNOUNCEMENTS) - "Messages" -> listOf(ENDPOINT_LIBRUS_MESSAGES_RECEIVED) - "LuckyNumbers" -> listOf(ENDPOINT_LIBRUS_API_LUCKY_NUMBER) - "Timetables" -> listOf(ENDPOINT_LIBRUS_API_TIMETABLES) + "Notes" -> setOf(ENDPOINT_LIBRUS_API_NOTICES) + "Grades" -> setOf(ENDPOINT_LIBRUS_API_NORMAL_GRADES, ENDPOINT_LIBRUS_API_NORMAL_GRADE_CATEGORIES, ENDPOINT_LIBRUS_API_NORMAL_GRADE_COMMENTS) + "PointGrades" -> setOf(ENDPOINT_LIBRUS_API_POINT_GRADES, ENDPOINT_LIBRUS_API_POINT_GRADE_CATEGORIES) + "DescriptiveGrades" -> setOf(ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADES) + "DescriptiveGrades/Text/Categories" -> setOf(ENDPOINT_LIBRUS_API_DESCRIPTIVE_GRADE_CATEGORIES) + "DescriptiveTextGrades" -> setOf(ENDPOINT_LIBRUS_API_DESCRIPTIVE_TEXT_GRADES) + "TextGrades" -> setOf(ENDPOINT_LIBRUS_API_TEXT_GRADES) + "BehaviourGrades/Points" -> setOf(ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADES, ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_CATEGORIES, ENDPOINT_LIBRUS_API_BEHAVIOUR_GRADE_COMMENTS) + "BehaviourGrades" -> setOf() + "Attendances" -> setOf(ENDPOINT_LIBRUS_API_ATTENDANCES) + "HomeWorks" -> setOf(ENDPOINT_LIBRUS_API_EVENTS) + "ParentTeacherConferences" -> setOf(ENDPOINT_LIBRUS_API_PT_MEETINGS) + "Calendars/ClassFreeDays" -> setOf(ENDPOINT_LIBRUS_API_CLASS_FREE_DAYS) + "Calendars/TeacherFreeDays" -> setOf(ENDPOINT_LIBRUS_API_TEACHER_FREE_DAYS) + "Calendars/SchoolFreeDays" -> setOf(ENDPOINT_LIBRUS_API_SCHOOL_FREE_DAYS) + "Calendars/Substitutions" -> setOf(ENDPOINT_LIBRUS_API_TIMETABLES) + "HomeWorkAssignments" -> setOf(ENDPOINT_LIBRUS_API_HOMEWORK, ENDPOINT_LIBRUS_SYNERGIA_HOMEWORK) + "SchoolNotices" -> setOf(ENDPOINT_LIBRUS_API_ANNOUNCEMENTS) + "Messages" -> setOf(ENDPOINT_LIBRUS_MESSAGES_RECEIVED) + "LuckyNumbers" -> setOf(ENDPOINT_LIBRUS_API_LUCKY_NUMBER) + "Timetables" -> setOf(ENDPOINT_LIBRUS_API_TIMETABLES) else -> return@run } @@ -59,10 +84,10 @@ class SzkolnyLibrusFirebase(val app: App, val profiles: List, val messa return@run val tasks = profiles.filter { - it.loginStoreType == LOGIN_TYPE_LIBRUS && + it.loginStoreType == LoginType.LIBRUS && it.getStudentData("accountLogin", "")?.replace("u", "") == accountLogin }.map { - EdziennikTask.syncProfile(it.id, listOf(MainActivity.DRAWER_ITEM_HOME to 0), onlyEndpoints = endpoints) + EdziennikTask.syncProfile(it.id, setOf(FeatureType.ALWAYS_NEEDED), onlyEndpoints = endpoints) } IApiTask.enqueueAll(app, tasks) }} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyMobidziennikFirebase.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyMobidziennikFirebase.kt index b097dbed..c1397ab2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyMobidziennikFirebase.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyMobidziennikFirebase.kt @@ -5,17 +5,14 @@ package pl.szczodrzynski.edziennik.data.firebase import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_ATTENDANCE -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_BEHAVIOUR -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_GRADES -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES -import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_MOBIDZIENNIK import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.task.IApiTask -import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED import pl.szczodrzynski.edziennik.data.db.entity.Profile -import pl.szczodrzynski.edziennik.getLong -import pl.szczodrzynski.edziennik.getString +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.LoginType +import pl.szczodrzynski.edziennik.ext.getLong +import pl.szczodrzynski.edziennik.ext.getString +import pl.szczodrzynski.edziennik.ext.getStudentData class SzkolnyMobidziennikFirebase(val app: App, val profiles: List, val message: FirebaseService.Message) { /*{ @@ -52,19 +49,19 @@ class SzkolnyMobidziennikFirebase(val app: App, val profiles: List, val val globalId = message.data.getLong("global_id") /* assets/www/js/push.js */ - val viewIdPair = when (type) { - "wiadOdebrana" -> DRAWER_ITEM_MESSAGES to TYPE_RECEIVED - "oceny", "ocenyKoncowe", "zachowanie" -> DRAWER_ITEM_GRADES to 0 - "uwagi" -> DRAWER_ITEM_BEHAVIOUR to 0 - "nieobecnoscPierwszaLekcja", "nieobecnosciDzisiaj" -> DRAWER_ITEM_ATTENDANCE to 0 + val featureType = when (type) { + "wiadOdebrana" -> FeatureType.MESSAGES_INBOX + "oceny", "ocenyKoncowe", "zachowanie" -> FeatureType.GRADES + "uwagi" -> FeatureType.BEHAVIOUR + "nieobecnoscPierwszaLekcja", "nieobecnosciDzisiaj" -> FeatureType.ATTENDANCE else -> return@run } val tasks = profiles.filter { - it.loginStoreType == LOGIN_TYPE_MOBIDZIENNIK && + it.loginStoreType == LoginType.MOBIDZIENNIK && it.getStudentData("globalId", 0L) == globalId }.map { - EdziennikTask.syncProfile(it.id, listOf(viewIdPair)) + EdziennikTask.syncProfile(it.id, setOf(featureType)) } IApiTask.enqueueAll(app, tasks) }} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyVulcanFirebase.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyVulcanFirebase.kt index 06c2f171..ec8f8011 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyVulcanFirebase.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyVulcanFirebase.kt @@ -4,13 +4,16 @@ package pl.szczodrzynski.edziennik.data.firebase -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_VULCAN +import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.task.IApiTask -import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Profile -import java.util.* +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.LoginType +import pl.szczodrzynski.edziennik.ext.getInt +import pl.szczodrzynski.edziennik.ext.getString +import pl.szczodrzynski.edziennik.ext.getStudentData +import pl.szczodrzynski.edziennik.ext.toJsonObject class SzkolnyVulcanFirebase(val app: App, val profiles: List, val message: FirebaseService.Message) { /*{ @@ -32,22 +35,22 @@ class SzkolnyVulcanFirebase(val app: App, val profiles: List, val messa val loginId = data.getInt("loginid") /* pl.vulcan.uonetmobile.auxilary.enums.CDCPushEnum */ - val viewIdPair = when (type.toLowerCase(Locale.ROOT)) { - "wiadomosc" -> MainActivity.DRAWER_ITEM_MESSAGES to Message.TYPE_RECEIVED - "ocena" -> MainActivity.DRAWER_ITEM_GRADES to 0 - "uwaga" -> MainActivity.DRAWER_ITEM_BEHAVIOUR to 0 - "frekwencja" -> MainActivity.DRAWER_ITEM_ATTENDANCE to 0 + val featureType = when (type.lowercase()) { + "wiadomosc" -> FeatureType.MESSAGES_INBOX + "ocena" -> FeatureType.GRADES + "uwaga" -> FeatureType.BEHAVIOUR + "frekwencja" -> FeatureType.ATTENDANCE // this type is not even implemented in Dzienniczek+ - "sprawdzian" -> MainActivity.DRAWER_ITEM_AGENDA to 0 + "sprawdzian" -> FeatureType.AGENDA else -> return@run } val tasks = profiles.filter { - it.loginStoreType == LOGIN_TYPE_VULCAN + it.loginStoreType == LoginType.VULCAN && (it.getStudentData("studentId", 0) == studentId || it.getStudentData("studentLoginId", 0) == loginId) }.map { - EdziennikTask.syncProfile(it.id, listOf(viewIdPair)) + EdziennikTask.syncProfile(it.id, setOf(featureType)) } IApiTask.enqueueAll(app, tasks) }} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/ArrayExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/ArrayExtensions.kt new file mode 100644 index 00000000..e263627c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/ArrayExtensions.kt @@ -0,0 +1,203 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-17. + */ + +package pl.szczodrzynski.edziennik.ext + +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.SpannedString +import android.util.LongSparseArray +import android.util.SparseArray +import android.util.SparseIntArray +import androidx.core.util.forEach +import java.util.* + +fun Collection?.isNotNullNorEmpty(): Boolean { + return this != null && this.isNotEmpty() +} + +fun List.join(delimiter: String): String { + return concat(delimiter).toString() +} + +fun LongSparseArray.values(): List { + val result = mutableListOf() + forEach { _, value -> + result += value + } + return result +} + +fun SparseArray<*>.keys(): List { + val result = mutableListOf() + forEach { key, _ -> + result += key + } + return result +} +fun SparseArray.values(): List { + val result = mutableListOf() + forEach { _, value -> + result += value + } + return result +} + +fun SparseIntArray.keys(): List { + val result = mutableListOf() + forEach { key, _ -> + result += key + } + return result +} +fun SparseIntArray.values(): List { + val result = mutableListOf() + forEach { _, value -> + result += value + } + return result +} + +fun List>.keys() = map { it.first } +fun List>.values() = map { it.second } + +fun List.toSparseArray(destination: SparseArray, key: (T) -> Int) { + forEach { + destination.put(key(it), it) + } +} +fun List.toSparseArray(destination: LongSparseArray, key: (T) -> Long) { + forEach { + destination.put(key(it), it) + } +} + +fun List.toSparseArray(key: (T) -> Int): SparseArray { + val result = SparseArray() + toSparseArray(result, key) + return result +} +fun List.toSparseArray(key: (T) -> Long): LongSparseArray { + val result = LongSparseArray() + toSparseArray(result, key) + return result +} + +fun SparseArray.singleOrNull(predicate: (T) -> Boolean): T? { + forEach { _, value -> + if (predicate(value)) + return value + } + return null +} +fun LongSparseArray.singleOrNull(predicate: (T) -> Boolean): T? { + forEach { _, value -> + if (predicate(value)) + return value + } + return null +} + +/** + * Returns a new read-only list only of those given elements, that are not empty. + * Applies for CharSequence and descendants. + */ +fun listOfNotEmpty(vararg elements: T): List = elements.filterNot { it.isEmpty() } + +fun List.concat(delimiter: CharSequence? = null): CharSequence { + if (this.isEmpty()) { + return "" + } + + if (this.size == 1) { + return this[0] ?: "" + } + + var spanned = delimiter is Spanned + if (!spanned) { + for (piece in this) { + if (piece is Spanned) { + spanned = true + break + } + } + } + + var first = true + if (spanned) { + val ssb = SpannableStringBuilder() + for (piece in this) { + if (piece == null) + continue + if (!first && delimiter != null) + ssb.append(delimiter) + first = false + ssb.append(piece) + } + return SpannedString(ssb) + } else { + val sb = StringBuilder() + for (piece in this) { + if (piece == null) + continue + if (!first && delimiter != null) + sb.append(delimiter) + first = false + sb.append(piece) + } + return sb.toString() + } +} + +inline fun List.ifNotEmpty(block: (List) -> Unit) { + if (!isEmpty()) + block(this) +} + +inline fun LongSparseArray.filter(predicate: (T) -> Boolean): List { + val destination = ArrayList() + this.forEach { _, element -> if (predicate(element)) destination.add(element) } + return destination +} + +fun CharSequence.containsAll(list: List, ignoreCase: Boolean = false): Boolean { + for (i in list) { + if (!contains(i, ignoreCase)) + return false + } + return true +} + +fun MutableList.after(what: E, insert: E) { + val index = indexOf(what) + if (index != -1) + add(index + 1, insert) +} + +fun MutableList.before(what: E, insert: E) { + val index = indexOf(what) + if (index != -1) + add(index, insert) +} + +fun MutableList.after(what: E, insert: Collection) { + val index = indexOf(what) + if (index != -1) + addAll(index + 1, insert) +} + +fun MutableList.before(what: E, insert: Collection) { + val index = indexOf(what) + if (index != -1) + addAll(index, insert) +} + +operator fun Iterable>.get(key: K): V? { + return firstOrNull { it.first == key }?.second +} + +@kotlin.jvm.JvmName("averageOrNullOfInt") +fun Iterable.averageOrNull() = this.average().let { if (it.isNaN()) null else it } +@kotlin.jvm.JvmName("averageOrNullOfFloat") +fun Iterable.averageOrNull() = this.average().let { if (it.isNaN()) null else it } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/BundleExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/BundleExtensions.kt new file mode 100644 index 00000000..eddcf322 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/BundleExtensions.kt @@ -0,0 +1,96 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-17. + */ + +package pl.szczodrzynski.edziennik.ext + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import com.google.gson.JsonObject +import pl.szczodrzynski.edziennik.data.db.enums.* +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget +import java.io.Serializable + +fun Bundle?.getInt(key: String, defaultValue: Int) = + this?.getInt(key, defaultValue) ?: defaultValue + +fun Bundle?.getLong(key: String, defaultValue: Long) = + this?.getLong(key, defaultValue) ?: defaultValue + +fun Bundle?.getFloat(key: String, defaultValue: Float) = + this?.getFloat(key, defaultValue) ?: defaultValue + +fun Bundle?.getString(key: String, defaultValue: String) = + this?.getString(key, defaultValue) ?: defaultValue + +inline fun > Bundle?.getEnum(key: String) = this?.getInt(key)?.toEnum() +fun Bundle.putEnum(key: String, value: Enum<*>) = putInt(key, value.toInt()) + +fun Bundle?.getIntOrNull(key: String): Int? { + return this?.get(key) as? Int +} + +@Suppress("UNCHECKED_CAST") +fun Bundle?.get(key: String): T? { + return this?.get(key) as? T? +} + +@Suppress("UNCHECKED_CAST") +fun Bundle(vararg properties: Pair): Bundle { + return Bundle().apply { + for (property in properties) { + val (key, value) = property + when (value) { + is String -> putString(key, value as String?) + is Char -> putChar(key, value) + is Int -> putInt(key, value) + is Long -> putLong(key, value) + is Float -> putFloat(key, value) + is Short -> putShort(key, value) + is Double -> putDouble(key, value) + is Boolean -> putBoolean(key, value) + is Bundle -> putBundle(key, value) + is Enum<*> -> putEnum(key, value) + is Array<*> -> putParcelableArray(key, value as Array) + is Parcelable -> putParcelable(key, value) + is Serializable -> putSerializable(key, value) + } + } + } +} + +fun Intent(action: String? = null, vararg properties: Pair): Intent { + return Intent(action).putExtras(Bundle(*properties)) +} + +fun Intent(packageContext: Context, cls: Class<*>, vararg properties: Pair): Intent { + return Intent(packageContext, cls).putExtras(Bundle(*properties)) +} + +fun Intent.putExtras(vararg properties: Pair) = putExtras(Bundle(*properties)) +fun Bundle.putExtras(vararg properties: Pair) = putAll(Bundle(*properties)) + +fun Bundle.toJsonObject(): JsonObject { + val json = JsonObject() + keySet()?.forEach { key -> + get(key)?.let { + when (it) { + is String -> json.addProperty(key, it) + is Char -> json.addProperty(key, it) + is Int -> json.addProperty(key, it) + is Long -> json.addProperty(key, it) + is Float -> json.addProperty(key, it) + is Short -> json.addProperty(key, it) + is Double -> json.addProperty(key, it) + is Boolean -> json.addProperty(key, it) + is Bundle -> json.add(key, it.toJsonObject()) + else -> json.addProperty(key, it.toString()) + } + } + } + return json +} + +fun Intent.toJsonObject() = extras?.toJsonObject() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/ContextExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/ContextExtensions.kt new file mode 100644 index 00000000..395b0102 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/ContextExtensions.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-17. + */ + +package pl.szczodrzynski.edziennik.ext + +import android.content.Context +import android.os.Build +import java.util.* + +fun Context.setLanguage(language: String) { + val locale = Locale(language.lowercase()) + val configuration = resources.configuration + Locale.setDefault(locale) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + configuration.setLocale(locale) + } + configuration.locale = locale + resources.updateConfiguration(configuration, resources.displayMetrics) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/CryptoExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/CryptoExtensions.kt new file mode 100644 index 00000000..f1a67e37 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/CryptoExtensions.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-17. + */ + +package pl.szczodrzynski.edziennik.ext + +import android.util.Base64 +import java.math.BigInteger +import java.nio.charset.Charset +import java.security.MessageDigest +import java.util.zip.CRC32 +import javax.crypto.Mac +import javax.crypto.spec.SecretKeySpec + +fun String.crc16(): Int { + var crc = 0xFFFF + for (aBuffer in this) { + crc = crc.ushr(8) or (crc shl 8) and 0xffff + crc = crc xor (aBuffer.code and 0xff) // byte to int, trunc sign + crc = crc xor (crc and 0xff shr 4) + crc = crc xor (crc shl 12 and 0xffff) + crc = crc xor (crc and 0xFF shl 5 and 0xffff) + } + crc = crc and 0xffff + return crc + 32768 +} + +fun String.crc32(): Long { + val crc = CRC32() + crc.update(toByteArray()) + return crc.value +} + +fun String.hmacSHA1(password: String): String { + val key = SecretKeySpec(password.toByteArray(), "HmacSHA1") + + val mac = Mac.getInstance("HmacSHA1").apply { + init(key) + update(this@hmacSHA1.toByteArray()) + } + + return Base64.encodeToString(mac.doFinal(), Base64.NO_WRAP) +} + +fun String.md5(): String { + val md = MessageDigest.getInstance("MD5") + return BigInteger(1, md.digest(toByteArray())).toString(16).padStart(32, '0') +} + +fun String.sha1Hex(): String { + val md = MessageDigest.getInstance("SHA-1") + md.update(toByteArray()) + return md.digest().joinToString("") { "%02x".format(it) } +} + +fun String.sha256(): ByteArray { + val md = MessageDigest.getInstance("SHA-256") + md.update(toByteArray()) + return md.digest() +} + +fun String.base64Encode(): String { + return Base64.encodeToString(toByteArray(), Base64.NO_WRAP) +} +fun ByteArray.base64Encode(): String { + return Base64.encodeToString(this, Base64.NO_WRAP) +} +fun String.base64Decode(): ByteArray { + return Base64.decode(this, Base64.DEFAULT) +} +fun String.base64DecodeToString(): String { + return Base64.decode(this, Base64.DEFAULT).toString(Charset.defaultCharset()) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/DataExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/DataExtensions.kt new file mode 100644 index 00000000..977952e5 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/DataExtensions.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-17. + */ + +package pl.szczodrzynski.edziennik.ext + +import android.util.LongSparseArray +import androidx.core.util.forEach +import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.data.db.entity.Team + +fun List.byId(id: Long) = firstOrNull { it.id == id } +fun List.byNameFirstLast(nameFirstLast: String) = firstOrNull { it.name + " " + it.surname == nameFirstLast } +fun List.byNameLastFirst(nameLastFirst: String) = firstOrNull { it.surname + " " + it.name == nameLastFirst } +fun List.byNameFDotLast(nameFDotLast: String) = firstOrNull { it.name + "." + it.surname == nameFDotLast } +fun List.byNameFDotSpaceLast(nameFDotSpaceLast: String) = firstOrNull { it.name + ". " + it.surname == nameFDotSpaceLast } + +fun List.filterOutArchived() = this.filter { !it.archived } + +fun List.getById(id: Long): Team? { + return singleOrNull { it.id == id } +} + +fun LongSparseArray.getById(id: Long): Team? { + forEach { _, value -> + if (value.id == id) + return value + } + return null +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/DialogExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/DialogExtensions.kt new file mode 100644 index 00000000..61de4f77 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/DialogExtensions.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-3-30. + */ + +package pl.szczodrzynski.edziennik.ext + +import android.annotation.SuppressLint +import android.content.res.ColorStateList +import android.text.InputType +import android.view.LayoutInflater +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import androidx.core.graphics.ColorUtils +import androidx.core.view.ViewCompat +import androidx.core.view.isVisible +import androidx.core.widget.addTextChangedListener +import com.google.android.material.color.MaterialColors +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.dialog.MaterialDialogs +import com.google.android.material.shape.MaterialShapeDrawable +import com.google.android.material.textfield.TextInputEditText +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.DialogEditTextBinding + +fun MaterialAlertDialogBuilder.input( + message: CharSequence? = null, + type: Int = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE, + hint: CharSequence? = null, + value: CharSequence? = null, + changeListener: ((editText: TextInputEditText, input: String) -> Boolean)? = null, + positiveButton: Int? = null, + positiveListener: ((editText: TextInputEditText, input: String) -> Boolean)? = null, +): MaterialAlertDialogBuilder { + val b = DialogEditTextBinding.inflate(LayoutInflater.from(context), null, false) + b.title.text = message + b.title.isVisible = message.isNotNullNorBlank() + b.text1.hint = hint + b.text1.inputType = type + b.text1.setText(value) + b.text1.addTextChangedListener { text -> + if (changeListener?.invoke(b.text1, text?.toString() ?: "") != false) + b.text1.error = null + } + if (positiveButton != null) { + setPositiveButton(positiveButton) { dialog, _ -> + if (positiveListener?.invoke(b.text1, b.text1.text?.toString() ?: "") != false) + dialog.dismiss() + } + } + setView(b.root) + + return this +} + +fun MaterialAlertDialogBuilder.setTitle( + @StringRes resId: Int, + vararg formatArgs: Any, +): MaterialAlertDialogBuilder { + setTitle(context.getString(resId, *formatArgs)) + return this +} + +fun MaterialAlertDialogBuilder.setMessage( + @StringRes resId: Int, + vararg formatArgs: Any, +): MaterialAlertDialogBuilder { + setMessage(context.getString(resId, *formatArgs)) + return this +} + +@SuppressLint("RestrictedApi") +fun AlertDialog.overlayBackgroundColor(color: Int, alpha: Int) { + // this is absolutely horrible + val colorSurface16dp = ColorUtils.compositeColors( + R.color.colorSurface_16dp.resolveColor(context), + MaterialColors.getColor( + context, + R.attr.colorSurface, + javaClass.canonicalName, + ) + ) + val colorDialogBackground = MaterialColors.layer(colorSurface16dp, color, alpha / 255f) + val backgroundInsets = MaterialDialogs.getDialogBackgroundInsets( + context, + R.attr.alertDialogStyle, + R.style.MaterialAlertDialog_MaterialComponents, + ) + val background = MaterialShapeDrawable( + context, + null, + R.attr.alertDialogStyle, + R.style.MaterialAlertDialog_MaterialComponents + ) + with(background) { + initializeElevationOverlay(context) + fillColor = ColorStateList.valueOf(colorDialogBackground) + /*if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + setCornerSize(android.R.attr.dialogCornerRadius.resolveDimenAttr(context)) + }*/ + elevation = ViewCompat.getElevation(window?.decorView ?: return@with) + } + val insetDrawable = MaterialDialogs.insetDrawable(background, backgroundInsets) + window?.setBackgroundDrawable(insetDrawable) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/EnumExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/EnumExtensions.kt new file mode 100644 index 00000000..1a55edb6 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/EnumExtensions.kt @@ -0,0 +1,75 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-17. + */ + +package pl.szczodrzynski.edziennik.ext + +import android.view.View +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.data.db.enums.* +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget +import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem + +fun Int.asFeatureType() = FeatureType.values().first { it.id == this } +fun Int.asLoginMethod() = LoginMethod.values().first { it.id == this } +fun Int.asLoginMode() = LoginMode.values().first { it.id == this } +fun Int.asLoginType() = LoginType.values().first { it.id == this } +fun Int.asMetadataType() = MetadataType.values().first { it.id == this } +fun Int.asNotificationType() = NotificationType.values().first { it.id == this } +fun Int.asNavTarget() = NavTarget.values().first { it.id == this } + +fun Int?.asFeatureTypeOrNull() = FeatureType.values().firstOrNull { it.id == this } +fun Int?.asLoginMethodOrNull() = LoginMethod.values().firstOrNull { it.id == this } +fun Int?.asLoginModeOrNull() = LoginMode.values().firstOrNull { it.id == this } +fun Int?.asLoginTypeOrNull() = LoginType.values().firstOrNull { it.id == this } +fun Int?.asMetadataTypeOrNull() = MetadataType.values().firstOrNull { it.id == this } +fun Int?.asNotificationTypeOrNull() = NotificationType.values().firstOrNull { it.id == this } +fun Int?.asNavTargetOrNull() = NavTarget.values().firstOrNull { it.id == this } + +fun Enum<*>.toInt() = when (this) { + is FeatureType -> this.id + is LoginMethod -> this.id + is LoginMode -> this.id + is LoginType -> this.id + is MetadataType -> this.id + is NotificationType -> this.id + is NavTarget -> this.id + else -> this.ordinal +} + +inline fun > Int.toEnum() = when (E::class.java) { + // enums commented out are not really used in Bundles + FeatureType::class.java -> this.asFeatureType() + // LoginMethod::class.java -> this.asLoginMethod() + LoginMode::class.java -> this.asLoginMode() + LoginType::class.java -> this.asLoginType() + // MetadataType::class.java -> this.asMetadataType() + // NotificationType::class.java -> this.asNotificationType() + NavTarget::class.java -> this.asNavTarget() + else -> enumValues()[this] +} as E + +fun > Int.toEnum(type: Class<*>) = when (type) { + // this is used for Config so all enums are here + FeatureType::class.java -> this.asFeatureType() + LoginMethod::class.java -> this.asLoginMethod() + LoginMode::class.java -> this.asLoginMode() + LoginType::class.java -> this.asLoginType() + MetadataType::class.java -> this.asMetadataType() + NotificationType::class.java -> this.asNotificationType() + NavTarget::class.java -> this.asNavTarget() + else -> throw IllegalArgumentException("Unknown type $type") +} as E + +fun getFeatureTypesNecessary() = FeatureType.values().filter { it.isAlwaysNeeded }.toSet() +fun getFeatureTypesUnnecessary() = FeatureType.values().filter { !it.isAlwaysNeeded }.toSet() + +fun NavTarget.toBottomSheetItem(activity: MainActivity) = + BottomSheetPrimaryItem(isContextual = false).also { + it.titleRes = this.nameRes + if (this.icon != null) + it.iconicsIcon = this.icon + it.onClickListener = View.OnClickListener { + activity.navigate(navTarget = this) + } + } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/GraphicsExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/GraphicsExtensions.kt new file mode 100644 index 00000000..69cbfc33 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/GraphicsExtensions.kt @@ -0,0 +1,100 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-17. + */ + +package pl.szczodrzynski.edziennik.ext + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.drawable.Drawable +import android.util.TypedValue +import androidx.annotation.* +import androidx.core.content.res.ResourcesCompat +import com.mikepenz.iconics.typeface.IIcon +import pl.szczodrzynski.navlib.ImageHolder + +fun colorFromName(name: String?): Int { + val i = (name ?: "").crc32() + return when ((i / 10 % 16 + 1).toInt()) { + 13 -> 0xffF44336 + 4 -> 0xffF50057 + 2 -> 0xffD500F9 + 9 -> 0xff6200EA + 5 -> 0xffFFAB00 + 1 -> 0xff304FFE + 6 -> 0xff40C4FF + 14 -> 0xff26A69A + 15 -> 0xff00C853 + 7 -> 0xffFFD600 + 3 -> 0xffFF3D00 + 8 -> 0xffDD2C00 + 10 -> 0xff795548 + 12 -> 0xff2979FF + 11 -> 0xffFF6D00 + else -> 0xff64DD17 + }.toInt() +} + +fun colorFromCssName(name: String): Int { + return when (name) { + "red" -> 0xffff0000 + "green" -> 0xff008000 + "blue" -> 0xff0000ff + "violet" -> 0xffee82ee + "brown" -> 0xffa52a2a + "orange" -> 0xffffa500 + "black" -> 0xff000000 + "white" -> 0xffffffff + else -> -1L + }.toInt() +} + +@ColorInt +fun @receiver:AttrRes Int.resolveAttr(context: Context?): Int { + val typedValue = TypedValue() + context?.theme?.resolveAttribute(this, typedValue, true) + return typedValue.data +} +@Dimension +fun @receiver:AttrRes Int.resolveDimenAttr(context: Context): Float { + val typedValue = TypedValue() + context.theme?.resolveAttribute(this, typedValue, true) + return typedValue.getDimension(context.resources.displayMetrics) +} +@ColorInt +fun @receiver:ColorRes Int.resolveColor(context: Context): Int { + return ResourcesCompat.getColor(context.resources, this, context.theme) +} +fun @receiver:DrawableRes Int.resolveDrawable(context: Context): Drawable { + return ResourcesCompat.getDrawable(context.resources, this, context.theme)!! +} + +fun Int.toColorStateList(): ColorStateList { + val states = arrayOf( + intArrayOf( android.R.attr.state_enabled ), + intArrayOf(-android.R.attr.state_enabled ), + intArrayOf(-android.R.attr.state_checked ), + intArrayOf( android.R.attr.state_pressed ) + ) + + val colors = intArrayOf( + this, + this, + this, + this + ) + + return ColorStateList(states, colors) +} + +fun Drawable.setTintColor(color: Int): Drawable { + colorFilter = PorterDuffColorFilter( + color, + PorterDuff.Mode.SRC_ATOP + ) + return this +} + +fun IIcon.toImageHolder() = ImageHolder(this) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/JsonExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/JsonExtensions.kt new file mode 100644 index 00000000..fb261e0e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/JsonExtensions.kt @@ -0,0 +1,144 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-17. + */ + +package pl.szczodrzynski.edziennik.ext + +import android.os.Bundle +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.contract + +fun JsonObject?.get(key: String): JsonElement? = this?.get(key) + +fun JsonObject?.getBoolean(key: String): Boolean? = get(key)?.let { if (!it.isJsonPrimitive) null else it.asBoolean } +fun JsonObject?.getString(key: String): String? = get(key)?.let { if (!it.isJsonPrimitive) null else it.asString } +fun JsonObject?.getInt(key: String): Int? = get(key)?.let { if (!it.isJsonPrimitive) null else it.asInt } +fun JsonObject?.getLong(key: String): Long? = get(key)?.let { if (!it.isJsonPrimitive) null else it.asLong } +fun JsonObject?.getFloat(key: String): Float? = get(key)?.let { if(!it.isJsonPrimitive) null else it.asFloat } +fun JsonObject?.getChar(key: String): Char? = get(key)?.let { if(!it.isJsonPrimitive) null else it.asString[0] } +fun JsonObject?.getJsonObject(key: String): JsonObject? = get(key)?.let { if (it.isJsonObject) it.asJsonObject else null } +fun JsonObject?.getJsonArray(key: String): JsonArray? = get(key)?.let { if (it.isJsonArray) it.asJsonArray else null } + +fun JsonObject?.getBoolean(key: String, defaultValue: Boolean): Boolean = get(key)?.let { if (!it.isJsonPrimitive) defaultValue else it.asBoolean } ?: defaultValue +fun JsonObject?.getString(key: String, defaultValue: String): String = get(key)?.let { if (!it.isJsonPrimitive) defaultValue else it.asString } ?: defaultValue +fun JsonObject?.getInt(key: String, defaultValue: Int): Int = get(key)?.let { if (!it.isJsonPrimitive) defaultValue else it.asInt } ?: defaultValue +fun JsonObject?.getLong(key: String, defaultValue: Long): Long = get(key)?.let { if (!it.isJsonPrimitive) defaultValue else it.asLong } ?: defaultValue +fun JsonObject?.getFloat(key: String, defaultValue: Float): Float = get(key)?.let { if(!it.isJsonPrimitive) defaultValue else it.asFloat } ?: defaultValue +fun JsonObject?.getChar(key: String, defaultValue: Char): Char = get(key)?.let { if(!it.isJsonPrimitive) defaultValue else it.asString[0] } ?: defaultValue +fun JsonObject?.getJsonObject(key: String, defaultValue: JsonObject): JsonObject = get(key)?.let { if (it.isJsonObject) it.asJsonObject else defaultValue } ?: defaultValue +fun JsonObject?.getJsonArray(key: String, defaultValue: JsonArray): JsonArray = get(key)?.let { if (it.isJsonArray) it.asJsonArray else defaultValue } ?: defaultValue + +fun JsonArray.getBoolean(key: Int): Boolean? = if (key >= size()) null else get(key)?.let { if (!it.isJsonPrimitive) null else it.asBoolean } +fun JsonArray.getString(key: Int): String? = if (key >= size()) null else get(key)?.let { if (!it.isJsonPrimitive) null else it.asString } +fun JsonArray.getInt(key: Int): Int? = if (key >= size()) null else get(key)?.let { if (!it.isJsonPrimitive) null else it.asInt } +fun JsonArray.getLong(key: Int): Long? = if (key >= size()) null else get(key)?.let { if (!it.isJsonPrimitive) null else it.asLong } +fun JsonArray.getFloat(key: Int): Float? = if (key >= size()) null else get(key)?.let { if(!it.isJsonPrimitive) null else it.asFloat } +fun JsonArray.getChar(key: Int): Char? = if (key >= size()) null else get(key)?.let { if(!it.isJsonPrimitive) null else it.asString[0] } +fun JsonArray.getJsonObject(key: Int): JsonObject? = if (key >= size()) null else get(key)?.let { if (it.isJsonObject) it.asJsonObject else null } +fun JsonArray.getJsonArray(key: Int): JsonArray? = if (key >= size()) null else get(key)?.let { if (it.isJsonArray) it.asJsonArray else null } + +inline fun > JsonObject?.getEnum(key: String) = this?.getInt(key)?.toEnum() +fun JsonObject.putEnum(key: String, value: Enum<*>) = addProperty(key, value.toInt()) + +fun String.toJsonObject(): JsonObject? = try { JsonParser.parseString(this).asJsonObject } catch (ignore: Exception) { null } +fun String.toJsonArray(): JsonArray? = try { JsonParser.parseString(this).asJsonArray } catch (ignore: Exception) { null } + +fun Any?.toJsonElement(): JsonElement = when (this) { + is Collection<*> -> JsonArray(this) + else -> Gson().toJsonTree(this) +} + +operator fun JsonObject.set(key: String, value: JsonElement) = this.add(key, value) +operator fun JsonObject.set(key: String, value: Boolean) = this.addProperty(key, value) +operator fun JsonObject.set(key: String, value: String?) = this.addProperty(key, value) +operator fun JsonObject.set(key: String, value: Number) = this.addProperty(key, value) +operator fun JsonObject.set(key: String, value: Char) = this.addProperty(key, value) + +fun JsonArray.asJsonObjectList() = this.mapNotNull { it.asJsonObject } + +fun JsonObject(vararg properties: Pair): JsonObject { + return JsonObject().apply { + for (property in properties) { + val (key, value) = property + when (value) { + is JsonElement -> add(key, value) + is String -> addProperty(key, value) + is Char -> addProperty(key, value) + is Number -> addProperty(key, value) + is Boolean -> addProperty(key, value) + is Enum<*> -> addProperty(key, value.toInt()) + null -> add(key, null) + else -> add(key, value.toJsonElement()) + } + } + } +} + +fun JsonObject.toBundle(): Bundle { + return Bundle().also { + for ((key, value) in this.entrySet()) { + when (value) { + is JsonObject -> it.putBundle(key, value.toBundle()) + is JsonPrimitive -> when { + value.isString -> it.putString(key, value.asString) + value.isBoolean -> it.putBoolean(key, value.asBoolean) + value.isNumber -> it.putInt(key, value.asInt) + } + } + } + } +} + +fun JsonArray(vararg properties: Any?) = JsonArray(properties.toList()) + +fun JsonArray(properties: Collection): JsonArray { + return JsonArray().apply { + for (property in properties) { + when (property) { + is JsonElement -> add(property as JsonElement?) + is String -> add(property as String?) + is Char -> add(property as Char?) + is Number -> add(property as Number?) + is Boolean -> add(property as Boolean?) + is Enum<*> -> add(property.toInt()) + else -> add(property.toJsonElement()) + } + } + } +} + +@OptIn(ExperimentalContracts::class) +fun JsonArray?.isNullOrEmpty(): Boolean { + contract { + returns(false) implies (this@isNullOrEmpty != null) + } + return this == null || this.isEmpty +} +operator fun JsonArray.plusAssign(o: JsonElement) = this.add(o) +operator fun JsonArray.plusAssign(o: String) = this.add(o) +operator fun JsonArray.plusAssign(o: Char) = this.add(o) +operator fun JsonArray.plusAssign(o: Number) = this.add(o) +operator fun JsonArray.plusAssign(o: Boolean) = this.add(o) + +fun JsonObject.mergeWith(other: JsonObject): JsonObject { + for ((key, value) in other.entrySet()) { + when (value) { + is JsonObject -> when { + this.has(key) -> this.getJsonObject(key)?.mergeWith(value) + else -> this.add(key, value) + } + is JsonArray -> when { + this.has(key) -> this.getJsonArray(key)?.addAll(value) + else -> this.add(key, value) + } + else -> this.add(key, value) + } + } + return this +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/MiscExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/MiscExtensions.kt new file mode 100644 index 00000000..cd2d8a1f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/MiscExtensions.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-17. + */ + +package pl.szczodrzynski.edziennik.ext + +import android.app.PendingIntent +import android.database.Cursor +import android.os.Build +import androidx.core.database.getIntOrNull +import androidx.core.database.getLongOrNull +import androidx.core.database.getStringOrNull +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.io.PrintWriter +import java.io.StringWriter + +fun LiveData.observeOnce(lifecycleOwner: LifecycleOwner, observer: Observer) { + observe(lifecycleOwner, object : Observer { + override fun onChanged(t: T?) { + observer.onChanged(t) + removeObserver(this) + } + }) +} + +fun CoroutineScope.startCoroutineTimer(delayMillis: Long = 0, repeatMillis: Long = 0, action: suspend CoroutineScope.() -> Unit) = launch { + delay(delayMillis) + if (repeatMillis > 0) { + while (true) { + action() + delay(repeatMillis) + } + } else { + action() + } +} + +inline fun Any?.instanceOfOrNull(): T? { + return when (this) { + is T -> this + else -> null + } +} + +val Throwable.stackTraceString: String + get() { + val sw = StringWriter() + printStackTrace(PrintWriter(sw)) + return sw.toString() + } + +fun Cursor?.getString(columnName: String) = this?.getStringOrNull(getColumnIndex(columnName)) +fun Cursor?.getInt(columnName: String) = this?.getIntOrNull(getColumnIndex(columnName)) +fun Cursor?.getLong(columnName: String) = this?.getLongOrNull(getColumnIndex(columnName)) + +inline fun ifNotNull(a: A?, b: B?, code: (A, B) -> R): R? { + if (a != null && b != null) { + return code(a, b) + } + return null +} + +infix fun Int.hasSet(what: Int) = this and what == what + +fun pendingIntentFlag(): Int { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + return PendingIntent.FLAG_IMMUTABLE + 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 + +fun Long?.takeValue() = if (this == -1L) null else this +fun Long?.takePositive() = if (this == -1L || this == 0L) null else this + +fun String?.takeValue() = if (this.isNullOrBlank()) null else this + +fun Any?.ignore() = Unit diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/NetworkExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/NetworkExtensions.kt new file mode 100644 index 00000000..00c0cf91 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/NetworkExtensions.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-17. + */ + +package pl.szczodrzynski.edziennik.ext + +import im.wangchao.mhttp.Response +import okhttp3.RequestBody +import okio.Buffer +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApiException +import pl.szczodrzynski.edziennik.data.api.szkolny.response.ApiResponse +import java.io.InterruptedIOException +import java.net.ConnectException +import java.net.SocketTimeoutException +import java.net.UnknownHostException +import javax.net.ssl.SSLException + +fun RequestBody.bodyToString(): String { + val buffer = Buffer() + writeTo(buffer) + return buffer.readUtf8() +} + +fun Response.toErrorCode() = when (this.code()) { + 400 -> ERROR_REQUEST_HTTP_400 + 401 -> ERROR_REQUEST_HTTP_401 + 403 -> ERROR_REQUEST_HTTP_403 + 404 -> ERROR_REQUEST_HTTP_404 + 405 -> ERROR_REQUEST_HTTP_405 + 410 -> ERROR_REQUEST_HTTP_410 + 424 -> ERROR_REQUEST_HTTP_424 + 500 -> ERROR_REQUEST_HTTP_500 + 503 -> ERROR_REQUEST_HTTP_503 + else -> null +} + +fun Throwable.toErrorCode() = when (this) { + is UnknownHostException -> ERROR_REQUEST_FAILURE_HOSTNAME_NOT_FOUND + is SSLException -> ERROR_REQUEST_FAILURE_SSL_ERROR + is SocketTimeoutException -> ERROR_REQUEST_FAILURE_TIMEOUT + is InterruptedIOException, is ConnectException -> ERROR_REQUEST_FAILURE_NO_INTERNET + is SzkolnyApiException -> this.error?.toErrorCode() + else -> null +} +fun ApiResponse.Error.toErrorCode() = when (this.code) { + "PdoError" -> ERROR_API_PDO_ERROR + "InvalidClient" -> ERROR_API_INVALID_CLIENT + "InvalidArgument" -> ERROR_API_INVALID_ARGUMENT + "InvalidSignature" -> ERROR_API_INVALID_SIGNATURE + "MissingScopes" -> ERROR_API_MISSING_SCOPES + "ResourceNotFound" -> ERROR_API_RESOURCE_NOT_FOUND + "InternalServerError" -> ERROR_API_INTERNAL_SERVER_ERROR + "PhpError" -> ERROR_API_PHP_E_ERROR + "PhpWarning" -> ERROR_API_PHP_E_WARNING + "PhpParse" -> ERROR_API_PHP_E_PARSE + "PhpNotice" -> ERROR_API_PHP_E_NOTICE + "PhpOther" -> ERROR_API_PHP_E_OTHER + "ApiMaintenance" -> ERROR_API_MAINTENANCE + "MissingArgument" -> ERROR_API_MISSING_ARGUMENT + "MissingPayload" -> ERROR_API_PAYLOAD_EMPTY + "InvalidAction" -> ERROR_API_INVALID_ACTION + "VersionNotFound" -> ERROR_API_UPDATE_NOT_FOUND + "InvalidDeviceIdUserCode" -> ERROR_API_INVALID_DEVICEID_USERCODE + "InvalidPairToken" -> ERROR_API_INVALID_PAIRTOKEN + "InvalidBrowserId" -> ERROR_API_INVALID_BROWSERID + "InvalidDeviceId" -> ERROR_API_INVALID_DEVICEID + "InvalidDeviceIdBrowserId" -> ERROR_API_INVALID_DEVICEID_BROWSERID + "HelpCategoryNotFound" -> ERROR_API_HELP_CATEGORY_NOT_FOUND + else -> ERROR_API_EXCEPTION +} +fun Throwable.toApiError(tag: String) = ApiError.fromThrowable(tag, this) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/ProfileExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/ProfileExtensions.kt new file mode 100644 index 00000000..a2ef0289 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/ProfileExtensions.kt @@ -0,0 +1,127 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-25. + */ + +package pl.szczodrzynski.edziennik.ext + +import android.content.Context +import android.graphics.PorterDuff +import android.graphics.PorterDuffColorFilter +import android.graphics.drawable.Drawable +import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory +import com.google.android.material.datepicker.CalendarConstraints +import com.google.gson.JsonElement +import pl.droidsonroids.gif.GifDrawable +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.config.AppData +import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.utils.ProfileImageHolder +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.navlib.ImageHolder +import pl.szczodrzynski.navlib.getDrawableFromRes + +// TODO refactor Data* fields and make the receiver non-nullable +operator fun Profile?.set(key: String, value: JsonElement) = this?.studentData?.add(key, value) +operator fun Profile?.set(key: String, value: Boolean) = this?.studentData?.addProperty(key, value) +operator fun Profile?.set(key: String, value: String?) = this?.studentData?.addProperty(key, value) +operator fun Profile?.set(key: String, value: Number) = this?.studentData?.addProperty(key, value) +operator fun Profile?.set(key: String, value: Char) = this?.studentData?.addProperty(key, value) + +fun Profile.getStudentData(key: String, defaultValue: Boolean) = + studentData.getBoolean(key) ?: defaultValue + +fun Profile.getStudentData(key: String, defaultValue: String?) = + studentData.getString(key) ?: defaultValue + +fun Profile.getStudentData(key: String, defaultValue: Int) = + studentData.getInt(key) ?: defaultValue + +fun Profile.getStudentData(key: String, defaultValue: Long) = + studentData.getLong(key) ?: defaultValue + +fun Profile.getStudentData(key: String, defaultValue: Float) = + studentData.getFloat(key) ?: defaultValue + +fun Profile.getStudentData(key: String, defaultValue: Char) = + studentData.getChar(key) ?: defaultValue + +fun Profile.getSemesterStart(semester: Int) = + if (semester == 1) dateSemester1Start else dateSemester2Start + +fun Profile.getSemesterEnd(semester: Int) = + if (semester == 1) dateSemester2Start.clone().stepForward(0, 0, -1) else dateYearEnd + +fun Profile.dateToSemester(date: Date) = if (date >= dateSemester2Start) 2 else 1 +fun Profile.isBeforeYear() = false && Date.getToday() < dateSemester1Start + +fun Profile.getSchoolYearConstrains(): CalendarConstraints { + return CalendarConstraints.Builder() + .setStart(dateSemester1Start.inMillisUtc) + .setEnd(dateYearEnd.inMillisUtc) + .build() +} + +fun Profile.hasFeature(featureType: FeatureType) = featureType in this.loginStoreType.features +fun Profile.hasUIFeature(featureType: FeatureType) = + featureType.isUIAlwaysAvailable || hasFeature(featureType) + +fun Profile.getAppData() = + if (App.profileId == this.id) App.data else AppData.get(this.loginStoreType) + +fun Profile.shouldArchive(): Boolean { + // vulcan hotfix + if (dateYearEnd.month > 6) { + dateYearEnd.month = 6 + dateYearEnd.day = 30 + } + // fix for when versions <4.3 synced 2020/2021 year dates to older profiles during 2020 Jun-Aug + if (dateSemester1Start.year > studentSchoolYearStart) { + val diff = dateSemester1Start.year - studentSchoolYearStart + dateSemester1Start.year -= diff + dateSemester2Start.year -= diff + dateYearEnd.year -= diff + } + return App.config.archiverEnabled && Date.getToday() >= dateYearEnd && Date.getToday().year > studentSchoolYearStart +} + +fun Profile.getDrawable(context: Context): Drawable { + if (archived) { + return context.getDrawableFromRes(R.drawable.profile_archived).also { + it.colorFilter = PorterDuffColorFilter(colorFromName(name), PorterDuff.Mode.DST_OVER) + } + } + + if (!image.isNullOrEmpty()) { + try { + return if (image?.endsWith(".gif", true) == true) { + GifDrawable(image ?: "") + } else { + RoundedBitmapDrawableFactory.create(context.resources, image ?: "") + } + } catch (e: Exception) { + e.printStackTrace() + } + } + + return context.getDrawableFromRes(R.drawable.profile).also { + it.colorFilter = PorterDuffColorFilter(colorFromName(name), PorterDuff.Mode.DST_OVER) + } +} + +fun Profile.getHolder(): ImageHolder { + if (archived) { + return ImageHolder(R.drawable.profile_archived, colorFromName(name)) + } + + return if (!image.isNullOrEmpty()) { + try { + ProfileImageHolder(image ?: "") + } catch (_: Exception) { + ImageHolder(R.drawable.profile, colorFromName(name)) + } + } else { + ImageHolder(R.drawable.profile, colorFromName(name)) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt new file mode 100644 index 00000000..d4149320 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TextExtensions.kt @@ -0,0 +1,367 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-17. + */ + +package pl.szczodrzynski.edziennik.ext + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.graphics.Typeface +import android.text.Spannable +import android.text.SpannableString +import android.text.SpannableStringBuilder +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 +import java.net.URLDecoder +import java.net.URLEncoder + +fun CharSequence?.isNotNullNorEmpty(): Boolean { + return this != null && this.isNotEmpty() +} + +fun CharSequence?.isNotNullNorBlank(): Boolean { + return this != null && this.isNotBlank() +} + +/** + * ` The quick BROWN_fox Jumps OveR THE LAZy-DOG. ` + * + * converts to + * + * `The Quick Brown_fox Jumps Over The Lazy-Dog.` + */ +fun String?.fixName(): String { + return this?.fixWhiteSpaces()?.toProperCase() ?: "" +} + +/** + * `The quick BROWN_fox Jumps OveR THE LAZy-DOG.` + * + * converts to + * + * `The Quick Brown_fox Jumps Over The Lazy-Dog.` + */ +fun String.toProperCase(): String = changeStringCase(this) + +/** + * `John Smith` -> `Smith John` + * + * `JOHN SMith` -> `SMith JOHN` + */ +fun String.swapFirstLastName(): String { + return this.split(" ").let { + if (it.size > 1) + it[1]+" "+it[0] + else + it[0] + } +} + +fun String.splitName(): Pair? { + return this.split(" ").let { + if (it.size >= 2) Pair(it[0], it[1]) + else null + } +} + +fun changeStringCase(s: String): String { + val delimiters = " '-/" + val sb = StringBuilder() + var capNext = true + for (ch in s.toCharArray()) { + var c = ch + c = if (capNext) + Character.toUpperCase(c) + else + Character.toLowerCase(c) + sb.append(c) + capNext = delimiters.indexOf(c) >= 0 + } + return sb.toString() +} + +fun buildFullName(firstName: String?, lastName: String?): String { + return "$firstName $lastName".fixName() +} + +fun String.getShortName(): String { + return split(" ").let { + if (it.size > 1) + "${it[0]} ${it[1][0]}." + else + it[0] + } +} + +/** + * "John Smith" -> "JS" + * + * "JOHN SMith" -> "JS" + * + * "John" -> "J" + * + * "John " -> "J" + * + * "John Smith " -> "JS" + * + * " " -> "" + * + * " " -> "" + */ +fun String?.getNameInitials(): String { + if (this.isNullOrBlank()) return "" + return this.uppercase().fixWhiteSpaces().split(" ").take(2).map { it[0] }.joinToString("") +} + +operator fun MatchResult.get(group: Int): String { + if (group >= groupValues.size) + return "" + return groupValues[group] +} + +fun String.fixWhiteSpaces() = buildString(length) { + var wasWhiteSpace = true + for (c in this@fixWhiteSpaces) { + if (c.isWhitespace()) { + if (!wasWhiteSpace) { + append(c) + wasWhiteSpace = true + } + } else { + append(c) + wasWhiteSpace = false + } + } +}.trimEnd() + +fun CharSequence?.asColoredSpannable(colorInt: Int): Spannable { + val spannable = SpannableString(this) + spannable.setSpan(ForegroundColorSpan(colorInt), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + return spannable +} +fun CharSequence?.asStrikethroughSpannable(): Spannable { + val spannable = SpannableString(this) + spannable.setSpan(StrikethroughSpan(), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + return spannable +} +fun CharSequence?.asItalicSpannable(): Spannable { + val spannable = SpannableString(this) + spannable.setSpan(StyleSpan(Typeface.ITALIC), 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + return spannable +} +fun CharSequence?.asBoldSpannable(): Spannable { + val spannable = SpannableString(this) + 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, + ignoreCase: Boolean = false, + ignoreDiacritics: Boolean = false +): Spannable { + val spannable = SpannableString(this) + substring?.let { substr -> + val string = if (ignoreDiacritics) + this.cleanDiacritics() + else + this + val search = if (ignoreDiacritics) + substr.cleanDiacritics() + else + substr.toString() + + var index = 0 + do { + index = string.indexOf( + string = search, + startIndex = index, + ignoreCase = ignoreCase + ) + + if (index >= 0) { + spans.forEach { + spannable.setSpan( + CharacterStyle.wrap(it), + index, + index + substring.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + index += substring.length.coerceAtLeast(1) + } + } while (index >= 0) + + } ?: spans.forEach { + spannable.setSpan(it, 0, spannable.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + return spannable +} + +fun CharSequence.cleanDiacritics(): String { + val nameClean = StringBuilder() + forEach { + val ch = when (it) { + 'ż' -> 'z' + 'ó' -> 'o' + 'ł' -> 'l' + 'ć' -> 'c' + 'ę' -> 'e' + 'ś' -> 's' + 'ą' -> 'a' + 'ź' -> 'z' + 'ń' -> 'n' + else -> it + } + nameClean.append(ch) + } + return nameClean.toString() +} + +operator fun StringBuilder.plusAssign(str: String?) { + this.append(str) +} + +val String.firstLettersName: String + get() { + var nameShort = "" + this.split(" ").forEach { + if (it.isBlank()) + return@forEach + nameShort += it[0].lowercase() + } + return nameShort + } + +fun CharSequence.replaceSpanned(oldValue: String, newValue: CharSequence, ignoreCase: Boolean = false): CharSequence { + var seq = this + var index = seq.indexOf(oldValue, ignoreCase = ignoreCase) + while (index != -1) { + val sb = SpannableStringBuilder() + sb.appendRange(seq, 0, index) + sb.append(newValue) + sb.appendRange(seq, index + oldValue.length, seq.length) + seq = sb + index = seq.indexOf(oldValue, startIndex = index + 1, ignoreCase = ignoreCase) + } + return seq +} + +fun SpannableStringBuilder.replaceSpan(spanClass: Class<*>, prefix: CharSequence, suffix: CharSequence): SpannableStringBuilder { + getSpans(0, length, spanClass).forEach { + val spanStart = getSpanStart(it) + insert(spanStart, prefix) + val spanEnd = getSpanEnd(it) + insert(spanEnd, suffix) + } + return this +} + +fun SpannableStringBuilder.appendText(text: CharSequence): SpannableStringBuilder { + append(text) + return this +} +fun SpannableStringBuilder.appendSpan(text: CharSequence, what: Any, flags: Int): SpannableStringBuilder { + val start: Int = length + append(text) + setSpan(what, start, length, flags) + return this +} + +fun joinNotNullStrings(delimiter: String = "", vararg parts: String?): String { + var first = true + val sb = StringBuilder() + for (part in parts) { + if (part == null) + continue + if (!first) + sb += delimiter + first = false + sb += part + } + return sb.toString() +} + +fun String.notEmptyOrNull(): String? { + return if (isEmpty()) + null + else + this +} + +fun Context.plural(@PluralsRes resId: Int, value: Int): String = resources.getQuantityString(resId, value, value) + +fun String.copyToClipboard(context: Context) { + val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clipData = ClipData.newPlainText("Tekst", this) + clipboard.setPrimaryClip(clipData) +} + +fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) } + +fun CharSequence.getWordBounds(position: Int, onlyInWord: Boolean = false): Pair? { + if (length == 0) + return null + + // only if cursor between letters + if (onlyInWord) { + if (position < 1) + return null + if (position == length) + return null + + val charBefore = this[position - 1] + if (!charBefore.isLetterOrDigit()) + return null + val charAfter = this[position] + if (!charAfter.isLetterOrDigit()) + return null + } + + var rangeStart = substring(0 until position).indexOfLast { !it.isLetterOrDigit() } + if (rangeStart == -1) // no whitespace, set to first index + rangeStart = 0 + else // cut the leading whitespace + rangeStart += 1 + + var rangeEnd = substring(position).indexOfFirst { !it.isLetterOrDigit() } + if (rangeEnd == -1) // no whitespace, set to last index + rangeEnd = length + else // append the substring offset + rangeEnd += position + + if (!onlyInWord && rangeStart == rangeEnd) + return null + return rangeStart to rangeEnd +} + +fun Int.toStringHolder() = StringHolder(this) +fun CharSequence.toStringHolder() = StringHolder(this) + +fun @receiver:StringRes Int.resolveString(context: Context) = context.getString(this) + +fun String.urlEncode(): String = URLEncoder.encode(this, "UTF-8").replace("+", "%20") +fun String.urlDecode(): String = URLDecoder.decode(this, "UTF-8") + +fun Map.toQueryString() = this + .map { it.key.urlEncode() to it.value.urlEncode() } + .sortedBy { it.first } + .joinToString("&") { "${it.first}=${it.second}" } + +fun String.fromQueryString() = this + .substringAfter('?') + .split("&") + .map { it.split("=") } + .associate { it[0].urlDecode() to it[1].urlDecode() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/TimeExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TimeExtensions.kt new file mode 100644 index 00000000..31bd952c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/TimeExtensions.kt @@ -0,0 +1,126 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-17. + */ + +package pl.szczodrzynski.edziennik.ext + +import android.content.Context +import im.wangchao.mhttp.Response +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time +import java.text.SimpleDateFormat +import java.util.Locale + +const val MINUTE = 60L +const val HOUR = 60L*MINUTE +const val DAY = 24L*HOUR +const val WEEK = 7L*DAY +const val MONTH = 30L*DAY +const val YEAR = 365L*DAY +const val MS = 1000L + +fun currentTimeUnix() = System.currentTimeMillis() / 1000 + +fun Response?.getUnixDate(): Long { + val rfcDate = this?.headers()?.get("date") ?: return currentTimeUnix() + val pattern = "EEE, dd MMM yyyy HH:mm:ss Z" + val format = SimpleDateFormat(pattern, Locale.ENGLISH) + return (format.parse(rfcDate)?.time ?: 0) / 1000 +} + +fun Long.formatDate(format: String = "yyyy-MM-dd HH:mm:ss"): String = SimpleDateFormat(format).format(this) + +operator fun Time?.compareTo(other: Time?): Int { + if (this == null && other == null) + return 0 + if (this == null) + return -1 + if (other == null) + return 1 + return this.compareTo(other) +} + +fun Context.timeTill(time: Int, delimiter: String = " ", countInSeconds: Boolean = false): String { + val parts = mutableListOf>() + + val hours = time / 3600 + val minutes = (time - hours*3600) / 60 + val seconds = time - minutes*60 - hours*3600 + + if (!countInSeconds) { + var prefixAdded = false + if (hours > 0) { + if (!prefixAdded) parts += R.plurals.time_till_text to hours + prefixAdded = true + parts += R.plurals.time_till_hours to hours + } + if (minutes > 0) { + if (!prefixAdded) parts += R.plurals.time_till_text to minutes + prefixAdded = true + parts += R.plurals.time_till_minutes to minutes + } + if (hours == 0 && minutes < 10) { + if (!prefixAdded) parts += R.plurals.time_till_text to seconds + parts += R.plurals.time_till_seconds to seconds + } + } else { + parts += R.plurals.time_till_text to time + parts += R.plurals.time_till_seconds to time + } + + return parts.joinToString(delimiter) { resources.getQuantityString(it.first, it.second, it.second) } +} + +fun Context.timeLeft(time: Int, delimiter: String = " ", countInSeconds: Boolean = false): String { + val parts = mutableListOf>() + + val hours = time / 3600 + val minutes = (time - hours*3600) / 60 + val seconds = time - minutes*60 - hours*3600 + + if (!countInSeconds) { + var prefixAdded = false + if (hours > 0) { + if (!prefixAdded) parts += R.plurals.time_left_text to hours + prefixAdded = true + parts += R.plurals.time_left_hours to hours + } + if (minutes > 0) { + if (!prefixAdded) parts += R.plurals.time_left_text to minutes + prefixAdded = true + parts += R.plurals.time_left_minutes to minutes + } + if (hours == 0 && minutes < 10) { + if (!prefixAdded) parts += R.plurals.time_left_text to seconds + parts += R.plurals.time_left_seconds to seconds + } + } else { + parts += R.plurals.time_left_text to time + parts += R.plurals.time_left_seconds to time + } + + return parts.joinToString(delimiter) { resources.getQuantityString(it.first, it.second, it.second) } +} + +fun Context.getSyncInterval(interval: Int): String { + val hours = interval / 60 / 60 + val minutes = interval / 60 % 60 + val hoursText = if (hours > 0) + plural(R.plurals.time_till_hours, hours) + else + null + val minutesText = if (minutes > 0) + plural(R.plurals.time_till_minutes, minutes) + else + "" + return hoursText?.plus(" $minutesText") ?: minutesText +} + +fun ClosedRange.asSequence(): Sequence = sequence { + val date = this@asSequence.start.clone() + while (date in this@asSequence) { + yield(date.clone()) + date.stepForward(0, 0, 1) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/ViewExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/ViewExtensions.kt new file mode 100644 index 00000000..ddf49975 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/ViewExtensions.kt @@ -0,0 +1,173 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-17. + */ + +package pl.szczodrzynski.edziennik.ext + +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 +import androidx.recyclerview.widget.RecyclerView +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import androidx.viewpager.widget.ViewPager +import com.google.android.material.button.MaterialButton + +fun TextView.setText(@StringRes resid: Int, vararg formatArgs: Any) { + text = context.getString(resid, *formatArgs) +} + +@Suppress("UNCHECKED_CAST") +inline fun T.onClick(crossinline onClickListener: (v: T) -> Unit) { + setOnClickListener { v: View -> + onClickListener(v as T) + } +} + +@Suppress("UNCHECKED_CAST") +inline fun T.onLongClick(crossinline onLongClickListener: (v: T) -> Boolean) { + setOnLongClickListener { v: View -> + onLongClickListener(v as T) + } +} + +@Suppress("UNCHECKED_CAST") +inline fun T.onChange(crossinline onChangeListener: (v: T, isChecked: Boolean) -> Unit) { + setOnCheckedChangeListener { buttonView, isChecked -> + onChangeListener(buttonView as T, isChecked) + } +} + +@Suppress("UNCHECKED_CAST") +inline fun T.onChange(crossinline onChangeListener: (v: T, isChecked: Boolean) -> Unit) { + clearOnCheckedChangeListeners() + addOnCheckedChangeListener { buttonView, isChecked -> + onChangeListener(buttonView as T, isChecked) + } +} + +fun View.attachToastHint(stringRes: Int) = onLongClick { + Toast.makeText(it.context, stringRes, Toast.LENGTH_SHORT).show() + true +} + +fun View.detachToastHint() = setOnLongClickListener(null) + +/** + * Convert a value in dp to pixels. + */ +val Int.dp: Int + get() = (this * Resources.getSystem().displayMetrics.density).toInt() +/** + * Convert a value in pixels to dp. + */ +val Int.px: Int + get() = (this / Resources.getSystem().displayMetrics.density).toInt() + +fun View.findParentById(targetId: Int): View? { + if (id == targetId) { + return this + } + val viewParent = this.parent ?: return null + if (viewParent is View) { + return viewParent.findParentById(targetId) + } + return null +} + +fun CheckBox.trigger() { isChecked = !isChecked } + +inline fun RadioButton.setOnSelectedListener(crossinline listener: (buttonView: CompoundButton) -> Unit) + = setOnCheckedChangeListener { buttonView, isChecked -> if (isChecked) listener(buttonView) } + +fun TextView.getTextPosition(range: IntRange): Rect { + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + + // Initialize global value + var parentTextViewRect = Rect() + + // Initialize values for the computing of clickedText position + //val completeText = parentTextView.text as SpannableString + val textViewLayout = this.layout + + val startOffsetOfClickedText = range.first//completeText.getSpanStart(clickedText) + val endOffsetOfClickedText = range.last//completeText.getSpanEnd(clickedText) + var startXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal(startOffsetOfClickedText) + var endXCoordinatesOfClickedText = textViewLayout.getPrimaryHorizontal(endOffsetOfClickedText) + + // Get the rectangle of the clicked text + val currentLineStartOffset = textViewLayout.getLineForOffset(startOffsetOfClickedText) + val currentLineEndOffset = textViewLayout.getLineForOffset(endOffsetOfClickedText) + val keywordIsInMultiLine = currentLineStartOffset != currentLineEndOffset + textViewLayout.getLineBounds(currentLineStartOffset, parentTextViewRect) + + // Update the rectangle position to his real position on screen + val parentTextViewLocation = intArrayOf(0, 0) + this.getLocationOnScreen(parentTextViewLocation) + + val parentTextViewTopAndBottomOffset = (parentTextViewLocation[1] - this.scrollY + this.compoundPaddingTop) + parentTextViewRect.top += parentTextViewTopAndBottomOffset + parentTextViewRect.bottom += parentTextViewTopAndBottomOffset + + // In the case of multi line text, we have to choose what rectangle take + if (keywordIsInMultiLine) { + val screenHeight = windowManager.defaultDisplay.height + val dyTop = parentTextViewRect.top + val dyBottom = screenHeight - parentTextViewRect.bottom + val onTop = dyTop > dyBottom + + if (onTop) { + endXCoordinatesOfClickedText = textViewLayout.getLineRight(currentLineStartOffset); + } else { + parentTextViewRect = Rect() + textViewLayout.getLineBounds(currentLineEndOffset, parentTextViewRect); + parentTextViewRect.top += parentTextViewTopAndBottomOffset; + parentTextViewRect.bottom += parentTextViewTopAndBottomOffset; + startXCoordinatesOfClickedText = textViewLayout.getLineLeft(currentLineEndOffset); + } + } + + parentTextViewRect.left += ( + parentTextViewLocation[0] + + startXCoordinatesOfClickedText + + this.compoundPaddingLeft - + this.scrollX + ).toInt() + parentTextViewRect.right = ( + parentTextViewRect.left + + endXCoordinatesOfClickedText - + startXCoordinatesOfClickedText + ).toInt() + + return parentTextViewRect +} + +inline fun ViewPager.addOnPageSelectedListener(crossinline block: (position: Int) -> Unit) = addOnPageChangeListener(object : ViewPager.OnPageChangeListener { + override fun onPageScrollStateChanged(state: Int) {} + override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} + override fun onPageSelected(position: Int) { block(position) } +}) + +val SwipeRefreshLayout.onScrollListener: RecyclerView.OnScrollListener + get() = object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + if (recyclerView.canScrollVertically(-1)) + this@onScrollListener.isEnabled = false + if (!recyclerView.canScrollVertically(-1) && newState == RecyclerView.SCROLL_STATE_IDLE) + this@onScrollListener.isEnabled = true + } + } + +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) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookie.kt b/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookie.kt index 75b60309..ecd513b0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookie.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookie.kt @@ -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() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookieJar.kt b/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookieJar.kt index 39cec6eb..945e16ea 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookieJar.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/network/cookie/DumbCookieJar.kt @@ -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() - private val savedCookies = mutableSetOf() + private val sessionCookies = mutableSetOf() + + init { + val toRemove = mutableListOf() + 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) { + override fun saveFromResponse(url: HttpUrl, cookies: MutableList) { for (cookie in cookies) { val dc = DumbCookie(cookie) save(dc) @@ -54,6 +81,10 @@ class DumbCookieJar( }.map { it.cookie } } + fun getAllDomains(): List { + 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 { 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() + } + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/sync/SyncWorker.kt b/app/src/main/java/pl/szczodrzynski/edziennik/sync/SyncWorker.kt index 05a03fbc..3a2a5f40 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/sync/SyncWorker.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/sync/SyncWorker.kt @@ -5,7 +5,7 @@ import android.content.Context import androidx.work.* import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask -import pl.szczodrzynski.edziennik.formatDate +import pl.szczodrzynski.edziennik.ext.formatDate import pl.szczodrzynski.edziennik.utils.Utils.d import java.util.concurrent.TimeUnit diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateStateEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateStateEvent.kt new file mode 100644 index 00000000..878efdb8 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateStateEvent.kt @@ -0,0 +1,9 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-22. + */ + +package pl.szczodrzynski.edziennik.sync + +import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update + +class UpdateStateEvent(val running: Boolean, val update: Update?, val downloadId: Long) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateWorker.kt b/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateWorker.kt index 80e21e6b..3d22e044 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateWorker.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/sync/UpdateWorker.kt @@ -5,20 +5,13 @@ package pl.szczodrzynski.edziennik.sync import android.annotation.SuppressLint -import android.app.NotificationManager -import android.app.PendingIntent import android.content.Context -import android.content.Intent -import android.text.Html -import android.widget.Toast -import androidx.core.app.NotificationCompat import androidx.work.* import kotlinx.coroutines.* -import org.greenrobot.eventbus.EventBus import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update +import pl.szczodrzynski.edziennik.ext.DAY +import pl.szczodrzynski.edziennik.ext.formatDate import pl.szczodrzynski.edziennik.utils.Utils import java.util.concurrent.TimeUnit import kotlin.coroutines.CoroutineContext @@ -72,63 +65,6 @@ class UpdateWorker(val context: Context, val params: WorkerParameters) : Worker( Utils.d(TAG, "Cancelling work by tag $TAG") WorkManager.getInstance(app).cancelAllWorkByTag(TAG) } - - suspend fun runNow(app: App, overrideUpdate: Update? = null) { - try { - val update = overrideUpdate - ?: run { - withContext(Dispatchers.Default) { - SzkolnyApi(app).runCatching({ - getUpdate("beta") - }, { - Toast.makeText(app, app.getString(R.string.notification_cant_check_update), Toast.LENGTH_SHORT).show() - }) - } ?: return@run null - - if (app.config.update == null - || app.config.update?.versionCode ?: BuildConfig.VERSION_CODE <= BuildConfig.VERSION_CODE) { - app.config.update = null - Toast.makeText(app, app.getString(R.string.notification_no_update), Toast.LENGTH_SHORT).show() - return@run null - } - app.config.update - } ?: return - - if (update.versionCode <= BuildConfig.VERSION_CODE) { - app.config.update = null - return - } - - if (EventBus.getDefault().hasSubscriberForEvent(update::class.java)) { - if (!update.updateMandatory) // mandatory updates are posted by the SzkolnyApi - EventBus.getDefault().postSticky(update) - return - } - - val notificationIntent = Intent(app, UpdateDownloaderService::class.java) - val pendingIntent = PendingIntent.getService(app, 0, notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT) - val notification = NotificationCompat.Builder(app, app.notificationChannelsManager.updates.key) - .setContentTitle(app.getString(R.string.notification_updates_title)) - .setContentText(app.getString(R.string.notification_updates_text, update.versionName)) - .setTicker(app.getString(R.string.notification_updates_summary)) - .setSmallIcon(R.drawable.ic_notification) - .setStyle(NotificationCompat.BigTextStyle() - .bigText(listOf( - app.getString(R.string.notification_updates_text, update.versionName), - update.releaseNotes?.let { Html.fromHtml(it) } - ).concat("\n"))) - .setColor(0xff2196f3.toInt()) - .setLights(0xFF00FFFF.toInt(), 2000, 2000) - .setPriority(NotificationCompat.PRIORITY_HIGH) - .setDefaults(NotificationCompat.DEFAULT_ALL) - .setGroup(app.notificationChannelsManager.updates.key) - .setContentIntent(pendingIntent) - .setAutoCancel(false) - .build() - (app.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(app.notificationChannelsManager.updates.id, notification) - - } catch (ignore: Exception) { } - } } private val job = Job() @@ -142,22 +78,13 @@ class UpdateWorker(val context: Context, val params: WorkerParameters) : Worker( return Result.success() } - launch { - runNow(app) - } + val channel = if (App.devMode) + Update.Type.BETA + else + Update.Type.RELEASE + app.updateManager.checkNowSync(channel, notify = true) rescheduleNext(this.context) return Result.success() } - - class JavaWrapper(app: App) : CoroutineScope { - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - init { - launch { - runNow(app) - } - } - } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/sync/WorkerUtils.kt b/app/src/main/java/pl/szczodrzynski/edziennik/sync/WorkerUtils.kt index 0eaa65eb..e8266a64 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/sync/WorkerUtils.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/sync/WorkerUtils.kt @@ -11,8 +11,8 @@ import androidx.work.WorkManager import androidx.work.impl.WorkManagerImpl import org.greenrobot.eventbus.EventBus import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.MINUTE -import pl.szczodrzynski.edziennik.formatDate +import pl.szczodrzynski.edziennik.ext.MINUTE +import pl.szczodrzynski.edziennik.ext.formatDate import pl.szczodrzynski.edziennik.utils.Utils object WorkerUtils { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/AgendaFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/AgendaFragment.kt new file mode 100644 index 00000000..cbdbce7b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/AgendaFragment.kt @@ -0,0 +1,194 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2020-1-25 + */ + +package pl.szczodrzynski.edziennik.ui.agenda + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.databinding.ViewDataBinding +import androidx.fragment.app.Fragment +import com.applandeo.materialcalendarview.EventDay +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.sizeDp +import eu.szkolny.font.SzkolnyFont +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.EventType +import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.databinding.FragmentAgendaCalendarBinding +import pl.szczodrzynski.edziennik.databinding.FragmentAgendaDefaultBinding +import pl.szczodrzynski.edziennik.ui.dialogs.settings.AgendaConfigDialog +import pl.szczodrzynski.edziennik.ui.event.EventManualDialog +import pl.szczodrzynski.edziennik.utils.Themes +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem +import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem +import kotlin.coroutines.CoroutineContext + +class AgendaFragment : Fragment(), CoroutineScope { + + private lateinit var activity: MainActivity + private lateinit var b: ViewDataBinding + + private val app by lazy { activity.app } + + private var job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + private var type: Int = Profile.AGENDA_DEFAULT + + private var agendaDefault: AgendaFragmentDefault? = null + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + if (getActivity() == null || context == null) return null + activity = getActivity() as MainActivity + context?.theme?.applyStyle(Themes.appTheme, true) + type = app.profile.config.ui.agendaViewType + b = when (type) { + Profile.AGENDA_DEFAULT -> FragmentAgendaDefaultBinding.inflate(inflater, container, false) + Profile.AGENDA_CALENDAR -> FragmentAgendaCalendarBinding.inflate(inflater, container, false) + else -> return null + } + return b.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + if (!isAdded) return + + activity.bottomSheet.prependItems( + BottomSheetPrimaryItem(true) + .withTitle(R.string.menu_add_event) + .withDescription(R.string.menu_add_event_desc) + .withIcon(SzkolnyFont.Icon.szf_calendar_plus_outline) + .withOnClickListener { + activity.bottomSheet.close() + EventManualDialog( + activity, + app.profileId, + defaultDate = AgendaFragmentDefault.selectedDate + ).show() + }, + BottomSheetPrimaryItem(true) + .withTitle(R.string.menu_agenda_config) + .withIcon(CommunityMaterial.Icon.cmd_cog_outline) + .withOnClickListener { + activity.bottomSheet.close() + AgendaConfigDialog(activity, true, null, null).show() + }, + BottomSheetPrimaryItem(true) + .withTitle(R.string.menu_agenda_change_view) + .withIcon(if (type == Profile.AGENDA_DEFAULT) CommunityMaterial.Icon.cmd_calendar_outline else CommunityMaterial.Icon2.cmd_format_list_bulleted_square) + .withOnClickListener { + activity.bottomSheet.close() + type = + if (type == Profile.AGENDA_DEFAULT) Profile.AGENDA_CALENDAR else Profile.AGENDA_DEFAULT + app.profile.config.ui.agendaViewType = type + activity.reloadTarget() + }, + BottomSheetSeparatorItem(true), + BottomSheetPrimaryItem(true) + .withTitle(R.string.menu_mark_as_read) + .withIcon(CommunityMaterial.Icon.cmd_eye_check_outline) + .withOnClickListener { + launch { + activity.bottomSheet.close() + withContext(Dispatchers.Default) { + App.db.metadataDao() + .setAllSeen(app.profileId, MetadataType.EVENT, true) + } + Toast.makeText( + activity, + R.string.main_menu_mark_as_read_success, + Toast.LENGTH_SHORT + ).show() + } + } + ) + + activity.navView.bottomBar.fabEnable = true + activity.navView.bottomBar.fabExtendedText = getString(R.string.add) + activity.navView.bottomBar.fabIcon = CommunityMaterial.Icon3.cmd_plus + activity.navView.setFabOnClickListener { + EventManualDialog( + activity, + app.profileId, + defaultDate = AgendaFragmentDefault.selectedDate + ).show() + } + + activity.gainAttention() + activity.gainAttentionFAB() + + when (type) { + Profile.AGENDA_DEFAULT -> createDefaultAgendaView() + Profile.AGENDA_CALENDAR -> createCalendarAgendaView() + } + } + + private suspend fun checkEventTypes() { + withContext(Dispatchers.Default) { + app.db.eventTypeDao().getAllWithDefaults(app.profile) + } + } + + private fun createDefaultAgendaView() { (b as? FragmentAgendaDefaultBinding)?.let { b -> launch { + if (!isAdded) + return@launch + checkEventTypes() + delay(500) + + agendaDefault = AgendaFragmentDefault(activity, app, b) + agendaDefault?.initView(this@AgendaFragment) + }}} + + private fun createCalendarAgendaView() { (b as? FragmentAgendaCalendarBinding)?.let { b -> launch { + checkEventTypes() + delay(300) + + val dayList = mutableListOf() + + val events = withContext(Dispatchers.Default) { app.db.eventDao().getAllNow(app.profileId) } + val unreadEventDates = mutableSetOf() + + events.forEach { event -> + val eventIcon = IconicsDrawable(activity).apply { + icon = CommunityMaterial.Icon.cmd_checkbox_blank_circle + sizeDp = 10 + colorInt = event.eventColor + } + + dayList.add(EventDay(event.startTimeCalendar, eventIcon)) + + if (!event.seen) unreadEventDates.add(event.date.value) + } + + b.agendaCalendarView.setEvents(dayList) + b.agendaCalendarView.setOnDayClickListener { day -> this@AgendaFragment.launch { + val date = Date.fromCalendar(day.calendar) + + if (date.value in unreadEventDates) { + withContext(Dispatchers.Default) { app.db.eventDao().setSeenByDate(app.profileId, date, true) } + unreadEventDates.remove(date.value) + } + + DayDialog(activity, app.profileId, date).show() + }} + + b.progressBar.visibility = View.GONE + }}} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/AgendaFragmentDefault.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/AgendaFragmentDefault.kt new file mode 100644 index 00000000..f25dfacc --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/AgendaFragmentDefault.kt @@ -0,0 +1,322 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-8. + */ + +package pl.szczodrzynski.edziennik.ui.agenda + +import android.util.SparseIntArray +import android.widget.AbsListView +import android.widget.AbsListView.OnScrollListener +import androidx.core.util.forEach +import androidx.core.util.set +import androidx.core.view.isVisible +import com.github.tibolte.agendacalendarview.CalendarManager +import com.github.tibolte.agendacalendarview.CalendarPickerController +import com.github.tibolte.agendacalendarview.agenda.AgendaAdapter +import com.github.tibolte.agendacalendarview.models.BaseCalendarEvent +import com.github.tibolte.agendacalendarview.models.CalendarEvent +import com.github.tibolte.agendacalendarview.models.IDayItem +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.databinding.FragmentAgendaDefaultBinding +import pl.szczodrzynski.edziennik.ui.agenda.event.AgendaEvent +import pl.szczodrzynski.edziennik.ui.agenda.event.AgendaEventGroup +import pl.szczodrzynski.edziennik.ui.agenda.event.AgendaEventGroupRenderer +import pl.szczodrzynski.edziennik.ui.agenda.event.AgendaEventRenderer +import pl.szczodrzynski.edziennik.ui.agenda.lessonchanges.LessonChangesDialog +import pl.szczodrzynski.edziennik.ui.agenda.lessonchanges.LessonChangesEvent +import pl.szczodrzynski.edziennik.ui.agenda.lessonchanges.LessonChangesEventRenderer +import pl.szczodrzynski.edziennik.ui.agenda.teacherabsence.TeacherAbsenceDialog +import pl.szczodrzynski.edziennik.ui.agenda.teacherabsence.TeacherAbsenceEvent +import pl.szczodrzynski.edziennik.ui.agenda.teacherabsence.TeacherAbsenceEventRenderer +import pl.szczodrzynski.edziennik.ui.event.EventDetailsDialog +import pl.szczodrzynski.edziennik.utils.models.Date +import java.util.* + +class AgendaFragmentDefault( + private val activity: MainActivity, + private val app: App, + private val b: FragmentAgendaDefaultBinding +) : OnScrollListener, CoroutineScope { + companion object { + var selectedDate: Date = Date.getToday() + } + + override val coroutineContext = Job() + Dispatchers.Main + + private val unreadDates = mutableSetOf() + private val events = mutableListOf() + private var isInitialized = false + + private val listView + get() = b.agendaDefaultView.agendaView.agendaListView + private val adapter + get() = listView.adapter as? AgendaAdapter + private val manager + get() = CalendarManager.getInstance() + + private var scrollState = OnScrollListener.SCROLL_STATE_IDLE + private var updatePending = false + private var notifyPending = false + override fun onScrollStateChanged(view: AbsListView?, newScrollState: Int) { + b.agendaDefaultView.agendaScrollListener.onScrollStateChanged(view, scrollState) + scrollState = newScrollState + if (updatePending) updateData() + if (notifyPending) notifyDataSetChanged() + } + + override fun onScroll( + view: AbsListView?, + firstVisibleItem: Int, + visibleItemCount: Int, + totalItemCount: Int + ) = b.agendaDefaultView.agendaScrollListener.onScroll( + view, + firstVisibleItem, + visibleItemCount, + totalItemCount + ) + + /** + * Mark the data as needing update, either after 1 second (when + * not scrolling) or 1 second after scrolling stops. + */ + private fun updateData() = launch { + if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) { + updatePending = false + delay(1000) + notifyDataSetChanged() + } else updatePending = true + } + + /** + * Notify the adapter about changes, either instantly or after + * scrolling stops. + */ + private fun notifyDataSetChanged() { + if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) { + notifyPending = false + adapter?.notifyDataSetChanged() + } else notifyPending = true + } + + suspend fun initView(fragment: AgendaFragment) { + isInitialized = false + + withContext(Dispatchers.Default) { + if (app.profile.config.ui.agendaLessonChanges) + addLessonChanges(events) + + if (app.profile.config.ui.agendaTeacherAbsence) + addTeacherAbsence(events) + } + + app.db.eventDao().getAll(app.profileId).observe(fragment) { + addEvents(events, it) + if (isInitialized) + updateView() + else + initViewPriv() + } + } + + private fun initViewPriv() { + val dateStart = app.profile.dateSemester1Start.asCalendar + val dateEnd = app.profile.dateYearEnd.asCalendar + + val isCompactMode = app.profile.config.ui.agendaCompactMode + + b.agendaDefaultView.init( + events, + dateStart, + dateEnd, + Locale.getDefault(), + object : CalendarPickerController { + override fun onDaySelected(dayItem: IDayItem) { + val c = Calendar.getInstance() + c.time = dayItem.date + if (c.timeInMillis == selectedDate.inMillis) { + DayDialog(activity, app.profileId, selectedDate).show() + } + } + + override fun onEventSelected(event: CalendarEvent) { + val date = Date.fromCalendar(event.instanceDay) + + when (event) { + is AgendaEvent -> EventDetailsDialog(activity, event.event).show() + is LessonChangesEvent -> LessonChangesDialog( + activity = activity, + profileId = app.profileId, + defaultDate = date + ).show() + is TeacherAbsenceEvent -> TeacherAbsenceDialog( + activity = activity, + profileId = app.profileId, + date = date + ).show() + is AgendaEventGroup -> DayDialog( + activity = activity, + profileId = app.profileId, + date = date, + eventTypeId = event.typeId, + ).show() + is BaseCalendarEvent -> if (event.isPlaceHolder) + DayDialog(activity, app.profileId, date).show() + } + + if (event is BaseEvent && event.showItemBadge) { + val unreadCount = manager.events.count { + it.instanceDay.equals(event.instanceDay) && it.showBadge + } + // only clicked event is unread, remove the day badge + if (unreadCount == 1 && event.showBadge) { + event.dayReference.showBadge = false + unreadDates.remove(date.value) + } + setAsRead(event) + } + } + + override fun onScrollToDate(calendar: Calendar) { + selectedDate = Date.fromCalendar(calendar) + + // Mark as read scrolled date + if (selectedDate.value in unreadDates) { + setAsRead(calendar) + activity.launch(Dispatchers.Default) { + app.db.eventDao().setSeenByDate(app.profileId, selectedDate, true) + } + unreadDates.remove(selectedDate.value) + } + } + }, + AgendaEventRenderer(app.eventManager, isCompactMode), + AgendaEventGroupRenderer(), + LessonChangesEventRenderer(), + TeacherAbsenceEventRenderer() + ) + + listView.setOnScrollListener(this) + + isInitialized = true + b.progressBar.isVisible = false + } + + private fun updateView() { + manager.events.clear() + manager.loadEvents(events, BaseCalendarEvent()) + + adapter?.updateEvents(manager.events) + //listView.scrollToCurrentDate(selectedDate.asCalendar) + } + + private fun setAsRead(date: Calendar) { + // get all events matching the date + val events = manager.events.filter { + if (it.instanceDay.equals(date) && it.showBadge && it is AgendaEvent) { + // hide the day badge for the date + it.dayReference.showBadge = false + return@filter true + } + false + } + // set this date's events as read + setAsRead(*events.toTypedArray()) + } + + private fun setAsRead(vararg event: CalendarEvent) { + // hide per-event badges + for (e in event) { + events.firstOrNull { + it == e + }?.showBadge = false + e.showBadge = false + } + + listView.setOnScrollListener(this) + updateData() + } + + private fun addEvents( + events: MutableList, + eventList: List + ) { + events.removeAll { it is AgendaEvent || it is AgendaEventGroup } + + if (!app.profile.config.ui.agendaGroupByType) { + events += eventList.map { + if (!it.seen) + unreadDates.add(it.date.value) + AgendaEvent(it) + } + return + } + + eventList.groupBy { + it.date.value to it.type + }.forEach { (_, list) -> + val event = list.first() + if (list.size == 1) { + if (!event.seen) + unreadDates.add(event.date.value) + events += AgendaEvent(event) + } else { + events.add(0, AgendaEventGroup( + profileId = event.profileId, + date = event.date, + typeId = event.type, + typeName = event.typeName ?: "-", + typeColor = event.typeColor ?: event.eventColor, + count = list.size, + showBadge = list.any { !it.seen } + )) + } + } + } + + private fun addLessonChanges(events: MutableList) { + val lessons = app.db.timetableDao().getChangesNow(app.profileId) + + val grouped = lessons.groupBy { + it.displayDate + } + + events += grouped.mapNotNull { (date, changes) -> + LessonChangesEvent( + app.profileId, + date = date ?: return@mapNotNull null, + count = changes.size, + showBadge = changes.any { !it.seen } + ) + } + } + + private fun addTeacherAbsence(events: MutableList) { + val teacherAbsence = app.db.teacherAbsenceDao().getAllNow(app.profileId) + + val countMap = SparseIntArray() + + for (absence in teacherAbsence) { + while (absence.dateFrom <= absence.dateTo) { + countMap[absence.dateFrom.value] += 1 + absence.dateFrom.stepForward(0, 0, 1) + } + } + + countMap.forEach { dateInt, count -> + events += TeacherAbsenceEvent( + app.profileId, + date = Date.fromValue(dateInt), + count = count + ) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/BaseEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/BaseEvent.kt new file mode 100644 index 00000000..3f28ca40 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/BaseEvent.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-9. + */ + +package pl.szczodrzynski.edziennik.ui.agenda + +import com.github.tibolte.agendacalendarview.models.CalendarEvent +import com.github.tibolte.agendacalendarview.models.IDayItem +import com.github.tibolte.agendacalendarview.models.IWeekItem +import java.util.* + +open class BaseEvent( + private val id: Long, + private val time: Calendar, + private val color: Int, + private var showBadge: Boolean, + var showItemBadge: Boolean = showBadge +) : CalendarEvent { + + override fun copy() = BaseEvent(id, time, color, showBadge) + + private lateinit var date: Calendar + override fun getInstanceDay() = date + override fun setInstanceDay(value: Calendar) { + date = value + } + + private lateinit var dayReference: IDayItem + override fun getDayReference() = dayReference + override fun setDayReference(value: IDayItem) { + dayReference = value + } + + private lateinit var weekReference: IWeekItem + override fun getWeekReference() = weekReference + override fun setWeekReference(value: IWeekItem) { + weekReference = value + } + + override fun getShowBadge() = showBadge + override fun setShowBadge(value: Boolean) { + showBadge = value + showItemBadge = value + } + + override fun getId() = id + override fun getStartTime() = time + override fun getEndTime() = time + override fun getTitle() = "" + override fun getDescription() = "" + override fun getLocation() = "" + override fun getColor() = color + override fun getTextColor() = 0 + override fun isPlaceholder() = false + override fun isAllDay() = false + + override fun setId(value: Long) = Unit + override fun setStartTime(value: Calendar) = Unit + override fun setEndTime(value: Calendar) = Unit + override fun setTitle(value: String) = Unit + override fun setDescription(value: String) = Unit + override fun setLocation(value: String) = Unit + override fun setTextColor(value: Int) = Unit + override fun setPlaceholder(value: Boolean) = Unit + override fun setAllDay(value: Boolean) = Unit +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/DayDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/DayDialog.kt new file mode 100644 index 00000000..cacd51a1 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/DayDialog.kt @@ -0,0 +1,219 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-12-16. + */ + +package pl.szczodrzynski.edziennik.ui.agenda + +import android.view.LayoutInflater +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager +import kotlinx.coroutines.* +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.db.entity.Lesson +import pl.szczodrzynski.edziennik.databinding.DialogDayBinding +import pl.szczodrzynski.edziennik.ext.ifNotEmpty +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ext.setText +import pl.szczodrzynski.edziennik.ui.agenda.lessonchanges.LessonChangesDialog +import pl.szczodrzynski.edziennik.ui.agenda.lessonchanges.LessonChangesEvent +import pl.szczodrzynski.edziennik.ui.agenda.lessonchanges.LessonChangesEventRenderer +import pl.szczodrzynski.edziennik.ui.agenda.teacherabsence.TeacherAbsenceDialog +import pl.szczodrzynski.edziennik.ui.agenda.teacherabsence.TeacherAbsenceEvent +import pl.szczodrzynski.edziennik.ui.agenda.teacherabsence.TeacherAbsenceEventRenderer +import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog +import pl.szczodrzynski.edziennik.ui.event.EventDetailsDialog +import pl.szczodrzynski.edziennik.ui.event.EventListAdapter +import pl.szczodrzynski.edziennik.ui.event.EventManualDialog +import pl.szczodrzynski.edziennik.ui.notes.setupNotesButton +import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration +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 + +class DayDialog( + activity: AppCompatActivity, + private val profileId: Int, + private val date: Date, + private val eventTypeId: Long? = null, + private val showNotes: Boolean = false, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BindingDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "DayDialog" + + override fun getTitleRes(): Int? = null + override fun inflate(layoutInflater: LayoutInflater) = + DialogDayBinding.inflate(layoutInflater) + + override fun getPositiveButtonText() = R.string.close + override fun getNeutralButtonText() = R.string.add + + private lateinit var adapter: EventListAdapter + + override suspend fun onNeutralClick(): Boolean { + EventManualDialog( + activity, + profileId, + defaultDate = date, + onShowListener = onShowListener, + onDismissListener = onDismissListener + ).show() + return NO_DISMISS + } + + override suspend fun onShow() { + b.dayDate.setText( + R.string.dialog_day_date_format, + Week.getFullDayName(date.weekDay), + date.formattedString + ) + + val lessons = withContext(Dispatchers.Default) { + app.db.timetableDao().getAllForDateNow(profileId, date) + }.filter { it.type != Lesson.TYPE_NO_LESSONS } + + if (lessons.isNotEmpty()) { + run { + val startTime = lessons.first().startTime ?: return@run + val endTime = lessons.last().endTime ?: return@run + val diff = Time.diff(startTime, endTime) + + b.lessonsInfo.setText( + R.string.dialog_day_lessons_info, + startTime.stringHM, + endTime.stringHM, + lessons.size.toString(), + diff.hour.toString(), + diff.minute.toString() + ) + + b.lessonsInfo.visibility = View.VISIBLE + } + } + + val lessonChanges = withContext(Dispatchers.Default) { + app.db.timetableDao().getChangesForDateNow(profileId, date) + } + + lessonChanges.ifNotEmpty { + LessonChangesEventRenderer().render( + b.lessonChanges, LessonChangesEvent( + profileId = profileId, + date = date, + count = it.size, + showBadge = false + ) + ) + + b.lessonChangesFrame.onClick { + LessonChangesDialog( + activity, + profileId, + date, + onShowListener = onShowListener, + onDismissListener = onDismissListener + ).show() + } + } + b.lessonChangesFrame.isVisible = lessonChanges.isNotEmpty() + + val teacherAbsences = withContext(Dispatchers.Default) { + app.db.teacherAbsenceDao().getAllByDateNow(profileId, date) + } + + teacherAbsences.ifNotEmpty { + TeacherAbsenceEventRenderer().render( + b.teacherAbsence, TeacherAbsenceEvent( + profileId = profileId, + date = date, + count = it.size + ) + ) + + b.teacherAbsenceFrame.onClick { + TeacherAbsenceDialog( + activity, + profileId, + date, + onShowListener = onShowListener, + onDismissListener = onDismissListener + ).show() + } + } + b.teacherAbsenceFrame.isVisible = teacherAbsences.isNotEmpty() + + adapter = EventListAdapter( + activity = activity, + showWeekDay = false, + showDate = false, + showType = true, + showTime = true, + showSubject = true, + markAsSeen = true, + onEventClick = { + EventDetailsDialog( + activity, + it, + onShowListener = onShowListener, + onDismissListener = onDismissListener + ).show() + }, + onEventEditClick = { + EventManualDialog( + activity, + it.profileId, + editingEvent = it, + onShowListener = onShowListener, + onDismissListener = onDismissListener + ).show() + } + ) + + app.db.eventDao().getAllByDate(profileId, date).observe(activity) { events -> + events.forEach { + it.filterNotes() + } + + adapter.setAllItems( + if (eventTypeId != null) + events.filter { it.type == eventTypeId } + else + events, + ) + + if (b.eventsView.adapter == null) { + b.eventsView.adapter = adapter + b.eventsView.apply { + isNestedScrollingEnabled = false + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + addItemDecoration(SimpleDividerItemDecoration(context)) + } + } + adapter.notifyDataSetChanged() + + if (events != null && events.isNotEmpty()) { + b.eventsView.visibility = View.VISIBLE + b.eventsNoData.visibility = View.GONE + } else { + b.eventsView.visibility = View.GONE + b.eventsNoData.visibility = View.VISIBLE + } + } + + b.notesButton.isVisible = showNotes + b.notesButton.setupNotesButton( + activity = activity, + owner = date, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ) + b.legend.isVisible = showNotes + if (showNotes) + NoteManager.setLegendText(date, b.legend) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/event/AgendaEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/event/AgendaEvent.kt new file mode 100644 index 00000000..ebff08f9 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/event/AgendaEvent.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-8. + */ + +package pl.szczodrzynski.edziennik.ui.agenda.event + +import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.ui.agenda.BaseEvent + +class AgendaEvent( + val event: EventFull, + showBadge: Boolean = !event.seen +) : BaseEvent( + id = event.id, + time = event.startTimeCalendar, + color = event.eventColor, + showBadge = showBadge +) { + override fun copy() = AgendaEvent(event, showBadge) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/event/AgendaEventGroup.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/event/AgendaEventGroup.kt new file mode 100644 index 00000000..cccfb292 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/event/AgendaEventGroup.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-10. + */ + +package pl.szczodrzynski.edziennik.ui.agenda.event + +import pl.szczodrzynski.edziennik.ui.agenda.BaseEvent +import pl.szczodrzynski.edziennik.utils.models.Date + +class AgendaEventGroup( + val profileId: Int, + val date: Date, + val typeId: Long, + val typeName: String, + val typeColor: Int, + val count: Int, + showBadge: Boolean +) : BaseEvent( + id = date.value.toLong(), + time = date.asCalendar, + color = typeColor, + showBadge = showBadge +) { + override fun copy() = AgendaEventGroup(profileId, date, typeId, typeName, typeColor, count, showBadge) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/event/AgendaEventGroupRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/event/AgendaEventGroupRenderer.kt new file mode 100644 index 00000000..59d15cbe --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/event/AgendaEventGroupRenderer.kt @@ -0,0 +1,33 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-10. + */ + +package pl.szczodrzynski.edziennik.ui.agenda.event + +import android.view.View +import androidx.core.view.isVisible +import com.github.tibolte.agendacalendarview.render.EventRenderer +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.AgendaWrappedGroupBinding +import pl.szczodrzynski.edziennik.ext.resolveAttr +import pl.szczodrzynski.edziennik.ext.setTintColor +import pl.szczodrzynski.edziennik.utils.Colors + +class AgendaEventGroupRenderer : EventRenderer() { + + override fun render(view: View, event: AgendaEventGroup) { + val b = AgendaWrappedGroupBinding.bind(view).item + + b.card.foreground.setTintColor(event.color) + b.card.background.setTintColor(event.color) + b.name.text = event.typeName + b.name.setTextColor(Colors.legibleTextColor(event.color)) + b.count.text = event.count.toString() + b.count.background.setTintColor(android.R.attr.colorBackground.resolveAttr(view.context)) + + b.badge.isVisible = event.showItemBadge + } + + override fun getEventLayout(): Int = R.layout.agenda_wrapped_group +} + diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/event/AgendaEventRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/event/AgendaEventRenderer.kt new file mode 100644 index 00000000..a1b9ead4 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/event/AgendaEventRenderer.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-8. + */ + +package pl.szczodrzynski.edziennik.ui.agenda.event + +import android.annotation.SuppressLint +import android.view.View +import android.widget.FrameLayout +import android.widget.TextView +import androidx.core.view.isVisible +import com.github.tibolte.agendacalendarview.render.EventRenderer +import com.mikepenz.iconics.view.IconicsTextView +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.AgendaWrappedEventBinding +import pl.szczodrzynski.edziennik.databinding.AgendaWrappedEventCompactBinding +import pl.szczodrzynski.edziennik.ext.join +import pl.szczodrzynski.edziennik.ext.resolveAttr +import pl.szczodrzynski.edziennik.ext.setTintColor +import pl.szczodrzynski.edziennik.utils.Colors +import pl.szczodrzynski.edziennik.utils.managers.EventManager + +class AgendaEventRenderer( + val manager: EventManager, + val isCompact: Boolean +) : EventRenderer() { + + @SuppressLint("SetTextI18n") + override fun render(view: View, aEvent: AgendaEvent) { + if (isCompact) { + val b = AgendaWrappedEventCompactBinding.bind(view).item + bindView(aEvent, b.card, b.title, null, b.badgeBackground, b.badge) + } else { + val b = AgendaWrappedEventBinding.bind(view).item + bindView(aEvent, b.card, b.title, b.subtitle, b.badgeBackground, b.badge) + } + } + + private fun bindView( + aEvent: AgendaEvent, + card: FrameLayout, + title: IconicsTextView, + subtitle: TextView?, + badgeBackground: View, + badge: View + ) { + val event = aEvent.event + + val textColor = Colors.legibleTextColor(event.eventColor) + + val timeText = if (event.time == null) + card.context.getString(R.string.agenda_event_all_day) + else + event.time!!.stringHM + + val agendaSubjectImportant = App.profile.config.ui.agendaSubjectImportant + val eventSubtitle = listOfNotNull( + timeText, + event.subjectLongName.takeIf { !agendaSubjectImportant }, + event.typeName.takeIf { agendaSubjectImportant }, + event.teacherName, + event.teamName + ).join(", ") + + card.foreground.setTintColor(event.eventColor) + card.background.setTintColor(event.eventColor) + manager.setEventTopic( + title = title, + event = event, + doneIconColor = textColor, + showType = !agendaSubjectImportant, + showSubject = agendaSubjectImportant, + ) + title.setTextColor(textColor) + subtitle?.text = eventSubtitle + subtitle?.setTextColor(textColor) + + badgeBackground.isVisible = aEvent.showItemBadge + badgeBackground.background.setTintColor( + android.R.attr.colorBackground.resolveAttr(card.context) + ) + badge.isVisible = aEvent.showItemBadge + } + + override fun getEventLayout() = if (isCompact) + R.layout.agenda_wrapped_event_compact + else + R.layout.agenda_wrapped_event +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/lessonchange/LessonChangeAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/lessonchanges/LessonChangesAdapter.kt similarity index 91% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/lessonchange/LessonChangeAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/lessonchanges/LessonChangesAdapter.kt index b15803ed..cf6b0cae 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/lessonchange/LessonChangeAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/lessonchanges/LessonChangesAdapter.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2019-12-19. */ -package pl.szczodrzynski.edziennik.ui.dialogs.lessonchange +package pl.szczodrzynski.edziennik.ui.agenda.lessonchanges import android.content.Context import android.graphics.PorterDuff @@ -11,16 +11,19 @@ import android.view.LayoutInflater import android.view.ViewGroup import androidx.core.view.updateLayoutParams import androidx.recyclerview.widget.RecyclerView -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.Lesson 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 LessonChangeAdapter( +class LessonChangesAdapter( val context: Context, - private val onItemClick: ((lesson: LessonFull) -> Unit)? = null -) : RecyclerView.Adapter() { + private val showNotes: Boolean = true, + private val onLessonClick: ((lesson: LessonFull) -> Unit)? = null +) : RecyclerView.Adapter() { var items = listOf() @@ -38,8 +41,10 @@ class LessonChangeAdapter( val lesson = items[position] val b = holder.b - b.root.onClick { - onItemClick?.invoke(lesson) + if (onLessonClick != null) { + b.root.onClick { + onLessonClick.invoke(lesson) + } } val startTime = lesson.displayStartTime ?: return @@ -83,7 +88,8 @@ class LessonChangeAdapter( b.lessonNumber = lesson.displayLessonNumber - b.subjectName.text = lesson.displaySubjectName?.let { + val lessonText = lesson.getNoteSubstituteText(showNotes) ?: lesson.displaySubjectName + b.subjectName.text = lessonText?.let { if (lesson.type == Lesson.TYPE_CANCELLED || lesson.type == Lesson.TYPE_SHIFTED_SOURCE) it.asStrikethroughSpannable().asColoredSpannable(colorSecondary) else @@ -92,6 +98,9 @@ class LessonChangeAdapter( b.detailsFirst.text = listOfNotEmpty(timeRange, classroomInfo).concat(bullet) b.detailsSecond.text = listOfNotEmpty(teacherInfo, teamInfo).concat(bullet) + if (showNotes) + NoteManager.prependIcon(lesson, b.subjectName) + //lb.subjectName.typeface = Typeface.create("sans-serif-light", Typeface.BOLD) when (lesson.type) { Lesson.TYPE_NORMAL -> { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/lessonchanges/LessonChangesDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/lessonchanges/LessonChangesDialog.kt new file mode 100644 index 00000000..50e972a6 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/lessonchanges/LessonChangesDialog.kt @@ -0,0 +1,53 @@ +package pl.szczodrzynski.edziennik.ui.agenda.lessonchanges + +import android.view.LayoutInflater +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.DialogLessonChangeListBinding +import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog +import pl.szczodrzynski.edziennik.ui.timetable.LessonDetailsDialog +import pl.szczodrzynski.edziennik.utils.models.Date + +class LessonChangesDialog( + activity: AppCompatActivity, + private val profileId: Int, + private val defaultDate: Date, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BindingDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "LessonChangesDialog" + + override fun getTitle(): String = defaultDate.formattedString + override fun getTitleRes(): Int? = null + override fun inflate(layoutInflater: LayoutInflater) = + DialogLessonChangeListBinding.inflate(layoutInflater) + + override fun getPositiveButtonText() = R.string.close + + override suspend fun onShow() { + val lessonChanges = withContext(Dispatchers.Default) { + app.db.timetableDao().getChangesForDateNow(profileId, defaultDate) + } + + val adapter = LessonChangesAdapter( + activity, + onLessonClick = { + LessonDetailsDialog( + activity, + it, + onShowListener = onShowListener, + onDismissListener = onDismissListener + ).show() + } + ).apply { + items = lessonChanges + } + + b.lessonChangeView.adapter = adapter + b.lessonChangeView.layoutManager = LinearLayoutManager(activity) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/lessonchanges/LessonChangesEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/lessonchanges/LessonChangesEvent.kt new file mode 100644 index 00000000..c011af59 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/lessonchanges/LessonChangesEvent.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-8. + */ + +package pl.szczodrzynski.edziennik.ui.agenda.lessonchanges + +import pl.szczodrzynski.edziennik.ui.agenda.BaseEvent +import pl.szczodrzynski.edziennik.utils.models.Date + +class LessonChangesEvent( + val profileId: Int, + val date: Date, + val count: Int, + showBadge: Boolean +) : BaseEvent( + id = date.value.toLong(), + time = date.asCalendar, + color = 0xff78909c.toInt(), + showBadge = false, + showItemBadge = showBadge +) { + override fun copy() = LessonChangesEvent(profileId, date, count, showItemBadge) + + override fun getShowBadge() = false +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/lessonchanges/LessonChangesEventRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/lessonchanges/LessonChangesEventRenderer.kt new file mode 100644 index 00000000..2ef4aecc --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/lessonchanges/LessonChangesEventRenderer.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-8. + */ + +package pl.szczodrzynski.edziennik.ui.agenda.lessonchanges + +import android.view.View +import androidx.core.view.isVisible +import com.github.tibolte.agendacalendarview.render.EventRenderer +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.AgendaCounterItemBinding +import pl.szczodrzynski.edziennik.databinding.AgendaWrappedCounterBinding +import pl.szczodrzynski.edziennik.ext.resolveAttr +import pl.szczodrzynski.edziennik.ext.setTintColor +import pl.szczodrzynski.edziennik.utils.Colors + +class LessonChangesEventRenderer : EventRenderer() { + + override fun render(view: View, event: LessonChangesEvent) { + val b = AgendaWrappedCounterBinding.bind(view).item + val textColor = Colors.legibleTextColor(event.color) + + b.card.foreground.setTintColor(event.color) + b.card.background.setTintColor(event.color) + b.name.setText(R.string.agenda_lesson_changes) + b.name.setTextColor(textColor) + b.count.text = event.count.toString() + b.count.setTextColor(textColor) + + b.badgeBackground.isVisible = event.showItemBadge + b.badgeBackground.background.setTintColor( + android.R.attr.colorBackground.resolveAttr(view.context) + ) + b.badge.isVisible = event.showItemBadge + } + + fun render(b: AgendaCounterItemBinding, event: LessonChangesEvent) { + val textColor = Colors.legibleTextColor(event.color) + + b.card.foreground.setTintColor(event.color) + b.card.background.setTintColor(event.color) + b.name.setText(R.string.agenda_lesson_changes) + b.name.setTextColor(textColor) + b.count.text = event.count.toString() + b.count.setTextColor(textColor) + + b.badgeBackground.isVisible = event.showItemBadge + b.badgeBackground.background.setTintColor( + android.R.attr.colorBackground.resolveAttr(b.root.context) + ) + b.badge.isVisible = event.showItemBadge + } + + override fun getEventLayout(): Int = R.layout.agenda_wrapped_counter +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/teacherabsence/TeacherAbsenceAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/teacherabsence/TeacherAbsenceAdapter.kt similarity index 97% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/teacherabsence/TeacherAbsenceAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/teacherabsence/TeacherAbsenceAdapter.kt index 443d4734..89cbf3ee 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/teacherabsence/TeacherAbsenceAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/teacherabsence/TeacherAbsenceAdapter.kt @@ -1,4 +1,4 @@ -package pl.szczodrzynski.edziennik.ui.dialogs.teacherabsence +package pl.szczodrzynski.edziennik.ui.agenda.teacherabsence import android.content.Context import android.view.LayoutInflater diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/teacherabsence/TeacherAbsenceDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/teacherabsence/TeacherAbsenceDialog.kt new file mode 100644 index 00000000..7a1071de --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/teacherabsence/TeacherAbsenceDialog.kt @@ -0,0 +1,42 @@ +package pl.szczodrzynski.edziennik.ui.agenda.teacherabsence + +import android.view.LayoutInflater +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.LifecycleOwner +import androidx.recyclerview.widget.LinearLayoutManager +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.DialogTeacherAbsenceListBinding +import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog +import pl.szczodrzynski.edziennik.utils.models.Date + +class TeacherAbsenceDialog( + activity: AppCompatActivity, + private val profileId: Int, + private val date: Date, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BindingDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "TeacherAbsenceDialog" + + override fun getTitle(): String = date.formattedString + override fun getTitleRes(): Int? = null + override fun inflate(layoutInflater: LayoutInflater) = + DialogTeacherAbsenceListBinding.inflate(layoutInflater) + + override fun getPositiveButtonText() = R.string.close + + override suspend fun onShow() { + b.teacherAbsenceView.setHasFixedSize(true) + b.teacherAbsenceView.layoutManager = LinearLayoutManager(activity) + + app.db.teacherAbsenceDao().getAllByDate(profileId, date).observe( + activity as LifecycleOwner + ) { absenceList -> + val adapter = TeacherAbsenceAdapter(activity, date, absenceList) + b.teacherAbsenceView.adapter = adapter + b.teacherAbsenceView.visibility = View.VISIBLE + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/teacherabsence/TeacherAbsenceEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/teacherabsence/TeacherAbsenceEvent.kt new file mode 100644 index 00000000..d9defda1 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/teacherabsence/TeacherAbsenceEvent.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-8. + */ + +package pl.szczodrzynski.edziennik.ui.agenda.teacherabsence + +import pl.szczodrzynski.edziennik.ui.agenda.BaseEvent +import pl.szczodrzynski.edziennik.utils.models.Date + +class TeacherAbsenceEvent( + val profileId: Int, + val date: Date, + val count: Int +) : BaseEvent( + id = date.value.toLong(), + time = date.asCalendar, + color = 0xffff1744.toInt(), + showBadge = false +) { + override fun copy() = TeacherAbsenceEvent(profileId, date, count) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt new file mode 100644 index 00000000..7d87acfa --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-8. + */ + +package pl.szczodrzynski.edziennik.ui.agenda.teacherabsence + +import android.view.View +import androidx.core.view.isVisible +import com.github.tibolte.agendacalendarview.render.EventRenderer +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.AgendaCounterItemBinding +import pl.szczodrzynski.edziennik.databinding.AgendaWrappedCounterBinding +import pl.szczodrzynski.edziennik.ext.setTintColor +import pl.szczodrzynski.edziennik.utils.Colors + +class TeacherAbsenceEventRenderer : EventRenderer() { + + override fun render(view: View, event: TeacherAbsenceEvent) { + val b = AgendaWrappedCounterBinding.bind(view).item + val textColor = Colors.legibleTextColor(event.color) + + b.card.foreground.setTintColor(event.color) + b.card.background.setTintColor(event.color) + b.name.setText(R.string.agenda_teacher_absence) + b.name.setTextColor(textColor) + b.count.text = event.count.toString() + b.count.setTextColor(textColor) + + b.badgeBackground.isVisible = false + b.badge.isVisible = false + } + + fun render(b: AgendaCounterItemBinding, event: TeacherAbsenceEvent) { + val textColor = Colors.legibleTextColor(event.color) + + b.card.foreground.setTintColor(event.color) + b.card.background.setTintColor(event.color) + b.name.setText(R.string.agenda_teacher_absence) + b.name.setTextColor(textColor) + b.count.text = event.count.toString() + b.count.setTextColor(textColor) + + b.badgeBackground.isVisible = false + b.badge.isVisible = false + } + + override fun getEventLayout(): Int = R.layout.agenda_wrapped_counter +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/announcements/AnnouncementsAdapter.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/announcements/AnnouncementsAdapter.java similarity index 88% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/announcements/AnnouncementsAdapter.java rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/announcements/AnnouncementsAdapter.java index ee88da07..d50d6670 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/announcements/AnnouncementsAdapter.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/announcements/AnnouncementsAdapter.java @@ -1,4 +1,4 @@ -package pl.szczodrzynski.edziennik.ui.modules.announcements; +package pl.szczodrzynski.edziennik.ui.announcements; import android.content.Context; import android.graphics.Bitmap; @@ -10,6 +10,7 @@ import android.view.View; import android.view.ViewGroup; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.databinding.DataBindingUtil; import androidx.recyclerview.widget.RecyclerView; @@ -18,19 +19,20 @@ import java.util.List; import pl.szczodrzynski.edziennik.R; import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull; import pl.szczodrzynski.edziennik.databinding.RowAnnouncementsItemBinding; -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils; +import pl.szczodrzynski.edziennik.ui.messages.MessagesUtils; public class AnnouncementsAdapter extends RecyclerView.Adapter { private Context context; public List announcementList; + @Nullable public OnAnnouncementClickListener onClick; public interface OnAnnouncementClickListener { void onClick(View v, AnnouncementFull announcement); } - public AnnouncementsAdapter(Context context, List announcementList, OnAnnouncementClickListener onClick) { + public AnnouncementsAdapter(Context context, List announcementList, @Nullable OnAnnouncementClickListener onClick) { //setHasStableIds(true); this.context = context; @@ -54,11 +56,12 @@ public class AnnouncementsAdapter extends RecyclerView.Adapter { - if (onClick != null) { - onClick.onClick(v, item); - } - })); + if (onClick != null) { + b.announcementsItem.setOnClickListener(v -> onClick.onClick(v, item)); + } + else { + b.announcementsItem.setOnClickListener(null); + } b.announcementsItemSender.setText(item.getTeacherName()); b.announcementsItemTitle.setText(item.getSubject()); b.announcementsItemText.setText(item.getText()); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/announcements/AnnouncementsFragment.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/announcements/AnnouncementsFragment.java similarity index 89% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/announcements/AnnouncementsFragment.java rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/announcements/AnnouncementsFragment.java index d8cdbc2c..3bc0e13e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/announcements/AnnouncementsFragment.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/announcements/AnnouncementsFragment.java @@ -1,4 +1,6 @@ -package pl.szczodrzynski.edziennik.ui.modules.announcements; +package pl.szczodrzynski.edziennik.ui.announcements; + +import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE; import android.os.AsyncTask; import android.os.Bundle; @@ -26,6 +28,8 @@ import pl.szczodrzynski.edziennik.MainActivity; import pl.szczodrzynski.edziennik.R; import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask; import pl.szczodrzynski.edziennik.data.api.events.AnnouncementGetEvent; +import pl.szczodrzynski.edziennik.data.db.enums.LoginType; +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType; import pl.szczodrzynski.edziennik.data.db.full.AnnouncementFull; import pl.szczodrzynski.edziennik.databinding.DialogAnnouncementBinding; import pl.szczodrzynski.edziennik.databinding.FragmentAnnouncementsBinding; @@ -33,10 +37,6 @@ import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration; import pl.szczodrzynski.edziennik.utils.Themes; import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem; -import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE; -import static pl.szczodrzynski.edziennik.data.db.entity.LoginStore.LOGIN_TYPE_LIBRUS; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_ANNOUNCEMENT; - public class AnnouncementsFragment extends Fragment { private App app = null; @@ -69,10 +69,10 @@ public class AnnouncementsFragment extends Fragment { .withIcon(CommunityMaterial.Icon.cmd_eye_check_outline) .withOnClickListener(v3 -> { activity.getBottomSheet().close(); - if (app.getProfile().getLoginStoreType() == LOGIN_TYPE_LIBRUS) { + if (app.getProfile().getLoginStoreType() == LoginType.LIBRUS) { EdziennikTask.Companion.announcementsRead(App.Companion.getProfileId()).enqueue(requireContext()); } else { - AsyncTask.execute(() -> App.db.metadataDao().setAllSeen(App.Companion.getProfileId(), TYPE_ANNOUNCEMENT, true)); + AsyncTask.execute(() -> App.Companion.getDb().metadataDao().setAllSeen(App.Companion.getProfileId(), MetadataType.ANNOUNCEMENT, true)); Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show(); } }) @@ -103,10 +103,14 @@ public class AnnouncementsFragment extends Fragment { } }); - app.db.announcementDao().getAll(App.Companion.getProfileId()).observe(this, announcements -> { + app.getDb().announcementDao().getAll(App.Companion.getProfileId()).observe(getViewLifecycleOwner(), announcements -> { if (app == null || activity == null || b == null || !isAdded()) return; + for (AnnouncementFull it : announcements) { + it.filterNotes(); + } + if (announcements == null) { recyclerView.setVisibility(View.GONE); b.announcementsNoData.setVisibility(View.VISIBLE); @@ -120,7 +124,7 @@ public class AnnouncementsFragment extends Fragment { return; }*/ AnnouncementsAdapter announcementsAdapter = new AnnouncementsAdapter(activity, announcements, (v, announcement) -> { - if (announcement.getText() == null || (app.getProfile().getLoginStoreType() == LOGIN_TYPE_LIBRUS && !announcement.getSeen())) { + if (announcement.getText() == null || (app.getProfile().getLoginStoreType() == LoginType.LIBRUS && !announcement.getSeen())) { EdziennikTask.Companion.announcementGet(App.Companion.getProfileId(), announcement).enqueue(requireContext()); } else { showAnnouncementDetailsDialog(announcement); @@ -168,9 +172,9 @@ public class AnnouncementsFragment extends Fragment { .setPositiveButton(R.string.ok, null) .show(); b.text.setText(announcement.getTeacherName() +"\n\n"+ (announcement.getStartDate() != null ? announcement.getStartDate().getFormattedString() : "-") + (announcement.getEndDate() != null ? " do " + announcement.getEndDate().getFormattedString() : "")+"\n\n" +announcement.getText()); - if (!announcement.getSeen() && app.getProfile().getLoginStoreType() != LOGIN_TYPE_LIBRUS) { + if (!announcement.getSeen() && app.getProfile().getLoginStoreType() != LoginType.LIBRUS) { announcement.setSeen(true); - AsyncTask.execute(() -> App.db.metadataDao().setSeen(App.Companion.getProfileId(), announcement, true)); + AsyncTask.execute(() -> App.Companion.getDb().metadataDao().setSeen(App.Companion.getProfileId(), announcement, true)); if (recyclerView.getAdapter() != null) recyclerView.getAdapter().notifyDataSetChanged(); } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceAdapter.kt similarity index 92% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceAdapter.kt index 0dcae97e..00a1b361 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceAdapter.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-29. */ -package pl.szczodrzynski.edziennik.ui.modules.attendance +package pl.szczodrzynski.edziennik.ui.attendance import android.animation.ObjectAnimator import android.view.LayoutInflater @@ -18,16 +18,17 @@ import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull -import pl.szczodrzynski.edziennik.startCoroutineTimer -import pl.szczodrzynski.edziennik.ui.modules.attendance.models.* -import pl.szczodrzynski.edziennik.ui.modules.attendance.viewholder.* -import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel -import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer +import pl.szczodrzynski.edziennik.ui.attendance.models.* +import pl.szczodrzynski.edziennik.ui.attendance.viewholder.* +import pl.szczodrzynski.edziennik.ui.grades.models.ExpandableItemModel +import pl.szczodrzynski.edziennik.ui.grades.viewholder.BindableViewHolder import kotlin.coroutines.CoroutineContext class AttendanceAdapter( val activity: AppCompatActivity, val type: Int, + val showNotes: Boolean = true, var onAttendanceClick: ((item: AttendanceFull) -> Unit)? = null ) : RecyclerView.Adapter(), CoroutineScope { companion object { @@ -175,7 +176,10 @@ class AttendanceAdapter( holder is EmptyViewHolder && item is AttendanceEmpty -> holder.onBind(activity, app, item, position, this) } - holder.itemView.setOnClickListener(onClickListener) + if (item !is AttendanceFull || onAttendanceClick != null) + holder.itemView.setOnClickListener(onClickListener) + else + holder.itemView.setOnClickListener(null) } fun notifyItemChanged(model: Any) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceBar.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceBar.kt similarity index 95% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceBar.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceBar.kt index 70be9256..f8dee4ce 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceBar.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceBar.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-5-1. */ -package pl.szczodrzynski.edziennik.ui.modules.attendance +package pl.szczodrzynski.edziennik.ui.attendance import android.annotation.SuppressLint import android.content.Context @@ -10,7 +10,7 @@ import android.graphics.* import android.text.TextPaint import android.util.AttributeSet import android.view.View -import pl.szczodrzynski.edziennik.dp +import pl.szczodrzynski.edziennik.ext.dp import pl.szczodrzynski.edziennik.utils.Colors import kotlin.math.roundToInt @@ -69,7 +69,7 @@ class AttendanceBar : View { override fun onDraw(canvas: Canvas?) { canvas ?: return - val sum = attendancesList.sumBy { it.count } + val sum = attendancesList.sumOf { it.count } if (sum == 0) { return } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceDetailsDialog.kt new file mode 100644 index 00000000..b0c67241 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceDetailsDialog.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-5-9. + */ + +package pl.szczodrzynski.edziennik.ui.attendance + +import android.view.LayoutInflater +import androidx.appcompat.app.AppCompatActivity +import androidx.core.graphics.ColorUtils +import androidx.core.view.isVisible +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull +import pl.szczodrzynski.edziennik.databinding.AttendanceDetailsDialogBinding +import pl.szczodrzynski.edziennik.ext.setTintColor +import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog +import pl.szczodrzynski.edziennik.ui.notes.setupNotesButton +import pl.szczodrzynski.edziennik.utils.BetterLink +import pl.szczodrzynski.edziennik.utils.managers.NoteManager + +class AttendanceDetailsDialog( + activity: AppCompatActivity, + private val attendance: AttendanceFull, + private val showNotes: Boolean = true, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BindingDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "AttendanceDetailsDialog" + + override fun getTitleRes(): Int? = null + override fun inflate(layoutInflater: LayoutInflater) = + AttendanceDetailsDialogBinding.inflate(layoutInflater) + + override fun getPositiveButtonText() = R.string.close + + override suspend fun onShow() { + val manager = app.attendanceManager + + val attendanceColor = manager.getAttendanceColor(attendance) + b.attendance = attendance + b.devMode = App.devMode + b.attendanceName.setTextColor(if (ColorUtils.calculateLuminance(attendanceColor) > 0.3) 0xaa000000.toInt() else 0xccffffff.toInt()) + b.attendanceName.background.setTintColor(attendanceColor) + + b.attendanceIsCounted.setText(if (attendance.isCounted) R.string.yes else R.string.no) + + attendance.teacherName?.let { name -> + BetterLink.attach( + b.teacherName, + teachers = mapOf(attendance.teacherId to name), + onActionSelected = dialog::dismiss + ) + } + + b.notesButton.isVisible = showNotes + b.notesButton.setupNotesButton( + activity = activity, + owner = attendance, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ) + b.legend.isVisible = showNotes + if (showNotes) + NoteManager.setLegendText(attendance, b.legend) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceFragment.kt similarity index 86% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceFragment.kt index 194f36d2..f3dbc2f7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-30. */ -package pl.szczodrzynski.edziennik.ui.modules.attendance +package pl.szczodrzynski.edziennik.ui.attendance import android.os.AsyncTask import android.os.Bundle @@ -15,11 +15,15 @@ import com.mikepenz.iconics.typeface.library.community.material.CommunityMateria import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.databinding.AttendanceFragmentBinding +import pl.szczodrzynski.edziennik.ext.Bundle +import pl.szczodrzynski.edziennik.ext.addOnPageSelectedListener +import pl.szczodrzynski.edziennik.ui.base.lazypager.FragmentLazyPagerAdapter import pl.szczodrzynski.edziennik.ui.dialogs.settings.AttendanceConfigDialog -import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.FragmentLazyPagerAdapter import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem import kotlin.coroutines.CoroutineContext @@ -63,7 +67,7 @@ class AttendanceFragment : Fragment(), CoroutineScope { .withIcon(CommunityMaterial.Icon.cmd_cog_outline) .withOnClickListener(View.OnClickListener { activity.bottomSheet.close() - AttendanceConfigDialog(activity, true, null, null) + AttendanceConfigDialog(activity, true, null, null).show() }), BottomSheetSeparatorItem(true), BottomSheetPrimaryItem(true) @@ -71,17 +75,17 @@ class AttendanceFragment : Fragment(), CoroutineScope { .withIcon(CommunityMaterial.Icon.cmd_eye_check_outline) .withOnClickListener(View.OnClickListener { activity.bottomSheet.close() - AsyncTask.execute { App.db.metadataDao().setAllSeen(App.profileId, Metadata.TYPE_ATTENDANCE, true) } + AsyncTask.execute { App.db.metadataDao().setAllSeen(App.profileId, MetadataType.ATTENDANCE, true) } Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show() }) ) activity.gainAttention() if (pageSelection == 1) - pageSelection = app.config.forProfile().attendance.attendancePageSelection + pageSelection = app.profile.config.attendance.attendancePageSelection val pagerAdapter = FragmentLazyPagerAdapter( - fragmentManager ?: return, + parentFragmentManager, b.refreshLayout, listOf( AttendanceSummaryFragment() to getString(R.string.attendance_tab_summary), @@ -109,7 +113,7 @@ class AttendanceFragment : Fragment(), CoroutineScope { currentItem = pageSelection addOnPageSelectedListener { pageSelection = it - app.config.forProfile().attendance.attendancePageSelection = it + app.profile.config.attendance.attendancePageSelection = it } b.tabLayout.setupWithViewPager(this) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceListFragment.kt similarity index 89% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceListFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceListFragment.kt index 8e68cdbb..02eae698 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceListFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-30. */ -package pl.szczodrzynski.edziennik.ui.modules.attendance +package pl.szczodrzynski.edziennik.ui.attendance import android.os.Bundle import android.view.LayoutInflater @@ -17,13 +17,13 @@ import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.data.db.entity.Attendance import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull import pl.szczodrzynski.edziennik.databinding.AttendanceListFragmentBinding -import pl.szczodrzynski.edziennik.isNotNullNorEmpty -import pl.szczodrzynski.edziennik.startCoroutineTimer -import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceDayRange -import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceMonth -import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceTypeGroup -import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment -import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesSubject +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer +import pl.szczodrzynski.edziennik.ui.attendance.models.AttendanceDayRange +import pl.szczodrzynski.edziennik.ui.attendance.models.AttendanceMonth +import pl.szczodrzynski.edziennik.ui.attendance.models.AttendanceTypeGroup +import pl.szczodrzynski.edziennik.ui.base.lazypager.LazyFragment +import pl.szczodrzynski.edziennik.ui.grades.models.GradesSubject import pl.szczodrzynski.edziennik.utils.models.Date import kotlin.coroutines.CoroutineContext @@ -41,7 +41,8 @@ class AttendanceListFragment : LazyFragment(), CoroutineScope { get() = job + Dispatchers.Main // local/private variables go here - private val manager by lazy { app.attendanceManager } + private val manager + get() = app.attendanceManager private var viewType = AttendanceFragment.VIEW_DAYS private var expandSubjectId = 0L @@ -65,6 +66,10 @@ class AttendanceListFragment : LazyFragment(), CoroutineScope { app.db.attendanceDao().getAll(App.profileId).observe(this@AttendanceListFragment, Observer { items -> this@AttendanceListFragment.launch { if (!isAdded) return@launch + items.forEach { + it.filterNotes() + } + // load & configure the adapter adapter.items = withContext(Dispatchers.Default) { processAttendance(items) } if (adapter.items.isNotNullNorEmpty() && b.list.adapter == null) { @@ -95,7 +100,7 @@ class AttendanceListFragment : LazyFragment(), CoroutineScope { }}) adapter.onAttendanceClick = { - AttendanceDetailsDialog(activity, it) + AttendanceDetailsDialog(activity, it).show() } }; return true} @@ -124,8 +129,8 @@ class AttendanceListFragment : LazyFragment(), CoroutineScope { if (attendance.isEmpty()) return mutableListOf() - val groupConsecutiveDays = app.config.forProfile().attendance.groupConsecutiveDays - val showPresenceInMonth = app.config.forProfile().attendance.showPresenceInMonth + val groupConsecutiveDays = app.profile.config.attendance.groupConsecutiveDays + val showPresenceInMonth = app.profile.config.attendance.showPresenceInMonth if (viewType == AttendanceFragment.VIEW_DAYS) { val items = attendance @@ -178,12 +183,12 @@ class AttendanceListFragment : LazyFragment(), CoroutineScope { .sortedBy { it.first } .toMap() - val totalCount = month.typeCountMap.entries.sumBy { + val totalCount = month.typeCountMap.entries.sumOf { if (!it.key.isCounted || it.key.baseType == Attendance.TYPE_UNKNOWN) 0 else it.value } - val presenceCount = month.typeCountMap.entries.sumBy { + val presenceCount = month.typeCountMap.entries.sumOf { when (it.key.baseType) { Attendance.TYPE_PRESENT, Attendance.TYPE_PRESENT_CUSTOM, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceSummaryFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceSummaryFragment.kt similarity index 92% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceSummaryFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceSummaryFragment.kt index 5f61020b..828e5a95 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceSummaryFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceSummaryFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-5-4. */ -package pl.szczodrzynski.edziennik.ui.modules.attendance +package pl.szczodrzynski.edziennik.ui.attendance import android.graphics.Color import android.os.Bundle @@ -24,10 +24,14 @@ import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.Attendance import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull import pl.szczodrzynski.edziennik.databinding.AttendanceSummaryFragmentBinding -import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceFragment.Companion.VIEW_SUMMARY -import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceSubject -import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment -import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesSubject +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.ext.setText +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer +import pl.szczodrzynski.edziennik.ui.attendance.AttendanceFragment.Companion.VIEW_SUMMARY +import pl.szczodrzynski.edziennik.ui.attendance.models.AttendanceSubject +import pl.szczodrzynski.edziennik.ui.base.lazypager.LazyFragment +import pl.szczodrzynski.edziennik.ui.grades.models.GradesSubject import pl.szczodrzynski.edziennik.utils.models.Date import java.text.DecimalFormat import kotlin.coroutines.CoroutineContext @@ -47,7 +51,8 @@ class AttendanceSummaryFragment : LazyFragment(), CoroutineScope { get() = job + Dispatchers.Main // local/private variables go here - private val manager by lazy { app.attendanceManager } + private val manager + get() = app.attendanceManager private var expandSubjectId = 0L private var attendance = listOf() @@ -70,6 +75,10 @@ class AttendanceSummaryFragment : LazyFragment(), CoroutineScope { app.db.attendanceDao().getAll(App.profileId).observe(this@AttendanceSummaryFragment, Observer { items -> this@AttendanceSummaryFragment.launch { if (!isAdded) return@launch + items.forEach { + it.filterNotes() + } + // load & configure the adapter attendance = items adapter.items = withContext(Dispatchers.Default) { processAttendance() } @@ -103,7 +112,7 @@ class AttendanceSummaryFragment : LazyFragment(), CoroutineScope { }}) adapter.onAttendanceClick = { - AttendanceDetailsDialog(activity, it) + AttendanceDetailsDialog(activity, it).show() } b.toggleGroup.check(when (periodSelection) { @@ -176,7 +185,7 @@ class AttendanceSummaryFragment : LazyFragment(), CoroutineScope { subjectName = it.value.firstOrNull()?.subjectLongName ?: "", items = it.value.toMutableList() ) } - .sortedBy { it.subjectName.toLowerCase() } + .sortedBy { it.subjectName.lowercase() } var totalCountSum = 0 var presenceCountSum = 0 @@ -188,12 +197,12 @@ class AttendanceSummaryFragment : LazyFragment(), CoroutineScope { .sortedBy { it.first } .toMap() - val totalCount = subject.typeCountMap.entries.sumBy { + val totalCount = subject.typeCountMap.entries.sumOf { if (!it.key.isCounted || it.key.baseType == Attendance.TYPE_UNKNOWN) 0 else it.value } - val presenceCount = subject.typeCountMap.entries.sumBy { + val presenceCount = subject.typeCountMap.entries.sumOf { when (it.key.baseType) { Attendance.TYPE_PRESENT, Attendance.TYPE_PRESENT_CUSTOM, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceView.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceView.kt similarity index 95% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceView.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceView.kt index dfe20eda..02be63ad 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceView.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceView.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-29. */ -package pl.szczodrzynski.edziennik.ui.modules.attendance +package pl.szczodrzynski.edziennik.ui.attendance import android.annotation.SuppressLint import android.content.Context @@ -17,8 +17,8 @@ import androidx.appcompat.widget.AppCompatTextView import androidx.core.graphics.ColorUtils import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.Attendance -import pl.szczodrzynski.edziennik.dp -import pl.szczodrzynski.edziennik.setTintColor +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ext.setTintColor import pl.szczodrzynski.edziennik.utils.managers.AttendanceManager class AttendanceView : AppCompatTextView { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceCount.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/models/AttendanceCount.kt similarity index 84% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceCount.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/models/AttendanceCount.kt index c2595885..787c10e3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceCount.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/models/AttendanceCount.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-30. */ -package pl.szczodrzynski.edziennik.ui.modules.attendance.models +package pl.szczodrzynski.edziennik.ui.attendance.models class AttendanceCount { var normalSum = 0f diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceDayRange.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/models/AttendanceDayRange.kt similarity index 81% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceDayRange.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/models/AttendanceDayRange.kt index e993d785..a3815861 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceDayRange.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/models/AttendanceDayRange.kt @@ -2,11 +2,11 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-30. */ -package pl.szczodrzynski.edziennik.ui.modules.attendance.models +package pl.szczodrzynski.edziennik.ui.attendance.models import pl.szczodrzynski.edziennik.data.db.entity.Attendance import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull -import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel +import pl.szczodrzynski.edziennik.ui.grades.models.ExpandableItemModel import pl.szczodrzynski.edziennik.utils.models.Date data class AttendanceDayRange( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceEmpty.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/models/AttendanceEmpty.kt similarity index 54% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceEmpty.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/models/AttendanceEmpty.kt index 46877f63..82a8a5be 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceEmpty.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/models/AttendanceEmpty.kt @@ -2,6 +2,6 @@ * Copyright (c) Kuba Szczodrzyński 2020-5-4. */ -package pl.szczodrzynski.edziennik.ui.modules.attendance.models +package pl.szczodrzynski.edziennik.ui.attendance.models class AttendanceEmpty diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceMonth.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/models/AttendanceMonth.kt similarity index 83% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceMonth.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/models/AttendanceMonth.kt index c6d14914..a51af848 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceMonth.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/models/AttendanceMonth.kt @@ -2,12 +2,12 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-30. */ -package pl.szczodrzynski.edziennik.ui.modules.attendance.models +package pl.szczodrzynski.edziennik.ui.attendance.models import pl.szczodrzynski.edziennik.data.db.entity.Attendance import pl.szczodrzynski.edziennik.data.db.entity.AttendanceType import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull -import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel +import pl.szczodrzynski.edziennik.ui.grades.models.ExpandableItemModel data class AttendanceMonth( val year: Int, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceSubject.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/models/AttendanceSubject.kt similarity index 83% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceSubject.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/models/AttendanceSubject.kt index b56cd62a..db5f34a1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceSubject.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/models/AttendanceSubject.kt @@ -2,12 +2,12 @@ * Copyright (c) Kuba Szczodrzyński 2020-5-4. */ -package pl.szczodrzynski.edziennik.ui.modules.attendance.models +package pl.szczodrzynski.edziennik.ui.attendance.models import pl.szczodrzynski.edziennik.data.db.entity.Attendance import pl.szczodrzynski.edziennik.data.db.entity.AttendanceType import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull -import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel +import pl.szczodrzynski.edziennik.ui.grades.models.ExpandableItemModel data class AttendanceSubject( val subjectId: Long, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceTypeGroup.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/models/AttendanceTypeGroup.kt similarity index 82% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceTypeGroup.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/models/AttendanceTypeGroup.kt index d6b431ec..a6399456 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/models/AttendanceTypeGroup.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/models/AttendanceTypeGroup.kt @@ -2,12 +2,12 @@ * Copyright (c) Kuba Szczodrzyński 2020-5-8. */ -package pl.szczodrzynski.edziennik.ui.modules.attendance.models +package pl.szczodrzynski.edziennik.ui.attendance.models import pl.szczodrzynski.edziennik.data.db.entity.Attendance import pl.szczodrzynski.edziennik.data.db.entity.AttendanceType import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull -import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel +import pl.szczodrzynski.edziennik.ui.grades.models.ExpandableItemModel data class AttendanceTypeGroup( val type: AttendanceType, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/AttendanceViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/AttendanceViewHolder.kt similarity index 76% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/AttendanceViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/AttendanceViewHolder.kt index 3e7a1d32..ab6a0b92 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/AttendanceViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/AttendanceViewHolder.kt @@ -2,25 +2,24 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-30. */ -package pl.szczodrzynski.edziennik.ui.modules.attendance.viewholder +package pl.szczodrzynski.edziennik.ui.attendance.viewholder import android.view.LayoutInflater import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ContextThemeWrapper import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.concat import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull import pl.szczodrzynski.edziennik.databinding.AttendanceItemAttendanceBinding -import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter -import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceDayRange -import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceMonth -import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel -import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder -import pl.szczodrzynski.edziennik.utils.Themes +import pl.szczodrzynski.edziennik.ext.concat +import pl.szczodrzynski.edziennik.ui.attendance.AttendanceAdapter +import pl.szczodrzynski.edziennik.ui.attendance.models.AttendanceDayRange +import pl.szczodrzynski.edziennik.ui.attendance.models.AttendanceMonth +import pl.szczodrzynski.edziennik.ui.grades.models.ExpandableItemModel +import pl.szczodrzynski.edziennik.ui.grades.viewholder.BindableViewHolder +import pl.szczodrzynski.edziennik.utils.managers.NoteManager import pl.szczodrzynski.edziennik.utils.models.Week class AttendanceViewHolder( @@ -34,14 +33,16 @@ class AttendanceViewHolder( override fun onBind(activity: AppCompatActivity, app: App, item: AttendanceFull, position: Int, adapter: AttendanceAdapter) { val manager = app.attendanceManager - val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme) val bullet = " • " b.attendanceView.setAttendance(item, manager, bigView = true) b.type.text = item.typeName - b.subjectName.text = item.subjectLongName ?: item.lessonTopic + b.subjectName.text = item.getNoteSubstituteText(adapter.showNotes) ?: item.subjectLongName + ?: item.lessonTopic + if (adapter.showNotes) + NoteManager.prependIcon(item, b.subjectName) b.dateTime.text = listOf( Week.getFullDayName(item.date.weekDay), item.date.formattedStringShort, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/DayRangeViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/DayRangeViewHolder.kt similarity index 82% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/DayRangeViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/DayRangeViewHolder.kt index b42d5e16..ef0647bb 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/DayRangeViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/DayRangeViewHolder.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-30. */ -package pl.szczodrzynski.edziennik.ui.modules.attendance.viewholder +package pl.szczodrzynski.edziennik.ui.attendance.viewholder import android.view.LayoutInflater import android.view.View @@ -14,14 +14,14 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.concat import pl.szczodrzynski.edziennik.data.db.entity.Attendance import pl.szczodrzynski.edziennik.databinding.AttendanceItemDayRangeBinding -import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter -import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter.Companion.STATE_CLOSED -import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceView -import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceDayRange -import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder +import pl.szczodrzynski.edziennik.ext.concat +import pl.szczodrzynski.edziennik.ui.attendance.AttendanceAdapter +import pl.szczodrzynski.edziennik.ui.attendance.AttendanceAdapter.Companion.STATE_CLOSED +import pl.szczodrzynski.edziennik.ui.attendance.AttendanceView +import pl.szczodrzynski.edziennik.ui.attendance.models.AttendanceDayRange +import pl.szczodrzynski.edziennik.ui.grades.viewholder.BindableViewHolder import pl.szczodrzynski.edziennik.utils.Themes class DayRangeViewHolder( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/EmptyViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/EmptyViewHolder.kt similarity index 73% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/EmptyViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/EmptyViewHolder.kt index 37fe635c..0a0786d6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/EmptyViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/EmptyViewHolder.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-5-4. */ -package pl.szczodrzynski.edziennik.ui.modules.attendance.viewholder +package pl.szczodrzynski.edziennik.ui.attendance.viewholder import android.view.LayoutInflater import android.view.ViewGroup @@ -10,9 +10,9 @@ import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.databinding.AttendanceItemEmptyBinding -import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter -import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceEmpty -import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder +import pl.szczodrzynski.edziennik.ui.attendance.AttendanceAdapter +import pl.szczodrzynski.edziennik.ui.attendance.models.AttendanceEmpty +import pl.szczodrzynski.edziennik.ui.grades.viewholder.BindableViewHolder class EmptyViewHolder( inflater: LayoutInflater, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/MonthViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/MonthViewHolder.kt similarity index 83% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/MonthViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/MonthViewHolder.kt index 71c139ac..f9afd2f3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/MonthViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/MonthViewHolder.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-30. */ -package pl.szczodrzynski.edziennik.ui.modules.attendance.viewholder +package pl.szczodrzynski.edziennik.ui.attendance.viewholder import android.view.LayoutInflater import android.view.ViewGroup @@ -13,14 +13,19 @@ import androidx.appcompat.view.ContextThemeWrapper import androidx.core.view.isInvisible import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.Attendance import pl.szczodrzynski.edziennik.databinding.AttendanceItemMonthBinding -import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter -import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter.Companion.STATE_CLOSED -import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceView -import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceMonth -import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder +import pl.szczodrzynski.edziennik.ext.concat +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ext.fixName +import pl.szczodrzynski.edziennik.ext.setText +import pl.szczodrzynski.edziennik.ui.attendance.AttendanceAdapter +import pl.szczodrzynski.edziennik.ui.attendance.AttendanceAdapter.Companion.STATE_CLOSED +import pl.szczodrzynski.edziennik.ui.attendance.AttendanceView +import pl.szczodrzynski.edziennik.ui.attendance.models.AttendanceMonth +import pl.szczodrzynski.edziennik.ui.grades.viewholder.BindableViewHolder import pl.szczodrzynski.edziennik.utils.Themes import pl.szczodrzynski.edziennik.utils.models.Date @@ -57,7 +62,6 @@ class MonthViewHolder( b.previewContainer.removeAllViews() - val sum = item.typeCountMap.entries.sumBy { it.value }.toFloat() item.typeCountMap.forEach { (type, count) -> val layout = LinearLayout(contextWrapper) val attendance = Attendance( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/SubjectViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/SubjectViewHolder.kt similarity index 71% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/SubjectViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/SubjectViewHolder.kt index e439111d..a7e5e562 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/SubjectViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/SubjectViewHolder.kt @@ -2,23 +2,21 @@ * Copyright (c) Kuba Szczodrzyński 2020-5-4. */ -package pl.szczodrzynski.edziennik.ui.modules.attendance.viewholder +package pl.szczodrzynski.edziennik.ui.attendance.viewholder import android.view.LayoutInflater import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ContextThemeWrapper import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.databinding.AttendanceItemSubjectBinding -import pl.szczodrzynski.edziennik.setText -import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter -import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter.Companion.STATE_CLOSED -import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceSubject -import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder -import pl.szczodrzynski.edziennik.utils.Themes +import pl.szczodrzynski.edziennik.ext.setText +import pl.szczodrzynski.edziennik.ui.attendance.AttendanceAdapter +import pl.szczodrzynski.edziennik.ui.attendance.AttendanceAdapter.Companion.STATE_CLOSED +import pl.szczodrzynski.edziennik.ui.attendance.models.AttendanceSubject +import pl.szczodrzynski.edziennik.ui.grades.viewholder.BindableViewHolder class SubjectViewHolder( inflater: LayoutInflater, @@ -31,7 +29,6 @@ class SubjectViewHolder( override fun onBind(activity: AppCompatActivity, app: App, item: AttendanceSubject, position: Int, adapter: AttendanceAdapter) { val manager = app.attendanceManager - val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme) b.title.text = item.subjectName diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/TypeViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/TypeViewHolder.kt similarity index 80% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/TypeViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/TypeViewHolder.kt index a2f8b689..6719c3a6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/viewholder/TypeViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/TypeViewHolder.kt @@ -2,23 +2,21 @@ * Copyright (c) Kuba Szczodrzyński 2020-5-8. */ -package pl.szczodrzynski.edziennik.ui.modules.attendance.viewholder +package pl.szczodrzynski.edziennik.ui.attendance.viewholder import android.view.LayoutInflater import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ContextThemeWrapper import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.concat import pl.szczodrzynski.edziennik.data.db.entity.Attendance import pl.szczodrzynski.edziennik.databinding.AttendanceItemTypeBinding -import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter -import pl.szczodrzynski.edziennik.ui.modules.attendance.models.AttendanceTypeGroup -import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder -import pl.szczodrzynski.edziennik.utils.Themes +import pl.szczodrzynski.edziennik.ext.concat +import pl.szczodrzynski.edziennik.ui.attendance.AttendanceAdapter +import pl.szczodrzynski.edziennik.ui.attendance.models.AttendanceTypeGroup +import pl.szczodrzynski.edziennik.ui.grades.viewholder.BindableViewHolder import pl.szczodrzynski.edziennik.utils.models.Date class TypeViewHolder( @@ -32,7 +30,6 @@ class TypeViewHolder( override fun onBind(activity: AppCompatActivity, app: App, item: AttendanceTypeGroup, position: Int, adapter: AttendanceAdapter) { val manager = app.attendanceManager - val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme) val type = item.type b.title.text = type.typeName diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/BuildInvalidActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/BuildInvalidActivity.kt similarity index 91% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/BuildInvalidActivity.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/base/BuildInvalidActivity.kt index 766c79ad..5aa6a84a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/BuildInvalidActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/BuildInvalidActivity.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2021-3-27. */ -package pl.szczodrzynski.edziennik.ui.modules.base +package pl.szczodrzynski.edziennik.ui.base import android.graphics.Color import android.os.Bundle @@ -10,7 +10,7 @@ import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isVisible import com.mikepenz.iconics.utils.colorInt import pl.szczodrzynski.edziennik.databinding.ActivityBuildInvalidBinding -import pl.szczodrzynski.edziennik.onClick +import pl.szczodrzynski.edziennik.ext.onClick import pl.szczodrzynski.edziennik.utils.Themes class BuildInvalidActivity : AppCompatActivity() { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/CrashActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/CrashActivity.kt similarity index 94% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/CrashActivity.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/base/CrashActivity.kt index 0b39b913..398134b3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/CrashActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/CrashActivity.kt @@ -1,4 +1,4 @@ -package pl.szczodrzynski.edziennik.ui.modules.base +package pl.szczodrzynski.edziennik.ui.base import android.content.ClipData import android.content.ClipboardManager @@ -6,7 +6,6 @@ import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle -import android.text.Html import android.view.View import android.widget.Button import android.widget.Toast @@ -20,8 +19,9 @@ import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.ERROR_APP_CRASH import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.data.api.szkolny.request.ErrorReportRequest -import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.ext.resolveColor import pl.szczodrzynski.edziennik.utils.Themes.appTheme +import pl.szczodrzynski.edziennik.utils.html.BetterHtml import kotlin.coroutines.CoroutineContext /* @@ -82,7 +82,7 @@ class CrashActivity : AppCompatActivity(), CoroutineScope { Toast.makeText(app, getString(R.string.crash_report_sent), Toast.LENGTH_SHORT).show() reportButton.isEnabled = false - reportButton.setTextColor(resources.getColor(android.R.color.darker_gray)) + reportButton.setTextColor(android.R.color.darker_gray.resolveColor(this@CrashActivity)) } } @@ -90,7 +90,7 @@ class CrashActivity : AppCompatActivity(), CoroutineScope { moreInfoButton.setOnClickListener { MaterialAlertDialogBuilder(this, R.style.AppTheme_MaterialAlertDialogMonospace) .setTitle(R.string.crash_details) - .setMessage(Html.fromHtml(getErrorString(intent, false))) + .setMessage(BetterHtml.fromHtml(context = null, getErrorString(intent, false))) .setPositiveButton(R.string.close, null) .setNeutralButton(R.string.copy_to_clipboard) { _, _ -> copyErrorToClipboard() } .show() @@ -115,7 +115,7 @@ class CrashActivity : AppCompatActivity(), CoroutineScope { content = content.replace(packageName.toRegex(), "$packageName") content = content.replace("\n".toRegex(), "
    ") contentPlain += "\n" + Build.MANUFACTURER + "\n" + Build.BRAND + "\n" + Build.MODEL + "\n" + Build.DEVICE + "\n" - if (app.profile != null && app.profile.registration == Profile.REGISTRATION_ENABLED) { + if (!app.profile.canShare) { contentPlain += "U: " + app.profile.userCode + "\nS: " + app.profile.studentNameLong + "\n" } contentPlain += BuildConfig.VERSION_NAME + " " + BuildConfig.BUILD_TYPE diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/MainSnackbar.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/MainSnackbar.kt similarity index 96% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/MainSnackbar.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/base/MainSnackbar.kt index 362ffe9b..93e340b3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/MainSnackbar.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/MainSnackbar.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2019-12-22. */ -package pl.szczodrzynski.edziennik.ui.modules.base +package pl.szczodrzynski.edziennik.ui.base import android.view.View import androidx.appcompat.app.AppCompatActivity diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/enums/NavTarget.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/enums/NavTarget.kt new file mode 100644 index 00000000..749c1343 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/enums/NavTarget.kt @@ -0,0 +1,243 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-17. + */ + +package pl.szczodrzynski.edziennik.ui.base.enums + +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import eu.szkolny.font.SzkolnyFont +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.ui.agenda.AgendaFragment +import pl.szczodrzynski.edziennik.ui.announcements.AnnouncementsFragment +import pl.szczodrzynski.edziennik.ui.attendance.AttendanceFragment +import pl.szczodrzynski.edziennik.ui.behaviour.BehaviourFragment +import pl.szczodrzynski.edziennik.ui.debug.DebugFragment +import pl.szczodrzynski.edziennik.ui.debug.LabFragment +import pl.szczodrzynski.edziennik.ui.feedback.FeedbackFragment +import pl.szczodrzynski.edziennik.ui.grades.GradesListFragment +import pl.szczodrzynski.edziennik.ui.grades.editor.GradesEditorFragment +import pl.szczodrzynski.edziennik.ui.home.HomeFragment +import pl.szczodrzynski.edziennik.ui.homework.HomeworkFragment +import pl.szczodrzynski.edziennik.ui.messages.compose.MessagesComposeFragment +import pl.szczodrzynski.edziennik.ui.messages.list.MessagesFragment +import pl.szczodrzynski.edziennik.ui.messages.single.MessageFragment +import pl.szczodrzynski.edziennik.ui.notes.NotesFragment +import pl.szczodrzynski.edziennik.ui.notifications.NotificationsListFragment +import pl.szczodrzynski.edziennik.ui.settings.ProfileManagerFragment +import pl.szczodrzynski.edziennik.ui.settings.SettingsFragment +import pl.szczodrzynski.edziennik.ui.teachers.TeachersListFragment +import pl.szczodrzynski.edziennik.ui.timetable.TimetableFragment +import pl.szczodrzynski.edziennik.ui.webpush.WebPushFragment + +enum class NavTarget( + val id: Int, + val fragmentClass: Class?, + val location: NavTargetLocation = NavTargetLocation.NOWHERE, + @StringRes val nameRes: Int, + @StringRes val descriptionRes: Int? = null, + @StringRes val titleRes: Int? = null, + val icon: IIcon? = null, + val popTo: NavTarget? = null, + val badgeType: MetadataType? = null, + val featureType: FeatureType? = null, + val devModeOnly: Boolean = false, +) { + HOME( + id = 1, + fragmentClass = HomeFragment::class.java, + location = NavTargetLocation.DRAWER, + nameRes = R.string.menu_home_page, + titleRes = R.string.app_name, + icon = CommunityMaterial.Icon2.cmd_home_outline, + ), + TIMETABLE( + id = 11, + fragmentClass = TimetableFragment::class.java, + location = NavTargetLocation.DRAWER, + nameRes = R.string.menu_timetable, + icon = CommunityMaterial.Icon3.cmd_timetable, + popTo = HOME, + badgeType = MetadataType.LESSON_CHANGE, + featureType = FeatureType.TIMETABLE, + ), + AGENDA( + id = 12, + fragmentClass = AgendaFragment::class.java, + location = NavTargetLocation.DRAWER, + nameRes = R.string.menu_agenda, + icon = CommunityMaterial.Icon.cmd_calendar_outline, + popTo = HOME, + badgeType = MetadataType.EVENT, + featureType = FeatureType.AGENDA, + ), + GRADES( + id = 13, + fragmentClass = GradesListFragment::class.java, + location = NavTargetLocation.DRAWER, + nameRes = R.string.menu_grades, + icon = CommunityMaterial.Icon3.cmd_numeric_5_box_outline, + popTo = HOME, + badgeType = MetadataType.GRADE, + featureType = FeatureType.GRADES, + ), + MESSAGES( + id = 17, + fragmentClass = MessagesFragment::class.java, + location = NavTargetLocation.DRAWER, + nameRes = R.string.menu_messages, + icon = CommunityMaterial.Icon.cmd_email_outline, + popTo = HOME, + badgeType = MetadataType.MESSAGE, + featureType = FeatureType.MESSAGES_INBOX, + ), + HOMEWORK( + id = 14, + fragmentClass = HomeworkFragment::class.java, + location = NavTargetLocation.DRAWER, + nameRes = R.string.menu_homework, + icon = SzkolnyFont.Icon.szf_notebook_outline, + popTo = HOME, + badgeType = MetadataType.HOMEWORK, + featureType = FeatureType.HOMEWORK, + ), + BEHAVIOUR( + id = 15, + fragmentClass = BehaviourFragment::class.java, + location = NavTargetLocation.DRAWER, + nameRes = R.string.menu_notices, + icon = CommunityMaterial.Icon.cmd_emoticon_outline, + popTo = HOME, + badgeType = MetadataType.NOTICE, + featureType = FeatureType.BEHAVIOUR, + ), + ATTENDANCE( + id = 16, + fragmentClass = AttendanceFragment::class.java, + location = NavTargetLocation.DRAWER, + nameRes = R.string.menu_attendance, + icon = CommunityMaterial.Icon.cmd_calendar_remove_outline, + popTo = HOME, + badgeType = MetadataType.ATTENDANCE, + featureType = FeatureType.ATTENDANCE, + ), + ANNOUNCEMENTS( + id = 18, + fragmentClass = AnnouncementsFragment::class.java, + location = NavTargetLocation.DRAWER, + nameRes = R.string.menu_announcements, + icon = CommunityMaterial.Icon.cmd_bullhorn_outline, + popTo = HOME, + badgeType = MetadataType.ANNOUNCEMENT, + featureType = FeatureType.ANNOUNCEMENTS, + ), + NOTES( + id = 23, + fragmentClass = NotesFragment::class.java, + location = NavTargetLocation.DRAWER_MORE, + nameRes = R.string.menu_notes, + icon = CommunityMaterial.Icon3.cmd_text_box_multiple_outline, + ), + TEACHERS( + id = 22, + fragmentClass = TeachersListFragment::class.java, + location = NavTargetLocation.DRAWER_MORE, + nameRes = R.string.menu_teachers, + icon = CommunityMaterial.Icon3.cmd_shield_account_outline, + ), + NOTIFICATIONS( + id = 20, + fragmentClass = NotificationsListFragment::class.java, + location = NavTargetLocation.DRAWER_BOTTOM, + nameRes = R.string.menu_notifications, + icon = CommunityMaterial.Icon.cmd_bell_ring_outline, + popTo = HOME, + ), + SETTINGS( + id = 101, + fragmentClass = SettingsFragment::class.java, + location = NavTargetLocation.DRAWER_BOTTOM, + nameRes = R.string.menu_settings, + icon = CommunityMaterial.Icon.cmd_cog_outline, + ), + LAB( + id = 1000, + fragmentClass = LabFragment::class.java, + location = NavTargetLocation.DRAWER_BOTTOM, + nameRes = R.string.menu_lab, + icon = CommunityMaterial.Icon2.cmd_flask_outline, + popTo = HOME, + devModeOnly = true, + ), + PROFILE_ADD( + id = 200, + fragmentClass = null, + location = NavTargetLocation.PROFILE_LIST, + nameRes = R.string.menu_add_new_profile, + descriptionRes = R.string.drawer_add_new_profile_desc, + icon = CommunityMaterial.Icon3.cmd_plus, + ), + PROFILE_MANAGER( + id = 203, + fragmentClass = ProfileManagerFragment::class.java, + location = NavTargetLocation.NOWHERE, + nameRes = R.string.menu_manage_profiles, + titleRes = R.string.title_profile_manager, + descriptionRes = R.string.drawer_manage_profiles_desc, + icon = CommunityMaterial.Icon.cmd_account_group, + ), + PROFILE_MARK_AS_READ( + id = 204, + fragmentClass = null, + location = NavTargetLocation.PROFILE_LIST, + nameRes = R.string.menu_mark_everything_as_read, + icon = CommunityMaterial.Icon.cmd_eye_check_outline, + ), + PROFILE_SYNC_ALL( + id = 201, + fragmentClass = null, + location = NavTargetLocation.PROFILE_LIST, + nameRes = R.string.menu_sync_all, + icon = CommunityMaterial.Icon.cmd_download_outline, + ), + FEEDBACK( + id = 120, + fragmentClass = FeedbackFragment::class.java, + location = NavTargetLocation.BOTTOM_SHEET, + nameRes = R.string.menu_feedback, + icon = CommunityMaterial.Icon2.cmd_help_circle_outline, + ), + DEBUG( + id = 102, + fragmentClass = DebugFragment::class.java, + location = NavTargetLocation.BOTTOM_SHEET, + nameRes = R.string.menu_debug, + icon = CommunityMaterial.Icon.cmd_android_debug_bridge, + devModeOnly = true, + ), + GRADES_EDITOR( + id = 501, + fragmentClass = GradesEditorFragment::class.java, + nameRes = R.string.menu_grades_editor, + ), + MESSAGE( + id = 503, + fragmentClass = MessageFragment::class.java, + nameRes = R.string.menu_message, + popTo = MESSAGES, + ), + MESSAGE_COMPOSE( + id = 504, + fragmentClass = MessagesComposeFragment::class.java, + nameRes = R.string.menu_message_compose, + ), + WEB_PUSH( + id = 140, + fragmentClass = WebPushFragment::class.java, + nameRes = R.string.menu_web_push, + ) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/enums/NavTargetLocation.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/enums/NavTargetLocation.kt new file mode 100644 index 00000000..24ab9134 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/enums/NavTargetLocation.kt @@ -0,0 +1,14 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-17. + */ + +package pl.szczodrzynski.edziennik.ui.base.enums + +enum class NavTargetLocation { + NOWHERE, + DRAWER, + DRAWER_MORE, + DRAWER_BOTTOM, + PROFILE_LIST, + BOTTOM_SHEET, +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/FragmentLazyPagerAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/lazypager/FragmentLazyPagerAdapter.kt similarity index 83% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/FragmentLazyPagerAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/base/lazypager/FragmentLazyPagerAdapter.kt index d6205596..91159270 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/FragmentLazyPagerAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/lazypager/FragmentLazyPagerAdapter.kt @@ -2,14 +2,14 @@ * Copyright (c) Kuba Szczodrzyński 2020-3-30. */ -package pl.szczodrzynski.edziennik.ui.modules.base.lazypager +package pl.szczodrzynski.edziennik.ui.base.lazypager import androidx.fragment.app.FragmentManager import androidx.swiperefreshlayout.widget.SwipeRefreshLayout class FragmentLazyPagerAdapter( fragmentManager: FragmentManager, - swipeRefreshLayout: SwipeRefreshLayout, + swipeRefreshLayout: SwipeRefreshLayout? = null, val fragments: List> ) : LazyPagerAdapter(fragmentManager, swipeRefreshLayout) { override fun getPage(position: Int) = fragments[position].first diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/LazyFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/lazypager/LazyFragment.kt similarity index 96% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/LazyFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/base/lazypager/LazyFragment.kt index b5f26172..dd00436a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/LazyFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/lazypager/LazyFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-3-29. */ -package pl.szczodrzynski.edziennik.ui.modules.base.lazypager +package pl.szczodrzynski.edziennik.ui.base.lazypager import android.os.Bundle import androidx.fragment.app.Fragment diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/LazyPagerAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/lazypager/LazyPagerAdapter.kt similarity index 94% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/LazyPagerAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/base/lazypager/LazyPagerAdapter.kt index b70de59e..383d0c6f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/LazyPagerAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/lazypager/LazyPagerAdapter.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-3-29. */ -package pl.szczodrzynski.edziennik.ui.modules.base.lazypager +package pl.szczodrzynski.edziennik.ui.base.lazypager import android.util.SparseBooleanArray import androidx.core.util.set diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/LazyViewPager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/lazypager/LazyViewPager.kt similarity index 95% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/LazyViewPager.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/base/lazypager/LazyViewPager.kt index 45337990..2026d974 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/base/lazypager/LazyViewPager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/lazypager/LazyViewPager.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-3-29. */ -package pl.szczodrzynski.edziennik.ui.modules.base.lazypager +package pl.szczodrzynski.edziennik.ui.base.lazypager import android.content.Context import android.util.AttributeSet diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/behaviour/BehaviourFragment.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/behaviour/BehaviourFragment.java similarity index 94% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/behaviour/BehaviourFragment.java rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/behaviour/BehaviourFragment.java index d0b46f4a..f0eb11d2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/behaviour/BehaviourFragment.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/behaviour/BehaviourFragment.java @@ -1,4 +1,6 @@ -package pl.szczodrzynski.edziennik.ui.modules.behaviour; +package pl.szczodrzynski.edziennik.ui.behaviour; + +import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE; import android.graphics.Color; import android.os.AsyncTask; @@ -27,14 +29,12 @@ 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; -import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE; -import static pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_NOTICE; - public class BehaviourFragment extends Fragment { private App app = null; @@ -72,7 +72,7 @@ public class BehaviourFragment extends Fragment { .withIcon(CommunityMaterial.Icon.cmd_eye_check_outline) .withOnClickListener(v3 -> { activity.getBottomSheet().close(); - AsyncTask.execute(() -> App.db.metadataDao().setAllSeen(App.Companion.getProfileId(), TYPE_NOTICE, true)); + 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(); }) ); @@ -111,7 +111,7 @@ public class BehaviourFragment extends Fragment { } }); - app.db.noticeDao().getAll(App.Companion.getProfileId()).observe(this, notices -> { + app.getDb().noticeDao().getAll(App.Companion.getProfileId()).observe(getViewLifecycleOwner(), notices -> { if (app == null || activity == null || b == null || !isAdded()) return; diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/behaviour/NoticesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/behaviour/NoticesAdapter.kt similarity index 91% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/behaviour/NoticesAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/behaviour/NoticesAdapter.kt index a3288da2..89dbb773 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/behaviour/NoticesAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/behaviour/NoticesAdapter.kt @@ -1,4 +1,4 @@ -package pl.szczodrzynski.edziennik.ui.modules.behaviour +package pl.szczodrzynski.edziennik.ui.behaviour import android.content.Context import android.graphics.PorterDuff @@ -18,9 +18,9 @@ import com.mikepenz.iconics.utils.sizeDp import eu.szkolny.font.SzkolnyFont import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_MOBIDZIENNIK import pl.szczodrzynski.edziennik.data.db.entity.Notice import pl.szczodrzynski.edziennik.data.db.full.NoticeFull +import pl.szczodrzynski.edziennik.utils.BetterLink import pl.szczodrzynski.edziennik.utils.Utils.bs import pl.szczodrzynski.edziennik.utils.models.Date @@ -39,7 +39,7 @@ class NoticesAdapter//getting the context and product list with constructor val notice = noticeList[position] - if (app.profile.loginStoreType == LOGIN_TYPE_MOBIDZIENNIK && false) { + 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 { @@ -83,6 +83,14 @@ class NoticesAdapter//getting the context and product list with constructor } else { holder.noticesItemReason.background = null } + + BetterLink.attach(holder.noticesItemReason) + + notice.teacherName?.let { name -> + BetterLink.attach(holder.noticesItemTeacherName, teachers = mapOf( + notice.teacherId to name + )) + } } override fun getItemCount(): Int { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaDialog.kt new file mode 100644 index 00000000..56d3976a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaDialog.kt @@ -0,0 +1,225 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-2-15. + */ + +package pl.szczodrzynski.edziennik.ui.captcha + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.view.LayoutInflater +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import kotlinx.coroutines.* +import okhttp3.* +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.RecaptchaDialogBinding +import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog +import java.io.IOException +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class RecaptchaDialog( + activity: AppCompatActivity, + private val siteKey: String, + private val referer: String, + 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(activity, onShowListener, onDismissListener) { + + override val TAG = "RecaptchaDialog" + + override fun getTitleRes(): Int? = null + override fun inflate(layoutInflater: LayoutInflater) = + RecaptchaDialogBinding.inflate(layoutInflater) + + override fun getPositiveButtonText() = R.string.ok + + private val captchaUrl = "https://www.google.com/recaptcha/api/fallback?k=$siteKey" + private var success = false + private var code = "" + private var payload = "" + + override suspend fun onBeforeShow(): Boolean { + val (title, text, bitmap) = withContext(Dispatchers.Default) { + val html = loadCaptchaHtml() + if (html == null) { + onServerError?.invoke() + return@withContext null + } + return@withContext loadCaptchaData(html) + } ?: run { + onFailure?.invoke() + return false + } + + initViews(title, text, bitmap) + return true + } + + override suspend fun onShow() { + b.image0.isChecked = false + b.image1.isChecked = false + b.image2.isChecked = false + b.image3.isChecked = false + b.image4.isChecked = false + b.image5.isChecked = false + b.image6.isChecked = false + b.image7.isChecked = false + b.image8.isChecked = false + } + + override fun onDismiss() { + if (!success) + onFailure?.invoke() + } + + private fun initViews(title: String, text: String, bitmap: Bitmap) { + b.descTitle.text = title + b.descText.text = text + b.payload.setImageBitmap(bitmap) + } + + private suspend fun loadCaptchaHtml(): String? { + val request = Request.Builder() + .url(captchaUrl) + .addHeader("Referer", referer) + .addHeader("Accept-Language", "pl") + .build() + + return suspendCoroutine { cont -> + app.http.newCall(request).enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + val html = response.body()?.string() + cont.resume(html) + } + + override fun onFailure(call: Call, e: IOException) { + cont.resume(null) + } + }) + } + } + + private suspend fun loadCaptchaData(html: String): Triple? { + var title = "" + var text = "" + "class=\"rc-imageselect-desc(?:-no-canonical)?\">(.+?) (.+?)" + .toRegex() + .find(html) + ?.let { + title = it.groupValues[1] + text = it.groupValues[2] + } + + code = "name=\"c\" value=\"([A-z0-9-_]+)\"" + .toRegex() + .find(html) + ?.let { it.groupValues[1] } + ?: return null + + payload = "https://www.google.com/recaptcha/api2/payload?c=$code&k=$siteKey" + val request = Request.Builder() + .url(payload) + .addHeader("Referer", captchaUrl) + .addHeader("Accept-Language", "pl") + .build() + + val bitmap = suspendCoroutine { cont -> + app.http.newCall(request).enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + val bitmap: Bitmap? = BitmapFactory.decodeStream(response.body()?.byteStream()) + if (bitmap == null) { + Toast.makeText( + activity, + "Nie udało się załadować reCAPTCHA.", + Toast.LENGTH_SHORT + ).show() + } + cont.resume(bitmap) + } + + override fun onFailure(call: Call, e: IOException) { + cont.resume(null) + } + }) + } ?: return null + + return Triple(title, text, bitmap) + } + + override suspend fun onPositiveClick(): Boolean { + val list = mutableListOf( + "c=$code" + ) + if (b.image0.isChecked) list += "response=0" + if (b.image1.isChecked) list += "response=1" + if (b.image2.isChecked) list += "response=2" + if (b.image3.isChecked) list += "response=3" + if (b.image4.isChecked) list += "response=4" + if (b.image5.isChecked) list += "response=5" + if (b.image6.isChecked) list += "response=6" + if (b.image7.isChecked) list += "response=7" + if (b.image8.isChecked) list += "response=8" + + val request = Request.Builder() + .url(captchaUrl) + .addHeader("Referer", captchaUrl) + .addHeader("Accept-Language", "pl") + .addHeader("Origin", "https://www.google.com") + .addHeader("Content-Type", "application/x-www-form-urlencoded") + .post(RequestBody.create( + MediaType.parse("application/x-www-form-urlencoded"), + list.joinToString("&"), + )) + .build() + + val (code, html) = withContext(Dispatchers.Default) { + return@withContext suspendCoroutine> { cont -> + app.http.newCall(request).enqueue(object : Callback { + override fun onResponse(call: Call, response: Response) { + val html = response.body()?.string() ?: run { + cont.resume(null to null) + return + } + val match = "([A-z0-9-_]+)".toRegex().find(html) + if (match == null) { + cont.resume(null to html) + } else { + cont.resume(match.groupValues[1] to null) + } + } + + override fun onFailure(call: Call, e: IOException) { + cont.resume(null to null) + } + }) + } + } + + when { + code != null -> { + success = true + onSuccess(code) + return DISMISS + } + html != null -> { + val (title, text, bitmap) = withContext(Dispatchers.Default) { + return@withContext loadCaptchaData(html) + } ?: run { + onFailure?.invoke() + return DISMISS + } + initViews(title, text, bitmap) + return NO_DISMISS + } + else -> { + onFailure?.invoke() + return DISMISS + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt new file mode 100644 index 00000000..ca876ddb --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/captcha/RecaptchaPromptDialog.kt @@ -0,0 +1,76 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-2-15. + */ + +package pl.szczodrzynski.edziennik.ui.captcha + +import android.graphics.drawable.Drawable +import android.view.LayoutInflater +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.RecaptchaViewBinding +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog + +class RecaptchaPromptDialog( + activity: AppCompatActivity, + private val siteKey: String, + 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(activity, onShowListener, onDismissListener) { + override val TAG = "RecaptchaPromptDialog" + + override fun getTitleRes(): Int? = null + override fun inflate(layoutInflater: LayoutInflater) = + RecaptchaViewBinding.inflate(layoutInflater) + + override fun getNegativeButtonText() = R.string.cancel + + private lateinit var checkboxBackground: Drawable + private lateinit var checkboxForeground: Drawable + private var success = false + + override suspend fun onShow() { + checkboxBackground = b.checkbox.background + checkboxForeground = b.checkbox.foreground + success = false + + b.root.onClick { + b.checkbox.performClick() + } + b.checkbox.onClick { + b.checkbox.background = null + b.checkbox.foreground = null + b.progress.visibility = View.VISIBLE + RecaptchaDialog( + activity, + siteKey = siteKey, + referer = referer, + onSuccess = { recaptchaCode -> + b.checkbox.background = checkboxBackground + b.checkbox.foreground = checkboxForeground + b.progress.visibility = View.GONE + success = true + onSuccess(recaptchaCode) + dialog.dismiss() + }, + onFailure = { + b.checkbox.background = checkboxBackground + b.checkbox.foreground = checkboxForeground + b.progress.visibility = View.GONE + }, + onServerError = onServerError, + ).show() + } + } + + override fun onDismiss() { + if (!success) + onCancel?.invoke() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/DebugFragment.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/DebugFragment.java similarity index 99% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/DebugFragment.java rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/DebugFragment.java index 62d5462b..6ff081db 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/DebugFragment.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/DebugFragment.java @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-3. */ -package pl.szczodrzynski.edziennik.ui.modules.debug; +package pl.szczodrzynski.edziennik.ui.debug; import android.os.Bundle; import android.util.Log; diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabFragment.kt similarity index 69% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabFragment.kt index 60c30cf9..ec47b5b6 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabFragment.kt @@ -2,21 +2,24 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-3. */ -package pl.szczodrzynski.edziennik.ui.modules.debug +package pl.szczodrzynski.edziennik.ui.debug import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.MainActivity -import pl.szczodrzynski.edziennik.addOnPageSelectedListener import pl.szczodrzynski.edziennik.databinding.TemplateFragmentBinding -import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.FragmentLazyPagerAdapter +import pl.szczodrzynski.edziennik.ext.addOnPageSelectedListener +import pl.szczodrzynski.edziennik.ui.base.lazypager.FragmentLazyPagerAdapter +import pl.szczodrzynski.edziennik.ui.login.LoginActivity +import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoTouch import kotlin.coroutines.CoroutineContext class LabFragment : Fragment(), CoroutineScope { @@ -26,7 +29,7 @@ class LabFragment : Fragment(), CoroutineScope { } private lateinit var app: App - private lateinit var activity: MainActivity + private lateinit var activity: AppCompatActivity private lateinit var b: TemplateFragmentBinding private val job: Job = Job() @@ -36,11 +39,15 @@ class LabFragment : Fragment(), CoroutineScope { // local/private variables go here override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - activity = (getActivity() as MainActivity?) ?: return null + activity = (getActivity() as AppCompatActivity?) ?: return null context ?: return null app = activity.application as App b = TemplateFragmentBinding.inflate(inflater) - b.refreshLayout.setParent(activity.swipeRefreshLayout) + when (activity) { + is MainActivity -> b.refreshLayout.setParent((activity as MainActivity).swipeRefreshLayout) + is LoginActivity -> b.refreshLayout.setParent((activity as LoginActivity).swipeRefreshLayout) + } + b.refreshLayout.isEnabled = false return b.root } @@ -48,7 +55,7 @@ class LabFragment : Fragment(), CoroutineScope { if (!isAdded) return val pagerAdapter = FragmentLazyPagerAdapter( - fragmentManager ?: return, + parentFragmentManager, b.refreshLayout, listOf( LabPageFragment() to "click me", diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabJsonAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabJsonAdapter.kt similarity index 90% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabJsonAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabJsonAdapter.kt index a4a332c5..8d605885 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabJsonAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabJsonAdapter.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-5-12. */ -package pl.szczodrzynski.edziennik.ui.modules.debug +package pl.szczodrzynski.edziennik.ui.debug import android.animation.ObjectAnimator import android.view.LayoutInflater @@ -20,15 +20,15 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.ui.modules.debug.models.LabJsonArray -import pl.szczodrzynski.edziennik.ui.modules.debug.models.LabJsonElement -import pl.szczodrzynski.edziennik.ui.modules.debug.models.LabJsonObject -import pl.szczodrzynski.edziennik.ui.modules.debug.viewholder.JsonArrayViewHolder -import pl.szczodrzynski.edziennik.ui.modules.debug.viewholder.JsonElementViewHolder -import pl.szczodrzynski.edziennik.ui.modules.debug.viewholder.JsonObjectViewHolder -import pl.szczodrzynski.edziennik.ui.modules.debug.viewholder.JsonSubObjectViewHolder -import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel -import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder +import pl.szczodrzynski.edziennik.ui.debug.models.LabJsonArray +import pl.szczodrzynski.edziennik.ui.debug.models.LabJsonElement +import pl.szczodrzynski.edziennik.ui.debug.models.LabJsonObject +import pl.szczodrzynski.edziennik.ui.debug.viewholder.JsonArrayViewHolder +import pl.szczodrzynski.edziennik.ui.debug.viewholder.JsonElementViewHolder +import pl.szczodrzynski.edziennik.ui.debug.viewholder.JsonObjectViewHolder +import pl.szczodrzynski.edziennik.ui.debug.viewholder.JsonSubObjectViewHolder +import pl.szczodrzynski.edziennik.ui.grades.models.ExpandableItemModel +import pl.szczodrzynski.edziennik.ui.grades.viewholder.BindableViewHolder import kotlin.coroutines.CoroutineContext class LabJsonAdapter( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabPageFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabPageFragment.kt new file mode 100644 index 00000000..2e40eefc --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabPageFragment.kt @@ -0,0 +1,220 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-5-12. + */ + +package pl.szczodrzynski.edziennik.ui.debug + +import android.os.Bundle +import android.os.Process +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.core.widget.doAfterTextChanged +import androidx.sqlite.db.SimpleSQLiteQuery +import com.chuckerteam.chucker.api.Chucker +import com.chuckerteam.chucker.api.Chucker.SCREEN_HTTP +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +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 +import pl.szczodrzynski.edziennik.ui.dialogs.ProfileRemoveDialog +import pl.szczodrzynski.edziennik.utils.TextInputDropDown +import pl.szczodrzynski.fslogin.decode +import kotlin.coroutines.CoroutineContext +import kotlin.system.exitProcess + +class LabPageFragment : LazyFragment(), CoroutineScope { + companion object { + private const val TAG = "LabPageFragment" + } + + private lateinit var app: App + private lateinit var activity: AppCompatActivity + private lateinit var b: LabFragmentBinding + + private val job: Job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + // local/private variables go here + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + activity = (getActivity() as AppCompatActivity?) ?: return null + context ?: return null + app = activity.application as App + b = LabFragmentBinding.inflate(inflater) + return b.root + } + + override fun onPageCreated(): Boolean { + b.app = app + + if (app.profile.id == 0) { + b.last10unseen.isVisible = false + b.fullSync.isVisible = false + b.clearProfile.isVisible = false + b.clearEndpointTimers.isVisible = false + b.rodo.isVisible = false + b.removeHomework.isVisible = false + b.resetEventTypes.isVisible = false + b.unarchive.isVisible = false + b.profile.isVisible = false + } + + b.last10unseen.onClick { + launch(Dispatchers.Default) { + val events = app.db.eventDao().getAllNow(App.profileId) + val ids = events.sortedBy { it.date }.filter { it.isHomework }.takeLast(10) + ids.forEach { + app.db.metadataDao().setSeen(App.profileId, it, false) + } + } + } + + b.rodo.onClick { + app.db.teacherDao().query(SimpleSQLiteQuery("UPDATE teachers SET teacherSurname = \"\" WHERE profileId = ${App.profileId}")) + } + + b.fullSync.onClick { + app.profile.empty = true + app.profileSave() + } + + b.clearProfile.onClick { + ProfileRemoveDialog(activity, App.profileId, "FAKE", noProfileRemoval = true).show() + } + + b.clearEndpointTimers.onClick { + app.db.endpointTimerDao().clear(app.profileId) + } + + b.removeHomework.onClick { + 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 + App.enableChucker = isChecked + MaterialAlertDialogBuilder(activity) + .setTitle("Restart") + .setMessage("Wymagany restart aplikacji") + .setPositiveButton(R.string.ok) { _, _ -> + Process.killProcess(Process.myPid()) + Runtime.getRuntime().exit(0) + exitProcess(0) + } + .setCancelable(false) + .show() + } + + if (App.enableChucker) { + b.openChucker.isVisible = true + b.openChucker.onClick { + startActivity(Chucker.getLaunchIntent(activity, SCREEN_HTTP)) + } + } + + b.disableDebug.onClick { + app.config.devMode = false + App.devMode = false + MaterialAlertDialogBuilder(activity) + .setTitle("Restart") + .setMessage("Wymagany restart aplikacji") + .setPositiveButton(R.string.ok) { _, _ -> + Process.killProcess(Process.myPid()) + Runtime.getRuntime().exit(0) + exitProcess(0) + } + .setCancelable(false) + .show() + } + + b.unarchive.onClick { + app.profile.archived = false + app.profile.archiveId = null + app.profileSave() + } + + b.resetCert.onClick { + app.config.apiInvalidCert = null + } + + b.apiKey.setText(app.config.apiKeyCustom ?: SignatureInterceptor.API_KEY) + b.apiKey.doAfterTextChanged { + it?.toString()?.let { key -> + if (key == SignatureInterceptor.API_KEY) + app.config.apiKeyCustom = null + else + app.config.apiKeyCustom = key.takeValue()?.trim() + } + } + + b.rebuildConfig.onClick { + App.config = Config(App.db) + } + + val profiles = app.db.profileDao().allNow + b.profile.clear() + b.profile += profiles.map { TextInputDropDown.Item(it.id.toLong(), "${it.id} ${it.name} archived ${it.archived}", tag = it) } + b.profile.select(app.profileId.toLong()) + b.profile.setOnChangeListener { + if (activity is MainActivity) + (activity as MainActivity).navigate(profileId = it.id.toInt()) + return@setOnChangeListener true + } + + b.clearCookies.onClick { + app.cookieJar.clearAllDomains() + } + + val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity) + startCoroutineTimer(500L, 300L) { + 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 + } + + return true + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabProfileFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabProfileFragment.kt similarity index 73% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabProfileFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabProfileFragment.kt index ee3ea2c6..f4267a25 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabProfileFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/LabProfileFragment.kt @@ -2,24 +2,31 @@ * Copyright (c) Kuba Szczodrzyński 2020-5-12. */ -package pl.szczodrzynski.edziennik.ui.modules.debug +package pl.szczodrzynski.edziennik.ui.debug import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.gson.* +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import com.google.gson.JsonPrimitive import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.databinding.TemplateListPageFragmentBinding -import pl.szczodrzynski.edziennik.ui.dialogs.input -import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.ui.base.lazypager.LazyFragment +import pl.szczodrzynski.edziennik.ui.login.LoginActivity import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import kotlin.coroutines.CoroutineContext @@ -29,7 +36,7 @@ class LabProfileFragment : LazyFragment(), CoroutineScope { } private lateinit var app: App - private lateinit var activity: MainActivity + private lateinit var activity: AppCompatActivity private lateinit var b: TemplateListPageFragmentBinding private val job: Job = Job() @@ -43,7 +50,7 @@ class LabProfileFragment : LazyFragment(), CoroutineScope { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - activity = (getActivity() as MainActivity?) ?: return null + activity = (getActivity() as AppCompatActivity?) ?: return null context ?: return null app = activity.application as App b = TemplateListPageFragmentBinding.inflate(inflater) @@ -55,14 +62,16 @@ class LabProfileFragment : LazyFragment(), CoroutineScope { try { var parent: Any = Unit var obj: Any = Unit - var objName: String = "" + var objName = "" item.key.split(":").forEach { el -> parent = obj obj = when (el) { - "App.profile" -> app.profile - "App.profile.studentData" -> app.profile.studentData - "App.profile.loginStore" -> loginStore?.data ?: JsonObject() - "App.config" -> app.config.values + "Profile" -> app.profile + "Profile / studentData" -> app.profile.studentData + "LoginStore" -> loginStore + "LoginStore / data" -> loginStore?.data ?: JsonObject() + "Config" -> app.config.values + "Config (profile)" -> app.profile.config.values else -> when (obj) { is JsonObject -> (obj as JsonObject).get(el) is JsonArray -> (obj as JsonArray).get(el.toInt()) @@ -85,6 +94,7 @@ class LabProfileFragment : LazyFragment(), CoroutineScope { objVal.isBoolean -> objVal.asBoolean.toString() else -> objVal.asString } + is Enum<*> -> objVal.toInt().toString() else -> objVal.toString() } @@ -119,6 +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) else -> input } field.set(parent, newVal) @@ -126,9 +137,8 @@ class LabProfileFragment : LazyFragment(), CoroutineScope { } when (item.key.substringBefore(":")) { - "App.profile" -> app.profileSave() - "App.profile.studentData" -> app.profileSave() - "App.profile.loginStore" -> app.db.loginStoreDao().add(loginStore) + "Profile", "Profile / studentData" -> app.profileSave() + "LoginStore", "LoginStore / data" -> app.db.loginStoreDao().add(loginStore) } showJson() @@ -140,7 +150,10 @@ class LabProfileFragment : LazyFragment(), CoroutineScope { .show() } catch (e: Exception) { - activity.error(ApiError.fromThrowable(TAG, e)) + if (activity is MainActivity) + (activity as MainActivity).error(ApiError.fromThrowable(TAG, e)) + if (activity is LoginActivity) + (activity as LoginActivity).error(ApiError.fromThrowable(TAG, e)) } }) @@ -163,10 +176,12 @@ class LabProfileFragment : LazyFragment(), CoroutineScope { private fun showJson() { val json = JsonObject().also { json -> - json.add("App.profile", app.gson.toJsonTree(app.profile)) - json.add("App.profile.studentData", app.profile.studentData) - json.add("App.profile.loginStore", loginStore?.data ?: JsonObject()) - json.add("App.config", JsonParser().parse(app.gson.toJson(app.config.values))) + json.add("Profile", app.gson.toJsonTree(app.profile)) + json.add("Profile / studentData", app.profile.studentData) + json.add("LoginStore", app.gson.toJsonTree(loginStore)) + json.add("LoginStore / data", loginStore?.data ?: JsonObject()) + json.add("Config", JsonParser.parseString(app.gson.toJson(app.config.values.toSortedMap()))) + json.add("Config (profile)", JsonParser.parseString(app.gson.toJson(app.profile.config.values.toSortedMap()))) } adapter.items = LabJsonAdapter.expand(json, 0) adapter.notifyDataSetChanged() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/models/LabJsonArray.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/models/LabJsonArray.kt similarity index 68% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/models/LabJsonArray.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/models/LabJsonArray.kt index a606e238..e4cbbf65 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/models/LabJsonArray.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/models/LabJsonArray.kt @@ -2,11 +2,11 @@ * Copyright (c) Kuba Szczodrzyński 2020-5-12. */ -package pl.szczodrzynski.edziennik.ui.modules.debug.models +package pl.szczodrzynski.edziennik.ui.debug.models import com.google.gson.JsonArray import com.google.gson.JsonElement -import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel +import pl.szczodrzynski.edziennik.ui.grades.models.ExpandableItemModel data class LabJsonArray( val key: String, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/models/LabJsonElement.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/models/LabJsonElement.kt similarity index 77% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/models/LabJsonElement.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/models/LabJsonElement.kt index e0e0e686..3592168c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/models/LabJsonElement.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/models/LabJsonElement.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-5-12. */ -package pl.szczodrzynski.edziennik.ui.modules.debug.models +package pl.szczodrzynski.edziennik.ui.debug.models import com.google.gson.JsonElement diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/models/LabJsonObject.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/models/LabJsonObject.kt similarity index 71% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/models/LabJsonObject.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/models/LabJsonObject.kt index 43a69201..e67e609e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/models/LabJsonObject.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/models/LabJsonObject.kt @@ -2,11 +2,11 @@ * Copyright (c) Kuba Szczodrzyński 2020-5-12. */ -package pl.szczodrzynski.edziennik.ui.modules.debug.models +package pl.szczodrzynski.edziennik.ui.debug.models import com.google.gson.JsonElement import com.google.gson.JsonObject -import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel +import pl.szczodrzynski.edziennik.ui.grades.models.ExpandableItemModel data class LabJsonObject( val key: String, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/viewholder/JsonArrayViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/viewholder/JsonArrayViewHolder.kt similarity index 73% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/viewholder/JsonArrayViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/viewholder/JsonArrayViewHolder.kt index 56645c28..967ab5cf 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/viewholder/JsonArrayViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/viewholder/JsonArrayViewHolder.kt @@ -2,23 +2,21 @@ * Copyright (c) Kuba Szczodrzyński 2020-5-12. */ -package pl.szczodrzynski.edziennik.ui.modules.debug.viewholder +package pl.szczodrzynski.edziennik.ui.debug.viewholder import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ContextThemeWrapper import androidx.core.view.isInvisible import androidx.recyclerview.widget.RecyclerView import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.databinding.LabItemObjectBinding -import pl.szczodrzynski.edziennik.dp -import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter -import pl.szczodrzynski.edziennik.ui.modules.debug.LabJsonAdapter -import pl.szczodrzynski.edziennik.ui.modules.debug.models.LabJsonArray -import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder -import pl.szczodrzynski.edziennik.utils.Themes +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ui.attendance.AttendanceAdapter +import pl.szczodrzynski.edziennik.ui.debug.LabJsonAdapter +import pl.szczodrzynski.edziennik.ui.debug.models.LabJsonArray +import pl.szczodrzynski.edziennik.ui.grades.viewholder.BindableViewHolder class JsonArrayViewHolder( inflater: LayoutInflater, @@ -31,8 +29,6 @@ class JsonArrayViewHolder( @SuppressLint("SetTextI18n") override fun onBind(activity: AppCompatActivity, app: App, item: LabJsonArray, position: Int, adapter: LabJsonAdapter) { - val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme) - b.root.setPadding(item.level * 8.dp + 8.dp, 8.dp, 8.dp, 8.dp) b.type.text = "Array" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/viewholder/JsonElementViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/viewholder/JsonElementViewHolder.kt similarity index 77% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/viewholder/JsonElementViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/viewholder/JsonElementViewHolder.kt index 20b1c2c8..0e80d109 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/viewholder/JsonElementViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/viewholder/JsonElementViewHolder.kt @@ -2,22 +2,21 @@ * Copyright (c) Kuba Szczodrzyński 2020-5-12. */ -package pl.szczodrzynski.edziennik.ui.modules.debug.viewholder +package pl.szczodrzynski.edziennik.ui.debug.viewholder import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ContextThemeWrapper import androidx.recyclerview.widget.RecyclerView import com.google.gson.JsonNull import com.google.gson.JsonPrimitive -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.databinding.LabItemElementBinding -import pl.szczodrzynski.edziennik.ui.modules.debug.LabJsonAdapter -import pl.szczodrzynski.edziennik.ui.modules.debug.models.LabJsonElement -import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder -import pl.szczodrzynski.edziennik.utils.Themes +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.ui.debug.LabJsonAdapter +import pl.szczodrzynski.edziennik.ui.debug.models.LabJsonElement +import pl.szczodrzynski.edziennik.ui.grades.viewholder.BindableViewHolder class JsonElementViewHolder( inflater: LayoutInflater, @@ -30,8 +29,6 @@ class JsonElementViewHolder( @SuppressLint("SetTextI18n") override fun onBind(activity: AppCompatActivity, app: App, item: LabJsonElement, position: Int, adapter: LabJsonAdapter) { - val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme) - b.root.setPadding(item.level * 8.dp + 8.dp, 8.dp, 8.dp, 8.dp) b.type.text = when (item.jsonElement) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/viewholder/JsonObjectViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/viewholder/JsonObjectViewHolder.kt similarity index 73% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/viewholder/JsonObjectViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/viewholder/JsonObjectViewHolder.kt index b4de8efe..847f19d5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/viewholder/JsonObjectViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/viewholder/JsonObjectViewHolder.kt @@ -2,23 +2,21 @@ * Copyright (c) Kuba Szczodrzyński 2020-5-12. */ -package pl.szczodrzynski.edziennik.ui.modules.debug.viewholder +package pl.szczodrzynski.edziennik.ui.debug.viewholder import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ContextThemeWrapper import androidx.core.view.isInvisible import androidx.recyclerview.widget.RecyclerView import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.databinding.LabItemObjectBinding -import pl.szczodrzynski.edziennik.dp -import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter -import pl.szczodrzynski.edziennik.ui.modules.debug.LabJsonAdapter -import pl.szczodrzynski.edziennik.ui.modules.debug.models.LabJsonObject -import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder -import pl.szczodrzynski.edziennik.utils.Themes +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ui.attendance.AttendanceAdapter +import pl.szczodrzynski.edziennik.ui.debug.LabJsonAdapter +import pl.szczodrzynski.edziennik.ui.debug.models.LabJsonObject +import pl.szczodrzynski.edziennik.ui.grades.viewholder.BindableViewHolder class JsonObjectViewHolder( inflater: LayoutInflater, @@ -31,8 +29,6 @@ class JsonObjectViewHolder( @SuppressLint("SetTextI18n") override fun onBind(activity: AppCompatActivity, app: App, item: LabJsonObject, position: Int, adapter: LabJsonAdapter) { - val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme) - b.root.setPadding(item.level * 8.dp + 8.dp, 8.dp, 8.dp, 8.dp) b.type.text = "Object" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/viewholder/JsonSubObjectViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/viewholder/JsonSubObjectViewHolder.kt similarity index 67% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/viewholder/JsonSubObjectViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/viewholder/JsonSubObjectViewHolder.kt index dbc71ca2..3b9e2699 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/viewholder/JsonSubObjectViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/debug/viewholder/JsonSubObjectViewHolder.kt @@ -2,22 +2,20 @@ * Copyright (c) Kuba Szczodrzyński 2021-2-26. */ -package pl.szczodrzynski.edziennik.ui.modules.debug.viewholder +package pl.szczodrzynski.edziennik.ui.debug.viewholder import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.view.ContextThemeWrapper import androidx.recyclerview.widget.RecyclerView import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.databinding.LabItemSubObjectBinding -import pl.szczodrzynski.edziennik.dp -import pl.szczodrzynski.edziennik.ui.modules.attendance.AttendanceAdapter -import pl.szczodrzynski.edziennik.ui.modules.debug.LabJsonAdapter -import pl.szczodrzynski.edziennik.ui.modules.debug.models.LabJsonObject -import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder -import pl.szczodrzynski.edziennik.utils.Themes +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ui.attendance.AttendanceAdapter +import pl.szczodrzynski.edziennik.ui.debug.LabJsonAdapter +import pl.szczodrzynski.edziennik.ui.debug.models.LabJsonObject +import pl.szczodrzynski.edziennik.ui.grades.viewholder.BindableViewHolder class JsonSubObjectViewHolder( inflater: LayoutInflater, @@ -30,8 +28,6 @@ class JsonSubObjectViewHolder( @SuppressLint("SetTextI18n") override fun onBind(activity: AppCompatActivity, app: App, item: LabJsonObject, position: Int, adapter: LabJsonAdapter) { - val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme) - b.root.setPadding(item.level * 8.dp + 8.dp, 8.dp, 8.dp, 8.dp) b.type.text = "Object" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/BellSyncDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/BellSyncDialog.kt new file mode 100644 index 00000000..abf48b45 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/BellSyncDialog.kt @@ -0,0 +1,77 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-20 + */ + +package pl.szczodrzynski.edziennik.ui.dialogs + +import android.view.LayoutInflater +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.Job +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.DialogBellSyncBinding +import pl.szczodrzynski.edziennik.ext.resolveDrawable +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer +import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog +import pl.szczodrzynski.edziennik.utils.models.Time + +class BellSyncDialog( + activity: AppCompatActivity, + private val bellTime: Time, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BindingDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "BellSyncDialog" + + override fun getTitleRes() = R.string.bell_sync_title + override fun inflate(layoutInflater: LayoutInflater) = + DialogBellSyncBinding.inflate(layoutInflater) + + override fun getNeutralButtonText() = R.string.cancel + + private var counterJob: Job? = null + + private val actualBellDiff: Pair + get() { + val now = Time.getNow() + val bellDiff = Time.diff(now, bellTime) + val multiplier = if (bellTime > now) -1 else 1 + return Pair(bellDiff, multiplier) + } + + override suspend fun onShow() { + b.bellSyncButton.setOnClickListener { + val (bellDiff, multiplier) = actualBellDiff + val bellDiffText = (if (multiplier == -1) '-' else '+') + bellDiff.stringHMS + app.config.timetable.bellSyncDiff = bellDiff + app.config.timetable.bellSyncMultiplier = multiplier + + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.bell_sync_title) + .setMessage(app.getString(R.string.bell_sync_results, bellDiffText)) + .setPositiveButton(R.string.ok) { resultsDialog, _ -> + resultsDialog.dismiss() + dialog.dismiss() + if (activity is MainActivity) activity.reloadTarget() + } + .show() + } + + if (Time.diff(Time.getNow(), bellTime) > Time(2, 0, 0)) { // Easter egg ^^ + b.bellSyncButton.setImageDrawable(R.drawable.ic_bell_wtf.resolveDrawable(app)) // wtf + } + + counterJob = startCoroutineTimer(repeatMillis = 500) { + val (bellDiff, multiplier) = actualBellDiff + val bellDiffText = (if (multiplier == -1) '-' else '+') + bellDiff.stringHMS + b.bellSyncHowto.text = + app.getString(R.string.bell_sync_howto, bellTime.stringHM, bellDiffText) + } + } + + override fun onDismiss() { + counterJob?.cancel() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/BellSyncTimeChooseDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/BellSyncTimeChooseDialog.kt new file mode 100644 index 00000000..6e11a50d --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/BellSyncTimeChooseDialog.kt @@ -0,0 +1,151 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-12-20 + */ + +package pl.szczodrzynski.edziennik.ui.dialogs + +import android.view.LayoutInflater +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.Lesson +import pl.szczodrzynski.edziennik.databinding.DialogBellSyncTimeChooseBinding +import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog +import pl.szczodrzynski.edziennik.utils.TextInputDropDown +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time + +class BellSyncTimeChooseDialog( + activity: AppCompatActivity, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BindingDialog(activity, onShowListener, onDismissListener) { + companion object { + private const val MAX_DIFF_MINUTES = 10 + } + + override val TAG = "BellSyncTimeChooseDialog" + + override fun getTitleRes() = R.string.bell_sync_title + override fun inflate(layoutInflater: LayoutInflater) = + DialogBellSyncTimeChooseBinding.inflate(layoutInflater) + + override fun getPositiveButtonText() = R.string.ok + override fun getNeutralButtonText() = R.string.reset + override fun getNegativeButtonText() = R.string.cancel + + override suspend fun onShow() = Unit + + private val today = Date.getToday() + private val selectedTime: Time? + get() = b.timeDropdown.selected?.tag as Time? + + override suspend fun onPositiveClick(): Boolean { + selectedTime?.let { + BellSyncDialog(activity, it).show() + } + return DISMISS + } + + override suspend fun onNeutralClick(): Boolean { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.bell_sync_title) + .setMessage(R.string.bell_sync_reset_confirm) + .setPositiveButton(R.string.yes) { dialog, _ -> + app.config.timetable.bellSyncDiff = null + app.config.timetable.bellSyncMultiplier = 0 + + dialog.dismiss() + reload() + if (activity is MainActivity) + activity.reloadTarget() + } + .setNegativeButton(R.string.no, null) + .show() + return NO_DISMISS + } + + override suspend fun onBeforeShow(): Boolean { + b.bellSyncHowto.text = app.getString(R.string.bell_sync_choose_howto) + + app.config.timetable.bellSyncDiff?.let { bellDiff -> + val multiplier = app.config.timetable.bellSyncMultiplier + val bellDiffText = (if (multiplier == -1) '-' else '+') + bellDiff.stringHMS + b.bellSyncHowto.text = app.getString(R.string.concat_2_strings, + app.getString(R.string.bell_sync_choose_howto), + app.getString(R.string.bell_sync_current_dialog, bellDiffText) + ) + } + + return loadTimeList() + } + + private fun checkForLessons(timeList: List
  • ", "
  • - ")) - } - - textView.movementMethod = BetterLinkMovementMethod.getInstance() - - val scrollView = ScrollView(activity) - scrollView.addView(textView) - - dialog = MaterialAlertDialogBuilder(activity) - .setTitle(R.string.whats_new) - .setView(scrollView) - .setPositiveButton(R.string.close) { dialog, _ -> - dialog.dismiss() - } - .setOnDismissListener { - onDismissListener?.invoke(TAG) - } - .show() - }} -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/day/DayDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/day/DayDialog.kt deleted file mode 100644 index e5b7768d..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/day/DayDialog.kt +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2019-12-16. - */ - -package pl.szczodrzynski.edziennik.ui.dialogs.day - -import android.view.View -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.Observer -import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.* -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.db.entity.Lesson -import pl.szczodrzynski.edziennik.databinding.DialogDayBinding -import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog -import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter -import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog -import pl.szczodrzynski.edziennik.ui.dialogs.lessonchange.LessonChangeDialog -import pl.szczodrzynski.edziennik.ui.dialogs.teacherabsence.TeacherAbsenceDialog -import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration -import pl.szczodrzynski.edziennik.utils.models.Date -import pl.szczodrzynski.edziennik.utils.models.Time -import pl.szczodrzynski.edziennik.utils.models.Week -import kotlin.coroutines.CoroutineContext - -class DayDialog( - val activity: AppCompatActivity, - val profileId: Int, - val date: Date, - val onShowListener: ((tag: String) -> Unit)? = null, - val onDismissListener: ((tag: String) -> Unit)? = null -) : CoroutineScope { - companion object { - private const val TAG = "DayDialog" - } - - private lateinit var app: App - private lateinit var b: DialogDayBinding - private lateinit var dialog: AlertDialog - - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - private lateinit var adapter: EventListAdapter - - init { run { - if (activity.isFinishing) - return@run - onShowListener?.invoke(TAG) - app = activity.applicationContext as App - b = DialogDayBinding.inflate(activity.layoutInflater) - dialog = MaterialAlertDialogBuilder(activity) - .setView(b.root) - .setPositiveButton(R.string.close) { dialog, _ -> - dialog.dismiss() - } - .setNeutralButton(R.string.add, null) - .setOnDismissListener { - onDismissListener?.invoke(TAG) - } - .show() - - dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.onClick { - EventManualDialog( - activity, - profileId, - defaultDate = date, - onShowListener = onShowListener, - onDismissListener = onDismissListener - ) - } - - update() - }} - - private fun update() { launch { - b.dayDate.setText( - R.string.dialog_day_date_format, - Week.getFullDayName(date.weekDay), - date.formattedString - ) - - val lessons = withContext(Dispatchers.Default) { - app.db.timetableDao().getAllForDateNow(profileId, date) - }.filter { it.type != Lesson.TYPE_NO_LESSONS } - - if (lessons.isNotEmpty()) { run { - val startTime = lessons.first().startTime ?: return@run - val endTime = lessons.last().endTime ?: return@run - val diff = Time.diff(startTime, endTime) - - b.lessonsInfo.setText( - R.string.dialog_day_lessons_info, - startTime.stringHM, - endTime.stringHM, - lessons.size.toString(), - diff.hour.toString(), - diff.minute.toString() - ) - - b.lessonsInfo.visibility = View.VISIBLE - }} - - val lessonChanges = withContext(Dispatchers.Default) { - app.db.timetableDao().getChangesForDateNow(profileId, date) - } - - lessonChanges.ifNotEmpty { - b.lessonChangeContainer.root.visibility = View.VISIBLE - b.lessonChangeContainer.lessonChangeCount.text = it.size.toString() - - b.lessonChangeLayout.onClick { - LessonChangeDialog( - activity, - profileId, - date, - onShowListener = onShowListener, - onDismissListener = onDismissListener - ) - } - } - - val teacherAbsences = withContext(Dispatchers.Default) { - app.db.teacherAbsenceDao().getAllByDateNow(profileId, date) - } - - teacherAbsences.ifNotEmpty { - b.teacherAbsenceContainer.root.visibility = View.VISIBLE - b.teacherAbsenceContainer.teacherAbsenceCount.text = it.size.toString() - - b.teacherAbsenceLayout.onClick { - TeacherAbsenceDialog( - activity, - profileId, - date, - onShowListener = onShowListener, - onDismissListener = onDismissListener - ) - } - } - - adapter = EventListAdapter( - activity, - showWeekDay = false, - showDate = false, - showType = true, - showTime = true, - showSubject = true, - markAsSeen = true, - onItemClick = { - EventDetailsDialog( - activity, - it, - onShowListener = onShowListener, - onDismissListener = onDismissListener - ) - }, - onEventEditClick = { - EventManualDialog( - activity, - it.profileId, - editingEvent = it, - onShowListener = onShowListener, - onDismissListener = onDismissListener - ) - } - ) - - app.db.eventDao().getAllByDate(profileId, date).observe(activity, Observer { events -> - adapter.items = events - if (b.eventsView.adapter == null) { - b.eventsView.adapter = adapter - b.eventsView.apply { - isNestedScrollingEnabled = false - setHasFixedSize(true) - layoutManager = LinearLayoutManager(context) - addItemDecoration(SimpleDividerItemDecoration(context)) - } - } - adapter.notifyDataSetChanged() - - if (events != null && events.isNotEmpty()) { - b.eventsView.visibility = View.VISIBLE - b.eventsNoData.visibility = View.GONE - } else { - b.eventsView.visibility = View.GONE - b.eventsNoData.visibility = View.VISIBLE - } - }) - }} -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt deleted file mode 100644 index 34cc6a2d..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventListAdapter.kt +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2019-11-30 - */ - -package pl.szczodrzynski.edziennik.ui.dialogs.event - -import android.content.Context -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.db.full.EventFull -import pl.szczodrzynski.edziennik.databinding.EventListItemBinding -import pl.szczodrzynski.edziennik.utils.models.Date -import pl.szczodrzynski.edziennik.utils.models.Week -import kotlin.coroutines.CoroutineContext - -class EventListAdapter( - val context: Context, - val simpleMode: Boolean = false, - val showWeekDay: Boolean = false, - val showDate: Boolean = false, - val showType: Boolean = true, - val showTime: Boolean = true, - val showSubject: Boolean = true, - val markAsSeen: Boolean = true, - val onItemClick: ((event: EventFull) -> Unit)? = null, - val onEventEditClick: ((event: EventFull) -> Unit)? = null -) : RecyclerView.Adapter(), CoroutineScope { - - private val app = context.applicationContext as App - private val manager = app.eventManager - - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - var items = listOf() - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - val inflater = LayoutInflater.from(parent.context) - val view = EventListItemBinding.inflate(inflater, parent, false) - return ViewHolder(view) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val event = items[position] - val b = holder.b - val manager = app.eventManager - - b.root.onClick { - onItemClick?.invoke(event) - if (!event.seen) { - manager.markAsSeen(event) - } - if (event.showAsUnseen == true) { - event.showAsUnseen = false - notifyItemChanged(event) - } - } - - val bullet = " • " - - b.simpleMode = simpleMode - - b.topic.text = event.topic - b.topic.maxLines = if (simpleMode) 2 else 3 - - b.details.text = mutableListOf( - if (showWeekDay) Week.getFullDayName(event.date.weekDay) else null, - if (showDate) event.date.getRelativeString(context, 7) ?: event.date.formattedStringShort else null, - if (showType) event.typeName else null, - if (showTime) event.time?.stringHM ?: app.getString(R.string.event_all_day) else null, - if (showSubject) event.subjectLongName else null - ).concat(bullet) - - b.addedBy.setText( - when (event.sharedBy) { - null -> when { - event.addedManually -> R.string.event_list_added_by_self_format - event.teacherName == null -> R.string.event_list_added_by_unknown_format - else -> R.string.event_list_added_by_format - } - "self" -> R.string.event_list_shared_by_self_format - else -> R.string.event_list_shared_by_format - }, - Date.fromMillis(event.addedDate).formattedString, - event.sharedByName ?: event.teacherName ?: "", - event.teamName?.let { bullet+it } ?: "" - ) - - b.typeColor.background?.setTintColor(event.eventColor) - b.typeColor.isVisible = showType - - b.editButton.isVisible = !simpleMode && event.addedManually && !event.isDone - b.editButton.onClick { - onEventEditClick?.invoke(event) - } - b.editButton.attachToastHint(R.string.hint_edit_event) - - b.isDone.isVisible = event.isDone - - if (event.showAsUnseen == null) - event.showAsUnseen = !event.seen - - b.unread.isVisible = event.showAsUnseen == true - if (markAsSeen && !event.seen) { - manager.markAsSeen(event) - } - } - - private fun notifyItemChanged(model: Any) { - startCoroutineTimer(1000L, 0L) { - val index = items.indexOf(model) - if (index != -1) - notifyItemChanged(index) - } - } - - override fun getItemCount() = items.size - - class ViewHolder(val b: EventListItemBinding) : RecyclerView.ViewHolder(b.root) -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/grade/GradeDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/grade/GradeDetailsDialog.kt deleted file mode 100644 index 5b9579ed..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/grade/GradeDetailsDialog.kt +++ /dev/null @@ -1,91 +0,0 @@ -package pl.szczodrzynski.edziennik.ui.dialogs.grade - -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity -import androidx.core.graphics.ColorUtils -import androidx.core.view.isVisible -import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.* -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.db.full.GradeFull -import pl.szczodrzynski.edziennik.databinding.DialogGradeDetailsBinding -import pl.szczodrzynski.edziennik.onClick -import pl.szczodrzynski.edziennik.setTintColor -import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter -import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration -import kotlin.coroutines.CoroutineContext - -class GradeDetailsDialog( - val activity: AppCompatActivity, - val grade: GradeFull, - val onShowListener: ((tag: String) -> Unit)? = null, - val onDismissListener: ((tag: String) -> Unit)? = null -) : CoroutineScope { - companion object { - private const val TAG = "GradeDetailsDialog" - } - - private lateinit var app: App - private lateinit var b: DialogGradeDetailsBinding - private lateinit var dialog: AlertDialog - - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - // local variables go here - - init { run { - if (activity.isFinishing) - return@run - onShowListener?.invoke(TAG) - app = activity.applicationContext as App - b = DialogGradeDetailsBinding.inflate(activity.layoutInflater) - dialog = MaterialAlertDialogBuilder(activity) - .setView(b.root) - .setPositiveButton(R.string.close, null) - .setOnDismissListener { - onDismissListener?.invoke(TAG) - } - .show() - val manager = app.gradesManager - - val gradeColor = manager.getGradeColor(grade) - b.grade = grade - b.weightText = manager.getWeightString(app, grade) - b.commentVisible = false - b.devMode = App.devMode - b.gradeName.setTextColor(if (ColorUtils.calculateLuminance(gradeColor) > 0.3) 0xaa000000.toInt() else 0xccffffff.toInt()) - b.gradeName.background.setTintColor(gradeColor) - - b.gradeValue = if (grade.weight == 0f || grade.value < 0f) -1f else manager.getGradeValue(grade) - - b.customValueDivider.isVisible = manager.plusValue != null || manager.minusValue != null - b.customValueLayout.isVisible = b.customValueDivider.isVisible - b.customValueButton.onClick { - GradesConfigDialog(activity, reloadOnDismiss = true) - } - - launch { - val historyList = withContext(Dispatchers.Default) { - app.db.gradeDao().getByParentIdNow(App.profileId, grade.id) - } - if (historyList.isEmpty()) { - b.historyVisible = false - return@launch - } - b.historyVisible = true - //b.gradeHistoryNest.isNestedScrollingEnabled = false - b.gradeHistoryList.adapter = GradesAdapter(activity, { - GradeDetailsDialog(activity, it) - }).also { it.items = historyList.toMutableList() } - b.gradeHistoryList.apply { - setHasFixedSize(true) - layoutManager = LinearLayoutManager(context) - addItemDecoration(SimpleDividerItemDecoration(context)) - } - } - }} -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/grade/GradesConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/grade/GradesConfigDialog.kt deleted file mode 100644 index 20325447..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/grade/GradesConfigDialog.kt +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2020-1-16 - */ - -package pl.szczodrzynski.edziennik.ui.dialogs.grade - -import android.annotation.SuppressLint -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.isVisible -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import it.sephiroth.android.library.numberpicker.doOnStopTrackingTouch -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.databinding.DialogConfigGradesBinding -import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_DEFAULT -import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED -import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.ORDER_BY_DATE_DESC -import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.ORDER_BY_SUBJECT_ASC -import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_AVG_2_AVG -import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_AVG_2_SEM -import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_SEM_2_AVG -import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_SEM_2_SEM -import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_ALL_GRADES -import java.util.* - -class GradesConfigDialog( - val activity: AppCompatActivity, - private val reloadOnDismiss: Boolean = true, - val onShowListener: ((tag: String) -> Unit)? = null, - val onDismissListener: ((tag: String) -> Unit)? = null -) { - companion object { - const val TAG = "GradesConfigDialog" - } - - private val app by lazy { activity.application as App } - private val config by lazy { app.config.grades } - private val profileConfig by lazy { app.config.getFor(app.profileId).grades } - - private lateinit var b: DialogConfigGradesBinding - private lateinit var dialog: AlertDialog - - init { run { - if (activity.isFinishing) - return@run - b = DialogConfigGradesBinding.inflate(activity.layoutInflater) - onShowListener?.invoke(TAG) - dialog = MaterialAlertDialogBuilder(activity) - .setTitle(R.string.menu_grades_config) - .setView(b.root) - .setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() } - .setOnDismissListener { - saveConfig() - onDismissListener?.invoke(TAG) - if (reloadOnDismiss) (activity as? MainActivity)?.reloadTarget() - } - .create() - initView() - loadConfig() - dialog.show() - }} - - @SuppressLint("SetTextI18n") - private fun loadConfig() { - b.customPlusCheckBox.isChecked = profileConfig.plusValue != null - b.customPlusValue.isVisible = b.customPlusCheckBox.isChecked - b.customMinusCheckBox.isChecked = profileConfig.minusValue != null - b.customMinusValue.isVisible = b.customMinusCheckBox.isChecked - - b.customPlusValue.progress = profileConfig.plusValue ?: 0.5f - b.customMinusValue.progress = profileConfig.minusValue ?: 0.25f - - when (config.orderBy) { - ORDER_BY_DATE_DESC -> b.sortGradesByDateRadio - ORDER_BY_SUBJECT_ASC -> b.sortGradesBySubjectRadio - else -> null - }?.isChecked = true - - when (profileConfig.colorMode) { - COLOR_MODE_DEFAULT -> b.gradeColorFromERegister - COLOR_MODE_WEIGHTED -> b.gradeColorByValue - else -> null - }?.isChecked = true - - when (profileConfig.yearAverageMode) { - YEAR_ALL_GRADES -> b.gradeAverageMode4 - YEAR_1_AVG_2_AVG -> b.gradeAverageMode0 - YEAR_1_SEM_2_AVG -> b.gradeAverageMode1 - YEAR_1_AVG_2_SEM -> b.gradeAverageMode2 - YEAR_1_SEM_2_SEM -> b.gradeAverageMode3 - else -> null - }?.isChecked = true - - b.dontCountGrades.isChecked = profileConfig.dontCountEnabled && profileConfig.dontCountGrades.isNotEmpty() - b.hideImproved.isChecked = profileConfig.hideImproved - b.averageWithoutWeight.isChecked = profileConfig.averageWithoutWeight - - if (profileConfig.dontCountGrades.isEmpty()) { - b.dontCountGradesText.setText("nb, 0, bz, bd") - } - else { - b.dontCountGradesText.setText(profileConfig.dontCountGrades.join(", ")) - } - } - - private fun saveConfig() { - profileConfig.plusValue = if (b.customPlusCheckBox.isChecked) b.customPlusValue.progress else null - profileConfig.minusValue = if (b.customMinusCheckBox.isChecked) b.customMinusValue.progress else null - - b.dontCountGradesText.setText( - b.dontCountGradesText - .text - ?.toString() - ?.toLowerCase(Locale.getDefault()) - ?.replace(", ", ",") - ) - profileConfig.dontCountEnabled = b.dontCountGrades.isChecked - profileConfig.dontCountGrades = b.dontCountGradesText.text - ?.split(",") - ?.map { it.trim() } - ?: listOf() - } - - private fun initView() { - b.customPlusCheckBox.onChange { _, isChecked -> - b.customPlusValue.isVisible = isChecked - } - b.customMinusCheckBox.onChange { _, isChecked -> - b.customMinusValue.isVisible = isChecked - } - - // who the hell named those methods - // THIS SHIT DOES NOT EVEN WORK - b.customPlusValue.doOnStopTrackingTouch { - profileConfig.plusValue = it.progress - } - b.customMinusValue.doOnStopTrackingTouch { - profileConfig.minusValue = it.progress - } - - b.sortGradesByDateRadio.setOnSelectedListener { config.orderBy = ORDER_BY_DATE_DESC } - b.sortGradesBySubjectRadio.setOnSelectedListener { config.orderBy = ORDER_BY_SUBJECT_ASC } - - b.gradeColorFromERegister.setOnSelectedListener { profileConfig.colorMode = COLOR_MODE_DEFAULT } - b.gradeColorByValue.setOnSelectedListener { profileConfig.colorMode = COLOR_MODE_WEIGHTED } - - b.gradeAverageMode4.setOnSelectedListener { profileConfig.yearAverageMode = YEAR_ALL_GRADES } - b.gradeAverageMode0.setOnSelectedListener { profileConfig.yearAverageMode = YEAR_1_AVG_2_AVG } - b.gradeAverageMode1.setOnSelectedListener { profileConfig.yearAverageMode = YEAR_1_SEM_2_AVG } - b.gradeAverageMode2.setOnSelectedListener { profileConfig.yearAverageMode = YEAR_1_AVG_2_SEM } - b.gradeAverageMode3.setOnSelectedListener { profileConfig.yearAverageMode = YEAR_1_SEM_2_SEM } - - b.hideImproved.onChange { _, isChecked -> profileConfig.hideImproved = isChecked } - b.averageWithoutWeight.onChange { _, isChecked -> profileConfig.averageWithoutWeight = isChecked } - - b.averageWithoutWeightHelp.onClick { - MaterialAlertDialogBuilder(activity) - .setTitle(R.string.grades_config_average_without_weight) - .setMessage(R.string.grades_config_average_without_weight_message) - .setPositiveButton(R.string.ok, null) - .show() - } - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/lessonchange/LessonChangeDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/lessonchange/LessonChangeDialog.kt deleted file mode 100644 index 78d40e68..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/lessonchange/LessonChangeDialog.kt +++ /dev/null @@ -1,76 +0,0 @@ -package pl.szczodrzynski.edziennik.ui.dialogs.lessonchange - -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity -import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.* -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.databinding.DialogLessonChangeListBinding -import pl.szczodrzynski.edziennik.ui.dialogs.timetable.LessonDetailsDialog -import pl.szczodrzynski.edziennik.utils.models.Date -import kotlin.coroutines.CoroutineContext - -class LessonChangeDialog( - val activity: AppCompatActivity, - val profileId: Int, - private val defaultDate: Date, - val onShowListener: ((tag: String) -> Unit)? = null, - val onDismissListener: ((tag: String) -> Unit)? = null -) : CoroutineScope { - companion object { - const val TAG = "LessonChangeDialog" - } - - private val app by lazy { activity.application as App } - - private lateinit var job: Job - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - private lateinit var b: DialogLessonChangeListBinding - private lateinit var dialog: AlertDialog - - init { run { - if (activity.isFinishing) - return@run - job = Job() - onShowListener?.invoke(TAG) - b = DialogLessonChangeListBinding.inflate(activity.layoutInflater) - dialog = MaterialAlertDialogBuilder(activity) - .setTitle(defaultDate.formattedString) - .setView(b.root) - .setPositiveButton(R.string.close) { dialog, _ -> dialog.dismiss() } - .setOnDismissListener { - onDismissListener?.invoke(TAG) - } - .create() - loadLessonChanges() - }} - - private fun loadLessonChanges() { launch { - val lessonChanges = withContext(Dispatchers.Default) { - app.db.timetableDao().getChangesForDateNow(profileId, defaultDate) - } - - val adapter = LessonChangeAdapter( - activity, - onItemClick = { - LessonDetailsDialog( - activity, - it, - onShowListener = onShowListener, - onDismissListener = onDismissListener - ) - } - ).apply { - items = lessonChanges - } - - b.lessonChangeView.adapter = adapter - b.lessonChangeView.layoutManager = LinearLayoutManager(activity) - - dialog.show() - }} -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/AgendaConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/AgendaConfigDialog.kt new file mode 100644 index 00000000..5cc4ff3f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/AgendaConfigDialog.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-10. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs.settings + +import android.view.LayoutInflater +import androidx.appcompat.app.AppCompatActivity +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.databinding.DialogConfigAgendaBinding +import pl.szczodrzynski.edziennik.ext.onChange +import pl.szczodrzynski.edziennik.ui.dialogs.base.ConfigDialog + +class AgendaConfigDialog( + activity: AppCompatActivity, + reloadOnDismiss: Boolean = true, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : ConfigDialog( + activity, + reloadOnDismiss, + onShowListener, + onDismissListener, +) { + + override val TAG = "AgendaConfigDialog" + + override fun getTitleRes() = R.string.menu_agenda_config + override fun inflate(layoutInflater: LayoutInflater) = + DialogConfigAgendaBinding.inflate(layoutInflater) + + override suspend fun loadConfig() { + b.config = app.profile.config + b.isAgendaMode = app.profile.config.ui.agendaViewType == Profile.AGENDA_DEFAULT + + var calledFromListener = false + b.eventSharingEnabled.isChecked = app.profile.canShare + b.shareByDefault.isEnabled = app.profile.canShare + b.eventSharingEnabled.onChange { _, isChecked -> + if (calledFromListener) { + calledFromListener = false + return@onChange + } + b.eventSharingEnabled.isChecked = !isChecked + val dialog = RegistrationConfigDialog( + activity, + app.profile, + onChangeListener = { enabled -> + calledFromListener = true + b.eventSharingEnabled.isChecked = enabled + b.shareByDefault.isEnabled = enabled + }, + onShowListener, + onDismissListener, + ) + if (isChecked) + dialog.showEnableDialog() + else + dialog.showDisableDialog() + return@onChange + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/AppLanguageDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/AppLanguageDialog.kt index 6db33e70..751c91de 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/AppLanguageDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/AppLanguageDialog.kt @@ -4,68 +4,40 @@ package pl.szczodrzynski.edziennik.ui.dialogs.settings -import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.* -import kotlin.coroutines.CoroutineContext +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.ui.dialogs.base.BaseDialog class AppLanguageDialog( - val activity: AppCompatActivity, - val onShowListener: ((tag: String) -> Unit)? = null, - val onDismissListener: ((tag: String) -> Unit)? = null -) : CoroutineScope { - companion object { - private const val TAG = "AppLanguageDialog" + activity: AppCompatActivity, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BaseDialog(activity, onShowListener, onDismissListener) { + override val TAG = "AppLanguageDialog" + + override fun getTitleRes() = R.string.app_language_dialog_title + override fun getMessage() = activity.getString(R.string.app_language_dialog_text) + override fun getPositiveButtonText() = R.string.ok + override fun getNegativeButtonText() = R.string.cancel + + override fun getSingleChoiceItems(): Map = mapOf( + R.string.language_system to "", + R.string.language_polish to "pl", + R.string.language_english to "en", + R.string.language_german to "de", + ).mapKeys { (resId, _) -> activity.getString(resId) } + + override fun getDefaultSelectedItem() = app.config.ui.language + + override suspend fun onShow() = Unit + + override suspend fun onPositiveClick(): Boolean { + val language = getSingleSelection() as? String ?: return DISMISS + if (language.isEmpty()) + app.config.ui.language = null + else + app.config.ui.language = language + activity.recreate() + return NO_DISMISS } - - private lateinit var app: App - private lateinit var dialog: AlertDialog - - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - // local variables go here - - init { run { - if (activity.isFinishing) - return@run - onShowListener?.invoke(TAG) - app = activity.applicationContext as App - - val languages = mapOf( - null to R.string.language_system, - "pl" to R.string.language_polish, - "en" to R.string.language_english, - "de" to R.string.language_german - ) - val languageIds = languages.map { it.key } - val languageNames = languages.map { - activity.getString(it.value) - } - - dialog = MaterialAlertDialogBuilder(activity) - .setTitle(R.string.app_language_dialog_title) - //.setMessage(R.string.settings_about_language_dialog_text) - .setSingleChoiceItems( - languageNames.toTypedArray(), - languageIds.indexOf(app.config.ui.language), - null - ) - .setPositiveButton(R.string.ok) { _, _ -> - val which = dialog.listView.checkedItemPosition - - app.config.ui.language = languageIds[which] - activity.recreate() - } - .setNegativeButton(R.string.cancel, null) - .setOnDismissListener { - onDismissListener?.invoke(TAG) - } - .show() - }} } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/AttendanceConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/AttendanceConfigDialog.kt index e7d66ed5..b4734141 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/AttendanceConfigDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/AttendanceConfigDialog.kt @@ -1,75 +1,49 @@ /* - * Copyright (c) Kuba Szczodrzyński 2020-5-4. + * Copyright (c) Kuba Szczodrzyński 2021-10-17. */ package pl.szczodrzynski.edziennik.ui.dialogs.settings -import android.annotation.SuppressLint -import androidx.appcompat.app.AlertDialog +import android.view.LayoutInflater import androidx.appcompat.app.AppCompatActivity -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.databinding.AttendanceConfigDialogBinding -import pl.szczodrzynski.edziennik.onChange +import pl.szczodrzynski.edziennik.ext.onChange +import pl.szczodrzynski.edziennik.ui.dialogs.base.ConfigDialog class AttendanceConfigDialog( - val activity: AppCompatActivity, - private val reloadOnDismiss: Boolean = true, - val onShowListener: ((tag: String) -> Unit)? = null, - val onDismissListener: ((tag: String) -> Unit)? = null + activity: AppCompatActivity, + reloadOnDismiss: Boolean = true, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : ConfigDialog( + activity, + reloadOnDismiss, + onShowListener, + onDismissListener, ) { - companion object { - const val TAG = "GradesConfigDialog" + + override val TAG = "AttendanceConfigDialog" + + override fun getTitleRes() = R.string.menu_attendance_config + override fun inflate(layoutInflater: LayoutInflater) = + AttendanceConfigDialogBinding.inflate(layoutInflater) + + override suspend fun loadConfig() { + b.useSymbols.isChecked = app.profile.config.attendance.useSymbols + b.groupConsecutiveDays.isChecked = app.profile.config.attendance.groupConsecutiveDays + b.showPresenceInMonth.isChecked = app.profile.config.attendance.showPresenceInMonth } - private val app by lazy { activity.application as App } - private val profileConfig by lazy { app.config.getFor(app.profileId).attendance } - - private lateinit var b: AttendanceConfigDialogBinding - private lateinit var dialog: AlertDialog - - init { run { - if (activity.isFinishing) - return@run - b = AttendanceConfigDialogBinding.inflate(activity.layoutInflater) - onShowListener?.invoke(TAG) - dialog = MaterialAlertDialogBuilder(activity) - .setTitle(R.string.menu_attendance_config) - .setView(b.root) - .setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() } - .setOnDismissListener { - saveConfig() - onDismissListener?.invoke(TAG) - if (reloadOnDismiss) (activity as? MainActivity)?.reloadTarget() - } - .create() - initView() - loadConfig() - dialog.show() - }} - - @SuppressLint("SetTextI18n") - private fun loadConfig() { - b.useSymbols.isChecked = profileConfig.useSymbols - b.groupConsecutiveDays.isChecked = profileConfig.groupConsecutiveDays - b.showPresenceInMonth.isChecked = profileConfig.showPresenceInMonth - } - - private fun saveConfig() { - // nothing to do here, yet - } - - private fun initView() { + override fun initView() { b.useSymbols.onChange { _, isChecked -> - profileConfig.useSymbols = isChecked + app.profile.config.attendance.useSymbols = isChecked } b.groupConsecutiveDays.onChange { _, isChecked -> - profileConfig.groupConsecutiveDays = isChecked + app.profile.config.attendance.groupConsecutiveDays = isChecked } b.showPresenceInMonth.onChange { _, isChecked -> - profileConfig.showPresenceInMonth = isChecked + app.profile.config.attendance.showPresenceInMonth = isChecked } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/BellSyncConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/BellSyncConfigDialog.kt new file mode 100644 index 00000000..65539ad4 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/BellSyncConfigDialog.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-3-20. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs.settings + +import android.view.LayoutInflater +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.core.widget.addTextChangedListener +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.DialogEditTextBinding +import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog +import pl.szczodrzynski.edziennik.utils.models.Time + +class BellSyncConfigDialog( + activity: AppCompatActivity, + private val onChangeListener: (() -> Unit)? = null, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BindingDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "BellSyncConfigDialog" + + override fun getTitleRes() = R.string.bell_sync_title + override fun inflate(layoutInflater: LayoutInflater) = + DialogEditTextBinding.inflate(layoutInflater) + + override fun getPositiveButtonText() = R.string.ok + override fun getNeutralButtonText() = R.string.reset + override fun getNegativeButtonText() = R.string.cancel + + private fun parse(input: String): Pair? { + if (input.length < 8) { + return null + } + if (input[2] != ':' || input[5] != ':') { + return null + } + val multiplier = when { + input[0] == '+' -> 1 + input[0] == '-' -> -1 + else -> return null + } + val time = Time.fromH_m_s("0" + input.substring(1)) + + return time to multiplier + } + + override suspend fun onShow() { + b.title.setText(R.string.bell_sync_adjust_content) + b.text1.hint = "±H:MM:SS" + b.text1.setText(app.config.timetable.bellSyncDiff?.let { + (if (app.config.timetable.bellSyncMultiplier == -1) "-" else "+") + it.stringHMS + } ?: "+0:00:00") + b.text1.addTextChangedListener { text -> + val input = text?.toString() + b.textInputLayout.error = + if (input != null && parse(input) == null) + activity.getString(R.string.bell_sync_adjust_error) + else + null + } + } + + override suspend fun onPositiveClick(): Boolean { + val input = b.text1.text?.toString() ?: return NO_DISMISS + val parsed = parse(input) + if (parsed == null) { + Toast.makeText(activity, R.string.bell_sync_adjust_error, Toast.LENGTH_SHORT).show() + return NO_DISMISS + } + + val (time, multiplier) = parsed + app.config.timetable.bellSyncDiff = + if (time.value == 0) + null + else + time + app.config.timetable.bellSyncMultiplier = multiplier + + onChangeListener?.invoke() + return DISMISS + } + + override suspend fun onNeutralClick(): Boolean { + app.config.timetable.bellSyncDiff = null + app.config.timetable.bellSyncMultiplier = 0 + onChangeListener?.invoke() + return DISMISS + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/GradesConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/GradesConfigDialog.kt new file mode 100644 index 00000000..b1c0c86f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/GradesConfigDialog.kt @@ -0,0 +1,165 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2020-1-16 + */ + +package pl.szczodrzynski.edziennik.ui.dialogs.settings + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import it.sephiroth.android.library.numberpicker.doOnStopTrackingTouch +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.DialogConfigGradesBinding +import pl.szczodrzynski.edziennik.ext.join +import pl.szczodrzynski.edziennik.ext.onChange +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ext.setOnSelectedListener +import pl.szczodrzynski.edziennik.ui.dialogs.base.ConfigDialog +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_DEFAULT +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.ORDER_BY_DATE_DESC +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.ORDER_BY_SUBJECT_ASC +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_AVG_2_AVG +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_AVG_2_SEM +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_SEM_2_AVG +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_SEM_2_SEM +import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_ALL_GRADES + +class GradesConfigDialog( + activity: AppCompatActivity, + reloadOnDismiss: Boolean = true, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : ConfigDialog( + activity, + reloadOnDismiss, + onShowListener, + onDismissListener, +) { + + override val TAG = "GradesConfigDialog" + + override fun getTitleRes() = R.string.menu_grades_config + override fun inflate(layoutInflater: LayoutInflater) = + DialogConfigGradesBinding.inflate(layoutInflater) + + @SuppressLint("SetTextI18n") + override suspend fun loadConfig() { + b.customPlusCheckBox.isChecked = app.profile.config.grades.plusValue != null + b.customPlusValue.isVisible = b.customPlusCheckBox.isChecked + b.customMinusCheckBox.isChecked = app.profile.config.grades.minusValue != null + b.customMinusValue.isVisible = b.customMinusCheckBox.isChecked + + b.customPlusValue.progress = app.profile.config.grades.plusValue ?: 0.5f + b.customMinusValue.progress = app.profile.config.grades.minusValue ?: 0.25f + + when (config.orderBy) { + ORDER_BY_DATE_DESC -> b.sortGradesByDateRadio + ORDER_BY_SUBJECT_ASC -> b.sortGradesBySubjectRadio + else -> null + }?.isChecked = true + + when (app.profile.config.grades.colorMode) { + COLOR_MODE_DEFAULT -> b.gradeColorFromERegister + COLOR_MODE_WEIGHTED -> b.gradeColorByValue + else -> null + }?.isChecked = true + + when (app.profile.config.grades.yearAverageMode) { + YEAR_ALL_GRADES -> b.gradeAverageMode4 + YEAR_1_AVG_2_AVG -> b.gradeAverageMode0 + YEAR_1_SEM_2_AVG -> b.gradeAverageMode1 + YEAR_1_AVG_2_SEM -> b.gradeAverageMode2 + YEAR_1_SEM_2_SEM -> b.gradeAverageMode3 + else -> null + }?.isChecked = true + + b.dontCountGrades.isChecked = + app.profile.config.grades.dontCountEnabled && app.profile.config.grades.dontCountGrades.isNotEmpty() + b.hideImproved.isChecked = app.profile.config.grades.hideImproved + b.averageWithoutWeight.isChecked = app.profile.config.grades.averageWithoutWeight + + if (app.profile.config.grades.dontCountGrades.isEmpty()) { + b.dontCountGradesText.setText("nb, 0, bz, bd") + } else { + b.dontCountGradesText.setText(app.profile.config.grades.dontCountGrades.join(", ")) + } + } + + override suspend fun saveConfig() { + app.profile.config.grades.plusValue = + if (b.customPlusCheckBox.isChecked) b.customPlusValue.progress else null + app.profile.config.grades.minusValue = + if (b.customMinusCheckBox.isChecked) b.customMinusValue.progress else null + + b.dontCountGradesText.setText( + b.dontCountGradesText + .text + ?.toString() + ?.lowercase() + ?.replace(", ", ",") + ) + app.profile.config.grades.dontCountEnabled = b.dontCountGrades.isChecked + app.profile.config.grades.dontCountGrades = b.dontCountGradesText.text + ?.split(",") + ?.map { it.trim() } + ?: listOf() + } + + override fun initView() { + b.customPlusCheckBox.onChange { _, isChecked -> + b.customPlusValue.isVisible = isChecked + } + b.customMinusCheckBox.onChange { _, isChecked -> + b.customMinusValue.isVisible = isChecked + } + + // who the hell named those methods + // THIS SHIT DOES NOT EVEN WORK + b.customPlusValue.doOnStopTrackingTouch { + app.profile.config.grades.plusValue = it.progress + } + b.customMinusValue.doOnStopTrackingTouch { + app.profile.config.grades.minusValue = it.progress + } + + b.sortGradesByDateRadio.setOnSelectedListener { config.orderBy = ORDER_BY_DATE_DESC } + b.sortGradesBySubjectRadio.setOnSelectedListener { config.orderBy = ORDER_BY_SUBJECT_ASC } + + b.gradeColorFromERegister.setOnSelectedListener { + app.profile.config.grades.colorMode = COLOR_MODE_DEFAULT + } + b.gradeColorByValue.setOnSelectedListener { app.profile.config.grades.colorMode = COLOR_MODE_WEIGHTED } + + b.gradeAverageMode4.setOnSelectedListener { + app.profile.config.grades.yearAverageMode = YEAR_ALL_GRADES + } + b.gradeAverageMode0.setOnSelectedListener { + app.profile.config.grades.yearAverageMode = YEAR_1_AVG_2_AVG + } + b.gradeAverageMode1.setOnSelectedListener { + app.profile.config.grades.yearAverageMode = YEAR_1_SEM_2_AVG + } + b.gradeAverageMode2.setOnSelectedListener { + app.profile.config.grades.yearAverageMode = YEAR_1_AVG_2_SEM + } + b.gradeAverageMode3.setOnSelectedListener { + app.profile.config.grades.yearAverageMode = YEAR_1_SEM_2_SEM + } + + b.hideImproved.onChange { _, isChecked -> app.profile.config.grades.hideImproved = isChecked } + b.averageWithoutWeight.onChange { _, isChecked -> + app.profile.config.grades.averageWithoutWeight = isChecked + } + + b.averageWithoutWeightHelp.onClick { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.grades_config_average_without_weight) + .setMessage(R.string.grades_config_average_without_weight_message) + .setPositiveButton(R.string.ok, null) + .show() + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/MessagesConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/MessagesConfigDialog.kt new file mode 100644 index 00000000..0e5696db --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/MessagesConfigDialog.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-14. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs.settings + +import android.view.LayoutInflater +import androidx.appcompat.app.AppCompatActivity +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.MessagesConfigDialogBinding +import pl.szczodrzynski.edziennik.ui.dialogs.base.ConfigDialog + +class MessagesConfigDialog( + activity: AppCompatActivity, + reloadOnDismiss: Boolean = true, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : ConfigDialog( + activity, + reloadOnDismiss, + onShowListener, + onDismissListener, +) { + + override val TAG = "MessagesConfigDialog" + + override fun getTitleRes() = R.string.menu_messages_config + override fun inflate(layoutInflater: LayoutInflater) = + MessagesConfigDialogBinding.inflate(layoutInflater) + + override suspend fun loadConfig() { + b.config = app.profile.config.ui + + b.greetingText.setText( + app.profile.config.ui.messagesGreetingText + ?: "\n\nZ poważaniem\n${app.profile.accountOwnerName}" + ) + } + + override suspend fun saveConfig() { + val greetingText = b.greetingText.text?.toString()?.trim() + if (greetingText.isNullOrEmpty()) + app.profile.config.ui.messagesGreetingText = null + else + app.profile.config.ui.messagesGreetingText = "\n\n$greetingText" + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/MiniMenuConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/MiniMenuConfigDialog.kt index 61e95a8b..5a87f1ce 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/MiniMenuConfigDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/MiniMenuConfigDialog.kt @@ -4,93 +4,49 @@ package pl.szczodrzynski.edziennik.ui.dialogs.settings -import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.MainActivity -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_AGENDA -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_ANNOUNCEMENTS -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_ATTENDANCE -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_BEHAVIOUR -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_GRADES -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_HOME -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_HOMEWORK -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_NOTIFICATIONS -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_SETTINGS -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_TIMETABLE import pl.szczodrzynski.edziennik.R -import kotlin.coroutines.CoroutineContext +import pl.szczodrzynski.edziennik.ext.resolveString +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget +import pl.szczodrzynski.edziennik.ui.base.enums.NavTargetLocation +import pl.szczodrzynski.edziennik.ui.dialogs.base.BaseDialog class MiniMenuConfigDialog( - val activity: AppCompatActivity, - val onShowListener: ((tag: String) -> Unit)? = null, - val onDismissListener: ((tag: String) -> Unit)? = null -) : CoroutineScope { - companion object { - private const val TAG = "MiniMenuConfigDialog" - } + activity: AppCompatActivity, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BaseDialog(activity, onShowListener, onDismissListener) { - private lateinit var app: App - private lateinit var dialog: AlertDialog + override val TAG = "BellSyncTimeChooseDialog" - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main + override fun getTitleRes() = R.string.settings_theme_mini_drawer_buttons_dialog_title + override fun getMessageRes() = R.string.settings_theme_mini_drawer_buttons_dialog_text + override fun getPositiveButtonText() = R.string.ok + override fun getNegativeButtonText() = R.string.cancel - // local variables go here - - init { run { - if (activity.isFinishing) - return@run - onShowListener?.invoke(TAG) - app = activity.applicationContext as App - - val buttons = mapOf( - DRAWER_ITEM_HOME to R.string.menu_home_page, - DRAWER_ITEM_TIMETABLE to R.string.menu_timetable, - DRAWER_ITEM_AGENDA to R.string.menu_agenda, - DRAWER_ITEM_GRADES to R.string.menu_grades, - DRAWER_ITEM_MESSAGES to R.string.menu_messages, - DRAWER_ITEM_HOMEWORK to R.string.menu_homework, - DRAWER_ITEM_BEHAVIOUR to R.string.menu_notices, - DRAWER_ITEM_ATTENDANCE to R.string.menu_attendance, - DRAWER_ITEM_ANNOUNCEMENTS to R.string.menu_announcements, - DRAWER_ITEM_NOTIFICATIONS to R.string.menu_notifications, - DRAWER_ITEM_SETTINGS to R.string.menu_settings - ) - val miniMenuButtons = app.config.ui.miniMenuButtons - - dialog = MaterialAlertDialogBuilder(activity) - .setTitle(R.string.settings_theme_mini_drawer_buttons_dialog_title) - //.setMessage(R.string.settings_theme_mini_drawer_buttons_dialog_text) - .setMultiChoiceItems( - buttons.map { activity.getString(it.value) }.toTypedArray(), - buttons.map { it.key in miniMenuButtons }.toBooleanArray(), - null + @Suppress("USELESS_CAST") + override fun getMultiChoiceItems() = NavTarget.values() + .filter { + (!it.devModeOnly || App.devMode) && it.location in listOf( + NavTargetLocation.DRAWER, + // NavTargetLocation.DRAWER_MORE, + NavTargetLocation.DRAWER_BOTTOM, ) - .setPositiveButton(R.string.ok) { _, _ -> - app.config.ui.miniMenuButtons = - buttons.keys.mapIndexedNotNull { index, id -> - if (dialog.listView.checkedItemPositions[index]) - id - else - null - } + } + .associateBy { it.nameRes.resolveString(activity) as CharSequence } - if (activity is MainActivity) { - activity.setDrawerItems() - activity.drawer.updateBadges() - } - } - .setNegativeButton(R.string.cancel, null) - .setOnDismissListener { - onDismissListener?.invoke(TAG) - } - .show() - }} + override fun getDefaultSelectedItems() = app.config.ui.miniMenuButtons + + override suspend fun onShow() = Unit + + override suspend fun onPositiveClick(): Boolean { + app.config.ui.miniMenuButtons = getMultiSelection() + if (activity is MainActivity) { + activity.setDrawerItems() + activity.drawer.updateBadges() + } + return DISMISS + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/NotificationFilterDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/NotificationFilterDialog.kt new file mode 100644 index 00000000..5c157eb7 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/NotificationFilterDialog.kt @@ -0,0 +1,62 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-2-21. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs.settings + +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.enums.NotificationType +import pl.szczodrzynski.edziennik.ext.resolveString +import pl.szczodrzynski.edziennik.ui.dialogs.base.BaseDialog + +class NotificationFilterDialog( + activity: AppCompatActivity, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BaseDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "NotificationFilterDialog" + + override fun getTitleRes() = R.string.dialog_notification_filter_title + override fun getMessageRes() = R.string.dialog_notification_filter_text + override fun getPositiveButtonText() = R.string.ok + override fun getNegativeButtonText() = R.string.cancel + + @Suppress("USELESS_CAST") + override fun getMultiChoiceItems() = NotificationType.values() + .filter { it.enabledByDefault != null } + .associateBy { it.titleRes.resolveString(activity) as CharSequence } + + override fun getDefaultSelectedItems() = NotificationType.values() + .filter { it.enabledByDefault != null && it !in app.profile.config.sync.notificationFilter } + .toSet() + + override suspend fun onShow() = Unit + + override suspend fun onPositiveClick(): Boolean { + val enabledTypes = getMultiSelection() + val disabledTypes = NotificationType.values() + .filter { it.enabledByDefault != null && it !in enabledTypes } + .toSet() + + if (disabledTypes.any { it.enabledByDefault == true }) { + // warn user when he tries to disable some notifications + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.are_you_sure) + .setMessage(R.string.notification_filter_warning) + .setPositiveButton(R.string.ok) { _, _ -> + app.profile.config.sync.notificationFilter = disabledTypes + dismiss() + } + .setNegativeButton(R.string.cancel, null) + .show() + return NO_DISMISS + } + + app.profile.config.sync.notificationFilter = disabledTypes + + return DISMISS + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/profile/ProfileConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/ProfileConfigDialog.kt similarity index 51% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/profile/ProfileConfigDialog.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/ProfileConfigDialog.kt index e9204090..7ded038d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/profile/ProfileConfigDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/ProfileConfigDialog.kt @@ -2,68 +2,52 @@ * Copyright (c) Kuba Szczodrzyński 2021-3-23. */ -package pl.szczodrzynski.edziennik.ui.dialogs.profile +package pl.szczodrzynski.edziennik.ui.dialogs.settings import android.content.res.ColorStateList -import androidx.appcompat.app.AlertDialog +import android.view.LayoutInflater import androidx.core.widget.addTextChangedListener import com.google.android.material.color.MaterialColors -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.shape.MaterialShapeDrawable -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.databinding.DialogProfileConfigBinding -import kotlin.coroutines.CoroutineContext +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ext.onChange +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ui.dialogs.ProfileRemoveDialog +import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog class ProfileConfigDialog( - val activity: MainActivity, - val profile: Profile, - val onProfileSaved: ((profile: Profile) -> Unit)? = null, - val onShowListener: ((tag: String) -> Unit)? = null, - val onDismissListener: ((tag: String) -> Unit)? = null -) : CoroutineScope { - companion object { - private const val TAG = "ProfileConfigDialog" - } + activity: MainActivity, + private val profile: Profile, + private val onProfileSaved: ((profile: Profile) -> Unit)? = null, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BindingDialog(activity, onShowListener, onDismissListener) { - private lateinit var app: App - private lateinit var b: DialogProfileConfigBinding - private lateinit var dialog: AlertDialog + override val TAG = "ProfileConfigDialog" - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main + override fun getTitleRes(): Int? = null + override fun inflate(layoutInflater: LayoutInflater) = + DialogProfileConfigBinding.inflate(layoutInflater) + + override fun getPositiveButtonText() = R.string.close - // local variables go here private var profileChanged = false private var profileRemoved = false - init { run { - if (activity.isFinishing) - return@run - onShowListener?.invoke(TAG) - app = activity.applicationContext as App - b = DialogProfileConfigBinding.inflate(activity.layoutInflater) - dialog = MaterialAlertDialogBuilder(activity) - .setView(b.root) - .setPositiveButton(R.string.close, null) - .setOnDismissListener { - if (!profileRemoved && profileChanged) { - app.profileSave(profile) - onProfileSaved?.invoke(profile) - } - onDismissListener?.invoke(TAG) - } - .show() - + override suspend fun onShow() { b.profile = profile profile.applyImageTo(b.image) // I can't believe how simple it is to get the dialog's background color !! - val shape = MaterialShapeDrawable(activity, null, R.attr.alertDialogStyle, R.style.MaterialAlertDialog_MaterialComponents) + val shape = MaterialShapeDrawable( + activity, + null, + R.attr.alertDialogStyle, + R.style.MaterialAlertDialog_MaterialComponents + ) val surface = MaterialColors.getColor(activity, R.attr.colorSurface, TAG) shape.setCornerSize(18.dp.toFloat()) shape.initializeElevationOverlay(activity) @@ -80,6 +64,8 @@ class ProfileConfigDialog( } b.imageButton.onClick { + if (activity !is MainActivity) + return@onClick activity.requestHandler.requestProfileImage(profile) { val profile = it as? Profile ?: return@requestProfileImage if (this@ProfileConfigDialog.profile == profile) { @@ -94,7 +80,14 @@ class ProfileConfigDialog( ProfileRemoveDialog(activity, profile.id, profile.name) { profileRemoved = true dialog.dismiss() - } + }.show() } - }} + } + + override fun onDismiss() { + if (!profileRemoved && profileChanged) { + app.profileSave(profile) + onProfileSaved?.invoke(profile) + } + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/QuietHoursConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/QuietHoursConfigDialog.kt similarity index 98% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/QuietHoursConfigDialog.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/QuietHoursConfigDialog.kt index 5c933e20..1319b540 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/QuietHoursConfigDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/QuietHoursConfigDialog.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2021-3-20. */ -package pl.szczodrzynski.edziennik.ui.dialogs.sync +package pl.szczodrzynski.edziennik.ui.dialogs.settings import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/RegistrationConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/RegistrationConfigDialog.kt similarity index 80% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/RegistrationConfigDialog.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/RegistrationConfigDialog.kt index fff65b43..dbe25d98 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/RegistrationConfigDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/RegistrationConfigDialog.kt @@ -2,24 +2,28 @@ * Copyright (c) Kuba Szczodrzyński 2020-3-15. */ -package pl.szczodrzynski.edziennik.ui.dialogs.sync +package pl.szczodrzynski.edziennik.ui.dialogs.settings -import android.text.Html import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.data.api.task.AppSync import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.utils.html.BetterHtml import kotlin.coroutines.CoroutineContext class RegistrationConfigDialog( val activity: AppCompatActivity, val profile: Profile, - val onChangeListener: ((enabled: Boolean) -> Unit)? = null, + val onChangeListener: (suspend (enabled: Boolean) -> Unit)? = null, val onShowListener: ((tag: String) -> Unit)? = null, val onDismissListener: ((tag: String) -> Unit)? = null ) : CoroutineScope { @@ -57,11 +61,26 @@ class RegistrationConfigDialog( .show() } + fun showNoteShareDialog() { + onShowListener?.invoke(TAG + "NoteShare") + dialog = MaterialAlertDialogBuilder(activity) + .setTitle(R.string.registration_config_note_sharing_title) + .setMessage(R.string.registration_config_note_sharing_text) + .setPositiveButton(R.string.i_agree) { _, _ -> + enableRegistration() + } + .setNegativeButton(R.string.cancel, null) + .setOnDismissListener { + onDismissListener?.invoke(TAG + "NoteShare") + } + .show() + } + fun showEnableDialog() { onShowListener?.invoke(TAG + "Enable") dialog = MaterialAlertDialogBuilder(activity) .setTitle(R.string.registration_config_title) - .setMessage(Html.fromHtml(app.getString(R.string.registration_config_enable_text))) + .setMessage(BetterHtml.fromHtml(activity, R.string.registration_config_enable_text)) .setPositiveButton(R.string.i_agree) { _, _ -> enableRegistration() } @@ -76,7 +95,7 @@ class RegistrationConfigDialog( onShowListener?.invoke(TAG + "Disable") dialog = MaterialAlertDialogBuilder(activity) .setTitle(R.string.registration_config_title) - .setMessage(Html.fromHtml(app.getString(R.string.registration_config_disable_text))) + .setMessage(R.string.registration_config_disable_text) .setPositiveButton(R.string.ok) { _, _ -> disableRegistration() } @@ -102,7 +121,7 @@ class RegistrationConfigDialog( profile.registration = Profile.REGISTRATION_ENABLED // force full registration of the user - App.config.getFor(profile.id).hash = "" + profile.config.hash = "" SzkolnyApi(app).runCatching(activity) { AppSync(app, mutableListOf(), listOf(profile), this).run( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/home/StudentNumberDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/StudentNumberDialog.kt similarity index 93% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/home/StudentNumberDialog.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/StudentNumberDialog.kt index 3f3f8176..a4da704e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/home/StudentNumberDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/StudentNumberDialog.kt @@ -2,14 +2,14 @@ * Copyright (c) Kuba Szczodrzyński 2019-11-24. */ -package pl.szczodrzynski.edziennik.ui.dialogs.home +package pl.szczodrzynski.edziennik.ui.dialogs.settings import android.text.InputType import androidx.appcompat.app.AppCompatActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.Profile -import pl.szczodrzynski.edziennik.ui.dialogs.input +import pl.szczodrzynski.edziennik.ext.input class StudentNumberDialog( val activity: AppCompatActivity, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/SyncIntervalDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/SyncIntervalDialog.kt new file mode 100644 index 00000000..b64ce774 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/SyncIntervalDialog.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-3-20. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs.settings + +import androidx.appcompat.app.AppCompatActivity +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.ext.HOUR +import pl.szczodrzynski.edziennik.ext.MINUTE +import pl.szczodrzynski.edziennik.ext.getSyncInterval +import pl.szczodrzynski.edziennik.ui.dialogs.base.BaseDialog + +class SyncIntervalDialog( + activity: AppCompatActivity, + private val onChangeListener: (() -> Unit)? = null, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BaseDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "SyncIntervalDialog" + + override fun getTitleRes() = R.string.settings_sync_sync_interval_dialog_title + override fun getMessageRes() = R.string.settings_sync_sync_interval_dialog_text + override fun getPositiveButtonText() = R.string.ok + override fun getNegativeButtonText() = R.string.cancel + + override fun getSingleChoiceItems(): Map = listOf( + 30 * MINUTE, + 45 * MINUTE, + 60 * MINUTE, + 90 * MINUTE, + 2 * HOUR, + 3 * HOUR, + 4 * HOUR, + 6 * HOUR, + 10 * HOUR, + ).associateBy { activity.getSyncInterval(it.toInt()) } + + override fun getDefaultSelectedItem() = app.config.sync.interval.toLong() + + override suspend fun onShow() = Unit + + override suspend fun onPositiveClick(): Boolean { + val interval = getSingleSelection() as? Long ?: return DISMISS + app.config.sync.interval = interval.toInt() + onChangeListener?.invoke() + return DISMISS + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/ThemeChooserDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/ThemeChooserDialog.kt index ca9b2f11..34d86324 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/ThemeChooserDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/ThemeChooserDialog.kt @@ -4,62 +4,38 @@ package pl.szczodrzynski.edziennik.ui.dialogs.settings -import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.ui.dialogs.base.BaseDialog import pl.szczodrzynski.edziennik.utils.Themes -import kotlin.coroutines.CoroutineContext class ThemeChooserDialog( - val activity: AppCompatActivity, - val onShowListener: ((tag: String) -> Unit)? = null, - val onDismissListener: ((tag: String) -> Unit)? = null -) : CoroutineScope { - companion object { - private const val TAG = "ThemeChooserDialog" + activity: AppCompatActivity, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BaseDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "ThemeChooserDialog" + + override fun getTitleRes() = R.string.settings_theme_theme_text + override fun getPositiveButtonText() = R.string.ok + override fun getNegativeButtonText() = R.string.cancel + + override fun getSingleChoiceItems(): Map = Themes.themeList.associate { + activity.getString(it.name) to it.id } - private lateinit var app: App - private lateinit var dialog: AlertDialog + override fun getDefaultSelectedItem() = Themes.theme.id - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main + override suspend fun onShow() = Unit - // local variables go here - - init { run { - if (activity.isFinishing) - return@run - onShowListener?.invoke(TAG) - app = activity.applicationContext as App - - dialog = MaterialAlertDialogBuilder(activity) - .setTitle(R.string.settings_theme_theme_text) - .setSingleChoiceItems( - Themes.getThemeNames(activity).toTypedArray(), - Themes.themeIndex, - null - ) - .setPositiveButton(R.string.ok) { _, _ -> - val which = dialog.listView.checkedItemPosition - - val theme = Themes.themeList[which] - if (app.config.ui.theme == theme.id) - return@setPositiveButton - app.config.ui.theme = theme.id - Themes.themeIndex = which - activity.recreate() - } - .setNegativeButton(R.string.cancel, null) - .setOnDismissListener { - onDismissListener?.invoke(TAG) - } - .show() - }} + override suspend fun onPositiveClick(): Boolean { + val themeId = getSingleSelection() as? Int ?: return DISMISS + if (app.config.ui.theme != themeId) { + app.config.ui.theme = themeId + Themes.themeInt = themeId + activity.recreate() + } + return DISMISS + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/TimetableConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/TimetableConfigDialog.kt new file mode 100644 index 00000000..09893c6a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/TimetableConfigDialog.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-7. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs.settings + +import android.view.LayoutInflater +import androidx.appcompat.app.AppCompatActivity +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.TimetableConfigDialogBinding +import pl.szczodrzynski.edziennik.ext.Intent +import pl.szczodrzynski.edziennik.ui.dialogs.base.ConfigDialog +import pl.szczodrzynski.edziennik.ui.timetable.TimetableFragment + +class TimetableConfigDialog( + activity: AppCompatActivity, + reloadOnDismiss: Boolean = true, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : ConfigDialog( + activity, + reloadOnDismiss, + onShowListener, + onDismissListener, +) { + + override val TAG = "TimetableConfigDialog" + + override fun getTitleRes() = R.string.menu_timetable_config + override fun inflate(layoutInflater: LayoutInflater) = + TimetableConfigDialogBinding.inflate(layoutInflater) + + override fun initView() { + b.features = app.profile.loginStoreType.features + } + + override suspend fun loadConfig() { + b.config = app.profile.config.ui + } + + override suspend fun saveConfig() { + activity.sendBroadcast(Intent(TimetableFragment.ACTION_RELOAD_PAGES)) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/NotificationFilterDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/NotificationFilterDialog.kt deleted file mode 100644 index ad31c02d..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/NotificationFilterDialog.kt +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2020-2-21. - */ - -package pl.szczodrzynski.edziennik.ui.dialogs.sync - -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.db.entity.Notification -import pl.szczodrzynski.edziennik.onClick -import kotlin.coroutines.CoroutineContext - -// TODO refactor dialog to allow configuring other profiles -// than the selected one in UI -class NotificationFilterDialog( - val activity: AppCompatActivity, - val onShowListener: ((tag: String) -> Unit)? = null, - val onDismissListener: ((tag: String) -> Unit)? = null -) : CoroutineScope { - companion object { - private const val TAG = "NotificationFilterDialog" - private val notificationTypes = listOf( - Notification.TYPE_TIMETABLE_LESSON_CHANGE to R.string.notification_type_timetable_lesson_change, - Notification.TYPE_NEW_GRADE to R.string.notification_type_new_grade, - Notification.TYPE_NEW_EVENT to R.string.notification_type_new_event, - Notification.TYPE_NEW_HOMEWORK to R.string.notification_type_new_homework, - Notification.TYPE_NEW_MESSAGE to R.string.notification_type_new_message, - Notification.TYPE_LUCKY_NUMBER to R.string.notification_type_lucky_number, - Notification.TYPE_NEW_NOTICE to R.string.notification_type_notice, - Notification.TYPE_NEW_ATTENDANCE to R.string.notification_type_attendance, - Notification.TYPE_NEW_ANNOUNCEMENT to R.string.notification_type_new_announcement, - Notification.TYPE_NEW_SHARED_EVENT to R.string.notification_type_new_shared_event, - Notification.TYPE_NEW_SHARED_HOMEWORK to R.string.notification_type_new_shared_homework, - Notification.TYPE_REMOVED_SHARED_EVENT to R.string.notification_type_removed_shared_event, - Notification.TYPE_TEACHER_ABSENCE to R.string.notification_type_new_teacher_absence - ) - } - - private lateinit var app: App - private lateinit var dialog: AlertDialog - - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - private val notificationFilter = mutableListOf() - - init { run { - if (activity.isFinishing) - return@run - onShowListener?.invoke(TAG) - app = activity.applicationContext as App - - notificationFilter.clear() - notificationFilter += app.config.forProfile().sync.notificationFilter - val items = notificationTypes.map { app.getString(it.second) }.toTypedArray() - val checkedItems = notificationTypes.map { !notificationFilter.contains(it.first) }.toBooleanArray() - - dialog = MaterialAlertDialogBuilder(activity) - .setTitle(R.string.dialog_notification_filter_title) - //.setMessage(R.string.dialog_notification_filter_text) - .setMultiChoiceItems(items, checkedItems) { _, which, isChecked -> - val type = notificationTypes[which].first - notificationFilter.remove(type) - if (!isChecked) - notificationFilter += type - } - .setPositiveButton(R.string.ok, null) - .setNegativeButton(R.string.cancel, null) - .setOnDismissListener { - onDismissListener?.invoke(TAG) - } - .show() - - dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.onClick { - if (notificationFilter.isEmpty()) { - app.config.forProfile().sync.notificationFilter = notificationFilter - dialog.dismiss() - return@onClick - } - // warn user when he tries to disable some notifications - MaterialAlertDialogBuilder(activity) - .setTitle(R.string.are_you_sure) - .setMessage(R.string.notification_filter_warning) - .setPositiveButton(R.string.ok) { _, _ -> - app.config.forProfile().sync.notificationFilter = notificationFilter - dialog.dismiss() - } - .setNegativeButton(R.string.cancel, null) - .show() - } - }} -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/RegisterUnavailableDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/RegisterUnavailableDialog.kt new file mode 100644 index 00000000..3045b5d6 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/RegisterUnavailableDialog.kt @@ -0,0 +1,65 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-9-3. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs.sync + +import android.text.method.LinkMovementMethod +import android.view.LayoutInflater +import androidx.appcompat.app.AppCompatActivity +import coil.load +import pl.szczodrzynski.edziennik.BuildConfig +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus +import pl.szczodrzynski.edziennik.databinding.DialogRegisterUnavailableBinding +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog +import pl.szczodrzynski.edziennik.utils.Utils + +class RegisterUnavailableDialog( + activity: AppCompatActivity, + private val status: RegisterAvailabilityStatus, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BindingDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "RegisterUnavailableDialog" + + override fun getTitleRes(): Int? = null + override fun inflate(layoutInflater: LayoutInflater) = + DialogRegisterUnavailableBinding.inflate(layoutInflater) + + override fun getPositiveButtonText() = R.string.close + + override suspend fun onBeforeShow(): Boolean { + if (!status.available && status.userMessage != null) + return true + + if (status.minVersionCode <= BuildConfig.VERSION_CODE) + return false + + val update = app.config.update + UpdateAvailableDialog( + activity = activity, + update = update, + mandatory = update != null && update.versionCode >= status.minVersionCode, + onShowListener = onShowListener, + onDismissListener = onDismissListener + ).show() + return false + } + + override suspend fun onShow() { + b.message = status.userMessage ?: return + b.text.movementMethod = LinkMovementMethod.getInstance() + + if (status.userMessage.image != null) { + b.image.load(status.userMessage.image) + } + if (status.userMessage.url != null) { + b.readMore.onClick { + Utils.openUrl(activity, status.userMessage.url) + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/ServerMessageDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/ServerMessageDialog.kt new file mode 100644 index 00000000..b0f4f3fa --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/ServerMessageDialog.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-1-19. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs.sync + +import androidx.appcompat.app.AppCompatActivity +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.ui.dialogs.base.BaseDialog + +class ServerMessageDialog( + activity: AppCompatActivity, + private val titleText: String, + private val messageText: CharSequence, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BaseDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "ServerMessageDialog" + + override fun getTitle() = titleText + override fun getTitleRes(): Int? = null + override fun getMessage() = messageText + override fun getPositiveButtonText() = R.string.close + + override suspend fun onShow() = Unit +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/SyncIntervalDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/SyncIntervalDialog.kt deleted file mode 100644 index 6f4f027b..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/SyncIntervalDialog.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2021-3-20. - */ - -package pl.szczodrzynski.edziennik.ui.dialogs.sync - -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.* -import kotlin.coroutines.CoroutineContext - -class SyncIntervalDialog( - val activity: AppCompatActivity, - val onChangeListener: (() -> Unit)? = null, - val onShowListener: ((tag: String) -> Unit)? = null, - val onDismissListener: ((tag: String) -> Unit)? = null -) : CoroutineScope { - companion object { - private const val TAG = "SyncIntervalDialog" - } - - private lateinit var app: App - private lateinit var dialog: AlertDialog - - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - // local variables go here - - init { run { - if (activity.isFinishing) - return@run - onShowListener?.invoke(TAG) - app = activity.applicationContext as App - - val intervals = listOf( - 30 * MINUTE, - 45 * MINUTE, - 60 * MINUTE, - 90 * MINUTE, - 2 * HOUR, - 3 * HOUR, - 4 * HOUR, - 6 * HOUR, - 10 * HOUR - ) - val intervalNames = intervals.map { - activity.getSyncInterval(it.toInt()) - } - - dialog = MaterialAlertDialogBuilder(activity) - .setTitle(R.string.settings_sync_sync_interval_dialog_title) - //.setMessage(R.string.settings_sync_sync_interval_dialog_text) - .setSingleChoiceItems( - intervalNames.toTypedArray(), - intervals.indexOf(app.config.sync.interval.toLong()), - null - ) - .setPositiveButton(R.string.ok) { _, _ -> - val which = dialog.listView.checkedItemPosition - - val interval = intervals[which] - app.config.sync.interval = interval.toInt() - onChangeListener?.invoke() - } - .setNegativeButton(R.string.cancel, null) - .setOnDismissListener { - onDismissListener?.invoke(TAG) - } - .show() - }} -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/SyncViewListDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/SyncViewListDialog.kt index b9cbd0a0..50f918d7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/SyncViewListDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/SyncViewListDialog.kt @@ -4,113 +4,67 @@ package pl.szczodrzynski.edziennik.ui.dialogs.sync -import androidx.appcompat.app.AlertDialog -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job 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.databinding.DialogLessonDetailsBinding -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment -import kotlin.coroutines.CoroutineContext +import pl.szczodrzynski.edziennik.data.db.entity.Message +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.ext.hasFeature +import pl.szczodrzynski.edziennik.ext.resolveString +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget +import pl.szczodrzynski.edziennik.ui.dialogs.base.BaseDialog +import pl.szczodrzynski.edziennik.ui.messages.list.MessagesFragment class SyncViewListDialog( - val activity: MainActivity, - val currentViewId: Int? = null -) : CoroutineScope { - companion object { - private const val TAG = "SyncViewListDialog" + activity: MainActivity, + private val currentNavTarget: NavTarget, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BaseDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "SyncViewListDialog" + + override fun getTitleRes() = R.string.dialog_sync_view_list_title + override fun getPositiveButtonText() = R.string.ok + override fun getNeutralButtonText() = R.string.sync_feature_all + override fun getNegativeButtonText() = R.string.cancel + + @Suppress("USELESS_CAST") + override fun getMultiChoiceItems() = FeatureType.values() + .filter { it.nameRes != null && app.profile.hasFeature(it) } + .associateBy { it.nameRes!!.resolveString(activity) as CharSequence } + + override fun getDefaultSelectedItems() = when (currentNavTarget) { + NavTarget.HOME -> getMultiChoiceItems().values.toSet() + NavTarget.MESSAGES -> when (MessagesFragment.pageSelection) { + Message.TYPE_SENT -> setOf(FeatureType.MESSAGES_SENT) + else -> setOf(FeatureType.MESSAGES_INBOX) + } + else -> currentNavTarget.featureType?.let { setOf(it) } ?: getMultiChoiceItems().values.toSet() } - private lateinit var job: Job - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main + override suspend fun onShow() = Unit - private val app by lazy { activity.application as App } - private lateinit var b: DialogLessonDetailsBinding - private lateinit var dialog: AlertDialog + @Suppress("UNCHECKED_CAST") + override suspend fun onPositiveClick(): Boolean { + val selected = getMultiSelection() + if (selected.isEmpty()) + return DISMISS - init { run { - job = Job() + if (activity is MainActivity) + activity.swipeRefreshLayout.isRefreshing = true + EdziennikTask.syncProfile( + App.profileId, + selected + ).enqueue(activity) + return DISMISS + } - val viewIds = arrayOf( - MainActivity.DRAWER_ITEM_TIMETABLE, - MainActivity.DRAWER_ITEM_AGENDA, - MainActivity.DRAWER_ITEM_GRADES, - MainActivity.DRAWER_ITEM_HOMEWORK, - MainActivity.DRAWER_ITEM_BEHAVIOUR, - MainActivity.DRAWER_ITEM_ATTENDANCE, - MainActivity.DRAWER_ITEM_MESSAGES, - MainActivity.DRAWER_ITEM_MESSAGES, - MainActivity.DRAWER_ITEM_ANNOUNCEMENTS - ) - - val items = arrayOf( - app.getString(R.string.menu_timetable), - app.getString(R.string.menu_agenda), - app.getString(R.string.menu_grades), - app.getString(R.string.menu_homework), - app.getString(R.string.menu_notices), - app.getString(R.string.menu_attendance), - app.getString(R.string.title_messages_inbox_single), - app.getString(R.string.title_messages_sent_single), - app.getString(R.string.menu_announcements) - ) - - val everything = currentViewId == MainActivity.DRAWER_ITEM_HOME - val checkedItems = booleanArrayOf( - everything || currentViewId == MainActivity.DRAWER_ITEM_TIMETABLE, - everything || currentViewId == MainActivity.DRAWER_ITEM_AGENDA, - everything || currentViewId == MainActivity.DRAWER_ITEM_GRADES, - everything || currentViewId == MainActivity.DRAWER_ITEM_HOMEWORK, - everything || currentViewId == MainActivity.DRAWER_ITEM_BEHAVIOUR, - everything || currentViewId == MainActivity.DRAWER_ITEM_ATTENDANCE, - everything || currentViewId == MainActivity.DRAWER_ITEM_MESSAGES && MessagesFragment.pageSelection != 1, - everything || currentViewId == MainActivity.DRAWER_ITEM_MESSAGES && MessagesFragment.pageSelection == 1, - everything || currentViewId == MainActivity.DRAWER_ITEM_ANNOUNCEMENTS - ) - val userChooses = checkedItems.toMutableList() - - dialog = MaterialAlertDialogBuilder(activity) - .setTitle(R.string.dialog_sync_view_list_title) - .setMultiChoiceItems(items, checkedItems) { _, which, isChecked -> - userChooses[which] = isChecked - } - .setPositiveButton(R.string.ok) { _, _ -> - dialog.dismiss() - - val selectedViewIds = userChooses.mapIndexed { index, it -> - if (it) - viewIds[index] to when (index) { - 7 -> 1 - else -> 0 - } - else - null - }.let { - listOfNotNull(*it.toTypedArray()) - } - - if (selectedViewIds.isNotEmpty()) { - activity.swipeRefreshLayout.isRefreshing = true - EdziennikTask.syncProfile( - App.profileId, - selectedViewIds - ).enqueue(activity) - } - } - .setNeutralButton(R.string.sync_feature_all) { _, _ -> - dialog.dismiss() - - activity.swipeRefreshLayout.isRefreshing = true - EdziennikTask.syncProfile(App.profileId).enqueue(activity) - } - .setNegativeButton(R.string.cancel) { _, _ -> - dialog.dismiss() - } - .show() - }} + override suspend fun onNeutralClick(): Boolean { + if (activity is MainActivity) + activity.swipeRefreshLayout.isRefreshing = true + EdziennikTask.syncProfile(App.profileId).enqueue(activity) + return DISMISS + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/UpdateAvailableDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/UpdateAvailableDialog.kt new file mode 100644 index 00000000..c6766ece --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/UpdateAvailableDialog.kt @@ -0,0 +1,57 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-9-3. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs.sync + +import androidx.appcompat.app.AppCompatActivity +import pl.szczodrzynski.edziennik.BuildConfig +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update +import pl.szczodrzynski.edziennik.ext.Intent +import pl.szczodrzynski.edziennik.sync.UpdateDownloaderService +import pl.szczodrzynski.edziennik.ui.dialogs.base.BaseDialog +import pl.szczodrzynski.edziennik.utils.Utils +import pl.szczodrzynski.edziennik.utils.html.BetterHtml + +class UpdateAvailableDialog( + activity: AppCompatActivity, + private val update: Update?, + private val mandatory: Boolean = update?.updateMandatory ?: false, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BaseDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "UpdateAvailableDialog" + + override fun getTitleRes() = R.string.update_available_title + override fun getMessageFormat(): Pair> { + if (update != null) { + return R.string.update_available_format to listOf( + BuildConfig.VERSION_NAME, + update.versionName, + update.releaseNotes?.let { BetterHtml.fromHtml(activity, it) } ?: "---", + ) + } + return R.string.update_available_fallback to emptyList() + } + + override fun isCancelable() = !mandatory + override fun getPositiveButtonText() = R.string.update_available_button + override fun getNeutralButtonText() = if (mandatory) null else R.string.update_available_later + + override suspend fun onShow() = Unit + + override suspend fun onPositiveClick(): Boolean { + if (update == null || update.isOnGooglePlay) + Utils.openGooglePlay(activity) + else + activity.startService(Intent(app, UpdateDownloaderService::class.java)) + return DISMISS + } + + override suspend fun onBeforeShow(): Boolean { + // show only if app is older than available + return update == null || update.versionCode > BuildConfig.VERSION_CODE + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/UpdateProgressDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/UpdateProgressDialog.kt new file mode 100644 index 00000000..68bd8d54 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/sync/UpdateProgressDialog.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-22. + */ + +package pl.szczodrzynski.edziennik.ui.dialogs.sync + +import android.app.DownloadManager +import android.database.CursorIndexOutOfBoundsException +import android.view.LayoutInflater +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.getSystemService +import kotlinx.coroutines.Job +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update +import pl.szczodrzynski.edziennik.databinding.UpdateProgressDialogBinding +import pl.szczodrzynski.edziennik.ext.getInt +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer +import pl.szczodrzynski.edziennik.sync.UpdateStateEvent +import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog +import pl.szczodrzynski.edziennik.utils.Utils + +class UpdateProgressDialog( + activity: AppCompatActivity, + private val update: Update, + private val downloadId: Long, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BindingDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "UpdateProgressDialog" + + override fun getTitleRes() = R.string.notification_downloading_update + override fun inflate(layoutInflater: LayoutInflater) = + UpdateProgressDialogBinding.inflate(layoutInflater) + + override fun isCancelable() = false + override fun getNegativeButtonText() = R.string.cancel + + private var timerJob: Job? = null + + override suspend fun onShow() { + EventBus.getDefault().register(this) + b.update = update + b.progress.progress = 0 + + val downloadManager = app.getSystemService() ?: return + val query = DownloadManager.Query().setFilterById(downloadId) + + timerJob?.cancel() + timerJob = startCoroutineTimer(repeatMillis = 100L) { + try { + val cursor = downloadManager.query(query) + cursor.moveToFirst() + val progress = cursor.getInt(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR) + ?.toFloat() ?: return@startCoroutineTimer + b.downloadedSize.text = Utils.readableFileSize(progress.toLong()) + val total = cursor.getInt(DownloadManager.COLUMN_TOTAL_SIZE_BYTES) + ?.toFloat() ?: return@startCoroutineTimer + b.totalSize.text = Utils.readableFileSize(total.toLong()) + b.progress.progress = (progress / total * 100.0f).toInt() + } catch (_: CursorIndexOutOfBoundsException) {} + } + } + + override fun onDismiss() { + EventBus.getDefault().unregister(this) + timerJob?.cancel() + } + + override suspend fun onNegativeClick(): Boolean { + val downloadManager = app.getSystemService() ?: return NO_DISMISS + downloadManager.remove(downloadId) + return DISMISS + } + + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) + fun onUpdateStateEvent(event: UpdateStateEvent) { + if (event.downloadId != downloadId) + return + EventBus.getDefault().removeStickyEvent(event) + if (!event.running) + dismiss() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/teacherabsence/TeacherAbsenceDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/teacherabsence/TeacherAbsenceDialog.kt deleted file mode 100644 index 7ffab515..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/teacherabsence/TeacherAbsenceDialog.kt +++ /dev/null @@ -1,57 +0,0 @@ -package pl.szczodrzynski.edziennik.ui.dialogs.teacherabsence - -import android.view.View -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.Observer -import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.databinding.DialogTeacherAbsenceListBinding -import pl.szczodrzynski.edziennik.utils.models.Date - -class TeacherAbsenceDialog( - val activity: AppCompatActivity, - val profileId: Int, - val date: Date, - val onShowListener: ((tag: String) -> Unit)? = null, - val onDismissListener: ((tag: String) -> Unit)? = null -) { - companion object { - private const val TAG = "TeacherAbsenceDialog" - } - - private val app by lazy { activity.application as App } - - private lateinit var b: DialogTeacherAbsenceListBinding - private lateinit var dialog: AlertDialog - - init { run { - if (activity.isFinishing) - return@run - - b = DialogTeacherAbsenceListBinding.inflate(activity.layoutInflater) - - dialog = MaterialAlertDialogBuilder(activity) - .setTitle(date.formattedString) - .setView(b.root) - .setPositiveButton(R.string.close) { dialog, _ -> dialog.dismiss() } - .setOnDismissListener { - onDismissListener?.invoke(TAG) - } - .create() - - b.teacherAbsenceView.setHasFixedSize(true) - b.teacherAbsenceView.layoutManager = LinearLayoutManager(activity) - - app.db.teacherAbsenceDao().getAllByDate(profileId, date).observe(activity as LifecycleOwner, Observer { absenceList -> - val adapter = TeacherAbsenceAdapter(activity, date, absenceList) - b.teacherAbsenceView.adapter = adapter - b.teacherAbsenceView.visibility = View.VISIBLE - }) - - dialog.show() - }} -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt deleted file mode 100644 index 5896def0..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/LessonDetailsDialog.kt +++ /dev/null @@ -1,220 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2019-11-11. - */ - -package pl.szczodrzynski.edziennik.ui.dialogs.timetable - -import android.content.Intent -import android.view.View -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.Observer -import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.db.entity.Lesson -import pl.szczodrzynski.edziennik.data.db.full.LessonFull -import pl.szczodrzynski.edziennik.databinding.DialogLessonDetailsBinding -import pl.szczodrzynski.edziennik.onClick -import pl.szczodrzynski.edziennik.setText -import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog -import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter -import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog -import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment -import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration -import pl.szczodrzynski.edziennik.utils.models.Date -import pl.szczodrzynski.edziennik.utils.models.Week -import kotlin.coroutines.CoroutineContext - -class LessonDetailsDialog( - val activity: AppCompatActivity, - val lesson: LessonFull, - val onShowListener: ((tag: String) -> Unit)? = null, - val onDismissListener: ((tag: String) -> Unit)? = null -) : CoroutineScope { - companion object { - private const val TAG = "LessonDetailsDialog" - } - - private lateinit var app: App - private lateinit var b: DialogLessonDetailsBinding - private lateinit var dialog: AlertDialog - - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - private lateinit var adapter: EventListAdapter - private val manager by lazy { app.timetableManager } - - init { run { - if (activity.isFinishing) - return@run - onShowListener?.invoke(TAG) - app = activity.applicationContext as App - b = DialogLessonDetailsBinding.inflate(activity.layoutInflater) - dialog = MaterialAlertDialogBuilder(activity) - .setView(b.root) - .setPositiveButton(R.string.close) { dialog, _ -> - dialog.dismiss() - } - .setNeutralButton(R.string.add, null) - .setOnDismissListener { - onDismissListener?.invoke(TAG) - } - .show() - - dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.onClick { - EventManualDialog( - activity, - lesson.profileId, - defaultLesson = lesson, - onShowListener = onShowListener, - onDismissListener = onDismissListener - ) - } - - if (App.devMode) - b.lessonId.visibility = View.VISIBLE - - update() - }} - - private fun update() { - b.lesson = lesson - val lessonDate = lesson.displayDate ?: return - val lessonTime = lesson.displayStartTime ?: return - b.lessonDate.text = Week.getFullDayName(lessonDate.weekDay) + ", " + lessonDate.formattedString - - b.annotationVisible = manager.getAnnotation(activity, lesson, b.annotation) - - if (lesson.type >= Lesson.TYPE_SHIFTED_SOURCE) { - b.shiftedLayout.visibility = View.VISIBLE - var otherLessonDate: Date? = null - when (lesson.type) { - Lesson.TYPE_SHIFTED_SOURCE -> { - otherLessonDate = lesson.date - when { - lesson.date != lesson.oldDate -> b.shiftedText.setText( - R.string.timetable_lesson_shifted_other_day, - lesson.date?.stringY_m_d ?: "?", - lesson.startTime?.stringHM ?: "?" - ) - lesson.startTime != lesson.oldStartTime -> b.shiftedText.setText( - R.string.timetable_lesson_shifted_same_day, - lesson.startTime?.stringHM ?: "?" - ) - else -> b.shiftedText.setText(R.string.timetable_lesson_shifted) - } - } - Lesson.TYPE_SHIFTED_TARGET -> { - otherLessonDate = lesson.oldDate - when { - lesson.date != lesson.oldDate -> b.shiftedText.setText( - R.string.timetable_lesson_shifted_from_other_day, - lesson.oldDate?.stringY_m_d ?: "?", - lesson.oldStartTime?.stringHM ?: "?" - ) - lesson.startTime != lesson.oldStartTime -> b.shiftedText.setText( - R.string.timetable_lesson_shifted_from_same_day, - lesson.oldStartTime?.stringHM ?: "?" - ) - else -> b.shiftedText.setText(R.string.timetable_lesson_shifted_from) - } - } - } - b.shiftedGoTo.setOnClickListener { - dialog.dismiss() - val dateStr = otherLessonDate?.stringY_m_d ?: return@setOnClickListener - val intent = Intent(TimetableFragment.ACTION_SCROLL_TO_DATE).apply { - putExtra("timetableDate", dateStr) - } - activity.sendBroadcast(intent) - } - } - else { - b.shiftedLayout.visibility = View.GONE - } - - if (lesson.type < Lesson.TYPE_SHIFTED_SOURCE && lesson.oldSubjectId != null && lesson.subjectId != lesson.oldSubjectId) { - b.oldSubjectName = lesson.oldSubjectName - } - if (lesson.type != Lesson.TYPE_CANCELLED && lesson.displaySubjectId != null) { - b.subjectName = lesson.subjectName - } - - if (lesson.type < Lesson.TYPE_SHIFTED_SOURCE && lesson.oldTeacherId != null && lesson.teacherId != lesson.oldTeacherId) { - b.oldTeacherName = lesson.oldTeacherName - } - if (lesson.type != Lesson.TYPE_CANCELLED && lesson.displayTeacherId != null) { - b.teacherName = lesson.teacherName - } - - if (lesson.oldClassroom != null && lesson.classroom != lesson.oldClassroom) { - b.oldClassroom = lesson.oldClassroom - } - if (lesson.type != Lesson.TYPE_CANCELLED && lesson.displayClassroom != null) { - b.classroom = lesson.classroom - } - - if (lesson.type < Lesson.TYPE_SHIFTED_SOURCE && lesson.oldTeamId != null && lesson.teamId != lesson.oldTeamId) { - b.oldTeamName = lesson.oldTeamName - } - if (lesson.type != Lesson.TYPE_CANCELLED && lesson.displayTeamId != null) { - b.teamName = lesson.teamName - } - - adapter = EventListAdapter( - activity, - showWeekDay = false, - showDate = false, - showType = true, - showTime = true, - showSubject = true, - markAsSeen = true, - onItemClick = { - EventDetailsDialog( - activity, - it, - onShowListener = onShowListener, - onDismissListener = onDismissListener - ) - }, - onEventEditClick = { - EventManualDialog( - activity, - it.profileId, - editingEvent = it, - onShowListener = onShowListener, - onDismissListener = onDismissListener - ) - } - ) - - app.db.eventDao().getAllByDateTime(lesson.profileId, lessonDate, lessonTime).observe(activity, Observer { events -> - adapter.items = events - if (b.eventsView.adapter == null) { - b.eventsView.adapter = adapter - b.eventsView.apply { - isNestedScrollingEnabled = false - setHasFixedSize(true) - layoutManager = LinearLayoutManager(context) - addItemDecoration(SimpleDividerItemDecoration(context)) - } - } - adapter.notifyDataSetChanged() - - if (events != null && events.isNotEmpty()) { - b.eventsView.visibility = View.VISIBLE - b.eventsNoData.visibility = View.GONE - } else { - b.eventsView.visibility = View.GONE - b.eventsNoData.visibility = View.VISIBLE - } - }) - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/error/ErrorDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/error/ErrorDetailsDialog.kt new file mode 100644 index 00000000..10822787 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/error/ErrorDetailsDialog.kt @@ -0,0 +1,68 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-2-16. + */ + +package pl.szczodrzynski.edziennik.ui.error + +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.ui.dialogs.base.BaseDialog + +class ErrorDetailsDialog( + activity: AppCompatActivity, + private val errors: List, + private val titleRes: Int = R.string.dialog_error_details_title, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BaseDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "ErrorDetailsDialog" + + override fun getTitleRes() = titleRes + override fun getMessage() = errors.map { + listOf( + it.getStringReason(activity) + .asBoldSpannable() + .asColoredSpannable(R.attr.colorOnBackground.resolveAttr(activity)), + activity.getString(R.string.error_unknown_format, it.errorCode, it.tag), + if (App.devMode) + it.throwable?.stackTraceString ?: it.throwable?.localizedMessage + else + it.throwable?.localizedMessage + ).concat("\n") + }.concat("\n\n") + + override fun isCancelable() = false + override fun getPositiveButtonText() = R.string.close + override fun getNeutralButtonText() = R.string.report + + override suspend fun onShow() = Unit + + private val api by lazy { SzkolnyApi(activity.applicationContext as App) } + + override suspend fun onBeforeShow(): Boolean { + return errors.isNotEmpty() + } + + override suspend fun onNeutralClick(): Boolean { + api.runCatching({ + withContext(Dispatchers.Default) { + errorReport(errors.map { it.toReportableError(activity) }) + } + }, { + Toast.makeText( + activity, + activity.getString(R.string.crash_report_cannot_send) + it, + Toast.LENGTH_LONG + ).show() + }) + return DISMISS + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/error/ErrorDialog.kt similarity index 96% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorDialog.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/error/ErrorDialog.kt index bf0ff712..20f47522 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/error/ErrorDialog.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2019-11-13. */ -package pl.szczodrzynski.edziennik.ui.modules.error +package pl.szczodrzynski.edziennik.ui.error import android.util.Log import androidx.appcompat.app.AlertDialog diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorSnackbar.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/error/ErrorSnackbar.kt similarity index 95% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorSnackbar.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/error/ErrorSnackbar.kt index be715f4d..89f6eb1d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorSnackbar.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/error/ErrorSnackbar.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2019-11-13. */ -package pl.szczodrzynski.edziennik.ui.modules.error +package pl.szczodrzynski.edziennik.ui.error import android.view.View import androidx.appcompat.app.AppCompatActivity @@ -34,7 +34,7 @@ class ErrorSnackbar(val activity: AppCompatActivity) : CoroutineScope { this.coordinator = coordinatorLayout snackbar = Snackbar.make(coordinator, R.string.snackbar_error_text, Snackbar.LENGTH_INDEFINITE) snackbar?.setAction(R.string.more) { - ErrorDetailsDialog(activity, errors) + ErrorDetailsDialog(activity, errors).show() errors = mutableListOf() } val bgColor = ColorUtils.compositeColors( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventDetailsDialog.kt similarity index 74% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventDetailsDialog.kt index b41da04b..30701f9c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventDetailsDialog.kt @@ -2,12 +2,13 @@ * Copyright (c) Kuba Szczodrzyński 2019-12-18. */ -package pl.szczodrzynski.edziennik.ui.dialogs.event +package pl.szczodrzynski.edziennik.ui.event import android.content.ActivityNotFoundException import android.content.Intent import android.provider.CalendarContract import android.provider.CalendarContract.Events +import android.view.LayoutInflater import android.view.View import android.widget.Toast import androidx.appcompat.app.AlertDialog @@ -22,34 +23,40 @@ import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.events.EventGetEvent import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi -import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.databinding.DialogEventDetailsBinding -import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget +import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog +import pl.szczodrzynski.edziennik.ui.notes.setupNotesButton +import pl.szczodrzynski.edziennik.ui.timetable.TimetableFragment import pl.szczodrzynski.edziennik.utils.BetterLink import pl.szczodrzynski.edziennik.utils.models.Date -import kotlin.coroutines.CoroutineContext +// TODO: 2021-10-19 rewrite to the new dialog style class EventDetailsDialog( - val activity: AppCompatActivity, - val event: EventFull, - val onShowListener: ((tag: String) -> Unit)? = null, - val onDismissListener: ((tag: String) -> Unit)? = null -) : CoroutineScope { - companion object { - private const val TAG = "EventDetailsDialog" - } + activity: AppCompatActivity, + // this event is observed for changes + private var event: EventFull, + private val showNotes: Boolean = true, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BindingDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "EventDetailsDialog" + + override fun getTitleRes(): Int? = null + override fun inflate(layoutInflater: LayoutInflater) = + DialogEventDetailsBinding.inflate(layoutInflater) + + override fun getPositiveButtonText() = R.string.close + override fun getNeutralButtonText() = if (event.addedManually) R.string.remove else null - private lateinit var app: App - private lateinit var b: DialogEventDetailsBinding - private lateinit var dialog: AlertDialog private var removeEventDialog: AlertDialog? = null private val eventShared = event.sharedBy != null private val eventOwn = event.sharedBy == "self" - - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main + private val manager + get() = app.eventManager private val api by lazy { SzkolnyApi(app) @@ -57,41 +64,43 @@ class EventDetailsDialog( private var progressDialog: AlertDialog? = null - init { run { - if (activity.isFinishing) - return@run - onShowListener?.invoke(TAG) + override suspend fun onNeutralClick(): Boolean { + showRemoveEventDialog() + return NO_DISMISS + } + + override suspend fun onBeforeShow(): Boolean { EventBus.getDefault().register(this) - app = activity.applicationContext as App - b = DialogEventDetailsBinding.inflate(activity.layoutInflater) - dialog = MaterialAlertDialogBuilder(activity) - .setView(b.root) - .setPositiveButton(R.string.close) { dialog, _ -> - dialog.dismiss() - } - .apply { - if (event.addedManually) - setNeutralButton(R.string.remove, null) - } - .setOnDismissListener { - onDismissListener?.invoke(TAG) - EventBus.getDefault().unregister(this@EventDetailsDialog) - progressDialog?.dismiss() - } - .show() + return true + } - dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.onClick { - showRemoveEventDialog() + override suspend fun onShow() { + // watch the event for changes + app.db.eventDao().getById(event.profileId, event.id).observe(activity) { + event = it ?: return@observe + update() } + } - update() - }} + override fun onDismiss() { + EventBus.getDefault().unregister(this@EventDetailsDialog) + progressDialog?.dismiss() + } private fun update() { b.event = event b.eventShared = eventShared b.eventOwn = eventOwn + b.topic.text = event.topicHtml + b.body.text = event.bodyHtml + + if (!event.seen) { + manager.markAsSeen(event) + } + + event.filterNotes() + val bullet = " • " val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity) @@ -100,11 +109,24 @@ class EventDetailsDialog( } catch (_: Exception) {} + manager.setLegendText(b.legend, event, showNotes) + b.typeColor.background?.setTintColor(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( @@ -135,6 +157,7 @@ class EventDetailsDialog( launch(Dispatchers.Default) { app.db.eventDao().replace(event) } + update() b.checkDoneButton.isChecked = true } .setNegativeButton(R.string.cancel, null) @@ -145,6 +168,7 @@ class EventDetailsDialog( launch(Dispatchers.Default) { app.db.eventDao().replace(event) } + update() } } b.checkDoneButton.attachToastHint(R.string.hint_mark_as_done) @@ -156,9 +180,18 @@ class EventDetailsDialog( activity, event.profileId, editingEvent = event, + onSaveListener = { + if (it == null) { + dialog.dismiss() + return@EventManualDialog + } + // this should not be needed as the event is observed by the ID + // event = it + // update() + }, onShowListener = onShowListener, onDismissListener = onDismissListener - ) + ).show() } b.editButton.attachToastHint(R.string.hint_edit_event) @@ -174,7 +207,7 @@ class EventDetailsDialog( val dateStr = event.date.stringY_m_d val intent = - if (activity is MainActivity && activity.navTargetId == MainActivity.DRAWER_ITEM_TIMETABLE) + if (activity is MainActivity && activity.navTarget == NavTarget.TIMETABLE) Intent(TimetableFragment.ACTION_SCROLL_TO_DATE) else if (activity is MainActivity) Intent("android.intent.action.MAIN") @@ -182,8 +215,10 @@ class EventDetailsDialog( Intent(activity, MainActivity::class.java) intent.apply { - putExtra("fragmentId", MainActivity.DRAWER_ITEM_TIMETABLE) - putExtra("timetableDate", dateStr) + putExtras( + "fragmentId" to NavTarget.TIMETABLE, + "timetableDate" to dateStr, + ) } if (activity is MainActivity) activity.sendBroadcast(intent) @@ -199,13 +234,17 @@ class EventDetailsDialog( } b.downloadButton.attachToastHint(R.string.hint_download_again) + BetterLink.attach(b.topic, onActionSelected = dialog::dismiss) - b.topic.text = event.topic - BetterLink.attach(b.topic) { - dialog.dismiss() + event.teacherName?.let { name -> + BetterLink.attach( + b.teacherName, + teachers = mapOf(event.teacherId to name), + onActionSelected = dialog::dismiss + ) } - if (event.homeworkBody == null && !event.addedManually && event.type == Event.TYPE_HOMEWORK) { + if (!event.addedManually && (!event.isDownloaded || event.isHomework && event.homeworkBody == null)) { b.bodyTitle.isVisible = true b.bodyProgressBar.isVisible = true b.body.isVisible = false @@ -220,10 +259,7 @@ class EventDetailsDialog( b.bodyTitle.isVisible = true b.bodyProgressBar.isVisible = false b.body.isVisible = true - b.body.text = event.homeworkBody - BetterLink.attach(b.body) { - dialog.dismiss() - } + BetterLink.attach(b.body, onActionSelected = dialog::dismiss) } if (event.attachmentIds.isNullOrEmpty() || event.attachmentNames.isNullOrEmpty()) { @@ -239,6 +275,14 @@ class EventDetailsDialog( it.putStringArray("attachmentNames", event.attachmentNames!!.toTypedArray()) }, owner = event) } + + b.notesButton.isVisible = showNotes + b.notesButton.setupNotesButton( + activity = activity, + owner = event, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ) } @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) @@ -322,8 +366,6 @@ class EventDetailsDialog( removeEventDialog?.dismiss() dialog.dismiss() Toast.makeText(activity, R.string.removed, Toast.LENGTH_SHORT).show() - if (activity is MainActivity && activity.navTargetId == MainActivity.DRAWER_ITEM_AGENDA) - activity.reloadTarget() } private fun openInCalendar() { launch { @@ -334,7 +376,7 @@ class EventDetailsDialog( val intent = Intent(Intent.ACTION_EDIT).apply { data = Events.CONTENT_URI putExtra(Events.TITLE, title) - putExtra(Events.DESCRIPTION, event.topic) + putExtra(Events.DESCRIPTION, event.topicHtml.toString()) if (event.time == null) { putExtra(CalendarContract.EXTRA_EVENT_ALL_DAY, true) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventListAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventListAdapter.kt new file mode 100644 index 00000000..5c2f4eef --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventListAdapter.kt @@ -0,0 +1,74 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2019-11-30 + */ + +package pl.szczodrzynski.edziennik.ui.event + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer +import pl.szczodrzynski.edziennik.ui.search.SearchableAdapter +import kotlin.coroutines.CoroutineContext + +class EventListAdapter( + val activity: AppCompatActivity, + val simpleMode: Boolean = false, + val showWeekDay: Boolean = false, + 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, + val showNotes: Boolean = true, + isReversed: Boolean = false, + val onEventClick: ((event: EventFull) -> Unit)? = null, + val onEventEditClick: ((event: EventFull) -> Unit)? = null, +) : SearchableAdapter(isReversed), CoroutineScope { + companion object { + private const val TAG = "EventListAdapter" + private const val ITEM_TYPE_EVENT = 0 + } + + private val app = activity.applicationContext as App + private val manager + get() = app.eventManager + + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + override fun getItemViewType(item: EventFull) = ITEM_TYPE_EVENT + + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + item: EventFull, + ) { + if (holder !is EventViewHolder) + return + holder.onBind(activity, app, item, position, this) + } + + override fun onCreateViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int, + ) = EventViewHolder(inflater, parent) + + internal fun notifyItemChanged(model: Any) { + startCoroutineTimer(1000L, 0L) { + val index = items.indexOf(model) + if (index != -1) + notifyItemChanged(index) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventManualDialog.kt similarity index 73% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventManualDialog.kt index 97e34009..49553e9a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/event/EventManualDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventManualDialog.kt @@ -2,69 +2,88 @@ * Copyright (c) Kuba Szczodrzyński 2019-11-12. */ -package pl.szczodrzynski.edziennik.ui.dialogs.event +package pl.szczodrzynski.edziennik.ui.event -import android.graphics.PorterDuff -import android.graphics.PorterDuffColorFilter +import android.view.LayoutInflater import android.view.View import android.widget.Toast import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AlertDialog.BUTTON_NEUTRAL import androidx.appcompat.app.AlertDialog.BUTTON_POSITIVE import androidx.appcompat.app.AppCompatActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.jaredrummler.android.colorpicker.ColorPickerDialogListener -import kotlinx.coroutines.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_AGENDA +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 import pl.szczodrzynski.edziennik.data.api.events.ApiTaskFinishedEvent import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.data.db.entity.Event -import pl.szczodrzynski.edziennik.data.db.entity.EventType import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.entity.Subject +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType 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.ui.dialogs.sync.RegistrationConfigDialog -import pl.szczodrzynski.edziennik.ui.modules.views.TimeDropdown.Companion.DISPLAY_LESSONS +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 +import pl.szczodrzynski.edziennik.ui.dialogs.settings.RegistrationConfigDialog +import pl.szczodrzynski.edziennik.ui.views.TimeDropdown.Companion.DISPLAY_LESSONS import pl.szczodrzynski.edziennik.utils.Anim -import pl.szczodrzynski.edziennik.utils.TextInputDropDown +import pl.szczodrzynski.edziennik.utils.html.BetterHtml +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.HtmlMode.SIMPLE +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.StylingConfigBase import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time -import kotlin.coroutines.CoroutineContext +// TODO: 2021-10-19 rewrite to the new dialog style class EventManualDialog( - val activity: AppCompatActivity, - val profileId: Int, - val defaultLesson: LessonFull? = null, - val defaultDate: Date? = null, - val defaultTime: Time? = null, - val defaultType: Long? = null, - val editingEvent: EventFull? = null, - val onShowListener: ((tag: String) -> Unit)? = null, - val onDismissListener: ((tag: String) -> Unit)? = null -) : CoroutineScope { + activity: AppCompatActivity, + private val profileId: Int, + private val defaultLesson: LessonFull? = null, + private val defaultDate: Date? = null, + private val defaultTime: Time? = null, + private val defaultType: Long? = null, + private val editingEvent: EventFull? = null, + private val onSaveListener: ((event: EventFull?) -> Unit)? = null, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BindingDialog(activity, onShowListener, onDismissListener) { - companion object { - private const val TAG = "EventManualDialog" - } + override val TAG = "EventManualDialog" - private val job: Job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main + override fun getTitleRes() = R.string.dialog_event_manual_title + override fun inflate(layoutInflater: LayoutInflater) = + DialogEventManualV2Binding.inflate(layoutInflater) + + override fun isCancelable() = false + override fun getPositiveButtonText() = R.string.save + override fun getNeutralButtonText() = if (editingEvent != null) R.string.remove else null + override fun getNegativeButtonText() = R.string.cancel - private val app by lazy { activity.application as App } - private lateinit var b: DialogEventManualV2Binding - private lateinit var dialog: AlertDialog private lateinit var profile: Profile + private lateinit var stylingConfig: StylingConfigBase + + private val textStylingManager + get() = app.textStylingManager private var customColor: Int? = null private val editingShared = editingEvent?.sharedBy != null @@ -80,48 +99,36 @@ class EventManualDialog( private var progressDialog: AlertDialog? = null - init { launch { - if (activity.isFinishing) - return@launch - onShowListener?.invoke(TAG) + override suspend fun onPositiveClick(): Boolean { + saveEvent() + return NO_DISMISS + } + + override suspend fun onNeutralClick(): Boolean { + showRemoveEventDialog() + return NO_DISMISS + } + + override suspend fun onBeforeShow(): Boolean { EventBus.getDefault().register(this@EventManualDialog) - b = DialogEventManualV2Binding.inflate(activity.layoutInflater) - dialog = MaterialAlertDialogBuilder(activity) - .setTitle(R.string.dialog_event_manual_title) - .setView(b.root) - .setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() } - .setPositiveButton(R.string.save, null) - .apply { - if (editingEvent != null) { - setNeutralButton(R.string.remove, null) - } - } - .setOnDismissListener { - onDismissListener?.invoke(TAG) - EventBus.getDefault().unregister(this@EventManualDialog) - enqueuedWeekDialog?.dismiss() - progressDialog?.dismiss() - } - .setCancelable(false) - .create() - .apply { - setOnShowListener { dialog -> - val positiveButton = (dialog as AlertDialog).getButton(BUTTON_POSITIVE) - positiveButton?.setOnClickListener { - saveEvent() - } + return true + } - val neutralButton = dialog.getButton(BUTTON_NEUTRAL) - neutralButton?.setOnClickListener { - showRemoveEventDialog() - } - } + override fun onDismiss() { + EventBus.getDefault().unregister(this@EventManualDialog) + enqueuedWeekDialog?.dismiss() + progressDialog?.dismiss() + } - show() - } - - b.shareSwitch.isChecked = editingShared - b.shareSwitch.isEnabled = !editingShared || (editingShared && editingOwn) + 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 { @@ -134,13 +141,28 @@ class EventManualDialog( } } + textStylingManager.attachToField( + activity = activity, + textLayout = b.topicLayout, + textEdit = b.topic, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ) + + stylingConfig = StylingConfigBase(editText = b.topic, htmlMode = SIMPLE) + updateShareText() b.shareSwitch.onChange { _, isChecked -> updateShareText(isChecked) } loadLists() - }} + + val shareByDefault = app.profile.config.shareByDefault && profile.canShare + + b.shareSwitch.isChecked = editingShared || editingEvent == null && shareByDefault + b.shareSwitch.isEnabled = !editingShared || editingOwn + } private fun updateShareText(checked: Boolean = b.shareSwitch.isChecked) { b.shareDetails.visibility = if (checked || editingShared) @@ -175,9 +197,7 @@ class EventManualDialog( EdziennikTask.syncProfile( profileId = profileId, - viewIds = listOf( - MainActivity.DRAWER_ITEM_TIMETABLE to 0 - ), + featureTypes = setOf(FeatureType.TIMETABLE), arguments = JsonObject( "weekStart" to weekStart.stringY_m_d ) @@ -236,13 +256,13 @@ class EventManualDialog( progressDialog?.dismiss() } - private fun loadLists() = launch { + private suspend fun loadLists() { val profile = withContext(Dispatchers.Default) { app.db.profileDao().getByIdNow(profileId) } if (profile == null) { Toast.makeText(activity, R.string.event_manual_no_profile, Toast.LENGTH_SHORT).show() - return@launch + return } this@EventManualDialog.profile = profile @@ -323,57 +343,41 @@ class EventManualDialog( selectDefault(defaultLesson?.displayTeacherId) } + with (b.typeDropdown) { + db = app.db + profileId = this@EventManualDialog.profileId + loadItems() + selectDefault(editingEvent?.type) + selectDefault(defaultType) - val deferred = async(Dispatchers.Default) { - // get the event type list - var eventTypes = app.db.eventTypeDao().getAllNow(profileId) - - if (eventTypes.none { it.id in -1L..10L }) { - eventTypes = app.db.eventTypeDao().addDefaultTypes(activity, profileId) + onTypeSelected = { + b.typeColor.background.setTintColor(it.color) + customColor = null } - - b.typeDropdown.clear() - b.typeDropdown += eventTypes.map { TextInputDropDown.Item(it.id, it.name, tag = it) } - } - deferred.await() - - b.typeDropdown.isEnabled = true - - defaultType?.let { - b.typeDropdown.select(it) } - b.typeDropdown.selected?.let { item -> - customColor = (item.tag as EventType).color - } - - // copy IDs from event being edited + // copy data from event being edited editingEvent?.let { - b.topic.setText(it.topic) - b.typeDropdown.select(it.type)?.let { item -> - customColor = (item.tag as EventType).color - } - if (it.color != null && it.color != -1) + b.topic.setText(BetterHtml.fromHtml(activity, it.topic, nl2br = true)) + if (it.color != -1) customColor = it.color } + b.typeColor.background.setTintColor( + customColor + ?: b.typeDropdown.getSelected()?.color + ?: Event.COLOR_DEFAULT + ) + // copy IDs from the LessonFull defaultLesson?.let { b.teamDropdown.select(it.displayTeamId) } - b.typeDropdown.setOnChangeListener { - b.typeColor.background.colorFilter = PorterDuffColorFilter((it.tag as EventType).color, PorterDuff.Mode.SRC_ATOP) - customColor = null - return@setOnChangeListener true - } - - (customColor ?: Event.COLOR_DEFAULT).let { - b.typeColor.background.colorFilter = PorterDuffColorFilter(it, PorterDuff.Mode.SRC_ATOP) - } - b.typeColor.onClick { - val currentColor = (b.typeDropdown.selected?.tag as EventType?)?.color ?: Event.COLOR_DEFAULT + val currentColor = customColor + ?: b.typeDropdown.getSelected()?.color + ?: Event.COLOR_DEFAULT val colorPickerDialog = ColorPickerDialog.newBuilder() .setColor(currentColor) .create() @@ -381,7 +385,7 @@ class EventManualDialog( object : ColorPickerDialogListener { override fun onDialogDismissed(dialogId: Int) {} override fun onColorSelected(dialogId: Int, color: Int) { - b.typeColor.background.colorFilter = PorterDuffColorFilter(color, PorterDuff.Mode.SRC_ATOP) + b.typeColor.background.setTintColor(color) customColor = color } }) @@ -416,15 +420,15 @@ class EventManualDialog( private fun saveEvent() { val date = b.dateDropdown.getSelected() as? Date val timeSelected = b.timeDropdown.getSelected() - val teamId = b.teamDropdown.getSelected() as? Long - val type = b.typeDropdown.selected?.id + val team = b.teamDropdown.getSelected() + val type = b.typeDropdown.getSelected() val topic = b.topic.text?.toString() - val subjectId = b.subjectDropdown.getSelected() as? Long - val teacherId = b.teacherDropdown.getSelected() + val subject = b.subjectDropdown.getSelected() as? Subject + val teacher = b.teacherDropdown.getSelected() val share = b.shareSwitch.isChecked - if (share && profile.registration != Profile.REGISTRATION_ENABLED) { + if (share && !profile.canShare) { RegistrationConfigDialog(activity, profile, onChangeListener = { enabled -> if (enabled) saveEvent() @@ -451,7 +455,7 @@ class EventManualDialog( isError = true } - if (share && teamId == null) { + if (share && team == null) { b.teamDropdown.error = app.getString(R.string.dialog_event_manual_team_choose) if (!isError) b.teamDropdown.parent.requestChildFocus(b.teamDropdown, b.teamDropdown) isError = true @@ -480,17 +484,18 @@ class EventManualDialog( val id = System.currentTimeMillis() + val topicHtml = textStylingManager.getHtmlText(stylingConfig) val eventObject = Event( profileId = profileId, id = editingEvent?.id ?: id, date = date, time = startTime, - topic = topic, + topic = topicHtml, color = customColor, - type = type ?: Event.TYPE_DEFAULT, - teacherId = teacherId ?: -1, - subjectId = subjectId ?: -1, - teamId = teamId ?: -1, + type = type?.id ?: Event.TYPE_DEFAULT, + teacherId = teacher?.id ?: -1, + subjectId = subject?.id ?: -1, + teamId = team?.id ?: -1, addedDate = editingEvent?.addedDate ?: System.currentTimeMillis() ).also { it.addedManually = true @@ -498,9 +503,9 @@ class EventManualDialog( val metadataObject = Metadata( profileId, - when (type) { - Event.TYPE_HOMEWORK -> Metadata.TYPE_HOMEWORK - else -> Metadata.TYPE_EVENT + when (type?.id) { + Event.TYPE_HOMEWORK -> MetadataType.HOMEWORK + else -> MetadataType.EVENT }, eventObject.id, true, @@ -597,10 +602,14 @@ class EventManualDialog( } } - dialog.dismiss() + onSaveListener?.invoke(eventObject.withMetadata(metadataObject).also { + it.subjectLongName = (b.subjectDropdown.getSelected() as? Subject)?.longName + it.teacherName = b.teacherDropdown.getSelected()?.fullName + it.teamName = b.teamDropdown.getSelected()?.name + it.typeName = b.typeDropdown.getSelected()?.name + }) + dismiss() Toast.makeText(activity, R.string.saved, Toast.LENGTH_SHORT).show() - if (activity is MainActivity && activity.navTargetId == DRAWER_ITEM_AGENDA) - activity.reloadTarget() } private fun finishRemoving() { editingEvent ?: return @@ -611,9 +620,8 @@ class EventManualDialog( } removeEventDialog?.dismiss() - dialog.dismiss() + onSaveListener?.invoke(null) + dismiss() Toast.makeText(activity, R.string.removed, Toast.LENGTH_SHORT).show() - if (activity is MainActivity && activity.navTargetId == DRAWER_ITEM_AGENDA) - activity.reloadTarget() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventViewHolder.kt new file mode 100644 index 00000000..f22be345 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventViewHolder.kt @@ -0,0 +1,138 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-10. + */ + +package pl.szczodrzynski.edziennik.ui.event + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.full.EventFull +import pl.szczodrzynski.edziennik.databinding.EventListItemBinding +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.ui.grades.viewholder.BindableViewHolder +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Week + +class EventViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + val b: EventListItemBinding = EventListItemBinding.inflate(inflater, parent, false), +) : RecyclerView.ViewHolder(b.root), BindableViewHolder { + companion object { + private const val TAG = "EventViewHolder" + } + + override fun onBind( + activity: AppCompatActivity, + app: App, + item: EventFull, + position: Int, + adapter: EventListAdapter, + ) { + val manager = app.eventManager + + if (adapter.onEventClick != null) { + b.root.onClick { + adapter.onEventClick.invoke(item) + if (!item.seen) { + manager.markAsSeen(item) + } + if (item.showAsUnseen == true) { + item.showAsUnseen = false + adapter.notifyItemChanged(item) + } + } + } + + val bullet = " • " + val colorHighlight = R.attr.colorControlHighlight.resolveAttr(activity) + + b.simpleMode = adapter.simpleMode + + manager.setEventTopic(b.topic, item, showType = false, showNotes = adapter.showNotes) + b.topic.text = adapter.highlightSearchText( + item = item, + text = b.topic.text, + color = colorHighlight + ) + b.topic.maxLines = if (adapter.simpleMode) 2 else 3 + + b.details.text = mutableListOf( + if (adapter.showWeekDay) + Week.getFullDayName(item.date.weekDay) + else null, + if (adapter.showDate) + item.date.getRelativeString(activity, 7) ?: item.date.formattedStringShort + else null, + if (adapter.showType && item.typeName != null) + item.typeName + else null, + if (adapter.showTime) + item.time?.stringHM ?: app.getString(R.string.event_all_day) + else null, + if (adapter.showSubject && item.subjectLongName != null) + adapter.highlightSearchText( + item = item, + text = item.subjectLongName ?: "", + color = colorHighlight + ) + else null, + ).concat(bullet) + + val addedBy = item.sharedByName ?: item.teacherName ?: "" + b.addedBy.setText( + when (item.sharedBy) { + null -> when { + item.addedManually -> R.string.event_list_added_by_self_format + item.teacherName == null -> R.string.event_list_added_by_unknown_format + else -> R.string.event_list_added_by_format + } + "self" -> R.string.event_list_shared_by_self_format + else -> R.string.event_list_shared_by_format + }, + /* 1$ */ + Date.fromMillis(item.addedDate).formattedString, + /* 2$ */ + addedBy, + /* 3$ */ + item.teamName?.let { bullet + it } ?: "", + ) + // workaround for the span data lost during setText above + val addedBySpanned = adapter.highlightSearchText( + item = item, + text = addedBy, + color = colorHighlight + ) + b.addedBy.text = b.addedBy.text.replaceSpanned(addedBy, addedBySpanned) + + b.attachmentIcon.isVisible = item.hasAttachments + + b.typeColor.background?.setTintColor(item.eventColor) + b.typeColor.isVisible = adapter.showTypeColor + + b.editButton.isVisible = !adapter.simpleMode + && item.addedManually + && !item.isDone + && adapter.onEventEditClick != null + + if (adapter.onEventEditClick != null) { + b.editButton.onClick { + adapter.onEventEditClick.invoke(item) + } + b.editButton.attachToastHint(R.string.hint_edit_event) + } + + if (item.showAsUnseen == null) + item.showAsUnseen = !item.seen + + b.unread.isVisible = item.showAsUnseen == true + if (adapter.markAsSeen && !item.seen) { + manager.markAsSeen(item) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/feedback/FeedbackActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/feedback/FeedbackActivity.kt similarity index 93% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/feedback/FeedbackActivity.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/feedback/FeedbackActivity.kt index fb751e3a..d27b4dd0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/feedback/FeedbackActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/feedback/FeedbackActivity.kt @@ -1,4 +1,4 @@ -package pl.szczodrzynski.edziennik.ui.modules.feedback +package pl.szczodrzynski.edziennik.ui.feedback import android.os.Bundle import android.view.MenuItem diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/feedback/FeedbackFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/feedback/FeedbackFragment.kt similarity index 98% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/feedback/FeedbackFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/feedback/FeedbackFragment.kt index 3c33be8d..c9089c45 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/feedback/FeedbackFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/feedback/FeedbackFragment.kt @@ -1,4 +1,4 @@ -package pl.szczodrzynski.edziennik.ui.modules.feedback +package pl.szczodrzynski.edziennik.ui.feedback import android.content.BroadcastReceiver import android.graphics.Bitmap @@ -25,12 +25,12 @@ import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.crc16 import pl.szczodrzynski.edziennik.data.api.events.FeedbackMessageEvent import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.data.db.entity.FeedbackMessage import pl.szczodrzynski.edziennik.databinding.FragmentFeedbackBinding -import pl.szczodrzynski.edziennik.onClick +import pl.szczodrzynski.edziennik.ext.crc16 +import pl.szczodrzynski.edziennik.ext.onClick import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.Utils.openUrl import java.util.* diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradeDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradeDetailsDialog.kt new file mode 100644 index 00000000..db77fe66 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradeDetailsDialog.kt @@ -0,0 +1,108 @@ +package pl.szczodrzynski.edziennik.ui.grades + +import android.view.LayoutInflater +import androidx.appcompat.app.AppCompatActivity +import androidx.core.graphics.ColorUtils +import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.full.GradeFull +import pl.szczodrzynski.edziennik.databinding.DialogGradeDetailsBinding +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ext.setTintColor +import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog +import pl.szczodrzynski.edziennik.ui.dialogs.settings.GradesConfigDialog +import pl.szczodrzynski.edziennik.ui.notes.setupNotesButton +import pl.szczodrzynski.edziennik.utils.BetterLink +import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration +import pl.szczodrzynski.edziennik.utils.managers.NoteManager + +class GradeDetailsDialog( + activity: AppCompatActivity, + private val grade: GradeFull, + private val showNotes: Boolean = true, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BindingDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "GradeDetailsDialog" + + override fun getTitleRes(): Int? = null + override fun inflate(layoutInflater: LayoutInflater) = + DialogGradeDetailsBinding.inflate(layoutInflater) + + override fun getPositiveButtonText() = R.string.close + + override suspend fun onShow() { + val manager = app.gradesManager + + val gradeColor = manager.getGradeColor(grade) + b.grade = grade + b.weightText = manager.getWeightString(app, grade) + b.commentVisible = false + b.devMode = App.devMode + b.gradeName.setTextColor( + if (ColorUtils.calculateLuminance(gradeColor) > 0.3) + 0xaa000000.toInt() + else + 0xccffffff.toInt() + ) + b.gradeName.background.setTintColor(gradeColor) + + b.gradeValue = if (grade.weight == 0f || grade.value < 0f) + -1f + else + manager.getGradeValue(grade) + + b.customValueDivider.isVisible = manager.plusValue != null || manager.minusValue != null + b.customValueLayout.isVisible = b.customValueDivider.isVisible + b.customValueButton.onClick { + GradesConfigDialog(activity, reloadOnDismiss = true).show() + } + + grade.teacherName?.let { name -> + BetterLink.attach( + b.teacherName, + teachers = mapOf(grade.teacherId to name), + onActionSelected = dialog::dismiss + ) + } + + val historyList = withContext(Dispatchers.Default) { + app.db.gradeDao().getByParentIdNow(App.profileId, grade.id) + } + + historyList.forEach { + it.filterNotes() + } + + b.historyVisible = historyList.isNotEmpty() + if (historyList.isNotEmpty()) { + b.gradeHistoryList.adapter = GradesAdapter(activity, onGradeClick = { + GradeDetailsDialog(activity, it).show() + }).also { + it.items = historyList.toMutableList() + } + + b.gradeHistoryList.apply { + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + addItemDecoration(SimpleDividerItemDecoration(context)) + } + } + + b.notesButton.isVisible = showNotes + b.notesButton.setupNotesButton( + activity = activity, + owner = grade, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ) + b.legend.isVisible = showNotes + if (showNotes) + NoteManager.setLegendText(grade, b.legend) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradeView.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradeView.kt similarity index 95% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradeView.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradeView.kt index b80ad2ac..e9f35c16 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradeView.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradeView.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-3-1. */ -package pl.szczodrzynski.edziennik.ui.modules.grades +package pl.szczodrzynski.edziennik.ui.grades import android.annotation.SuppressLint import android.content.Context @@ -23,9 +23,9 @@ import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_SEMESTER2_ import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_SEMESTER2_PROPOSED import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_YEAR_FINAL import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_YEAR_PROPOSED -import pl.szczodrzynski.edziennik.dp -import pl.szczodrzynski.edziennik.resolveAttr -import pl.szczodrzynski.edziennik.setTintColor +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ext.resolveAttr +import pl.szczodrzynski.edziennik.ext.setTintColor import pl.szczodrzynski.edziennik.utils.managers.GradesManager class GradeView : AppCompatTextView { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesAdapter.kt similarity index 85% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesAdapter.kt index 46c75d8d..05bb1da0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesAdapter.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-2-29. */ -package pl.szczodrzynski.edziennik.ui.modules.grades +package pl.szczodrzynski.edziennik.ui.grades import android.animation.ObjectAnimator import android.view.LayoutInflater @@ -17,14 +17,15 @@ import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.Grade import pl.szczodrzynski.edziennik.data.db.full.GradeFull -import pl.szczodrzynski.edziennik.onClick -import pl.szczodrzynski.edziennik.startCoroutineTimer -import pl.szczodrzynski.edziennik.ui.modules.grades.models.* -import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.* +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer +import pl.szczodrzynski.edziennik.ui.grades.models.* +import pl.szczodrzynski.edziennik.ui.grades.viewholder.* import kotlin.coroutines.CoroutineContext class GradesAdapter( val activity: AppCompatActivity, + val showNotes: Boolean = true, var onGradeClick: ((item: GradeFull) -> Unit)? = null, var onGradesEditorClick: ((subject: GradesSubject, semester: GradesSemester) -> Unit)? = null ) : RecyclerView.Adapter(), CoroutineScope { @@ -35,12 +36,14 @@ class GradesAdapter( private const val ITEM_TYPE_EMPTY = 2 private const val ITEM_TYPE_GRADE = 3 private const val ITEM_TYPE_STATS = 4 + private const val ITEM_TYPE_UNKNOWN_SUBJECT = 5 const val STATE_CLOSED = 0 const val STATE_OPENED = 1 } private val app = activity.applicationContext as App - private val manager = app.gradesManager + private val manager + get() = app.gradesManager private val job = Job() override val coroutineContext: CoroutineContext @@ -56,6 +59,7 @@ class GradesAdapter( ITEM_TYPE_EMPTY -> EmptyViewHolder(inflater, parent) ITEM_TYPE_GRADE -> GradeViewHolder(inflater, parent) ITEM_TYPE_STATS -> StatsViewHolder(inflater, parent) + ITEM_TYPE_UNKNOWN_SUBJECT -> UnknownSubjectViewHolder(inflater, parent) else -> throw IllegalArgumentException("Incorrect viewType") } } @@ -67,6 +71,7 @@ class GradesAdapter( is GradesEmpty -> ITEM_TYPE_EMPTY is Grade -> ITEM_TYPE_GRADE is GradesStats -> ITEM_TYPE_STATS + is GradesUnknownSubject -> ITEM_TYPE_UNKNOWN_SUBJECT else -> throw IllegalArgumentException("Incorrect viewType") } } @@ -84,7 +89,7 @@ class GradesAdapter( fun expandModel(model: ExpandableItemModel<*>?, view: View?, notifyAdapter: Boolean = true) { model ?: return - val position = items.indexOf(model) + var position = items.indexOf(model) if (position == -1) return @@ -136,9 +141,16 @@ class GradesAdapter( else -> model.items } + if (model is GradesSubject && model.isUnknown) { + position++ + items.add(position, GradesUnknownSubject()) + if (notifyAdapter) notifyItemInserted(position) + } + + position++ model.state = STATE_OPENED - items.addAll(position + 1, subItems.filterNotNull()) - if (notifyAdapter) notifyItemRangeInserted(position + 1, subItems.size) + items.addAll(position, subItems.filterNotNull()) + if (notifyAdapter) notifyItemRangeInserted(position, subItems.size) if (model is GradesSubject) { // auto expand first semester @@ -154,9 +166,10 @@ class GradesAdapter( else -> semester.grades } + position++ semester.state = STATE_OPENED - items.addAll(position + 2 + semesterIndex, grades) - if (notifyAdapter) notifyItemRangeInserted(position + 2 + semesterIndex, grades.size) + items.addAll(position + semesterIndex, grades) + if (notifyAdapter) notifyItemRangeInserted(position + semesterIndex, grades.size) } } } @@ -196,6 +209,7 @@ class GradesAdapter( is EmptyViewHolder -> ITEM_TYPE_EMPTY is GradeViewHolder -> ITEM_TYPE_GRADE is StatsViewHolder -> ITEM_TYPE_STATS + is UnknownSubjectViewHolder -> ITEM_TYPE_UNKNOWN_SUBJECT else -> throw IllegalArgumentException("Incorrect viewType") } holder.itemView.setTag(R.string.tag_key_view_type, viewType) @@ -208,6 +222,7 @@ class GradesAdapter( holder is EmptyViewHolder && item is GradesEmpty -> holder.onBind(activity, app, item, position, this) holder is GradeViewHolder && item is GradeFull -> holder.onBind(activity, app, item, position, this) holder is StatsViewHolder && item is GradesStats -> holder.onBind(activity, app, item, position, this) + holder is UnknownSubjectViewHolder && item is GradesUnknownSubject -> holder.onBind(activity, app, item, position, this) } if (holder is SemesterViewHolder && item is GradesSemester) { @@ -217,7 +232,10 @@ class GradesAdapter( } } - holder.itemView.setOnClickListener(onClickListener) + if (item !is GradeFull || onGradeClick != null) + holder.itemView.setOnClickListener(onClickListener) + else + holder.itemView.setOnClickListener(null) } fun notifyItemChanged(model: Any) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesListFragment.kt similarity index 85% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesListFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesListFragment.kt index 63521e97..905abefd 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/GradesListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesListFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-3-4. */ -package pl.szczodrzynski.edziennik.ui.modules.grades +package pl.szczodrzynski.edziennik.ui.grades import android.os.AsyncTask import android.os.Bundle @@ -17,17 +17,17 @@ import androidx.recyclerview.widget.LinearLayoutManager import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import kotlinx.coroutines.* import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.MainActivity.Companion.TARGET_GRADES_EDITOR import pl.szczodrzynski.edziennik.data.db.entity.Grade -import pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_GRADE +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.data.db.full.GradeFull import pl.szczodrzynski.edziennik.databinding.GradesListFragmentBinding -import pl.szczodrzynski.edziennik.ui.dialogs.grade.GradeDetailsDialog -import pl.szczodrzynski.edziennik.ui.dialogs.grade.GradesConfigDialog -import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesAverages -import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesSemester -import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesStats -import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesSubject +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget +import pl.szczodrzynski.edziennik.ui.dialogs.settings.GradesConfigDialog +import pl.szczodrzynski.edziennik.ui.grades.models.GradesAverages +import pl.szczodrzynski.edziennik.ui.grades.models.GradesSemester +import pl.szczodrzynski.edziennik.ui.grades.models.GradesStats +import pl.szczodrzynski.edziennik.ui.grades.models.GradesSubject import pl.szczodrzynski.edziennik.utils.managers.GradesManager import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem @@ -48,9 +48,12 @@ class GradesListFragment : Fragment(), CoroutineScope { get() = job + Dispatchers.Main // local/private variables go here - private val manager by lazy { app.gradesManager } - private val dontCountEnabled by lazy { manager.dontCountEnabled } - private val dontCountGrades by lazy { manager.dontCountGrades } + private val manager + get() = app.gradesManager + private val dontCountEnabled + get() = manager.dontCountEnabled + private val dontCountGrades + get() = manager.dontCountGrades private var expandSubjectId = 0L override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -73,8 +76,12 @@ class GradesListFragment : Fragment(), CoroutineScope { app.db.gradeDao().getAllOrderBy(App.profileId, app.gradesManager.getOrderByString()).observe(viewLifecycleOwner, Observer { grades -> this@GradesListFragment.launch { if (!isAdded) return@launch + grades.forEach { + it.filterNotes() + } + val items = when { - app.config.forProfile().grades.hideSticksFromOld && App.devMode -> grades.filter { it.value != 1.0f } + app.profile.config.grades.hideSticksFromOld && App.devMode -> grades.filter { it.value != 1.0f } else -> grades } @@ -107,7 +114,7 @@ class GradesListFragment : Fragment(), CoroutineScope { }}) adapter.onGradeClick = { - GradeDetailsDialog(activity, it) + GradeDetailsDialog(activity, it).show() } adapter.onGradesEditorClick = { subject, semester -> @@ -119,7 +126,7 @@ class GradesListFragment : Fragment(), CoroutineScope { gradeCountOtherSemester = otherSemester?.averages?.normalCount?.toFloat() } - activity.loadTarget(TARGET_GRADES_EDITOR, Bundle( + activity.navigate(navTarget = NavTarget.GRADES_EDITOR, args = Bundle( "subjectId" to subject.subjectId, "semester" to semester.number, "averageMode" to manager.yearAverageMode, @@ -137,7 +144,7 @@ class GradesListFragment : Fragment(), CoroutineScope { .withIcon(CommunityMaterial.Icon.cmd_cog_outline) .withOnClickListener(View.OnClickListener { activity.bottomSheet.close() - GradesConfigDialog(activity, true, null, null) + GradesConfigDialog(activity, true, null, null).show() }), BottomSheetSeparatorItem(true), BottomSheetPrimaryItem(true) @@ -145,7 +152,7 @@ class GradesListFragment : Fragment(), CoroutineScope { .withIcon(CommunityMaterial.Icon.cmd_eye_check_outline) .withOnClickListener(View.OnClickListener { activity.bottomSheet.close() - AsyncTask.execute { App.db.metadataDao().setAllSeen(App.profileId, TYPE_GRADE, true) } + AsyncTask.execute { App.db.metadataDao().setAllSeen(App.profileId, MetadataType.GRADE, true) } Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show() }) ) @@ -175,6 +182,7 @@ class GradesListFragment : Fragment(), CoroutineScope { @Suppress("SuspendFunctionOnCoroutineScope") private fun processGrades(grades: List): MutableList { val items = mutableListOf() + var unknownSubjectItem: GradesSubject? = null var subjectId = -1L var semesterNumber = 0 @@ -193,17 +201,31 @@ class GradesListFragment : Fragment(), CoroutineScope { subjectId = grade.subjectId semesterNumber = 0 - subject = items.firstOrNull { it.subjectId == subjectId } - ?: GradesSubject(grade.subjectId, grade.subjectLongName ?: "").also { + subject = items.firstOrNull { it.subjectId == subjectId } ?: run { + if (grade.subjectLongName != null) { + return@run GradesSubject(grade.subjectId, grade.subjectLongName!!).also { items += it it.semester = 2 } + } + if (unknownSubjectItem == null) { + unknownSubjectItem = GradesSubject(-1, "unknown").also { + items += it + it.semester = 2 + it.isUnknown = true + } + } + return@run unknownSubjectItem!! + } } if (grade.semester != semesterNumber) { semesterNumber = grade.semester semester = subject.semesters.firstOrNull { it.number == semesterNumber } - ?: GradesSemester(subject.subjectId, grade.semester).also { subject.semesters += it } + ?: GradesSemester(subject.subjectId, grade.semester).also { + subject.semesters += it + it.hideEditor = subject.isUnknown + } } grade.showAsUnseen = !grade.seen @@ -214,6 +236,11 @@ class GradesListFragment : Fragment(), CoroutineScope { semester.hasUnseen = true } + if (subject.isUnknown) { + // unknown subjects may have final grades (i.e. Mobidziennik) + grade.type = Grade.TYPE_NORMAL + } + when (grade.type) { Grade.TYPE_SEMESTER1_PROPOSED, Grade.TYPE_SEMESTER2_PROPOSED -> semester.proposedGrade = grade @@ -248,6 +275,10 @@ class GradesListFragment : Fragment(), CoroutineScope { val yearlyPoint = mutableListOf() for (item in items) { + if (item.isUnknown) { + // do not count averages for "unknown" subjects + continue + } item.semesters.forEach { sem -> manager.calculateAverages(sem.averages) if (sem.number == 1) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/editor/GradesEditorAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/editor/GradesEditorAdapter.kt similarity index 96% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/editor/GradesEditorAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/editor/GradesEditorAdapter.kt index df9faad5..22a7b7e8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/editor/GradesEditorAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/editor/GradesEditorAdapter.kt @@ -1,4 +1,4 @@ -package pl.szczodrzynski.edziennik.ui.modules.grades.editor +package pl.szczodrzynski.edziennik.ui.grades.editor import android.content.Context import android.graphics.PorterDuff @@ -14,7 +14,7 @@ import com.daimajia.swipe.SwipeLayout import com.mikepenz.iconics.view.IconicsImageView import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.ui.modules.grades.editor.GradesEditorFragment.Companion.modifyGradeChooser +import pl.szczodrzynski.edziennik.ui.grades.editor.GradesEditorFragment.Companion.modifyGradeChooser import pl.szczodrzynski.edziennik.utils.Colors.gradeNameToColor import java.text.DecimalFormat diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/editor/GradesEditorFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/editor/GradesEditorFragment.kt similarity index 96% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/editor/GradesEditorFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/editor/GradesEditorFragment.kt index 2d90f4f9..154a42bf 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/editor/GradesEditorFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/editor/GradesEditorFragment.kt @@ -1,4 +1,4 @@ -package pl.szczodrzynski.edziennik.ui.modules.grades.editor +package pl.szczodrzynski.edziennik.ui.grades.editor import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter @@ -12,10 +12,15 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.dialog.MaterialAlertDialogBuilder -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.Grade import pl.szczodrzynski.edziennik.databinding.FragmentGradesEditorBinding -import pl.szczodrzynski.edziennik.ui.dialogs.input +import pl.szczodrzynski.edziennik.ext.getFloat +import pl.szczodrzynski.edziennik.ext.getInt +import pl.szczodrzynski.edziennik.ext.getLong +import pl.szczodrzynski.edziennik.ext.input import pl.szczodrzynski.edziennik.utils.Colors import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_AVG_2_AVG import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_1_AVG_2_SEM @@ -34,7 +39,7 @@ class GradesEditorFragment : Fragment() { private val navController: NavController by lazy { Navigation.findNavController(b.root) } */ - private val config by lazy { app.config.getFor(App.profileId).grades } + private val config by lazy { app.profile.config.grades } private var subjectId: Long = -1 private var semester: Int = 1 @@ -106,7 +111,7 @@ class GradesEditorFragment : Fragment() { continue } var weight = editorGrade.weight - if (config.dontCountEnabled && config.dontCountGrades.contains(editorGrade.name.toLowerCase().trim())) { + if (config.dontCountEnabled && config.dontCountGrades.contains(editorGrade.name.lowercase().trim())) { weight = 0f } val value = editorGrade.value * weight @@ -171,7 +176,7 @@ class GradesEditorFragment : Fragment() { averageSemester = 0f for (editorGrade in editorGrades) { var weight = editorGrade.weight - if (config.dontCountEnabled && config.dontCountGrades.contains(editorGrade.name.toLowerCase().trim())) { + if (config.dontCountEnabled && config.dontCountGrades.contains(editorGrade.name.lowercase().trim())) { weight = 0f } val value = editorGrade.value * weight @@ -213,7 +218,7 @@ class GradesEditorFragment : Fragment() { continue } var weight = grade.weight - if (config.dontCountEnabled && config.dontCountGrades.contains(grade.name.toLowerCase().trim())) { + if (config.dontCountEnabled && config.dontCountGrades.contains(grade.name.lowercase().trim())) { weight = 0f } val value = grade.value * weight diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/ExpandableItemModel.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/ExpandableItemModel.kt similarity index 56% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/ExpandableItemModel.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/ExpandableItemModel.kt index 20cb3ffc..39b2a08c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/ExpandableItemModel.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/ExpandableItemModel.kt @@ -2,9 +2,9 @@ * Copyright (c) Kuba Szczodrzyński 2020-2-29. */ -package pl.szczodrzynski.edziennik.ui.modules.grades.models +package pl.szczodrzynski.edziennik.ui.grades.models -import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter.Companion.STATE_CLOSED +import pl.szczodrzynski.edziennik.ui.grades.GradesAdapter.Companion.STATE_CLOSED abstract class ExpandableItemModel(open val items: MutableList) { open var level: Int = 3 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/GradesAverages.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesAverages.kt similarity index 84% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/GradesAverages.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesAverages.kt index 185710d1..551f145c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/GradesAverages.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesAverages.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-3-1. */ -package pl.szczodrzynski.edziennik.ui.modules.grades.models +package pl.szczodrzynski.edziennik.ui.grades.models class GradesAverages { var normalSum = 0f diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/GradesEmpty.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesEmpty.kt similarity index 55% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/GradesEmpty.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesEmpty.kt index 4b726f3f..3b80e741 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/GradesEmpty.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesEmpty.kt @@ -2,6 +2,6 @@ * Copyright (c) Kuba Szczodrzyński 2020-3-1. */ -package pl.szczodrzynski.edziennik.ui.modules.grades.models +package pl.szczodrzynski.edziennik.ui.grades.models class GradesEmpty diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/GradesSemester.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesSemester.kt similarity index 85% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/GradesSemester.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesSemester.kt index e8e107ad..edc3b2ff 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/GradesSemester.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesSemester.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-2-29. */ -package pl.szczodrzynski.edziennik.ui.modules.grades.models +package pl.szczodrzynski.edziennik.ui.grades.models import pl.szczodrzynski.edziennik.data.db.full.GradeFull @@ -14,6 +14,7 @@ data class GradesSemester( override var level = 2 var hasUnseen = false + var hideEditor = false val averages = GradesAverages() var proposedGrade: GradeFull? = null diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/GradesStats.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesStats.kt similarity index 89% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/GradesStats.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesStats.kt index 76833b45..26041dcd 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/GradesStats.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesStats.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-3-3. */ -package pl.szczodrzynski.edziennik.ui.modules.grades.models +package pl.szczodrzynski.edziennik.ui.grades.models class GradesStats { var normalSem1 = 0f diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/GradesSubject.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesSubject.kt similarity index 88% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/GradesSubject.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesSubject.kt index a61abc0d..59407cb1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/models/GradesSubject.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesSubject.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-2-29. */ -package pl.szczodrzynski.edziennik.ui.modules.grades.models +package pl.szczodrzynski.edziennik.ui.grades.models import pl.szczodrzynski.edziennik.data.db.full.GradeFull @@ -15,6 +15,7 @@ data class GradesSubject( var lastAddedDate = 0L var semester: Int = 1 + var isUnknown = false var hasUnseen: Boolean = false get() = field || semesters.any { it.hasUnseen } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesUnknownSubject.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesUnknownSubject.kt new file mode 100644 index 00000000..8768ed93 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/models/GradesUnknownSubject.kt @@ -0,0 +1,7 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-3-14. + */ + +package pl.szczodrzynski.edziennik.ui.grades.models + +class GradesUnknownSubject diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/BindableViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/BindableViewHolder.kt similarity index 81% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/BindableViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/BindableViewHolder.kt index 6dd83384..caa5605b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/BindableViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/BindableViewHolder.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-2-29. */ -package pl.szczodrzynski.edziennik.ui.modules.grades.viewholder +package pl.szczodrzynski.edziennik.ui.grades.viewholder import androidx.appcompat.app.AppCompatActivity import pl.szczodrzynski.edziennik.App diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/EmptyViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/EmptyViewHolder.kt similarity index 79% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/EmptyViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/EmptyViewHolder.kt index ec0eb8ba..866eda04 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/EmptyViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/EmptyViewHolder.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-3-1. */ -package pl.szczodrzynski.edziennik.ui.modules.grades.viewholder +package pl.szczodrzynski.edziennik.ui.grades.viewholder import android.view.LayoutInflater import android.view.ViewGroup @@ -10,8 +10,8 @@ import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.databinding.GradesItemEmptyBinding -import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter -import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesEmpty +import pl.szczodrzynski.edziennik.ui.grades.GradesAdapter +import pl.szczodrzynski.edziennik.ui.grades.models.GradesEmpty class EmptyViewHolder( inflater: LayoutInflater, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/GradeViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/GradeViewHolder.kt similarity index 83% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/GradeViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/GradeViewHolder.kt index 2af79ba3..85684886 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/GradeViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/GradeViewHolder.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-2-29. */ -package pl.szczodrzynski.edziennik.ui.modules.grades.viewholder +package pl.szczodrzynski.edziennik.ui.grades.viewholder import android.view.LayoutInflater import android.view.ViewGroup @@ -13,8 +13,9 @@ import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.full.GradeFull import pl.szczodrzynski.edziennik.databinding.GradesItemGradeBinding -import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter -import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesSubject +import pl.szczodrzynski.edziennik.ui.grades.GradesAdapter +import pl.szczodrzynski.edziennik.ui.grades.models.GradesSubject +import pl.szczodrzynski.edziennik.utils.managers.NoteManager import pl.szczodrzynski.edziennik.utils.models.Date class GradeViewHolder( @@ -33,14 +34,16 @@ class GradeViewHolder( b.gradeName.setGrade(grade, manager, bigView = true) if (grade.description.isNullOrBlank()) { - b.gradeDescription.text = grade.category + b.gradeDescription.text = + grade.getNoteSubstituteText(adapter.showNotes) ?: grade.category b.gradeCategory.text = if (grade.isImprovement) app.getString(R.string.grades_improvement_category_format, "") else "" } else { - b.gradeDescription.text = grade.description + b.gradeDescription.text = + grade.getNoteSubstituteText(adapter.showNotes) ?: grade.description b.gradeCategory.text = if (grade.isImprovement) app.getString(R.string.grades_improvement_category_format, grade.category) @@ -48,6 +51,9 @@ class GradeViewHolder( grade.category } + if (adapter.showNotes) + NoteManager.prependIcon(grade, b.gradeDescription) + val weightText = manager.getWeightString(activity, grade, showClassAverage = true) b.gradeWeight.text = weightText b.gradeWeight.isVisible = weightText != null diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SemesterViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/SemesterViewHolder.kt similarity index 86% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SemesterViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/SemesterViewHolder.kt index 5686a37f..895fa699 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SemesterViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/SemesterViewHolder.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-2-29. */ -package pl.szczodrzynski.edziennik.ui.modules.grades.viewholder +package pl.szczodrzynski.edziennik.ui.grades.viewholder import android.view.LayoutInflater import android.view.ViewGroup @@ -12,10 +12,10 @@ import androidx.recyclerview.widget.RecyclerView import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.databinding.GradesItemSemesterBinding -import pl.szczodrzynski.edziennik.setText -import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter -import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesSemester -import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesSubject +import pl.szczodrzynski.edziennik.ext.setText +import pl.szczodrzynski.edziennik.ui.grades.GradesAdapter +import pl.szczodrzynski.edziennik.ui.grades.models.GradesSemester +import pl.szczodrzynski.edziennik.ui.grades.models.GradesSubject class SemesterViewHolder( inflater: LayoutInflater, @@ -61,6 +61,8 @@ class SemesterViewHolder( } } + b.editButton.isVisible = !item.hideEditor + b.average.text = manager.getAverageString(app, item.averages) b.proposedGrade.setGrade(item.proposedGrade, manager) b.finalGrade.setGrade(item.finalGrade, manager) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/StatsViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/StatsViewHolder.kt similarity index 95% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/StatsViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/StatsViewHolder.kt index 532ade84..3386b276 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/StatsViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/StatsViewHolder.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-3-3. */ -package pl.szczodrzynski.edziennik.ui.modules.grades.viewholder +package pl.szczodrzynski.edziennik.ui.grades.viewholder import android.content.Context import android.view.LayoutInflater @@ -14,10 +14,10 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.databinding.GradesItemStatsBinding -import pl.szczodrzynski.edziennik.onClick -import pl.szczodrzynski.edziennik.ui.dialogs.grade.GradesConfigDialog -import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter -import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesStats +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ui.dialogs.settings.GradesConfigDialog +import pl.szczodrzynski.edziennik.ui.grades.GradesAdapter +import pl.szczodrzynski.edziennik.ui.grades.models.GradesStats import java.text.DecimalFormat class StatsViewHolder( @@ -105,7 +105,7 @@ class StatsViewHolder( b.customValueDivider.isVisible = manager.dontCountEnabled || manager.plusValue != null || manager.minusValue != null b.customValueLayout.isVisible = b.customValueDivider.isVisible b.customValueButton.onClick { - GradesConfigDialog(activity, reloadOnDismiss = true) + GradesConfigDialog(activity, reloadOnDismiss = true).show() } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SubjectViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/SubjectViewHolder.kt similarity index 89% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SubjectViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/SubjectViewHolder.kt index 58015a0a..1033fd65 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/grades/viewholder/SubjectViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/SubjectViewHolder.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-2-29. */ -package pl.szczodrzynski.edziennik.ui.modules.grades.viewholder +package pl.szczodrzynski.edziennik.ui.grades.viewholder import android.text.TextUtils import android.view.LayoutInflater @@ -16,12 +16,14 @@ import androidx.appcompat.view.ContextThemeWrapper import androidx.core.view.get import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.databinding.GradesItemSubjectBinding -import pl.szczodrzynski.edziennik.ui.modules.grades.GradeView -import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter -import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter.Companion.STATE_CLOSED -import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesSubject +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.ui.grades.GradeView +import pl.szczodrzynski.edziennik.ui.grades.GradesAdapter +import pl.szczodrzynski.edziennik.ui.grades.GradesAdapter.Companion.STATE_CLOSED +import pl.szczodrzynski.edziennik.ui.grades.models.GradesSubject import pl.szczodrzynski.edziennik.utils.Themes class SubjectViewHolder( @@ -37,7 +39,12 @@ class SubjectViewHolder( val manager = app.gradesManager val contextWrapper = ContextThemeWrapper(activity, Themes.appTheme) - b.subjectName.text = item.subjectName + if (!item.isUnknown) { + b.subjectName.text = item.subjectName + } + else { + b.subjectName.text = R.string.grades_subject_unknown.resolveString(activity).asItalicSpannable() + } b.dropdownIcon.rotation = when (item.state) { STATE_CLOSED -> 0f else -> 180f diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/UnknownSubjectViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/UnknownSubjectViewHolder.kt new file mode 100644 index 00000000..04ea1829 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/UnknownSubjectViewHolder.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-3-14. + */ + +package pl.szczodrzynski.edziennik.ui.grades.viewholder + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.RecyclerView +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.databinding.GradesItemUnknownSubjectBinding +import pl.szczodrzynski.edziennik.ui.grades.GradesAdapter +import pl.szczodrzynski.edziennik.ui.grades.models.GradesUnknownSubject + +class UnknownSubjectViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + val b: GradesItemUnknownSubjectBinding = GradesItemUnknownSubjectBinding.inflate(inflater, parent, false) +) : RecyclerView.ViewHolder(b.root), BindableViewHolder { + companion object { + private const val TAG = "UnknownSubjectViewHolder" + } + + override fun onBind(activity: AppCompatActivity, app: App, item: GradesUnknownSubject, position: Int, adapter: GradesAdapter) { + + } +} + diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/CardItemTouchHelperCallback.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/CardItemTouchHelperCallback.kt similarity index 90% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/CardItemTouchHelperCallback.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/home/CardItemTouchHelperCallback.kt index 7cfb3c2f..d1a0aa49 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/CardItemTouchHelperCallback.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/CardItemTouchHelperCallback.kt @@ -2,14 +2,14 @@ * Copyright (c) Kuba Szczodrzyński 2019-11-23. */ -package pl.szczodrzynski.edziennik.ui.modules.home +package pl.szczodrzynski.edziennik.ui.home import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper.* import androidx.recyclerview.widget.RecyclerView import com.google.android.material.card.MaterialCardView -import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment.Companion.removeCard -import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment.Companion.swapCards +import pl.szczodrzynski.edziennik.ui.home.HomeFragment.Companion.removeCard +import pl.szczodrzynski.edziennik.ui.home.HomeFragment.Companion.swapCards import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator class CardItemTouchHelperCallback(private val cardAdapter: HomeCardAdapter, private val refreshLayout: SwipeRefreshLayoutNoIndicator?) : ItemTouchHelper.Callback() { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/CounterActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/CounterActivity.kt similarity index 68% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/CounterActivity.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/home/CounterActivity.kt index 868b330f..b1f10d3b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/CounterActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/CounterActivity.kt @@ -2,10 +2,12 @@ * Copyright (c) Kacper Ziubryniewicz 2019-12-21 */ -package pl.szczodrzynski.edziennik.ui.modules.home +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 @@ -15,7 +17,12 @@ import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.Lesson import pl.szczodrzynski.edziennik.data.db.full.LessonFull import pl.szczodrzynski.edziennik.databinding.ActivityCounterBinding -import pl.szczodrzynski.edziennik.ui.dialogs.bell.BellSyncTimeChooseDialog +import pl.szczodrzynski.edziennik.ext.onClick +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 @@ -57,6 +64,7 @@ class CounterActivity : AppCompatActivity(), CoroutineScope { it.type != Lesson.TYPE_SHIFTED_SOURCE }) } + lessonList.onEach { it.filterNotes() } } b.bellSync.setImageDrawable( @@ -66,7 +74,7 @@ class CounterActivity : AppCompatActivity(), CoroutineScope { } ) b.bellSync.onClick { - BellSyncTimeChooseDialog(activity = this@CounterActivity) + BellSyncTimeChooseDialog(activity = this@CounterActivity).show() } app.config.timetable.bellSyncDiff?.let { @@ -77,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() { @@ -97,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) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/HomeCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeCard.kt similarity index 91% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/HomeCard.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeCard.kt index 2c7894ce..59bb24f1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/HomeCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeCard.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2019-11-23. */ -package pl.szczodrzynski.edziennik.ui.modules.home +package pl.szczodrzynski.edziennik.ui.home interface HomeCard { companion object { @@ -24,10 +24,11 @@ interface HomeCard { const val CARD_TIMETABLE = 2 const val CARD_GRADES = 3 const val CARD_EVENTS = 4 + const val CARD_NOTES = 5 } val id: Int fun bind(position: Int, holder: HomeCardAdapter.ViewHolder) fun unbind(position: Int, holder: HomeCardAdapter.ViewHolder) -} \ No newline at end of file +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/HomeCardAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeCardAdapter.kt similarity index 97% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/HomeCardAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeCardAdapter.kt index 05ba72b6..3e3df7f2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/HomeCardAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeCardAdapter.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2019-11-23. */ -package pl.szczodrzynski.edziennik.ui.modules.home +package pl.szczodrzynski.edziennik.ui.home import android.annotation.SuppressLint import android.view.LayoutInflater @@ -48,4 +48,4 @@ class HomeCardAdapter(var items: MutableList) : RecyclerView.Adapter Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BaseDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "HomeConfigDialog" + + override fun getTitleRes() = R.string.home_configure_add_remove + override fun getPositiveButtonText() = R.string.ok + override fun getNegativeButtonText() = R.string.cancel + + override fun getMultiChoiceItems(): Map = mapOf( + R.string.card_type_lucky_number to CARD_LUCKY_NUMBER, + R.string.card_type_timetable to CARD_TIMETABLE, + R.string.card_type_grades to CARD_GRADES, + R.string.card_type_events to CARD_EVENTS, + R.string.card_type_notes to CARD_NOTES, + ).mapKeys { (resId, _) -> activity.getString(resId) } + + override fun getDefaultSelectedItems() = + app.profile.config.ui.homeCards + .filter { it.profileId == App.profileId } + .map { it.cardId } + .toSet() + + override suspend fun onShow() = Unit + + private var configChanged = false + + override suspend fun onPositiveClick(): Boolean { + val homeCards = app.profile.config.ui.homeCards.toMutableList() + homeCards.removeAll { it.profileId == App.profileId } + homeCards += getMultiSelection().mapNotNull { + HomeCardModel( + profileId = App.profileId, + cardId = it as? Int ?: return@mapNotNull null + ) + } + app.profile.config.ui.homeCards = homeCards + return DISMISS + } + + override suspend fun onMultiSelectionChanged(items: Set) { + configChanged = true + } + + override fun onDismiss() { + if (configChanged && reloadOnDismiss && activity is MainActivity) + activity.reloadTarget() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/HomeDummyCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeDummyCard.kt similarity index 93% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/HomeDummyCard.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeDummyCard.kt index 1f6904d2..3963c60b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/HomeDummyCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeDummyCard.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2019-11-23. */ -package pl.szczodrzynski.edziennik.ui.modules.home +package pl.szczodrzynski.edziennik.ui.home import android.widget.TextView import androidx.core.view.plusAssign @@ -12,7 +12,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.MainActivity -import pl.szczodrzynski.edziennik.startCoroutineTimer +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer import kotlin.coroutines.CoroutineContext class HomeDummyCard(override val id: Int) : HomeCard, CoroutineScope { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/HomeFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeFragment.kt similarity index 81% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/HomeFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeFragment.kt index 944c60e8..ed8e7519 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/HomeFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2019-11-23. */ -package pl.szczodrzynski.edziennik.ui.modules.home +package pl.szczodrzynski.edziennik.ui.home import android.os.Bundle import android.view.LayoutInflater @@ -21,11 +21,16 @@ import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial.Icon import eu.szkolny.font.SzkolnyFont import kotlinx.coroutines.* -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.db.entity.LoginStore +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.BuildConfig +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType import pl.szczodrzynski.edziennik.databinding.FragmentHomeBinding -import pl.szczodrzynski.edziennik.ui.dialogs.home.StudentNumberDialog -import pl.szczodrzynski.edziennik.ui.modules.home.cards.* +import pl.szczodrzynski.edziennik.ext.hasUIFeature +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ui.dialogs.settings.StudentNumberDialog +import pl.szczodrzynski.edziennik.ui.home.cards.* import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem import kotlin.coroutines.CoroutineContext @@ -45,18 +50,18 @@ class HomeFragment : Fragment(), CoroutineScope { cardAdapter.items[toPosition] = fromCard cardAdapter.notifyItemMoved(fromPosition, toPosition) - val homeCards = App.config.forProfile().ui.homeCards.toMutableList() + val homeCards = App.profile.config.ui.homeCards.toMutableList() val fromIndex = homeCards.indexOfFirst { it.cardId == fromCard.id } val toIndex = homeCards.indexOfFirst { it.cardId == toCard.id } val fromPair = homeCards[fromIndex] homeCards[fromIndex] = homeCards[toIndex] homeCards[toIndex] = fromPair - App.config.forProfile().ui.homeCards = homeCards + App.profile.config.ui.homeCards = homeCards return true } fun removeCard(position: Int, cardAdapter: HomeCardAdapter) { - val homeCards = App.config.forProfile().ui.homeCards.toMutableList() + val homeCards = App.profile.config.ui.homeCards.toMutableList() if (position >= homeCards.size) return val card = cardAdapter.items[position] @@ -66,7 +71,7 @@ class HomeFragment : Fragment(), CoroutineScope { return } homeCards.removeAll { it.cardId == card.id } - App.config.forProfile().ui.homeCards = homeCards + App.profile.config.ui.homeCards = homeCards } } @@ -77,7 +82,8 @@ class HomeFragment : Fragment(), CoroutineScope { private val job: Job = Job() override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main - + private val manager + get() = app.permissionManager override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { activity = (getActivity() as MainActivity?) ?: return null context ?: return null @@ -92,13 +98,17 @@ class HomeFragment : Fragment(), CoroutineScope { if (!isAdded) return + if (!manager.isNotificationPermissionGranted) { + manager.requestNotificationsPermission(activity, 0, false){} + } + activity.bottomSheet.prependItems( BottomSheetPrimaryItem(true) .withTitle(R.string.menu_add_remove_cards) .withIcon(Icon.cmd_card_bulleted_settings_outline) .withOnClickListener(OnClickListener { activity.bottomSheet.close() - HomeConfigDialog(activity, reloadOnDismiss = true) + HomeConfigDialog(activity, reloadOnDismiss = true).show() }), BottomSheetPrimaryItem(true) .withTitle(R.string.menu_set_student_number) @@ -116,7 +126,7 @@ class HomeFragment : Fragment(), CoroutineScope { .withOnClickListener(OnClickListener { activity.bottomSheet.close() launch { withContext(Dispatchers.Default) { - if (app.profile.loginStoreType != LoginStore.LOGIN_TYPE_LIBRUS) { + if (!app.data.uiConfig.enableMarkAsReadAnnouncements) { app.db.metadataDao().setAllSeenExceptMessagesAndAnnouncements(App.profileId, true) } else { app.db.metadataDao().setAllSeenExceptMessages(App.profileId, true) @@ -127,24 +137,23 @@ class HomeFragment : Fragment(), CoroutineScope { }) ) b.configureCards.onClick { - HomeConfigDialog(activity, reloadOnDismiss = true) + HomeConfigDialog(activity, reloadOnDismiss = true).show() } b.scrollView.setOnScrollChangeListener { _: NestedScrollView?, _: Int, scrollY: Int, _: Int, _: Int -> b.refreshLayout.isEnabled = scrollY == 0 } - val showUnified = false - - val cards = app.config.forProfile().ui.homeCards.filter { it.profileId == app.profile.id }.toMutableList() + val cards = app.profile.config.ui.homeCards.filter { it.profileId == app.profile.id }.toMutableList() if (cards.isEmpty()) { - cards += listOf( - HomeCardModel(app.profile.id, HomeCard.CARD_LUCKY_NUMBER), - HomeCardModel(app.profile.id, HomeCard.CARD_TIMETABLE), - HomeCardModel(app.profile.id, HomeCard.CARD_EVENTS), - HomeCardModel(app.profile.id, HomeCard.CARD_GRADES) + cards += listOfNotNull( + HomeCardModel(app.profile.id, HomeCard.CARD_LUCKY_NUMBER).takeIf { app.profile.hasUIFeature(FeatureType.LUCKY_NUMBER) }, + HomeCardModel(app.profile.id, HomeCard.CARD_TIMETABLE).takeIf { app.profile.hasUIFeature(FeatureType.TIMETABLE) }, + HomeCardModel(app.profile.id, HomeCard.CARD_EVENTS).takeIf { app.profile.hasUIFeature(FeatureType.AGENDA) }, + HomeCardModel(app.profile.id, HomeCard.CARD_GRADES).takeIf { app.profile.hasUIFeature(FeatureType.GRADES) }, + HomeCardModel(app.profile.id, HomeCard.CARD_NOTES), ) - app.config.forProfile().ui.homeCards = app.config.forProfile().ui.homeCards.toMutableList().also { it.addAll(cards) } + app.profile.config.ui.homeCards = app.profile.config.ui.homeCards.toMutableList().also { it.addAll(cards) } } val items = mutableListOf() @@ -155,6 +164,7 @@ class HomeFragment : Fragment(), CoroutineScope { HomeCard.CARD_TIMETABLE -> HomeTimetableCard(it.cardId, app, activity, this, app.profile) HomeCard.CARD_GRADES -> HomeGradesCard(it.cardId, app, activity, this, app.profile) HomeCard.CARD_EVENTS -> HomeEventsCard(it.cardId, app, activity, this, app.profile) + HomeCard.CARD_NOTES -> HomeNotesCard(it.cardId, app, activity, this, app.profile) else -> null } as HomeCard? } @@ -163,10 +173,9 @@ class HomeFragment : Fragment(), CoroutineScope { if (app.profile.archived) items.add(0, HomeArchiveCard(101, app, activity, this, app.profile)) - val status = app.config.sync.registerAvailability[app.profile.registerName] + val status = app.availabilityManager.check(app.profile, cacheOnly = true)?.status val update = app.config.update - if (update != null && update.versionCode > BuildConfig.VERSION_CODE - || status != null && (!status.available || status.minVersionCode > BuildConfig.VERSION_CODE)) { + if (update != null && update.versionCode > BuildConfig.VERSION_CODE || status?.userMessage != null) { items.add(0, HomeAvailabilityCard(102, app, activity, this, app.profile)) } @@ -195,7 +204,7 @@ class HomeFragment : Fragment(), CoroutineScope { } } - override fun performAccessibilityAction(host: View, action: Int, args: Bundle): Boolean { + 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) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeArchiveCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeArchiveCard.kt similarity index 81% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeArchiveCard.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeArchiveCard.kt index 17fc89e4..2404d0f1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeArchiveCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeArchiveCard.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-8-25. */ -package pl.szczodrzynski.edziennik.ui.modules.home.cards +package pl.szczodrzynski.edziennik.ui.home.cards import android.view.LayoutInflater import android.view.ViewGroup @@ -14,9 +14,14 @@ import kotlinx.coroutines.* import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.databinding.CardHomeArchiveBinding -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardAdapter -import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ext.setMessage +import pl.szczodrzynski.edziennik.ext.setText +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget +import pl.szczodrzynski.edziennik.ui.home.HomeCard +import pl.szczodrzynski.edziennik.ui.home.HomeCardAdapter +import pl.szczodrzynski.edziennik.ui.home.HomeFragment import kotlin.coroutines.CoroutineContext class HomeArchiveCard( @@ -66,12 +71,12 @@ class HomeArchiveCard( .show() return@launch } - activity.loadProfile(profile) + activity.navigate(profile = profile) } } holder.root.onClick { - activity.loadTarget(MainActivity.DRAWER_ITEM_AGENDA) + activity.navigate(navTarget = NavTarget.AGENDA) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeAvailabilityCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeAvailabilityCard.kt similarity index 59% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeAvailabilityCard.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeAvailabilityCard.kt index fd54748e..4bb264d0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeAvailabilityCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeAvailabilityCard.kt @@ -2,13 +2,12 @@ * Copyright (c) Kuba Szczodrzyński 2020-9-3. */ -package pl.szczodrzynski.edziennik.ui.modules.home.cards +package pl.szczodrzynski.edziennik.ui.home.cards import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.FrameLayout -import androidx.core.text.HtmlCompat import androidx.core.view.isVisible import androidx.core.view.plusAssign import androidx.core.view.setMargins @@ -16,15 +15,24 @@ import coil.load import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.BuildConfig +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.databinding.CardHomeAvailabilityBinding +import pl.szczodrzynski.edziennik.ext.Intent +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ext.setText import pl.szczodrzynski.edziennik.sync.UpdateDownloaderService -import pl.szczodrzynski.edziennik.ui.dialogs.RegisterUnavailableDialog -import pl.szczodrzynski.edziennik.ui.dialogs.UpdateAvailableDialog -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardAdapter -import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment +import pl.szczodrzynski.edziennik.ui.dialogs.sync.RegisterUnavailableDialog +import pl.szczodrzynski.edziennik.ui.dialogs.sync.UpdateAvailableDialog +import pl.szczodrzynski.edziennik.ui.home.HomeCard +import pl.szczodrzynski.edziennik.ui.home.HomeCardAdapter +import pl.szczodrzynski.edziennik.ui.home.HomeFragment +import pl.szczodrzynski.edziennik.utils.Utils +import pl.szczodrzynski.edziennik.utils.html.BetterHtml import kotlin.coroutines.CoroutineContext class HomeAvailabilityCard( @@ -50,7 +58,8 @@ class HomeAvailabilityCard( } holder.root += b.root - val status = app.config.sync.registerAvailability[profile.registerName] + val error = app.availabilityManager.check(profile, cacheOnly = true) + val status = error?.status val update = app.config.update if (update == null && status == null) @@ -58,31 +67,39 @@ class HomeAvailabilityCard( var onInfoClick = { _: View -> } - if (status != null && !status.available && status.userMessage != null) { - b.homeAvailabilityTitle.text = HtmlCompat.fromHtml(status.userMessage.title, HtmlCompat.FROM_HTML_MODE_LEGACY) - b.homeAvailabilityText.text = HtmlCompat.fromHtml(status.userMessage.contentShort, HtmlCompat.FROM_HTML_MODE_LEGACY) + // show "register unavailable" only when disabled + if (status?.userMessage != null) { + b.homeAvailabilityTitle.text = BetterHtml.fromHtml(activity, status.userMessage.title) + b.homeAvailabilityText.text = BetterHtml.fromHtml(activity, status.userMessage.contentShort) b.homeAvailabilityUpdate.isVisible = false b.homeAvailabilityIcon.setImageResource(R.drawable.ic_sync) if (status.userMessage.icon != null) b.homeAvailabilityIcon.load(status.userMessage.icon) onInfoClick = { - RegisterUnavailableDialog(activity, status) + RegisterUnavailableDialog(activity, status).show() } } + // show "update available" when available OR version too old for the register else if (update != null && update.versionCode > BuildConfig.VERSION_CODE) { b.homeAvailabilityTitle.setText(R.string.home_availability_title) b.homeAvailabilityText.setText(R.string.home_availability_text, update.versionName) - b.homeAvailabilityUpdate.isVisible = true + b.homeAvailabilityUpdate.isVisible = !app.buildManager.isPlayRelease b.homeAvailabilityIcon.setImageResource(R.drawable.ic_update) onInfoClick = { - UpdateAvailableDialog(activity, update) + UpdateAvailableDialog(activity, update).show() } } + else { + b.root.isVisible = false + } b.homeAvailabilityUpdate.onClick { if (update == null) return@onClick - activity.startService(Intent(app, UpdateDownloaderService::class.java)) + if (update.isOnGooglePlay) + Utils.openGooglePlay(activity) + else + activity.startService(Intent(app, UpdateDownloaderService::class.java)) } b.homeAvailabilityInfo.onClick(onInfoClick) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeDebugCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeDebugCard.kt similarity index 89% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeDebugCard.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeDebugCard.kt index 3fde41ab..6947b74b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeDebugCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeDebugCard.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2019-12-22. */ -package pl.szczodrzynski.edziennik.ui.modules.home.cards +package pl.szczodrzynski.edziennik.ui.home.cards import android.appwidget.AppWidgetManager import android.content.ComponentName @@ -25,12 +25,12 @@ import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.databinding.CardHomeDebugBinding -import pl.szczodrzynski.edziennik.dp -import pl.szczodrzynski.edziennik.onClick -import pl.szczodrzynski.edziennik.ui.dialogs.captcha.LibrusCaptchaDialog -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardAdapter -import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ui.captcha.RecaptchaPromptDialog +import pl.szczodrzynski.edziennik.ui.home.HomeCard +import pl.szczodrzynski.edziennik.ui.home.HomeCardAdapter +import pl.szczodrzynski.edziennik.ui.home.HomeFragment import pl.szczodrzynski.edziennik.ui.widgets.WidgetConfigActivity import pl.szczodrzynski.edziennik.ui.widgets.luckynumber.WidgetLuckyNumberProvider import pl.szczodrzynski.edziennik.ui.widgets.notifications.WidgetNotificationsProvider @@ -85,11 +85,6 @@ class HomeDebugCard( app.startActivity(Chucker.getLaunchIntent(activity, 1)); } - b.librusCaptchaButton.onClick { - //app.startActivity(Intent(activity, LoginLibrusCaptchaActivity::class.java)) - LibrusCaptchaDialog(activity, onSuccess = {}, onFailure = {}) - } - b.getLogs.onClick { val logs = HyperLog.getDeviceLogsInFile(activity, true) val intent = Intent(Intent.ACTION_SEND) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeEventsCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeEventsCard.kt similarity index 77% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeEventsCard.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeEventsCard.kt index 9a831c0f..acf99b2c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeEventsCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeEventsCard.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-2-28. */ -package pl.szczodrzynski.edziennik.ui.modules.home.cards +package pl.szczodrzynski.edziennik.ui.home.cards import android.view.LayoutInflater import android.view.View @@ -20,14 +20,15 @@ import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.databinding.CardHomeEventsBinding -import pl.szczodrzynski.edziennik.dp -import pl.szczodrzynski.edziennik.onClick -import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog -import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter -import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardAdapter -import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget +import pl.szczodrzynski.edziennik.ui.event.EventDetailsDialog +import pl.szczodrzynski.edziennik.ui.event.EventListAdapter +import pl.szczodrzynski.edziennik.ui.event.EventManualDialog +import pl.szczodrzynski.edziennik.ui.home.HomeCard +import pl.szczodrzynski.edziennik.ui.home.HomeCardAdapter +import pl.szczodrzynski.edziennik.ui.home.HomeFragment import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import pl.szczodrzynski.edziennik.utils.models.Date import kotlin.coroutines.CoroutineContext @@ -62,27 +63,32 @@ 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, - onItemClick = { + onEventClick = { EventDetailsDialog( activity, it - ) + ).show() }, onEventEditClick = { EventManualDialog( activity, it.profileId, editingEvent = it - ) + ).show() } ) app.db.eventDao().getNearestNotDone(profile.id, Date.getToday(), 4).observe(activity, Observer { events -> - adapter.items = events + events.forEach { + it.filterNotes() + } + + adapter.setAllItems(events) if (b.eventsView.adapter == null) { b.eventsView.adapter = adapter b.eventsView.apply { @@ -104,7 +110,7 @@ class HomeEventsCard( }) holder.root.onClick { - activity.loadTarget(MainActivity.DRAWER_ITEM_AGENDA) + activity.navigate(navTarget = NavTarget.AGENDA) } }} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeGradesCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeGradesCard.kt similarity index 91% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeGradesCard.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeGradesCard.kt index f1c3373e..c64f2cc3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeGradesCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeGradesCard.kt @@ -2,7 +2,7 @@ * Copyright (c) Kacper Ziubryniewicz 2019-11-29 */ -package pl.szczodrzynski.edziennik.ui.modules.home.cards +package pl.szczodrzynski.edziennik.ui.home.cards import android.graphics.Typeface import android.os.Build @@ -29,11 +29,12 @@ import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.data.db.entity.Subject import pl.szczodrzynski.edziennik.data.db.full.GradeFull import pl.szczodrzynski.edziennik.databinding.CardHomeGradesBinding -import pl.szczodrzynski.edziennik.dp -import pl.szczodrzynski.edziennik.ui.modules.grades.GradeView -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardAdapter -import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget +import pl.szczodrzynski.edziennik.ui.grades.GradeView +import pl.szczodrzynski.edziennik.ui.home.HomeCard +import pl.szczodrzynski.edziennik.ui.home.HomeCardAdapter +import pl.szczodrzynski.edziennik.ui.home.HomeFragment import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.ItemGradesSubjectModel @@ -75,7 +76,7 @@ class HomeGradesCard( }) b.root.setOnClickListener { - activity.loadTarget(MainActivity.DRAWER_ITEM_GRADES) + activity.navigate(navTarget = NavTarget.GRADES) } } @@ -85,6 +86,9 @@ class HomeGradesCard( grades.forEach { grade -> val model = ItemGradesSubjectModel.searchModelBySubjectId(subjects, grade.subjectId) ?: run { + if (grade.subjectLongName == null) { + return@forEach + } subjects.add(ItemGradesSubjectModel( profile, Subject(profile.id, grade.subjectId, grade.subjectLongName, grade.subjectShortName), diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeLuckyNumberCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeLuckyNumberCard.kt similarity index 85% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeLuckyNumberCard.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeLuckyNumberCard.kt index 31611e48..7efec925 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeLuckyNumberCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeLuckyNumberCard.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2019-11-24. */ -package pl.szczodrzynski.edziennik.ui.modules.home.cards +package pl.szczodrzynski.edziennik.ui.home.cards import android.view.LayoutInflater import android.view.ViewGroup.LayoutParams.MATCH_PARENT @@ -15,13 +15,18 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.databinding.CardHomeLuckyNumberBinding -import pl.szczodrzynski.edziennik.ui.dialogs.home.StudentNumberDialog -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardAdapter -import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ext.setText +import pl.szczodrzynski.edziennik.ui.dialogs.settings.StudentNumberDialog +import pl.szczodrzynski.edziennik.ui.home.HomeCard +import pl.szczodrzynski.edziennik.ui.home.HomeCardAdapter +import pl.szczodrzynski.edziennik.ui.home.HomeFragment import pl.szczodrzynski.edziennik.utils.models.Date import kotlin.coroutines.CoroutineContext @@ -58,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.name, profile.studentNumber) app.db.luckyNumberDao().getNearestFuture(profile.id, today).observe(fragment, Observer { luckyNumber -> val isYours = luckyNumber?.number == profile.studentNumber @@ -99,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.name, profile.studentNumber) }) } }} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeNotesCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeNotesCard.kt new file mode 100644 index 00000000..be829285 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeNotesCard.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-28. + */ + +package pl.szczodrzynski.edziennik.ui.home.cards + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.core.view.isVisible +import androidx.core.view.plusAssign +import androidx.core.view.setMargins +import androidx.recyclerview.widget.LinearLayoutManager +import kotlinx.coroutines.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.data.db.entity.Note +import pl.szczodrzynski.edziennik.data.db.entity.Noteable +import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.databinding.CardHomeNotesBinding +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget +import pl.szczodrzynski.edziennik.ui.home.HomeCard +import pl.szczodrzynski.edziennik.ui.home.HomeCardAdapter +import pl.szczodrzynski.edziennik.ui.home.HomeFragment +import pl.szczodrzynski.edziennik.ui.notes.NoteDetailsDialog +import pl.szczodrzynski.edziennik.ui.notes.NoteEditorDialog +import pl.szczodrzynski.edziennik.ui.notes.NoteListAdapter +import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration +import kotlin.coroutines.CoroutineContext + +class HomeNotesCard( + override val id: Int, + val app: App, + val activity: MainActivity, + val fragment: HomeFragment, + val profile: Profile, +) : HomeCard, CoroutineScope { + companion object { + private const val TAG = "HomeNotesCard" + } + + private var job: Job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + private val manager + get() = app.noteManager + + private lateinit var adapter: NoteListAdapter + + private fun onNoteClick(note: Note) = launch { + val owner = withContext(Dispatchers.IO) { + manager.getOwner(note) + } as? Noteable + + NoteDetailsDialog( + activity = activity, + owner = owner, + note = note, + ).show() + } + + private fun onNoteAddClick(view: View?) { + NoteEditorDialog( + activity = activity, + owner = null, + editingNote = null, + profileId = profile.id, + ).show() + } + + override fun bind(position: Int, holder: HomeCardAdapter.ViewHolder) { launch { + holder.root.removeAllViews() + val b = CardHomeNotesBinding.inflate(LayoutInflater.from(holder.root.context)) + b.root.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT).apply { + setMargins(8.dp) + } + holder.root += b.root + + adapter = NoteListAdapter( + activity = activity, + onNoteClick = this@HomeNotesCard::onNoteClick, + onNoteEditClick = null, + ) + + app.db.noteDao().getAllNoOwner(profileId = profile.id).observe(activity) { notes -> + + // show/hide relevant views + b.list.isVisible = notes.isNotEmpty() + b.noData.isVisible = notes.isEmpty() + if (notes.isEmpty()) { + return@observe + } + + // apply the new note list + adapter.setAllItems(notes.take(4)) + + // configure the adapter & recycler view + if (b.list.adapter == null) { + b.list.adapter = adapter + b.list.apply { + //setHasFixedSize(true) + isNestedScrollingEnabled = false + layoutManager = LinearLayoutManager(context) + addItemDecoration(SimpleDividerItemDecoration(context)) + } + } else { + adapter.notifyDataSetChanged() + } + } + + b.addNote.onClick(this@HomeNotesCard::onNoteAddClick) + + holder.root.onClick { + activity.navigate(navTarget = NavTarget.NOTES) + } + }} + + override fun unbind(position: Int, holder: HomeCardAdapter.ViewHolder) = Unit +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeTemplateCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeTemplateCard.kt similarity index 83% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeTemplateCard.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeTemplateCard.kt index 0bb2e881..5cc7b77c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeTemplateCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeTemplateCard.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2019-12-22. */ -package pl.szczodrzynski.edziennik.ui.modules.home.cards +package pl.szczodrzynski.edziennik.ui.home.cards import android.view.LayoutInflater import android.view.ViewGroup @@ -17,11 +17,11 @@ import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.databinding.CardHomeTemplateBinding -import pl.szczodrzynski.edziennik.dp -import pl.szczodrzynski.edziennik.onClick -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardAdapter -import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ui.home.HomeCard +import pl.szczodrzynski.edziennik.ui.home.HomeCardAdapter +import pl.szczodrzynski.edziennik.ui.home.HomeFragment import kotlin.coroutines.CoroutineContext class HomeTemplateCard( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeTimetableCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeTimetableCard.kt similarity index 83% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeTimetableCard.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeTimetableCard.kt index d1a30053..39959121 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/cards/HomeTimetableCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeTimetableCard.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2019-11-24. */ -package pl.szczodrzynski.edziennik.ui.modules.home.cards +package pl.szczodrzynski.edziennik.ui.home.cards import android.content.Intent import android.os.Bundle @@ -27,13 +27,16 @@ import pl.szczodrzynski.edziennik.data.api.events.ApiTaskAllFinishedEvent import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Lesson import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType import pl.szczodrzynski.edziennik.data.db.full.LessonFull import pl.szczodrzynski.edziennik.databinding.CardHomeTimetableBinding -import pl.szczodrzynski.edziennik.ui.dialogs.bell.BellSyncTimeChooseDialog -import pl.szczodrzynski.edziennik.ui.modules.home.CounterActivity -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCardAdapter -import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget +import pl.szczodrzynski.edziennik.ui.dialogs.BellSyncTimeChooseDialog +import pl.szczodrzynski.edziennik.ui.home.CounterActivity +import pl.szczodrzynski.edziennik.ui.home.HomeCard +import pl.szczodrzynski.edziennik.ui.home.HomeCardAdapter +import pl.szczodrzynski.edziennik.ui.home.HomeFragment import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Week @@ -72,9 +75,10 @@ class HomeTimetableCard( private var counterJob: Job? = null private var counterStart: Time? = null private var counterEnd: Time? = null + private var showAllLessons: Boolean = false private var subjectSpannable: CharSequence? = null - private val ignoreCancelled = true + private val ignoreCancelled = false private val countInSeconds: Boolean get() = app.config.timetable.countInSeconds @@ -111,7 +115,7 @@ class HomeTimetableCard( b.bellSync.setOnClickListener { BellSyncTimeChooseDialog( activity - ) + ).show() } b.showCounter.setOnClickListener { @@ -119,9 +123,9 @@ class HomeTimetableCard( } b.root.onClick { - activity.loadTarget(MainActivity.DRAWER_ITEM_TIMETABLE, Bundle().apply { - putString("timetableDate", timetableDate.stringY_m_d) - }) + activity.navigate(navTarget = NavTarget.TIMETABLE, args = Bundle( + "timetableDate" to timetableDate.stringY_m_d, + )) } if (app.profile.getStudentData("timetableNotPublic", false)) { @@ -159,6 +163,9 @@ class HomeTimetableCard( && it.displayEndTime > now && !(it.isCancelled && ignoreCancelled) } + if (!ignoreCancelled && lessons.all { it.isCancelled }) + // skip current day if all lessons are cancelled + lessons = listOf() while ((lessons.isEmpty() || lessons.none { it.type != Lesson.TYPE_NO_LESSONS && (it.displayDate != today @@ -173,6 +180,7 @@ class HomeTimetableCard( it.profileId == profile.id && it.displayDate == timetableDate } + lessons = lessons.dropWhile { it.isCancelled } if (lessons.isEmpty()) break @@ -202,9 +210,7 @@ class HomeTimetableCard( it.isEnabled = false EdziennikTask.syncProfile( profileId = profile.id, - viewIds = listOf( - MainActivity.DRAWER_ITEM_TIMETABLE to 0 - ), + featureTypes = setOf(FeatureType.TIMETABLE), arguments = JsonObject( "weekStart" to weekStart.stringY_m_d ) @@ -226,6 +232,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 @@ -237,15 +244,18 @@ class HomeTimetableCard( b.counter.visibility = View.GONE val now = syncedNow - val firstLesson = lessons.firstOrNull() - val lastLesson = lessons.lastOrNull() + val firstLesson = lessons.firstOrNull { !it.isCancelled } + val lastLesson = lessons.lastOrNull { !it.isCancelled } + val skipFirst = if (firstLesson == null) 0 else lessons.indexOf(firstLesson) + val skipLast = if (lastLesson == null) 0 else lessons.size - 1 - lessons.indexOf(lastLesson) + val lessonCount = lessons.size - skipFirst - skipLast if (isToday) { // today b.dayInfo.setText(R.string.home_timetable_today) b.lessonInfo.setText( R.string.home_timetable_lessons_remaining, - lessons.size, + lessonCount, lastLesson?.displayEndTime?.stringHM ?: "?" ) counterStart = firstLesson?.displayStartTime @@ -268,6 +278,8 @@ class HomeTimetableCard( counterJob = startCoroutineTimer(repeatMillis = 500) { count() } + + showAllLessons = !isOngoing } else { val isTomorrow = today.clone().stepForward(0, 0, 1) == timetableDate @@ -290,26 +302,37 @@ class HomeTimetableCard( b.dayInfo.setText(dayInfoRes, Week.getFullDayName(timetableDate.weekDay), timetableDate.formattedString) b.lessonInfo.setText( R.string.home_timetable_lessons_info, - lessons.size, + lessonCount, firstLesson?.displayStartTime?.stringHM ?: "?", lastLesson?.displayEndTime?.stringHM ?: "?" ) b.lessonBig.setText(R.string.home_timetable_lesson_first, firstLesson.subjectSpannable) + b.counter.visibility = View.VISIBLE + b.counter.text = firstLesson?.displayStartTime?.stringHM firstLesson?.displayClassroom?.let { b.classroom.visibility = View.VISIBLE b.classroom.text = it } ?: run { b.classroom.visibility = View.GONE } + + showAllLessons = true } val text = mutableListOf( + if (showAllLessons) + activity.getString(R.string.home_timetable_all_lessons) + else activity.getString(R.string.home_timetable_later) ) - var first = true - for (lesson in lessons) { - if (first) { first = false; continue } + + val nextLessons = if (showAllLessons) + lessons.drop(skipFirst) + else + lessons.drop(skipFirst + 1) + + for (lesson in nextLessons) { text += listOf( lesson.displayStartTime?.stringHM, lesson.subjectSpannable @@ -322,6 +345,7 @@ class HomeTimetableCard( private val LessonFull?.subjectSpannable: CharSequence get() = if (this == null) "?" else when { + hasReplacingNotes() -> getNoteSubstituteText(showNotes = true) ?: "?" isCancelled -> displaySubjectName?.asStrikethroughSpannable() ?: "?" isChange -> displaySubjectName?.asItalicSpannable() ?: "?" else -> displaySubjectName ?: "?" @@ -339,6 +363,14 @@ class HomeTimetableCard( } val now = syncedNow + if (now >= counterStart && showAllLessons) { + // update "next lessons" view to remove current lesson + this.counterJob?.cancel() + this.counterStart = null + this.counterEnd = null + update() + return + } if (now > counterEnd) { // the lesson is already over b.progress.visibility = View.GONE @@ -362,7 +394,10 @@ class HomeTimetableCard( b.progress.visibility = View.GONE b.counter.visibility = View.VISIBLE val diff = counterStart - now - b.counter.text = activity.timeTill(diff.toInt(), "\n", countInSeconds) + if (diff >= 60 * MINUTE) + b.counter.text = counterStart.stringHM + else + b.counter.text = activity.timeTill(diff.toInt(), "\n", countInSeconds) } else { // the lesson is right now diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkDate.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/homework/HomeworkDate.kt similarity index 66% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkDate.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/homework/HomeworkDate.kt index d923c5a1..a66fed71 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkDate.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/homework/HomeworkDate.kt @@ -1,4 +1,4 @@ -package pl.szczodrzynski.edziennik.ui.modules.homework +package pl.szczodrzynski.edziennik.ui.homework class HomeworkDate { companion object { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/homework/HomeworkFragment.kt similarity index 86% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/homework/HomeworkFragment.kt index c8a62def..f47fa63e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/homework/HomeworkFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-3-30. */ -package pl.szczodrzynski.edziennik.ui.modules.homework +package pl.szczodrzynski.edziennik.ui.homework import android.os.AsyncTask import android.os.Bundle @@ -16,12 +16,17 @@ import eu.szkolny.font.SzkolnyFont import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType import pl.szczodrzynski.edziennik.databinding.HomeworkFragmentBinding -import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog -import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.FragmentLazyPagerAdapter +import pl.szczodrzynski.edziennik.ext.Bundle +import pl.szczodrzynski.edziennik.ext.addOnPageSelectedListener +import pl.szczodrzynski.edziennik.ui.base.lazypager.FragmentLazyPagerAdapter +import pl.szczodrzynski.edziennik.ui.event.EventManualDialog import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem import kotlin.coroutines.CoroutineContext @@ -61,7 +66,7 @@ class HomeworkFragment : Fragment(), CoroutineScope { .withIcon(SzkolnyFont.Icon.szf_calendar_plus_outline) .withOnClickListener(View.OnClickListener { activity.bottomSheet.close() - EventManualDialog(activity, App.profileId, defaultType = Event.TYPE_HOMEWORK) + EventManualDialog(activity, App.profileId, defaultType = Event.TYPE_HOMEWORK).show() }), BottomSheetSeparatorItem(true), BottomSheetPrimaryItem(true) @@ -69,12 +74,12 @@ class HomeworkFragment : Fragment(), CoroutineScope { .withIcon(CommunityMaterial.Icon.cmd_eye_check_outline) .withOnClickListener(View.OnClickListener { activity.bottomSheet.close() - AsyncTask.execute { app.db.metadataDao().setAllSeen(App.profileId, Metadata.TYPE_HOMEWORK, true) } + AsyncTask.execute { app.db.metadataDao().setAllSeen(App.profileId, MetadataType.HOMEWORK, true) } Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show() })) val pagerAdapter = FragmentLazyPagerAdapter( - fragmentManager ?: return, + parentFragmentManager, b.refreshLayout, listOf( HomeworkListFragment().apply { @@ -104,7 +109,7 @@ class HomeworkFragment : Fragment(), CoroutineScope { } setFabOnClickListener(View.OnClickListener { - EventManualDialog(activity, App.profileId, defaultType = Event.TYPE_HOMEWORK) + EventManualDialog(activity, App.profileId, defaultType = Event.TYPE_HOMEWORK).show() }) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/homework/HomeworkListFragment.kt similarity index 60% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkListFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/homework/HomeworkListFragment.kt index 1852c340..4c354801 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/homework/HomeworkListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/homework/HomeworkListFragment.kt @@ -1,4 +1,4 @@ -package pl.szczodrzynski.edziennik.ui.modules.homework +package pl.szczodrzynski.edziennik.ui.homework import android.os.Bundle import android.view.LayoutInflater @@ -13,10 +13,12 @@ import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.databinding.HomeworkListFragmentBinding -import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog -import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter -import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog -import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment +import pl.szczodrzynski.edziennik.ext.getInt +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer +import pl.szczodrzynski.edziennik.ui.base.lazypager.LazyFragment +import pl.szczodrzynski.edziennik.ui.event.EventDetailsDialog +import pl.szczodrzynski.edziennik.ui.event.EventListAdapter +import pl.szczodrzynski.edziennik.ui.event.EventManualDialog import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import pl.szczodrzynski.edziennik.utils.models.Date import kotlin.coroutines.CoroutineContext @@ -54,57 +56,61 @@ class HomeworkListFragment : LazyFragment(), CoroutineScope { } val adapter = EventListAdapter( - activity, - showWeekDay = true, - showDate = true, - showType = false, - showTime = true, - showSubject = true, - markAsSeen = true, - onItemClick = { - EventDetailsDialog( - activity, - it - ) - }, - onEventEditClick = { - EventManualDialog( - activity, - it.profileId, - editingEvent = it - ) - } + activity, + showWeekDay = true, + showDate = true, + showType = false, + showTime = true, + showSubject = true, + markAsSeen = true, + isReversed = homeworkDate == HomeworkDate.PAST, + onEventClick = { + EventDetailsDialog( + activity, + it + ).show() + }, + onEventEditClick = { + EventManualDialog( + activity, + it.profileId, + editingEvent = it + ).show() + } ) - app.db.eventDao().getAllByType(App.profileId, Event.TYPE_HOMEWORK, filter).observe(this@HomeworkListFragment, Observer { items -> + app.db.eventDao().getAllByType(App.profileId, Event.TYPE_HOMEWORK, filter).observe(this@HomeworkListFragment, Observer { events -> if (!isAdded) return@Observer - // load & configure the adapter - adapter.items = items - if (items.isNotNullNorEmpty() && b.list.adapter == null) { - b.list.adapter = adapter - b.list.apply { - setHasFixedSize(true) - layoutManager = LinearLayoutManager(context).apply { - reverseLayout = homeworkDate == HomeworkDate.PAST - stackFromEnd = homeworkDate == HomeworkDate.PAST - } - addItemDecoration(SimpleDividerItemDecoration(context)) - addOnScrollListener(onScrollListener) - } + events.forEach { + it.filterNotes() } - adapter.notifyDataSetChanged() - setSwipeToRefresh(items.isNullOrEmpty()) // show/hide relevant views + setSwipeToRefresh(events.isEmpty()) b.progressBar.isVisible = false - if (items.isNullOrEmpty()) { - b.list.isVisible = false - b.noData.isVisible = true - } else { - b.list.isVisible = true - b.noData.isVisible = false + b.list.isVisible = events.isNotEmpty() + b.noData.isVisible = events.isEmpty() + if (events.isEmpty()) { + return@Observer } + + // apply the new event list + adapter.setAllItems(events, addSearchField = true) + + // configure the adapter & recycler view + if (b.list.adapter == null) { + b.list.apply { + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + addItemDecoration(SimpleDividerItemDecoration(context)) + addOnScrollListener(onScrollListener) + this.adapter = adapter + } + } + + // reapply the filter + adapter.getSearchField()?.applyTo(adapter) }) }; return true } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/intro/ChangelogIntroActivity.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/intro/ChangelogIntroActivity.java similarity index 98% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/intro/ChangelogIntroActivity.java rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/intro/ChangelogIntroActivity.java index 0c60efbd..f5632876 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/intro/ChangelogIntroActivity.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/intro/ChangelogIntroActivity.java @@ -1,4 +1,4 @@ -package pl.szczodrzynski.edziennik.ui.modules.intro; +package pl.szczodrzynski.edziennik.ui.intro; import android.os.Build; import android.os.Bundle; diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/intro/FragmentSlideMod.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/intro/FragmentSlideMod.java similarity index 99% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/intro/FragmentSlideMod.java rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/intro/FragmentSlideMod.java index 1d3cfb92..47bf2a48 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/intro/FragmentSlideMod.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/intro/FragmentSlideMod.java @@ -22,22 +22,23 @@ * SOFTWARE. */ -package pl.szczodrzynski.edziennik.ui.modules.intro; +package pl.szczodrzynski.edziennik.ui.intro; import android.content.Context; import android.os.Build; import android.os.Bundle; -import androidx.annotation.ColorRes; -import androidx.annotation.LayoutRes; -import androidx.annotation.StringRes; -import androidx.annotation.StyleRes; -import androidx.fragment.app.Fragment; -import androidx.appcompat.view.ContextThemeWrapper; import android.text.Html; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import androidx.annotation.ColorRes; +import androidx.annotation.LayoutRes; +import androidx.annotation.StringRes; +import androidx.annotation.StyleRes; +import androidx.appcompat.view.ContextThemeWrapper; +import androidx.fragment.app.Fragment; + import com.heinrichreimersoftware.materialintro.app.ButtonCtaFragment; import com.heinrichreimersoftware.materialintro.app.SlideFragment; import com.heinrichreimersoftware.materialintro.slide.ButtonCtaSlide; diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginActivity.kt similarity index 90% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginActivity.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginActivity.kt index 32446605..08d4917d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginActivity.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-16. */ -package pl.szczodrzynski.edziennik.ui.modules.login +package pl.szczodrzynski.edziennik.ui.login import android.app.Activity import android.os.Bundle @@ -19,7 +19,8 @@ import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.db.entity.LoginStore import pl.szczodrzynski.edziennik.databinding.LoginActivityBinding -import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar +import pl.szczodrzynski.edziennik.ui.error.ErrorSnackbar +import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoTouch import kotlin.coroutines.CoroutineContext class LoginActivity : AppCompatActivity(), CoroutineScope { @@ -30,8 +31,10 @@ class LoginActivity : AppCompatActivity(), CoroutineScope { private val app: App by lazy { applicationContext as App } private lateinit var b: LoginActivityBinding lateinit var navOptions: NavOptions + lateinit var navOptionsBuilder: NavOptions.Builder val nav by lazy { Navigation.findNavController(this, R.id.nav_host_fragment) } val errorSnackbar: ErrorSnackbar by lazy { ErrorSnackbar(this) } + val swipeRefreshLayout: SwipeRefreshLayoutNoTouch by lazy { b.swipeRefreshLayout } private val job: Job = Job() override val coroutineContext: CoroutineContext @@ -85,12 +88,12 @@ class LoginActivity : AppCompatActivity(), CoroutineScope { super.onCreate(savedInstanceState) setTheme(R.style.AppTheme_Light) - navOptions = NavOptions.Builder() + navOptionsBuilder = NavOptions.Builder() .setEnterAnim(R.anim.slide_in_right) .setExitAnim(R.anim.slide_out_left) .setPopEnterAnim(R.anim.slide_in_left) .setPopExitAnim(R.anim.slide_out_right) - .build() + navOptions = navOptionsBuilder.build() b = LoginActivityBinding.inflate(layoutInflater) setContentView(b.root) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginChooserAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginChooserAdapter.kt similarity index 92% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginChooserAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginChooserAdapter.kt index 704e660a..e2e02a05 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginChooserAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginChooserAdapter.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-16. */ -package pl.szczodrzynski.edziennik.ui.modules.login +package pl.szczodrzynski.edziennik.ui.login import android.view.LayoutInflater import android.view.View @@ -14,10 +14,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel -import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder -import pl.szczodrzynski.edziennik.ui.modules.login.viewholder.ModeViewHolder -import pl.szczodrzynski.edziennik.ui.modules.login.viewholder.RegisterViewHolder +import pl.szczodrzynski.edziennik.ui.grades.models.ExpandableItemModel +import pl.szczodrzynski.edziennik.ui.grades.viewholder.BindableViewHolder +import pl.szczodrzynski.edziennik.ui.login.viewholder.ModeViewHolder +import pl.szczodrzynski.edziennik.ui.login.viewholder.RegisterViewHolder import kotlin.coroutines.CoroutineContext class LoginChooserAdapter( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginChooserFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginChooserFragment.kt similarity index 75% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginChooserFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginChooserFragment.kt index 858b4a01..f48258a9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginChooserFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginChooserFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-16. */ -package pl.szczodrzynski.edziennik.ui.modules.login +package pl.szczodrzynski.edziennik.ui.login import android.animation.ValueAnimator import android.annotation.SuppressLint @@ -10,7 +10,6 @@ import android.app.Activity import android.content.Intent import android.graphics.Color import android.os.Bundle -import android.text.Html import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -24,15 +23,18 @@ import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.* -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.api.* -import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi -import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.enums.LoginMode +import pl.szczodrzynski.edziennik.data.db.enums.LoginType import pl.szczodrzynski.edziennik.databinding.LoginChooserFragmentBinding -import pl.szczodrzynski.edziennik.ui.dialogs.RegisterUnavailableDialog -import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackActivity +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.ui.dialogs.sync.RegisterUnavailableDialog +import pl.szczodrzynski.edziennik.ui.feedback.FeedbackActivity import pl.szczodrzynski.edziennik.utils.BetterLinkMovementMethod import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration +import pl.szczodrzynski.edziennik.utils.html.BetterHtml +import pl.szczodrzynski.edziennik.utils.managers.AvailabilityManager.Error.Type import pl.szczodrzynski.edziennik.utils.models.Date import kotlin.coroutines.CoroutineContext @@ -51,7 +53,8 @@ class LoginChooserFragment : Fragment(), CoroutineScope { private val job: Job = Job() override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main - + private val manager + get() = app.permissionManager // local/private variables go here override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -66,6 +69,11 @@ class LoginChooserFragment : Fragment(), CoroutineScope { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { if (!isAdded) return + val adapter = LoginChooserAdapter(activity, this::onLoginModeClicked) + if (!manager.isNotificationPermissionGranted) { + manager.requestNotificationsPermission(activity, 0, false){} + } + b.versionText.setText( R.string.login_chooser_version_format, app.buildManager.versionName, @@ -73,10 +81,32 @@ class LoginChooserFragment : Fragment(), CoroutineScope { ) b.versionText.onClick { app.buildManager.showVersionDialog(activity) + if (!App.devMode) + return@onClick + if (adapter.items.firstOrNull { it is LoginInfo.Register && it.loginType == LoginType.TEMPLATE } != null) + return@onClick + adapter.items.add( + index = 0, + element = LoginInfo.Register( + loginType = LoginType.TEMPLATE, + registerName = R.string.menu_lab, + registerLogo = R.drawable.face_2, + loginModes = listOf( + LoginInfo.Mode( + loginMode = LoginMode.PODLASIE_API, + name = 0, + icon = 0, + guideText = 0, + credentials = listOf(), + errorCodes = mapOf() + ) + ) + ) + ) + adapter.notifyItemInserted(0) + b.list.smoothScrollToPosition(0) } - val adapter = LoginChooserAdapter(activity, this::onLoginModeClicked) - LoginInfo.chooserList = LoginInfo.chooserList ?: LoginInfo.list.toMutableList() @@ -164,16 +194,15 @@ class LoginChooserFragment : Fragment(), CoroutineScope { anim.fillAfter = true activity.getRootView().startAnimation(anim) - b.list.smoothScrollToPosition(0) + adapter.items.removeAll { it !is LoginInfo.Register } adapter.items.add( LoginInfo.Register( - loginType = 74, - internalName = "eggs", + loginType = LoginType.DEMO, registerName = R.string.eggs, registerLogo = R.drawable.face_1, loginModes = listOf( LoginInfo.Mode( - loginMode = 0, + loginMode = LoginMode.PODLASIE_API, name = 0, icon = 0, guideText = 0, @@ -183,7 +212,8 @@ class LoginChooserFragment : Fragment(), CoroutineScope { ) ) ) - adapter.notifyItemInserted(adapter.items.size - 1) + adapter.notifyDataSetChanged() + b.list.smoothScrollToPosition(adapter.items.size - 1) } when { @@ -211,15 +241,20 @@ class LoginChooserFragment : Fragment(), CoroutineScope { loginType: LoginInfo.Register, loginMode: LoginInfo.Mode ) { - if (loginType.internalName == "eggs") { + if (loginType.loginType == LoginType.DEMO) { nav.navigate(R.id.loginEggsFragment, null, activity.navOptions) return } + if (loginType.loginType == LoginType.TEMPLATE) { + nav.navigate(R.id.labFragment, null, activity.navOptions) + return + } + if (!app.config.privacyPolicyAccepted) { MaterialAlertDialogBuilder(activity) .setTitle(R.string.privacy_policy) - .setMessage(Html.fromHtml(activity.getString(R.string.privacy_policy_dialog_html))) + .setMessage(BetterHtml.fromHtml(activity, R.string.privacy_policy_dialog_html)) .setPositiveButton(R.string.i_agree) { _, _ -> app.config.privacyPolicyAccepted = true onLoginModeClicked(loginType, loginMode) @@ -268,53 +303,24 @@ class LoginChooserFragment : Fragment(), CoroutineScope { ), activity.navOptions) } - private suspend fun checkAvailability(loginType: Int): Boolean { - when (loginType) { - LOGIN_TYPE_LIBRUS -> "librus" - LOGIN_TYPE_VULCAN -> "vulcan" - LOGIN_TYPE_IDZIENNIK -> "idziennik" - LOGIN_TYPE_MOBIDZIENNIK -> "mobidziennik" - LOGIN_TYPE_PODLASIE -> "podlasie" - LOGIN_TYPE_EDUDZIENNIK -> "edudziennik" - else -> null - }?.let { registerName -> - var status = app.config.sync.registerAvailability[registerName] - if (status == null || status.nextCheckAt < currentTimeUnix()) { - val api = SzkolnyApi(app) - val result = withContext(Dispatchers.IO) { - return@withContext api.runCatching({ - val availability = getRegisterAvailability() - app.config.sync.registerAvailability = availability - availability[registerName] - }, onError = { - if (it.toErrorCode() == ERROR_API_INVALID_SIGNATURE) { - return@withContext false - } - return@withContext it - }) - } + private suspend fun checkAvailability(loginType: LoginType): Boolean { + val error = withContext(Dispatchers.IO) { + app.availabilityManager.check(loginType) + } ?: return true - when (result) { - false -> { - Toast.makeText(activity, R.string.error_no_api_access, Toast.LENGTH_SHORT).show() - return@let - } - is Throwable -> { - activity.errorSnackbar.addError(result.toApiError(TAG)).show() - return false - } - is RegisterAvailabilityStatus -> { - status = result - } - } + return when (error.type) { + Type.NOT_AVAILABLE -> { + RegisterUnavailableDialog(activity, error.status!!).show() + false } - - if (status?.available != true || status.minVersionCode > BuildConfig.VERSION_CODE) { - if (status != null) - RegisterUnavailableDialog(activity, status) - return false + Type.API_ERROR -> { + activity.errorSnackbar.addError(error.apiError!!).show() + false + } + Type.NO_API_ACCESS -> { + Toast.makeText(activity, R.string.error_no_api_access, Toast.LENGTH_SHORT).show() + true } } - return true } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginEggsFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginEggsFragment.kt similarity index 92% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginEggsFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginEggsFragment.kt index 0c11bb5a..429fd4fb 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginEggsFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginEggsFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-10-18. */ -package pl.szczodrzynski.edziennik.ui.modules.login +package pl.szczodrzynski.edziennik.ui.login import android.annotation.SuppressLint import android.os.Build @@ -24,7 +24,7 @@ import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.BuildConfig import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.md5 +import pl.szczodrzynski.edziennik.ext.md5 import kotlin.coroutines.CoroutineContext class LoginEggsFragment : Fragment(), CoroutineScope { @@ -95,8 +95,10 @@ class LoginEggsFragment : Fragment(), CoroutineScope { anim.interpolator = AccelerateDecelerateInterpolator() anim.duration = 10 anim.fillAfter = true - activity.getRootView().startAnimation(anim) - nav.navigate(R.id.loginPrizeFragment, null, activity.navOptions) + activity.runOnUiThread { + activity.getRootView().startAnimation(anim) + nav.navigate(R.id.loginPrizeFragment, null, activity.navOptions) + } } }, "EggInterface") loadUrl("https://szkolny.eu/game/runner.html") diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginFinishFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFinishFragment.kt similarity index 88% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginFinishFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFinishFragment.kt index 7b2e66b8..ee9e34af 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginFinishFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFinishFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-16. */ -package pl.szczodrzynski.edziennik.ui.modules.login +package pl.szczodrzynski.edziennik.ui.login import android.app.Activity import android.os.Bundle @@ -15,6 +15,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.databinding.LoginFinishFragmentBinding +import pl.szczodrzynski.edziennik.ext.Intent +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget import kotlin.coroutines.CoroutineContext class LoginFinishFragment : Fragment(), CoroutineScope { @@ -62,14 +65,14 @@ class LoginFinishFragment : Fragment(), CoroutineScope { activity, MainActivity::class.java, "profileId" to firstProfileId, - "fragmentId" to MainActivity.DRAWER_ITEM_HOME + "fragmentId" to NavTarget.HOME )) } else { activity.setResult(Activity.RESULT_OK, Intent( null, "profileId" to firstProfileId, - "fragmentId" to MainActivity.DRAWER_ITEM_HOME + "fragmentId" to NavTarget.HOME )) } activity.finish() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt new file mode 100644 index 00000000..c4485aba --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginFormFragment.kt @@ -0,0 +1,321 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-16. + */ + +package pl.szczodrzynski.edziennik.ui.login + +import android.annotation.SuppressLint +import android.os.Bundle +import android.text.InputType +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.core.view.isVisible +import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.Fragment +import androidx.navigation.NavOptions +import androidx.navigation.navOptions +import androidx.viewbinding.ViewBinding +import com.google.android.material.textfield.TextInputLayout +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import com.mikepenz.iconics.utils.sizeDp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.enums.LoginMode +import pl.szczodrzynski.edziennik.data.db.enums.LoginType +import pl.szczodrzynski.edziennik.databinding.LoginFormCheckboxItemBinding +import pl.szczodrzynski.edziennik.databinding.LoginFormFieldItemBinding +import pl.szczodrzynski.edziennik.databinding.LoginFormFragmentBinding +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.ui.dialogs.QrScannerDialog +import pl.szczodrzynski.edziennik.ui.login.LoginInfo.BaseCredential +import pl.szczodrzynski.edziennik.ui.login.LoginInfo.FormCheckbox +import pl.szczodrzynski.edziennik.ui.login.LoginInfo.FormField +import pl.szczodrzynski.navlib.colorAttr +import kotlin.coroutines.CoroutineContext + +class LoginFormFragment : Fragment(), CoroutineScope { + companion object { + private const val TAG = "LoginFormFragment" + + // eggs + var wantEggs = false + var isEggs = false + } + + private lateinit var app: App + private lateinit var activity: LoginActivity + private lateinit var b: LoginFormFragmentBinding + private val nav by lazy { activity.nav } + + private val job: Job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + private val credentials = mutableMapOf() + private val platformName + get() = arguments?.getString("platformName") + private val platformGuideText + get() = arguments?.getString("platformGuideText") + private val platformDescription + get() = arguments?.getString("platformDescription") + private val platformFormFields + get() = arguments?.getString("platformFormFields")?.split(";") + private val platformData + get() = arguments?.getString("platformData")?.toJsonObject() + private val platformStoreKey + get() = arguments?.getString("platformStoreKey") + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View? { + activity = (getActivity() as LoginActivity?) ?: return null + context ?: return null + app = activity.application as App + b = LoginFormFragmentBinding.inflate(inflater) + return b.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + if (!isAdded) return + b.backButton.onClick { nav.navigateUp() } + + b.errorLayout.isVisible = false + b.errorLayout.background?.setTintColor(R.attr.colorError.resolveAttr(activity)) + + val loginType = arguments?.getEnum("loginType") ?: return + val register = LoginInfo.list.firstOrNull { it.loginType == loginType } ?: return + val loginMode = arguments?.getEnum("loginMode") ?: return + val mode = register.loginModes.firstOrNull { it.loginMode == loginMode } ?: return + + if (mode.credentials.isEmpty()) { + login(loginType, loginMode) + return + } + + b.title.setText(R.string.login_form_title_format, app.getString(register.registerName)) + b.subTitle.text = platformName ?: app.getString(mode.name) + b.text.text = platformGuideText ?: app.getString(mode.guideText) + + // eggs + isEggs = register.loginType == LoginType.PODLASIE + + for (credential in mode.credentials) { + if (platformFormFields?.contains(credential.keyName) == false) + continue + + val b = when (credential) { + is FormField -> buildFormField(credential) + is FormCheckbox -> buildFormCheckbox(credential) + else -> continue + } + this.b.formContainer.addView(b.root) + credentials[credential] = b + } + + activity.lastError?.let { error -> + activity.lastError = null + startCoroutineTimer(delayMillis = 200L) { + for (credential in credentials) { + credential.key.errorCodes[error.errorCode]?.let { + (credential.value as? LoginFormFieldItemBinding)?.let { b -> + b.textLayout.error = app.getString(it) + } + (credential.value as? LoginFormCheckboxItemBinding)?.let { b -> + b.errorText.text = app.getString(it) + } + return@startCoroutineTimer + } + } + mode.errorCodes[error.errorCode]?.let { + b.errorText.text = app.getString(it) + b.errorLayout.isVisible = true + return@startCoroutineTimer + } + } + } + + b.loginButton.onClick { + login(loginType, loginMode) + } + } + + @Suppress("UNCHECKED_CAST") + private fun getCredential(keyName: String): Pair? { + val (credential, binding) = credentials.entries.firstOrNull { + it.key.keyName == keyName + } ?: return null + val c = credential as? C ?: return null + val b = binding as? B ?: return null + return c to b + } + + @SuppressLint("ResourceType") + private fun buildFormField(credential: FormField): LoginFormFieldItemBinding { + val b = LoginFormFieldItemBinding.inflate(layoutInflater) + + if (credential.isNumber) { + b.textEdit.inputType = InputType.TYPE_CLASS_NUMBER + } + + if (credential.qrDecoderClass != null) { + b.textLayout.endIconMode = TextInputLayout.END_ICON_CUSTOM + b.textLayout.endIconDrawable = IconicsDrawable(activity).apply { + icon = CommunityMaterial.Icon3.cmd_qrcode + sizeDp = 24 + colorAttr(activity, R.attr.colorOnBackground) + } + b.textLayout.setEndIconOnClickListener { + scanQrCode(credential) + } + } + + if (credential.hideText) { + b.textEdit.inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD + b.textLayout.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE + } + + b.textEdit.addTextChangedListener { + b.textLayout.error = null + } + + b.textEdit.id = credential.name + b.textEdit.setText(arguments?.getString(credential.keyName)) + b.textLayout.hint = credential.name.resolveString(app) + b.textLayout.prefixText = credential.prefix?.resolveString(app) + b.textLayout.suffixText = credential.suffix?.resolveString(app) + b.textLayout.tag = credential + + b.textLayout.startIconDrawable = IconicsDrawable(activity).apply { + icon = credential.icon + sizeDp = 24 + colorAttr(activity, R.attr.colorOnBackground) + } + + return b + } + + private fun buildFormCheckbox(credential: FormCheckbox): LoginFormCheckboxItemBinding { + val b = LoginFormCheckboxItemBinding.inflate(layoutInflater) + + b.checkbox.onChange { _, isChecked -> + b.errorText.text = null + + // eggs + if (isEggs) { + wantEggs = !isChecked + } + } + + if (arguments?.containsKey(credential.keyName) == true) { + b.checkbox.isChecked = arguments?.getBoolean(credential.keyName) == true + } + + b.checkbox.tag = credential + b.checkbox.text = credential.name.resolveString(app) + + return b + } + + private fun scanQrCode(credential: FormField) { + val qrDecoderClass = credential.qrDecoderClass ?: return + app.permissionManager.requestCameraPermission(activity, R.string.permissions_qr_scanner) { + QrScannerDialog(activity, onCodeScanned = { code -> + val decoder = qrDecoderClass.newInstance() + val values = decoder.decode(code) + if (values == null) { + Toast.makeText(activity, R.string.login_qr_decoding_error, Toast.LENGTH_SHORT).show() + return@QrScannerDialog + } + + values.forEach { (keyName, fieldText) -> + val (_, b) = getCredential(keyName) + ?: return@forEach + b.textEdit.setText(fieldText) + } + + decoder.focusFieldName()?.let { keyName -> + val (_, b) = getCredential(keyName) + ?: return@let + b.textEdit.requestFocus() + } + }).show() + } + } + + private fun login(loginType: LoginType, loginMode: LoginMode) { + val payload = Bundle( + "loginType" to loginType, + "loginMode" to loginMode + ) + + if (App.debugMode && b.fakeLogin.isChecked) { + payload.putBoolean("fakeLogin", true) + } + + if (platformStoreKey == null) + payload.putAll(platformData?.toBundle() ?: Bundle()) + else + payload.putBundle(platformStoreKey, platformData?.toBundle()) + + var hasErrors = false + credentials.forEach { (credential, b) -> + if (credential is FormField && b is LoginFormFieldItemBinding) { + var text = b.textEdit.text?.toString() ?: return@forEach + if (!credential.hideText) + text = text.trim() + + if (credential.caseMode == FormField.CaseMode.UPPER_CASE) + text = text.uppercase() + if (credential.caseMode == FormField.CaseMode.LOWER_CASE) + text = text.lowercase() + + credential.stripTextRegex?.let { + text = text.replace(it.toRegex(), "") + } + + b.textEdit.setText(text) + + if (credential.isRequired && text.isBlank()) { + b.textLayout.error = app.getString(credential.emptyText) + hasErrors = true + return@forEach + } + + if (!text.matches(credential.validationRegex.toRegex())) { + b.textLayout.error = app.getString(credential.invalidText) + hasErrors = true + return@forEach + } + + payload.putString(credential.keyName, text) + arguments?.putString(credential.keyName, text) + } + if (credential is FormCheckbox && b is LoginFormCheckboxItemBinding) { + val checked = b.checkbox.isChecked + payload.putBoolean(credential.keyName, checked) + arguments?.putBoolean(credential.keyName, checked) + } + } + + if (hasErrors) + return + + val navOptions = + if (credentials.isEmpty()) + activity.navOptionsBuilder + .setPopUpTo(R.id.loginPlatformListFragment, inclusive = false) + .build() + else + activity.navOptions + + nav.navigate(R.id.loginProgressFragment, payload, navOptions) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginInfo.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt similarity index 87% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginInfo.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt index 77036f19..33e191d7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginInfo.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginInfo.kt @@ -2,15 +2,21 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-16. */ -package pl.szczodrzynski.edziennik.ui.modules.login +package pl.szczodrzynski.edziennik.ui.login import androidx.annotation.DrawableRes import androidx.annotation.StringRes +import com.google.gson.JsonObject import com.mikepenz.iconics.typeface.IIcon import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.* -import pl.szczodrzynski.edziennik.ui.modules.grades.models.ExpandableItemModel +import pl.szczodrzynski.edziennik.data.db.enums.LoginMode +import pl.szczodrzynski.edziennik.data.db.enums.LoginType +import pl.szczodrzynski.edziennik.ui.grades.models.ExpandableItemModel +import pl.szczodrzynski.edziennik.ui.login.qr.LoginLibrusQrDecoder +import pl.szczodrzynski.edziennik.ui.login.qr.LoginQrDecoder +import pl.szczodrzynski.edziennik.ui.login.qr.LoginVulcanQrDecoder import pl.szczodrzynski.fslogin.realm.RealmData object LoginInfo { @@ -42,13 +48,12 @@ object LoginInfo { val list by lazy { listOf( Register( - loginType = LOGIN_TYPE_LIBRUS, - internalName = "librus", + loginType = LoginType.LIBRUS, registerName = R.string.login_register_librus, registerLogo = R.drawable.login_logo_librus, loginModes = listOf( Mode( - loginMode = LOGIN_MODE_LIBRUS_EMAIL, + loginMode = LoginMode.LIBRUS_EMAIL, name = R.string.login_mode_librus_email, icon = R.drawable.login_mode_librus_email, hintText = R.string.login_mode_librus_email_hint, @@ -61,11 +66,10 @@ object LoginInfo { errorCodes = mapOf( ERROR_LOGIN_LIBRUS_PORTAL_NOT_ACTIVATED to R.string.login_error_account_not_activated, ERROR_LOGIN_LIBRUS_PORTAL_INVALID_LOGIN to R.string.login_error_incorrect_login_or_password, - ERROR_CAPTCHA_LIBRUS_PORTAL to R.string.error_3001_reason ) ), /*Mode( - loginMode = LOGIN_MODE_LIBRUS_SYNERGIA, + loginMode = LoginMode.LIBRUS_SYNERGIA, name = R.string.login_mode_librus_synergia, icon = R.drawable.login_mode_librus_synergia, hintText = R.string.login_mode_librus_synergia_hint, @@ -90,7 +94,7 @@ object LoginInfo { ) ),*/ Mode( - loginMode = LOGIN_MODE_LIBRUS_JST, + loginMode = LoginMode.LIBRUS_JST, name = R.string.login_mode_librus_jst, icon = R.drawable.login_mode_librus_jst, hintText = R.string.login_mode_librus_jst_hint, @@ -105,7 +109,8 @@ object LoginInfo { errorCodes = mapOf(), isRequired = true, validationRegex = "[A-Z0-9_]+", - caseMode = FormField.CaseMode.UPPER_CASE + caseMode = FormField.CaseMode.UPPER_CASE, + qrDecoderClass = LoginLibrusQrDecoder::class.java ), FormField( keyName = "accountPin", @@ -127,13 +132,12 @@ object LoginInfo { ) ), Register( - loginType = LOGIN_TYPE_VULCAN, - internalName = "vulcan", + loginType = LoginType.VULCAN, registerName = R.string.login_type_vulcan, registerLogo = R.drawable.login_logo_vulcan, loginModes = listOf( Mode( - loginMode = LOGIN_MODE_VULCAN_HEBE, + loginMode = LoginMode.VULCAN_HEBE, name = R.string.login_mode_vulcan_api, icon = R.drawable.login_mode_vulcan_hebe, hintText = R.string.login_mode_vulcan_api_hint, @@ -152,7 +156,8 @@ object LoginInfo { ), isRequired = true, validationRegex = "[A-Z0-9]{5,12}", - caseMode = FormField.CaseMode.UPPER_CASE + caseMode = FormField.CaseMode.UPPER_CASE, + qrDecoderClass = LoginVulcanQrDecoder::class.java ), FormField( keyName = "symbol", @@ -179,6 +184,7 @@ object LoginInfo { ERROR_LOGIN_VULCAN_INVALID_PIN_2_REMAINING to R.string.error_312_reason ), isRequired = true, + isNumber = true, validationRegex = "[0-9]+", caseMode = FormField.CaseMode.LOWER_CASE ) @@ -186,7 +192,7 @@ object LoginInfo { errorCodes = mapOf() ), Mode( - loginMode = LOGIN_MODE_VULCAN_WEB, + loginMode = LoginMode.VULCAN_WEB, name = R.string.login_mode_vulcan_web, icon = R.drawable.login_mode_vulcan_web, hintText = R.string.login_mode_vulcan_web_hint, @@ -213,13 +219,12 @@ object LoginInfo { ) ), Register( - loginType = LOGIN_TYPE_MOBIDZIENNIK, - internalName = "mobidziennik", + loginType = LoginType.MOBIDZIENNIK, registerName = R.string.login_type_mobidziennik, registerLogo = R.drawable.login_logo_mobidziennik, loginModes = listOf( Mode( - loginMode = LOGIN_MODE_MOBIDZIENNIK_WEB, + loginMode = LoginMode.MOBIDZIENNIK_WEB, name = R.string.login_mode_mobidziennik_web, icon = R.drawable.login_mode_mobidziennik_web, hintText = R.string.login_mode_mobidziennik_web_hint, @@ -258,6 +263,8 @@ object LoginInfo { errorCodes = mapOf( ERROR_LOGIN_MOBIDZIENNIK_WEB_INVALID_ADDRESS to R.string.login_error_incorrect_address ), + prefix = R.string.login_mobidziennik_server_prefix, + suffix = R.string.login_mobidziennik_server_suffix, isRequired = true, validationRegex = "^[a-z0-9_\\-]+\$", caseMode = FormField.CaseMode.LOWER_CASE @@ -271,35 +278,12 @@ object LoginInfo { ) ), Register( - loginType = LOGIN_TYPE_EDUDZIENNIK, - internalName = "edudziennik", - registerName = R.string.login_type_edudziennik, - registerLogo = R.drawable.login_logo_edudziennik, - loginModes = listOf( - Mode( - loginMode = LOGIN_MODE_EDUDZIENNIK_WEB, - name = R.string.login_mode_edudziennik_web, - icon = R.drawable.login_mode_edudziennik_web, - hintText = R.string.login_mode_edudziennik_web_hint, - guideText = R.string.login_mode_edudziennik_web_guide, - credentials = listOf( - getEmailCredential("email"), - getPasswordCredential("password") - ), - errorCodes = mapOf( - ERROR_LOGIN_EDUDZIENNIK_WEB_INVALID_LOGIN to R.string.login_error_incorrect_login_or_password - ) - ) - ) - ), - Register( - loginType = LOGIN_TYPE_PODLASIE, - internalName = "podlasie", + loginType = LoginType.PODLASIE, registerName = R.string.login_type_podlasie, registerLogo = R.drawable.login_logo_podlasie, loginModes = listOf( Mode( - loginMode = LOGIN_MODE_PODLASIE_API, + loginMode = LoginMode.PODLASIE_API, name = R.string.login_mode_podlasie_api, icon = R.drawable.login_mode_podlasie_api, guideText = R.string.login_mode_podlasie_api_guide, @@ -327,13 +311,28 @@ object LoginInfo { errorCodes = mapOf() ) ) - ) + ), + Register( + loginType = LoginType.USOS, + registerName = R.string.login_type_usos, + registerLogo = R.drawable.login_logo_usos, + loginModes = listOf( + Mode( + loginMode = LoginMode.USOS_OAUTH, + name = R.string.login_mode_usos_oauth, + icon = R.drawable.login_mode_usos_api, + guideText = R.string.login_mode_usos_oauth_guide, + isPlatformSelection = true, + credentials = listOf(), + errorCodes = mapOf(), + ), + ), + ), ) } data class Register( - val loginType: Int, - val internalName: String, + val loginType: LoginType, val registerName: Int, @DrawableRes val registerLogo: Int, @@ -344,7 +343,7 @@ object LoginInfo { } data class Mode( - val loginMode: Int, + val loginMode: LoginMode, @StringRes val name: Int, @@ -371,7 +370,8 @@ object LoginInfo { val icon: String, val screenshot: String?, val formFields: List, - val realmData: RealmData + val data: JsonObject, + val storeKey: String?, ) open class BaseCredential( @@ -396,12 +396,18 @@ object LoginInfo { override val errorCodes: Map, @StringRes val hintText: Int? = null, + @StringRes + val prefix: Int? = null, + @StringRes + val suffix: Int? = null, val isRequired: Boolean = true, val validationRegex: String, val caseMode: CaseMode = CaseMode.UNCHANGED, val hideText: Boolean = false, - val stripTextRegex: String? = null + val isNumber: Boolean = false, + val stripTextRegex: String? = null, + val qrDecoderClass: Class? = null, ) : BaseCredential(keyName, name, errorCodes) { enum class CaseMode { UNCHANGED, UPPER_CASE, LOWER_CASE } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginPlatformAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPlatformAdapter.kt similarity index 89% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginPlatformAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPlatformAdapter.kt index 5bc80f36..7f990514 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginPlatformAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPlatformAdapter.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-16. */ -package pl.szczodrzynski.edziennik.ui.modules.login +package pl.szczodrzynski.edziennik.ui.login import android.view.LayoutInflater import android.view.ViewGroup @@ -12,8 +12,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.onClick -import pl.szczodrzynski.edziennik.ui.modules.login.viewholder.PlatformViewHolder +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ui.login.viewholder.PlatformViewHolder import kotlin.coroutines.CoroutineContext class LoginPlatformAdapter( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginPlatformListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPlatformListFragment.kt similarity index 84% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginPlatformListFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPlatformListFragment.kt index 988d1bde..5055e228 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginPlatformListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPlatformListFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-16. */ -package pl.szczodrzynski.edziennik.ui.modules.login +package pl.szczodrzynski.edziennik.ui.login import android.os.Bundle import android.view.LayoutInflater @@ -17,7 +17,13 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi +import pl.szczodrzynski.edziennik.data.db.enums.LoginMode +import pl.szczodrzynski.edziennik.data.db.enums.LoginType import pl.szczodrzynski.edziennik.databinding.LoginPlatformListFragmentBinding +import pl.szczodrzynski.edziennik.ext.Bundle +import pl.szczodrzynski.edziennik.ext.getEnum +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import kotlin.coroutines.CoroutineContext @@ -53,9 +59,9 @@ class LoginPlatformListFragment : Fragment(), CoroutineScope { if (!isAdded) return b.backButton.onClick { nav.navigateUp() } - val loginType = arguments?.getInt("loginType") ?: return + val loginType = arguments?.getEnum("loginType") ?: return val register = LoginInfo.list.firstOrNull { it.loginType == loginType } ?: return - val loginMode = arguments?.getInt("loginMode") ?: return + val loginMode = arguments?.getEnum("loginMode") ?: return val mode = register.loginModes.firstOrNull { it.loginMode == loginMode } ?: return adapter = LoginPlatformAdapter(activity) { platform -> @@ -65,7 +71,8 @@ class LoginPlatformListFragment : Fragment(), CoroutineScope { "platformName" to platform.name, "platformDescription" to platform.description, "platformFormFields" to platform.formFields.joinToString(";"), - "platformRealmData" to app.gson.toJson(platform.realmData) + "platformData" to platform.data.toString(), + "platformStoreKey" to platform.storeKey, ), activity.navOptions) } @@ -96,7 +103,7 @@ class LoginPlatformListFragment : Fragment(), CoroutineScope { val platforms = LoginInfo.platformList[mode.name] ?: run { api.runCatching(activity) { - getRealms(register.internalName) + getRealms(register.loginType.name.lowercase()) } ?: run { nav.navigateUp() return@launch diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginPrizeFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPrizeFragment.kt similarity index 91% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginPrizeFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPrizeFragment.kt index 1c9634fc..35c57041 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginPrizeFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginPrizeFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-10-18. */ -package pl.szczodrzynski.edziennik.ui.modules.login +package pl.szczodrzynski.edziennik.ui.login import android.os.Bundle import android.os.Process @@ -18,7 +18,7 @@ import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.databinding.LoginPrizeFragmentBinding -import pl.szczodrzynski.edziennik.onClick +import pl.szczodrzynski.edziennik.ext.onClick import kotlin.coroutines.CoroutineContext import kotlin.system.exitProcess @@ -53,7 +53,7 @@ class LoginPrizeFragment : Fragment(), CoroutineScope { .setTitle(R.string.are_you_sure) .setMessage(R.string.dev_mode_enable_warning) .setPositiveButton(R.string.yes) { _, _ -> - app.config.debugMode = true + app.config.devMode = true App.devMode = true MaterialAlertDialogBuilder(activity) .setTitle("Restart") @@ -67,8 +67,8 @@ class LoginPrizeFragment : Fragment(), CoroutineScope { .show() } .setNegativeButton(R.string.no) { _, _ -> - app.config.debugMode = false - App.devMode = false + app.config.devMode = App.debugMode + App.devMode = App.debugMode activity.finish() } .show() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginProgressFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginProgressFragment.kt similarity index 81% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginProgressFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginProgressFragment.kt index 7550b08c..a0ad6e78 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginProgressFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginProgressFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-16. */ -package pl.szczodrzynski.edziennik.ui.modules.login +package pl.szczodrzynski.edziennik.ui.login import android.os.Bundle import android.view.LayoutInflater @@ -19,7 +19,7 @@ import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.api.ERROR_CAPTCHA_NEEDED +import pl.szczodrzynski.edziennik.data.api.ERROR_REQUIRES_USER_ACTION import pl.szczodrzynski.edziennik.data.api.LOGIN_NO_ARGUMENTS import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent @@ -27,8 +27,12 @@ import pl.szczodrzynski.edziennik.data.api.events.FirstLoginFinishedEvent import pl.szczodrzynski.edziennik.data.api.events.UserActionRequiredEvent import pl.szczodrzynski.edziennik.data.api.models.ApiError import pl.szczodrzynski.edziennik.data.db.entity.LoginStore +import pl.szczodrzynski.edziennik.data.db.enums.LoginMode +import pl.szczodrzynski.edziennik.data.db.enums.LoginType import pl.szczodrzynski.edziennik.databinding.LoginProgressFragmentBinding -import pl.szczodrzynski.edziennik.joinNotNullStrings +import pl.szczodrzynski.edziennik.ext.getEnum +import pl.szczodrzynski.edziennik.ext.joinNotNullStrings +import pl.szczodrzynski.edziennik.utils.managers.UserActionManager import kotlin.coroutines.CoroutineContext import kotlin.math.max @@ -76,10 +80,10 @@ class LoginProgressFragment : Fragment(), CoroutineScope { val maxProfileId = max( app.db.profileDao().lastId ?: 0, - activity.profiles.maxBy { it.profile.id }?.profile?.id ?: 0 + activity.profiles.maxByOrNull { it.profile.id }?.profile?.id ?: 0 ) - val loginType = args.getInt("loginType", -1) - val loginMode = args.getInt("loginMode", 0) + val loginType = args.getEnum("loginType") ?: return@launch + val loginMode = args.getEnum("loginMode") ?: return@launch val loginStore = LoginStore( id = maxProfileId + 1, @@ -137,14 +141,21 @@ class LoginProgressFragment : Fragment(), CoroutineScope { return } - app.userActionManager.execute(activity, event.profileId, event.type, onSuccess = { code -> - args.putString("recaptchaCode", code) - args.putLong("recaptchaTime", System.currentTimeMillis()) - doFirstLogin(args) - }, onFailure = { - activity.error(ApiError(TAG, ERROR_CAPTCHA_NEEDED)) - nav.navigateUp() - }) + val callback = UserActionManager.UserActionCallback( + onSuccess = { data -> + args.putAll(data) + doFirstLogin(args) + }, + onFailure = { + activity.error(ApiError(TAG, ERROR_REQUIRES_USER_ACTION)) + nav.navigateUp() + }, + onCancel = { + nav.navigateUp() + }, + ) + + app.userActionManager.execute(activity, event, callback) } override fun onStart() { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSummaryAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginSummaryAdapter.kt similarity index 95% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSummaryAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginSummaryAdapter.kt index bc9cafbf..f38f4f87 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSummaryAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginSummaryAdapter.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-16. */ -package pl.szczodrzynski.edziennik.ui.modules.login +package pl.szczodrzynski.edziennik.ui.login import android.view.LayoutInflater import android.view.ViewGroup @@ -14,8 +14,8 @@ import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.databinding.LoginSummaryItemBinding -import pl.szczodrzynski.edziennik.onClick -import pl.szczodrzynski.edziennik.trigger +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ext.trigger import kotlin.coroutines.CoroutineContext class LoginSummaryAdapter( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSummaryFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginSummaryFragment.kt similarity index 94% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSummaryFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginSummaryFragment.kt index ceca8a00..e03bba78 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSummaryFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginSummaryFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-16. */ -package pl.szczodrzynski.edziennik.ui.modules.login +package pl.szczodrzynski.edziennik.ui.login import android.os.Bundle import android.view.LayoutInflater @@ -16,6 +16,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.databinding.LoginSummaryFragmentBinding +import pl.szczodrzynski.edziennik.ext.Bundle +import pl.szczodrzynski.edziennik.ext.onChange +import pl.szczodrzynski.edziennik.ext.onClick import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import kotlin.coroutines.CoroutineContext diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSyncErrorFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginSyncErrorFragment.kt similarity index 94% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSyncErrorFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginSyncErrorFragment.kt index e13130a5..f9d694bd 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSyncErrorFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginSyncErrorFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-14. */ -package pl.szczodrzynski.edziennik.ui.modules.login +package pl.szczodrzynski.edziennik.ui.login import android.os.Bundle import android.view.LayoutInflater @@ -15,7 +15,7 @@ import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.databinding.LoginSyncErrorFragmentBinding -import pl.szczodrzynski.edziennik.onClick +import pl.szczodrzynski.edziennik.ext.onClick import kotlin.coroutines.CoroutineContext class LoginSyncErrorFragment : Fragment(), CoroutineScope { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSyncFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginSyncFragment.kt similarity index 76% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSyncFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginSyncFragment.kt index 3736e20f..c7f6d6a4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginSyncFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/LoginSyncFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-16. */ -package pl.szczodrzynski.edziennik.ui.modules.login +package pl.szczodrzynski.edziennik.ui.login import android.os.Bundle import android.view.LayoutInflater @@ -14,10 +14,14 @@ import androidx.navigation.Navigation import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode -import pl.szczodrzynski.edziennik.* +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 @@ -25,6 +29,10 @@ import pl.szczodrzynski.edziennik.data.api.events.ApiTaskProgressEvent import pl.szczodrzynski.edziennik.data.api.events.ApiTaskStartedEvent import pl.szczodrzynski.edziennik.data.db.entity.Profile import pl.szczodrzynski.edziennik.databinding.LoginSyncFragmentBinding +import pl.szczodrzynski.edziennik.ext.Bundle +import pl.szczodrzynski.edziennik.ext.asBoldSpannable +import pl.szczodrzynski.edziennik.ext.concat +import pl.szczodrzynski.edziennik.ext.ignore import kotlin.coroutines.CoroutineContext import kotlin.math.roundToInt @@ -52,7 +60,7 @@ class LoginSyncFragment : Fragment(), CoroutineScope { return b.root } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + override fun onViewCreated(view: View, savedInstanceState: Bundle?) = launch { EventBus.getDefault().removeStickyEvent(ApiTaskAllFinishedEvent::class.java) EventBus.getDefault().removeStickyEvent(ApiTaskErrorEvent::class.java) @@ -60,26 +68,33 @@ class LoginSyncFragment : Fragment(), CoroutineScope { val profiles = activity.profiles.filter { it.isSelected }.map { it.profile } val loginStores = activity.loginStores.filter { store -> profiles.any { it.loginStoreId == store.id } } - val registrationAllowed = arguments?.getBoolean("registrationAllowed") ?: false - profiles.forEach { - it.registration = if (registrationAllowed) - Profile.REGISTRATION_ENABLED - else - Profile.REGISTRATION_DISABLED + withContext(Dispatchers.IO) { + val registrationAllowed = arguments?.getBoolean("registrationAllowed") ?: false + profiles.forEach { + it.registration = if (registrationAllowed) + Profile.REGISTRATION_ENABLED + else + Profile.REGISTRATION_DISABLED - app.db.eventTypeDao().addDefaultTypes(activity, it.id) + val data = AppData.get(it.loginStoreType) + for ((key, value) in data.configOverrides) { + it.config.set(key, value) + } + + app.db.eventTypeDao().addDefaultTypes(it) + } + + app.db.profileDao().addAll(profiles) + app.db.loginStoreDao().addAll(loginStores) } - app.db.profileDao().addAll(profiles) - app.db.loginStoreDao().addAll(loginStores) - finishArguments = Bundle( "firstProfileId" to profiles.firstOrNull()?.id ) - val profileIds = profiles.map { it.id } + val profileIds = profiles.map { it.id }.toSet() EdziennikTask.syncProfileList(profileIds).enqueue(activity) - } + }.ignore() @Subscribe(threadMode = ThreadMode.MAIN) fun onSyncStartedEvent(event: ApiTaskStartedEvent) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/oauth/OAuthLoginActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/oauth/OAuthLoginActivity.kt new file mode 100644 index 00000000..c80c3d82 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/oauth/OAuthLoginActivity.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-15. + */ + +package pl.szczodrzynski.edziennik.ui.login.oauth + +import android.annotation.SuppressLint +import android.graphics.Bitmap +import android.os.Bundle +import android.view.ViewGroup.LayoutParams.MATCH_PARENT +import android.webkit.WebView +import android.webkit.WebViewClient +import android.widget.FrameLayout +import androidx.appcompat.app.AppCompatActivity +import org.greenrobot.eventbus.EventBus +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.utils.Utils.d + +class OAuthLoginActivity : AppCompatActivity() { + companion object { + private const val TAG = "OAuthLoginActivity" + } + + private var isSuccessful = false + + @SuppressLint("SetJavaScriptEnabled") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setTitle(R.string.oauth_dialog_title) + + val authorizeUrl = intent.getStringExtra("authorizeUrl") ?: return + val redirectUrl = intent.getStringExtra("redirectUrl") ?: return + + val webView = WebView(this) + webView.webViewClient = object : WebViewClient() { + override fun onPageStarted(view: WebView, url: String, favicon: Bitmap?) { + d(TAG, "Navigating to $url") + if (url.startsWith(redirectUrl)) { + isSuccessful = true + EventBus.getDefault().post(OAuthLoginResult( + isError = false, + responseUrl = url, + )) + finish() + } + } + } + webView.settings.javaScriptEnabled = true + webView.layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) + setContentView(webView) + + webView.loadUrl(authorizeUrl) + } + + override fun onDestroy() { + super.onDestroy() + if (!isSuccessful) + EventBus.getDefault().post(OAuthLoginResult( + isError = false, + responseUrl = null, + )) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/oauth/OAuthLoginResult.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/oauth/OAuthLoginResult.kt new file mode 100644 index 00000000..108c0c8e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/oauth/OAuthLoginResult.kt @@ -0,0 +1,10 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2022-10-15. + */ + +package pl.szczodrzynski.edziennik.ui.login.oauth + +data class OAuthLoginResult( + val isError: Boolean, + val responseUrl: String?, +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/qr/LoginLibrusQrDecoder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/qr/LoginLibrusQrDecoder.kt new file mode 100644 index 00000000..ed059858 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/qr/LoginLibrusQrDecoder.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-23. + */ + +package pl.szczodrzynski.edziennik.ui.login.qr + +class LoginLibrusQrDecoder : LoginQrDecoder { + + private val regex = "[A-Z0-9_]+".toRegex() + + override fun decode(value: String): Map? { + if (!regex.matches(value) || value.length > 10) + return null + return mapOf( + "accountCode" to value, + ) + } + + override fun focusFieldName() = "accountPin" +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/qr/LoginQrDecoder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/qr/LoginQrDecoder.kt new file mode 100644 index 00000000..0f7ac50e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/qr/LoginQrDecoder.kt @@ -0,0 +1,7 @@ +package pl.szczodrzynski.edziennik.ui.login.qr + +interface LoginQrDecoder { + + fun decode(value: String): Map? + fun focusFieldName(): String? +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/qr/LoginVulcanQrDecoder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/qr/LoginVulcanQrDecoder.kt new file mode 100644 index 00000000..73530348 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/qr/LoginVulcanQrDecoder.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-23. + */ + +package pl.szczodrzynski.edziennik.ui.login.qr + +import pl.szczodrzynski.edziennik.ext.get +import pl.szczodrzynski.edziennik.utils.Utils + +class LoginVulcanQrDecoder : LoginQrDecoder { + + private val regex = "CERT#https?://.+?/([A-z]+)/mobile-api#([A-z0-9]+)#ENDCERT".toRegex() + + override fun decode(value: String): Map? { + val data = try { + Utils.VulcanQrEncryptionUtils.decode(value) + } catch (e: Exception) { + return null + } + + val match = regex.find(data) ?: return null + return mapOf( + "deviceToken" to match[2], + "symbol" to match[1], + ) + } + + override fun focusFieldName() = "devicePin" +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaActivity.kt new file mode 100644 index 00000000..2e58da07 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaActivity.kt @@ -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, + ) + ) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaResult.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaResult.kt new file mode 100644 index 00000000..ae5fae19 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/recaptcha/RecaptchaResult.kt @@ -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?, +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/viewholder/ModeViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/viewholder/ModeViewHolder.kt similarity index 83% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/viewholder/ModeViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/login/viewholder/ModeViewHolder.kt index 6b0d20b5..c91f18b9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/viewholder/ModeViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/viewholder/ModeViewHolder.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-10. */ -package pl.szczodrzynski.edziennik.ui.modules.login.viewholder +package pl.szczodrzynski.edziennik.ui.login.viewholder import android.view.LayoutInflater import android.view.ViewGroup @@ -12,11 +12,11 @@ import androidx.recyclerview.widget.RecyclerView import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.databinding.LoginChooserModeItemBinding -import pl.szczodrzynski.edziennik.resolveColor -import pl.szczodrzynski.edziennik.setTintColor -import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder -import pl.szczodrzynski.edziennik.ui.modules.login.LoginChooserAdapter -import pl.szczodrzynski.edziennik.ui.modules.login.LoginInfo +import pl.szczodrzynski.edziennik.ext.resolveColor +import pl.szczodrzynski.edziennik.ext.setTintColor +import pl.szczodrzynski.edziennik.ui.grades.viewholder.BindableViewHolder +import pl.szczodrzynski.edziennik.ui.login.LoginChooserAdapter +import pl.szczodrzynski.edziennik.ui.login.LoginInfo class ModeViewHolder( inflater: LayoutInflater, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/viewholder/PlatformViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/viewholder/PlatformViewHolder.kt similarity index 79% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/viewholder/PlatformViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/login/viewholder/PlatformViewHolder.kt index 2c08c27e..2a558479 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/viewholder/PlatformViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/viewholder/PlatformViewHolder.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-10. */ -package pl.szczodrzynski.edziennik.ui.modules.login.viewholder +package pl.szczodrzynski.edziennik.ui.login.viewholder import android.view.LayoutInflater import android.view.ViewGroup @@ -12,9 +12,9 @@ import androidx.recyclerview.widget.RecyclerView import coil.load import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.databinding.LoginPlatformItemBinding -import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder -import pl.szczodrzynski.edziennik.ui.modules.login.LoginInfo -import pl.szczodrzynski.edziennik.ui.modules.login.LoginPlatformAdapter +import pl.szczodrzynski.edziennik.ui.grades.viewholder.BindableViewHolder +import pl.szczodrzynski.edziennik.ui.login.LoginInfo +import pl.szczodrzynski.edziennik.ui.login.LoginPlatformAdapter class PlatformViewHolder( inflater: LayoutInflater, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/viewholder/RegisterViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/viewholder/RegisterViewHolder.kt similarity index 78% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/viewholder/RegisterViewHolder.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/login/viewholder/RegisterViewHolder.kt index 921041c8..33f2b75d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/viewholder/RegisterViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/login/viewholder/RegisterViewHolder.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-10. */ -package pl.szczodrzynski.edziennik.ui.modules.login.viewholder +package pl.szczodrzynski.edziennik.ui.login.viewholder import android.view.LayoutInflater import android.view.ViewGroup @@ -11,9 +11,9 @@ import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.databinding.LoginChooserItemBinding -import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder -import pl.szczodrzynski.edziennik.ui.modules.login.LoginChooserAdapter -import pl.szczodrzynski.edziennik.ui.modules.login.LoginInfo +import pl.szczodrzynski.edziennik.ui.grades.viewholder.BindableViewHolder +import pl.szczodrzynski.edziennik.ui.login.LoginChooserAdapter +import pl.szczodrzynski.edziennik.ui.login.LoginInfo class RegisterViewHolder( inflater: LayoutInflater, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesUtils.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/MessagesUtils.kt similarity index 96% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesUtils.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/MessagesUtils.kt index 07a0769a..cc5a14d4 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesUtils.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/MessagesUtils.kt @@ -1,4 +1,4 @@ -package pl.szczodrzynski.edziennik.ui.modules.messages +package pl.szczodrzynski.edziennik.ui.messages import android.content.Context import android.graphics.Bitmap @@ -9,10 +9,9 @@ import android.text.Spanned import androidx.core.graphics.ColorUtils import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.full.MessageFull -import pl.szczodrzynski.edziennik.fixName -import pl.szczodrzynski.edziennik.getNameInitials +import pl.szczodrzynski.edziennik.ext.fixName +import pl.szczodrzynski.edziennik.ext.getNameInitials import pl.szczodrzynski.edziennik.utils.Colors import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.html.BetterHtml @@ -123,10 +122,10 @@ object MessagesUtils { fun getMessageInfo(app: App, message: MessageFull, diameterDp: Int, textSizeBigDp: Int, textSizeMediumDp: Int, textSizeSmallDp: Int): MessageInfo { var profileImage: Bitmap? = null var profileName: String? = null - if (message.type == Message.TYPE_RECEIVED || message.type == Message.TYPE_DELETED) { + if (message.isReceived || message.isDeleted) { profileName = message.senderName?.fixName() profileImage = getProfileImage(diameterDp, textSizeBigDp, textSizeMediumDp, textSizeSmallDp, 1, profileName) - } else if (message.type == Message.TYPE_SENT || message.type == Message.TYPE_DRAFT && message.recipients != null) { + } else if (message.isSent || message.isDraft && message.recipients != null) { when (val count = message.recipients?.size ?: 0) { 0 -> { profileName = app.getString(R.string.messages_draft_title) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/compose/MessagesComposeChipCreator.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/compose/MessagesComposeChipCreator.kt new file mode 100644 index 00000000..f2ea8e94 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/compose/MessagesComposeChipCreator.kt @@ -0,0 +1,144 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-4. + */ + +package pl.szczodrzynski.edziennik.ui.messages.compose + +import android.content.Context +import android.graphics.drawable.BitmapDrawable +import android.text.style.AbsoluteSizeSpan +import android.text.style.ForegroundColorSpan +import android.widget.Toast +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.hootsuite.nachos.ChipConfiguration +import com.hootsuite.nachos.NachoTextView +import com.hootsuite.nachos.chip.ChipInfo +import com.hootsuite.nachos.chip.ChipSpan +import com.hootsuite.nachos.chip.ChipSpanChipCreator +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.ui.messages.MessagesUtils +import pl.szczodrzynski.edziennik.utils.Colors +import pl.szczodrzynski.edziennik.utils.Themes +import pl.szczodrzynski.navlib.elevateSurface + +class MessagesComposeChipCreator( + private val context: Context, + private val nacho: NachoTextView, + private val teacherList: List, +) : ChipSpanChipCreator() { + + override fun createChip(context: Context, text: CharSequence, data: Any?): ChipSpan? { + if (data == null || data !is Teacher) + return null + if (data.id !in -24L..0L) { + nacho.allChips.forEach { + if (it.data == data) { + Toast.makeText( + context, + R.string.messages_compose_recipient_exists, + Toast.LENGTH_SHORT + ).show() + return null + } + } + val chipSpan = ChipSpan( + context, + data.fullName, + BitmapDrawable(context.resources, data.image), + data + ) + chipSpan.setIconBackgroundColor(Colors.stringToMaterialColor(data.fullName)) + return chipSpan + } + + val type = (data.id * -1).toInt() + + val textColorPrimary = android.R.attr.textColorPrimary.resolveAttr(context) + val textColorSecondary = android.R.attr.textColorSecondary.resolveAttr(context) + + val sortByCategory = type in listOf( + Teacher.TYPE_PARENTS_COUNCIL, + Teacher.TYPE_EDUCATOR, + Teacher.TYPE_STUDENT + ) + + val adapter = nacho.adapter as? MessagesComposeSuggestionAdapter ?: return null + val teachers = if (sortByCategory) + adapter.originalList.sortedBy { it.typeDescription } + else + adapter.originalList + + val category = mutableListOf() + val categoryNames = mutableListOf() + val categoryCheckedItems = mutableListOf() + teachers.forEach { teacher -> + if (!teacher.isType(type)) + return@forEach + + category += teacher + val name = teacher.fullName + val description = when (type) { + Teacher.TYPE_TEACHER -> null + Teacher.TYPE_PARENTS_COUNCIL -> teacher.typeDescription + Teacher.TYPE_SCHOOL_PARENTS_COUNCIL -> null + Teacher.TYPE_PEDAGOGUE -> null + Teacher.TYPE_LIBRARIAN -> null + Teacher.TYPE_SCHOOL_ADMIN -> null + Teacher.TYPE_SUPER_ADMIN -> null + Teacher.TYPE_SECRETARIAT -> null + Teacher.TYPE_PRINCIPAL -> null + Teacher.TYPE_EDUCATOR -> teacher.typeDescription + Teacher.TYPE_PARENT -> teacher.typeDescription + Teacher.TYPE_STUDENT -> teacher.typeDescription + Teacher.TYPE_SPECIALIST -> null + else -> teacher.typeDescription + } + categoryNames += listOfNotNull( + name.asSpannable( + ForegroundColorSpan(textColorPrimary) + ), + description?.asSpannable( + ForegroundColorSpan(textColorSecondary), + AbsoluteSizeSpan(14.dp) + ) + ).concat("\n") + + // check the teacher if already added as a recipient + categoryCheckedItems += nacho.allChips.firstOrNull { it.data == teacher } != null + } + + MaterialAlertDialogBuilder(context) + .setTitle("Dodaj odbiorców - " + Teacher.typeName(context, type)) + //.setMessage(getString(R.string.messages_compose_recipients_text_format, Teacher.typeName(activity, type))) + .setPositiveButton("OK", null) + .setNeutralButton("Anuluj", null) + .setMultiChoiceItems( + categoryNames.toTypedArray(), + categoryCheckedItems.toBooleanArray() + ) { _, which, isChecked -> + val teacher = category[which] + if (isChecked) { + val chipInfoList = mutableListOf() + teacher.image = + MessagesUtils.getProfileImage(48, 24, 16, 12, 1, teacher.fullName) + chipInfoList.add(ChipInfo(teacher.fullName, teacher)) + nacho.addTextWithChips(chipInfoList) + } else { + nacho.allChips.forEach { + if (it.data == teacher) + nacho.chipTokenizer?.deleteChipAndPadding(it, nacho.text) + } + } + } + .show() + return null + } + + override fun configureChip(chip: ChipSpan, chipConfiguration: ChipConfiguration) { + super.configureChip(chip, chipConfiguration) + chip.setBackgroundColor(elevateSurface(context, 8).toColorStateList()) + chip.setTextColor(Themes.getPrimaryTextColor(context)) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/compose/MessagesComposeChipTokenizer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/compose/MessagesComposeChipTokenizer.kt new file mode 100644 index 00000000..6dfc22f8 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/compose/MessagesComposeChipTokenizer.kt @@ -0,0 +1,25 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-4. + */ + +package pl.szczodrzynski.edziennik.ui.messages.compose + +import android.content.Context +import com.hootsuite.nachos.NachoTextView +import com.hootsuite.nachos.chip.ChipSpan +import com.hootsuite.nachos.tokenizer.SpanChipTokenizer +import pl.szczodrzynski.edziennik.data.db.entity.Teacher + +class MessagesComposeChipTokenizer( + context: Context, + nacho: NachoTextView, + teacherList: List, +) : SpanChipTokenizer( + context, + MessagesComposeChipCreator( + context = context, + nacho = nacho, + teacherList = teacherList + ), + ChipSpan::class.java +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/compose/MessagesComposeFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/compose/MessagesComposeFragment.kt new file mode 100644 index 00000000..426c337f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/compose/MessagesComposeFragment.kt @@ -0,0 +1,492 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-4. + */ + +package pl.szczodrzynski.edziennik.ui.messages.compose + +import android.annotation.SuppressLint +import android.os.Bundle +import android.text.* +import android.text.Spanned.* +import android.text.style.* +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.AutoCompleteTextView +import android.widget.ScrollView +import android.widget.Toast +import androidx.core.view.isVisible +import androidx.core.widget.addTextChangedListener +import androidx.fragment.app.Fragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import kotlinx.coroutines.* +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.ERROR_MESSAGE_NOT_SENT +import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask +import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent +import pl.szczodrzynski.edziennik.data.api.events.RecipientListGetEvent +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.db.entity.Message +import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.data.db.enums.LoginType +import pl.szczodrzynski.edziennik.databinding.MessagesComposeFragmentBinding +import pl.szczodrzynski.edziennik.ext.Bundle +import pl.szczodrzynski.edziennik.ext.DAY +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget +import pl.szczodrzynski.edziennik.ui.dialogs.settings.MessagesConfigDialog +import pl.szczodrzynski.edziennik.ui.messages.list.MessagesFragment +import pl.szczodrzynski.edziennik.utils.DefaultTextStyles +import pl.szczodrzynski.edziennik.utils.Themes +import pl.szczodrzynski.edziennik.utils.managers.MessageManager.UIConfig +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.HtmlMode.COMPATIBLE +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.HtmlMode.ORIGINAL +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.StylingConfig +import pl.szczodrzynski.edziennik.utils.span.* +import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem +import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem +import kotlin.coroutines.CoroutineContext + +class MessagesComposeFragment : Fragment(), CoroutineScope { + companion object { + private const val TAG = "MessagesComposeFragment" + } + + private lateinit var app: App + private lateinit var activity: MainActivity + private lateinit var b: MessagesComposeFragmentBinding + + private val job: Job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + private val manager + get() = app.messageManager + private val textStylingManager + get() = app.textStylingManager + private val greetingText + get() = app.profile.config.ui.messagesGreetingText ?: "\n\nZ poważaniem\n${app.profile.accountOwnerName}" + + private val teachers = mutableListOf() + + private lateinit var stylingConfig: StylingConfig + private lateinit var uiConfig: UIConfig + private var changedRecipients = false + private var changedSubject = false + private var changedBody = false + private var discardDraftItem: BottomSheetPrimaryItem? = null + private var draftMessageId: Long? = 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 + requireContext().theme.applyStyle(Themes.appTheme, true) + // activity, context and profile is valid + b = MessagesComposeFragmentBinding.inflate(inflater) + return b.root + } + override fun onDestroy() { + EventBus.getDefault().unregister(this) + super.onDestroy() + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + // TODO check if app, activity, b can be null + if (!isAdded) + return + + EventBus.getDefault().register(this) + + b.breakpoints.visibility = if (App.devMode) View.VISIBLE else View.GONE + b.breakpoints.setOnClickListener { + b.breakpoints.isEnabled = true + @SuppressLint("SetTextI18n") + b.breakpoints.text = "Breakpoints!" + // do your job + } + + discardDraftItem = BottomSheetPrimaryItem(true) + .withTitle(R.string.messages_compose_discard_draft) + .withIcon(CommunityMaterial.Icon3.cmd_text_box_remove_outline) + .withOnClickListener { + activity.bottomSheet.close() + discardDraftDialog() + } + + activity.bottomSheet.prependItems( + BottomSheetPrimaryItem(true) + .withTitle(R.string.messages_compose_send_long) + .withIcon(CommunityMaterial.Icon3.cmd_send_outline) + .withOnClickListener { + activity.bottomSheet.close() + sendMessage() + }, + BottomSheetPrimaryItem(true) + .withTitle(R.string.messages_compose_save_draft) + .withIcon(CommunityMaterial.Icon.cmd_content_save_edit_outline) + .withOnClickListener { + activity.bottomSheet.close() + saveDraft() + }, + BottomSheetSeparatorItem(true), + BottomSheetPrimaryItem(true) + .withTitle(R.string.menu_messages_config) + .withIcon(CommunityMaterial.Icon.cmd_cog_outline) + .withOnClickListener { + activity.bottomSheet.close() + MessagesConfigDialog(activity, false, null, null).show() + } + ) + + launch { + delay(100) + getRecipientList() + + createView() + } + } + + private fun getMessageBody(): String { + return if (app.data.messagesConfig.textStyling) + textStylingManager.getHtmlText(stylingConfig) + else + b.text.text?.toString() ?: "" + } + + private fun getRecipientList() { + if (app.data.messagesConfig.syncRecipientList && System.currentTimeMillis() - app.profile.lastReceiversSync > 1 * DAY * 1000) { + activity.snackbar("Pobieranie listy odbiorców...") + EdziennikTask.recipientListGet(App.profileId).enqueue(activity) + } + else { + launch { + val list = withContext(Dispatchers.Default) { + app.db.teacherDao().getAllNow(App.profileId).filter { it.loginId != null } + } + updateRecipientList(list) + } + } + } + + private fun createView() { + b.recipientsLayout.setBoxCornerRadii(0f, 0f, 0f, 0f) + b.subjectLayout.setBoxCornerRadii(0f, 0f, 0f, 0f) + b.textLayout.setBoxCornerRadii(0f, 0f, 0f, 0f) + + b.recipients.addTextChangedListener(onTextChanged = { _, _, _, _ -> + b.recipientsLayout.error = null + changedRecipients = true + }) + b.subject.addTextChangedListener(onTextChanged = { _, _, _, _ -> + b.subjectLayout.error = null + changedSubject = true + }) + b.text.addTextChangedListener(onTextChanged = { _, _, _, _ -> + b.textLayout.error = null + changedBody = true + }) + + b.subjectLayout.counterMaxLength = when (app.profile.loginStoreType) { + LoginType.MOBIDZIENNIK -> 100 + LoginType.LIBRUS -> 150 + LoginType.VULCAN -> 200 + LoginType.IDZIENNIK -> 180 + LoginType.EDUDZIENNIK -> 0 + else -> -1 + } + b.textLayout.counterMaxLength = when (app.profile.loginStoreType) { + LoginType.MOBIDZIENNIK -> -1 + LoginType.LIBRUS -> 20000 + LoginType.VULCAN -> -1 + LoginType.IDZIENNIK -> 1983 + LoginType.EDUDZIENNIK -> 0 + else -> -1 + } + + b.recipients.chipTokenizer = MessagesComposeChipTokenizer(activity, b.recipients, teachers) + b.recipients.setIllegalCharacterIdentifier { c -> + c.toString().matches("[\\n;:_ ]".toRegex()) + } + b.recipients.setOnChipRemoveListener { + b.recipients.setSelection(b.recipients.text.length) + } + + b.recipients.addTextChangedListener( beforeTextChanged = { _, _, _, _ -> + b.recipients.ignoreThreshold = false + }) + b.recipients.onDismissListener = AutoCompleteTextView.OnDismissListener { + b.recipients.ignoreThreshold = false + } + b.recipientsLayout.setEndIconOnClickListener { + b.recipients.error = null + b.recipients.ignoreThreshold = true + b.recipients.showDropDown() + val adapter = b.recipients.adapter ?: return@setEndIconOnClickListener + if (adapter is MessagesComposeSuggestionAdapter) + adapter.filter.filter(null) + } + + b.recipientsLayout.isEnabled = false + b.subjectLayout.isEnabled = false + b.textLayout.isEnabled = false + + val styles = DefaultTextStyles.getAsList(b.fontStyle) + + uiConfig = UIConfig( + context = activity, + recipients = b.recipients, + subject = b.subject, + body = b.text, + teachers = teachers, + greetingOnCompose = app.profile.config.ui.messagesGreetingOnCompose, + greetingOnReply = app.profile.config.ui.messagesGreetingOnReply, + greetingOnForward = app.profile.config.ui.messagesGreetingOnForward, + greetingText = greetingText, + ) + stylingConfig = StylingConfig( + editText = b.text, + fontStyleGroup = b.fontStyle.styles, + fontStyleClear = b.fontStyle.clear, + styles = styles, + textHtml = if (App.devMode) b.textHtml else null, + htmlMode = when (app.profile.loginStoreType) { + LoginType.MOBIDZIENNIK -> COMPATIBLE + else -> ORIGINAL + }, + ) + + b.fontStyle.root.isVisible = app.data.messagesConfig.textStyling + if (app.data.messagesConfig.textStyling) { + textStylingManager.attach(stylingConfig) + b.fontStyle.styles.addOnButtonCheckedListener { _, _, _ -> + changedBody = true + } + } + + activity.navView.bottomBar.apply { + fabEnable = true + fabExtendedText = getString(R.string.messages_compose_send) + fabIcon = CommunityMaterial.Icon3.cmd_send_outline + + setFabOnClickListener { + sendMessage() + } + } + + activity.gainAttentionFAB() + } + + private fun onBeforeNavigate(): Boolean { + val messageText = b.text.text?.toString()?.trim() ?: "" + val greetingText = this.greetingText.trim() + // navigateUp if nothing changed + if ((!changedRecipients || b.recipients.allChips.isEmpty()) + && (!changedSubject || b.subject.text.isNullOrBlank()) + && (!changedBody || messageText.isEmpty() || messageText == greetingText) + ) + return true + saveDraftDialog() + return false + } + + private fun saveDraftDialog() { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.messages_compose_save_draft_title) + .setMessage(R.string.messages_compose_save_draft_text) + .setPositiveButton(R.string.save) { _, _ -> + saveDraft() + MessagesFragment.pageSelection = Message.TYPE_DRAFT + activity.navigate(navTarget = NavTarget.MESSAGES, skipBeforeNavigate = true) + } + .setNegativeButton(R.string.discard) { _, _ -> + activity.resumePausedNavigation() + } + .show() + } + + private fun saveDraft() { + launch { + manager.saveAsDraft(uiConfig, stylingConfig, App.profileId, draftMessageId) + Toast.makeText(activity, R.string.messages_compose_draft_saved, Toast.LENGTH_SHORT).show() + changedRecipients = false + changedSubject = false + changedBody = false + } + if (discardDraftItem != null) + activity.bottomSheet.addItemAt(2, discardDraftItem!!) + discardDraftItem = null + } + + private fun discardDraftDialog() { + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.messages_compose_discard_draft_title) + .setMessage(R.string.messages_compose_discard_draft_text) + .setPositiveButton(R.string.remove) { _, _ -> + launch { + if (draftMessageId != null) + manager.deleteDraft(App.profileId, draftMessageId!!) + Toast.makeText(activity, R.string.messages_compose_draft_discarded, Toast.LENGTH_SHORT).show() + activity.navigateUp(skipBeforeNavigate = true) + } + } + .setNegativeButton(R.string.cancel, null) + .show() + } + + @SuppressLint("SetTextI18n") + private fun updateRecipientList(list: List) { launch { + withContext(Dispatchers.Default) { + teachers.clear() + teachers.addAll(list.sortedBy { it.fullName }) + Teacher.types.mapTo(teachers) { + Teacher(-1, -it.toLong(), Teacher.typeName(activity, it), "") + } + /*teachers.forEach { + println(it) + }*/ + } + + b.recipientsLayout.isEnabled = true + b.subjectLayout.isEnabled = true + b.textLayout.isEnabled = true + + val adapter = MessagesComposeSuggestionAdapter(activity, teachers) + b.recipients.setAdapter(adapter) + + val message = manager.fillWithBundle(uiConfig, arguments) + if (message != null && message.isDraft) { + draftMessageId = message.id + if (discardDraftItem != null) + activity.bottomSheet.addItemAt(2, discardDraftItem!!) + discardDraftItem = null + } + + when { + b.recipients.text.isBlank() -> b.recipients.requestFocus() + b.subject.text.isNullOrBlank() -> b.subject.requestFocus() + else -> b.text.requestFocus() + } + + if (!app.data.messagesConfig.textStyling) + b.text.setText(b.text.text?.toString()) + b.text.setSelection(0) + (b.root as? ScrollView)?.smoothScrollTo(0, 0) + + changedRecipients = false + changedSubject = false + changedBody = false + }} + + private fun sendMessage() { + b.recipientsLayout.error = null + b.subjectLayout.error = null + b.textLayout.error = null + + if (b.recipients.tokenValues.isNotEmpty()) { + b.recipientsLayout.error = getString(R.string.messages_compose_recipients_error) + return + } + val recipients = mutableSetOf() + b.recipients.allChips.forEach { chip -> + if (chip.data !is Teacher) + return@forEach + val teacher = chip.data as Teacher + + recipients += teacher + //println(teacher) + } + val subject = b.subject.text?.toString() + val text = b.text.text + if (recipients.isEmpty()) { + b.recipientsLayout.error = getString(R.string.messages_compose_recipients_empty) + return + } + if (subject.isNullOrBlank() || subject.length < 3) { + b.subjectLayout.error = getString(R.string.messages_compose_subject_empty) + return + } + if (text.isNullOrBlank() || text.length < 3) { + b.textLayout.error = getString(R.string.messages_compose_text_empty) + return + } + + // do magic + // apparently this removes an underline + // span from the text where the caret is + b.subject.requestFocus() + b.subject.clearFocus() + activity.navView.bottomSheet.hideKeyboard() + b.text.clearFocus() + b.text.setSelection(0) + + if (b.subjectLayout.counterMaxLength != -1 && b.subject.length() > b.subjectLayout.counterMaxLength) + return + if (b.textLayout.counterMaxLength != -1 && b.text.length() > b.textLayout.counterMaxLength) + return + + val body = getMessageBody() + + activity.bottomSheet.hideKeyboard() + + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.messages_compose_confirm_title) + .setMessage(R.string.messages_compose_confirm_text) + .setPositiveButton(R.string.send) { _, _ -> + EdziennikTask.messageSend(App.profileId, recipients, subject.trim(), body).enqueue(activity) + } + .setNegativeButton(R.string.cancel, null) + .show() + } + + override fun onResume() { + super.onResume() + if (!isAdded || !this::activity.isInitialized) + return + activity.onBeforeNavigate = this::onBeforeNavigate + } + + override fun onPause() { + super.onPause() + if (!this::activity.isInitialized) + return + activity.onBeforeNavigate = null + } + + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) + fun onRecipientListGetEvent(event: RecipientListGetEvent) { + if (event.profileId != App.profileId) + return + EventBus.getDefault().removeStickyEvent(event) + + activity.snackbarDismiss() + updateRecipientList(event.teacherList) + } + + @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) + fun onMessageSentEvent(event: MessageSentEvent) { + if (event.profileId != App.profileId) + return + EventBus.getDefault().removeStickyEvent(event) + + if (event.message == null) { + activity.error(ApiError(TAG, ERROR_MESSAGE_NOT_SENT)) + return + } + + if (draftMessageId != null) { + launch { + manager.deleteDraft(App.profileId, draftMessageId!!) + } + } + + activity.snackbar(app.getString(R.string.messages_sent_success), app.getString(R.string.ok)) + activity.navigate(navTarget = NavTarget.MESSAGE, args = Bundle( + "messageId" to event.message.id, + "message" to app.gson.toJson(event.message), + "sentDate" to event.sentDate + ), skipBeforeNavigate = true) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeInfo.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/compose/MessagesComposeInfo.kt similarity index 89% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeInfo.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/compose/MessagesComposeInfo.kt index 30b89472..71a0e14d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeInfo.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/compose/MessagesComposeInfo.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-4. */ -package pl.szczodrzynski.edziennik.ui.modules.messages.compose +package pl.szczodrzynski.edziennik.ui.messages.compose class MessagesComposeInfo( /** diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeSuggestionAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/compose/MessagesComposeSuggestionAdapter.kt similarity index 93% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeSuggestionAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/compose/MessagesComposeSuggestionAdapter.kt index 83a5ba39..db5072bc 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeSuggestionAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/compose/MessagesComposeSuggestionAdapter.kt @@ -1,4 +1,4 @@ -package pl.szczodrzynski.edziennik.ui.modules.messages.compose +package pl.szczodrzynski.edziennik.ui.messages.compose import android.content.Context import android.graphics.Typeface.BOLD @@ -12,11 +12,11 @@ import android.widget.Filter import android.widget.ImageView import android.widget.TextView import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.asSpannable -import pl.szczodrzynski.edziennik.cleanDiacritics import pl.szczodrzynski.edziennik.data.db.entity.Teacher -import pl.szczodrzynski.edziennik.resolveAttr -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage +import pl.szczodrzynski.edziennik.ext.asSpannable +import pl.szczodrzynski.edziennik.ext.cleanDiacritics +import pl.szczodrzynski.edziennik.ext.resolveAttr +import pl.szczodrzynski.edziennik.ui.messages.MessagesUtils.getProfileImage import java.util.* class MessagesComposeSuggestionAdapter( @@ -30,7 +30,7 @@ class MessagesComposeSuggestionAdapter( private val comparator by lazy { Comparator { o1: Teacher, o2: Teacher -> o1.recipientWeight - o2.recipientWeight } } override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val listItem = convertView ?: LayoutInflater.from(context).inflate(R.layout.messages_compose_suggestion_item, parent, false) + val listItem = convertView ?: LayoutInflater.from(context).inflate(R.layout.teacher_item, parent, false) val teacher = teacherList[position] val name = listItem.findViewById(R.id.name) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/list/MessageViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/list/MessageViewHolder.kt new file mode 100644 index 00000000..9e7cf3c4 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/list/MessageViewHolder.kt @@ -0,0 +1,90 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-5. + */ + +package pl.szczodrzynski.edziennik.ui.messages.list + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.full.MessageFull +import pl.szczodrzynski.edziennik.databinding.MessagesListItemBinding +import pl.szczodrzynski.edziennik.ext.attachToastHint +import pl.szczodrzynski.edziennik.ext.detachToastHint +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ext.resolveAttr +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 + +class MessageViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + val b: MessagesListItemBinding = MessagesListItemBinding.inflate(inflater, parent, false), +) : RecyclerView.ViewHolder(b.root), BindableViewHolder { + companion object { + private const val TAG = "MessageViewHolder" + } + + override fun onBind( + activity: AppCompatActivity, + app: App, + item: MessageFull, + position: Int, + adapter: MessagesAdapter, + ) { + b.messageDate.text = Date.fromMillis(item.addedDate).formattedStringShort + b.messageAttachmentImage.isVisible = item.hasAttachments + + 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 + // set text styles + b.messageSender.setTextAppearance(activity, style) + b.messageSender.typeface = typeface + b.messageSubject.setTextAppearance(activity, style) + b.messageSubject.typeface = typeface + b.messageDate.setTextAppearance(activity, style) + b.messageDate.typeface = typeface + + if (adapter.onStarClick == null) { + b.messageStar.isVisible = false + } + b.messageStar.detachToastHint() + + val messageInfo = MessagesUtils.getMessageInfo(app, item, 48, 24, 18, 12) + b.messageProfileBackground.setImageBitmap(messageInfo.profileImage) + + val colorHighlight = R.attr.colorControlHighlight.resolveAttr(activity) + b.messageSubject.text = adapter.highlightSearchText( + item = item, + text = item.subject, + color = colorHighlight + ) + b.messageSender.text = adapter.highlightSearchText( + item = item, + text = messageInfo.profileName ?: "", + color = colorHighlight + ) + + if (adapter.showNotes) + NoteManager.prependIcon(item, b.messageSubject) + + adapter.onMessageClick?.let { listener -> + b.root.onClick { listener(item) } + } + adapter.onStarClick?.let { listener -> + b.messageStar.isVisible = true + adapter.manager.setStarIcon(b.messageStar, item) + b.messageStar.onClick { listener(item) } + b.messageStar.attachToastHint(R.string.hint_message_star) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/list/MessagesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/list/MessagesAdapter.kt new file mode 100644 index 00000000..3273340d --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/list/MessagesAdapter.kt @@ -0,0 +1,51 @@ +package pl.szczodrzynski.edziennik.ui.messages.list + +import android.graphics.Typeface +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.RecyclerView +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.data.db.full.MessageFull +import pl.szczodrzynski.edziennik.ui.search.SearchableAdapter + +class MessagesAdapter( + val activity: AppCompatActivity, + val teachers: List, + val showNotes: Boolean = true, + val onMessageClick: ((item: MessageFull) -> Unit)? = null, + val onStarClick: ((item: MessageFull) -> Unit)? = null, +) : SearchableAdapter() { + companion object { + private const val TAG = "MessagesAdapter" + private const val ITEM_TYPE_MESSAGE = 0 + } + + private val app = activity.applicationContext as App + + // optional: place the manager here + internal val manager + get() = app.messageManager + + val typefaceNormal: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) } + val typefaceBold: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.BOLD) } + + override fun getItemViewType(item: MessageFull) = ITEM_TYPE_MESSAGE + + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + item: MessageFull, + ) { + if (holder !is MessageViewHolder) + return + holder.onBind(activity, app, item, position, this) + } + + override fun onCreateViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int, + ) = MessageViewHolder(inflater, parent) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/list/MessagesFragment.kt similarity index 50% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/list/MessagesFragment.kt index add36a3c..823a3289 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/list/MessagesFragment.kt @@ -1,4 +1,4 @@ -package pl.szczodrzynski.edziennik.ui.modules.messages +package pl.szczodrzynski.edziennik.ui.messages.list import android.os.Bundle import android.view.LayoutInflater @@ -12,7 +12,12 @@ import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.databinding.MessagesFragmentBinding -import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.FragmentLazyPagerAdapter +import pl.szczodrzynski.edziennik.ext.Bundle +import pl.szczodrzynski.edziennik.ext.addOnPageSelectedListener +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget +import pl.szczodrzynski.edziennik.ui.base.lazypager.FragmentLazyPagerAdapter +import pl.szczodrzynski.edziennik.ui.dialogs.settings.MessagesConfigDialog +import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import kotlin.coroutines.CoroutineContext class MessagesFragment : Fragment(), CoroutineScope { @@ -48,40 +53,48 @@ class MessagesFragment : Fragment(), CoroutineScope { val args = Bundle() args.putLong("messageId", messageId) arguments?.remove("messageId") - activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, args) + activity.navigate(navTarget = NavTarget.MESSAGE, args = args) return } val args = arguments val pagerAdapter = FragmentLazyPagerAdapter( - fragmentManager ?: return, - b.refreshLayout, - listOf( - MessagesListFragment().apply { - onPageDestroy = this@MessagesFragment.onPageDestroy - arguments = Bundle("messageType" to Message.TYPE_RECEIVED) - args?.getBundle("page0")?.let { - arguments?.putAll(it) - } - } to getString(R.string.messages_tab_received), + fragmentManager = parentFragmentManager, + swipeRefreshLayout = b.refreshLayout, + fragments = listOf( + MessagesListFragment().apply { + onPageDestroy = this@MessagesFragment.onPageDestroy + arguments = Bundle("messageType" to Message.TYPE_RECEIVED) + args?.getBundle("page0")?.let { + arguments?.putAll(it) + } + } to getString(R.string.messages_tab_received), - MessagesListFragment().apply { - onPageDestroy = this@MessagesFragment.onPageDestroy - arguments = Bundle("messageType" to Message.TYPE_SENT) - args?.getBundle("page1")?.let { - arguments?.putAll(it) - } - } to getString(R.string.messages_tab_sent), + MessagesListFragment().apply { + onPageDestroy = this@MessagesFragment.onPageDestroy + arguments = Bundle("messageType" to Message.TYPE_SENT) + args?.getBundle("page1")?.let { + arguments?.putAll(it) + } + } to getString(R.string.messages_tab_sent), - MessagesListFragment().apply { - onPageDestroy = this@MessagesFragment.onPageDestroy - arguments = Bundle("messageType" to Message.TYPE_DELETED) - args?.getBundle("page2")?.let { - arguments?.putAll(it) - } - } to getString(R.string.messages_tab_deleted) - ) + MessagesListFragment().apply { + onPageDestroy = this@MessagesFragment.onPageDestroy + arguments = Bundle("messageType" to Message.TYPE_DELETED) + args?.getBundle("page2")?.let { + arguments?.putAll(it) + } + } to getString(R.string.messages_tab_deleted), + + MessagesListFragment().apply { + onPageDestroy = this@MessagesFragment.onPageDestroy + arguments = Bundle("messageType" to Message.TYPE_DRAFT) + args?.getBundle("page3")?.let { + arguments?.putAll(it) + } + } to getString(R.string.messages_tab_draft), + ), ) b.viewPager.apply { offscreenPageLimit = 1 @@ -100,9 +113,19 @@ class MessagesFragment : Fragment(), CoroutineScope { fabIcon = CommunityMaterial.Icon3.cmd_pencil_outline } - setFabOnClickListener(View.OnClickListener { - activity.loadTarget(MainActivity.TARGET_MESSAGES_COMPOSE) - }) + bottomSheet.prependItem( + BottomSheetPrimaryItem(true) + .withTitle(R.string.menu_messages_config) + .withIcon(CommunityMaterial.Icon.cmd_cog_outline) + .withOnClickListener { + activity.bottomSheet.close() + MessagesConfigDialog(activity, false, null, null).show() + } + ) + + setFabOnClickListener { + activity.navigate(navTarget = NavTarget.MESSAGE_COMPOSE) + } } activity.gainAttentionFAB() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/list/MessagesListFragment.kt similarity index 51% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/list/MessagesListFragment.kt index 2e4a30b8..fc19abfa 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/list/MessagesListFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-4. */ -package pl.szczodrzynski.edziennik.ui.modules.messages +package pl.szczodrzynski.edziennik.ui.messages.list import android.os.Bundle import android.view.LayoutInflater @@ -11,17 +11,16 @@ import android.view.ViewGroup import androidx.core.view.isVisible import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView.NO_POSITION -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.databinding.MessagesListFragmentBinding -import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment -import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch +import pl.szczodrzynski.edziennik.ext.Bundle +import pl.szczodrzynski.edziennik.ext.getInt +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget +import pl.szczodrzynski.edziennik.ui.base.lazypager.LazyFragment import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import kotlin.coroutines.CoroutineContext @@ -33,12 +32,15 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { private lateinit var app: App private lateinit var activity: MainActivity private lateinit var b: MessagesListFragmentBinding + private lateinit var adapter: MessagesAdapter private val job: Job = Job() override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main // local/private variables go here + private val manager + get() = app.messageManager var teachers = listOf() override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -51,23 +53,36 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { override fun onPageCreated(): Boolean { startCoroutineTimer(100L) { val messageType = arguments.getInt("messageType", Message.TYPE_RECEIVED) - var topPosition = arguments.getInt("topPosition", NO_POSITION) - var bottomPosition = arguments.getInt("bottomPosition", NO_POSITION) + var recyclerViewState = + arguments?.getParcelable("recyclerViewState") + val searchText = arguments?.getString("searchText") teachers = withContext(Dispatchers.Default) { app.db.teacherDao().getAllNow(App.profileId) } - val adapter = MessagesAdapter(activity, teachers) { - activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, Bundle( - "messageId" to it.id - )) - } + adapter = MessagesAdapter(activity, teachers, onMessageClick = { + val (target, args) = + if (it.isDraft) { + NavTarget.MESSAGE_COMPOSE to Bundle("message" to app.gson.toJson(it)) + } else { + NavTarget.MESSAGE to Bundle("messageId" to it.id) + } + activity.navigate(navTarget = target, args = args) + }, onStarClick = { + this@MessagesListFragment.launch { + manager.starMessage(it, !it.isStarred) + } + }) - app.db.messageDao().getAllByType(App.profileId, messageType).observe(this@MessagesListFragment, Observer { items -> - if (!isAdded) return@Observer + app.db.messageDao().getAllByType(App.profileId, messageType).observe(this@MessagesListFragment, Observer { messages -> + if (!isAdded || !this@MessagesListFragment::adapter.isInitialized) + return@Observer - items.forEach { message -> + messages.forEach { message -> + message.filterNotes() + + // uh oh, so these are the workarounds ?? message.recipients?.removeAll { it.profileId != message.profileId } message.recipients?.forEach { recipient -> if (recipient.fullName == null) { @@ -76,53 +91,53 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { } } - // load & configure the adapter - adapter.items = items.toMutableList() - adapter.items.add(0, MessagesSearch().also { - it.count = items.size - }) - adapter.allItems = adapter.items.toMutableList() - if (items.isNotNullNorEmpty() && b.list.adapter == null) { - b.list.adapter = adapter + // show/hide relevant views + setSwipeToRefresh(messageType in Message.TYPE_RECEIVED..Message.TYPE_SENT) + b.progressBar.isVisible = false + b.list.isVisible = messages.isNotEmpty() + b.noData.isVisible = messages.isEmpty() + if (messages.isEmpty()) { + return@Observer + } + + // apply the new message list + adapter.setAllItems(messages, searchText, addSearchField = true) + + // configure the adapter & recycler view + if (b.list.adapter == null) { b.list.apply { setHasFixedSize(true) layoutManager = LinearLayoutManager(context) addItemDecoration(SimpleDividerItemDecoration(context)) if (messageType in Message.TYPE_RECEIVED..Message.TYPE_SENT) addOnScrollListener(onScrollListener) + this.adapter = this@MessagesListFragment.adapter } } - adapter.notifyDataSetChanged() - setSwipeToRefresh(messageType in Message.TYPE_RECEIVED..Message.TYPE_SENT && items.isNullOrEmpty()) - (b.list.layoutManager as? LinearLayoutManager)?.let { layoutManager -> - if (topPosition != NO_POSITION && topPosition > layoutManager.findLastCompletelyVisibleItemPosition()) { - b.list.scrollToPosition(topPosition) - } else if (bottomPosition != NO_POSITION && bottomPosition < layoutManager.findFirstVisibleItemPosition()) { - b.list.scrollToPosition(bottomPosition) + val layoutManager = (b.list.layoutManager as? LinearLayoutManager) ?: return@Observer + + // reapply the filter + adapter.getSearchField()?.applyTo(adapter) { + // restore the previously saved scroll position + recyclerViewState?.let { + layoutManager.onRestoreInstanceState(it) } - topPosition = NO_POSITION - bottomPosition = NO_POSITION - } - - // show/hide relevant views - b.progressBar.isVisible = false - if (items.isNullOrEmpty()) { - b.list.isVisible = false - b.noData.isVisible = true - } else { - b.list.isVisible = true - b.noData.isVisible = false + recyclerViewState = null } }) }; return true } override fun onDestroy() { super.onDestroy() - if (!isAdded) return + if (!isAdded || !this::adapter.isInitialized) + return + val layoutManager = (b.list.layoutManager as? LinearLayoutManager) + val searchField = adapter.getSearchField() + onPageDestroy?.invoke(position, Bundle( - "topPosition" to (b.list.layoutManager as? LinearLayoutManager)?.findFirstVisibleItemPosition(), - "bottomPosition" to (b.list.layoutManager as? LinearLayoutManager)?.findLastCompletelyVisibleItemPosition() + "recyclerViewState" to layoutManager?.onSaveInstanceState(), + "searchText" to searchField?.searchText?.toString() )) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/single/MessageFragment.kt similarity index 69% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/single/MessageFragment.kt index 30f40c30..a278660d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessageFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/single/MessageFragment.kt @@ -2,10 +2,9 @@ * Copyright (c) Kuba Szczodrzyński 2019-11-12. */ -package pl.szczodrzynski.edziennik.ui.modules.messages +package pl.szczodrzynski.edziennik.ui.messages.single import android.os.Bundle -import android.text.Html import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -23,17 +22,21 @@ import org.greenrobot.eventbus.ThreadMode import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.events.MessageGetEvent -import pl.szczodrzynski.edziennik.data.db.entity.LoginStore -import pl.szczodrzynski.edziennik.data.db.entity.LoginStore.Companion.LOGIN_TYPE_IDZIENNIK -import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_DELETED -import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_RECEIVED -import pl.szczodrzynski.edziennik.data.db.entity.Message.Companion.TYPE_SENT +import pl.szczodrzynski.edziennik.data.db.enums.LoginType import pl.szczodrzynski.edziennik.data.db.full.MessageFull import pl.szczodrzynski.edziennik.databinding.MessageFragmentBinding +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget +import pl.szczodrzynski.edziennik.ui.dialogs.settings.MessagesConfigDialog +import pl.szczodrzynski.edziennik.ui.messages.MessagesUtils +import pl.szczodrzynski.edziennik.ui.messages.list.MessagesFragment +import pl.szczodrzynski.edziennik.ui.notes.setupNotesButton import pl.szczodrzynski.edziennik.utils.Anim import pl.szczodrzynski.edziennik.utils.BetterLink +import pl.szczodrzynski.edziennik.utils.html.BetterHtml import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time +import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.colorAttr import kotlin.coroutines.CoroutineContext import kotlin.math.min @@ -51,6 +54,8 @@ class MessageFragment : Fragment(), CoroutineScope { override val coroutineContext: CoroutineContext get() = job + Dispatchers.Main + private val manager + get() = app.messageManager private lateinit var message: MessageFull override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { @@ -64,10 +69,20 @@ class MessageFragment : Fragment(), CoroutineScope { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { if (!isAdded) return + activity.bottomSheet.prependItem( + BottomSheetPrimaryItem(true) + .withTitle(R.string.menu_messages_config) + .withIcon(CommunityMaterial.Icon.cmd_cog_outline) + .withOnClickListener { + activity.bottomSheet.close() + MessagesConfigDialog(activity, false, null, null).show() + } + ) + b.closeButton.setImageDrawable( IconicsDrawable(activity, CommunityMaterial.Icon3.cmd_window_close).apply { colorAttr(activity, android.R.attr.textColorSecondary) - sizeDp = 16 + sizeDp = 24 } ) b.closeButton.setOnClickListener { activity.navigateUp() } @@ -80,14 +95,43 @@ 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) + manager.setStarIcon(b.messageStar, message) + } + } + b.messageStar.attachToastHint(R.string.hint_message_star) + b.replyButton.onClick { - activity.loadTarget(MainActivity.TARGET_MESSAGES_COMPOSE, Bundle( + activity.navigate(navTarget = NavTarget.MESSAGE_COMPOSE, args = Bundle( "message" to app.gson.toJson(message), "type" to "reply" )) } b.forwardButton.onClick { - activity.loadTarget(MainActivity.TARGET_MESSAGES_COMPOSE, Bundle( + activity.navigate(navTarget = NavTarget.MESSAGE_COMPOSE, args = Bundle( "message" to app.gson.toJson(message), "type" to "forward" )) @@ -98,10 +142,7 @@ class MessageFragment : Fragment(), CoroutineScope { .setMessage(R.string.messages_delete_confirmation_text) .setPositiveButton(R.string.ok) { _, _ -> launch { - message.type = TYPE_DELETED - withContext(Dispatchers.Default) { - app.db.messageDao().replace(message) - } + manager.markAsDeleted(message) Toast.makeText(activity, "Wiadomość przeniesiona do usuniętych", Toast.LENGTH_SHORT).show() activity.navigateUp() } @@ -115,50 +156,10 @@ class MessageFragment : Fragment(), CoroutineScope { } launch { - - val messageString = arguments?.getString("message") - val messageId = arguments?.getLong("messageId") - if (messageId == null) { + message = manager.getMessage(App.profileId, arguments) ?: run { activity.navigateUp() return@launch } - - val msg = withContext(Dispatchers.Default) { - - val msg = - if (messageString != null) - app.gson.fromJson(messageString, MessageFull::class.java)?.also { - if (arguments?.getLong("sentDate") ?: 0L > 0L) - it.addedDate = arguments?.getLong("sentDate") ?: 0L - } - else - app.db.messageDao().getByIdNow(App.profileId, messageId) - - // load recipients in sent messages - val teachers by lazy { app.db.teacherDao().getAllNow(App.profileId) } - msg?.recipients?.forEach { recipient -> - if (recipient.fullName == null) { - recipient.fullName = teachers.firstOrNull { it.id == recipient.id }?.fullName ?: "" - } - } - - if (msg?.type == TYPE_SENT && msg.senderName == null) { - msg.senderName = app.profile.accountName ?: app.profile.studentNameLong - } - - msg?.also { - //it.recipients = app.db.messageRecipientDao().getAllByMessageId(it.profileId, it.id) - if (it.body != null && !it.seen) { - app.db.metadataDao().setSeen(it.profileId, it, true) - } - } - - } ?: run { - activity.navigateUp() - return@launch - } - - message = msg b.subject.text = message.subject checkMessage() } @@ -177,7 +178,7 @@ class MessageFragment : Fragment(), CoroutineScope { return } - if (app.profile.loginStoreType == LOGIN_TYPE_IDZIENNIK) { + if (app.profile.loginStoreType == LoginType.IDZIENNIK) { val meta = "\\[META:([A-z0-9]+);([0-9-]+)]".toRegex().find(message.body!!) val messageIdBefore = meta?.get(2)?.toLong() ?: -1 @@ -187,15 +188,14 @@ class MessageFragment : Fragment(), CoroutineScope { } } - val readByAll = checkRecipients() - if (app.profile.loginStoreType == LoginStore.LOGIN_TYPE_VULCAN) { + if (app.data.messagesConfig.needsReadStatus) { // vulcan: change message status or download attachments - if (message.type == TYPE_RECEIVED && !message.seen || message.attachmentIds == null) { + if ((message.isReceived || message.isDeleted) && !message.seen || message.attachmentIds == null) { EdziennikTask.messageGet(App.profileId, message).enqueue(activity) return } } - else if (!readByAll) { + else if (!message.readByEveryone) { // if a sent msg is not read by everyone, download it again to check the read status EdziennikTask.messageGet(App.profileId, message).enqueue(activity) return @@ -204,16 +204,6 @@ class MessageFragment : Fragment(), CoroutineScope { showMessage() } - private fun checkRecipients(): Boolean { - message.recipients?.forEach { recipient -> - if (recipient.id == -1L) - recipient.fullName = app.profile.accountName ?: app.profile.studentNameLong ?: "" - if (message.type == TYPE_SENT && recipient.readDate < 1) - return false - } - return true - } - private fun showMessage() { b.body.text = MessagesUtils.htmlToSpannable(activity, message.body.toString()) b.date.text = getString(R.string.messages_date_time_format, Date.fromMillis(message.addedDate).formattedStringShort, Time.fromMillis(message.addedDate).stringHM) @@ -224,9 +214,11 @@ class MessageFragment : Fragment(), CoroutineScope { b.subject.text = message.subject - b.replyButton.isVisible = message.type == TYPE_RECEIVED || message.type == TYPE_DELETED - b.deleteButton.isVisible = message.type == TYPE_RECEIVED - if (message.type == TYPE_RECEIVED || message.type == TYPE_DELETED) { + manager.setStarIcon(b.messageStar, message) + + b.replyButton.isVisible = message.isReceived || message.isDeleted + b.deleteButton.isVisible = message.isReceived + if (message.isReceived || message.isDeleted) { activity.navView.apply { bottomBar.apply { fabEnable = true @@ -234,9 +226,9 @@ class MessageFragment : Fragment(), CoroutineScope { fabIcon = CommunityMaterial.Icon3.cmd_reply_outline } - setFabOnClickListener(View.OnClickListener { + setFabOnClickListener { b.replyButton.performClick() - }) + } } activity.gainAttentionFAB() } @@ -265,7 +257,7 @@ class MessageFragment : Fragment(), CoroutineScope { } } messageRecipients.append("") - b.recipients.text = Html.fromHtml(messageRecipients.toString()) + b.recipients.text = BetterHtml.fromHtml(activity, messageRecipients) showAttachments() @@ -275,6 +267,13 @@ class MessageFragment : Fragment(), CoroutineScope { b.progress.visibility = View.GONE Anim.fadeIn(b.content, 200, null) MessagesFragment.pageSelection = min(message.type, 1) + + b.notesButton.setupNotesButton( + activity = activity, + owner = message, + onShowListener = null, + onDismissListener = null, + ) } private fun showAttachments() { @@ -289,8 +288,8 @@ class MessageFragment : Fragment(), CoroutineScope { it.putInt("profileId", message.profileId) it.putLongArray("attachmentIds", message.attachmentIds!!.toLongArray()) it.putStringArray("attachmentNames", message.attachmentNames!!.toTypedArray()) - //if (message.attachmentSizes.isNotNullNorEmpty()) - // it.putLongArray("attachmentSizes", message.attachmentSizes!!.toLongArray()) + if (message.attachmentSizes.isNotNullNorEmpty()) + it.putLongArray("attachmentSizes", message.attachmentSizes!!.toLongArray()) }, owner = message) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt deleted file mode 100644 index 367b00da..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/AgendaFragment.kt +++ /dev/null @@ -1,305 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2020-1-25 - */ - -package pl.szczodrzynski.edziennik.ui.modules.agenda - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Toast -import androidx.databinding.ViewDataBinding -import androidx.fragment.app.Fragment -import com.applandeo.materialcalendarview.EventDay -import com.github.tibolte.agendacalendarview.CalendarPickerController -import com.github.tibolte.agendacalendarview.models.BaseCalendarEvent -import com.github.tibolte.agendacalendarview.models.CalendarEvent -import com.github.tibolte.agendacalendarview.models.IDayItem -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.sizeDp -import eu.szkolny.font.SzkolnyFont -import kotlinx.coroutines.* -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.MainActivity -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.db.entity.Metadata -import pl.szczodrzynski.edziennik.data.db.entity.Profile -import pl.szczodrzynski.edziennik.databinding.FragmentAgendaCalendarBinding -import pl.szczodrzynski.edziennik.databinding.FragmentAgendaDefaultBinding -import pl.szczodrzynski.edziennik.ui.dialogs.day.DayDialog -import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog -import pl.szczodrzynski.edziennik.ui.dialogs.lessonchange.LessonChangeDialog -import pl.szczodrzynski.edziennik.ui.dialogs.teacherabsence.TeacherAbsenceDialog -import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange.LessonChangeCounter -import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange.LessonChangeEvent -import pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange.LessonChangeEventRenderer -import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceCounter -import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEvent -import pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence.TeacherAbsenceEventRenderer -import pl.szczodrzynski.edziennik.utils.Colors -import pl.szczodrzynski.edziennik.utils.Themes -import pl.szczodrzynski.edziennik.utils.models.Date -import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem -import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem -import java.util.* -import kotlin.coroutines.CoroutineContext - -class AgendaFragment : Fragment(), CoroutineScope { - - private lateinit var activity: MainActivity - private lateinit var b: ViewDataBinding - - private val app by lazy { activity.app } - - private var job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - private var type: Int = Profile.AGENDA_DEFAULT - private var actualDate: Date? = null - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - if (getActivity() == null || context == null) return null - activity = getActivity() as MainActivity - context?.theme?.applyStyle(Themes.appTheme, true) - type = app.config.forProfile().ui.agendaViewType - b = when (type) { - Profile.AGENDA_DEFAULT -> FragmentAgendaDefaultBinding.inflate(inflater, container, false) - Profile.AGENDA_CALENDAR -> FragmentAgendaCalendarBinding.inflate(inflater, container, false) - else -> return null - } - return b.root - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - if (!isAdded) return - - activity.bottomSheet.prependItems( - BottomSheetPrimaryItem(true) - .withTitle(R.string.menu_add_event) - .withDescription(R.string.menu_add_event_desc) - .withIcon(SzkolnyFont.Icon.szf_calendar_plus_outline) - .withOnClickListener(View.OnClickListener { - activity.bottomSheet.close() - EventManualDialog(activity, app.profileId, defaultDate = actualDate) - }), - BottomSheetPrimaryItem(true) - .withTitle(R.string.menu_agenda_change_view) - .withIcon(if (type == Profile.AGENDA_DEFAULT) CommunityMaterial.Icon.cmd_calendar_outline else CommunityMaterial.Icon2.cmd_format_list_bulleted_square) - .withOnClickListener(View.OnClickListener { - activity.bottomSheet.close() - type = if (type == Profile.AGENDA_DEFAULT) Profile.AGENDA_CALENDAR else Profile.AGENDA_DEFAULT - app.config.forProfile().ui.agendaViewType = type - activity.reloadTarget() - }), - BottomSheetSeparatorItem(true), - BottomSheetPrimaryItem(true) - .withTitle(R.string.menu_mark_as_read) - .withIcon(CommunityMaterial.Icon.cmd_eye_check_outline) - .withOnClickListener(View.OnClickListener { launch { - activity.bottomSheet.close() - withContext(Dispatchers.Default) { - App.db.metadataDao().setAllSeen(app.profileId, Metadata.TYPE_EVENT, true) - } - Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show() - }}) - ) - - activity.navView.bottomBar.fabEnable = true - activity.navView.bottomBar.fabExtendedText = getString(R.string.add) - activity.navView.bottomBar.fabIcon = CommunityMaterial.Icon3.cmd_plus - activity.navView.setFabOnClickListener(View.OnClickListener { - EventManualDialog(activity, app.profileId, defaultDate = actualDate) - }) - - activity.gainAttention() - activity.gainAttentionFAB() - - when (type) { - Profile.AGENDA_DEFAULT -> createDefaultAgendaView() - Profile.AGENDA_CALENDAR -> createCalendarAgendaView() - } - } - - private fun createDefaultAgendaView() { (b as? FragmentAgendaDefaultBinding)?.let { b -> launch { - if (!isAdded) - return@launch - delay(500) - - val eventList = mutableListOf() - - val minDate = Calendar.getInstance().apply { - add(Calendar.MONTH, -2) - set(Calendar.DAY_OF_MONTH, 1) - } - val maxDate = Calendar.getInstance().apply { add(Calendar.MONTH, 2) } - - /** - * LESSON CHANGES - */ - if (!isAdded) - return@launch - - val lessons = withContext(Dispatchers.Default) { app.db.timetableDao().getChangesNow(app.profileId) } - val lessonChangeCounters = mutableListOf() - - lessons.forEach { lesson -> - lessonChangeCounters.firstOrNull { it.lessonChangeDate == lesson.displayDate }?.let { - it.lessonChangeCount += 1 - } ?: run { - lessonChangeCounters.add(LessonChangeCounter( - lesson.displayDate ?: return@forEach, - 1 - )) - } - } - - lessonChangeCounters.forEach { counter -> - eventList.add(LessonChangeEvent( - counter.lessonChangeDate.inMillis, - 0xff78909c.toInt(), - Colors.legibleTextColor(0xff78909c.toInt()), - counter.startTime, - counter.endTime, - app.profileId, - counter.lessonChangeDate, - counter.lessonChangeCount - )) - } - - /** - * TEACHER ABSENCES - */ - if (!isAdded) - return@launch - - val showTeacherAbsences = app.profile.getStudentData("showTeacherAbsences", true) - - if (showTeacherAbsences) { - val teacherAbsenceList = withContext(Dispatchers.Default) { app.db.teacherAbsenceDao().getAllNow(app.profileId) } - val teacherAbsenceCounters = mutableListOf() - - teacherAbsenceList.forEach { absence -> - val date = absence.dateFrom.clone() - - while (date <= absence.dateTo) { - teacherAbsenceCounters.firstOrNull { it.teacherAbsenceDate == date }?.let { - it.teacherAbsenceCount += 1 - } ?: run { - teacherAbsenceCounters.add(TeacherAbsenceCounter(date.clone(), 1)) - } - - date.stepForward(0, 0, 1) - } - } - - teacherAbsenceCounters.forEach { counter -> - eventList.add(TeacherAbsenceEvent( - counter.teacherAbsenceDate.inMillis, - 0xffff1744.toInt(), - Colors.legibleTextColor(0xffff1744.toInt()), - counter.startTime, - counter.endTime, - app.profileId, - counter.teacherAbsenceDate, - counter.teacherAbsenceCount - )) - } - } - - /** - * EVENTS - */ - if (!isAdded) - return@launch - - val events = withContext(Dispatchers.Default) { app.db.eventDao().getAllNow(app.profileId) } - val unreadEventDates = mutableSetOf() - - events.forEach { event -> - eventList.add(BaseCalendarEvent( - "${event.typeName ?: "wydarzenie"} - ${event.topic}", - "", - (if (event.time == null) getString(R.string.agenda_event_all_day) else event.time!!.stringHM) + - (event.subjectLongName?.let { ", $it" } ?: "") + - (event.teacherName?.let { ", $it" } ?: "") + - (event.teamName?.let { ", $it" } ?: ""), - event.eventColor, - Colors.legibleTextColor(event.eventColor), - event.startTimeCalendar, - event.endTimeCalendar, - event.time == null, - event.id, - !event.seen - )) - - if (!event.seen) unreadEventDates.add(event.date.value) - } - - b.agendaDefaultView.init(eventList, minDate, maxDate, Locale.getDefault(), object : CalendarPickerController { - override fun onDaySelected(dayItem: IDayItem?) {} - - override fun onScrollToDate(calendar: Calendar) { this@AgendaFragment.launch { - val date = Date.fromCalendar(calendar) - actualDate = date - - // Mark as read scrolled date - if (date.value in unreadEventDates) { - withContext(Dispatchers.Default) { app.db.eventDao().setSeenByDate(app.profileId, date, true) } - unreadEventDates.remove(date.value) - } - }} - - override fun onEventSelected(event: CalendarEvent) { - val date = Date.fromCalendar(event.instanceDay) - - when (event) { - is BaseCalendarEvent -> DayDialog(activity, app.profileId, date) - is LessonChangeEvent -> LessonChangeDialog(activity, app.profileId, date) - is TeacherAbsenceEvent -> TeacherAbsenceDialog(activity, app.profileId, date) - } - } - - }, LessonChangeEventRenderer(), TeacherAbsenceEventRenderer()) - - b.progressBar.visibility = View.GONE - }}} - - private fun createCalendarAgendaView() { (b as? FragmentAgendaCalendarBinding)?.let { b -> launch { - delay(300) - - val dayList = mutableListOf() - - val events = withContext(Dispatchers.Default) { app.db.eventDao().getAllNow(app.profileId) } - val unreadEventDates = mutableSetOf() - - events.forEach { event -> - val eventIcon = IconicsDrawable(activity).apply { - icon = CommunityMaterial.Icon.cmd_checkbox_blank_circle - sizeDp = 10 - colorInt = event.eventColor - } - - dayList.add(EventDay(event.startTimeCalendar, eventIcon)) - - if (!event.seen) unreadEventDates.add(event.date.value) - } - - b.agendaCalendarView.setEvents(dayList) - b.agendaCalendarView.setOnDayClickListener { day -> this@AgendaFragment.launch { - val date = Date.fromCalendar(day.calendar) - - if (date.value in unreadEventDates) { - withContext(Dispatchers.Default) { app.db.eventDao().setSeenByDate(app.profileId, date, true) } - unreadEventDates.remove(date.value) - } - - DayDialog(activity, app.profileId, date) - }} - - b.progressBar.visibility = View.GONE - }}} -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchange/LessonChangeCounter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchange/LessonChangeCounter.kt deleted file mode 100644 index 2b264ede..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchange/LessonChangeCounter.kt +++ /dev/null @@ -1,19 +0,0 @@ -package pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange - -import pl.szczodrzynski.edziennik.utils.models.Date -import java.util.* - -class LessonChangeCounter( - val lessonChangeDate: Date, - var lessonChangeCount: Int -) { - val startTime: Calendar - get() = Calendar.getInstance().apply { - set(lessonChangeDate.year, lessonChangeDate.month - 1, lessonChangeDate.day, 10, 0, 0) - } - - val endTime: Calendar - get() = Calendar.getInstance().apply { - timeInMillis = startTime.timeInMillis + (45 * 60 * 1000) - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchange/LessonChangeEvent.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchange/LessonChangeEvent.java deleted file mode 100644 index 077c2206..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchange/LessonChangeEvent.java +++ /dev/null @@ -1,243 +0,0 @@ -package pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange; - -import com.github.tibolte.agendacalendarview.models.CalendarEvent; -import com.github.tibolte.agendacalendarview.models.IDayItem; -import com.github.tibolte.agendacalendarview.models.IWeekItem; - -import java.util.Calendar; - -import pl.szczodrzynski.edziennik.utils.models.Date; - -public class LessonChangeEvent implements CalendarEvent { - - /** - * Id of the event. - */ - private long mId; - /** - * Color to be displayed in the agenda view. - */ - private int mColor; - /** - * Text color displayed on the background color - */ - private int mTextColor; - /** - * Calendar instance helping sorting the events per section in the agenda view. - */ - private Calendar mInstanceDay; - /** - * Start time of the event. - */ - private Calendar mStartTime; - /** - * End time of the event. - */ - private Calendar mEndTime; - /** - * References to a DayItem instance for that event, used to link interaction between the - * calendar view and the agenda view. - */ - private IDayItem mDayReference; - /** - * References to a WeekItem instance for that event, used to link interaction between the - * calendar view and the agenda view. - */ - private IWeekItem mWeekReference; - - - private int profileId; - private Date lessonChangeDate; - private int lessonChangeCount; - - public LessonChangeEvent(LessonChangeEvent calendarEvent) { - this.mId = calendarEvent.getId(); - this.mColor = calendarEvent.getColor(); - this.mTextColor = calendarEvent.getTextColor(); - this.mStartTime = calendarEvent.getStartTime(); - this.mEndTime = calendarEvent.getEndTime(); - this.profileId = calendarEvent.getProfileId(); - this.lessonChangeDate = calendarEvent.getLessonChangeDate(); - this.lessonChangeCount = calendarEvent.getLessonChangeCount(); - } - - public LessonChangeEvent(long mId, int mColor, int mTextColor, Calendar mStartTime, Calendar mEndTime, int profileId, Date lessonChangeDate, int lessonChangeCount) { - this.mId = mId; - this.mColor = mColor; - this.mTextColor = mTextColor; - this.mStartTime = mStartTime; - this.mEndTime = mEndTime; - this.profileId = profileId; - this.lessonChangeDate = lessonChangeDate; - this.lessonChangeCount = lessonChangeCount; - } - - public int getProfileId() { - return profileId; - } - - public Date getLessonChangeDate() { - return lessonChangeDate; - } - - public int getLessonChangeCount() { - return lessonChangeCount; - } - - public void setProfileId(int profileId) { - this.profileId = profileId; - } - - public void setLessonChangeDate(Date lessonChangeDate) { - this.lessonChangeDate = lessonChangeDate; - } - - public void setLessonChangeCount(int lessonChangeCount) { - this.lessonChangeCount = lessonChangeCount; - } - - @Override - public void setPlaceholder(boolean placeholder) { - - } - - @Override - public boolean isPlaceholder() { - return false; - } - - @Override - public String getLocation() { - return null; - } - - @Override - public void setLocation(String mLocation) { - - } - - @Override - public long getId() { - return mId; - } - - @Override - public void setId(long mId) { - this.mId = mId; - } - - @Override - public boolean getShowBadge() { - return false; - } - - @Override - public void setShowBadge(boolean mShowBadge) { - - } - - @Override - public int getTextColor() { - return mTextColor; - } - - @Override - public void setTextColor(int mTextColor) { - this.mTextColor = mTextColor; - } - - @Override - public String getDescription() { - return null; - } - - @Override - public void setDescription(String mDescription) { - - } - - @Override - public boolean isAllDay() { - return false; - } - - @Override - public void setAllDay(boolean allDay) { - - } - - @Override - public Calendar getStartTime() { - return mStartTime; - } - - @Override - public void setStartTime(Calendar mStartTime) { - this.mStartTime = mStartTime; - } - - @Override - public Calendar getEndTime() { - return mEndTime; - } - - @Override - public void setEndTime(Calendar mEndTime) { - this.mEndTime = mEndTime; - } - - @Override - public String getTitle() { - return null; - } - - @Override - public void setTitle(String mTitle) { - - } - - @Override - public Calendar getInstanceDay() { - return mInstanceDay; - } - - @Override - public void setInstanceDay(Calendar mInstanceDay) { - this.mInstanceDay = mInstanceDay; - this.mInstanceDay.set(Calendar.HOUR, 0); - this.mInstanceDay.set(Calendar.MINUTE, 0); - this.mInstanceDay.set(Calendar.SECOND, 0); - this.mInstanceDay.set(Calendar.MILLISECOND, 0); - this.mInstanceDay.set(Calendar.AM_PM, 0); - } - - @Override - public IDayItem getDayReference() { - return mDayReference; - } - - @Override - public void setDayReference(IDayItem mDayReference) { - this.mDayReference = mDayReference; - } - - @Override - public IWeekItem getWeekReference() { - return mWeekReference; - } - - @Override - public void setWeekReference(IWeekItem mWeekReference) { - this.mWeekReference = mWeekReference; - } - - @Override - public CalendarEvent copy() { - return new LessonChangeEvent(this); - } - - @Override - public int getColor() { - return mColor; - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchange/LessonChangeEventRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchange/LessonChangeEventRenderer.kt deleted file mode 100644 index e2306039..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/lessonchange/LessonChangeEventRenderer.kt +++ /dev/null @@ -1,21 +0,0 @@ -package pl.szczodrzynski.edziennik.ui.modules.agenda.lessonchange - -import android.view.View -import android.widget.TextView -import androidx.cardview.widget.CardView -import com.github.tibolte.agendacalendarview.render.EventRenderer -import pl.szczodrzynski.edziennik.R - -class LessonChangeEventRenderer : EventRenderer() { - override fun render(view: View?, event: LessonChangeEvent) { - val card = view?.findViewById(R.id.lesson_change_card) - val changeText = view?.findViewById(R.id.lesson_change_text) - val changeCount = view?.findViewById(R.id.lessonChangeCount) - card?.setCardBackgroundColor(event.color) - changeText?.setTextColor(event.textColor) - changeCount?.setTextColor(event.textColor) - changeCount?.text = event.lessonChangeCount.toString() - } - - override fun getEventLayout(): Int = R.layout.agenda_event_lesson_change -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceCounter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceCounter.kt deleted file mode 100644 index 71dea025..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceCounter.kt +++ /dev/null @@ -1,19 +0,0 @@ -package pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence - -import pl.szczodrzynski.edziennik.utils.models.Date -import java.util.* - -class TeacherAbsenceCounter ( - val teacherAbsenceDate: Date, - var teacherAbsenceCount: Int = 0 -) { - val startTime: Calendar - get() = Calendar.getInstance().apply { - set(teacherAbsenceDate.year, teacherAbsenceDate.month - 1, teacherAbsenceDate.day, 10, 0, 0) - } - - val endTime: Calendar - get() = Calendar.getInstance().apply { - timeInMillis = startTime.timeInMillis + (45 * 60 * 1000) - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEvent.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEvent.kt deleted file mode 100644 index fc1c28db..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEvent.kt +++ /dev/null @@ -1,188 +0,0 @@ -package pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence - -import com.github.tibolte.agendacalendarview.models.CalendarEvent -import com.github.tibolte.agendacalendarview.models.IDayItem -import com.github.tibolte.agendacalendarview.models.IWeekItem -import pl.szczodrzynski.edziennik.utils.models.Date -import java.util.* - -class TeacherAbsenceEvent : CalendarEvent { - /** - * Id of the event. - */ - private var mId: Long = 0 - /** - * Color to be displayed in the agenda view. - */ - private var mColor: Int = 0 - /** - * Text color displayed on the background color - */ - private var mTextColor: Int = 0 - /** - * Calendar instance helping sorting the events per section in the agenda view. - */ - private var mInstanceDay: Calendar? = null - /** - * Start time of the event. - */ - private var mStartTime: Calendar? = null - /** - * End time of the event. - */ - private var mEndTime: Calendar? = null - /** - * References to a DayItem instance for that event, used to link interaction between the - * calendar view and the agenda view. - */ - private var mDayReference: IDayItem? = null - /** - * References to a WeekItem instance for that event, used to link interaction between the - * calendar view and the agenda view. - */ - private var mWeekReference: IWeekItem? = null - - - private var profileId: Int = 0 - var teacherAbsenceDate: Date? = null - var teacherAbsenceCount: Int = 0 - - constructor(calendarEvent: TeacherAbsenceEvent) { - this.mId = calendarEvent.id - this.mColor = calendarEvent.color - this.mTextColor = calendarEvent.textColor - this.mStartTime = calendarEvent.startTime - this.mEndTime = calendarEvent.endTime - this.profileId = calendarEvent.profileId - this.teacherAbsenceDate = calendarEvent.teacherAbsenceDate - this.teacherAbsenceCount = calendarEvent.teacherAbsenceCount - } - - constructor(mId: Long, mColor: Int, mTextColor: Int, mStartTime: Calendar, mEndTime: Calendar, profileId: Int, teacherAbsenceDate: Date, teacherAbsenceCount: Int) { - this.mId = mId - this.mColor = mColor - this.mTextColor = mTextColor - this.mStartTime = mStartTime - this.mEndTime = mEndTime - this.profileId = profileId - this.teacherAbsenceDate = teacherAbsenceDate - this.teacherAbsenceCount = teacherAbsenceCount - } - - override fun setPlaceholder(placeholder: Boolean) { - - } - - override fun isPlaceholder(): Boolean { - return false - } - - override fun getLocation(): String? { - return null - } - - override fun setLocation(mLocation: String) { - - } - - override fun getId(): Long { - return mId - } - - override fun setId(mId: Long) { - this.mId = mId - } - - override fun getShowBadge(): Boolean { - return false - } - - override fun setShowBadge(mShowBadge: Boolean) { - - } - - override fun getTextColor(): Int { - return mTextColor - } - - override fun setTextColor(mTextColor: Int) { - this.mTextColor = mTextColor - } - - override fun getDescription(): String? { - return null - } - - override fun setDescription(mDescription: String) { - - } - - override fun isAllDay(): Boolean { - return false - } - - override fun setAllDay(allDay: Boolean) { - - } - - override fun getStartTime(): Calendar? { - return mStartTime - } - - override fun setStartTime(mStartTime: Calendar) { - this.mStartTime = mStartTime - } - - override fun getEndTime(): Calendar? { - return mEndTime - } - - override fun setEndTime(mEndTime: Calendar) { - this.mEndTime = mEndTime - } - - override fun getTitle(): String? { - return null - } - - override fun setTitle(mTitle: String) { - - } - - override fun getInstanceDay(): Calendar? { - return mInstanceDay - } - - override fun setInstanceDay(mInstanceDay: Calendar) { - this.mInstanceDay = mInstanceDay - this.mInstanceDay!!.set(Calendar.HOUR, 0) - this.mInstanceDay!!.set(Calendar.MINUTE, 0) - this.mInstanceDay!!.set(Calendar.SECOND, 0) - this.mInstanceDay!!.set(Calendar.MILLISECOND, 0) - this.mInstanceDay!!.set(Calendar.AM_PM, 0) - } - - override fun getDayReference(): IDayItem? { - return mDayReference - } - - override fun setDayReference(mDayReference: IDayItem) { - this.mDayReference = mDayReference - } - - override fun getWeekReference(): IWeekItem? { - return mWeekReference - } - - override fun setWeekReference(mWeekReference: IWeekItem) { - this.mWeekReference = mWeekReference - } - - override fun copy(): CalendarEvent { - return TeacherAbsenceEvent(this) - } - - override fun getColor(): Int { - return mColor - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt deleted file mode 100644 index b437769b..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/agenda/teacherabsence/TeacherAbsenceEventRenderer.kt +++ /dev/null @@ -1,21 +0,0 @@ -package pl.szczodrzynski.edziennik.ui.modules.agenda.teacherabsence - -import android.view.View -import android.widget.TextView -import androidx.cardview.widget.CardView -import com.github.tibolte.agendacalendarview.render.EventRenderer -import pl.szczodrzynski.edziennik.R - -class TeacherAbsenceEventRenderer : EventRenderer() { - override fun render(view: View?, event: TeacherAbsenceEvent) { - val card = view?.findViewById(R.id.teacherAbsenceCard) - val changeText = view?.findViewById(R.id.teacherAbsenceText) - val changeCount = view?.findViewById(R.id.teacherAbsenceCount) - card?.setCardBackgroundColor(event.color) - changeText?.setTextColor(event.textColor) - changeCount?.setTextColor(event.textColor) - changeCount?.text = event.teacherAbsenceCount.toString() - } - - override fun getEventLayout(): Int = R.layout.agenda_event_teacher_absence -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceDetailsDialog.kt deleted file mode 100644 index 9e6c14d6..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/attendance/AttendanceDetailsDialog.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2020-5-9. - */ - -package pl.szczodrzynski.edziennik.ui.modules.attendance - -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity -import androidx.core.graphics.ColorUtils -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull -import pl.szczodrzynski.edziennik.databinding.AttendanceDetailsDialogBinding -import pl.szczodrzynski.edziennik.setTintColor -import kotlin.coroutines.CoroutineContext - -class AttendanceDetailsDialog( - val activity: AppCompatActivity, - val attendance: AttendanceFull, - val onShowListener: ((tag: String) -> Unit)? = null, - val onDismissListener: ((tag: String) -> Unit)? = null -) : CoroutineScope { - companion object { - private const val TAG = "AttendanceDetailsDialog" - } - - private lateinit var app: App - private lateinit var b: AttendanceDetailsDialogBinding - private lateinit var dialog: AlertDialog - - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - // local variables go here - - init { run { - if (activity.isFinishing) - return@run - onShowListener?.invoke(TAG) - app = activity.applicationContext as App - b = AttendanceDetailsDialogBinding.inflate(activity.layoutInflater) - dialog = MaterialAlertDialogBuilder(activity) - .setView(b.root) - .setPositiveButton(R.string.close, null) - .setOnDismissListener { - onDismissListener?.invoke(TAG) - } - .show() - val manager = app.attendanceManager - - val attendanceColor = manager.getAttendanceColor(attendance) - b.attendance = attendance - b.devMode = App.devMode - b.attendanceName.setTextColor(if (ColorUtils.calculateLuminance(attendanceColor) > 0.3) 0xaa000000.toInt() else 0xccffffff.toInt()) - b.attendanceName.background.setTintColor(attendanceColor) - - b.attendanceIsCounted.setText(if (attendance.isCounted) R.string.yes else R.string.no) - }} -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabPageFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabPageFragment.kt deleted file mode 100644 index b1b84f9d..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/debug/LabPageFragment.kt +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2020-5-12. - */ - -package pl.szczodrzynski.edziennik.ui.modules.debug - -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.sqlite.db.SimpleSQLiteQuery -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.db.entity.Event -import pl.szczodrzynski.edziennik.databinding.LabFragmentBinding -import pl.szczodrzynski.edziennik.ui.dialogs.profile.ProfileRemoveDialog -import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment -import pl.szczodrzynski.edziennik.utils.TextInputDropDown -import pl.szczodrzynski.fslogin.decode -import kotlin.coroutines.CoroutineContext - -class LabPageFragment : LazyFragment(), CoroutineScope { - companion object { - private const val TAG = "LabPageFragment" - } - - private lateinit var app: App - private lateinit var activity: MainActivity - private lateinit var b: LabFragmentBinding - - private val job: Job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - // local/private variables go here - - 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 = LabFragmentBinding.inflate(inflater) - return b.root - } - - override fun onPageCreated(): Boolean { - b.app = app - - b.last10unseen.onClick { - launch(Dispatchers.Default) { - val events = app.db.eventDao().getAllNow(App.profileId) - val ids = events.sortedBy { it.date }.filter { it.type == Event.TYPE_HOMEWORK }.takeLast(10) - ids.forEach { - app.db.metadataDao().setSeen(App.profileId, it, false) - } - } - } - - b.rodo.onClick { - app.db.teacherDao().query(SimpleSQLiteQuery("UPDATE teachers SET teacherSurname = \"\" WHERE profileId = ${App.profileId}")) - } - - b.fullSync.onClick { - app.db.query(SimpleSQLiteQuery("UPDATE profiles SET empty = 1 WHERE profileId = ${App.profileId}")) - app.db.query(SimpleSQLiteQuery("DELETE FROM endpointTimers WHERE profileId = ${App.profileId}")) - } - - b.clearProfile.onClick { - ProfileRemoveDialog(activity, App.profileId, "FAKE", noProfileRemoval = true) - } - - b.removeHomework.onClick { - app.db.eventDao().getRawNow("UPDATE events SET homeworkBody = NULL WHERE profileId = ${App.profileId}") - } - - b.unarchive.onClick { - app.profile.archived = false - app.profile.archiveId = null - app.profileSave() - } - - val profiles = app.db.profileDao().allNow - b.profile.clear() - b.profile += profiles.map { TextInputDropDown.Item(it.id.toLong(), "${it.id} ${it.name} archived ${it.archived}", tag = it) } - b.profile.select(app.profileId.toLong()) - b.profile.setOnChangeListener { - activity.loadProfile(it.id.toInt()) - return@setOnChangeListener true - } - - 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") - b.cookies.text = text - } - - return true - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorDetailsDialog.kt deleted file mode 100644 index 32ec46b9..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/error/ErrorDetailsDialog.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2020-2-16. - */ - -package pl.szczodrzynski.edziennik.ui.modules.error - -import android.widget.Toast -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.* -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi -import kotlin.coroutines.CoroutineContext - -class ErrorDetailsDialog( - val activity: AppCompatActivity, - val errors: List, - val titleRes: Int = R.string.dialog_error_details_title, - val onShowListener: ((tag: String) -> Unit)? = null, - val onDismissListener: ((tag: String) -> Unit)? = null -) : CoroutineScope { - companion object { - private const val TAG = "ApiErrorDialog" - } - - private lateinit var app: App - private lateinit var dialog: AlertDialog - - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - private val api by lazy { SzkolnyApi(activity.applicationContext as App) } - - init { run { - if (activity.isFinishing) - return@run - onShowListener?.invoke(TAG) - app = activity.applicationContext as App - - if (errors.isNotEmpty()) { - val message = errors.map { - listOf( - it.getStringReason(activity).asBoldSpannable().asColoredSpannable(R.attr.colorOnBackground.resolveAttr(activity)), - activity.getString(R.string.error_unknown_format, it.errorCode, it.tag), - if (App.devMode) - it.throwable?.stackTraceString ?: it.throwable?.localizedMessage - else - it.throwable?.localizedMessage - ).concat("\n") - }.concat("\n\n") - - dialog = MaterialAlertDialogBuilder(activity) - .setTitle(titleRes) - .setMessage(message) - .setPositiveButton(R.string.ok) { _, _ -> - dialog.dismiss() - } - .setNeutralButton(R.string.report) { _, _ -> - launch { - api.runCatching({ - withContext(Dispatchers.Default) { - errorReport(errors.map { it.toReportableError(activity) }) - } - }, { - Toast.makeText(activity, activity.getString(R.string.crash_report_cannot_send) + it, Toast.LENGTH_LONG).show() - }) ?: return@launch - - dialog.dismiss() - } - } - .setCancelable(false) - .setOnDismissListener { - onDismissListener?.invoke(TAG) - } - .show() - } - }} -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/HomeConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/HomeConfigDialog.kt deleted file mode 100644 index 268bf211..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/home/HomeConfigDialog.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2020-3-11. - */ - -package pl.szczodrzynski.edziennik.ui.modules.home - -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.MainActivity -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard.Companion.CARD_EVENTS -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard.Companion.CARD_GRADES -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard.Companion.CARD_LUCKY_NUMBER -import pl.szczodrzynski.edziennik.ui.modules.home.HomeCard.Companion.CARD_TIMETABLE -import kotlin.collections.set - -class HomeConfigDialog( - val activity: AppCompatActivity, - private val reloadOnDismiss: Boolean = true, - val onShowListener: ((tag: String) -> Unit)? = null, - val onDismissListener: ((tag: String) -> Unit)? = null -) { - companion object { - const val TAG = "HomeConfigDialog" - } - - private val app by lazy { activity.application as App } - private val profileConfig by lazy { app.config.getFor(app.profileId).ui } - - private lateinit var dialog: AlertDialog - - init { run { - if (activity.isFinishing) - return@run - onShowListener?.invoke(TAG) - - val ids = listOf( - CARD_LUCKY_NUMBER, - CARD_TIMETABLE, - CARD_GRADES, - CARD_EVENTS - ) - val items = listOf( - app.getString(R.string.card_type_lucky_number), - app.getString(R.string.card_type_timetable), - app.getString(R.string.card_type_grades), - app.getString(R.string.card_type_events) - ) - val checkedItems = ids.map { it to false }.toMap().toMutableMap() - - val profileId = App.profileId - val homeCards = profileConfig.homeCards - .filter { it.profileId == profileId } - .toMutableList() - - homeCards.forEach { - checkedItems[it.cardId] = true - } - - dialog = MaterialAlertDialogBuilder(activity) - .setTitle(R.string.home_configure_add_remove) - .setMultiChoiceItems(items.toTypedArray(), checkedItems.values.toBooleanArray()) { _, which, isChecked -> - if (isChecked) { - homeCards += HomeCardModel(profileId, ids[which]) - } - else { - homeCards.removeAll { it.profileId == profileId && it.cardId == ids[which] } - } - } - .setPositiveButton(R.string.ok) { dialog, _ -> dialog.dismiss() } - .setOnDismissListener { - profileConfig.homeCards = homeCards - onDismissListener?.invoke(TAG) - if (reloadOnDismiss) (activity as? MainActivity)?.reloadTarget() - } - .show() - }} -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginFormFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginFormFragment.kt deleted file mode 100644 index ea6bbe4d..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/login/LoginFormFragment.kt +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2020-4-16. - */ - -package pl.szczodrzynski.edziennik.ui.modules.login - -import android.annotation.SuppressLint -import android.os.Bundle -import android.text.InputType -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.core.view.isVisible -import androidx.core.widget.addTextChangedListener -import androidx.fragment.app.Fragment -import com.google.android.material.textfield.TextInputLayout -import com.mikepenz.iconics.IconicsDrawable -import com.mikepenz.iconics.utils.sizeDp -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.databinding.LoginFormCheckboxItemBinding -import pl.szczodrzynski.edziennik.databinding.LoginFormFieldItemBinding -import pl.szczodrzynski.edziennik.databinding.LoginFormFragmentBinding -import pl.szczodrzynski.navlib.colorAttr -import java.util.* -import kotlin.coroutines.CoroutineContext - -class LoginFormFragment : Fragment(), CoroutineScope { - companion object { - private const val TAG = "LoginFormFragment" - // eggs - var wantEggs = false - } - - private lateinit var app: App - private lateinit var activity: LoginActivity - private lateinit var b: LoginFormFragmentBinding - private val nav by lazy { activity.nav } - - private val job: Job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - // local/private variables go here - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - activity = (getActivity() as LoginActivity?) ?: return null - context ?: return null - app = activity.application as App - b = LoginFormFragmentBinding.inflate(inflater) - return b.root - } - - @SuppressLint("ResourceType") - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - if (!isAdded) return - b.backButton.onClick { nav.navigateUp() } - - b.errorLayout.isVisible = false - b.errorLayout.background?.setTintColor(R.attr.colorError.resolveAttr(activity)) - - val loginType = arguments?.getInt("loginType") ?: return - val register = LoginInfo.list.firstOrNull { it.loginType == loginType } ?: return - val loginMode = arguments?.getInt("loginMode") ?: return - val mode = register.loginModes.firstOrNull { it.loginMode == loginMode } ?: return - - val platformName = arguments?.getString("platformName") - val platformGuideText = arguments?.getString("platformGuideText") - val platformDescription = arguments?.getString("platformDescription") - val platformFormFields = arguments?.getString("platformFormFields")?.split(";") - val platformRealmData = arguments?.getString("platformRealmData")?.toJsonObject() - - b.title.setText(R.string.login_form_title_format, app.getString(register.registerName)) - b.subTitle.text = platformName ?: app.getString(mode.name) - b.text.text = platformGuideText ?: app.getString(mode.guideText) - - val credentials = mutableMapOf() - - for (credential in mode.credentials) { - if (platformFormFields?.contains(credential.keyName) == false) - continue - - if (credential is LoginInfo.FormField) { - val b = LoginFormFieldItemBinding.inflate(layoutInflater) - b.textLayout.hint = app.getString(credential.name) - if (credential.hideText) { - b.textEdit.inputType = InputType.TYPE_TEXT_VARIATION_PASSWORD - b.textLayout.endIconMode = TextInputLayout.END_ICON_PASSWORD_TOGGLE - } - b.textEdit.addTextChangedListener { - b.textLayout.error = null - } - - b.textEdit.id = credential.name - - b.textEdit.setText(arguments?.getString(credential.keyName) ?: "") - b.textLayout.startIconDrawable = IconicsDrawable(activity).apply { - icon = credential.icon - sizeDp = 24 - colorAttr(activity, R.attr.colorOnBackground) - } - - this.b.formContainer.addView(b.root) - credentials[credential] = b - } - if (credential is LoginInfo.FormCheckbox) { - val b = LoginFormCheckboxItemBinding.inflate(layoutInflater) - b.checkbox.text = app.getString(credential.name) - b.checkbox.onChange { _, isChecked -> - b.errorText.text = null - - // eggs - if (register.internalName == "podlasie") { - wantEggs = !isChecked - } - } - if (arguments?.containsKey(credential.keyName) == true) { - b.checkbox.isChecked = arguments?.getBoolean(credential.keyName) == true - } - - this.b.formContainer.addView(b.root) - credentials[credential] = b - } - } - - activity.lastError?.let { error -> - activity.lastError = null - startCoroutineTimer(delayMillis = 200L) { - for (credential in credentials) { - credential.key.errorCodes[error.errorCode]?.let { - (credential.value as? LoginFormFieldItemBinding)?.let { b -> - b.textLayout.error = app.getString(it) - } - (credential.value as? LoginFormCheckboxItemBinding)?.let { b -> - b.errorText.text = app.getString(it) - } - return@startCoroutineTimer - } - } - mode.errorCodes[error.errorCode]?.let { - b.errorText.text = app.getString(it) - b.errorLayout.isVisible = true - return@startCoroutineTimer - } - } - } - - b.loginButton.onClick { - val payload = Bundle( - "loginType" to loginType, - "loginMode" to loginMode - ) - - if (App.debugMode && b.fakeLogin.isChecked) { - payload.putBoolean("fakeLogin", true) - } - - payload.putBundle("webRealmData", platformRealmData?.toBundle()) - - var hasErrors = false - credentials.forEach { (credential, b) -> - if (credential is LoginInfo.FormField && b is LoginFormFieldItemBinding) { - var text = b.textEdit.text?.toString() ?: return@forEach - if (!credential.hideText) - text = text.trim() - - if (credential.caseMode == LoginInfo.FormField.CaseMode.UPPER_CASE) - text = text.toUpperCase(Locale.getDefault()) - if (credential.caseMode == LoginInfo.FormField.CaseMode.LOWER_CASE) - text = text.toLowerCase(Locale.getDefault()) - - credential.stripTextRegex?.let { - text = text.replace(it.toRegex(), "") - } - - b.textEdit.setText(text) - - if (credential.isRequired && text.isBlank()) { - b.textLayout.error = app.getString(credential.emptyText) - hasErrors = true - return@forEach - } - - if (!text.matches(credential.validationRegex.toRegex())) { - b.textLayout.error = app.getString(credential.invalidText) - hasErrors = true - return@forEach - } - - payload.putString(credential.keyName, text) - arguments?.putString(credential.keyName, text) - } - if (credential is LoginInfo.FormCheckbox && b is LoginFormCheckboxItemBinding) { - val checked = b.checkbox.isChecked - payload.putBoolean(credential.keyName, checked) - arguments?.putBoolean(credential.keyName, checked) - } - } - - if (hasErrors) - return@onClick - - nav.navigate(R.id.loginProgressFragment, payload, activity.navOptions) - } - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt deleted file mode 100644 index f8ccb491..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/MessagesAdapter.kt +++ /dev/null @@ -1,240 +0,0 @@ -package pl.szczodrzynski.edziennik.ui.modules.messages - -import android.graphics.Typeface -import android.text.Editable -import android.text.TextWatcher -import android.view.LayoutInflater -import android.view.ViewGroup -import android.widget.Filter -import android.widget.Filterable -import androidx.appcompat.app.AppCompatActivity -import androidx.recyclerview.widget.RecyclerView -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.cleanDiacritics -import pl.szczodrzynski.edziennik.data.db.entity.Message -import pl.szczodrzynski.edziennik.data.db.entity.Teacher -import pl.szczodrzynski.edziennik.data.db.full.MessageFull -import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder -import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch -import pl.szczodrzynski.edziennik.ui.modules.messages.viewholder.MessageViewHolder -import pl.szczodrzynski.edziennik.ui.modules.messages.viewholder.SearchViewHolder -import java.util.* -import kotlin.coroutines.CoroutineContext -import kotlin.math.min - -class MessagesAdapter( - val activity: AppCompatActivity, - val teachers: List, - val onItemClick: ((item: MessageFull) -> Unit)? = null -) : RecyclerView.Adapter(), CoroutineScope, Filterable { - companion object { - private const val TAG = "MessagesAdapter" - private const val ITEM_TYPE_MESSAGE = 0 - private const val ITEM_TYPE_SEARCH = 1 - } - - private val app = activity.applicationContext as App - // optional: place the manager here - - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - var items = mutableListOf() - var allItems = mutableListOf() - val typefaceNormal: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.NORMAL) } - val typefaceBold: Typeface by lazy { Typeface.create(Typeface.DEFAULT, Typeface.BOLD) } - private val comparator by lazy { Comparator { o1: Any, o2: Any -> - if (o1 !is MessageFull || o2 !is MessageFull) - return@Comparator 0 - when { - // standard sorting - o1.filterWeight > o2.filterWeight -> return@Comparator 1 - o1.filterWeight < o2.filterWeight -> return@Comparator -1 - else -> when { - // reversed sorting - o1.addedDate > o2.addedDate -> return@Comparator -1 - o1.addedDate < o2.addedDate -> return@Comparator 1 - else -> return@Comparator 0 - } - } - }} - - val textWatcher by lazy { - object : TextWatcher { - override fun afterTextChanged(s: Editable?) { - getFilter().filter(s.toString()) - } - override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} - override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) { - /*items.getOrNull(0)?.let { - if (it is MessagesSearch) { - it.searchText = s?.toString() ?: "" - } - }*/ - } - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - val inflater = LayoutInflater.from(parent.context) - return when (viewType) { - ITEM_TYPE_MESSAGE -> MessageViewHolder(inflater, parent) - ITEM_TYPE_SEARCH -> SearchViewHolder(inflater, parent) - else -> throw IllegalArgumentException("Incorrect viewType") - } - } - - override fun getItemViewType(position: Int): Int { - return when (items[position]) { - is MessageFull -> ITEM_TYPE_MESSAGE - is MessagesSearch -> ITEM_TYPE_SEARCH - else -> throw IllegalArgumentException("Incorrect viewType") - } - } - - @Suppress("DEPRECATION") - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - val item = items[position] - if (holder !is BindableViewHolder<*, *>) - return - - when { - holder is MessageViewHolder && item is MessageFull -> holder.onBind(activity, app, item, position, this) - holder is SearchViewHolder && item is MessagesSearch -> holder.onBind(activity, app, item, position, this) - } - } - - override fun getItemCount() = items.size - override fun getFilter() = filter - private var prevCount = -1 - private val filter by lazy { object : Filter() { - override fun performFiltering(prefix: CharSequence?): FilterResults { - val results = FilterResults() - - if (prevCount == -1) - prevCount = allItems.size - - if (prefix.isNullOrEmpty()) { - allItems.forEach { - if (it is MessageFull) - it.searchHighlightText = null - } - results.values = allItems.toList() - results.count = allItems.size - return results - } - - val items = mutableListOf() - val prefixString = prefix.toString() - - allItems.forEach { - if (it !is MessageFull) { - items.add(it) - return@forEach - } - it.filterWeight = 100 - it.searchHighlightText = null - - var weight: Int - if (it.type == Message.TYPE_SENT) { - it.recipients?.forEach { recipient -> - weight = getMatchWeight(recipient.fullName, prefixString) - if (weight != 100) { - if (weight == 3) - weight = 31 - it.filterWeight = min(it.filterWeight, 10 + weight) - } - } - } - else { - weight = getMatchWeight(it.senderName, prefixString) - if (weight != 100) { - if (weight == 3) - weight = 31 - it.filterWeight = min(it.filterWeight, 10 + weight) - } - } - - - weight = getMatchWeight(it.subject, prefixString) - if (weight != 100) { - if (weight == 3) - weight = 22 - it.filterWeight = min(it.filterWeight, 20 + weight) - } - - if (it.filterWeight != 100) { - it.searchHighlightText = prefixString - items.add(it) - } - } - - Collections.sort(items, comparator) - results.values = items - results.count = items.size - return results - } - - override fun publishResults(constraint: CharSequence?, results: FilterResults) { - results.values?.let { items = it as MutableList } - // do not re-bind the search box - val count = results.count - 1 - - // this tries to update every item except the search field - when { - count > prevCount -> { - notifyItemRangeInserted(prevCount + 1, count - prevCount) - notifyItemRangeChanged(1, prevCount) - } - count < prevCount -> { - notifyItemRangeRemoved(prevCount + 1, prevCount - count) - notifyItemRangeChanged(1, count) - } - else -> { - notifyItemRangeChanged(1, count) - } - } - - /*if (prevCount != count) { - items.getOrNull(0)?.let { - if (it is MessagesSearch) { - it.count = count - notifyItemChanged(0) - } - } - }*/ - - prevCount = count - } - }} - - private fun getMatchWeight(name: CharSequence?, prefix: String): Int { - if (name == null) - return 100 - - val nameClean = name.cleanDiacritics() - - // First match against the whole, non-split value - if (nameClean.startsWith(prefix, ignoreCase = true) || name.startsWith(prefix, ignoreCase = true)) { - return 1 - } else { - // check if prefix matches any of the words - val words = nameClean.split(" ").toTypedArray() + name.split(" ").toTypedArray() - for (word in words) { - if (word.startsWith(prefix, ignoreCase = true)) { - return 2 - } - } - } - // finally check if the prefix matches any part of the name - if (nameClean.contains(prefix, ignoreCase = true) || name.contains(prefix, ignoreCase = true)) { - return 3 - } - - return 100 - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt deleted file mode 100644 index 942876f2..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/compose/MessagesComposeFragment.kt +++ /dev/null @@ -1,513 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2020-4-4. - */ - -package pl.szczodrzynski.edziennik.ui.modules.messages.compose - -import android.content.Context -import android.graphics.Typeface -import android.graphics.drawable.BitmapDrawable -import android.os.Bundle -import android.text.Spannable -import android.text.SpannableString -import android.text.SpannableStringBuilder -import android.text.style.AbsoluteSizeSpan -import android.text.style.ForegroundColorSpan -import android.text.style.StyleSpan -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.AutoCompleteTextView -import android.widget.Toast -import androidx.core.text.HtmlCompat -import androidx.core.widget.addTextChangedListener -import androidx.fragment.app.Fragment -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.hootsuite.nachos.ChipConfiguration -import com.hootsuite.nachos.chip.ChipInfo -import com.hootsuite.nachos.chip.ChipSpan -import com.hootsuite.nachos.chip.ChipSpanChipCreator -import com.hootsuite.nachos.tokenizer.SpanChipTokenizer -import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial -import kotlinx.coroutines.* -import org.greenrobot.eventbus.EventBus -import org.greenrobot.eventbus.Subscribe -import org.greenrobot.eventbus.ThreadMode -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.api.ERROR_MESSAGE_NOT_SENT -import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask -import pl.szczodrzynski.edziennik.data.api.events.MessageSentEvent -import pl.szczodrzynski.edziennik.data.api.events.RecipientListGetEvent -import pl.szczodrzynski.edziennik.data.api.models.ApiError -import pl.szczodrzynski.edziennik.data.db.entity.LoginStore -import pl.szczodrzynski.edziennik.data.db.entity.Teacher -import pl.szczodrzynski.edziennik.data.db.full.MessageFull -import pl.szczodrzynski.edziennik.databinding.MessagesComposeFragmentBinding -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils.getProfileImage -import pl.szczodrzynski.edziennik.utils.Colors -import pl.szczodrzynski.edziennik.utils.Themes -import pl.szczodrzynski.edziennik.utils.Themes.getPrimaryTextColor -import pl.szczodrzynski.edziennik.utils.models.Date -import pl.szczodrzynski.edziennik.utils.models.Time -import pl.szczodrzynski.navlib.elevateSurface -import kotlin.coroutines.CoroutineContext -import kotlin.text.replace - -class MessagesComposeFragment : Fragment(), CoroutineScope { - companion object { - private const val TAG = "MessagesComposeFragment" - } - - private lateinit var app: App - private lateinit var activity: MainActivity - private lateinit var b: MessagesComposeFragmentBinding - - private val job: Job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - private var teachers = mutableListOf() - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - activity = (getActivity() as MainActivity?) ?: return null - context ?: return null - app = activity.application as App - context!!.theme.applyStyle(Themes.appTheme, true) - // activity, context and profile is valid - b = MessagesComposeFragmentBinding.inflate(inflater) - return b.root - } - override fun onDestroy() { - EventBus.getDefault().unregister(this) - super.onDestroy() - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - // TODO check if app, activity, b can be null - if (!isAdded) - return - - EventBus.getDefault().register(this) - - b.breakpoints.visibility = if (App.devMode) View.VISIBLE else View.GONE - b.breakpoints.setOnClickListener { - b.breakpoints.isEnabled = true - b.breakpoints.text = "Breakpoints!" - // do your job - } - - /*b.fontStyleBold.icon = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_format_bold) - .sizeDp(24) - .colorAttr(activity, R.attr.colorOnPrimary) - b.fontStyleItalic.icon = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_format_italic) - .sizeDp(24) - .colorAttr(activity, R.attr.colorOnPrimary) - b.fontStyleUnderline.icon = IconicsDrawable(activity, CommunityMaterial.Icon.cmd_format_underline) - .sizeDp(24) - .colorAttr(activity, R.attr.colorOnPrimary) - - b.fontStyle.addOnButtonCheckedListener { _, checkedId, isChecked -> - val span: Any = when (checkedId) { - R.id.fontStyleBold -> StyleSpan(Typeface.BOLD) - R.id.fontStyleItalic -> StyleSpan(Typeface.ITALIC) - R.id.fontStyleUnderline -> UnderlineSpan() - else -> StyleSpan(Typeface.NORMAL) - } - - if (isChecked) { - val flags = if (b.text.selectionStart == b.text.selectionEnd) - SpannableString.SPAN_INCLUSIVE_INCLUSIVE - else - SpannableString.SPAN_EXCLUSIVE_INCLUSIVE - b.text.text?.setSpan(span, b.text.selectionStart, b.text.selectionEnd, flags) - } - else { - b.text.text?.getSpans(b.text.selectionStart, b.text.selectionEnd, span.javaClass)?.forEach { - if (it is StyleSpan && span is StyleSpan && it.style == span.style) - b.text.text?.removeSpan(it) - else if (it.javaClass == span.javaClass) - b.text.text?.removeSpan(it) - } - } - }*/ - - launch { - delay(100) - getRecipientList() - - createView() - } - } - - private fun getRecipientList() { - if (System.currentTimeMillis() - app.profile.lastReceiversSync > 1 * DAY * 1000 && app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN) { - activity.snackbar("Pobieranie listy odbiorców...") - EdziennikTask.recipientListGet(App.profileId).enqueue(activity) - } - else { - launch { - val list = withContext(Dispatchers.Default) { - app.db.teacherDao().getAllNow(App.profileId).filter { it.loginId != null } - } - updateRecipientList(list) - } - } - } - - private fun createView() { - b.recipientsLayout.setBoxCornerRadii(0f, 0f, 0f, 0f) - b.subjectLayout.setBoxCornerRadii(0f, 0f, 0f, 0f) - b.textLayout.setBoxCornerRadii(0f, 0f, 0f, 0f) - - b.recipients.addTextChangedListener(onTextChanged = { _, _, _, _ -> - b.recipientsLayout.error = null - }) - b.subject.addTextChangedListener(onTextChanged = { _, _, _, _ -> - b.subjectLayout.error = null - }) - b.text.addTextChangedListener(onTextChanged = { _, _, _, _ -> - b.textLayout.error = null - }) - - b.subjectLayout.counterMaxLength = when (app.profile.loginStoreType) { - LoginStore.LOGIN_TYPE_MOBIDZIENNIK -> 100 - LoginStore.LOGIN_TYPE_LIBRUS -> 150 - LoginStore.LOGIN_TYPE_VULCAN -> 200 - LoginStore.LOGIN_TYPE_IDZIENNIK -> 180 - LoginStore.LOGIN_TYPE_EDUDZIENNIK -> 0 - else -> -1 - } - b.textLayout.counterMaxLength = when (app.profile.loginStoreType) { - LoginStore.LOGIN_TYPE_MOBIDZIENNIK -> -1 - LoginStore.LOGIN_TYPE_LIBRUS -> 20000 - LoginStore.LOGIN_TYPE_VULCAN -> -1 - LoginStore.LOGIN_TYPE_IDZIENNIK -> 1983 - LoginStore.LOGIN_TYPE_EDUDZIENNIK -> 0 - else -> -1 - } - - b.recipients.chipTokenizer = SpanChipTokenizer(activity, object : ChipSpanChipCreator() { - override fun createChip(context: Context, text: CharSequence, data: Any?): ChipSpan? { - if (data == null || data !is Teacher) - return null - if (data.id !in -24L..0L) { - b.recipients.allChips.forEach { - if (it.data == data) { - Toast.makeText(activity, R.string.messages_compose_recipient_exists, Toast.LENGTH_SHORT).show() - return null - } - } - val chipSpan = ChipSpan(context, data.fullName, BitmapDrawable(context.resources, data.image), data) - chipSpan.setIconBackgroundColor(Colors.stringToMaterialColor(data.fullName)) - return chipSpan - } - - val type = (data.id * -1).toInt() - - val textColorPrimary = android.R.attr.textColorPrimary.resolveAttr(activity) - val textColorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity) - - val sortByCategory = type in listOf( - Teacher.TYPE_PARENTS_COUNCIL, - Teacher.TYPE_EDUCATOR, - Teacher.TYPE_STUDENT - ) - val teachers = if (sortByCategory) - teachers.sortedBy { it.typeDescription } - else - teachers - - val category = mutableListOf() - val categoryNames = mutableListOf() - val categoryCheckedItems = mutableListOf() - teachers.forEach { teacher -> - if (!teacher.isType(type)) - return@forEach - - category += teacher - val name = teacher.fullName - val description = when (type) { - Teacher.TYPE_TEACHER -> null - Teacher.TYPE_PARENTS_COUNCIL -> teacher.typeDescription - Teacher.TYPE_SCHOOL_PARENTS_COUNCIL -> null - Teacher.TYPE_PEDAGOGUE -> null - Teacher.TYPE_LIBRARIAN -> null - Teacher.TYPE_SCHOOL_ADMIN -> null - Teacher.TYPE_SUPER_ADMIN -> null - Teacher.TYPE_SECRETARIAT -> null - Teacher.TYPE_PRINCIPAL -> null - Teacher.TYPE_EDUCATOR -> teacher.typeDescription - Teacher.TYPE_PARENT -> teacher.typeDescription - Teacher.TYPE_STUDENT -> teacher.typeDescription - Teacher.TYPE_SPECIALIST -> null - else -> teacher.typeDescription - } - categoryNames += listOfNotNull( - name.asSpannable( - ForegroundColorSpan(textColorPrimary) - ), - description?.asSpannable( - ForegroundColorSpan(textColorSecondary), - AbsoluteSizeSpan(14.dp) - ) - ).concat("\n") - - // check the teacher if already added as a recipient - categoryCheckedItems += b.recipients.allChips.firstOrNull { it.data == teacher } != null - } - - MaterialAlertDialogBuilder(activity) - .setTitle("Dodaj odbiorców - "+ Teacher.typeName(activity, type)) - //.setMessage(getString(R.string.messages_compose_recipients_text_format, Teacher.typeName(activity, type))) - .setPositiveButton("OK", null) - .setNeutralButton("Anuluj", null) - .setMultiChoiceItems(categoryNames.toTypedArray(), categoryCheckedItems.toBooleanArray()) { _, which, isChecked -> - val teacher = category[which] - if (isChecked) { - val chipInfoList = mutableListOf() - teacher.image = getProfileImage(48, 24, 16, 12, 1, teacher.fullName) - chipInfoList.add(ChipInfo(teacher.fullName, teacher)) - b.recipients.addTextWithChips(chipInfoList) - } - else { - b.recipients.allChips.forEach { - if (it.data == teacher) - b.recipients.chipTokenizer?.deleteChipAndPadding(it, b.recipients.text) - } - } - } - .show() - return null - } - - override fun configureChip(chip: ChipSpan, chipConfiguration: ChipConfiguration) { - super.configureChip(chip, chipConfiguration) - chip.setBackgroundColor(elevateSurface(activity, 8).toColorStateList()) - chip.setTextColor(getPrimaryTextColor(activity)) - } - }, ChipSpan::class.java) - - b.recipients.setIllegalCharacterIdentifier { c -> - c.toString().matches("[\\n;:_ ]".toRegex()) - } - b.recipients.setOnChipRemoveListener { _ -> - b.recipients.setSelection(b.recipients.text.length) - } - - b.recipients.addTextChangedListener( beforeTextChanged = { _, _, _, _ -> - b.recipients.ignoreThreshold = false - }) - b.recipients.onDismissListener = AutoCompleteTextView.OnDismissListener { - b.recipients.ignoreThreshold = false - } - b.recipientsLayout.setEndIconOnClickListener { - b.recipients.error = null - b.recipients.ignoreThreshold = true - b.recipients.showDropDown() - val adapter = b.recipients.adapter ?: return@setEndIconOnClickListener - if (adapter is MessagesComposeSuggestionAdapter) - adapter.filter.filter(null) - } - - b.recipientsLayout.isEnabled = false - b.subjectLayout.isEnabled = false - b.textLayout.isEnabled = false - - activity.navView.bottomBar.apply { - fabEnable = true - fabExtendedText = getString(R.string.messages_compose_send) - fabIcon = CommunityMaterial.Icon3.cmd_send_outline - - setFabOnClickListener(View.OnClickListener { - sendMessage() - }) - } - - activity.gainAttentionFAB() - } - - private fun updateRecipientList(list: List) { launch { - withContext(Dispatchers.Default) { - teachers = list.sortedBy { it.fullName }.toMutableList() - Teacher.types.mapTo(teachers) { - Teacher(-1, -it.toLong(), Teacher.typeName(activity, it), "") - } - /*teachers.forEach { - println(it) - }*/ - } - - b.recipientsLayout.isEnabled = true - b.subjectLayout.isEnabled = true - b.textLayout.isEnabled = true - - val adapter = MessagesComposeSuggestionAdapter(activity, teachers) - b.recipients.setAdapter(adapter) - - handleReplyMessage() - }} - - private fun handleReplyMessage() { launch { - val replyMessage = arguments?.getString("message") - if (replyMessage != null) { - val chipList = mutableListOf() - var subject = "" - val span = SpannableStringBuilder() - var body: CharSequence = "" - - withContext(Dispatchers.Default) { - val msg = app.gson.fromJson(replyMessage, MessageFull::class.java) - val dateString = getString(R.string.messages_date_time_format, Date.fromMillis(msg.addedDate).formattedStringShort, Time.fromMillis(msg.addedDate).stringHM) - // add original message info - span.appendText("W dniu ") - span.appendSpan(dateString, StyleSpan(Typeface.ITALIC), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - span.appendText(", ") - span.appendSpan(msg.senderName.fixName(), StyleSpan(Typeface.ITALIC), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - span.appendText(" napisał(a):") - span.setSpan(StyleSpan(Typeface.BOLD), 0, span.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) - span.appendText("\n\n") - - if (arguments?.getString("type") == "reply") { - // add greeting text - span.replace(0, 0, "\n\nZ poważaniem,\n${app.profile.accountName - ?: app.profile.studentNameLong ?: ""}\n\n\n") - - teachers.firstOrNull { it.id == msg.senderId }?.let { teacher -> - teacher.image = getProfileImage(48, 24, 16, 12, 1, teacher.fullName) - chipList += ChipInfo(teacher.fullName, teacher) - } - subject = "Re: ${msg.subject}" - } else { - span.replace(0, 0, "\n\n") - subject = "Fwd: ${msg.subject}" - } - body = MessagesUtils.htmlToSpannable(activity, msg.body - ?: "Nie udało się wczytać oryginalnej wiadomości.")//Html.fromHtml(msg.body?.replace("".toRegex(), "\n") ?: "Nie udało się wczytać oryginalnej wiadomości.") - } - - b.recipients.addTextWithChips(chipList) - if (b.recipients.text.isNullOrEmpty()) - b.recipients.requestFocus() - else - b.text.requestFocus() - b.subject.setText(subject) - b.text.apply { - text = span.appendText(body) - setSelection(0) - } - b.root.scrollTo(0, 0) - } - else { - b.recipients.requestFocus() - } - }} - - private fun sendMessage() { - b.recipientsLayout.error = null - b.subjectLayout.error = null - b.textLayout.error = null - - if (b.recipients.tokenValues.isNotEmpty()) { - b.recipientsLayout.error = getString(R.string.messages_compose_recipients_error) - return - } - val recipients = mutableListOf() - b.recipients.allChips.forEach { chip -> - if (chip.data !is Teacher) - return@forEach - val teacher = chip.data as Teacher - - recipients += teacher - //println(teacher) - } - val subject = b.subject.text?.toString() - val text = b.text.text - if (recipients.isEmpty()) { - b.recipientsLayout.error = getString(R.string.messages_compose_recipients_empty) - return - } - if (subject.isNullOrBlank() || subject.length < 3) { - b.subjectLayout.error = getString(R.string.messages_compose_subject_empty) - return - } - if (text.isNullOrBlank() || text.length < 3) { - b.textLayout.error = getString(R.string.messages_compose_text_empty) - return - } - - // do magic - // apparently this removes an underline - // span from the text where the caret is - b.subject.requestFocus() - b.subject.clearFocus() - activity.navView.bottomSheet.hideKeyboard() - b.text.clearFocus() - b.text.setSelection(0) - - if (b.subjectLayout.counterMaxLength != -1 && b.subject.length() > b.subjectLayout.counterMaxLength) - return - if (b.textLayout.counterMaxLength != -1 && b.text.length() > b.textLayout.counterMaxLength) - return - - var textHtml = if (app.profile.loginStoreType != LoginStore.LOGIN_TYPE_VULCAN) { - HtmlCompat.toHtml(SpannableString(text), HtmlCompat.TO_HTML_PARAGRAPH_LINES_INDIVIDUAL) - .replace("\n", "") - .replace(" dir=\"ltr\"", "") - } - else { - text.toString() - } - - if (app.profile.loginStoreType == LoginStore.LOGIN_TYPE_MOBIDZIENNIK) { - textHtml = textHtml - .replace("p style=\"margin-top:0; margin-bottom:0;\"", "span") - .replace("

    ", "") - .replace("", "") - .replace("", "") - .replace("", "") - .replace("", "") - .replace("", "") - .replace("", "") - } - - activity.bottomSheet.hideKeyboard() - - MaterialAlertDialogBuilder(activity) - .setTitle(R.string.messages_compose_confirm_title) - .setMessage(R.string.messages_compose_confirm_text) - .setPositiveButton(R.string.send) { _, _ -> - EdziennikTask.messageSend(App.profileId, recipients, subject.trim(), textHtml.trim()).enqueue(activity) - } - .setNegativeButton(R.string.cancel, null) - .show() - } - - @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) - fun onRecipientListGetEvent(event: RecipientListGetEvent) { - if (event.profileId != App.profileId) - return - EventBus.getDefault().removeStickyEvent(event) - - activity.snackbarDismiss() - updateRecipientList(event.teacherList) - } - - @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) - fun onMessageSentEvent(event: MessageSentEvent) { - if (event.profileId != App.profileId) - return - EventBus.getDefault().removeStickyEvent(event) - - if (event.message == null) { - activity.error(ApiError(TAG, ERROR_MESSAGE_NOT_SENT)) - return - } - - activity.snackbar(app.getString(R.string.messages_sent_success), app.getString(R.string.ok)) - activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, Bundle( - "messageId" to event.message.id, - "message" to app.gson.toJson(event.message), - "sentDate" to event.sentDate - )) - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt deleted file mode 100644 index ad206c62..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/models/MessagesSearch.kt +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2020-4-5. - */ - -package pl.szczodrzynski.edziennik.ui.modules.messages.models - -class MessagesSearch { - var isFocused = false - var searchText = "" - var count = 0 -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt deleted file mode 100644 index 62789a0c..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/MessageViewHolder.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2020-4-5. - */ - -package pl.szczodrzynski.edziennik.ui.modules.messages.viewholder - -import android.graphics.Typeface -import android.text.style.BackgroundColorSpan -import android.text.style.StyleSpan -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.isVisible -import androidx.recyclerview.widget.RecyclerView -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.data.db.entity.Message -import pl.szczodrzynski.edziennik.data.db.full.MessageFull -import pl.szczodrzynski.edziennik.databinding.MessagesListItemBinding -import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesAdapter -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesUtils -import pl.szczodrzynski.edziennik.utils.models.Date - -class MessageViewHolder( - inflater: LayoutInflater, - parent: ViewGroup, - val b: MessagesListItemBinding = MessagesListItemBinding.inflate(inflater, parent, false) -) : RecyclerView.ViewHolder(b.root), BindableViewHolder { - companion object { - private const val TAG = "MessageViewHolder" - } - - override fun onBind(activity: AppCompatActivity, app: App, item: MessageFull, position: Int, adapter: MessagesAdapter) { - val manager = app.gradesManager - - b.messageSubject.text = item.subject - b.messageDate.text = Date.fromMillis(item.addedDate).formattedStringShort - b.messageAttachmentImage.isVisible = item.hasAttachments - - val text = item.body?.take(200) ?: "" - b.messageBody.text = MessagesUtils.htmlToSpannable(activity, text) - - val isRead = item.type == Message.TYPE_SENT || item.type == Message.TYPE_DRAFT || 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 - // set text styles - b.messageSender.setTextAppearance(activity, style) - b.messageSender.typeface = typeface - b.messageSubject.setTextAppearance(activity, style) - b.messageSubject.typeface = typeface - b.messageDate.setTextAppearance(activity, style) - b.messageDate.typeface = typeface - - val messageInfo = MessagesUtils.getMessageInfo(app, item, 48, 24, 18, 12) - b.messageProfileBackground.setImageBitmap(messageInfo.profileImage) - b.messageSender.text = messageInfo.profileName - - item.searchHighlightText?.let { highlight -> - val colorHighlight = R.attr.colorControlHighlight.resolveAttr(activity) - - b.messageSubject.text = b.messageSubject.text.asSpannable( - StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight), - substring = highlight, ignoreCase = true, ignoreDiacritics = true) - b.messageSender.text = b.messageSender.text.asSpannable( - StyleSpan(Typeface.BOLD), BackgroundColorSpan(colorHighlight), - substring = highlight, ignoreCase = true, ignoreDiacritics = true) - } - - adapter.onItemClick?.let { listener -> - b.root.onClick { listener(item) } - } - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt deleted file mode 100644 index 5adb1984..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/messages/viewholder/SearchViewHolder.kt +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2020-4-5. - */ - -package pl.szczodrzynski.edziennik.ui.modules.messages.viewholder - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.appcompat.app.AppCompatActivity -import androidx.recyclerview.widget.RecyclerView -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.databinding.MessagesListItemSearchBinding -import pl.szczodrzynski.edziennik.ui.modules.grades.viewholder.BindableViewHolder -import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesAdapter -import pl.szczodrzynski.edziennik.ui.modules.messages.models.MessagesSearch - -class SearchViewHolder( - inflater: LayoutInflater, - parent: ViewGroup, - val b: MessagesListItemSearchBinding = MessagesListItemSearchBinding.inflate(inflater, parent, false) -) : RecyclerView.ViewHolder(b.root), BindableViewHolder { - companion object { - private const val TAG = "SearchViewHolder" - } - - override fun onBind(activity: AppCompatActivity, app: App, item: MessagesSearch, position: Int, adapter: MessagesAdapter) { - b.searchEdit.removeTextChangedListener(adapter.textWatcher) - b.searchEdit.addTextChangedListener(adapter.textWatcher) - - /*b.searchEdit.setOnKeyboardListener(object : TextInputKeyboardEdit.KeyboardListener { - override fun onStateChanged(keyboardEditText: TextInputKeyboardEdit, showing: Boolean) { - item.isFocused = showing - } - })*/ - - /*if (b.searchEdit.text.toString() != item.searchText) { - b.searchEdit.setText(item.searchText) - b.searchEdit.setSelection(item.searchText.length) - }*/ - - //b.searchLayout.helperText = app.getString(R.string.messages_search_results, item.count) - - /*if (item.isFocused && !b.searchEdit.isFocused) - b.searchEdit.requestFocus()*/ - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt deleted file mode 100644 index 36239083..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsRegisterCard.kt +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2021-3-18. - */ - -package pl.szczodrzynski.edziennik.ui.modules.settings.cards - -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial -import eu.szkolny.font.SzkolnyFont -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.after -import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_LIBRUS -import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.REGISTRATION_ENABLED -import pl.szczodrzynski.edziennik.ui.dialogs.bell.BellSyncConfigDialog -import pl.szczodrzynski.edziennik.ui.dialogs.grade.GradesConfigDialog -import pl.szczodrzynski.edziennik.ui.dialogs.settings.AttendanceConfigDialog -import pl.szczodrzynski.edziennik.ui.dialogs.sync.RegistrationConfigDialog -import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsCard -import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsUtil - -class SettingsRegisterCard(util: SettingsUtil) : SettingsCard(util) { - - override fun buildCard() = util.createCard( - R.string.settings_card_register_title, - items = getItems(), - itemsMore = getItemsMore() - ) - - private fun getBellSync() = - configGlobal.timetable.bellSyncDiff?.let { - activity.getString( - R.string.settings_register_bell_sync_subtext_format, - (if (configGlobal.timetable.bellSyncMultiplier == -1) "-" else "+") + it.stringHMS - ) - } ?: activity.getString(R.string.settings_register_bell_sync_subtext_disabled) - - private val sharedEventsItem by lazy { - util.createPropertyItem( - text = R.string.settings_register_shared_events_text, - subText = R.string.settings_register_shared_events_subtext, - icon = CommunityMaterial.Icon3.cmd_share_outline, - value = app.profile.enableSharedEvents - ) { _, value -> - app.profile.enableSharedEvents = value - app.profileSave() - MaterialAlertDialogBuilder(activity) - .setTitle(R.string.event_sharing) - .setMessage( - if (value) - R.string.settings_register_shared_events_dialog_enabled_text - else - R.string.settings_register_shared_events_dialog_disabled_text - ) - .setPositiveButton(R.string.ok, null) - .show() - } - } - - override fun getItems() = listOfNotNull( - util.createActionItem( - text = R.string.menu_grades_config, - icon = CommunityMaterial.Icon3.cmd_numeric_5_box_outline - ) { - GradesConfigDialog(activity, reloadOnDismiss = false) - }, - - util.createActionItem( - text = R.string.menu_attendance_config, - icon = CommunityMaterial.Icon.cmd_calendar_remove_outline - ) { - AttendanceConfigDialog(activity, reloadOnDismiss = false) - }, - - util.createPropertyItem( - text = R.string.settings_register_allow_registration_text, - subText = R.string.settings_register_allow_registration_subtext, - icon = CommunityMaterial.Icon.cmd_account_circle_outline, - value = app.profile.registration == REGISTRATION_ENABLED, - beforeChange = { item, value -> - if (app.profile.registration == REGISTRATION_ENABLED == value) - // allow the switch to change - needed for util.refresh() to change the visual state - return@createPropertyItem true - val dialog = - RegistrationConfigDialog(activity, app.profile, onChangeListener = { enabled -> - if (item.isChecked == enabled) - return@RegistrationConfigDialog - item.isChecked = enabled - if (value) { - card.items.after(item, sharedEventsItem) - } else { - card.items.remove(sharedEventsItem) - } - util.refresh() - }) - if (value) - dialog.showEnableDialog() - else - dialog.showDisableDialog() - false - } - ) { _, _ -> }, - - if (app.profile.registration == REGISTRATION_ENABLED) - sharedEventsItem - else - null - ) - - override fun getItemsMore() = listOfNotNull( - util.createActionItem( - text = R.string.settings_register_bell_sync_text, - icon = SzkolnyFont.Icon.szf_alarm_bell_outline, - onClick = { - BellSyncConfigDialog(activity, onChangeListener = { - it.subText = getBellSync() - util.refresh() - }) - } - ).also { - it.subText = getBellSync() - }, - - util.createPropertyItem( - text = R.string.settings_register_count_in_seconds_text, - subText = R.string.settings_register_count_in_seconds_subtext, - icon = CommunityMaterial.Icon3.cmd_timer_outline, - value = configGlobal.timetable.countInSeconds - ) { _, it -> - configGlobal.timetable.countInSeconds = it - }, - - if (app.profile.loginStoreType == LOGIN_TYPE_LIBRUS) - util.createPropertyItem( - text = R.string.settings_register_show_teacher_absences_text, - icon = CommunityMaterial.Icon.cmd_account_arrow_right_outline, - value = app.profile.getStudentData("showTeacherAbsences", true) - ) { _, it -> - app.profile.putStudentData("showTeacherAbsences", it) - app.profileSave() - } - else - null, - - if (App.devMode) - util.createPropertyItem( - text = R.string.settings_register_hide_sticks_from_old, - icon = CommunityMaterial.Icon3.cmd_numeric_1_box_outline, - value = configProfile.grades.hideSticksFromOld - ) { _, it -> - configProfile.grades.hideSticksFromOld = it - } - else - null - ) -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateDialog.kt deleted file mode 100644 index e5d68ebe..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateDialog.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) Kuba Szczodrzyński 2020-3-30. - */ - -package pl.szczodrzynski.edziennik.ui.modules.template - -import androidx.appcompat.app.AlertDialog -import androidx.appcompat.app.AppCompatActivity -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.App -import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.databinding.DialogTemplateBinding -import pl.szczodrzynski.edziennik.onClick -import kotlin.coroutines.CoroutineContext - -class TemplateDialog( - val activity: AppCompatActivity, - val onActionPerformed: (() -> Unit)? = null, - val onShowListener: ((tag: String) -> Unit)? = null, - val onDismissListener: ((tag: String) -> Unit)? = null -) : CoroutineScope { - companion object { - private const val TAG = "TemplateDialog" - } - - private lateinit var app: App - private lateinit var b: DialogTemplateBinding - private lateinit var dialog: AlertDialog - - private val job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - // local variables go here - - init { run { - if (activity.isFinishing) - return@run - onShowListener?.invoke(TAG) - app = activity.applicationContext as App - b = DialogTemplateBinding.inflate(activity.layoutInflater) - dialog = MaterialAlertDialogBuilder(activity) - .setView(b.root) - .setPositiveButton(R.string.close) { dialog, _ -> - dialog.dismiss() - } - .setNeutralButton(R.string.add, null) - .setOnDismissListener { - onDismissListener?.invoke(TAG) - } - .show() - - dialog.getButton(AlertDialog.BUTTON_NEUTRAL)?.onClick { - // do custom action on neutral button click - // (does not dismiss the dialog) - } - - b.clickMe.onClick { - onActionPerformed?.invoke() - dialog.dismiss() - } - }} -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt deleted file mode 100644 index e6464d7d..00000000 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableDayFragment.kt +++ /dev/null @@ -1,305 +0,0 @@ -/* - * Copyright (c) Kacper Ziubryniewicz 2020-1-6 - */ - -package pl.szczodrzynski.edziennik.ui.modules.timetable - -import android.os.Bundle -import android.view.Gravity -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.ProgressBar -import android.widget.TextView -import androidx.asynclayoutinflater.view.AsyncLayoutInflater -import androidx.core.view.setPadding -import androidx.lifecycle.Observer -import com.linkedin.android.tachyon.DayView -import com.linkedin.android.tachyon.DayViewConfig -import kotlinx.coroutines.* -import pl.szczodrzynski.edziennik.* -import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_TIMETABLE -import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask -import pl.szczodrzynski.edziennik.data.db.entity.Lesson -import pl.szczodrzynski.edziennik.data.db.full.EventFull -import pl.szczodrzynski.edziennik.data.db.full.LessonFull -import pl.szczodrzynski.edziennik.databinding.TimetableLessonBinding -import pl.szczodrzynski.edziennik.databinding.TimetableNoTimetableBinding -import pl.szczodrzynski.edziennik.ui.dialogs.timetable.LessonDetailsDialog -import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment -import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment.Companion.DEFAULT_END_HOUR -import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment.Companion.DEFAULT_START_HOUR -import pl.szczodrzynski.edziennik.utils.ListenerScrollView -import pl.szczodrzynski.edziennik.utils.models.Date -import java.util.* -import kotlin.coroutines.CoroutineContext -import kotlin.math.min - -class TimetableDayFragment : LazyFragment(), CoroutineScope { - companion object { - private const val TAG = "TimetableDayFragment" - } - - private lateinit var app: App - private lateinit var activity: MainActivity - private lateinit var inflater: AsyncLayoutInflater - - private val job: Job = Job() - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - private lateinit var date: Date - private var startHour = DEFAULT_START_HOUR - private var endHour = DEFAULT_END_HOUR - private var firstEventMinute = 24 * 60 - - private val manager by lazy { app.timetableManager } - - // find SwipeRefreshLayout in the hierarchy - private val refreshLayout by lazy { view?.findParentById(R.id.refreshLayout) } - // the day ScrollView - private val dayScrollDelegate = lazy { - val dayScroll = ListenerScrollView(context!!) - dayScroll.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) - dayScroll.setOnRefreshLayoutEnabledListener { enabled -> - refreshLayout?.isEnabled = enabled - } - dayScroll - } - private val dayScroll by dayScrollDelegate - // the lesson DayView - private val dayView by lazy { - val dayView = DayView(context!!, DayViewConfig( - startHour = startHour, - endHour = endHour, - dividerHeight = 1.dp, - halfHourHeight = 60.dp, - hourDividerColor = R.attr.hourDividerColor.resolveAttr(context), - halfHourDividerColor = R.attr.halfHourDividerColor.resolveAttr(context), - hourLabelWidth = 40.dp, - hourLabelMarginEnd = 10.dp, - eventMargin = 2.dp - ), true) - dayView.setPadding(10.dp) - dayScroll.layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) - dayScroll.addView(dayView) - dayView - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - activity = (getActivity() as MainActivity?) ?: return null - context ?: return null - app = activity.application as App - this.inflater = AsyncLayoutInflater(context!!) - date = arguments?.getInt("date")?.let { Date.fromValue(it) } ?: Date.getToday() - startHour = arguments?.getInt("startHour") ?: DEFAULT_START_HOUR - endHour = arguments?.getInt("endHour") ?: DEFAULT_END_HOUR - return FrameLayout(activity).apply { - layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT) - addView(ProgressBar(activity).apply { - layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER) - }) - } - } - - override fun onPageCreated(): Boolean { - // observe lesson database - app.db.timetableDao().getAllForDate(App.profileId, date).observe(this, Observer { lessons -> - launch { - val events = withContext(Dispatchers.Default) { - app.db.eventDao().getAllByDateNow(App.profileId, date) - } - processLessonList(lessons, events) - } - }) - - return true - } - - private fun processLessonList(lessons: List, events: List) { - // no lessons - timetable not downloaded yet - if (lessons.isEmpty()) { - inflater.inflate(R.layout.timetable_no_timetable, view as FrameLayout?) { view, _, parent -> - parent?.removeAllViews() - parent?.addView(view) - val b = TimetableNoTimetableBinding.bind(view) - val weekStart = date.weekStart.stringY_m_d - b.noTimetableSync.onClick { - it.isEnabled = false - EdziennikTask.syncProfile( - profileId = App.profileId, - viewIds = listOf( - DRAWER_ITEM_TIMETABLE to 0 - ), - arguments = JsonObject( - "weekStart" to weekStart - ) - ).enqueue(activity) - } - b.noTimetableWeek.setText(R.string.timetable_no_timetable_week, weekStart) - } - return - } - // one lesson indicating a day without lessons - if (lessons.size == 1 && lessons[0].type == Lesson.TYPE_NO_LESSONS) { - inflater.inflate(R.layout.timetable_no_lessons, view as FrameLayout?) { view, _, parent -> - parent?.removeAllViews() - parent?.addView(view) - } - return - } - - // reload the fragment when: no lessons, user wants to sync the week, the timetable is not public, pager gets removed - if (app.profile.getStudentData("timetableNotPublic", false)) { - activity.reloadTarget() - // TODO fix for (not really)possible infinite loops - return - } - - // clear the root view and add the ScrollView - (view as FrameLayout?)?.removeAllViews() - (view as FrameLayout?)?.addView(dayScroll) - - // Inflate a label view for each hour the day view will display - val hourLabelViews = ArrayList() - for (i in dayView.startHour..dayView.endHour) { - if (!isAdded) - continue - val hourLabelView = layoutInflater.inflate(R.layout.timetable_hour_label, dayView, false) as TextView - hourLabelView.text = "$i:00" - hourLabelViews.add(hourLabelView) - } - dayView.setHourLabelViews(hourLabelViews) - - lessons.forEach { it.showAsUnseen = !it.seen } - - buildLessonViews(lessons.filter { it.type != Lesson.TYPE_NO_LESSONS }, events) - } - - private fun buildLessonViews(lessons: List, events: List) { - if (!isAdded) - return - - val eventViews = mutableListOf() - val eventTimeRanges = mutableListOf() - - // Reclaim all of the existing event views so we can reuse them if needed, this process - // can be useful if your day view is hosted in a recycler view for example - val recycled = dayView.removeEventViews() - var remaining = recycled?.size ?: 0 - - val arrowRight = " → " - val bullet = " • " - val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity) - - for (lesson in lessons) { - val startTime = lesson.displayStartTime ?: continue - val endTime = lesson.displayEndTime ?: continue - - firstEventMinute = min(firstEventMinute, startTime.hour * 60 + startTime.minute) - - // Try to recycle an existing event view if there are enough left, otherwise inflate - // a new one - val eventView = (if (remaining > 0) recycled?.get(--remaining) else layoutInflater.inflate(R.layout.timetable_lesson, dayView, false)) - ?: continue - val lb = TimetableLessonBinding.bind(eventView) - eventViews += eventView - - eventView.tag = lesson - - eventView.setOnClickListener { - if (isAdded && it.tag is LessonFull) - LessonDetailsDialog(activity, it.tag as LessonFull) - } - - val eventList = events.filter { it.time != null && it.time == lesson.displayStartTime }.take(3) - eventList.getOrNull(0).let { - lb.event1.visibility = if (it == null) View.GONE else View.VISIBLE - lb.event1.background = it?.let { - R.drawable.bg_circle.resolveDrawable(activity).setTintColor(it.eventColor) - } - } - eventList.getOrNull(1).let { - lb.event2.visibility = if (it == null) View.GONE else View.VISIBLE - lb.event2.background = it?.let { - R.drawable.bg_circle.resolveDrawable(activity).setTintColor(it.eventColor) - } - } - eventList.getOrNull(2).let { - lb.event3.visibility = if (it == null) View.GONE else View.VISIBLE - lb.event3.background = it?.let { - R.drawable.bg_circle.resolveDrawable(activity).setTintColor(it.eventColor) - } - } - - - val timeRange = "${startTime.stringHM} - ${endTime.stringHM}".asColoredSpannable(colorSecondary) - - // teacher - val teacherInfo = if (lesson.teacherId != null && lesson.teacherId == lesson.oldTeacherId) - lesson.teacherName ?: "?" - else - mutableListOf().apply { - lesson.oldTeacherName?.let { add(it.asStrikethroughSpannable()) } - lesson.teacherName?.let { add(it) } - }.concat(arrowRight) - - // team - val teamInfo = if (lesson.teamId != null && lesson.teamId == lesson.oldTeamId) - lesson.teamName ?: "?" - else - mutableListOf().apply { - lesson.oldTeamName?.let { add(it.asStrikethroughSpannable()) } - lesson.teamName?.let { add(it) } - }.concat(arrowRight) - - // classroom - val classroomInfo = if (lesson.classroom != null && lesson.classroom == lesson.oldClassroom) - lesson.classroom ?: "?" - else - mutableListOf().apply { - lesson.oldClassroom?.let { add(it.asStrikethroughSpannable()) } - lesson.classroom?.let { add(it) } - }.concat(arrowRight) - - - lb.lessonNumber = lesson.displayLessonNumber - lb.subjectName.text = lesson.displaySubjectName?.let { - if (lesson.type == Lesson.TYPE_CANCELLED || lesson.type == Lesson.TYPE_SHIFTED_SOURCE) - it.asStrikethroughSpannable().asColoredSpannable(colorSecondary) - else - it - } - lb.detailsFirst.text = listOfNotEmpty(timeRange, classroomInfo).concat(bullet) - lb.detailsSecond.text = listOfNotEmpty(teacherInfo, teamInfo).concat(bullet) - - lb.unread = lesson.type != Lesson.TYPE_NORMAL && lesson.showAsUnseen - if (!lesson.seen) { - manager.markAsSeen(lesson) - } - - //lb.subjectName.typeface = Typeface.create("sans-serif-light", Typeface.BOLD) - 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 - val startMinute = 60 * (lesson.displayStartTime?.hour - ?: 0) + (lesson.displayStartTime?.minute ?: 0) - val endMinute = startMinute + 45 - eventTimeRanges.add(DayView.EventTimeRange(startMinute, endMinute)) - } - - dayView.setEventViews(eventViews, eventTimeRanges) - val firstEventTop = (firstEventMinute - dayView.startHour * 60) * dayView.minuteHeight - dayScroll.scrollTo(0, firstEventTop.toInt()) - } - - override fun onResume() { - super.onResume() - if (dayScrollDelegate.isInitialized()) { - val firstEventTop = (firstEventMinute - dayView.startHour * 60) * dayView.minuteHeight - dayScroll.scrollTo(0, firstEventTop.toInt()) - } - } -} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteCategoryViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteCategoryViewHolder.kt new file mode 100644 index 00000000..49fbf91c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteCategoryViewHolder.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-27. + */ + +package pl.szczodrzynski.edziennik.ui.notes + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.RecyclerView +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.Binding +import pl.szczodrzynski.edziennik.data.db.entity.Note +import pl.szczodrzynski.edziennik.databinding.NoteListCategoryItemBinding +import pl.szczodrzynski.edziennik.ext.resolveDrawable +import pl.szczodrzynski.edziennik.ui.grades.viewholder.BindableViewHolder + +class NoteCategoryViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + val b: NoteListCategoryItemBinding = NoteListCategoryItemBinding.inflate( + inflater, + parent, + false, + ), +) : RecyclerView.ViewHolder(b.root), BindableViewHolder { + companion object { + private const val TAG = "NoteCategoryViewHolder" + } + + override fun onBind( + activity: AppCompatActivity, + app: App, + item: Note, + position: Int, + adapter: NoteListAdapter, + ) { + val manager = app.noteManager + val title = b.root as? TextView ?: return + val ownerType = item.ownerType ?: return + + title.setText(manager.getOwnerTypeText(ownerType)) + title.setCompoundDrawables( + manager.getOwnerTypeImage(ownerType).resolveDrawable(activity), + null, + null, + null, + ) + Binding.drawableLeftAutoSize(title, enable = true) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteDetailsDialog.kt new file mode 100644 index 00000000..9ec765ec --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteDetailsDialog.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-24. + */ + +package pl.szczodrzynski.edziennik.ui.notes + +import android.view.LayoutInflater +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.Note +import pl.szczodrzynski.edziennik.data.db.entity.Noteable +import pl.szczodrzynski.edziennik.databinding.NoteDetailsDialogBinding +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog +import pl.szczodrzynski.edziennik.utils.models.Date + +class NoteDetailsDialog( + activity: AppCompatActivity, + private val owner: Noteable?, + private var note: Note, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BindingDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "NoteDetailsDialog" + + override fun getTitleRes(): Int? = null + override fun inflate(layoutInflater: LayoutInflater) = + NoteDetailsDialogBinding.inflate(layoutInflater) + + override fun getPositiveButtonText() = R.string.close + override fun getNeutralButtonText() = if (note.canEdit) R.string.homework_edit else null + + private val manager + get() = app.noteManager + + override suspend fun onNeutralClick(): Boolean { + NoteEditorDialog( + activity = activity, + owner = owner, + editingNote = note, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ).show() + return NO_DISMISS + } + + override suspend fun onShow() { + manager.configureHeader(activity, owner, b.header) + + b.idsLayout.isVisible = App.devMode + + // watch the note for changes + app.db.noteDao().get(note.profileId, note.id).observe(activity) { + if (it == null) { + dismiss() + return@observe + } + note = it + update() + } + } + + private fun update() { + b.note = note + + if (note.color != null) { + dialog.overlayBackgroundColor(note.color!!.toInt(), 0x50) + } else { + dialog.overlayBackgroundColor(0, 0) + } + + b.addedBy.setText( + when (note.sharedBy) { + null -> R.string.notes_added_by_you_format + "self" -> R.string.event_details_shared_by_self_format + else -> R.string.event_details_shared_by_format + }, + Date.fromMillis(note.addedDate).formattedString, + note.sharedByName ?: "", + ) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteEditorDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteEditorDialog.kt new file mode 100644 index 00000000..8114a6db --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteEditorDialog.kt @@ -0,0 +1,207 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-24. + */ + +package pl.szczodrzynski.edziennik.ui.notes + +import android.view.LayoutInflater +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +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.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.Note +import pl.szczodrzynski.edziennik.data.db.entity.Noteable +import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.databinding.NoteEditorDialogBinding +import pl.szczodrzynski.edziennik.ext.isNotNullNorBlank +import pl.szczodrzynski.edziennik.ext.resolveString +import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog +import pl.szczodrzynski.edziennik.ui.dialogs.settings.RegistrationConfigDialog +import pl.szczodrzynski.edziennik.utils.TextInputDropDown +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.HtmlMode +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.StylingConfigBase +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +class NoteEditorDialog( + activity: AppCompatActivity, + private val owner: Noteable?, + private val editingNote: Note?, + private val profileId: Int = + owner?.getNoteOwnerProfileId() + ?: editingNote?.profileId + ?: 0, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BindingDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "NoteEditorDialog" + + override fun getTitleRes(): Int? = null + override fun inflate(layoutInflater: LayoutInflater) = + NoteEditorDialogBinding.inflate(layoutInflater) + + override fun isCancelable() = false + override fun getPositiveButtonText() = R.string.save + override fun getNeutralButtonText() = if (editingNote != null) R.string.remove else null + override fun getNegativeButtonText() = R.string.cancel + + private lateinit var topicStylingConfig: StylingConfigBase + private lateinit var bodyStylingConfig: StylingConfigBase + private val manager + get() = app.noteManager + private val textStylingManager + get() = app.textStylingManager + + private var progressDialog: AlertDialog? = null + + override suspend fun onPositiveClick(): Boolean { + val profile = withContext(Dispatchers.IO) { + app.db.profileDao().getByIdNow(profileId) + } ?: return NO_DISMISS + + val note = buildNote(profile) ?: return NO_DISMISS + + if (note.isShared && !profile.canShare) { + RegistrationConfigDialog(activity, profile, onChangeListener = { enabled -> + if (enabled) + onPositiveClick() + }).showNoteShareDialog() + return NO_DISMISS + } + + if (note.isShared || editingNote?.isShared == true) { + progressDialog = MaterialAlertDialogBuilder(activity) + .setTitle(R.string.please_wait) + .setMessage(when (note.isShared) { + true -> R.string.notes_editor_progress_sharing + false -> R.string.notes_editor_progress_unsharing + }) + .setCancelable(false) + .show() + } + + val success = manager.saveNote( + activity = activity, + note = note, + teamId = owner?.getNoteShareTeamId(), + wasShared = editingNote?.isShared ?: false, + ) + progressDialog?.dismiss() + return success + } + + override suspend fun onNeutralClick(): Boolean { + // editingNote cannot be null, as the button is visible + + val confirmation = suspendCoroutine { cont -> + var result = false + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.are_you_sure) + .setMessage(R.string.notes_editor_confirmation_text) + .setPositiveButton(R.string.yes) { _, _ -> result = true } + .setNegativeButton(R.string.no, null) + .setOnDismissListener { cont.resume(result) } + .show() + } + if (!confirmation) + return NO_DISMISS + + if (editingNote?.isShared == true) { + progressDialog = MaterialAlertDialogBuilder(activity) + .setTitle(R.string.please_wait) + .setMessage(R.string.notes_editor_progress_unsharing) + .setCancelable(false) + .show() + } + + val success = manager.deleteNote(activity, editingNote ?: return NO_DISMISS) + progressDialog?.dismiss() + return success + } + + override suspend fun onShow() { + manager.configureHeader(activity, owner, b.header) + + topicStylingConfig = StylingConfigBase(editText = b.topic, htmlMode = HtmlMode.SIMPLE) + bodyStylingConfig = StylingConfigBase(editText = b.body, htmlMode = HtmlMode.SIMPLE) + + val profile = withContext(Dispatchers.IO) { + app.db.profileDao().getByIdNow(profileId) + } + + b.ownerType = owner?.getNoteType() ?: Note.OwnerType.NONE + b.editingNote = editingNote + b.shareByDefault = app.profile.config.shareByDefault && profile?.canShare == true + + b.color.clear().append(Note.Color.values().map { color -> + TextInputDropDown.Item( + id = color.value ?: 0L, + text = color.stringRes.resolveString(activity), + tag = color, + icon = if (color.value != null) + IconicsDrawable(activity).apply { + icon = CommunityMaterial.Icon.cmd_circle + sizeDp = 24 + colorInt = color.value.toInt() + } else null, + ) + }) + b.color.select(id = editingNote?.color ?: 0L) + + textStylingManager.attachToField( + activity = activity, + textLayout = b.topicLayout, + textEdit = b.topic, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ) + textStylingManager.attachToField( + activity = activity, + textLayout = b.bodyLayout, + textEdit = b.body, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ) + } + + private fun buildNote(profile: Profile): Note? { + val ownerType = owner?.getNoteType() ?: Note.OwnerType.NONE + val topic = b.topic.text?.toString() + val body = b.body.text?.toString() + val color = b.color.selected?.tag as? Note.Color + + val share = b.shareSwitch.isChecked && ownerType.isShareable + val replace = b.replaceSwitch.isChecked && ownerType.canReplace + + if (body.isNullOrBlank()) { + b.bodyLayout.error = app.getString(R.string.notes_editor_body_error) + b.body.requestFocus() + return null + } + + val topicHtml = if (topic.isNotNullNorBlank()) + textStylingManager.getHtmlText(topicStylingConfig) + else null + val bodyHtml = textStylingManager.getHtmlText(bodyStylingConfig) + + return Note( + profileId = profile.id, + id = editingNote?.id ?: System.currentTimeMillis(), + ownerType = owner?.getNoteType(), + ownerId = owner?.getNoteOwnerId(), + replacesOriginal = replace, + topic = topicHtml, + body = bodyHtml, + color = color?.value, + sharedBy = if (share) "self" else null, + sharedByName = if (share) profile.studentNameLong else null, + ) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteListAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteListAdapter.kt new file mode 100644 index 00000000..db38cf34 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteListAdapter.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-23. + */ + +package pl.szczodrzynski.edziennik.ui.notes + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.RecyclerView +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.db.entity.Note +import pl.szczodrzynski.edziennik.ui.search.SearchableAdapter + +class NoteListAdapter( + val activity: AppCompatActivity, + val onNoteClick: ((note: Note) -> Unit)? = null, + val onNoteEditClick: ((note: Note) -> Unit)? = null, +) : SearchableAdapter() { + companion object { + private const val TAG = "NoteListAdapter" + private const val ITEM_TYPE_NOTE = 0 + private const val ITEM_TYPE_CATEGORY = 1 + } + + private val app = activity.applicationContext as App + + override fun getItemViewType(item: Note) = when { + item.isCategoryItem -> ITEM_TYPE_CATEGORY + else -> ITEM_TYPE_NOTE + } + + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + item: Note, + ) { + when (holder) { + is NoteViewHolder -> holder.onBind(activity, app, item, position, this) + is NoteCategoryViewHolder -> holder.onBind(activity, app, item, position, this) + } + } + + override fun onCreateViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int, + ): RecyclerView.ViewHolder = when (viewType) { + ITEM_TYPE_CATEGORY -> NoteCategoryViewHolder(inflater, parent) + else -> NoteViewHolder(inflater, parent) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteListDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteListDialog.kt new file mode 100644 index 00000000..15acc30b --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteListDialog.kt @@ -0,0 +1,104 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-23. + */ + +package pl.szczodrzynski.edziennik.ui.notes + +import android.view.LayoutInflater +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.Noteable +import pl.szczodrzynski.edziennik.databinding.NoteListDialogBinding +import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog +import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration + +class NoteListDialog( + activity: AppCompatActivity, + private val owner: Noteable, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BindingDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "NoteListDialog" + + override fun getTitleRes(): Int? = null + override fun inflate(layoutInflater: LayoutInflater) = + NoteListDialogBinding.inflate(layoutInflater) + + override fun getPositiveButtonText() = R.string.close + override fun getNeutralButtonText() = R.string.add + + private val manager + get() = app.noteManager + + private lateinit var adapter: NoteListAdapter + + override suspend fun onNeutralClick(): Boolean { + NoteEditorDialog( + activity = activity, + owner = owner, + editingNote = null, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ).show() + return NO_DISMISS + } + + override suspend fun onShow() { + manager.configureHeader(activity, owner, b.header) + + adapter = NoteListAdapter( + activity = activity, + onNoteClick = { + NoteDetailsDialog( + activity = activity, + owner = owner, + note = it, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ).show() + }, + onNoteEditClick = { + NoteEditorDialog( + activity = activity, + owner = owner, + editingNote = it, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ).show() + }, + ) + + app.db.noteDao().getAllFor( + profileId = owner.getNoteOwnerProfileId(), + ownerType = owner.getNoteType(), + ownerId = owner.getNoteOwnerId() + ).observe(activity) { notes -> + + // show/hide relevant views + b.noteListLayout.isVisible = notes.isNotEmpty() + b.noData.isVisible = notes.isEmpty() + if (notes.isEmpty()) { + return@observe + } + + // apply the new note list + adapter.setAllItems(notes) + + // configure the adapter & recycler view + if (b.noteList.adapter == null) { + b.noteList.adapter = adapter + b.noteList.apply { + //setHasFixedSize(true) + isNestedScrollingEnabled = false + layoutManager = LinearLayoutManager(context) + addItemDecoration(SimpleDividerItemDecoration(context)) + } + } else { + adapter.notifyDataSetChanged() + } + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteViewHolder.kt new file mode 100644 index 00000000..195dc57f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteViewHolder.kt @@ -0,0 +1,84 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-23. + */ + +package pl.szczodrzynski.edziennik.ui.notes + +import android.graphics.drawable.ColorDrawable +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.graphics.ColorUtils +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.Note +import pl.szczodrzynski.edziennik.databinding.NoteListItemBinding +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.ui.grades.viewholder.BindableViewHolder +import pl.szczodrzynski.edziennik.utils.models.Date + +class NoteViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + val b: NoteListItemBinding = NoteListItemBinding.inflate(inflater, parent, false), +) : RecyclerView.ViewHolder(b.root), BindableViewHolder { + companion object { + private const val TAG = "NoteViewHolder" + } + + override fun onBind( + activity: AppCompatActivity, + app: App, + item: Note, + position: Int, + adapter: NoteListAdapter, + ) { + val colorHighlight = R.attr.colorControlHighlight.resolveAttr(activity) + val addedDate = Date.fromMillis(item.addedDate).formattedString + + b.topic.text = adapter.highlightSearchText( + item = item, + text = item.topicHtml ?: item.bodyHtml, + color = colorHighlight, + ) + + if (item.color != null) { + b.colorLayout.background = + ColorDrawable(ColorUtils.setAlphaComponent(item.color.toInt(), 0x50)) + } else { + b.colorLayout.background = null + } + + if (item.sharedBy != null && item.sharedByName != null) { + b.addedBy.text = listOf( + "{cmd-share-variant}", + item.sharedByName, + "•", + addedDate, + ).concat(" ") + + // workaround for the span data lost during setText above + val sharedBySpanned = adapter.highlightSearchText( + item = item, + text = item.sharedByName, + color = colorHighlight, + ) + b.addedBy.text = b.addedBy.text.replaceSpanned(item.sharedByName, sharedBySpanned) + } else { + b.addedBy.setText(R.string.notes_added_by_you_format, addedDate) + } + + b.editButton.isVisible = item.canEdit && adapter.onNoteEditClick != null + + if (adapter.onNoteClick != null) + b.root.onClick { + adapter.onNoteClick.invoke(item) + } + if (adapter.onNoteEditClick != null) + b.editButton.onClick { + adapter.onNoteEditClick.invoke(item) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NotesExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NotesExtensions.kt new file mode 100644 index 00000000..2a7ecb84 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NotesExtensions.kt @@ -0,0 +1,47 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-23. + */ + +package pl.szczodrzynski.edziennik.ui.notes + +import android.view.Gravity +import android.widget.LinearLayout +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams +import androidx.core.view.updatePadding +import com.google.android.material.button.MaterialButton +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.Noteable +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ext.onClick + +fun MaterialButton.setupNotesButton( + activity: AppCompatActivity, + owner: Noteable, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) { + if (!isVisible) + return + icon = IconicsDrawable(activity, CommunityMaterial.Icon3.cmd_playlist_edit) + setText(R.string.notes_button) + iconPadding = 8.dp + iconSize = 24.dp + + updateLayoutParams { + gravity = Gravity.CENTER_HORIZONTAL + } + updatePadding(left = 12.dp) + + onClick { + NoteListDialog( + activity = activity, + owner = owner, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ).show() + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NotesFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NotesFragment.kt new file mode 100644 index 00000000..6f2e8688 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NotesFragment.kt @@ -0,0 +1,156 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-27. + */ + +package pl.szczodrzynski.edziennik.ui.notes + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.LinearLayoutManager +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import kotlinx.coroutines.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.Note +import pl.szczodrzynski.edziennik.data.db.entity.Noteable +import pl.szczodrzynski.edziennik.databinding.NotesFragmentBinding +import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration +import kotlin.coroutines.CoroutineContext + +class NotesFragment : Fragment(), CoroutineScope { + companion object { + private const val TAG = "NotesFragment" + } + + private lateinit var app: App + private lateinit var activity: MainActivity + private lateinit var b: NotesFragmentBinding + + private val job: Job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + private val manager + get() = app.noteManager + + 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 = NotesFragmentBinding.inflate(inflater) + return b.root + } + + private fun onNoteClick(note: Note) = launch { + val owner = withContext(Dispatchers.IO) { + manager.getOwner(note) + } as? Noteable + + NoteDetailsDialog( + activity = activity, + owner = owner, + note = note, + ).show() + } + + private fun onNoteEditClick(note: Note) = launch { + val owner = withContext(Dispatchers.IO) { + manager.getOwner(note) + } as? Noteable + + NoteEditorDialog( + activity = activity, + owner = owner, + editingNote = note, + profileId = App.profileId, + ).show() + } + + private fun onNoteAddClick(view: View?) { + NoteEditorDialog( + activity = activity, + owner = null, + editingNote = null, + profileId = App.profileId, + ).show() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + if (!isAdded) return + + activity.navView.apply { + bottomBar.apply { + fabEnable = true + fabExtendedText = getString(R.string.notes_action_add) + fabIcon = CommunityMaterial.Icon3.cmd_text_box_plus_outline + } + + setFabOnClickListener(this@NotesFragment::onNoteAddClick) + } + activity.gainAttentionFAB() + + val adapter = NoteListAdapter( + activity = activity, + onNoteClick = this::onNoteClick, + onNoteEditClick = this::onNoteEditClick, + ) + + app.db.noteDao().getAll(profileId = App.profileId).observe(activity) { allNotes -> + if (!isAdded) return@observe + + // show/hide relevant views + b.progressBar.isVisible = false + b.list.isVisible = allNotes.isNotEmpty() + b.noData.isVisible = allNotes.isEmpty() + if (allNotes.isEmpty()) { + return@observe + } + + val notes = allNotes.groupBy { it.ownerType }.flatMap { (ownerType, notes) -> + if (ownerType != null) { + // construct a dummy note, used as the category separator + val categoryItem = Note( + profileId = 0, + id = 0, + ownerType = ownerType, + ownerId = 0, + topic = null, + body = "", + color = null, + ) + categoryItem.isCategoryItem = true + val mutableNotes = notes.toMutableList() + mutableNotes.add(0, categoryItem) + return@flatMap mutableNotes + } + return@flatMap notes + } + + // apply the new note list + adapter.setAllItems(notes, addSearchField = true) + + // configure the adapter & recycler view + if (b.list.adapter == null) { + b.list.adapter = adapter + b.list.apply { + //setHasFixedSize(true) + isNestedScrollingEnabled = false + layoutManager = LinearLayoutManager(context) + addItemDecoration(SimpleDividerItemDecoration(context)) + } + } + + // reapply the filter + adapter.getSearchField()?.applyTo(adapter) + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notifications/NotificationsAdapter.kt similarity index 81% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/notifications/NotificationsAdapter.kt index 8df33a8c..ee2d8746 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notifications/NotificationsAdapter.kt @@ -1,15 +1,19 @@ -package pl.szczodrzynski.edziennik.ui.modules.notifications +package pl.szczodrzynski.edziennik.ui.notifications import android.view.LayoutInflater import android.view.ViewGroup import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.RecyclerView +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.utils.colorRes import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R 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 kotlin.coroutines.CoroutineContext @@ -43,13 +47,17 @@ class NotificationsAdapter( val date = Date.fromMillis(item.addedDate).formattedString val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity) + b.notificationIcon.background = IconicsDrawable(app, item.type.icon).apply { + colorRes = R.color.colorPrimary + } + b.title.text = item.text b.profileDate.text = listOf( item.profileName ?: "", " • ", date ).concat().asColoredSpannable(colorSecondary) - b.type.text = activity.getNotificationTitle(item.type) + b.type.text = item.type.titleRes.resolveString(activity) onItemClick?.let { listener -> b.root.onClick { listener(item) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notifications/NotificationsListFragment.kt similarity index 91% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsListFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/notifications/NotificationsListFragment.kt index 7cd9703b..f2a98f11 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/notifications/NotificationsListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notifications/NotificationsListFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2019-11-22. */ -package pl.szczodrzynski.edziennik.ui.modules.notifications +package pl.szczodrzynski.edziennik.ui.notifications import android.app.Activity import android.content.Intent @@ -22,6 +22,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.databinding.NotificationsListFragmentBinding +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem @@ -65,14 +67,14 @@ class NotificationsListFragment : Fragment(), CoroutineScope { val intent = Intent("android.intent.action.MAIN") notification.fillIntent(intent) - Utils.d(TAG, "notification with item " + notification.viewId + " extras " + if (intent.extras == null) "null" else intent.extras!!.toString()) + Utils.d(TAG, "notification with item " + notification.navTarget + " extras " + if (intent.extras == null) "null" else intent.extras!!.toString()) if (notification.profileId != null && notification.profileId != -1 && notification.profileId != app.profile.id && context is Activity) { Toast.makeText(app, app.getString(R.string.toast_changing_profile), Toast.LENGTH_LONG).show() } app.sendBroadcast(intent) } - app.db.notificationDao().getAll().observe(this@NotificationsListFragment, Observer { items -> + app.db.notificationDao().getAll().observe(viewLifecycleOwner, Observer { items -> if (!isAdded) return@Observer // load & configure the adapter diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/SearchField.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/SearchField.kt new file mode 100644 index 00000000..11c322ef --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/SearchField.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-5. + */ + +package pl.szczodrzynski.edziennik.ui.search + +import android.widget.Filter + +class SearchField( + var searchText: CharSequence = "", +) : Searchable { + + override val searchKeywords = emptyList>() + override var searchPriority = 0 + override var searchHighlightText: String? = null + override fun compareTo(other: Searchable<*>) = 0 + + fun applyTo(adapter: SearchableAdapter<*>, listener: Filter.FilterListener? = null) { + adapter.filter.filter(searchText, listener) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/SearchFilter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/SearchFilter.kt new file mode 100644 index 00000000..3a76400e --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/SearchFilter.kt @@ -0,0 +1,129 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-14. + */ + +package pl.szczodrzynski.edziennik.ui.search + +import android.widget.Filter +import pl.szczodrzynski.edziennik.ext.cleanDiacritics +import java.util.* +import kotlin.math.min + +class SearchFilter>( + private val adapter: SearchableAdapter, +) : Filter() { + companion object { + private const val NO_MATCH = 1000 + } + + private var prevCount = -1 + + private val allItems + get() = adapter.allItems + + private fun getMatchWeight(name: CharSequence?, prefix: CharSequence): Int { + if (name == null) + return NO_MATCH + + val prefixClean = prefix.cleanDiacritics() + val nameClean = name.cleanDiacritics() + + return when { + // First match against the whole, non-split value + nameClean.startsWith(prefixClean, ignoreCase = true) -> 1 + // check if prefix matches any of the words + nameClean.split(" ").any { + it.startsWith(prefixClean, ignoreCase = true) + } -> 2 + // finally check if the prefix matches any part of the name + nameClean.contains(prefixClean, ignoreCase = true) -> 3 + + else -> NO_MATCH + } + } + + override fun performFiltering(prefix: CharSequence?): FilterResults { + val results = FilterResults() + + if (prevCount == -1) + prevCount = allItems.size + + if (prefix.isNullOrBlank()) { + allItems.forEach { + it.searchPriority = NO_MATCH + it.searchHighlightText = null + } + results.values = allItems.toList() + results.count = allItems.size + return results + } + + val newItems = allItems.mapNotNull { item -> + // get all keyword sets from the entity + val searchKeywords = item.searchKeywords + // keep the SearchField and items having no keywords + if (item is SearchField || searchKeywords.isEmpty()) { + return@mapNotNull item + } + item.searchPriority = NO_MATCH + item.searchHighlightText = null + + // a temporary variable for the loops below + var matchWeight: Int + + searchKeywords.forEachIndexed { priority, keywords -> + keywords ?: return@forEachIndexed + keywords.forEach { keyword -> + matchWeight = getMatchWeight(keyword, prefix) + if (matchWeight != NO_MATCH) { + // a match not at the word start boundary should be least prioritized + if (matchWeight == 3) + matchWeight = 100 + item.searchPriority = min(item.searchPriority, priority * 10 + matchWeight) + } + } + } + + if (item.searchPriority != NO_MATCH) { + // the adapter is reversed, the search priority also should be + if (adapter.isReversed) + item.searchPriority *= -1 + item.searchHighlightText = prefix.toString() + return@mapNotNull item + } + return@mapNotNull null + } + + results.values = newItems.sorted() + results.count = newItems.size + return results + } + + override fun publishResults(constraint: CharSequence?, results: FilterResults) { + results.values?.let { + @Suppress("UNCHECKED_CAST") // yes I know it's checked. + adapter.setFilteredItems(it as List) + } + // do not re-bind the search box + val count = results.count - 1 + + // this tries to update every item except the search field + with(adapter) { + when { + count > prevCount -> { + notifyItemRangeInserted(prevCount + 1, count - prevCount) + notifyItemRangeChanged(1, prevCount) + } + count < prevCount -> { + notifyItemRangeRemoved(prevCount + 1, prevCount - count) + notifyItemRangeChanged(1, count) + } + else -> { + notifyItemRangeChanged(1, count) + } + } + } + + prevCount = count + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/SearchTextWatcher.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/SearchTextWatcher.kt new file mode 100644 index 00000000..fdf041e4 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/SearchTextWatcher.kt @@ -0,0 +1,42 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-14. + */ + +package pl.szczodrzynski.edziennik.ui.search + +import android.text.Editable +import android.text.TextWatcher +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.SearchItemBinding + +class SearchTextWatcher( + private val b: SearchItemBinding, + private val filter: SearchFilter<*>, + private val item: SearchField, +) : TextWatcher { + + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) = Unit + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit + + override fun afterTextChanged(s: Editable?) { + item.searchText = s ?: "" + filter.filter(s) { count -> + if (s.isNullOrBlank()) + b.searchLayout.helperText = " " + else + b.searchLayout.helperText = + b.root.context.getString(R.string.messages_search_results, count - 1) + } + } + + override fun equals(other: Any?): Boolean { + return other is SearchTextWatcher + } + + override fun hashCode(): Int { + var result = b.hashCode() + result = 31 * result + filter.hashCode() + result = 31 * result + item.hashCode() + return result + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/SearchViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/SearchViewHolder.kt new file mode 100644 index 00000000..a9079672 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/SearchViewHolder.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-4-5. + */ + +package pl.szczodrzynski.edziennik.ui.search + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.SearchItemBinding + +class SearchViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + val b: SearchItemBinding = SearchItemBinding.inflate( + inflater, + parent, + false + ), +) : RecyclerView.ViewHolder(b.root) { + companion object { + private const val TAG = "SearchViewHolder" + } + + internal fun bind(item: SearchField, adapter: SearchableAdapter<*>) { + val watcher = SearchTextWatcher(b, adapter.filter, item) + b.searchEdit.removeTextChangedListener(watcher) + + if (adapter.items.isEmpty() || adapter.items.size == adapter.allItems.size) + b.searchLayout.helperText = " " + else + b.searchLayout.helperText = + b.root.context.getString(R.string.messages_search_results, adapter.items.size - 1) + b.searchEdit.setText(item.searchText) + + b.searchEdit.addTextChangedListener(watcher) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/Searchable.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/Searchable.kt new file mode 100644 index 00000000..5c145f86 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/Searchable.kt @@ -0,0 +1,24 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-10. + */ + +package pl.szczodrzynski.edziennik.ui.search + +interface Searchable : Comparable> { + + /** + * A prioritized list of keywords sets. First items are of the highest priority. + * Items within a keyword set have the same priority. + */ + val searchKeywords: List?> + + /** + * A priority assigned by [SearchFilter]. Lower numbers mean a higher priority. + */ + var searchPriority: Int + + /** + * The text to be highlighted when filtering. + */ + var searchHighlightText: String? +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/SearchableAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/SearchableAdapter.kt new file mode 100644 index 00000000..430fdc4d --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/SearchableAdapter.kt @@ -0,0 +1,133 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-10. + */ + +package pl.szczodrzynski.edziennik.ui.search + +import android.text.SpannableStringBuilder +import android.text.style.BackgroundColorSpan +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.Filterable +import androidx.recyclerview.widget.RecyclerView +import pl.szczodrzynski.edziennik.ext.asSpannable +import pl.szczodrzynski.edziennik.utils.span.BoldSpan + +abstract class SearchableAdapter>( + val isReversed: Boolean = false, +) : RecyclerView.Adapter(), Filterable { + companion object { + const val ITEM_TYPE_SEARCH = 2137 + } + + /** + * A mutable list managed by [setAllItems]. + * Items are never displayed straight from this list. + * Items in this list are always sorted according to their + * natural order, with the [SearchField] preceding any other. + */ + val allItems = mutableListOf() + + /** + * A mutable var changed by the [SearchFilter]. + * This list is the only direct source of displayed items. + * Items in this list may be in reverse order ([isReversed]), with the [SearchField] + * still as the first item. + */ + var items = listOf() + private set + + /** + * Set [items] as the currently displayed item list. The [items] are first + * sorted appropriately to the [isReversed] property. + */ + internal fun setFilteredItems(items: List) { + this.items = if (isReversed) + items.sortedDescending() // the sort is stable - SearchField should stay at the top + else + items.sorted() + } + + /** + * Put [items] to the sorted, unfiltered data source. + * + * @param searchText the text to fill the [SearchField] with, by default + * @param addSearchField whether searching should be enabled and visible + */ + fun setAllItems(items: List, searchText: String? = null, addSearchField: Boolean = false) { + if (allItems.isEmpty()) { + // items empty - add the search field + if (addSearchField) { + @Suppress("UNCHECKED_CAST") // what ??? + allItems += SearchField(searchText ?: "") as T + } + } else { + // items not empty - remove all except the search field + allItems.removeAll { it !is SearchField } + } + // add all new items + allItems.addAll(items.sorted()) + // show all items if searching is disabled + if (!addSearchField) { + setFilteredItems(allItems) + } + } + + /** + * Return the search field in this adapter's list, or null if not found. + */ + fun getSearchField(): SearchField? { + return allItems.filterIsInstance().firstOrNull() + } + + fun highlightSearchText(item: T, text: CharSequence, color: Int): CharSequence { + if (item.searchHighlightText == null) + return SpannableStringBuilder(text) + return text.asSpannable( + BoldSpan(), + BackgroundColorSpan(color), + substring = item.searchHighlightText, + ignoreCase = true, + ignoreDiacritics = true, + ) + } + + final override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + ITEM_TYPE_SEARCH -> SearchViewHolder(inflater, parent) + else -> onCreateViewHolder(inflater, parent, viewType) + } + } + + final override fun getItemViewType(position: Int): Int { + return when (val item = items[position]) { + is SearchField -> ITEM_TYPE_SEARCH + else -> getItemViewType(item) + } + } + + final override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + val item = items[position] + if (holder is SearchViewHolder && item is SearchField) { + holder.bind(item, this) + } else { + onBindViewHolder(holder, position, item) + } + } + + abstract fun getItemViewType(item: T): Int + abstract fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int, item: T) + abstract fun onCreateViewHolder( + inflater: LayoutInflater, + parent: ViewGroup, + viewType: Int, + ): RecyclerView.ViewHolder + + private val filter = SearchFilter(this) + override fun getItemCount() = items.size + override fun getFilter() = filter +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/MaterialAboutProfileItem.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/MaterialAboutProfileItem.kt similarity index 95% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/MaterialAboutProfileItem.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/MaterialAboutProfileItem.kt index dfff3246..3e3fb642 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/MaterialAboutProfileItem.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/MaterialAboutProfileItem.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2021-3-17. */ -package pl.szczodrzynski.edziennik.ui.modules.settings +package pl.szczodrzynski.edziennik.ui.settings import android.content.Context import android.view.View diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/ProfileManagerFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/ProfileManagerFragment.kt similarity index 96% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/ProfileManagerFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/ProfileManagerFragment.kt index 2ab29917..7ea042c0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/ProfileManagerFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/ProfileManagerFragment.kt @@ -1,4 +1,4 @@ -package pl.szczodrzynski.edziennik.ui.modules.settings +package pl.szczodrzynski.edziennik.ui.settings import android.os.Bundle import android.view.LayoutInflater diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/SettingsCard.kt similarity index 68% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsCard.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/SettingsCard.kt index 36ce3777..104cfc54 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/SettingsCard.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2021-3-17. */ -package pl.szczodrzynski.edziennik.ui.modules.settings +package pl.szczodrzynski.edziennik.ui.settings import com.danielstone.materialaboutlibrary.items.MaterialAboutItem import com.danielstone.materialaboutlibrary.model.MaterialAboutCard @@ -16,13 +16,13 @@ abstract class SettingsCard( protected val activity: MainActivity = util.activity protected val configGlobal by lazy { app.config } - protected val configProfile by lazy { app.config.forProfile() } + protected val configProfile by lazy { app.profile.config } val card by lazy { buildCard() } protected abstract fun buildCard(): MaterialAboutCard - protected abstract fun getItems(): List - protected open fun getItemsMore(): List = listOf() + protected abstract fun getItems(card: MaterialAboutCard): List + protected open fun getItemsMore(card: MaterialAboutCard): List = listOf() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/SettingsFragment.kt similarity index 93% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/SettingsFragment.kt index cf7fa625..fca2b49e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/SettingsFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2021-3-16. */ -package pl.szczodrzynski.edziennik.ui.modules.settings +package pl.szczodrzynski.edziennik.ui.settings import android.content.Context import android.os.Bundle @@ -16,7 +16,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.MainActivity -import pl.szczodrzynski.edziennik.ui.modules.settings.cards.* +import pl.szczodrzynski.edziennik.ui.settings.cards.* import kotlin.coroutines.CoroutineContext class SettingsFragment : MaterialAboutFragment(), CoroutineScope { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsLicenseActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/SettingsLicenseActivity.kt similarity index 99% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsLicenseActivity.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/SettingsLicenseActivity.kt index de829d8d..5a4a5e9b 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsLicenseActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/SettingsLicenseActivity.kt @@ -1,4 +1,4 @@ -package pl.szczodrzynski.edziennik.ui.modules.settings +package pl.szczodrzynski.edziennik.ui.settings import android.content.Context import android.net.Uri @@ -15,7 +15,7 @@ import com.mikepenz.iconics.typeface.library.community.material.CommunityMateria import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.resolveColor +import pl.szczodrzynski.edziennik.ext.resolveColor import pl.szczodrzynski.edziennik.utils.Themes class SettingsLicenseActivity : MaterialAboutActivity() { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsUtil.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/SettingsUtil.kt similarity index 93% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsUtil.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/SettingsUtil.kt index a86fe2e3..6b4fe5db 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsUtil.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/SettingsUtil.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2021-3-17. */ -package pl.szczodrzynski.edziennik.ui.modules.settings +package pl.szczodrzynski.edziennik.ui.settings import com.danielstone.materialaboutlibrary.items.* import com.danielstone.materialaboutlibrary.model.MaterialAboutCard @@ -13,8 +13,8 @@ import com.mikepenz.iconics.utils.colorInt import com.mikepenz.iconics.utils.sizeDp import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.after 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 @@ -34,8 +34,8 @@ class SettingsUtil( fun createCard( titleRes: Int?, - items: List, - itemsMore: List, + items: (card: MaterialAboutCard) -> List, + itemsMore: (card: MaterialAboutCard) -> List, backgroundColor: Int? = null, theme: Int? = null ): MaterialAboutCard { @@ -44,10 +44,11 @@ class SettingsUtil( .cardColor(backgroundColor ?: 0) .theme(theme ?: 0) .build() - card.items.addAll(items) + card.items.addAll(items(card)) - if (itemsMore.isNotEmpty()) { - card.items.add(createMoreItem(card, itemsMore)) + val more = itemsMore(card) + if (more.isNotEmpty()) { + card.items.add(createMoreItem(card, more)) } return card diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsViewTypeManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/SettingsViewTypeManager.kt similarity index 90% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsViewTypeManager.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/SettingsViewTypeManager.kt index 1fb14fcd..560862eb 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/SettingsViewTypeManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/SettingsViewTypeManager.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2021-3-17. */ -package pl.szczodrzynski.edziennik.ui.modules.settings +package pl.szczodrzynski.edziennik.ui.settings import android.content.Context import android.view.View @@ -11,7 +11,7 @@ import com.danielstone.materialaboutlibrary.items.MaterialAboutItem import com.danielstone.materialaboutlibrary.items.MaterialAboutTitleItem.MaterialAboutTitleItemViewHolder import com.danielstone.materialaboutlibrary.util.DefaultViewTypeManager import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsViewTypeManager.ItemType.Companion.PROFILE_ITEM +import pl.szczodrzynski.edziennik.ui.settings.SettingsViewTypeManager.ItemType.Companion.PROFILE_ITEM class SettingsViewTypeManager : DefaultViewTypeManager() { class ItemType { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsAboutCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/cards/SettingsAboutCard.kt similarity index 73% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsAboutCard.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/cards/SettingsAboutCard.kt index 5f55ec70..1d4da496 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsAboutCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/cards/SettingsAboutCard.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2021-3-18. */ -package pl.szczodrzynski.edziennik.ui.modules.settings.cards +package pl.szczodrzynski.edziennik.ui.settings.cards import android.content.Intent import android.media.MediaPlayer @@ -18,12 +18,13 @@ import kotlinx.coroutines.launch import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.BuildConfig import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.after -import pl.szczodrzynski.edziennik.sync.UpdateWorker -import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog -import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsCard -import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsLicenseActivity -import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsUtil +import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update +import pl.szczodrzynski.edziennik.ext.after +import pl.szczodrzynski.edziennik.ui.dialogs.ChangelogDialog +import pl.szczodrzynski.edziennik.ui.settings.SettingsCard +import pl.szczodrzynski.edziennik.ui.settings.SettingsLicenseActivity +import pl.szczodrzynski.edziennik.ui.settings.SettingsUtil +import pl.szczodrzynski.edziennik.ui.settings.contributors.ContributorsActivity import pl.szczodrzynski.edziennik.utils.Utils import kotlin.coroutines.CoroutineContext @@ -38,19 +39,13 @@ class SettingsAboutCard(util: SettingsUtil) : SettingsCard(util), CoroutineScope MediaPlayer.create(activity, R.raw.ogarnij_sie) } - override fun buildCard(): MaterialAboutCard = - util.createCard( - null, - items = listOf(), - itemsMore = listOf(), - backgroundColor = 0xff1976d2.toInt(), - theme = R.style.AppTheme_Dark - ).also { - it.items.addAll(getItems(it)) - } - - override fun getItems() = listOf() - override fun getItemsMore() = listOf() + override fun buildCard() = util.createCard( + null, + items = ::getItems, + itemsMore = ::getItemsMore, + backgroundColor = 0xff1976d2.toInt(), + theme = R.style.AppTheme_Dark + ) private val versionDetailsItem by lazy { util.createActionItem( @@ -63,7 +58,7 @@ class SettingsAboutCard(util: SettingsUtil) : SettingsCard(util), CoroutineScope ) } - private fun getItems(card: MaterialAboutCard) = listOf( + override fun getItems(card: MaterialAboutCard) = listOf( util.createTitleItem(), util.createActionItem( @@ -90,12 +85,20 @@ class SettingsAboutCard(util: SettingsUtil) : SettingsCard(util), CoroutineScope it.subText = BuildConfig.VERSION_NAME + ", " + BuildConfig.BUILD_TYPE }, + util.createActionItem( + text = R.string.settings_about_contributors_text, + subText = R.string.settings_about_contributors_subtext, + icon = CommunityMaterial.Icon.cmd_account_group_outline + ) { + activity.startActivity(Intent(activity, ContributorsActivity::class.java)) + }, + util.createMoreItem(card, items = listOf( util.createActionItem( text = R.string.settings_about_changelog_text, icon = CommunityMaterial.Icon3.cmd_radar ) { - ChangelogDialog(activity) + ChangelogDialog(activity).show() }, util.createActionItem( @@ -104,7 +107,17 @@ class SettingsAboutCard(util: SettingsUtil) : SettingsCard(util), CoroutineScope icon = CommunityMaterial.Icon3.cmd_update ) { launch { - UpdateWorker.runNow(app) + val channel = if (App.devMode) + Update.Type.BETA + else + Update.Type.RC + val result = app.updateManager.checkNow(channel, notify = false) + val update = result.getOrNull() + // the dialog is shown by MainActivity (EventBus) + when { + result.isFailure -> Toast.makeText(app, app.getString(R.string.notification_cant_check_update), Toast.LENGTH_SHORT).show() + update == null -> Toast.makeText(app, app.getString(R.string.notification_no_update), Toast.LENGTH_SHORT).show() + } } } )), diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsProfileCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/cards/SettingsProfileCard.kt similarity index 66% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsProfileCard.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/cards/SettingsProfileCard.kt index f88a0242..a5ec33d8 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsProfileCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/cards/SettingsProfileCard.kt @@ -2,23 +2,24 @@ * Copyright (c) Kuba Szczodrzyński 2021-3-18. */ -package pl.szczodrzynski.edziennik.ui.modules.settings.cards +package pl.szczodrzynski.edziennik.ui.settings.cards import android.content.Intent +import com.danielstone.materialaboutlibrary.model.MaterialAboutCard import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.ui.dialogs.profile.ProfileConfigDialog -import pl.szczodrzynski.edziennik.ui.modules.login.LoginActivity -import pl.szczodrzynski.edziennik.ui.modules.settings.MaterialAboutProfileItem -import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsCard -import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsUtil +import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileConfigDialog +import pl.szczodrzynski.edziennik.ui.login.LoginActivity +import pl.szczodrzynski.edziennik.ui.settings.MaterialAboutProfileItem +import pl.szczodrzynski.edziennik.ui.settings.SettingsCard +import pl.szczodrzynski.edziennik.ui.settings.SettingsUtil class SettingsProfileCard(util: SettingsUtil) : SettingsCard(util) { override fun buildCard() = util.createCard( null, - items = getItems(), - itemsMore = listOf() + items = ::getItems, + itemsMore = ::getItemsMore, ) private fun getProfileItem(): MaterialAboutProfileItem = util.createProfileItem( @@ -31,10 +32,10 @@ class SettingsProfileCard(util: SettingsUtil) : SettingsCard(util) { card.items.remove(item) card.items.add(index, getProfileItem()) util.refresh() - }) + }).show() } - override fun getItems() = listOf( + override fun getItems(card: MaterialAboutCard) = listOf( getProfileItem(), util.createActionItem( diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/cards/SettingsRegisterCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/cards/SettingsRegisterCard.kt new file mode 100644 index 00000000..7b689b47 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/cards/SettingsRegisterCard.kt @@ -0,0 +1,180 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-3-18. + */ + +package pl.szczodrzynski.edziennik.ui.settings.cards + +import com.danielstone.materialaboutlibrary.model.MaterialAboutCard +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import eu.szkolny.font.SzkolnyFont +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.enums.LoginType +import pl.szczodrzynski.edziennik.ext.after +import pl.szczodrzynski.edziennik.ext.getStudentData +import pl.szczodrzynski.edziennik.ext.hasUIFeature +import pl.szczodrzynski.edziennik.ext.set +import pl.szczodrzynski.edziennik.ui.dialogs.settings.AgendaConfigDialog +import pl.szczodrzynski.edziennik.ui.dialogs.settings.AttendanceConfigDialog +import pl.szczodrzynski.edziennik.ui.dialogs.settings.BellSyncConfigDialog +import pl.szczodrzynski.edziennik.ui.dialogs.settings.GradesConfigDialog +import pl.szczodrzynski.edziennik.ui.dialogs.settings.MessagesConfigDialog +import pl.szczodrzynski.edziennik.ui.dialogs.settings.RegistrationConfigDialog +import pl.szczodrzynski.edziennik.ui.dialogs.settings.TimetableConfigDialog +import pl.szczodrzynski.edziennik.ui.settings.SettingsCard +import pl.szczodrzynski.edziennik.ui.settings.SettingsUtil + +class SettingsRegisterCard(util: SettingsUtil) : SettingsCard(util) { + + override fun buildCard() = util.createCard( + R.string.settings_card_register_title, + items = ::getItems, + itemsMore = ::getItemsMore, + ) + + private fun getBellSync() = + configGlobal.timetable.bellSyncDiff?.let { + activity.getString( + R.string.settings_register_bell_sync_subtext_format, + (if (configGlobal.timetable.bellSyncMultiplier == -1) "-" else "+") + it.stringHMS + ) + } ?: activity.getString(R.string.settings_register_bell_sync_subtext_disabled) + + private val sharedEventsDefaultItem by lazy { + util.createPropertyItem( + text = R.string.settings_register_share_by_default_text, + subText = R.string.settings_register_share_by_default_subtext, + icon = CommunityMaterial.Icon3.cmd_toggle_switch_outline, + value = configProfile.shareByDefault + ) { _, value -> + configProfile.shareByDefault = value + } + } + + override fun getItems(card: MaterialAboutCard) = listOfNotNull( + util.createActionItem( + text = R.string.menu_timetable_config, + icon = CommunityMaterial.Icon3.cmd_timetable + ) { + TimetableConfigDialog(activity, reloadOnDismiss = false).show() + }.takeIf { app.profile.hasUIFeature(FeatureType.TIMETABLE) }, + + util.createActionItem( + text = R.string.menu_agenda_config, + icon = CommunityMaterial.Icon.cmd_calendar_outline + ) { + AgendaConfigDialog(activity, reloadOnDismiss = false).show() + }.takeIf { app.profile.hasUIFeature(FeatureType.AGENDA) }, + + util.createActionItem( + text = R.string.menu_grades_config, + icon = CommunityMaterial.Icon3.cmd_numeric_5_box_outline + ) { + GradesConfigDialog(activity, reloadOnDismiss = false).show() + }.takeIf { app.profile.hasUIFeature(FeatureType.GRADES) }, + + util.createActionItem( + text = R.string.menu_messages_config, + icon = CommunityMaterial.Icon.cmd_email_outline + ) { + MessagesConfigDialog(activity, reloadOnDismiss = false).show() + }.takeIf { + app.profile.hasUIFeature(FeatureType.MESSAGES_INBOX) || app.profile.hasUIFeature( + FeatureType.MESSAGES_SENT) + }, + + util.createActionItem( + text = R.string.menu_attendance_config, + icon = CommunityMaterial.Icon.cmd_calendar_remove_outline + ) { + AttendanceConfigDialog(activity, reloadOnDismiss = false).show() + }.takeIf { app.profile.hasUIFeature(FeatureType.ATTENDANCE) }, + + util.createMoreItem( + card = card, + items = listOfNotNull( + util.createActionItem( + text = R.string.settings_register_bell_sync_text, + icon = SzkolnyFont.Icon.szf_alarm_bell_outline, + onClick = { + BellSyncConfigDialog(activity, onChangeListener = { + it.subText = getBellSync() + util.refresh() + }).show() + } + ).also { + it.subText = getBellSync() + }, + + util.createPropertyItem( + text = R.string.settings_register_count_in_seconds_text, + subText = R.string.settings_register_count_in_seconds_subtext, + icon = CommunityMaterial.Icon3.cmd_timer_outline, + value = configGlobal.timetable.countInSeconds + ) { _, it -> + configGlobal.timetable.countInSeconds = it + }, + + util.createPropertyItem( + text = R.string.settings_register_show_teacher_absences_text, + icon = CommunityMaterial.Icon.cmd_account_arrow_right_outline, + value = app.profile.getStudentData("showTeacherAbsences", true) + ) { _, it -> + app.profile["showTeacherAbsences"] = it + app.profileSave() + }.takeIf { app.profile.loginStoreType == LoginType.LIBRUS }, + + util.createPropertyItem( + text = R.string.settings_register_hide_sticks_from_old, + icon = CommunityMaterial.Icon3.cmd_numeric_1_box_outline, + value = configProfile.grades.hideSticksFromOld + ) { _, it -> + configProfile.grades.hideSticksFromOld = it + }.takeIf { App.devMode && app.profile.hasUIFeature(FeatureType.GRADES) }, + ), + ), + + *(getRegistrationItems().takeIf { !app.profile.archived } ?: arrayOf()), + ) + + private fun getRegistrationItems() = listOfNotNull( + util.createSectionItem( + text = R.string.settings_registration_section, + ), + + util.createPropertyItem( + text = R.string.settings_register_allow_registration_text, + subText = R.string.settings_register_allow_registration_subtext, + icon = CommunityMaterial.Icon.cmd_account_circle_outline, + value = app.profile.canShare, + beforeChange = + { item, value -> + if (app.profile.canShare == value) + // allow the switch to change - needed for util.refresh() to change the visual state + return@createPropertyItem true + val dialog = + RegistrationConfigDialog(activity, + app.profile, + onChangeListener = { enabled -> + if (item.isChecked == enabled) + return@RegistrationConfigDialog + item.isChecked = enabled + if (value) { + card.items.after(item, sharedEventsDefaultItem) + } else { + card.items.remove(sharedEventsDefaultItem) + } + util.refresh() + }) + if (value) + dialog.showEnableDialog() + else + dialog.showDisableDialog() + false + } + ) { _, _ -> }, + + sharedEventsDefaultItem.takeIf { app.profile.canShare }, + ).toTypedArray() +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsSyncCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/cards/SettingsSyncCard.kt similarity index 88% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsSyncCard.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/cards/SettingsSyncCard.kt index 1d42b86b..a8a68da1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsSyncCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/cards/SettingsSyncCard.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2021-3-18. */ -package pl.szczodrzynski.edziennik.ui.modules.settings.cards +package pl.szczodrzynski.edziennik.ui.settings.cards import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_NEW_TASK @@ -10,26 +10,28 @@ import android.net.Uri import android.os.Build.VERSION.SDK_INT import android.os.Build.VERSION_CODES import android.provider.Settings +import com.danielstone.materialaboutlibrary.model.MaterialAboutCard import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.after -import pl.szczodrzynski.edziennik.getSyncInterval +import pl.szczodrzynski.edziennik.ext.after +import pl.szczodrzynski.edziennik.ext.getSyncInterval import pl.szczodrzynski.edziennik.sync.SyncWorker import pl.szczodrzynski.edziennik.sync.UpdateWorker -import pl.szczodrzynski.edziennik.ui.dialogs.sync.NotificationFilterDialog -import pl.szczodrzynski.edziennik.ui.dialogs.sync.QuietHoursConfigDialog -import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncIntervalDialog -import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsCard -import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsUtil +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget +import pl.szczodrzynski.edziennik.ui.dialogs.settings.NotificationFilterDialog +import pl.szczodrzynski.edziennik.ui.dialogs.settings.QuietHoursConfigDialog +import pl.szczodrzynski.edziennik.ui.dialogs.settings.SyncIntervalDialog +import pl.szczodrzynski.edziennik.ui.settings.SettingsCard +import pl.szczodrzynski.edziennik.ui.settings.SettingsUtil import pl.szczodrzynski.edziennik.utils.models.Time class SettingsSyncCard(util: SettingsUtil) : SettingsCard(util) { override fun buildCard() = util.createCard( R.string.settings_card_sync_title, - items = getItems(), - itemsMore = getItemsMore() + items = ::getItems, + itemsMore = ::getItemsMore, ) private fun getQuietHours(): String { @@ -62,7 +64,7 @@ class SettingsSyncCard(util: SettingsUtil) : SettingsCard(util) { } } - override fun getItems() = listOfNotNull( + override fun getItems(card: MaterialAboutCard) = listOfNotNull( util.createPropertyActionItem( text = R.string.settings_sync_sync_interval_text, subText = R.string.settings_sync_sync_interval_subtext_disabled, @@ -108,7 +110,7 @@ class SettingsSyncCard(util: SettingsUtil) : SettingsCard(util) { item.onCheckedChangedAction.onCheckedChanged(item, true) if (configGlobal.sync.enabled) util.refresh() - }) + }).show() } ).also { it.subTextChecked = activity.getSyncInterval(configGlobal.sync.interval) @@ -124,7 +126,7 @@ class SettingsSyncCard(util: SettingsUtil) : SettingsCard(util) { subText = R.string.settings_profile_notifications_subtext, icon = CommunityMaterial.Icon2.cmd_filter_outline ) { - NotificationFilterDialog(activity) + NotificationFilterDialog(activity).show() }, util.createPropertyActionItem( @@ -151,11 +153,11 @@ class SettingsSyncCard(util: SettingsUtil) : SettingsCard(util) { subText = R.string.settings_sync_web_push_subtext, icon = CommunityMaterial.Icon2.cmd_laptop ) { - activity.loadTarget(MainActivity.TARGET_WEB_PUSH) + activity.navigate(navTarget = NavTarget.WEB_PUSH) } ) - override fun getItemsMore() = listOfNotNull( + override fun getItemsMore(card: MaterialAboutCard) = listOfNotNull( util.createPropertyItem( text = R.string.settings_sync_updates_text, icon = CommunityMaterial.Icon.cmd_cellphone_arrow_down, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsThemeCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/cards/SettingsThemeCard.kt similarity index 81% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsThemeCard.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/cards/SettingsThemeCard.kt index 17b108e5..ccf884fa 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/settings/cards/SettingsThemeCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/cards/SettingsThemeCard.kt @@ -2,16 +2,18 @@ * Copyright (c) Kuba Szczodrzyński 2021-3-18. */ -package pl.szczodrzynski.edziennik.ui.modules.settings.cards +package pl.szczodrzynski.edziennik.ui.settings.cards +import com.danielstone.materialaboutlibrary.model.MaterialAboutCard import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.ui.dialogs.settings.AppLanguageDialog import pl.szczodrzynski.edziennik.ui.dialogs.settings.MiniMenuConfigDialog import pl.szczodrzynski.edziennik.ui.dialogs.settings.ThemeChooserDialog -import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsCard -import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsUtil +import pl.szczodrzynski.edziennik.ui.settings.SettingsCard +import pl.szczodrzynski.edziennik.ui.settings.SettingsUtil +import pl.szczodrzynski.edziennik.utils.BigNightUtil import pl.szczodrzynski.edziennik.utils.Themes import pl.szczodrzynski.edziennik.utils.models.Date @@ -19,11 +21,11 @@ class SettingsThemeCard(util: SettingsUtil) : SettingsCard(util) { override fun buildCard() = util.createCard( R.string.settings_card_theme_title, - items = getItems(), - itemsMore = getItemsMore() + items = ::getItems, + itemsMore = ::getItemsMore, ) - override fun getItems() = listOfNotNull( + override fun getItems(card: MaterialAboutCard) = listOfNotNull( if (Date.getToday().month % 11 == 1) // cool math games util.createPropertyItem( text = R.string.settings_theme_snowfall_text, @@ -36,12 +38,24 @@ class SettingsThemeCard(util: SettingsUtil) : SettingsCard(util) { } else null, + if (BigNightUtil().isDataWielkanocyNearDzisiaj()) // cool klasa for utility to dzień wielkanocy + util.createPropertyItem( + text = R.string.settings_theme_eggfall_text, + subText = R.string.settings_theme_eggfall_subtext, + icon = CommunityMaterial.Icon.cmd_egg_easter, + value = configGlobal.ui.eggfall + ) { _, it -> + configGlobal.ui.eggfall = it + activity.recreate() + } + else null, + util.createActionItem( text = R.string.settings_theme_theme_text, subText = Themes.getThemeNameRes(), icon = CommunityMaterial.Icon3.cmd_palette_outline ) { - ThemeChooserDialog(activity) + ThemeChooserDialog(activity).show() }, util.createActionItem( @@ -49,7 +63,7 @@ class SettingsThemeCard(util: SettingsUtil) : SettingsCard(util) { subText = R.string.settings_about_language_subtext, icon = CommunityMaterial.Icon3.cmd_translate ) { - AppLanguageDialog(activity) + AppLanguageDialog(activity).show() }, util.createPropertyItem( @@ -63,19 +77,19 @@ class SettingsThemeCard(util: SettingsUtil) : SettingsCard(util) { } ) - override fun getItemsMore() = listOf( + override fun getItemsMore(card: MaterialAboutCard) = listOf( util.createActionItem( text = R.string.settings_theme_mini_drawer_buttons_text, icon = CommunityMaterial.Icon2.cmd_format_list_checks ) { - MiniMenuConfigDialog(activity) + MiniMenuConfigDialog(activity).show() }, util.createActionItem( text = R.string.settings_theme_drawer_header_text, icon = CommunityMaterial.Icon2.cmd_image_outline ) { - if (app.config.ui.appBackground == null) { + if (app.config.ui.headerBackground == null) { setHeaderBackground() return@createActionItem } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/contributors/ContributorsActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/contributors/ContributorsActivity.kt new file mode 100644 index 00000000..9acbc0b6 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/contributors/ContributorsActivity.kt @@ -0,0 +1,143 @@ +package pl.szczodrzynski.edziennik.ui.settings.contributors + +import android.os.Bundle +import android.os.Process +import android.view.KeyEvent +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isInvisible +import androidx.core.view.isVisible +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi +import pl.szczodrzynski.edziennik.data.api.szkolny.response.ContributorsResponse +import pl.szczodrzynski.edziennik.databinding.ContributorsActivityBinding +import pl.szczodrzynski.edziennik.ext.Bundle +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ext.onLongClick +import pl.szczodrzynski.edziennik.ui.base.lazypager.FragmentLazyPagerAdapter +import pl.szczodrzynski.edziennik.ui.error.ErrorSnackbar +import kotlin.coroutines.CoroutineContext +import kotlin.system.exitProcess + +class ContributorsActivity : AppCompatActivity(), CoroutineScope { + companion object { + private const val TAG = "ContributorsActivity" + private var contributors: ContributorsResponse? = null + } + + private lateinit var app: App + private lateinit var b: ContributorsActivityBinding + + private var job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + // local/private variables go here + private val errorSnackbar: ErrorSnackbar by lazy { ErrorSnackbar(this) } + + private var konami = 0 + + override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean { + when (keyCode) { + KeyEvent.KEYCODE_DPAD_UP -> konami in 0..1 + KeyEvent.KEYCODE_DPAD_DOWN -> konami in 2..3 + KeyEvent.KEYCODE_DPAD_LEFT -> konami in 4..6 step 2 + KeyEvent.KEYCODE_DPAD_RIGHT -> konami in 5..7 step 2 + KeyEvent.KEYCODE_B -> konami == 8 + KeyEvent.KEYCODE_A -> konami == 9 + else -> false + }.let { + if (!it) { + konami = 0 + return super.onKeyUp(keyCode, event) + } + konami++ + b.konami.isVisible = konami == 10 + return true + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + app = application as App + b = ContributorsActivityBinding.inflate(layoutInflater) + setContentView(b.root) + + b.progressBar.isVisible = true + b.tabLayout.isVisible = false + b.viewPager.isVisible = false + + b.szkolny.onLongClick { + if (b.konami.isVisible) { + b.glove.isVisible = true + b.szkolny.isInvisible = true + } + true + } + + b.glove.onClick { + MaterialAlertDialogBuilder(this) + .setTitle(R.string.are_you_sure) + .setMessage(R.string.dev_mode_enable_warning) + .setPositiveButton(R.string.yes) { _, _ -> + app.config.devMode = true + App.devMode = true + MaterialAlertDialogBuilder(this) + .setTitle("Restart") + .setMessage("Wymagany restart aplikacji") + .setPositiveButton(R.string.ok) { _, _ -> + Process.killProcess(Process.myPid()) + Runtime.getRuntime().exit(0) + exitProcess(0) + } + .setCancelable(false) + .show() + } + .setNegativeButton(R.string.no) { _, _ -> + app.config.devMode = false + App.devMode = false + this.finish() + } + .show() + } + + launch { + contributors = contributors ?: SzkolnyApi(app).runCatching(errorSnackbar) { + getContributors() + } ?: return@launch + + val pagerAdapter = FragmentLazyPagerAdapter( + supportFragmentManager, + fragments = listOf( + ContributorsFragment().apply { + arguments = Bundle( + "items" to contributors!!.contributors.toTypedArray(), + "quantityPluralRes" to R.plurals.contributions_quantity, + ) + } to getString(R.string.contributors), + + ContributorsFragment().apply { + arguments = Bundle( + "items" to contributors!!.translators.toTypedArray(), + "quantityPluralRes" to R.plurals.translations_quantity, + ) + } to getString(R.string.translators), + ) + ) + + b.viewPager.apply { + offscreenPageLimit = 1 + adapter = pagerAdapter + b.tabLayout.setupWithViewPager(this) + } + + b.progressBar.isVisible = false + b.tabLayout.isVisible = true + b.viewPager.isVisible = true + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/contributors/ContributorsAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/contributors/ContributorsAdapter.kt new file mode 100644 index 00000000..4fe77a84 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/contributors/ContributorsAdapter.kt @@ -0,0 +1,63 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-9-7. + */ + +package pl.szczodrzynski.edziennik.ui.settings.contributors + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.annotation.PluralsRes +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.RecyclerView +import coil.load +import coil.transform.CircleCropTransformation +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.api.szkolny.response.ContributorsResponse +import pl.szczodrzynski.edziennik.databinding.ContributorsListItemBinding +import pl.szczodrzynski.edziennik.ext.plural +import pl.szczodrzynski.edziennik.ext.setText +import pl.szczodrzynski.edziennik.utils.Utils + +class ContributorsAdapter( + val activity: AppCompatActivity, + val items: List, + @PluralsRes + val quantityPluralRes: Int +) : RecyclerView.Adapter() { + companion object { + private const val TAG = "ContributorsAdapter" + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(parent.context) + val view = ContributorsListItemBinding.inflate(inflater, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = items[position] + val b = holder.b + + b.text.text = item.name ?: item.login + b.subtext.setText( + R.string.contributors_subtext_format, + item.login, + activity.plural( + quantityPluralRes, + item.contributions ?: 0 + ) + ) + + b.image.load(item.avatarUrl) { + transformations(CircleCropTransformation()) + } + + b.root.setOnClickListener { + Utils.openUrl(activity, item.itemUrl) + } + } + + override fun getItemCount() = items.size + + class ViewHolder(val b: ContributorsListItemBinding) : RecyclerView.ViewHolder(b.root) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/contributors/ContributorsFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/contributors/ContributorsFragment.kt new file mode 100644 index 00000000..d378848a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/settings/contributors/ContributorsFragment.kt @@ -0,0 +1,47 @@ +package pl.szczodrzynski.edziennik.ui.settings.contributors + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.data.api.szkolny.response.ContributorsResponse +import pl.szczodrzynski.edziennik.databinding.ContributorsListFragmentBinding +import pl.szczodrzynski.edziennik.ui.base.lazypager.LazyFragment +import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration + +class ContributorsFragment : LazyFragment() { + companion object { + private const val TAG = "ContributorsFragment" + } + + private lateinit var app: App + private lateinit var activity: ContributorsActivity + private lateinit var b: ContributorsListFragmentBinding + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + activity = (getActivity() as ContributorsActivity?) ?: return null + context ?: return null + app = activity.application as App + b = ContributorsListFragmentBinding.inflate(inflater) + return b.root + } + + override fun onPageCreated(): Boolean { + val contributorsArray = requireArguments().getParcelableArray("items") as Array + val contributors = contributorsArray.toList() + val quantityPluralRes = requireArguments().getInt("quantityPluralRes") + + val adapter = ContributorsAdapter(activity, contributors, quantityPluralRes) + b.list.adapter = adapter + b.list.apply { + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + addItemDecoration(SimpleDividerItemDecoration(context)) + addOnScrollListener(onScrollListener) + } + + return true + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/teachers/TeachersAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/teachers/TeachersAdapter.kt new file mode 100644 index 00000000..28a6541c --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/teachers/TeachersAdapter.kt @@ -0,0 +1,85 @@ +/* + * Copyright (c) Antoni Czaplicki 2021-10-15. + */ + +package pl.szczodrzynski.edziennik.ui.teachers + +import android.content.Intent +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.Subject +import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.databinding.TeacherItemBinding +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget +import kotlin.coroutines.CoroutineContext + +class TeachersAdapter( + private val activity: AppCompatActivity, + val onItemClick: ((item: Teacher) -> Unit)? = null, +) : RecyclerView.Adapter(), CoroutineScope { + companion object { + private const val TAG = "TeachersAdapter" + } + + private val app = activity.applicationContext as App + // optional: place the manager here + + private val job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + var items = listOf() + + var subjectList = listOf() + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val inflater = LayoutInflater.from(activity) + val view = TeacherItemBinding.inflate(inflater, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val item = items[position] + val b = holder.b + + b.name.text = item.fullName + b.image.setImageBitmap(item.image) + b.type.text = item.getTypeText(activity, subjectList) + b.copy.isVisible = true + b.copy.onClick { + item.fullName.copyToClipboard(activity) + Toast.makeText(activity, R.string.copied_to_clipboard, Toast.LENGTH_SHORT).show() + } + b.copy.attachToastHint(R.string.copy_to_clipboard) + if (item.loginId.isNotNullNorBlank()) { + b.sendMessage.isVisible = true + + b.sendMessage.onClick { + val intent = Intent( + Intent.ACTION_MAIN, + "fragmentId" to NavTarget.MESSAGE_COMPOSE, + "messageRecipientId" to item.id + ) + activity.sendBroadcast(intent) + } + b.sendMessage.attachToastHint(R.string.send_message) + } else { + b.sendMessage.isVisible = false + } + } + + override fun getItemCount() = items.size + + class ViewHolder(val b: TeacherItemBinding) : RecyclerView.ViewHolder(b.root) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/teachers/TeachersListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/teachers/TeachersListFragment.kt new file mode 100644 index 00000000..c95b4050 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/teachers/TeachersListFragment.kt @@ -0,0 +1,89 @@ +/* + * Copyright (c) Antoni Czaplicki 2021-10-15. + */ + +package pl.szczodrzynski.edziennik.ui.teachers + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.withContext +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.databinding.TeachersListFragmentBinding +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer +import pl.szczodrzynski.edziennik.ui.messages.MessagesUtils +import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration +import kotlin.coroutines.CoroutineContext + +class TeachersListFragment : Fragment(), CoroutineScope { + companion object { + private const val TAG = "TeachersListFragment" + } + + private lateinit var app: App + private lateinit var activity: MainActivity + private lateinit var b: TeachersListFragmentBinding + + private val job: Job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + 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 = TeachersListFragmentBinding.inflate(inflater) + return b.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { startCoroutineTimer(100L) { + if (!isAdded) return@startCoroutineTimer + + val adapter = TeachersAdapter(activity) + + adapter.subjectList = withContext(Dispatchers.IO) { + App.db.subjectDao().getAllNow(App.profileId) + } + + app.db.teacherDao().getAllTeachers(App.profileId).observe(viewLifecycleOwner, Observer { items -> + if (!isAdded) return@Observer + + // load & configure the adapter + adapter.items = items.sortedWith(compareBy( + { it.subjects.isEmpty() }, + { it.type == 0 }, + )) + adapter.items.forEach { + it.image = it.image ?: MessagesUtils.getProfileImage(48, 24, 16, 12, 1, it.fullName) + } + if (items.isNotNullNorEmpty() && b.list.adapter == null) { + b.list.adapter = adapter + b.list.apply { + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + addItemDecoration(SimpleDividerItemDecoration(context)) + } + } + adapter.notifyDataSetChanged() + + // show/hide relevant views + b.progressBar.isVisible = false + if (items.isNullOrEmpty()) { + b.list.isVisible = false + b.noData.isVisible = true + } else { + b.list.isVisible = true + b.noData.isVisible = false + } + }) + }} +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplateAdapter.kt similarity index 91% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplateAdapter.kt index fe78396f..9e5300cb 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplateAdapter.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-3-30. */ -package pl.szczodrzynski.edziennik.ui.modules.template +package pl.szczodrzynski.edziennik.ui.template import android.view.LayoutInflater import android.view.ViewGroup @@ -11,9 +11,10 @@ import androidx.recyclerview.widget.RecyclerView import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.data.db.entity.Notification import pl.szczodrzynski.edziennik.databinding.TemplateListItemBinding +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.models.Date import kotlin.coroutines.CoroutineContext @@ -53,7 +54,7 @@ class TemplateAdapter( " • ", date ).concat().asColoredSpannable(colorSecondary) - b.type.text = activity.getNotificationTitle(item.type) + b.type.text = item.type.titleRes.resolveString(app) onItemClick?.let { listener -> b.root.onClick { listener(item) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplateDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplateDialog.kt new file mode 100644 index 00000000..a1f3b088 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplateDialog.kt @@ -0,0 +1,80 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2020-3-30. + */ + +package pl.szczodrzynski.edziennik.ui.template + +import android.view.LayoutInflater +import androidx.appcompat.app.AppCompatActivity +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.DialogTemplateBinding +import pl.szczodrzynski.edziennik.ui.dialogs.base.BaseDialog +import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog +import pl.szczodrzynski.edziennik.ui.dialogs.base.ViewDialog + +/** + * This class represents a sample dialog using the new style. + * + * A dialog may subclass the [BaseDialog], [ViewDialog] or [BindingDialog]. + * + * Fields and methods have the preferred order which should be used when writing new code. + * The position of the first occurrence of duplicated methods should be used. + * Multi-line methods should be followed by a blank line, one-liners may be just joined together. + * + * Constructor properties should be private. + * + * [onShow], when not used, should be placed just before the local variables, as a one-liner. + * All other multi-line methods go below the local variables part. + */ +class TemplateDialog( + activity: AppCompatActivity, + private val onActionPerformed: (() -> Unit)? = null, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BindingDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "TemplateDialog" + + override fun getTitle(): CharSequence = "Template" + override fun getTitleRes() = R.string.menu_template + override fun inflate(layoutInflater: LayoutInflater) = + DialogTemplateBinding.inflate(layoutInflater) + + // override fun getTitle(): CharSequence = "Template" + // override fun getTitleRes() = R.string.menu_template + // override fun getMessage() = "" + // override fun getMessageRes() = R.string.edziennik_progress_login_template_api + // override fun getMessageFormat() = + // R.string.card_update_text_format to listOf( + // BuildConfig.VERSION_BASE, + // "5.0", + // ) + + // override fun getTitleRes() = R.string.menu_template + override fun isCancelable() = true + override fun getPositiveButtonText() = R.string.ok + override fun getNeutralButtonText() = R.string.reset + override fun getNegativeButtonText() = R.string.cancel + + // getSingleChoiceItem / getMultiChoiceItems + // getDefaultSelectedItem / getDefaultSelectedItems + + // to convert a map of StringIDs to CharSequences + // .mapKeys { (resId, _) -> activity.getString(resId) } + + override suspend fun onShow() = Unit + + // local variables go here + + // onPositiveClick + // onNeutralClick + // onNegativeClick + + // onSingleSelectionChanged + // onMultiSelectionChanged + + // getRootView + // onBeforeShow + // onShow + // onDismiss +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplateFragment.kt similarity index 84% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplateFragment.kt index 8adee96a..0e21a080 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplateFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-3-30. */ -package pl.szczodrzynski.edziennik.ui.modules.template +package pl.szczodrzynski.edziennik.ui.template import android.os.Bundle import android.view.LayoutInflater @@ -12,11 +12,15 @@ import androidx.fragment.app.Fragment import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.MainActivity +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.databinding.TemplateFragmentBinding -import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.FragmentLazyPagerAdapter -import pl.szczodrzynski.edziennik.ui.modules.homework.HomeworkDate -import pl.szczodrzynski.edziennik.ui.modules.homework.HomeworkListFragment +import pl.szczodrzynski.edziennik.ext.Bundle +import pl.szczodrzynski.edziennik.ext.addOnPageSelectedListener +import pl.szczodrzynski.edziennik.ui.base.lazypager.FragmentLazyPagerAdapter +import pl.szczodrzynski.edziennik.ui.homework.HomeworkDate +import pl.szczodrzynski.edziennik.ui.homework.HomeworkListFragment import kotlin.coroutines.CoroutineContext class TemplateFragment : Fragment(), CoroutineScope { @@ -48,7 +52,7 @@ class TemplateFragment : Fragment(), CoroutineScope { if (!isAdded) return val pagerAdapter = FragmentLazyPagerAdapter( - fragmentManager ?: return, + parentFragmentManager, b.refreshLayout, listOf( HomeworkListFragment().apply { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplateListFragment.kt similarity index 92% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateListFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplateListFragment.kt index c044c4cf..3bedf547 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplateListFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-3-30. */ -package pl.szczodrzynski.edziennik.ui.modules.template +package pl.szczodrzynski.edziennik.ui.template import android.os.Bundle import android.view.LayoutInflater @@ -17,6 +17,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.databinding.TemplateListFragmentBinding +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.ext.onScrollListener +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import kotlin.coroutines.CoroutineContext diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateListPageFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplateListPageFragment.kt similarity index 92% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateListPageFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplateListPageFragment.kt index e2395668..efddb51d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplateListPageFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplateListPageFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-3-30. */ -package pl.szczodrzynski.edziennik.ui.modules.template +package pl.szczodrzynski.edziennik.ui.template import android.os.Bundle import android.view.LayoutInflater @@ -17,9 +17,9 @@ import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.databinding.TemplateListPageFragmentBinding -import pl.szczodrzynski.edziennik.isNotNullNorEmpty -import pl.szczodrzynski.edziennik.startCoroutineTimer -import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer +import pl.szczodrzynski.edziennik.ui.base.lazypager.LazyFragment import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import kotlin.coroutines.CoroutineContext diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplatePageFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplatePageFragment.kt similarity index 92% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplatePageFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplatePageFragment.kt index f2404ea4..860536ce 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplatePageFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplatePageFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-3-30. */ -package pl.szczodrzynski.edziennik.ui.modules.template +package pl.szczodrzynski.edziennik.ui.template import android.os.Bundle import android.view.LayoutInflater @@ -14,7 +14,7 @@ import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.databinding.TemplatePageFragmentBinding -import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment +import pl.szczodrzynski.edziennik.ui.base.lazypager.LazyFragment import kotlin.coroutines.CoroutineContext class TemplatePageFragment : LazyFragment(), CoroutineScope { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplatePagerAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplatePagerAdapter.kt similarity index 78% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplatePagerAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplatePagerAdapter.kt index eaa8ba1d..7cef63ec 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/template/TemplatePagerAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/template/TemplatePagerAdapter.kt @@ -2,11 +2,11 @@ * Copyright (c) Kuba Szczodrzyński 2020-3-30. */ -package pl.szczodrzynski.edziennik.ui.modules.template +package pl.szczodrzynski.edziennik.ui.template import androidx.fragment.app.FragmentManager import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyPagerAdapter +import pl.szczodrzynski.edziennik.ui.base.lazypager.LazyPagerAdapter class TemplatePagerAdapter(fragmentManager: FragmentManager, swipeRefreshLayout: SwipeRefreshLayout) : LazyPagerAdapter(fragmentManager, swipeRefreshLayout) { override fun getPage(position: Int) = TemplatePageFragment() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/GenerateBlockTimetableDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/GenerateBlockTimetableDialog.kt similarity index 90% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/GenerateBlockTimetableDialog.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/GenerateBlockTimetableDialog.kt index c93d5a6e..66401d31 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/timetable/GenerateBlockTimetableDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/GenerateBlockTimetableDialog.kt @@ -2,13 +2,15 @@ * Copyright (c) Kacper Ziubryniewicz 2020-1-5 */ -package pl.szczodrzynski.edziennik.ui.dialogs.timetable +package pl.szczodrzynski.edziennik.ui.timetable +import android.content.ContentResolver +import android.content.ContentValues import android.content.Intent import android.graphics.* -import android.net.Uri import android.os.Build import android.os.Environment +import android.provider.MediaStore import android.util.Log import android.view.View import android.view.View.MeasureSpec @@ -17,7 +19,6 @@ import android.widget.TextView import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.cardview.widget.CardView -import androidx.core.content.FileProvider import com.google.android.material.datepicker.MaterialDatePicker import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.* @@ -30,13 +31,14 @@ import pl.szczodrzynski.edziennik.data.api.events.ApiTaskAllFinishedEvent import pl.szczodrzynski.edziennik.data.api.events.ApiTaskErrorEvent import pl.szczodrzynski.edziennik.data.api.events.ApiTaskFinishedEvent import pl.szczodrzynski.edziennik.data.db.entity.Lesson +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType import pl.szczodrzynski.edziennik.data.db.full.LessonFull import pl.szczodrzynski.edziennik.databinding.DialogGenerateBlockTimetableBinding +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Week import java.io.File -import java.io.FileOutputStream import kotlin.coroutines.CoroutineContext import kotlin.math.roundToInt @@ -107,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() } } }} @@ -201,9 +201,7 @@ class GenerateBlockTimetableDialog( EdziennikTask.syncProfile( profileId = App.profileId, - viewIds = listOf( - MainActivity.DRAWER_ITEM_TIMETABLE to 0 - ), + featureTypes = setOf(FeatureType.TIMETABLE), arguments = JsonObject( "weekStart" to weekStart.stringY_m_d ) @@ -373,25 +371,31 @@ class GenerateBlockTimetableDialog( val today = Date.getToday().stringY_m_d val now = Time.getNow().stringH_M_S + val filename = "plan_lekcji_${app.profile.name}_${today}_${now}.png" + val resolver: ContentResolver = activity.applicationContext.contentResolver + val values = ContentValues() + values.put(MediaStore.MediaColumns.MIME_TYPE, "image/png") - val outputDir = Environment.getExternalStoragePublicDirectory("Szkolny.eu").apply { mkdirs() } - val outputFile = File(outputDir, "plan_lekcji_${app.profile.name}_${today}_${now}.png") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + values.put(MediaStore.MediaColumns.DISPLAY_NAME, filename) + values.put(MediaStore.MediaColumns.RELATIVE_PATH, File(Environment.DIRECTORY_PICTURES, "Szkolny.eu").path) + } else { + val picturesDirectory = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "Szkolny.eu") + picturesDirectory.mkdirs() + values.put(MediaStore.MediaColumns.DATA, File(picturesDirectory, filename).path) + } try { - val fos = FileOutputStream(outputFile) - bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos) - fos.close() + 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) + } + uri } catch (e: Exception) { Log.e("SAVE_IMAGE", e.message, e) return@withContext null } - val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - FileProvider.getUriForFile(activity, app.packageName + ".provider", outputFile) - } else { - Uri.parse("file://" + outputFile.absolutePath) - } - uri } progressDialog.dismiss() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/LessonDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/LessonDetailsDialog.kt new file mode 100644 index 00000000..536e912f --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/LessonDetailsDialog.kt @@ -0,0 +1,266 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2019-11-11. + */ + +package pl.szczodrzynski.edziennik.ui.timetable + +import android.content.Intent +import android.view.LayoutInflater +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.Lesson +import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull +import pl.szczodrzynski.edziennik.data.db.full.LessonFull +import pl.szczodrzynski.edziennik.databinding.DialogLessonDetailsBinding +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ext.setText +import pl.szczodrzynski.edziennik.ui.attendance.AttendanceDetailsDialog +import pl.szczodrzynski.edziennik.ui.dialogs.base.BindingDialog +import pl.szczodrzynski.edziennik.ui.event.EventDetailsDialog +import pl.szczodrzynski.edziennik.ui.event.EventListAdapter +import pl.szczodrzynski.edziennik.ui.event.EventManualDialog +import pl.szczodrzynski.edziennik.ui.notes.setupNotesButton +import pl.szczodrzynski.edziennik.utils.BetterLink +import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration +import pl.szczodrzynski.edziennik.utils.managers.NoteManager +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Week + +class LessonDetailsDialog( + activity: AppCompatActivity, + private val lesson: LessonFull, + private val attendance: AttendanceFull? = null, + private val showNotes: Boolean = true, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, +) : BindingDialog(activity, onShowListener, onDismissListener) { + + override val TAG = "LessonDetailsDialog" + + override fun getTitleRes(): Int? = null + override fun inflate(layoutInflater: LayoutInflater) = + DialogLessonDetailsBinding.inflate(layoutInflater) + + override fun getPositiveButtonText() = R.string.close + override fun getNeutralButtonText() = R.string.add + + private lateinit var adapter: EventListAdapter + private val manager + get() = app.timetableManager + private val attendanceManager + get() = app.attendanceManager + + override suspend fun onNeutralClick(): Boolean { + EventManualDialog( + activity, + lesson.profileId, + defaultLesson = lesson, + onShowListener = onShowListener, + onDismissListener = onDismissListener + ).show() + return NO_DISMISS + } + + override suspend fun onShow() { + if (App.devMode) + b.lessonId.visibility = View.VISIBLE + + b.lesson = lesson + val lessonDate = lesson.displayDate ?: return + val lessonTime = lesson.displayStartTime ?: return + b.lessonDate.text = + Week.getFullDayName(lessonDate.weekDay) + ", " + lessonDate.formattedString + + b.annotationVisible = manager.getAnnotation(activity, lesson, b.annotation) + + if (lesson.type >= Lesson.TYPE_SHIFTED_SOURCE) { + b.shiftedLayout.visibility = View.VISIBLE + var otherLessonDate: Date? = null + when (lesson.type) { + Lesson.TYPE_SHIFTED_SOURCE -> { + otherLessonDate = lesson.date + when { + lesson.date != lesson.oldDate -> b.shiftedText.setText( + R.string.timetable_lesson_shifted_other_day, + lesson.date?.stringY_m_d ?: "?", + lesson.startTime?.stringHM ?: "?" + ) + lesson.startTime != lesson.oldStartTime -> b.shiftedText.setText( + R.string.timetable_lesson_shifted_same_day, + lesson.startTime?.stringHM ?: "?" + ) + else -> b.shiftedText.setText(R.string.timetable_lesson_shifted) + } + } + Lesson.TYPE_SHIFTED_TARGET -> { + otherLessonDate = lesson.oldDate + when { + lesson.date != lesson.oldDate -> b.shiftedText.setText( + R.string.timetable_lesson_shifted_from_other_day, + lesson.oldDate?.stringY_m_d ?: "?", + lesson.oldStartTime?.stringHM ?: "?" + ) + lesson.startTime != lesson.oldStartTime -> b.shiftedText.setText( + R.string.timetable_lesson_shifted_from_same_day, + lesson.oldStartTime?.stringHM ?: "?" + ) + else -> b.shiftedText.setText(R.string.timetable_lesson_shifted_from) + } + } + } + b.shiftedGoTo.setOnClickListener { + dialog.dismiss() + val dateStr = otherLessonDate?.stringY_m_d ?: return@setOnClickListener + val intent = Intent(TimetableFragment.ACTION_SCROLL_TO_DATE).apply { + putExtra("timetableDate", dateStr) + } + activity.sendBroadcast(intent) + } + } else { + b.shiftedLayout.visibility = View.GONE + } + + if (lesson.type < Lesson.TYPE_SHIFTED_SOURCE && lesson.oldSubjectId != null && lesson.subjectId != lesson.oldSubjectId) { + b.oldSubjectName = lesson.oldSubjectName + } + if (lesson.type != Lesson.TYPE_CANCELLED && lesson.displaySubjectId != null) { + b.subjectName = lesson.subjectName + } + + if (lesson.type < Lesson.TYPE_SHIFTED_SOURCE && lesson.oldTeacherId != null && lesson.teacherId != lesson.oldTeacherId) { + b.oldTeacherName = lesson.oldTeacherName + } + if (lesson.type != Lesson.TYPE_CANCELLED && lesson.displayTeacherId != null) { + b.teacherName = lesson.teacherName + } + + if (lesson.oldClassroom != null && lesson.classroom != lesson.oldClassroom) { + b.oldClassroom = lesson.oldClassroom + } + if (lesson.type != Lesson.TYPE_CANCELLED && lesson.displayClassroom != null) { + b.classroom = lesson.classroom + } + + if (lesson.type < Lesson.TYPE_SHIFTED_SOURCE && lesson.oldTeamId != null && lesson.teamId != lesson.oldTeamId) { + b.oldTeamName = lesson.oldTeamName + } + if (lesson.type != Lesson.TYPE_CANCELLED && lesson.displayTeamId != null) { + b.teamName = lesson.teamName + } + + b.attendanceDivider.isVisible = attendance != null + b.attendanceLayout.isVisible = attendance != null + if (attendance != null) { + b.attendanceView.setAttendance(attendance, app.attendanceManager, bigView = true) + b.attendanceType.text = attendance.typeName + b.attendanceIcon.isVisible = attendance.let { + val icon = attendanceManager.getAttendanceIcon(it) ?: return@let false + val color = attendanceManager.getAttendanceColor(it) + b.attendanceIcon.setImageDrawable( + IconicsDrawable(activity, icon).apply { + colorInt = color + sizeDp = 24 + } + ) + true + } + b.attendanceDetails.onClick { + AttendanceDetailsDialog( + activity = activity, + attendance = attendance, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ).show() + } + } + + adapter = EventListAdapter( + activity, + showWeekDay = false, + showDate = false, + showType = true, + showTime = true, + showSubject = true, + markAsSeen = true, + onEventClick = { + EventDetailsDialog( + activity, + it, + onShowListener = onShowListener, + onDismissListener = onDismissListener + ).show() + }, + onEventEditClick = { + EventManualDialog( + activity, + it.profileId, + editingEvent = it, + onShowListener = onShowListener, + onDismissListener = onDismissListener + ).show() + } + ) + + app.db.eventDao().getAllByDateTime( + lesson.profileId, + lessonDate, + lessonTime + ).observe(activity) { events -> + events.forEach { + it.filterNotes() + } + + adapter.setAllItems(events) + if (b.eventsView.adapter == null) { + b.eventsView.adapter = adapter + b.eventsView.apply { + isNestedScrollingEnabled = false + setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + addItemDecoration(SimpleDividerItemDecoration(context)) + } + } + adapter.notifyDataSetChanged() + + if (events != null && events.isNotEmpty()) { + b.eventsView.visibility = View.VISIBLE + b.eventsNoData.visibility = View.GONE + } else { + b.eventsView.visibility = View.GONE + b.eventsNoData.visibility = View.VISIBLE + } + } + + lesson.displayTeacherName?.let { name -> + lesson.displayTeacherId ?: return@let + BetterLink.attach( + b.teacherNameView, + teachers = mapOf(lesson.displayTeacherId!! to name), + onActionSelected = dialog::dismiss + ) + BetterLink.attach( + b.oldTeacherNameView, + teachers = mapOf(lesson.displayTeacherId!! to name), + onActionSelected = dialog::dismiss + ) + } + + b.notesButton.isVisible = showNotes + b.notesButton.setupNotesButton( + activity = activity, + owner = lesson, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ) + b.legend.isVisible = showNotes + if (showNotes) + NoteManager.setLegendText(lesson, b.legend) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt new file mode 100644 index 00000000..d2016b29 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt @@ -0,0 +1,475 @@ +/* + * Copyright (c) Kacper Ziubryniewicz 2020-1-6 + */ + +package pl.szczodrzynski.edziennik.ui.timetable + +import android.graphics.drawable.ColorDrawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.TextView +import androidx.asynclayoutinflater.view.AsyncLayoutInflater +import androidx.core.graphics.ColorUtils +import androidx.core.view.isVisible +import androidx.core.view.marginTop +import androidx.core.view.setPadding +import androidx.core.view.updateLayoutParams +import androidx.core.view.updateMargins +import com.linkedin.android.tachyon.DayView +import com.linkedin.android.tachyon.DayViewConfig +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +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.entity.Lesson +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull +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 +import pl.szczodrzynski.edziennik.ext.asColoredSpannable +import pl.szczodrzynski.edziennik.ext.asStrikethroughSpannable +import pl.szczodrzynski.edziennik.ext.concat +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ext.findParentById +import pl.szczodrzynski.edziennik.ext.getStudentData +import pl.szczodrzynski.edziennik.ext.listOfNotEmpty +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ext.resolveAttr +import pl.szczodrzynski.edziennik.ext.resolveDrawable +import pl.szczodrzynski.edziennik.ext.setText +import pl.szczodrzynski.edziennik.ext.setTintColor +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer +import pl.szczodrzynski.edziennik.ui.base.lazypager.LazyFragment +import pl.szczodrzynski.edziennik.ui.timetable.TimetableFragment.Companion.DEFAULT_END_HOUR +import pl.szczodrzynski.edziennik.ui.timetable.TimetableFragment.Companion.DEFAULT_START_HOUR +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 +import kotlin.math.min + +class TimetableDayFragment : LazyFragment(), CoroutineScope { + companion object { + private const val TAG = "TimetableDayFragment" + } + + private lateinit var app: App + private lateinit var activity: MainActivity + private lateinit var inflater: AsyncLayoutInflater + private lateinit var b: TimetableDayFragmentBinding + + private val job: Job = Job() + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + + private var timeIndicatorJob: Job? = null + + private lateinit var date: Date + private var startHour = DEFAULT_START_HOUR + private var endHour = DEFAULT_END_HOUR + private var firstEventMinute = 24 * 60 + private var paddingTop = 0 + + private var viewsRemoved = false + + private val manager + get() = app.timetableManager + private val attendanceManager + get() = app.attendanceManager + + // find SwipeRefreshLayout in the hierarchy + private val refreshLayout by lazy { view?.findParentById(R.id.refreshLayout) } + + private val dayViewDelegate = mutableLazy { + val dayView = DayView(activity, DayViewConfig( + startHour = startHour, + endHour = endHour, + dividerHeight = 1.dp, + halfHourHeight = app.data.uiConfig.lessonHeight.dp, + hourDividerColor = R.attr.hourDividerColor.resolveAttr(context), + halfHourDividerColor = R.attr.halfHourDividerColor.resolveAttr(context), + hourLabelWidth = 40.dp, + hourLabelMarginEnd = 10.dp, + eventMargin = 2.dp + ), true) + dayView.setPadding(10.dp) + return@mutableLazy dayView + } + private val dayView by dayViewDelegate + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + activity = (getActivity() as MainActivity?) ?: return null + context ?: return null + app = activity.application as App + this.inflater = AsyncLayoutInflater(requireContext()) + + date = arguments?.getInt("date")?.let { Date.fromValue(it) } ?: Date.getToday() + startHour = arguments?.getInt("startHour") ?: DEFAULT_START_HOUR + endHour = arguments?.getInt("endHour") ?: DEFAULT_END_HOUR + + b = TimetableDayFragmentBinding.inflate(inflater, null, false) + return b.root + } + + override fun onPageCreated(): Boolean { + // observe lesson database + app.db.timetableDao().getAllForDate(App.profileId, date).observe(this) { lessons -> + launch { + lessons.forEach { + it.filterNotes() + } + + val events = withContext(Dispatchers.Default) { + app.db.eventDao().getAllByDateNow(App.profileId, date) + } + val attendanceList = withContext(Dispatchers.Default) { + app.db.attendanceDao().getAllByDateNow(App.profileId, date) + } + processLessonList(lessons, events, attendanceList) + } + } + + return true + } + + private fun processLessonList(lessons: List, events: List, attendanceList: List) { + // no lessons - timetable not downloaded yet + if (lessons.isEmpty()) { + inflater.inflate(R.layout.timetable_no_timetable, b.root) { view, _, _ -> + b.root.removeAllViews() + b.root.addView(view) + viewsRemoved = true + + val b = TimetableNoTimetableBinding.bind(view) + val weekStart = date.weekStart.stringY_m_d + b.noTimetableSync.onClick { + it.isEnabled = false + EdziennikTask.syncProfile( + profileId = App.profileId, + featureTypes = setOf(FeatureType.TIMETABLE), + arguments = JsonObject( + "weekStart" to weekStart + ) + ).enqueue(activity) + } + b.noTimetableWeek.setText(R.string.timetable_no_timetable_week, weekStart) + } + return + } + // one lesson indicating a day without lessons + if (lessons.size == 1 && lessons[0].type == Lesson.TYPE_NO_LESSONS) { + inflater.inflate(R.layout.timetable_no_lessons, b.root) { view, _, _ -> + 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 + } + + // reload the fragment when: no lessons, user wants to sync the week, the timetable is not public, pager gets removed + if (app.profile.getStudentData("timetableNotPublic", false)) { + activity.reloadTarget() + // TODO fix for (not really)possible infinite loops + return + } + + // the timetable was not synced (the day layout views are removed) and is now available + if (viewsRemoved) { + viewsRemoved = false + activity.sendBroadcast(Intent(TimetableFragment.ACTION_RELOAD_PAGES)) + return + } + + if (dayViewDelegate.isInitialized()) + b.dayFrame.removeView(dayView) + + val lessonsActual = lessons.filter { it.type != Lesson.TYPE_NO_LESSONS } + + val minStartHour = lessonsActual.minOf { it.displayStartTime?.hour ?: DEFAULT_END_HOUR } + val maxEndHour = lessonsActual.maxOf { it.displayEndTime?.hour?.plus(1) ?: DEFAULT_START_HOUR } + + if (app.profile.config.ui.timetableTrimHourRange) { + dayViewDelegate.deinitialize() + // end/start defaults are swapped on purpose + startHour = minStartHour + endHour = maxEndHour + } else if (startHour > minStartHour || endHour < maxEndHour) { + dayViewDelegate.deinitialize() + startHour = min(startHour, minStartHour) + endHour = max(endHour, maxEndHour) + } + + b.scrollView.isVisible = true + b.dayFrame.addView(dayView, 0) + + // Inflate a label view for each hour the day view will display + val hourLabelViews = mutableListOf() + for (i in dayView.startHour..dayView.endHour) { + if (!isAdded) + continue + val hourLabelView = layoutInflater.inflate(R.layout.timetable_hour_label, dayView, false) as TextView + hourLabelView.text = "$i:00" + hourLabelViews.add(hourLabelView) + } + dayView.setHourLabelViews(hourLabelViews) + // measure dayView top padding needed for the timeIndicator + hourLabelViews.getOrNull(0)?.let { + it.measure(0, 0) + paddingTop = it.measuredHeight / 2 + dayView.paddingTop + } + + lessons.forEach { it.showAsUnseen = !it.seen } + + buildLessonViews(lessonsActual, events, attendanceList) + } + + private fun buildLessonViews(lessons: List, events: List, attendanceList: List) { + if (!isAdded) + return + + val eventViews = mutableListOf() + val eventTimeRanges = mutableListOf() + + // Reclaim all of the existing event views so we can reuse them if needed, this process + // can be useful if your day view is hosted in a recycler view for example + val recycled = dayView.removeEventViews() + var remaining = recycled?.size ?: 0 + + val arrowRight = " → " + val bullet = " • " + val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity) + + for (lesson in lessons) { + val attendance = if (app.profile.config.ui.timetableShowAttendance) + attendanceList.find { it.startTime == lesson.startTime } + else + null + val startTime = lesson.displayStartTime ?: continue + val endTime = lesson.displayEndTime ?: continue + + firstEventMinute = min(firstEventMinute, startTime.hour * 60 + startTime.minute) + + // Try to recycle an existing event view if there are enough left, otherwise inflate + // a new one + val eventView = + (if (remaining > 0) recycled?.get(--remaining) else layoutInflater.inflate( + R.layout.timetable_lesson, + dayView, + false + )) ?: continue + val lb = TimetableLessonBinding.bind(eventView) + eventViews += eventView + + eventView.tag = lesson to attendance + + eventView.setOnClickListener { + if (isAdded && it.tag is Pair<*, *>) { + val (lessonObj, attendanceObj) = it.tag as Pair<*, *> + LessonDetailsDialog( + activity = activity, + lesson = lessonObj as LessonFull, + attendance = attendanceObj as AttendanceFull? + ).show() + } + } + + val eventIcons = listOf(lb.event1, lb.event2, lb.event3) + if (app.profile.config.ui.timetableShowEvents) { + val eventList = events.filter { it.time != null && it.time == lesson.displayStartTime }.take(3) + for ((i, eventIcon) in eventIcons.withIndex()) { + eventList.getOrNull(i).let { + eventIcon.isVisible = it != null + eventIcon.background = it?.let { + R.drawable.bg_circle.resolveDrawable(activity).setTintColor(it.eventColor) + } + } + } + } else { + for (eventIcon in eventIcons) { + eventIcon.visibility = View.GONE + } + } + + + val timeRange = "${startTime.stringHM} - ${endTime.stringHM}".asColoredSpannable(colorSecondary) + + // teacher + val teacherInfo = if (lesson.teacherId != null && lesson.teacherId == lesson.oldTeacherId) + lesson.teacherName ?: "?" + else + mutableListOf().apply { + lesson.oldTeacherName?.let { add(it.asStrikethroughSpannable()) } + lesson.teacherName?.let { add(it) } + }.concat(arrowRight) + + // team + val teamInfo = if (lesson.teamId != null && lesson.teamId == lesson.oldTeamId) + lesson.teamName ?: "?" + else + mutableListOf().apply { + lesson.oldTeamName?.let { add(it.asStrikethroughSpannable()) } + lesson.teamName?.let { add(it) } + }.concat(arrowRight) + + // classroom + val classroomInfo = if (lesson.classroom != null && lesson.classroom == lesson.oldClassroom) + lesson.classroom ?: "?" + else + mutableListOf().apply { + lesson.oldClassroom?.let { add(it.asStrikethroughSpannable()) } + lesson.classroom?.let { add(it) } + }.concat(arrowRight) + + lb.annotationVisible = manager.getAnnotation(activity, lesson, lb.annotation) + + val lessonText = + lesson.getNoteSubstituteText(showNotes = true) ?: lesson.displaySubjectName + + val (subjectTextPrimary, subjectTextSecondary) = if (app.profile.config.ui.timetableColorSubjectName) { + val subjectColor = lesson.color ?: Colors.stringToMaterialColorCRC(lessonText?.toString() ?: "") + if (lb.annotationVisible) { + lb.subjectContainer.background = ColorDrawable(subjectColor) + } else { + lb.subjectContainer.setBackgroundResource(R.drawable.timetable_subject_color_rounded) + lb.subjectContainer.background.setTintColor(subjectColor) + } + when (ColorUtils.calculateLuminance(subjectColor) > 0.5) { + true -> /* light */ 0xFF000000 to 0xFF666666 + false -> /* dark */ 0xFFFFFFFF to 0xFFAAAAAA + } + } else { + lb.subjectContainer.background = null + null to colorSecondary + } + + lb.lessonNumber = lesson.displayLessonNumber + 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()) + else if (subjectTextPrimary != null) + it.asColoredSpannable(subjectTextPrimary.toInt()) + else + it + } + lb.detailsFirst.text = listOfNotEmpty(timeRange, classroomInfo).concat(bullet) + lb.detailsSecond.text = listOfNotEmpty(teacherInfo, teamInfo).concat(bullet) + + NoteManager.prependIcon(lesson, lb.subjectName) + + lb.attendanceIcon.isVisible = attendance?.let { + val icon = attendanceManager.getAttendanceIcon(it) ?: return@let false + val color = attendanceManager.getAttendanceColor(it) + lb.attendanceIcon.setImageDrawable( + IconicsDrawable(activity, icon).apply { + colorInt = color + sizeDp = 24 + } + ) + true + } ?: false + + lb.unread = lesson.type != Lesson.TYPE_NORMAL && lesson.showAsUnseen + if (!lesson.seen) { + manager.markAsSeen(lesson) + } + + //lb.subjectName.typeface = Typeface.create("sans-serif-light", Typeface.BOLD) + val lessonNumberMargin = + if (lb.annotationVisible) (-8).dp + else 0 + lb.lessonNumberText.updateLayoutParams { + updateMargins(top = lessonNumberMargin, bottom = lessonNumberMargin) + } + + // The day view needs the event time ranges in the start minute/end minute format, + // so calculate those here + val startMinute = 60 * startTime.hour + startTime.minute + val endMinute = 60 * endTime.hour + endTime.minute + eventTimeRanges.add(DayView.EventTimeRange(startMinute, endMinute)) + } + + updateTimeIndicator() + + dayView.setEventViews(eventViews, eventTimeRanges) + val firstEventTop = (firstEventMinute - dayView.startHour * 60) * dayView.minuteHeight + b.scrollView.scrollTo(0, firstEventTop.toInt()) + + b.progressBar.isVisible = false + } + + private fun updateTimeIndicator() { + val time = Time.getNow() + val isTimeInView = + date == Date.getToday() && time.hour in dayView.startHour..dayView.endHour + + b.timeIndicator.isVisible = isTimeInView + b.timeIndicatorMarker.isVisible = isTimeInView + if (isTimeInView) { + val startTime = Time(dayView.startHour, 0, 0) + val seconds = time.inSeconds - startTime.inSeconds * 1f + b.timeIndicator.updateLayoutParams { + topMargin = (seconds * dayView.minuteHeight / 60f).toInt() + paddingTop + } + b.timeIndicatorMarker.updateLayoutParams { + topMargin = b.timeIndicator.marginTop - (16.dp / 2) + (1.dp / 2) + } + } + + if (timeIndicatorJob == null) { + timeIndicatorJob = startCoroutineTimer(repeatMillis = 30000) { + updateTimeIndicator() + } + } + } + + override fun onResume() { + super.onResume() + val firstEventTop = (firstEventMinute - dayView.startHour * 60) * dayView.minuteHeight + b.scrollView.scrollTo(0, firstEventTop.toInt()) + updateTimeIndicator() + } + + override fun onPause() { + super.onPause() + timeIndicatorJob?.cancel() + timeIndicatorJob = null + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableFragment.kt similarity index 76% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableFragment.kt index 970daeec..2e037526 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetableFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kacper Ziubryniewicz 2020-1-6 */ -package pl.szczodrzynski.edziennik.ui.modules.timetable +package pl.szczodrzynski.edziennik.ui.timetable import android.content.BroadcastReceiver import android.content.Context @@ -19,15 +19,23 @@ import androidx.viewpager.widget.ViewPager import com.google.android.material.datepicker.MaterialDatePicker import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import eu.szkolny.font.SzkolnyFont -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.launch import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.data.db.entity.Metadata +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.getSchoolYearConstrains -import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog -import pl.szczodrzynski.edziennik.ui.dialogs.timetable.GenerateBlockTimetableDialog +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 +import pl.szczodrzynski.edziennik.ui.event.EventManualDialog import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem @@ -37,6 +45,7 @@ class TimetableFragment : Fragment(), CoroutineScope { companion object { private const val TAG = "TimetableFragment" const val ACTION_SCROLL_TO_DATE = "pl.szczodrzynski.edziennik.timetable.SCROLL_TO_DATE" + const val ACTION_RELOAD_PAGES = "pl.szczodrzynski.edziennik.timetable.RELOAD_PAGES" const val DEFAULT_START_HOUR = 6 const val DEFAULT_END_HOUR = 19 var pageSelection: Date? = null @@ -67,14 +76,22 @@ class TimetableFragment : Fragment(), CoroutineScope { override fun onReceive(context: Context, i: Intent) { if (!isAdded) return - val dateStr = i.extras?.getString("timetableDate", null) ?: return - val date = Date.fromY_m_d(dateStr) - b.viewPager.setCurrentItem(items.indexOf(date), true) + when (i.action) { + ACTION_SCROLL_TO_DATE -> { + val dateStr = i.extras?.getString("timetableDate", null) ?: return + val date = Date.fromY_m_d(dateStr) + b.viewPager.setCurrentItem(items.indexOf(date), true) + } + ACTION_RELOAD_PAGES -> { + b.viewPager.adapter?.notifyDataSetChanged() + } + } } } override fun onResume() { super.onResume() activity.registerReceiver(broadcastReceiver, IntentFilter(ACTION_SCROLL_TO_DATE)) + activity.registerReceiver(broadcastReceiver, IntentFilter(ACTION_RELOAD_PAGES)) } override fun onPause() { super.onPause() @@ -120,15 +137,15 @@ class TimetableFragment : Fragment(), CoroutineScope { } val lessonRanges = app.db.lessonRangeDao().getAllNow(App.profileId) - startHour = lessonRanges.map { it.startTime.hour }.min() ?: DEFAULT_START_HOUR - endHour = lessonRanges.map { it.endTime.hour }.max()?.plus(1) ?: DEFAULT_END_HOUR + startHour = lessonRanges.minOfOrNull { it.startTime.hour } ?: DEFAULT_START_HOUR + endHour = lessonRanges.maxOfOrNull { it.endTime.hour }?.plus(1) ?: DEFAULT_END_HOUR } deferred.await() if (!isAdded) return@launch val pagerAdapter = TimetablePagerAdapter( - fragmentManager ?: return@launch, + parentFragmentManager, items, startHour, endHour @@ -164,6 +181,21 @@ class TimetableFragment : Fragment(), CoroutineScope { b.tabLayout.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) @@ -190,7 +222,7 @@ class TimetableFragment : Fragment(), CoroutineScope { .withIcon(SzkolnyFont.Icon.szf_calendar_plus_outline) .withOnClickListener(View.OnClickListener { activity.bottomSheet.close() - EventManualDialog(activity, App.profileId, defaultDate = pageSelection) + EventManualDialog(activity, App.profileId, defaultDate = pageSelection).show() }), BottomSheetPrimaryItem(true) .withTitle(R.string.menu_generate_block_timetable) @@ -200,13 +232,20 @@ class TimetableFragment : Fragment(), CoroutineScope { activity.bottomSheet.close() GenerateBlockTimetableDialog(activity) }), + BottomSheetPrimaryItem(true) + .withTitle(R.string.menu_timetable_config) + .withIcon(CommunityMaterial.Icon.cmd_cog_outline) + .withOnClickListener { + activity.bottomSheet.close() + TimetableConfigDialog(activity, false, null, null).show() + }, BottomSheetSeparatorItem(true), BottomSheetPrimaryItem(true) .withTitle(R.string.menu_mark_as_read) .withIcon(CommunityMaterial.Icon.cmd_eye_check_outline) .withOnClickListener(View.OnClickListener { activity.bottomSheet.close() - AsyncTask.execute { app.db.metadataDao().setAllSeen(App.profileId, Metadata.TYPE_LESSON_CHANGE, true) } + AsyncTask.execute { app.db.metadataDao().setAllSeen(App.profileId, MetadataType.LESSON_CHANGE, true) } Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show() }) ) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetablePagerAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetablePagerAdapter.kt similarity index 83% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetablePagerAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetablePagerAdapter.kt index 289860ac..2f1adfd2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/timetable/TimetablePagerAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetablePagerAdapter.kt @@ -2,12 +2,12 @@ * Copyright (c) Kacper Ziubryniewicz 2020-1-6 */ -package pl.szczodrzynski.edziennik.ui.modules.timetable +package pl.szczodrzynski.edziennik.ui.timetable import android.os.Bundle import androidx.fragment.app.FragmentManager -import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment -import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyPagerAdapter +import pl.szczodrzynski.edziennik.ui.base.lazypager.LazyFragment +import pl.szczodrzynski.edziennik.ui.base.lazypager.LazyPagerAdapter import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Week @@ -47,4 +47,8 @@ class TimetablePagerAdapter( } return pageTitle } + + override fun getItemPosition(`object`: Any): Int { + return POSITION_NONE + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/AttachmentAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/AttachmentAdapter.kt similarity index 97% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/AttachmentAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/views/AttachmentAdapter.kt index 5712207c..b7cbf698 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/AttachmentAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/AttachmentAdapter.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-1. */ -package pl.szczodrzynski.edziennik.ui.modules.views +package pl.szczodrzynski.edziennik.ui.views import android.content.Context import android.view.LayoutInflater @@ -22,8 +22,8 @@ import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.databinding.AttachmentListItemBinding -import pl.szczodrzynski.edziennik.onClick -import pl.szczodrzynski.edziennik.onLongClick +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ext.onLongClick import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.navlib.colorAttr import kotlin.coroutines.CoroutineContext diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/AttachmentsView.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/AttachmentsView.kt similarity index 87% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/AttachmentsView.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/views/AttachmentsView.kt index 354a8921..81240d62 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/AttachmentsView.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/AttachmentsView.kt @@ -2,11 +2,11 @@ * Copyright (c) Kuba Szczodrzyński 2020-4-1. */ -package pl.szczodrzynski.edziennik.ui.modules.views +package pl.szczodrzynski.edziennik.ui.views import android.content.Context +import android.os.Build import android.os.Bundle -import android.os.Environment import android.util.AttributeSet import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.PopupMenu @@ -19,8 +19,8 @@ import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.edziennik.EdziennikTask import pl.szczodrzynski.edziennik.data.api.events.AttachmentGetEvent -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.isNotNullNorEmpty +import pl.szczodrzynski.edziennik.ext.get +import pl.szczodrzynski.edziennik.ext.isNotNullNorEmpty import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import pl.szczodrzynski.edziennik.utils.Utils import java.io.File @@ -37,9 +37,7 @@ class AttachmentsView @JvmOverloads constructor( } private val storageDir by lazy { - val storageDir = Environment.getExternalStoragePublicDirectory("Szkolny.eu") - storageDir.mkdirs() - storageDir + Utils.getStorageDir() } fun init(arguments: Bundle, owner: Any) { @@ -53,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) } @@ -60,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) } @@ -70,7 +76,10 @@ class AttachmentsView @JvmOverloads constructor( attachmentIds.forEachIndexed { index, id -> val name = attachmentNames[index] ?: return@forEachIndexed - val size = attachmentSizes?.getOrNull(index) + var size = attachmentSizes?.getOrNull(index) + // hide the size if less than 1 byte + if (size?.compareTo(1) == -1) + size = null val item = AttachmentAdapter.Item(profileId, owner, id, name, size) adapter.items += item @@ -82,6 +91,7 @@ class AttachmentsView @JvmOverloads constructor( list.adapter = adapter list.apply { setHasFixedSize(false) + isNestedScrollingEnabled = false layoutManager = LinearLayoutManager(context) addItemDecoration(SimpleDividerItemDecoration(context)) } @@ -101,6 +111,8 @@ class AttachmentsView @JvmOverloads constructor( val fileUrl = item.name.substringAfter(":", missingDelimiterValue = "") // update file name with the downloaded one item.name = attachmentFile.name + // update file size (useful for items with no defined size) + item.size = attachmentFile.length() // save the download url back if (fileUrl != "") item.name += ":$fileUrl" @@ -158,7 +170,7 @@ class AttachmentsView @JvmOverloads constructor( // get the download url before updating file name val fileUrl = attachment.name.substringAfter(":", missingDelimiterValue = "") // update file name with the downloaded one - attachment.name = File(event.fileName).name + attachment.name = File(event.fileName ?: return).name // save the download url back if (fileUrl != "") attachment.name += ":$fileUrl" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/DateDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/DateDropdown.kt similarity index 97% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/DateDropdown.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/views/DateDropdown.kt index 92bf0515..69ccb26a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/DateDropdown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/DateDropdown.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-2-23. */ -package pl.szczodrzynski.edziennik.ui.modules.views +package pl.szczodrzynski.edziennik.ui.views import android.content.Context import android.content.ContextWrapper @@ -16,7 +16,7 @@ import kotlinx.coroutines.withContext import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.AppDb import pl.szczodrzynski.edziennik.data.db.full.LessonFull -import pl.szczodrzynski.edziennik.observeOnce +import pl.szczodrzynski.edziennik.ext.observeOnce import pl.szczodrzynski.edziennik.utils.TextInputDropDown import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Week @@ -56,7 +56,6 @@ class DateDropdown : TextInputDropDown { suspend fun loadItems() { val date = Date.getToday() - val today = date.value var weekDay = date.weekDay val dates = withContext(Dispatchers.Default) { @@ -113,7 +112,7 @@ class DateDropdown : TextInputDropDown { date.stepForward(0, 0, -weekDay + 7) weekDay = 0 // ALL SCHOOL DAYS OF THE NEXT WEEK - while (weekDay < 4) { + while (weekDay < 5) { dates += Item( date.value.toLong(), context.getString(R.string.dialog_event_manual_date_next_week, Week.getFullDayName(weekDay), date.formattedString), @@ -175,7 +174,7 @@ class DateDropdown : TextInputDropDown { } } - fun pickerDialog() { + private fun pickerDialog() { val date = getSelected() as? Date ?: Date.getToday() MaterialDatePicker.Builder.datePicker() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/EventTypeDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/EventTypeDropdown.kt new file mode 100644 index 00000000..48d327fc --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/EventTypeDropdown.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-14. + */ + +package pl.szczodrzynski.edziennik.ui.views + +import android.content.Context +import android.util.AttributeSet +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.sizeDp +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import pl.szczodrzynski.edziennik.data.db.AppDb +import pl.szczodrzynski.edziennik.data.db.entity.EventType +import pl.szczodrzynski.edziennik.utils.TextInputDropDown + +class EventTypeDropdown : TextInputDropDown { + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + lateinit var db: AppDb + var profileId: Int = 0 + var onTypeSelected: ((eventType: EventType) -> Unit)? = null + + override fun create(context: Context) { + super.create(context) + isEnabled = false + } + + suspend fun loadItems() { + val types = withContext(Dispatchers.Default) { + val list = mutableListOf() + val types = db.eventTypeDao().getAllNow(profileId) + .sortedBy { it.order } + + list += types.map { + Item(it.id, it.name, tag = it, icon = IconicsDrawable(context).apply { + icon = CommunityMaterial.Icon.cmd_circle + sizeDp = 24 + colorInt = it.color + }) + } + + list + } + + clear().append(types) + isEnabled = true + + setOnChangeListener { + when (it.tag) { + is EventType -> { + // selected an event type + onTypeSelected?.invoke(it.tag) + true + } + else -> false + } + } + } + + /** + * Select an event type by the [typeId]. + */ + fun selectType(typeId: Long) = select(typeId) + + /** + * Select an event type by the [typeId] **if it's not selected yet**. + */ + fun selectDefault(typeId: Long?) { + if (typeId == null || selected != null) + return + selectType(typeId) + } + + /** + * Get the currently selected event type. + * ### Returns: + * - null if no valid type is selected + * - [EventType] - the selected event type + */ + fun getSelected(): EventType? { + return when (selected?.tag) { + is EventType -> selected?.tag as EventType + else -> null + } + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/SubjectDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/SubjectDropdown.kt similarity index 81% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/SubjectDropdown.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/views/SubjectDropdown.kt index 06e01225..cc2f0f83 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/SubjectDropdown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/SubjectDropdown.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-2-23. */ -package pl.szczodrzynski.edziennik.ui.modules.views +package pl.szczodrzynski.edziennik.ui.views import android.content.Context import android.content.ContextWrapper @@ -13,9 +13,10 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.crc16 import pl.szczodrzynski.edziennik.data.db.AppDb -import pl.szczodrzynski.edziennik.ui.dialogs.input +import pl.szczodrzynski.edziennik.data.db.entity.Subject +import pl.szczodrzynski.edziennik.ext.crc16 +import pl.szczodrzynski.edziennik.ext.input import pl.szczodrzynski.edziennik.utils.TextInputDropDown class SubjectDropdown : TextInputDropDown { @@ -40,7 +41,7 @@ class SubjectDropdown : TextInputDropDown { var showNoSubject = true var showCustomSubject = false var customSubjectName = "" - var onSubjectSelected: ((subjectId: Long?) -> Unit)? = null + var onSubjectSelected: ((subject: Subject?) -> Unit)? = null var onCustomSubjectSelected: ((subjectName: String) -> Unit)? = null override fun create(context: Context) { @@ -73,7 +74,7 @@ class SubjectDropdown : TextInputDropDown { list += subjects.map { Item( it.id, it.longName, - tag = it.id + tag = it ) } list @@ -91,10 +92,11 @@ class SubjectDropdown : TextInputDropDown { } -1L -> { // no subject + deselect() onSubjectSelected?.invoke(null) - true + false } - is Long -> { + is Subject -> { // selected a subject onSubjectSelected?.invoke(it.tag) true @@ -104,7 +106,7 @@ class SubjectDropdown : TextInputDropDown { } } - fun customNameDialog() { + private fun customNameDialog() { activity ?: return MaterialAlertDialogBuilder(activity!!) .setTitle("Własny przedmiot") @@ -127,32 +129,37 @@ class SubjectDropdown : TextInputDropDown { .show() } - fun selectSubject(subjectId: Long) { - if (select(subjectId) == null) - select(Item( - subjectId, - "nieznany przedmiot ($subjectId)", - tag = subjectId - )) + /** + * Select a subject by the [subjectId]. + */ + fun selectSubject(subjectId: Long): Item? { + if (subjectId == -1L) { + deselect() + return null + } + return select(subjectId) } - fun selectDefault(subjectId: Long?) { + /** + * Select a subject by the [subjectId] **if it's not selected yet**. + */ + fun selectDefault(subjectId: Long?): Item? { if (subjectId == null || selected != null) - return - selectSubject(subjectId) + return null + return selectSubject(subjectId) } /** * Get the currently selected subject. * ### Returns: * - null if no valid subject is selected - * - [Long] - the selected subject's ID + * - [Subject] - the selected subject * - [String] - a custom subject name entered, if [showCustomSubject] == true */ fun getSelected(): Any? { return when (selected?.tag) { -1L -> null - is Long -> selected?.tag as Long + is Subject -> selected?.tag as Subject is String -> selected?.tag as String else -> null } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeacherDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/TeacherDropdown.kt similarity index 66% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeacherDropdown.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/views/TeacherDropdown.kt index 6411426b..c67011a2 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeacherDropdown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/TeacherDropdown.kt @@ -2,16 +2,15 @@ * Copyright (c) Kuba Szczodrzyński 2020-2-23. */ -package pl.szczodrzynski.edziennik.ui.modules.views +package pl.szczodrzynski.edziennik.ui.views import android.content.Context -import android.content.ContextWrapper import android.util.AttributeSet -import androidx.appcompat.app.AppCompatActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.AppDb +import pl.szczodrzynski.edziennik.data.db.entity.Teacher import pl.szczodrzynski.edziennik.utils.TextInputDropDown class TeacherDropdown : TextInputDropDown { @@ -19,22 +18,10 @@ class TeacherDropdown : TextInputDropDown { constructor(context: Context, attrs: AttributeSet) : super(context, attrs) constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) - private val activity: AppCompatActivity? - get() { - var context: Context? = context ?: return null - if (context is AppCompatActivity) return context - while (context is ContextWrapper) { - if (context is AppCompatActivity) - return context - context = context.baseContext - } - return null - } - lateinit var db: AppDb var profileId: Int = 0 var showNoTeacher = true - var onTeacherSelected: ((teacherId: Long?) -> Unit)? = null + var onTeacherSelected: ((teacher: Teacher?) -> Unit)? = null override fun create(context: Context) { super.create(context) @@ -58,7 +45,7 @@ class TeacherDropdown : TextInputDropDown { list += teachers.map { Item( it.id, it.fullName, - tag = it.id + tag = it ) } list @@ -71,10 +58,11 @@ class TeacherDropdown : TextInputDropDown { when (it.tag) { -1L -> { // no teacher + deselect() onTeacherSelected?.invoke(null) - true + false } - is Long -> { + is Teacher -> { // selected a teacher onTeacherSelected?.invoke(it.tag) true @@ -84,31 +72,36 @@ class TeacherDropdown : TextInputDropDown { } } - fun selectTeacher(teacherId: Long) { - if (select(teacherId) == null) - select(Item( - teacherId, - "nieznany nauczyciel ($teacherId)", - tag = teacherId - )) + /** + * Select a teacher by the [teacherId]. + */ + fun selectTeacher(teacherId: Long): Item? { + if (teacherId == -1L) { + deselect() + return null + } + return select(teacherId) } - fun selectDefault(teacherId: Long?) { + /** + * Select a teacher by the [teacherId] **if it's not selected yet**. + */ + fun selectDefault(teacherId: Long?): Item? { if (teacherId == null || selected != null) - return - selectTeacher(teacherId) + return null + return selectTeacher(teacherId) } /** * Get the currently selected teacher. * ### Returns: * - null if no valid teacher is selected - * - [Long] - the selected teacher's ID + * - [Teacher] - the selected teacher */ - fun getSelected(): Long? { + fun getSelected(): Teacher? { return when (selected?.tag) { -1L -> null - is Long -> selected?.tag as Long + is Teacher -> selected?.tag as Teacher else -> null } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeamDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/TeamDropdown.kt similarity index 68% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeamDropdown.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/views/TeamDropdown.kt index b31d2ca8..2a395d14 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TeamDropdown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/TeamDropdown.kt @@ -2,12 +2,10 @@ * Copyright (c) Kuba Szczodrzyński 2020-3-7. */ -package pl.szczodrzynski.edziennik.ui.modules.views +package pl.szczodrzynski.edziennik.ui.views import android.content.Context -import android.content.ContextWrapper import android.util.AttributeSet -import androidx.appcompat.app.AppCompatActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import pl.szczodrzynski.edziennik.R @@ -20,22 +18,10 @@ class TeamDropdown : TextInputDropDown { constructor(context: Context, attrs: AttributeSet) : super(context, attrs) constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) - private val activity: AppCompatActivity? - get() { - var context: Context? = context ?: return null - if (context is AppCompatActivity) return context - while (context is ContextWrapper) { - if (context is AppCompatActivity) - return context - context = context.baseContext - } - return null - } - lateinit var db: AppDb var profileId: Int = 0 var showNoTeam = true - var onTeamSelected: ((teamId: Long?) -> Unit)? = null + var onTeamSelected: ((team: Team?) -> Unit)? = null override fun create(context: Context) { super.create(context) @@ -59,7 +45,7 @@ class TeamDropdown : TextInputDropDown { list += teams.map { Item( it.id, it.name, - tag = it.id + tag = it ) } list @@ -72,10 +58,11 @@ class TeamDropdown : TextInputDropDown { when (it.tag) { -1L -> { // no team + deselect() onTeamSelected?.invoke(null) - true + false } - is Long -> { + is Team -> { // selected a team onTeamSelected?.invoke(it.tag) true @@ -85,21 +72,29 @@ class TeamDropdown : TextInputDropDown { } } - fun selectTeam(teamId: Long) { - if (select(teamId) == null) - select(Item( - teamId, - "nieznana grupa ($teamId)", - tag = teamId - )) + /** + * Select a teacher by the [teamId]. + */ + fun selectTeam(teamId: Long): Item? { + if (teamId == -1L) { + deselect() + return null + } + return select(teamId) } - fun selectDefault(teamId: Long?) { + /** + * Select a team by the [teamId] **if it's not selected yet**. + */ + fun selectDefault(teamId: Long?): Item? { if (teamId == null || selected != null) - return - selectTeam(teamId) + return null + return selectTeam(teamId) } + /** + * Select a team of the [Team.TYPE_CLASS] type. + */ fun selectTeamClass() { select(items.singleOrNull { it.tag is Team && it.tag.type == Team.TYPE_CLASS @@ -110,12 +105,12 @@ class TeamDropdown : TextInputDropDown { * Get the currently selected team. * ### Returns: * - null if no valid team is selected - * - [Long] - the team's ID + * - [Team] - the selected team */ - fun getSelected(): Any? { + fun getSelected(): Team? { return when (selected?.tag) { -1L -> null - is Long -> selected?.tag as Long + is Team -> selected?.tag as Team else -> null } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TimeDropdown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/TimeDropdown.kt similarity index 96% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TimeDropdown.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/views/TimeDropdown.kt index 989184af..51a22d88 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/views/TimeDropdown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/views/TimeDropdown.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2020-2-23. */ -package pl.szczodrzynski.edziennik.ui.modules.views +package pl.szczodrzynski.edziennik.ui.views import android.content.Context import android.content.ContextWrapper @@ -17,6 +17,10 @@ import pl.szczodrzynski.edziennik.data.db.AppDb import pl.szczodrzynski.edziennik.data.db.entity.Lesson import pl.szczodrzynski.edziennik.data.db.entity.LessonRange import pl.szczodrzynski.edziennik.data.db.full.LessonFull +import pl.szczodrzynski.edziennik.ext.asItalicSpannable +import pl.szczodrzynski.edziennik.ext.asStrikethroughSpannable +import pl.szczodrzynski.edziennik.ext.concat +import pl.szczodrzynski.edziennik.ext.listOfNotEmpty import pl.szczodrzynski.edziennik.utils.TextInputDropDown import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time @@ -175,7 +179,7 @@ class TimeDropdown : TextInputDropDown { return !noTimetable } - fun pickerDialog() { + private fun pickerDialog() { val time = (getSelected() as? Pair<*, *>)?.first as? Time ?: Time.getNow() MaterialTimePicker.Builder() diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/webpush/WebPushBrowserAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/webpush/WebPushBrowserAdapter.kt similarity index 92% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/webpush/WebPushBrowserAdapter.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/webpush/WebPushBrowserAdapter.kt index 72d9a963..5ad04fcb 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/webpush/WebPushBrowserAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/webpush/WebPushBrowserAdapter.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2019-12-19. */ -package pl.szczodrzynski.edziennik.ui.modules.webpush +package pl.szczodrzynski.edziennik.ui.webpush import android.content.Context import android.view.LayoutInflater @@ -12,8 +12,8 @@ import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse import pl.szczodrzynski.edziennik.databinding.WebPushBrowserItemBinding -import pl.szczodrzynski.edziennik.onClick -import pl.szczodrzynski.edziennik.setText +import pl.szczodrzynski.edziennik.ext.onClick +import pl.szczodrzynski.edziennik.ext.setText class WebPushBrowserAdapter( val context: Context, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/webpush/WebPushFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/webpush/WebPushFragment.kt similarity index 95% rename from app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/webpush/WebPushFragment.kt rename to app/src/main/java/pl/szczodrzynski/edziennik/ui/webpush/WebPushFragment.kt index 0996c518..60403001 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/modules/webpush/WebPushFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/webpush/WebPushFragment.kt @@ -2,7 +2,7 @@ * Copyright (c) Kuba Szczodrzyński 2019-12-19. */ -package pl.szczodrzynski.edziennik.ui.modules.webpush +package pl.szczodrzynski.edziennik.ui.webpush import android.annotation.SuppressLint import android.os.Bundle @@ -19,6 +19,8 @@ import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse import pl.szczodrzynski.edziennik.databinding.WebPushFragmentBinding +import pl.szczodrzynski.edziennik.ext.crc32 +import pl.szczodrzynski.edziennik.ext.onClick import pl.szczodrzynski.edziennik.ui.dialogs.QrScannerDialog import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration import kotlin.coroutines.CoroutineContext @@ -61,14 +63,14 @@ class WebPushFragment : Fragment(), CoroutineScope { b.scanQrCode.onClick { manager.requestCameraPermission(activity, R.string.permissions_qr_scanner) { QrScannerDialog(activity, { - b.tokenEditText.setText(it.crc32().toString(36).toUpperCase()) + b.tokenEditText.setText(it.crc32().toString(36).uppercase()) pairBrowser(browserId = it) - }) + }).show() } } b.tokenAccept.onClick { - val pairToken = b.tokenEditText.text.toString().toUpperCase() + val pairToken = b.tokenEditText.text.toString().uppercase() if (!"[0-9A-Z]{3,13}".toRegex().matches(pairToken)) { b.tokenLayout.error = app.getString(R.string.web_push_token_invalid) return@onClick diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/LessonDialogActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/LessonDialogActivity.kt index 7a8f78ac..609657b5 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/LessonDialogActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/LessonDialogActivity.kt @@ -13,7 +13,9 @@ import androidx.appcompat.app.AppCompatActivity import kotlinx.coroutines.* import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.MainActivity -import pl.szczodrzynski.edziennik.ui.dialogs.timetable.LessonDetailsDialog +import pl.szczodrzynski.edziennik.ext.Intent +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget +import pl.szczodrzynski.edziennik.ui.timetable.LessonDetailsDialog import pl.szczodrzynski.edziennik.utils.Themes import kotlin.coroutines.CoroutineContext @@ -44,11 +46,12 @@ class LessonDialogActivity : AppCompatActivity(), CoroutineScope { val profileId = extras?.getInt("profileId") ?: return@async null if (extras.getBoolean("separatorItem", false)) { - val i = Intent(app, MainActivity::class.java) - .putExtra("fragmentId", MainActivity.DRAWER_ITEM_TIMETABLE) - .putExtra("profileId", profileId) - .putExtra("timetableDate", extras.getString("timetableDate", null)) - .addFlags(FLAG_ACTIVITY_REORDER_TO_FRONT or FLAG_ACTIVITY_NEW_TASK) + val i = Intent( + app, MainActivity::class.java, + "fragmentId" to NavTarget.TIMETABLE, + "profileId" to profileId, + "timetableDate" to extras.getString("timetableDate", null), + ).addFlags(FLAG_ACTIVITY_REORDER_TO_FRONT or FLAG_ACTIVITY_NEW_TASK) app.startActivity(i) finish() return@async null @@ -71,7 +74,7 @@ class LessonDialogActivity : AppCompatActivity(), CoroutineScope { if (shownDialogs.isEmpty()) finish() } - ) + ).show() } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/WidgetConfigActivity.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/WidgetConfigActivity.java index 5902f22e..d427e1b0 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/WidgetConfigActivity.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/WidgetConfigActivity.java @@ -4,6 +4,8 @@ package pl.szczodrzynski.edziennik.ui.widgets; +import static pl.szczodrzynski.edziennik.ext.DataExtensionsKt.filterOutArchived; + import android.app.Activity; import android.app.WallpaperManager; import android.appwidget.AppWidgetManager; @@ -27,14 +29,13 @@ import java.util.List; import pl.szczodrzynski.edziennik.App; import pl.szczodrzynski.edziennik.R; import pl.szczodrzynski.edziennik.data.db.entity.Profile; +import pl.szczodrzynski.edziennik.data.db.enums.LoginType; import pl.szczodrzynski.edziennik.databinding.DialogWidgetConfigBinding; import pl.szczodrzynski.edziennik.databinding.WidgetProfileDialogItemBinding; import pl.szczodrzynski.edziennik.ui.widgets.luckynumber.WidgetLuckyNumberProvider; import pl.szczodrzynski.edziennik.ui.widgets.notifications.WidgetNotificationsProvider; import pl.szczodrzynski.edziennik.ui.widgets.timetable.WidgetTimetableProvider; -import static pl.szczodrzynski.edziennik.ExtensionsKt.filterOutArchived; - public class WidgetConfigActivity extends Activity { public static final String TAG = "WidgetConfigActivity"; @@ -84,7 +85,7 @@ public class WidgetConfigActivity extends Activity { opacity = 0.8f; AsyncTask.execute(() -> { - profileList = App.db.profileDao().getAllNow(); + profileList = App.Companion.getDb().profileDao().getAllNow(); profileList = filterOutArchived(profileList); if (widgetType == WIDGET_NOTIFICATIONS) @@ -102,7 +103,7 @@ public class WidgetConfigActivity extends Activity { profileList.add( new Profile(-1, 0, - 0, + LoginType.TEMPLATE, getString(R.string.widget_config_all_profiles), null, "", diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/luckynumber/WidgetLuckyNumberProvider.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/luckynumber/WidgetLuckyNumberProvider.kt index 3dfa12f6..bde01d34 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/luckynumber/WidgetLuckyNumberProvider.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/luckynumber/WidgetLuckyNumberProvider.kt @@ -15,7 +15,10 @@ import android.widget.RemoteViews import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.R -import pl.szczodrzynski.edziennik.getJsonObject +import pl.szczodrzynski.edziennik.ext.getJsonObject +import pl.szczodrzynski.edziennik.ext.pendingIntentFlag +import pl.szczodrzynski.edziennik.ext.putExtras +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget import pl.szczodrzynski.edziennik.ui.widgets.WidgetConfig import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.models.Date @@ -42,10 +45,7 @@ class WidgetLuckyNumberProvider : AppWidgetProvider() { val views = getRemoteViews(app, config) val today = Date.getToday() - val todayValue = today.value - val tomorrow = Date.getToday().stepForward(0, 0, 1) - val tomorrowValue = tomorrow.value val profile = app.db.profileDao().getByIdNow(config.profileId) val luckyNumber = app.db.luckyNumberDao().getNearestFutureNow(config.profileId, today) @@ -83,8 +83,8 @@ class WidgetLuckyNumberProvider : AppWidgetProvider() { val openIntent = Intent(context, MainActivity::class.java) openIntent.action = Intent.ACTION_MAIN - openIntent.putExtra("fragmentId", MainActivity.DRAWER_ITEM_HOME) - val openPendingIntent = PendingIntent.getActivity(context, 0, openIntent, 0) + openIntent.putExtras("fragmentId" to NavTarget.HOME) + val openPendingIntent = PendingIntent.getActivity(context, 0, openIntent, pendingIntentFlag()) views.setOnClickPendingIntent(R.id.widgetLuckyNumberRoot, openPendingIntent) appWidgetManager.updateAppWidget(appWidgetId, views) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsFactory.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsFactory.kt index b5207300..11825724 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsFactory.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsFactory.kt @@ -11,8 +11,11 @@ import android.widget.AdapterView import android.widget.RemoteViews import android.widget.RemoteViewsService import com.google.gson.JsonParser -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.Notification +import pl.szczodrzynski.edziennik.data.db.enums.NotificationType +import pl.szczodrzynski.edziennik.ext.* import pl.szczodrzynski.edziennik.ui.widgets.WidgetConfig import pl.szczodrzynski.edziennik.utils.models.Date @@ -45,19 +48,20 @@ class WidgetNotificationsFactory(val app: App, val config: WidgetConfig) : Remot getLong("id") ?: 0, getString("title") ?: "", getString("text") ?: "", - getInt("type") ?: 0, + getString("textLong"), + getInt("type")?.asNotificationTypeOrNull() ?: NotificationType.GENERAL, getInt("profileId"), getString("profileName"), getInt("posted") == 1, - getInt("viewId"), - getString("extras")?.let { JsonParser().parse(it).asJsonObject }, + getInt("viewId")?.asNavTargetOrNull(), + getString("extras")?.let { JsonParser.parseString(it).asJsonObject }, getLong("addedDate") ?: System.currentTimeMillis() ) } ?: return views views.apply { setTextViewText(R.id.widgetNotificationsTitle, - app.getString(R.string.widget_notifications_title_format, notification.title, app.getNotificationTitle(notification.type))) + app.getString(R.string.widget_notifications_title_format, notification.title, notification.type.titleRes.resolveString(app))) setTextViewText(R.id.widgetNotificationsText, notification.text) setTextViewText(R.id.widgetNotificationsDate, Date.fromMillis(notification.addedDate).formattedString) setOnClickFillInIntent(R.id.widgetNotificationsRoot, Intent().also { notification.fillIntent(it) }) @@ -74,4 +78,4 @@ class WidgetNotificationsFactory(val app: App, val config: WidgetConfig) : Remot override fun hasStableIds() = true override fun getViewTypeCount() = 1 override fun onDestroy() = cursor?.close() ?: Unit -} \ No newline at end of file +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsProvider.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsProvider.kt index a0bf3b00..a082c3da 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsProvider.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/notifications/WidgetNotificationsProvider.kt @@ -17,8 +17,16 @@ 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.sizeDp -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.MainActivity +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 import pl.szczodrzynski.edziennik.ui.widgets.WidgetConfig class WidgetNotificationsProvider : AppWidgetProvider() { @@ -43,7 +51,7 @@ class WidgetNotificationsProvider : AppWidgetProvider() { val syncIntent = SzkolnyReceiver.getIntent(context, Bundle( "task" to "SyncRequest" )) - val syncPendingIntent = PendingIntent.getBroadcast(context, 0, syncIntent, 0) + val syncPendingIntent = PendingIntent.getBroadcast(context, 0, syncIntent, pendingIntentMutable()) views.setOnClickPendingIntent(R.id.widgetNotificationsSync, syncPendingIntent) views.setImageViewBitmap( @@ -64,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, 0) + 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.putExtra("fragmentId", MainActivity.DRAWER_ITEM_NOTIFICATIONS) - val headerPendingIntent = PendingIntent.getActivity(context, 0, headerIntent, 0) + headerIntent.putExtras("fragmentId" to NavTarget.NOTIFICATIONS) + val headerPendingIntent = PendingIntent.getActivity(context, appWidgetId, headerIntent, pendingIntentMutable()) views.setOnClickPendingIntent(R.id.widgetNotificationsHeader, headerPendingIntent) appWidgetManager.updateAppWidget(appWidgetId, views) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableFactory.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableFactory.java index 369f6783..960daef7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableFactory.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableFactory.java @@ -4,6 +4,8 @@ package pl.szczodrzynski.edziennik.ui.widgets.timetable; +import static android.util.TypedValue.COMPLEX_UNIT_SP; + import android.appwidget.AppWidgetManager; import android.content.ComponentName; import android.content.Context; @@ -16,7 +18,6 @@ import android.graphics.Paint; import android.graphics.PorterDuff; import android.graphics.PorterDuffColorFilter; import android.graphics.drawable.Drawable; -import android.text.Html; import android.util.Log; import android.view.View; import android.widget.RemoteViews; @@ -33,13 +34,12 @@ import java.util.List; import kotlin.Unit; import pl.szczodrzynski.edziennik.R; +import pl.szczodrzynski.edziennik.ext.TextExtensionsKt; import pl.szczodrzynski.edziennik.utils.models.Date; import pl.szczodrzynski.edziennik.utils.models.ItemWidgetTimetableModel; import pl.szczodrzynski.edziennik.utils.models.Time; import pl.szczodrzynski.edziennik.utils.models.Week; -import static android.util.TypedValue.COMPLEX_UNIT_SP; - public class WidgetTimetableFactory implements RemoteViewsService.RemoteViewsFactory { private static final String TAG = "WidgetTimetableProvider"; @@ -309,17 +309,17 @@ public class WidgetTimetableFactory implements RemoteViewsService.RemoteViewsFac views.setViewVisibility(R.id.widgetTimetableOldSubjectName, View.GONE); if (lesson.lessonChange) { - views.setTextViewText(R.id.widgetTimetableSubjectName, Html.fromHtml(""+lesson.subjectName+"")); + views.setTextViewText(R.id.widgetTimetableSubjectName, TextExtensionsKt.asItalicSpannable(lesson.subjectName)); if (lesson.lessonChangeNoClassroom) { - views.setTextViewText(R.id.widgetTimetableClassroomName, Html.fromHtml(""+lesson.classroomName+"")); + views.setTextViewText(R.id.widgetTimetableClassroomName, TextExtensionsKt.asStrikethroughSpannable(lesson.classroomName)); } else { - views.setTextViewText(R.id.widgetTimetableClassroomName, Html.fromHtml("" + lesson.classroomName + "")); + views.setTextViewText(R.id.widgetTimetableClassroomName, TextExtensionsKt.asItalicSpannable(lesson.classroomName)); } } else if (lesson.lessonCancelled) { - views.setTextViewText(R.id.widgetTimetableSubjectName, Html.fromHtml(""+lesson.subjectName+"")); - views.setTextViewText(R.id.widgetTimetableClassroomName, Html.fromHtml(""+lesson.classroomName+"")); + views.setTextViewText(R.id.widgetTimetableSubjectName, TextExtensionsKt.asStrikethroughSpannable(lesson.subjectName)); + views.setTextViewText(R.id.widgetTimetableClassroomName, TextExtensionsKt.asStrikethroughSpannable(lesson.classroomName)); } else { views.setTextViewText(R.id.widgetTimetableSubjectName, lesson.subjectName); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableProvider.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableProvider.kt index 6d5badf5..a0be777c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableProvider.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/widgets/timetable/WidgetTimetableProvider.kt @@ -25,11 +25,18 @@ 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.sizeDp -import pl.szczodrzynski.edziennik.* +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.entity.Event.Companion.TYPE_HOMEWORK import pl.szczodrzynski.edziennik.data.db.entity.Lesson import pl.szczodrzynski.edziennik.data.db.entity.Lesson.Companion.TYPE_NO_LESSONS +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 import pl.szczodrzynski.edziennik.ui.widgets.WidgetConfig import pl.szczodrzynski.edziennik.utils.models.Date @@ -52,8 +59,8 @@ class WidgetTimetableProvider : AppWidgetProvider() { return getPendingSelfIntent(context, intent) } - fun getPendingSelfIntent(context: Context, intent: Intent): PendingIntent { - return PendingIntent.getBroadcast(context, 0, intent, 0) + private fun getPendingSelfIntent(context: Context, intent: Intent): PendingIntent { + return PendingIntent.getBroadcast(context, 0, intent, pendingIntentFlag()) } fun drawableToBitmap(drawable: Drawable): Bitmap { @@ -110,10 +117,10 @@ class WidgetTimetableProvider : AppWidgetProvider() { refreshIntent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE refreshIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds) val refreshPendingIntent = PendingIntent.getBroadcast(context, - 0, refreshIntent, PendingIntent.FLAG_UPDATE_CURRENT) + 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, @@ -123,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) @@ -331,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 @@ -355,7 +357,7 @@ class WidgetTimetableProvider : AppWidgetProvider() { for (event in events) { if (event.time == null || event.time != lesson.displayStartTime) continue - model.eventColors.add(if (event.type == TYPE_HOMEWORK) ItemWidgetTimetableModel.EVENT_COLOR_HOMEWORK else event.eventColor) + model.eventColors.add(if (event.isHomework) ItemWidgetTimetableModel.EVENT_COLOR_HOMEWORK else event.eventColor) } models += model @@ -389,11 +391,11 @@ class WidgetTimetableProvider : AppWidgetProvider() { headerIntent.putExtra("profileId", it) } displayingDate?.let { - headerIntent.putExtra("timetableDate", it.value) + headerIntent.putExtra("timetableDate", it.stringY_m_d) } } - headerIntent.putExtra("fragmentId", MainActivity.DRAWER_ITEM_TIMETABLE) - val headerPendingIntent = PendingIntent.getActivity(app, appWidgetId, headerIntent, 0) + headerIntent.putExtras("fragmentId" to NavTarget.TIMETABLE) + val headerPendingIntent = PendingIntent.getActivity(app, appWidgetId, headerIntent, pendingIntentMutable()) views.setOnClickPendingIntent(R.id.widgetTimetableHeader, headerPendingIntent) timetables!!.put(appWidgetId, models) @@ -407,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, 0) + val itemPendingIntent = PendingIntent.getActivity(app, appWidgetId, itemIntent, pendingIntentMutable()) views.setPendingIntentTemplate(R.id.widgetTimetableListView, itemPendingIntent) if (!unified) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/BetterLink.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/BetterLink.kt index 53aa0429..ee6a3f65 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/BetterLink.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/BetterLink.kt @@ -7,6 +7,8 @@ package pl.szczodrzynski.edziennik.utils import android.annotation.SuppressLint +import android.content.Context +import android.content.Intent import android.graphics.Color import android.text.Spannable import android.text.SpannableString @@ -19,141 +21,273 @@ import android.view.ViewGroup import android.widget.TextView import androidx.appcompat.view.menu.MenuBuilder import androidx.appcompat.view.menu.MenuPopupHelper -import pl.szczodrzynski.edziennik.Intent -import pl.szczodrzynski.edziennik.copyToClipboard +import androidx.core.widget.addTextChangedListener +import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.api.Regexes -import pl.szczodrzynski.edziennik.get -import pl.szczodrzynski.edziennik.getTextPosition +import pl.szczodrzynski.edziennik.data.db.enums.FeatureType +import pl.szczodrzynski.edziennik.ext.* +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget import pl.szczodrzynski.edziennik.utils.models.Date +@SuppressLint("RestrictedApi") object BetterLink { - @SuppressLint("RestrictedApi") - fun attach(textView: TextView, onActionSelected: (() -> Unit)? = null) { - textView.autoLinkMask = Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES - BetterLinkMovementMethod.linkify(textView.autoLinkMask, textView).setOnLinkClickListener { v, span: BetterLinkMovementMethod.ClickableSpanWithText -> - val url = span.text() - val c = v.context + /** + * Used in conjunction with the item's ID to execute the + * [attach]'s onActionSelected listener when the item is + * clicked. + */ + private const val FLAG_ACTION = 0x8000 - val s = v.text as Spanned - val start = s.getSpanStart(span.span()) - val end = s.getSpanEnd(span.span()) + private fun MenuBuilder.setTitle(title: CharSequence): MenuBuilder { + this::class.java.getDeclaredMethod("setHeaderTitleInt", CharSequence::class.java).let { + it.isAccessible = true + it.invoke(this, title) + } + return this + } - val parent = v.rootView.findViewById(android.R.id.content) - val parentLocation = intArrayOf(0, 0) - parent.getLocationOnScreen(parentLocation) - - val rect = textView.getTextPosition(start..end) - - val view = View(c) - view.layoutParams = ViewGroup.LayoutParams(rect.width(), rect.height()) - view.setBackgroundColor(Color.TRANSPARENT) - - parent.addView(view) - - view.x = rect.left.toFloat() - parentLocation[0] - view.y = rect.top.toFloat() - parentLocation[1] - - val menu = MenuBuilder(c) - val helper = MenuPopupHelper(c, menu, view) - val popup = helper.popup - - var menuTitle = url.substringAfter(":") - var date: Date? = null - - var urlItem: MenuItem? = null - var createEventItem: MenuItem? = null - //var goToTimetableItem: MenuItem? = null // TODO 2020-03-19: implement this - var mailItem: MenuItem? = null - var copyItem: MenuItem? = null - - when { - url.startsWith("mailto:") -> { - mailItem = menu.add(1, 20, 2, "Napisz e-mail") + private fun MenuItem.addListener(listener: (item: MenuItem) -> Boolean): MenuItem { + this::class.java.getDeclaredField("mClickListener").let { + it.isAccessible = true + val oldListener = it.get(this) as? MenuItem.OnMenuItemClickListener + it.set(this, object : MenuItem.OnMenuItemClickListener { + override fun onMenuItemClick(item: MenuItem): Boolean { + oldListener?.onMenuItemClick(item) + return listener(item) } - url.startsWith("dateYmd:") -> { - createEventItem = menu.add(1, 10, 2, "Utwórz wydarzenie") - //goToTimetableItem = menu.add(1, 11, 3, "Idź do planu lekcji") - date = parseDateYmd(menuTitle) - } - url.startsWith("dateDmy:") -> { - createEventItem = menu.add(1, 10, 2, "Utwórz wydarzenie") - //goToTimetableItem = menu.add(1, 11, 3, "Idź do planu lekcji") - date = parseDateDmy(menuTitle) - } - url.startsWith("dateAbs:") -> { - createEventItem = menu.add(1, 10, 2, "Utwórz wydarzenie") - //goToTimetableItem = menu.add(1, 11, 3, "Idź do planu lekcji") - date = parseDateAbs(menuTitle) - } - url.startsWith("dateRel:") -> { - createEventItem = menu.add(1, 10, 2, "Utwórz wydarzenie") - //goToTimetableItem = menu.add(1, 11, 3, "Idź do planu lekcji") - date = parseDateRel(menuTitle) - } - else -> { - urlItem = menu.add(1, 1, 2, "Otwórz w przeglądarce") - menuTitle = url - } - } - copyItem = menu.add(1, 1000, 1000, "Kopiuj tekst") + }) + } + return this + } - helper.setOnDismissListener { parent.removeView(view) } + private fun createUrlItems(menu: MenuBuilder, context: Context, url: String) { + menu.setTitle(url) + menu.add( + 1, + 2, + 2, + "Otwórz w przeglądarce" + ).setOnMenuItemClickListener { + Utils.openUrl(context, url) + true + } + } - urlItem?.setOnMenuItemClickListener { Utils.openUrl(c, url); true } - mailItem?.setOnMenuItemClickListener { Utils.openUrl(c, url); true } - copyItem?.setOnMenuItemClickListener { menuTitle.copyToClipboard(c); true } - createEventItem?.setOnMenuItemClickListener { - onActionSelected?.invoke() - val intent = Intent( - android.content.Intent.ACTION_MAIN, - "action" to "createManualEvent", - "eventDate" to date?.stringY_m_d - ) - c.sendBroadcast(intent) - true - } + private fun createMailtoItems(menu: MenuBuilder, context: Context, url: String) { + menu.add( + 1, + 3, + 3, + "Napisz e-mail" + ).setOnMenuItemClickListener { + Utils.openUrl(context, url) + true + } + } - menu::class.java.getDeclaredMethod("setHeaderTitleInt", CharSequence::class.java).let { - it.isAccessible = true - it.invoke(menu, menuTitle) - } - popup::class.java.getDeclaredField("mShowTitle").let { - it.isAccessible = true - it.set(popup, true) - } - helper::class.java.getDeclaredMethod("showPopup", Int::class.java, Int::class.java, Boolean::class.java, Boolean::class.java).let { - it.isAccessible = true - it.invoke(helper, 0, 0, false, true) - } + private fun createDateItems(menu: MenuBuilder, context: Context, date: Date?) { + date ?: return + menu.setTitle(date.formattedString) + menu.add( + 1, + 4 or FLAG_ACTION, + 4, + "Utwórz wydarzenie" + ).setOnMenuItemClickListener { + val intent = Intent( + Intent.ACTION_MAIN, + "action" to "createManualEvent", + "eventDate" to date.stringY_m_d + ) + context.sendBroadcast(intent) + true + } + } + + private fun createTeacherItems(menu: MenuBuilder, context: Context, teacherId: Long, fullName: String) { + if (!(context.applicationContext as App).profile.hasFeature(FeatureType.MESSAGES_INBOX)) + return + menu.setTitle(fullName) + menu.add( + 1, + 5 or FLAG_ACTION, + 5, + "Napisz wiadomość" + ).setOnMenuItemClickListener { + val intent = Intent( + Intent.ACTION_MAIN, + "fragmentId" to NavTarget.MESSAGE_COMPOSE, + "messageRecipientId" to teacherId + ) + context.sendBroadcast(intent) + true + } + } + + private fun onClickListener( + view: TextView, + span: BetterLinkMovementMethod.ClickableSpanWithText, + onActionSelected: (() -> Unit)? + ): Boolean { + val context = view.context + + val spanned = view.text as Spanned + val start = spanned.getSpanStart(span.span()) + val end = spanned.getSpanEnd(span.span()) + + val parent = view.rootView.findViewById(android.R.id.content) + val parentLocation = intArrayOf(0, 0) + parent.getLocationOnScreen(parentLocation) + + val rect = view.getTextPosition(start..end) + + val popupView = View(context) + popupView.layoutParams = ViewGroup.LayoutParams(rect.width(), rect.height()) + popupView.setBackgroundColor(Color.TRANSPARENT) + + parent.addView(popupView) + + popupView.x = rect.left.toFloat() - parentLocation[0] + popupView.y = rect.top.toFloat() - parentLocation[1] + + val menu = MenuBuilder(context) + val helper = MenuPopupHelper(context, menu, popupView) + val popup = helper.popup + + val spanUrl = span.text() + val spanParameter = spanUrl.substringAfter(":") + val spanText = spanned.substring(start, end) + + //goToTimetableItem = menu.add(1, 11, 3, "Idź do planu lekcji") + + // create appropriate items for spans + when { + spanUrl.startsWith("mailto:") -> createMailtoItems(menu, context, spanUrl) + spanUrl.startsWith("dateYmd:") -> createDateItems(menu, context, parseDateYmd(spanParameter)) + spanUrl.startsWith("dateDmy:") -> createDateItems(menu, context, parseDateDmy(spanParameter)) + spanUrl.startsWith("dateAbs:") -> createDateItems(menu, context, parseDateAbs(spanParameter)) + spanUrl.startsWith("dateRel:") -> createDateItems(menu, context, parseDateRel(spanParameter)) + spanUrl.startsWith("teacher:") -> createTeacherItems( + menu, + context, + teacherId = spanParameter.toLongOrNull() ?: -1, + fullName = spanText + ) + else -> createUrlItems(menu, context, spanUrl) + } + menu.add(1, 1000, 1000, "Kopiuj tekst").setOnMenuItemClickListener { + spanParameter.copyToClipboard(context) true } - val spanned = textView.text as? Spannable ?: { - SpannableString(textView.text) - }() + helper.setOnDismissListener { parent.removeView(popupView) } + + menu.visibleItems.forEach { item -> + if ((item.itemId and FLAG_ACTION) != FLAG_ACTION) + return@forEach + item.addListener { + onActionSelected?.invoke() + true + } + } + + popup::class.java.getDeclaredField("mShowTitle").let { + it.isAccessible = true + it.set(popup, true) + } + helper::class.java.getDeclaredMethod( + "showPopup", + Int::class.java, + Int::class.java, + Boolean::class.java, + Boolean::class.java + ).let { + it.isAccessible = true + it.invoke(helper, 0, 0, false, true) + } + return true + } + + fun attach( + textView: TextView, + teachers: Map? = null, + onActionSelected: (() -> Unit)? = null + ) { + textView.autoLinkMask = Linkify.WEB_URLS or Linkify.EMAIL_ADDRESSES + + BetterLinkMovementMethod + .linkify(textView.autoLinkMask, textView) + .setOnLinkClickListener { view, span -> + onClickListener(view, span, onActionSelected) + } + + textView.addTextChangedListener { + attachSpan(textView, teachers) + } + + attachSpan(textView, teachers) + } + + private fun attachSpan( + textView: TextView, + teachers: Map? = null + ) { + val spanned = textView.text as? Spannable ?: SpannableString(textView.text) + + teachers?.forEach { (id, fullName) -> + val index = textView.text.indexOf(fullName) + if (index == -1) + return@forEach + val span = URLSpan("teacher:$id") + spanned.setSpan( + span, + index, + index + fullName.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } Regexes.LINKIFY_DATE_YMD.findAll(textView.text).forEach { match -> val span = URLSpan("dateYmd:" + match.value) - spanned.setSpan(span, match.range.first, match.range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spanned.setSpan( + span, + match.range.first, + match.range.last + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) } Regexes.LINKIFY_DATE_DMY.findAll(textView.text).forEach { match -> val span = URLSpan("dateDmy:" + match.value) - spanned.setSpan(span, match.range.first, match.range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spanned.setSpan( + span, + match.range.first, + match.range.last + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) } Regexes.LINKIFY_DATE_ABSOLUTE.findAll(textView.text).forEach { match -> val span = URLSpan("dateAbs:" + match.value) - spanned.setSpan(span, match.range.first, match.range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spanned.setSpan( + span, + match.range.first, + match.range.last + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) } Regexes.LINKIFY_DATE_RELATIVE.findAll(textView.text).forEach { match -> val span = URLSpan("dateRel:" + match.value) - spanned.setSpan(span, match.range.first, match.range.last + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spanned.setSpan( + span, + match.range.first, + match.range.last + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) } - //Linkify.addLinks(textView, LINKIFY_DATE_ABSOLUTE.toPattern(), "dateAbs:") - //Linkify.addLinks(textView, LINKIFY_DATE_RELATIVE.toPattern(), "dateRel:") } - private val monthNames = listOf("sty", "lut", "mar", "kwi", "maj", "cze", "lip", "sie", "wrz", "paź", "lis", "gru") + private val monthNames = + listOf("sty", "lut", "mar", "kwi", "maj", "cze", "lip", "sie", "wrz", "paź", "lis", "gru") private fun parseDateYmd(text: String): Date? { return Regexes.LINKIFY_DATE_YMD.find(text)?.let { @@ -163,6 +297,7 @@ object BetterLink { Date(year, month, day) } } + private fun parseDateDmy(text: String): Date? { return Regexes.LINKIFY_DATE_DMY.find(text)?.let { val day = it[1].toIntOrNull() ?: 1 @@ -194,7 +329,7 @@ object BetterLink { else -> 1 } - date.stepForward(0, 0, amount*unitInDays) + date.stepForward(0, 0, amount * unitInDays) } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/BigNightUtil.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/BigNightUtil.kt new file mode 100644 index 00000000..c7f57ccc --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/BigNightUtil.kt @@ -0,0 +1,69 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-4-8. + */ + +package pl.szczodrzynski.edziennik.utils + +import pl.szczodrzynski.edziennik.utils.models.Date +import kotlin.math.absoluteValue + +// Obliczanie daty wielkanocy - algorytm Gaussa +// www.algorytm.org +// (c) 2008 by Tomasz Lubinski +// http://www.algorytm.org/przetwarzanie-dat/wyznaczanie-daty-wielkanocy-algortym-gaussa/dwg-j.html + +class BigNightUtil { + + /* Pobierz wartosc A z tabeli lat */ + private fun getA(rok: Int) = when { + rok <= 1582 -> 15 + rok <= 1699 -> 22 + rok <= 1899 -> 23 + rok <= 2199 -> 24 + rok <= 2299 -> 25 + rok <= 2399 -> 26 + rok <= 2499 -> 25 + else -> 0 + } + + /* Pobierz wartosc B z tabeli lat */ + private fun getB(rok: Int) = when { + rok <= 1582 -> 6 + rok <= 1699 -> 2 + rok <= 1799 -> 3 + rok <= 1899 -> 4 + rok <= 2099 -> 5 + rok <= 2199 -> 6 + rok <= 2299 -> 0 + rok <= 2499 -> 1 + else -> 0 + } + + /* oblicz ile dni po 22 marca przypada wielkanoc */ + private fun Oblicz_Date_wielkanocy(rok: Int): Int { + val a = rok % 19 + val b = rok % 4 + val c = rok % 7 + var d = (a * 19 + getA(rok)) % 30 + val e = (2 * b + 4 * c + 6 * d + getB(rok)) % 7 + if (d == 29 && e == 6 || d == 28 && e == 6) { + d -= 7 + } + return d + e + } + + private fun get_dataOf_bigNight(): Date { + val date = Date.getToday() + date.month = 4 + date.day = 22 + Oblicz_Date_wielkanocy(date.year) + if (date.day > 31) + date.day = date.day % 31 + else + date.month = 3 + + return date + } + + fun isDataWielkanocyNearDzisiaj() = + Date.diffDays(Date.getToday(), get_dataOf_bigNight()).absoluteValue < 7 +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/Colors.java b/app/src/main/java/pl/szczodrzynski/edziennik/utils/Colors.java index 1f6c7ad3..39371516 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/Colors.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/Colors.java @@ -15,6 +15,7 @@ import androidx.core.graphics.ColorUtils; import java.security.MessageDigest; import java.util.Arrays; import java.util.Random; +import java.util.zip.CRC32; import pl.szczodrzynski.edziennik.data.db.entity.Grade; @@ -84,6 +85,28 @@ public class Colors { 0xFF6D4C41 }; + public static final int[] materialColorsBasic = { + 0xFFE53935, 0xFFD81B60, 0xFF8E24AA, 0xFF5E35B1, + 0xFF3949AB, 0xFF1E88E5, 0xFF039BE5, 0xFF00ACC1, + 0xFF00897B, 0xFF43A047, 0xFF7CB342, 0xFFC0CA33, + 0xFFFDD835, 0xFFFFB300, 0xFFFB8C00, 0xFFF4511E, + 0xFF6D4C41, 0xFF757575, 0xFF546E7A, 0xFF00E676, + + 0xFFEF9A9A, 0xFFF48FB1, 0xFFCE93D8, 0xFFB39DDB, + 0xFF9FA8DA, 0xFF90CAF9, 0xFF81D4FA, 0xFF80DEEA, + 0xFF80CBC4, 0xFFA5D6A7, 0xFFC5E1A5, 0xFFE6EE9C, + 0xFFFFF59D, 0xFFFFE082, 0xFFFFCC80, 0xFFFFAB91, + 0xFFBCAAA4, 0xFFEEEEEE, 0xFFB0BEC5, 0xFFCCFF90, + }; + + public static final int[] metroColors = { + 0xFF76FF03, 0xFF60A917, 0xFF00C853, 0xFF00ABA9, + 0xFF1BA1E2, 0xFF0050EF, 0xFF6A00FF, 0xFFAA00FF, + 0xFFF472D0, 0xFFD80073, 0xFFA20025, 0xFFE51400, + 0xFFFA6800, 0xFFF0A30A, 0xFFE3C800, 0xFF795548, + 0xFF6D8764, 0xFF647687, 0xFF76608A, 0xFFA0522D, + }; + /** * Used for teacher's images (e.g. in messages or announcements). * @param s teacher's fullName @@ -115,6 +138,18 @@ public class Colors { return materialColors[getRandomNumberInRange(0, materialColors.length-1, seed)]; } + public static int stringToMaterialColorCRC(String s) { + long seed; + try { + CRC32 crc = new CRC32(); + crc.update(s.getBytes()); + seed = crc.getValue(); + } catch (Exception e) { + seed = 1234; + } + return metroColors[(int) (seed % metroColors.length)]; + } + public static int gradeToColor(Grade grade) { if (grade.getType() == Grade.TYPE_POINT_SUM) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/DefaultTextStyles.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/DefaultTextStyles.kt new file mode 100644 index 00000000..7f1fa02a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/DefaultTextStyles.kt @@ -0,0 +1,55 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-11. + */ + +package pl.szczodrzynski.edziennik.utils + +import android.text.style.StrikethroughSpan +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.databinding.StyledTextButtonsBinding +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.StylingConfig +import pl.szczodrzynski.edziennik.utils.span.* + +object DefaultTextStyles { + + fun getAsList(b: StyledTextButtonsBinding) = listOf( + StylingConfig.Style( + button = b.bold, + spanClass = BoldSpan::class.java, + icon = CommunityMaterial.Icon2.cmd_format_bold, + hint = R.string.hint_style_bold, + ), + StylingConfig.Style( + button = b.italic, + spanClass = ItalicSpan::class.java, + icon = CommunityMaterial.Icon2.cmd_format_italic, + hint = R.string.hint_style_italic, + ), + StylingConfig.Style( + button = b.underline, + // a custom span is used to prevent issues with keyboards which underline words + spanClass = UnderlineCustomSpan::class.java, + icon = CommunityMaterial.Icon2.cmd_format_underline, + hint = R.string.hint_style_underline, + ), + StylingConfig.Style( + button = b.strike, + spanClass = StrikethroughSpan::class.java, + icon = CommunityMaterial.Icon2.cmd_format_strikethrough, + hint = R.string.hint_style_strike, + ), + StylingConfig.Style( + button = b.subscript, + spanClass = SubscriptSizeSpan::class.java, + icon = CommunityMaterial.Icon2.cmd_format_subscript, + hint = R.string.hint_style_subscript, + ), + StylingConfig.Style( + button = b.superscript, + spanClass = SuperscriptSizeSpan::class.java, + icon = CommunityMaterial.Icon2.cmd_format_superscript, + hint = R.string.hint_style_superscript, + ), + ) +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/MutableLazy.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/MutableLazy.kt index cf000e1f..f742c185 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/MutableLazy.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/MutableLazy.kt @@ -19,7 +19,6 @@ class MutableLazyImpl(initializer: () -> T, lock: Any? = null) { return synchronized(lock) { val typedValue = initializer!!() _value = typedValue - initializer = null typedValue } } @@ -29,7 +28,11 @@ class MutableLazyImpl(initializer: () -> T, lock: Any? = null) { fun isInitialized() = _value !== UNINITIALIZED_VALUE + fun deinitialize() { + _value = UNINITIALIZED_VALUE + } + override fun toString() = if (isInitialized()) _value.toString() else "ChangeableLazy value not initialized yet." } -fun mutableLazy(initializer: () -> T): MutableLazyImpl = MutableLazyImpl(initializer) \ No newline at end of file +fun mutableLazy(initializer: () -> T): MutableLazyImpl = MutableLazyImpl(initializer) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/PausedNavigationData.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/PausedNavigationData.kt new file mode 100644 index 00000000..b5975089 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/PausedNavigationData.kt @@ -0,0 +1,14 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-9. + */ + +package pl.szczodrzynski.edziennik.utils + +import android.os.Bundle +import pl.szczodrzynski.edziennik.ui.base.enums.NavTarget + +data class PausedNavigationData( + val profileId: Int?, + val navTarget: NavTarget?, + val args: Bundle?, +) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/SwipeRefreshLayoutNoIndicator.java b/app/src/main/java/pl/szczodrzynski/edziennik/utils/SwipeRefreshLayoutNoIndicator.java index 75192eec..b81123ec 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/SwipeRefreshLayoutNoIndicator.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/SwipeRefreshLayoutNoIndicator.java @@ -17,6 +17,7 @@ package pl.szczodrzynski.edziennik.utils; import android.content.Context; +import android.os.Build; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; @@ -83,7 +84,9 @@ public class SwipeRefreshLayoutNoIndicator extends SwipeRefreshLayout { @Override public boolean onInterceptTouchEvent(MotionEvent ev) { - ev.setSource(0x10000000); + if (Build.VERSION.SDK_INT < 32) { + ev.setSource(0x10000000); + } boolean parentConsumed = parent.onInterceptTouchEvent(ev); boolean superConsumed = super.onInterceptTouchEvent(ev); return parentConsumed && superConsumed; diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputDropDown.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputDropDown.kt index f8836374..44db5d9a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputDropDown.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputDropDown.kt @@ -1,7 +1,11 @@ package pl.szczodrzynski.edziennik.utils +import android.annotation.SuppressLint import android.content.Context +import android.graphics.drawable.Drawable import android.util.AttributeSet +import androidx.appcompat.view.menu.MenuBuilder +import androidx.appcompat.view.menu.MenuPopupHelper import androidx.appcompat.widget.PopupMenu import com.google.android.material.textfield.TextInputEditText import com.mikepenz.iconics.IconicsDrawable @@ -29,10 +33,11 @@ open class TextInputDropDown : TextInputEditText { val selectedId get() = selected?.id - fun updateText() { + private fun updateText() { setText(selected?.displayText ?: selected?.text) } + @SuppressLint("RestrictedApi") open fun create(context: Context) { val drawable = IconicsDrawable(context, CommunityMaterial.Icon.cmd_chevron_down).apply { colorInt = Themes.getPrimaryTextColor(context) @@ -58,7 +63,9 @@ open class TextInputDropDown : TextInputEditText { val popup = PopupMenu(context, this) items.forEachIndexed { index, item -> - popup.menu.add(0, item.id.toInt(), index, item.text) + popup.menu.add(0, item.id.toInt(), index, item.text).also { + it.icon = item.icon + } } popup.setOnMenuItemClickListener { menuItem -> @@ -70,29 +77,46 @@ open class TextInputDropDown : TextInputEditText { true } - popup.setOnDismissListener { + val helper = MenuPopupHelper(context, popup.menu as MenuBuilder, this) + helper.setForceShowIcon(true) + helper.setOnDismissListener { clearFocus() } - - popup.show() + helper.show() } } - fun select(item: Item): Item? { + /** + * Select an arbitrary [item]. Allows to select an item not present + * in the original list. + */ + fun select(item: Item): Item { selected = item updateText() error = null return item } + /** + * Select an item by its ID. Returns the selected item + * if found. + */ fun select(id: Long?): Item? { return items.singleOrNull { it.id == id }?.let { select(it) } } + /** + * Select an item by its tag. Returns the selected item + * if found. + */ fun select(tag: Any?): Item? { return items.singleOrNull { it.tag == tag }?.let { select(it) } } + /** + * Select an item by its index. Returns the selected item + * if the index exists. + */ fun select(index: Int): Item? { return items.getOrNull(index)?.let { select(it) } } @@ -143,5 +167,11 @@ open class TextInputDropDown : TextInputEditText { } } - class Item(val id: Long, val text: CharSequence, val displayText: CharSequence? = null, val tag: Any? = null) + class Item( + val id: Long, + val text: CharSequence, + val displayText: CharSequence? = null, + val tag: Any? = null, + val icon: Drawable? = null + ) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputKeyboardEdit.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputKeyboardEdit.kt index cbb4f5a0..0b5e1c67 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputKeyboardEdit.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/TextInputKeyboardEdit.kt @@ -18,6 +18,7 @@ class TextInputKeyboardEdit : AppCompatEditText { * Keyboard Listener */ internal var listener: KeyboardListener? = null + private var selectionListener: ((Int, Int) -> Unit)? = null constructor(context: Context) : super(context) @@ -27,14 +28,12 @@ class TextInputKeyboardEdit : AppCompatEditText { override fun onFocusChanged(focused: Boolean, direction: Int, previouslyFocusedRect: Rect?) { super.onFocusChanged(focused, direction, previouslyFocusedRect) - if (listener != null) - listener!!.onStateChanged(this, true) + listener?.onStateChanged(this, true) } override fun onKeyPreIme(keyCode: Int, @NonNull event: KeyEvent): Boolean { if (event.keyCode == KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) { - if (listener != null) - listener!!.onStateChanged(this, false) + listener?.onStateChanged(this, false) // Hide cursor isFocusable = false @@ -50,6 +49,15 @@ class TextInputKeyboardEdit : AppCompatEditText { this.listener = listener } + fun setSelectionChangedListener(listener: ((selectionStart: Int, selectionEnd: Int) -> Unit)?) { + this.selectionListener = listener + } + + override fun onSelectionChanged(selStart: Int, selEnd: Int) { + super.onSelectionChanged(selStart, selEnd) + selectionListener?.invoke(selStart, selEnd) + } + interface KeyboardListener { fun onStateChanged(keyboardEditText: TextInputKeyboardEdit, showing: Boolean) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/Utils.java b/app/src/main/java/pl/szczodrzynski/edziennik/utils/Utils.java index 910fc6f0..f039952d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/Utils.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/Utils.java @@ -774,13 +774,21 @@ public class Utils { private static File storageDir = null; public static File getStorageDir() { - if (storageDir != null) - return storageDir; - storageDir = Environment.getExternalStoragePublicDirectory("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); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/BetterHtml.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/BetterHtml.kt index 4119369e..97610845 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/BetterHtml.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/html/BetterHtml.kt @@ -6,46 +6,82 @@ package pl.szczodrzynski.edziennik.utils.html import android.content.Context import android.graphics.Color +import android.graphics.Typeface +import android.text.Editable import android.text.SpannableStringBuilder import android.text.Spanned -import android.text.style.BulletSpan +import android.text.Spanned.* +import android.text.style.* +import androidx.appcompat.widget.AppCompatEditText import androidx.core.graphics.ColorUtils import androidx.core.text.HtmlCompat -import pl.szczodrzynski.edziennik.dp -import pl.szczodrzynski.edziennik.resolveAttr +import pl.szczodrzynski.edziennik.data.api.Regexes.MESSAGE_META +import pl.szczodrzynski.edziennik.ext.dp +import pl.szczodrzynski.edziennik.ext.getWordBounds +import pl.szczodrzynski.edziennik.ext.resolveAttr +import pl.szczodrzynski.edziennik.utils.span.* import pl.szczodrzynski.navlib.blendColors object BetterHtml { + val customSpanClasses = listOf( + BoldSpan::class.java, + ItalicSpan::class.java, + UnderlineCustomSpan::class.java, + StrikethroughSpan::class.java, + SubscriptSizeSpan::class.java, + SuperscriptSizeSpan::class.java, + ) + + fun fromHtml(context: Context, stringRes: Int) = fromHtml( + context, + context.getString(stringRes), + nl2br = true, + ) + @JvmStatic - fun fromHtml(context: Context, html: String): Spanned { + fun fromHtml(context: Context?, html: CharSequence, nl2br: Boolean = false): Spanned { val hexPattern = "(#[a-fA-F0-9]{6})" val colorRegex = "(?:color=\"$hexPattern\")|(?:style=\"color: ?${hexPattern})" - .toRegex(RegexOption.IGNORE_CASE) + .toRegex(RegexOption.IGNORE_CASE) var text = html - .replace("\\[META:[A-z0-9]+;[0-9-]+]".toRegex(), "") - .replace("background-color: ?$hexPattern;".toRegex(), "") + .replace(MESSAGE_META, "") + .replace("background-color: ?$hexPattern;".toRegex(), "") + // treat paragraphs as if they had no margin + .replace("", "
    ") - val colorBackground = android.R.attr.colorBackground.resolveAttr(context) - val textColorPrimary = android.R.attr.textColorPrimary.resolveAttr(context) and 0xffffff + // this is used only in Notes and Events + if (nl2br) { + text = text.replace("\n", "
    ") + } - colorRegex.findAll(text).forEach { result -> - val group = result.groups.drop(1).firstOrNull { it != null } ?: return@forEach + if (context != null) { + val colorBackground = android.R.attr.colorBackground.resolveAttr(context) + val textColorPrimary = android.R.attr.textColorPrimary.resolveAttr(context) and 0xffffff - val color = Color.parseColor(group.value) - var newColor = 0xff000000.toInt() or color + colorRegex.findAll(text).forEach { result -> + val group = result.groups.drop(1).firstOrNull { it != null } ?: return@forEach - var blendAmount = 1 - var numIterations = 0 + val color = Color.parseColor(group.value) + var newColor = 0xff000000.toInt() or color - while (numIterations < 100 && ColorUtils.calculateContrast(colorBackground, newColor) < 4.5f) { - blendAmount += 2 - newColor = blendColors(color, blendAmount shl 24 or textColorPrimary) - numIterations++ + var blendAmount = 1 + var numIterations = 0 + + while (numIterations < 100 && ColorUtils.calculateContrast( + colorBackground, + newColor + ) < 4.5f + ) { + blendAmount += 2 + newColor = blendColors(color, blendAmount shl 24 or textColorPrimary) + numIterations++ + } + + text = text.replaceRange(group.range, "#" + (newColor and 0xffffff).toString(16)) } - - text = text.replaceRange(group.range, "#" + (newColor and 0xffffff).toString(16)) } /*val olRegex = """
      (.+?)""" @@ -59,26 +95,113 @@ object BetterHtml { @Suppress("DEPRECATION") val htmlSpannable = HtmlCompat.fromHtml( - text, - HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM or HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST or HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_DIV, - null, - LiTagHandler() - ) + text, + HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST_ITEM + or HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_LIST + or HtmlCompat.FROM_HTML_SEPARATOR_LINE_BREAK_DIV + or HtmlCompat.FROM_HTML_MODE_LEGACY, + null, + LiTagHandler() + ).trimEnd() // fromHtml seems to add two line breaks at the end, needlessly - val spannableBuilder = SpannableStringBuilder(htmlSpannable) - val bulletSpans = spannableBuilder.getSpans(0, spannableBuilder.length, BulletSpan::class.java) - bulletSpans.forEach { - val start = spannableBuilder.getSpanStart(it) - val end = spannableBuilder.getSpanEnd(it) - spannableBuilder.removeSpan(it) - spannableBuilder.setSpan( - ImprovedBulletSpan(bulletRadius = 3.dp, startWidth = 24.dp, gapWidth = 8.dp), - start, - end, - Spanned.SPAN_INCLUSIVE_EXCLUSIVE - ) + val spanned = SpannableStringBuilder(htmlSpannable) + spanned.getSpans(0, spanned.length, Any::class.java).forEach { + val spanStart = spanned.getSpanStart(it) + val spanEnd = spanned.getSpanEnd(it) + val spanFlags = spanned.getSpanFlags(it) + + val newSpan: Any? = when (it) { + is BulletSpan -> ImprovedBulletSpan( + bulletRadius = 3.dp, + startWidth = 24.dp, + gapWidth = 8.dp + ) + is StyleSpan -> when (it.style) { + Typeface.BOLD -> BoldSpan() + Typeface.ITALIC -> ItalicSpan() + else -> null + } + is UnderlineSpan -> UnderlineCustomSpan() + is SubscriptSpan -> SubscriptSizeSpan() + is SuperscriptSpan -> SuperscriptSizeSpan() + else -> null + } + + if (newSpan != null) { + spanned.removeSpan(it) + spanned.setSpan(newSpan, spanStart, spanEnd, spanFlags) + } } - return spannableBuilder + return spanned + } + + fun applyFormat(span: Any, editText: AppCompatEditText) { + applyFormat(span, editText.text ?: return, editText.selectionStart, editText.selectionEnd) + } + + fun removeFormat(span: Any?, editText: AppCompatEditText) { + removeFormat(span, editText.text ?: return, editText.selectionStart, editText.selectionEnd) + } + + fun applyFormat(span: Any, spanned: Editable, selectionStart: Int, selectionEnd: Int) { + if (selectionStart == -1 || selectionEnd == -1) return + val cursorOnly = selectionStart == selectionEnd + + val wordBounds = spanned.getWordBounds(selectionStart, onlyInWord = true) + if (cursorOnly && wordBounds != null) { + // use the detected word bounds instead of cursor/selection + val (start, end) = wordBounds + spanned.setSpan(span, start, end, SPAN_EXCLUSIVE_INCLUSIVE) + } else { + val spanFlags = if (cursorOnly) + SPAN_INCLUSIVE_INCLUSIVE + else + SPAN_EXCLUSIVE_INCLUSIVE + spanned.setSpan(span, selectionStart, selectionEnd, spanFlags) + } + } + + fun removeFormat(span: Any?, spanned: Editable, selectionStart: Int, selectionEnd: Int) { + if (selectionStart == -1 || selectionEnd == -1) return + val cursorOnly = selectionStart == selectionEnd + + val spanClass = span?.javaClass ?: Any::class.java + spanned.getSpans(selectionStart, selectionEnd, spanClass).forEach { + if (span == null && it::class.java !in customSpanClasses) + return@forEach + val spanStart = spanned.getSpanStart(it) + val spanEnd = spanned.getSpanEnd(it) + val wordBounds = spanned.getWordBounds(selectionStart, onlyInWord = true) + + val (newSpanStart, newSpanEnd, newSpanFlags) = when { + !cursorOnly -> { + // cut the selected range out of the span + Triple(selectionStart, selectionEnd, SPAN_EXCLUSIVE_INCLUSIVE) + } + wordBounds == null -> { + // this allows to change spans mid-word - EXCLUSIVE so the style does + // not apply to characters typed later + // it's set back to INCLUSIVE when the cursor enters the word again + // (onSelectionChanged) + Triple(selectionStart, selectionEnd, SPAN_EXCLUSIVE_EXCLUSIVE) + } + else /* wordBounds != null */ -> { + // a word is selected, slice the span in two + Triple(wordBounds.first, wordBounds.second, SPAN_EXCLUSIVE_INCLUSIVE) + } + } + + // remove the existing span + spanned.removeSpan(it) + // "clone" the span so it can be applied twice, if needed + // (can't use 'span' from the parameters as it's nullable) + val itClone = it::class.java.newInstance() + // reapply the span wherever needed + if (spanStart < newSpanStart) + spanned.setSpan(it, spanStart, newSpanStart, newSpanFlags) + if (spanEnd > newSpanEnd) + spanned.setSpan(itClone, newSpanEnd, spanEnd, newSpanFlags) + } } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/AttendanceManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/AttendanceManager.kt index cd726797..5b112af9 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/AttendanceManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/AttendanceManager.kt @@ -4,6 +4,9 @@ package pl.szczodrzynski.edziennik.utils.managers +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import eu.szkolny.font.SzkolnyFont import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -11,7 +14,7 @@ import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.data.db.entity.Attendance import pl.szczodrzynski.edziennik.data.db.entity.AttendanceType import pl.szczodrzynski.edziennik.data.db.full.AttendanceFull -import pl.szczodrzynski.edziennik.startCoroutineTimer +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer import kotlin.coroutines.CoroutineContext class AttendanceManager(val app: App) : CoroutineScope { @@ -21,7 +24,7 @@ class AttendanceManager(val app: App) : CoroutineScope { get() = job + Dispatchers.Default val useSymbols - get() = app.config.forProfile().attendance.useSymbols + get() = app.profile.config.attendance.useSymbols fun getTypeShort(baseType: Int): String { return when (baseType) { @@ -63,6 +66,17 @@ class AttendanceManager(val app: App) : CoroutineScope { else getAttendanceColor(attendance.baseType) } + fun getAttendanceIcon(attendance: Attendance): IIcon? = when (attendance.baseType) { + Attendance.TYPE_PRESENT, Attendance.TYPE_PRESENT_CUSTOM -> CommunityMaterial.Icon.cmd_check + Attendance.TYPE_ABSENT -> CommunityMaterial.Icon.cmd_close + Attendance.TYPE_ABSENT_EXCUSED -> CommunityMaterial.Icon3.cmd_progress_close + Attendance.TYPE_RELEASED -> CommunityMaterial.Icon.cmd_account_arrow_right_outline + Attendance.TYPE_BELATED -> CommunityMaterial.Icon.cmd_clock_alert_outline + Attendance.TYPE_BELATED_EXCUSED -> CommunityMaterial.Icon.cmd_clock_check_outline + Attendance.TYPE_DAY_FREE -> SzkolnyFont.Icon.szf_umbrella_beach_outline + else -> null + } + /* _ _ _____ _____ _ __ _ | | | |_ _| / ____| (_)/ _(_) | | | | | | | (___ _ __ ___ ___ _| |_ _ ___ diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/AvailabilityManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/AvailabilityManager.kt new file mode 100644 index 00000000..2c989a5a --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/AvailabilityManager.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-9-18. + */ + +package pl.szczodrzynski.edziennik.utils.managers + +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.BuildConfig +import pl.szczodrzynski.edziennik.data.api.* +import pl.szczodrzynski.edziennik.data.api.models.ApiError +import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi +import pl.szczodrzynski.edziennik.data.api.szkolny.response.RegisterAvailabilityStatus +import pl.szczodrzynski.edziennik.data.db.entity.Profile +import pl.szczodrzynski.edziennik.data.db.enums.LoginType +import pl.szczodrzynski.edziennik.ext.currentTimeUnix +import pl.szczodrzynski.edziennik.ext.toApiError + +class AvailabilityManager(val app: App) { + companion object { + private const val TAG = "AvailabilityManager" + } + + private val api = SzkolnyApi(app) + + data class Error( + val type: Type, + val status: RegisterAvailabilityStatus?, + val apiError: ApiError? + ) { + companion object { + fun notAvailable(status: RegisterAvailabilityStatus) = + Error(Type.NOT_AVAILABLE, status, null) + + fun apiError(apiError: ApiError) = + Error(Type.API_ERROR, null, apiError) + + fun noApiAccess() = + Error(Type.NO_API_ACCESS, null, null) + } + + enum class Type { + NOT_AVAILABLE, + API_ERROR, + NO_API_ACCESS, + } + } + + fun check(profile: Profile, cacheOnly: Boolean = false): Error? { + return check(profile.registerName, cacheOnly) + } + + fun check(loginType: LoginType, cacheOnly: Boolean = false): Error? { + return check(loginType.name.lowercase(), cacheOnly) + } + + fun check(registerName: String, cacheOnly: Boolean = false): Error? { + if (!app.config.apiAvailabilityCheck) + return null + val status = app.config.sync.registerAvailability[registerName] + if (status != null && status.nextCheckAt > currentTimeUnix()) { + return reportStatus(status) + } + if (cacheOnly) { + return reportStatus(status) + } + + return try { + val availability = api.getRegisterAvailability() + app.config.sync.registerAvailability = availability + reportStatus(availability[registerName]) + } catch (e: Throwable) { + reportApiError(e) + } + } + + private fun reportStatus(status: RegisterAvailabilityStatus?): Error? { + if (status == null) + return null + if (!status.available || status.minVersionCode > BuildConfig.VERSION_CODE) + return Error.notAvailable(status) + return null + } + + private fun reportApiError(throwable: Throwable): Error { + val apiError = throwable.toApiError(TAG) + if (apiError.errorCode == ERROR_API_INVALID_SIGNATURE) { + app.config.sync.registerAvailability = mapOf() + return Error.noApiAccess() + } + return Error.apiError(apiError) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/BuildManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/BuildManager.kt index ab126826..2cbf4f23 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/BuildManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/BuildManager.kt @@ -9,11 +9,30 @@ import android.text.TextUtils import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import okhttp3.Request -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.BuildConfig +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.Signing -import pl.szczodrzynski.edziennik.ui.modules.base.BuildInvalidActivity +import pl.szczodrzynski.edziennik.data.api.szkolny.response.Update +import pl.szczodrzynski.edziennik.ext.Intent +import pl.szczodrzynski.edziennik.ext.asBoldSpannable +import pl.szczodrzynski.edziennik.ext.asColoredSpannable +import pl.szczodrzynski.edziennik.ext.concat +import pl.szczodrzynski.edziennik.ext.getJsonObject +import pl.szczodrzynski.edziennik.ext.getString +import pl.szczodrzynski.edziennik.ext.isNotNullNorBlank +import pl.szczodrzynski.edziennik.ext.join +import pl.szczodrzynski.edziennik.ext.md5 +import pl.szczodrzynski.edziennik.ext.resolveAttr +import pl.szczodrzynski.edziennik.ext.resolveColor +import pl.szczodrzynski.edziennik.ext.toJsonObject +import pl.szczodrzynski.edziennik.ui.base.BuildInvalidActivity import pl.szczodrzynski.edziennik.utils.Utils import pl.szczodrzynski.edziennik.utils.Utils.d import java.time.Instant @@ -74,6 +93,14 @@ class BuildManager(val app: App) : CoroutineScope { else -> null } + val releaseType = when { + isNightly || isDaily -> Update.Type.NIGHTLY + BuildConfig.VERSION_BASE.endsWith("-dev") -> Update.Type.DEV + BuildConfig.VERSION_BASE.contains("-beta.") -> Update.Type.BETA + BuildConfig.VERSION_BASE.contains("-rc.") -> Update.Type.RC + else -> Update.Type.RELEASE + } + fun showVersionDialog(activity: AppCompatActivity) { val yes = activity.getString(R.string.yes) val no = activity.getString(R.string.no) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt index ca444ca5..38a8fee3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/EventManager.kt @@ -4,12 +4,23 @@ package pl.szczodrzynski.edziennik.utils.managers +import android.widget.TextView +import androidx.core.view.isVisible +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.sizeDp +import com.mikepenz.iconics.view.IconicsTextView import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.full.EventFull -import pl.szczodrzynski.edziennik.startCoroutineTimer +import pl.szczodrzynski.edziennik.ext.concat +import pl.szczodrzynski.edziennik.ext.join +import pl.szczodrzynski.edziennik.ext.resolveColor +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer import kotlin.coroutines.CoroutineContext class EventManager(val app: App) : CoroutineScope { @@ -32,4 +43,48 @@ class EventManager(val app: App) : CoroutineScope { app.db.metadataDao().setSeen(event.profileId, event, true) } } + + fun setEventTopic( + title: TextView, + event: EventFull, + showType: Boolean = true, + showSubject: Boolean = false, + showNotes: Boolean = true, + doneIconColor: Int? = null + ) { + val topicSpan = event.getNoteSubstituteText(showNotes) ?: event.topicHtml + val hasReplacingNotes = event.hasReplacingNotes() + + title.text = listOfNotNull( + if (event.addedManually && !event.isSharedReceived) "{cmd-calendar-edit} " else null, + if (event.isSharedReceived) "{cmd-share-variant} " else null, + if (event.hasNotes() && hasReplacingNotes && showNotes) "{cmd-swap-horizontal} " else null, + if (event.hasNotes() && !hasReplacingNotes && showNotes) "{cmd-playlist-edit} " else null, + if (showType) "${event.typeName ?: "wydarzenie"} - " else null, + if (showSubject) event.subjectLongName?.plus(" - ") else null, + topicSpan, + ).concat() + + title.setCompoundDrawables( + null, + null, + if (event.isDone) IconicsDrawable(title.context).apply { + icon = CommunityMaterial.Icon.cmd_check + colorInt = doneIconColor ?: R.color.md_green_500.resolveColor(title.context) + sizeDp = 24 + } else null, + null + ) + } + + fun setLegendText(legend: IconicsTextView, event: EventFull, showNotes: Boolean = true) { + legend.text = listOfNotNull( + if (event.addedManually) R.string.legend_event_added_manually else null, + if (event.isSharedSent) R.string.legend_event_shared_sent else null, + if (event.isSharedReceived) R.string.legend_event_shared_received else null, + if (event.isDone) R.string.legend_event_is_done else null, + if (showNotes) NoteManager.getLegendText(event) else null, + ).map { legend.context.getString(it) }.join("\n") + legend.isVisible = legend.text.isNotBlank() + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/GradesManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/GradesManager.kt index 61ea5ad2..bfce3067 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/GradesManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/GradesManager.kt @@ -8,15 +8,23 @@ import android.content.Context import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job -import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.db.entity.Grade import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_NORMAL import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_POINT_AVG import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_POINT_SUM import pl.szczodrzynski.edziennik.data.db.entity.Grade.Companion.TYPE_YEAR_FINAL import pl.szczodrzynski.edziennik.data.db.full.GradeFull -import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesAverages -import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesSemester +import pl.szczodrzynski.edziennik.ext.asColoredSpannable +import pl.szczodrzynski.edziennik.ext.get +import pl.szczodrzynski.edziennik.ext.ifNotNull +import pl.szczodrzynski.edziennik.ext.notEmptyOrNull +import pl.szczodrzynski.edziennik.ext.plural +import pl.szczodrzynski.edziennik.ext.resolveAttr +import pl.szczodrzynski.edziennik.ext.startCoroutineTimer +import pl.szczodrzynski.edziennik.ui.grades.models.GradesAverages +import pl.szczodrzynski.edziennik.ui.grades.models.GradesSemester import java.text.DecimalFormat import kotlin.coroutines.CoroutineContext import kotlin.math.floor @@ -46,21 +54,21 @@ class GradesManager(val app: App) : CoroutineScope { val orderBy get() = app.config.grades.orderBy val yearAverageMode - get() = app.config.forProfile().grades.yearAverageMode + get() = app.profile.config.grades.yearAverageMode val colorMode - get() = app.config.forProfile().grades.colorMode + get() = app.profile.config.grades.colorMode val plusValue - get() = app.config.forProfile().grades.plusValue + get() = app.profile.config.grades.plusValue val minusValue - get() = app.config.forProfile().grades.minusValue + get() = app.profile.config.grades.minusValue val dontCountEnabled - get() = app.config.forProfile().grades.dontCountEnabled + get() = app.profile.config.grades.dontCountEnabled val dontCountGrades - get() = app.config.forProfile().grades.dontCountGrades + get() = app.profile.config.grades.dontCountGrades val hideImproved - get() = app.config.forProfile().grades.hideImproved + get() = app.profile.config.grades.hideImproved val averageWithoutWeight - get() = app.config.forProfile().grades.averageWithoutWeight + get() = app.profile.config.grades.averageWithoutWeight fun getOrderByString() = when (orderBy) { @@ -118,7 +126,7 @@ class GradesManager(val app: App) : CoroutineScope { fun getGradeWeight(dontCountEnabled: Boolean, dontCountGrades: List, grade: Grade): Float { if (!dontCountEnabled) return grade.weight - if (grade.name.toLowerCase().trim() in dontCountGrades) + if (grade.name.lowercase().trim() in dontCountGrades) return 0f return grade.weight } @@ -151,7 +159,7 @@ class GradesManager(val app: App) : CoroutineScope { } type == TYPE_NORMAL && defColor -> grade.color and 0xffffff type in TYPE_NORMAL..TYPE_YEAR_FINAL -> { - when (grade.name.toLowerCase()) { + when (grade.name.lowercase()) { "+", "++", "+++" -> 0x4caf50 "0", "-", "-,", "-,-,", "np", "np.", "npnp", "np,", "np,np,", "bs", "nk", "bz" -> 0xff7043 "1-", "1", "f", "ng" -> 0xff0000 @@ -179,7 +187,7 @@ class GradesManager(val app: App) : CoroutineScope { * the specified [name]. */ fun getGradeValue(name: String): Float { - return when (name.toLowerCase()) { + return when (name.lowercase()) { "1-" -> 0.75f "1" -> 1.00f "1+" -> 1.50f @@ -209,7 +217,7 @@ class GradesManager(val app: App) : CoroutineScope { } fun getGradeNumberName(name: String): String { - return when(name.toLowerCase()){ + return when(name.lowercase()){ "niedostateczny", "f" -> "1" "niedostateczny plus", "f+" -> "1+" "niedostateczny minus", "f-" -> "1-" diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/MessageManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/MessageManager.kt new file mode 100644 index 00000000..6b0b66fa --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/MessageManager.kt @@ -0,0 +1,293 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-7. + */ + +package pl.szczodrzynski.edziennik.utils.managers + +import android.content.Context +import android.os.Bundle +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.widget.EditText +import com.hootsuite.nachos.NachoTextView +import com.hootsuite.nachos.chip.ChipInfo +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import com.mikepenz.iconics.utils.colorRes +import com.mikepenz.iconics.view.IconicsImageView +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.data.db.entity.Message +import pl.szczodrzynski.edziennik.data.db.entity.MessageRecipient +import pl.szczodrzynski.edziennik.data.db.entity.Metadata +import pl.szczodrzynski.edziennik.data.db.entity.Teacher +import pl.szczodrzynski.edziennik.data.db.enums.MetadataType +import pl.szczodrzynski.edziennik.data.db.full.MessageFull +import pl.szczodrzynski.edziennik.ext.appendSpan +import pl.szczodrzynski.edziennik.ext.appendText +import pl.szczodrzynski.edziennik.ext.fixName +import pl.szczodrzynski.edziennik.ext.setText +import pl.szczodrzynski.edziennik.ui.messages.MessagesUtils +import pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit +import pl.szczodrzynski.edziennik.utils.html.BetterHtml +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.HtmlMode.ORIGINAL +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.StylingConfig +import pl.szczodrzynski.edziennik.utils.models.Date +import pl.szczodrzynski.edziennik.utils.models.Time +import pl.szczodrzynski.edziennik.utils.span.BoldSpan +import pl.szczodrzynski.edziennik.utils.span.ItalicSpan +import pl.szczodrzynski.navlib.colorAttr + +class MessageManager(private val app: App) { + + class UIConfig( + val context: Context, + val recipients: NachoTextView, + val subject: EditText, + val body: TextInputKeyboardEdit, + val teachers: List, + val greetingOnCompose: Boolean, + val greetingOnReply: Boolean, + val greetingOnForward: Boolean, + val greetingText: String, + ) + + private val textStylingManager + get() = app.textStylingManager + + suspend fun getMessage(profileId: Int, args: Bundle?): MessageFull? { + val id = args?.getLong("messageId") ?: return null + val json = args.getString("message") + val addedDate = args.getLong("sentDate") + return getMessage(profileId, id, json, addedDate) + } + + suspend fun getMessage( + profileId: Int, + id: Long, + json: String?, + sentDate: Long = 0L + ): MessageFull? { + val message = if (json != null) { + app.gson.fromJson(json, MessageFull::class.java)?.also { + if (sentDate > 0L) { + it.addedDate = sentDate + } + withContext(Dispatchers.IO) { + it.recipients = app.db.messageRecipientDao().getAllByMessageId(profileId, it.id) + } + } + } else { + withContext(Dispatchers.IO) { + app.db.messageDao().getByIdNow(profileId, id) + } + } ?: return null + + // this helps when multiple profiles receive the same message + // (there are multiple -1 recipients for the same message ID) + val recipientsDistinct = message.recipients?.filter { it.profileId == profileId } ?: return null + message.recipients?.clear() + message.recipients?.addAll(recipientsDistinct) + + // load recipients for sent messages + val teachers = withContext(Dispatchers.IO) { + app.db.teacherDao().getAllNow(profileId) + } + + message.recipients?.forEach { recipient -> + // store the account name as a recipient + if (recipient.id == -1L) + recipient.fullName = app.profile.accountName ?: app.profile.studentNameLong + + // lookup a teacher by the recipient ID + if (recipient.fullName == null) + recipient.fullName = teachers.firstOrNull { it.id == recipient.id }?.fullName ?: "" + + // unset the readByEveryone flag + if (recipient.readDate < 1 && message.isSent) + message.readByEveryone = false + } + + // store the account name as sender for sent messages + if (message.isSent && message.senderName == null) { + message.senderName = app.profile.accountName ?: app.profile.studentNameLong + } + + // set the message as seen + if (message.body != null && !message.seen) { + app.db.metadataDao().setSeen(profileId, message, true) + } + //msg.recipients = app.db.messageRecipientDao().getAllByMessageId(msg.profileId, msg.id) + + return message + } + + fun setStarIcon(image: IconicsImageView, message: Message) { + if (message.isStarred) { + image.icon?.colorRes = R.color.md_amber_500 + image.icon?.icon = CommunityMaterial.Icon3.cmd_star + } else { + image.icon?.colorAttr(image.context, android.R.attr.textColorSecondary) + image.icon?.icon = CommunityMaterial.Icon3.cmd_star_outline + } + } + + suspend fun starMessage(message: Message, isStarred: Boolean) { + message.isStarred = isStarred + withContext(Dispatchers.Default) { + app.db.messageDao().replace(message) + } + } + + suspend fun markAsDeleted(message: Message) { + message.type = Message.TYPE_DELETED + withContext(Dispatchers.Default) { + app.db.messageDao().replace(message) + } + } + + suspend fun deleteDraft(profileId: Int, messageId: Long) { + withContext(Dispatchers.Default) { + app.db.messageRecipientDao().clearFor(profileId, messageId) + app.db.messageDao().delete(profileId, messageId) + app.db.metadataDao().delete(profileId, MetadataType.MESSAGE, messageId) + } + } + + suspend fun saveAsDraft(config: UIConfig, stylingConfig: StylingConfig, profileId: Int, messageId: Long?) { + val teachers = config.recipients.allChips.mapNotNull { it.data as? Teacher } + val subject = config.subject.text?.toString() ?: "" + val body = textStylingManager.getHtmlText(stylingConfig, htmlMode = ORIGINAL) + + withContext(Dispatchers.Default) { + if (messageId != null) { + app.db.messageRecipientDao().clearFor(profileId, messageId) + } + + val message = Message( + profileId = profileId, + id = messageId ?: System.currentTimeMillis(), + type = Message.TYPE_DRAFT, + subject = subject, + body = body, + senderId = -1L, + addedDate = System.currentTimeMillis(), + ) + val metadata = Metadata(profileId, MetadataType.MESSAGE, message.id, true, true) + + val recipients = teachers.map { + MessageRecipient(profileId, it.id, message.id) + } + + app.db.messageDao().replace(message) + app.db.messageRecipientDao().addAll(recipients) + app.db.metadataDao().add(metadata) + } + } + + fun fillWithBundle(config: UIConfig, args: Bundle?): Message? { + args ?: return null + val messageJson = args.getString("message") + val teacherId = args.getLong("messageRecipientId") + val subject = args.getString("messageSubject") + val payloadType = args.getString("type") + + if (config.greetingOnCompose) + config.body.setText(config.greetingText) + if (subject != null) + config.subject.setText(subject) + + val message = if (messageJson != null) + app.gson.fromJson(messageJson, MessageFull::class.java) + else null + + when { + message != null && message.isDraft -> { + fillWithDraftMessage(config, message) + } + message != null -> { + fillWithMessage(config, message, payloadType) + } + teacherId != 0L -> { + fillWithRecipientIds(config, teacherId) + } + } + + return message + } + + private fun createRecipientChips(config: UIConfig, vararg teacherIds: Long?): List { + return teacherIds.mapNotNull { teacherId -> + val teacher = config.teachers.firstOrNull { it.id == teacherId } ?: return@mapNotNull null + teacher.image = MessagesUtils.getProfileImage( + diameterDp = 48, + textSizeBigDp = 24, + textSizeMediumDp = 16, + textSizeSmallDp = 12, + count = 1, + teacher.fullName + ) + ChipInfo(teacher.fullName, teacher) + } + } + + private fun fillWithRecipientIds(config: UIConfig, vararg teacherIds: Long?) { + config.recipients.addTextWithChips(createRecipientChips(config, *teacherIds)) + } + + private fun fillWithMessage(config: UIConfig, message: MessageFull, payloadType: String?) { + val spanned = SpannableStringBuilder() + + val dateString = config.context.getString( + R.string.messages_reply_date_time_format, + Date.fromMillis(message.addedDate).formattedStringShort, + Time.fromMillis(message.addedDate).stringHM, + ) + // add original message info + spanned.appendText("W dniu ") + spanned.appendSpan(dateString, ItalicSpan(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spanned.appendText(", ") + spanned.appendSpan(message.senderName.fixName(), ItalicSpan(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spanned.appendText(" napisał(a):") + spanned.setSpan(BoldSpan(), 0, spanned.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + spanned.appendText("\n\n") + + val greeting = when (payloadType) { + "reply" -> { + config.subject.setText(R.string.messages_compose_subject_reply_format, message.subject) + if (config.greetingOnReply) + config.greetingText + else null + } + "forward" -> { + config.subject.setText(R.string.messages_compose_subject_forward_format, message.subject) + if (config.greetingOnForward) + config.greetingText + else null + } + else -> null + } + + if (greeting == null) { + spanned.replace(0, 0, "\n\n") + } else { + spanned.replace(0, 0, "$greeting\n\n\n") + } + + val body = message.body ?: config.context.getString(R.string.messages_compose_body_load_failed) + spanned.appendText(BetterHtml.fromHtml(config.context, body)) + + fillWithRecipientIds(config, message.senderId) + config.body.text = spanned + } + + private fun fillWithDraftMessage(config: UIConfig, message: MessageFull) { + val recipientIds = message.recipients?.map { it.id }?.toTypedArray() ?: emptyArray() + fillWithRecipientIds(config, *recipientIds) + + config.subject.setText(message.subject) + + val body = message.body ?: config.context.getString(R.string.messages_compose_body_load_failed) + config.body.setText(BetterHtml.fromHtml(config.context, body)) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/NoteManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/NoteManager.kt new file mode 100644 index 00000000..50ac84cd --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/NoteManager.kt @@ -0,0 +1,312 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-17. + */ + +package pl.szczodrzynski.edziennik.utils.managers + +import android.annotation.SuppressLint +import android.text.SpannableStringBuilder +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.isVisible +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.mikepenz.iconics.view.IconicsTextView +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import pl.szczodrzynski.edziennik.App +import pl.szczodrzynski.edziennik.R +import pl.szczodrzynski.edziennik.data.db.entity.Note +import pl.szczodrzynski.edziennik.data.db.entity.Note.OwnerType +import pl.szczodrzynski.edziennik.data.db.entity.Noteable +import pl.szczodrzynski.edziennik.data.db.full.* +import pl.szczodrzynski.edziennik.databinding.NoteDialogHeaderBinding +import pl.szczodrzynski.edziennik.ext.resolveDrawable +import pl.szczodrzynski.edziennik.ui.agenda.DayDialog +import pl.szczodrzynski.edziennik.ui.agenda.lessonchanges.LessonChangesAdapter +import pl.szczodrzynski.edziennik.ui.announcements.AnnouncementsAdapter +import pl.szczodrzynski.edziennik.ui.attendance.AttendanceAdapter +import pl.szczodrzynski.edziennik.ui.attendance.AttendanceDetailsDialog +import pl.szczodrzynski.edziennik.ui.attendance.AttendanceFragment +import pl.szczodrzynski.edziennik.ui.behaviour.NoticesAdapter +import pl.szczodrzynski.edziennik.ui.event.EventDetailsDialog +import pl.szczodrzynski.edziennik.ui.event.EventListAdapter +import pl.szczodrzynski.edziennik.ui.grades.GradeDetailsDialog +import pl.szczodrzynski.edziennik.ui.grades.GradesAdapter +import pl.szczodrzynski.edziennik.ui.messages.list.MessagesAdapter +import pl.szczodrzynski.edziennik.ui.timetable.LessonDetailsDialog +import pl.szczodrzynski.edziennik.utils.models.Date + +class NoteManager(private val app: App) { + companion object { + private const val TAG = "NoteManager" + + @SuppressLint("SetTextI18n") + fun prependIcon(owner: Noteable, textView: IconicsTextView) { + if (owner.hasNotes()) + textView.text = SpannableStringBuilder( + if (owner.hasReplacingNotes()) + "{cmd-swap-horizontal} " + else + "{cmd-playlist-edit} " + ).append(textView.text) + } + + fun getLegendText(owner: Noteable): Int? = when { + owner.hasReplacingNotes() -> R.string.legend_notes_added_replaced + owner.hasNotes() -> R.string.legend_notes_added + else -> null + } + + fun setLegendText(owner: Noteable, textView: IconicsTextView) { + textView.isVisible = owner.hasNotes() + textView.setText(getLegendText(owner) ?: return) + } + } + + fun getOwner(note: Note): Any? { + if (note.ownerId == null) + return null + return when (note.ownerType) { + OwnerType.ANNOUNCEMENT -> + app.db.announcementDao().getByIdNow(note.profileId, note.ownerId) + OwnerType.ATTENDANCE -> + app.db.attendanceDao().getByIdNow(note.profileId, note.ownerId) + OwnerType.BEHAVIOR -> + app.db.noticeDao().getByIdNow(note.profileId, note.ownerId) + OwnerType.EVENT -> + app.db.eventDao().getByIdNow(note.profileId, note.ownerId) + OwnerType.EVENT_SUBJECT, OwnerType.LESSON_SUBJECT -> + app.db.subjectDao().getByIdNow(note.profileId, note.ownerId) + OwnerType.GRADE -> + app.db.gradeDao().getByIdNow(note.profileId, note.ownerId) + OwnerType.LESSON -> + app.db.timetableDao().getByOwnerIdNow(note.profileId, note.ownerId) + OwnerType.MESSAGE -> + app.db.messageDao().getByIdNow(note.profileId, note.ownerId) + else -> null + } + } + + fun hasValidOwner(note: Note): Boolean { + if (note.ownerType == null || note.ownerType == OwnerType.DAY) + return true + return getOwner(note) != null + } + + suspend fun saveNote( + activity: AppCompatActivity, + note: Note, + teamId: Long?, + wasShared: Boolean, + ): Boolean { + val success = when { + !note.isShared && wasShared -> unshareNote(activity, note) + note.isShared -> shareNote(activity, note, teamId) + else -> true + } + + if (!success) + return false + + withContext(Dispatchers.IO) { + app.db.noteDao().add(note) + } + return true + } + + suspend fun deleteNote(activity: AppCompatActivity, note: Note): Boolean { + val success = when { + note.isShared -> unshareNote(activity, note) + else -> true + } + + if (!success) + return false + + withContext(Dispatchers.IO) { + app.db.noteDao().delete(note) + } + return true + } + + private suspend fun shareNote(activity: AppCompatActivity, note: Note, teamId: Long?): Boolean { + return app.api.runCatching(activity) { + shareNote(note, teamId) + } != null + } + + private suspend fun unshareNote(activity: AppCompatActivity, note: Note): Boolean { + return app.api.runCatching(activity) { + unshareNote(note) + } != null + } + + private fun getAdapterForItem( + activity: AppCompatActivity, + item: Noteable, + ): RecyclerView.Adapter<*>? { + return when (item) { + is AnnouncementFull -> AnnouncementsAdapter(activity, mutableListOf(item), null) + + is AttendanceFull -> AttendanceAdapter( + activity, + showNotes = false, + onAttendanceClick = { + showItemDetailsDialog(activity, it) + }, + type = AttendanceFragment.VIEW_LIST + ).also { + it.items = mutableListOf(item) + } + + is NoticeFull -> { + NoticesAdapter(activity, listOf(item)) + } + + is Date -> { + TODO("Date adapter is not yet implemented.") + } + + is EventFull -> EventListAdapter( + activity = activity, + simpleMode = true, + showDate = true, + showTypeColor = false, + showTime = false, + markAsSeen = false, + showNotes = false, + onEventClick = { + showItemDetailsDialog(activity, it) + }, + ).also { + it.setAllItems(listOf(item)) + } + + is GradeFull -> GradesAdapter(activity, showNotes = false, onGradeClick = { + showItemDetailsDialog(activity, it) + }).also { + it.items = mutableListOf(item) + } + + is LessonFull -> LessonChangesAdapter(activity, showNotes = false, onLessonClick = { + showItemDetailsDialog(activity, it) + }).also { + it.items = listOf(item) + } + + is MessageFull -> MessagesAdapter( + activity = activity, + teachers = listOf(), + showNotes = false, + onMessageClick = null, + ).also { + it.setAllItems(listOf(item)) + } + else -> null + } + } + + private fun showItemDetailsDialog( + activity: AppCompatActivity, + item: Noteable, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, + ) { + when (item) { + is AnnouncementFull -> return + is AttendanceFull -> AttendanceDetailsDialog( + activity = activity, + attendance = item, + showNotes = false, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ).show() + is NoticeFull -> return + is Date -> DayDialog( + activity = activity, + profileId = App.profileId, + date = item, + showNotes = false, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ).show() + is EventFull -> EventDetailsDialog( + activity = activity, + event = item, + showNotes = false, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ).show() + is GradeFull -> GradeDetailsDialog( + activity = activity, + grade = item, + showNotes = false, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ).show() + is LessonFull -> LessonDetailsDialog( + activity = activity, + lesson = item, + showNotes = false, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ).show() + is MessageFull -> return + } + } + + fun getOwnerTypeText(owner: OwnerType) = when (owner) { + OwnerType.ANNOUNCEMENT -> R.string.notes_type_announcement + OwnerType.ATTENDANCE -> R.string.notes_type_attendance + OwnerType.BEHAVIOR -> R.string.notes_type_behavior + OwnerType.DAY -> R.string.notes_type_day + OwnerType.EVENT -> R.string.notes_type_event + OwnerType.EVENT_SUBJECT -> TODO() + OwnerType.GRADE -> R.string.notes_type_grade + OwnerType.LESSON -> R.string.notes_type_lesson + OwnerType.LESSON_SUBJECT -> TODO() + OwnerType.MESSAGE -> R.string.notes_type_message + OwnerType.NONE -> throw Exception("NONE is not a valid OwnerType.") + } + + fun getOwnerTypeImage(owner: OwnerType) = when (owner) { + OwnerType.ANNOUNCEMENT -> R.drawable.ic_announcement + OwnerType.ATTENDANCE -> R.drawable.ic_attendance + OwnerType.BEHAVIOR -> R.drawable.ic_behavior + OwnerType.DAY -> R.drawable.ic_calendar_day + OwnerType.EVENT -> R.drawable.ic_calendar_event + OwnerType.EVENT_SUBJECT -> TODO() + OwnerType.GRADE -> R.drawable.ic_grade + OwnerType.LESSON -> R.drawable.ic_timetable + OwnerType.LESSON_SUBJECT -> TODO() + OwnerType.MESSAGE -> R.drawable.ic_message + OwnerType.NONE -> throw Exception("NONE is not a valid OwnerType.") + } + + fun configureHeader( + activity: AppCompatActivity, + noteOwner: Noteable?, + b: NoteDialogHeaderBinding, + ) { + if (noteOwner == null) { + b.title.isVisible = false + b.divider.isVisible = false + b.ownerItemList.isVisible = false + return + } + b.ownerItemList.apply { + adapter = getAdapterForItem(activity, noteOwner) + isNestedScrollingEnabled = false + //setHasFixedSize(true) + layoutManager = LinearLayoutManager(context) + } + + b.title.setText(getOwnerTypeText(noteOwner.getNoteType())) + b.title.setCompoundDrawables( + getOwnerTypeImage(noteOwner.getNoteType()).resolveDrawable(activity), + null, + null, + null, + ) + } +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/PermissionManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/PermissionManager.kt index 65430f60..e99f9e3e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/PermissionManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/PermissionManager.kt @@ -36,7 +36,13 @@ class PermissionManager(val app: App) : CoroutineScope { app.checkSelfPermission(name) == PackageManager.PERMISSION_GRANTED else true - + val isNotificationPermissionGranted by lazy { + if (Build.VERSION.SDK_INT >= 33) { + app.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED + } else { + true + } + } private fun openPermissionSettings(activity: AppCompatActivity) { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) val uri = Uri.fromParts("package", app.packageName, null) @@ -80,6 +86,10 @@ class PermissionManager(val app: App) : CoroutineScope { .show() } result.hasPermanentDenied() -> { + if (!isRequired) { + onSuccess() + return@launch + } MaterialAlertDialogBuilder(activity) .setTitle(R.string.permissions_required) .setMessage(R.string.permissions_denied) @@ -92,6 +102,18 @@ class PermissionManager(val app: App) : CoroutineScope { } } } + fun requestNotificationsPermission( + activity: AppCompatActivity, + @StringRes permissionMessage: Int, + isRequired: Boolean = false, + onSuccess: suspend CoroutineScope.() -> Unit + ) = requestPermission( + activity, + permissionMessage, + isRequired, + Manifest.permission.POST_NOTIFICATIONS, + onSuccess + ) fun requestStoragePermission( activity: AppCompatActivity, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/TextStylingManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/TextStylingManager.kt new file mode 100644 index 00000000..7a96c270 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/TextStylingManager.kt @@ -0,0 +1,307 @@ +/* + * Copyright (c) Kuba Szczodrzyński 2021-10-7. + */ + +package pl.szczodrzynski.edziennik.utils.managers + +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.StrikethroughSpan +import android.text.style.SubscriptSpan +import android.text.style.SuperscriptSpan +import android.text.style.UnderlineSpan +import android.widget.Button +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.appcompat.app.AppCompatActivity +import androidx.core.text.HtmlCompat +import androidx.core.view.isVisible +import androidx.core.widget.addTextChangedListener +import com.google.android.material.button.MaterialButton +import com.google.android.material.button.MaterialButtonToggleGroup +import com.google.android.material.textfield.TextInputLayout +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.IIcon +import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial +import com.mikepenz.iconics.utils.sizeDp +import pl.szczodrzynski.edziennik.* +import pl.szczodrzynski.edziennik.ext.attachToastHint +import pl.szczodrzynski.edziennik.ext.hasSet +import pl.szczodrzynski.edziennik.ext.replaceSpan +import pl.szczodrzynski.edziennik.ui.dialogs.StyledTextDialog +import pl.szczodrzynski.edziennik.utils.TextInputKeyboardEdit +import pl.szczodrzynski.edziennik.utils.html.BetterHtml +import pl.szczodrzynski.edziennik.utils.managers.TextStylingManager.HtmlMode.* +import pl.szczodrzynski.edziennik.utils.span.BoldSpan +import pl.szczodrzynski.edziennik.utils.span.ItalicSpan + +class TextStylingManager(private val app: App) { + companion object { + private const val TAG = "TextStylingManager" + } + + private val paragraphBrRegex by lazy { + "((?:
      )+)

      ".toRegex() + } + + enum class HtmlMode { + /** + * The default mode, suitable for fromHtml conversion. + */ + ORIGINAL, + + /** + * A more browser-compatible mode. + */ + COMPATIBLE, + + /** + * A simple, paragraph-stripped mode with \n instead of
      . + * The converted text has no HTML tags when no spans in source. + */ + SIMPLE, + + /** + * Markdown-compatible text mode. + */ + MARKDOWN, + } + + open class StylingConfigBase( + val editText: TextInputKeyboardEdit, + val htmlMode: HtmlMode = ORIGINAL, + ) { + var watchStyleChecked = true + var watchSelectionChanged = true + } + + class StylingConfig( + editText: TextInputKeyboardEdit, + val fontStyleGroup: MaterialButtonToggleGroup, + val fontStyleClear: Button, + val styles: List