From 7c925cb88af7eae07769fd03730fcd5a6e93ed2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kuba=20Szczodrzy=C5=84ski?= Date: Thu, 28 Oct 2021 22:35:30 +0200 Subject: [PATCH] [UI] Add Notes feature. (#99) * [DB] Add Room schema export location. * [DB] Add Note entity and migration 96. * Add correct database schema * [Notes] Implement basic note list UI. * [DB] Implement Noteable in Full entities. Add note relation and filtering. * [Notes] Make Note searchable. * [UI] Disable onClick listeners in adapters when null. * [UI] Implement showing note list in dialog. * [UI] Update note dialogs UI. * [Notes] Add note details dialog. * [Notes] Extract note dialogs header into a separate layout. * [Notes] Add note editor dialog. * [Notes] Show note icons in dialogs and lists. * [Notes] Add showing substitute text. * [Notes] Add replacing notes icon. * [Notes] Add sharing and receiving notes. * [Notes] Add notes list UI fragment. * [Notes] Implement adding notes without owner. * [Notes] Add color names. * [Notes] Add notes card on home screen. * [Notes] Add notes card migration. --- app/build.gradle | 6 + .../97.json | 2293 +++++++++++++++++ .../java/pl/szczodrzynski/edziennik/App.kt | 3 + .../pl/szczodrzynski/edziennik/Binding.kt | 63 + .../szczodrzynski/edziennik/MainActivity.kt | 9 + .../edziennik/config/ProfileConfig.kt | 2 +- .../config/utils/ProfileConfigMigration.kt | 10 + .../edziennik/data/api/szkolny/SzkolnyApi.kt | 106 +- .../data/api/szkolny/SzkolnyService.kt | 3 + .../api/szkolny/request/EventShareRequest.kt | 5 +- .../api/szkolny/request/NoteShareRequest.kt | 25 + .../szkolny/response/ServerSyncResponse.kt | 2 + .../edziennik/data/api/task/AppSync.kt | 8 +- .../edziennik/data/api/task/SzkolnyTask.kt | 2 +- .../szczodrzynski/edziennik/data/db/AppDb.kt | 5 +- .../edziennik/data/db/dao/BaseDao.kt | 4 + .../edziennik/data/db/dao/NoteDao.kt | 43 + .../edziennik/data/db/entity/Note.kt | 149 ++ .../edziennik/data/db/entity/Noteable.kt | 34 + .../edziennik/data/db/entity/Notification.kt | 2 + .../edziennik/data/db/entity/Profile.kt | 3 + .../data/db/full/AnnouncementFull.kt | 11 +- .../edziennik/data/db/full/AttendanceFull.kt | 11 +- .../edziennik/data/db/full/EventFull.kt | 11 +- .../edziennik/data/db/full/GradeFull.kt | 11 +- .../edziennik/data/db/full/LessonFull.kt | 11 +- .../edziennik/data/db/full/MessageFull.kt | 10 +- .../edziennik/data/db/full/NoticeFull.kt | 11 +- .../data/db/migration/Migration97.kt | 29 + .../data/firebase/SzkolnyAppFirebase.kt | 80 +- .../edziennik/ext/DataExtensions.kt | 1 + .../edziennik/ext/DialogExtensions.kt | 56 +- .../edziennik/ext/GraphicsExtensions.kt | 11 +- .../edziennik/ui/agenda/DayDialog.kt | 19 +- .../lessonchanges/LessonChangesAdapter.kt | 16 +- .../lessonchanges/LessonChangesDialog.kt | 2 +- .../announcements/AnnouncementsAdapter.java | 15 +- .../announcements/AnnouncementsFragment.java | 4 + .../ui/attendance/AttendanceAdapter.kt | 6 +- .../ui/attendance/AttendanceDetailsDialog.kt | 14 + .../ui/attendance/AttendanceListFragment.kt | 4 + .../attendance/AttendanceSummaryFragment.kt | 4 + .../viewholder/AttendanceViewHolder.kt | 6 +- .../edziennik/ui/base/CrashActivity.kt | 3 +- .../ui/dialogs/settings/AgendaConfigDialog.kt | 5 +- .../settings/RegistrationConfigDialog.kt | 17 +- .../edziennik/ui/event/EventDetailsDialog.kt | 14 +- .../edziennik/ui/event/EventListAdapter.kt | 4 +- .../edziennik/ui/event/EventManualDialog.kt | 27 +- .../edziennik/ui/event/EventViewHolder.kt | 40 +- .../edziennik/ui/grades/GradeDetailsDialog.kt | 42 +- .../edziennik/ui/grades/GradesAdapter.kt | 6 +- .../edziennik/ui/grades/GradesListFragment.kt | 4 + .../ui/grades/viewholder/GradeViewHolder.kt | 10 +- .../edziennik/ui/home/HomeCard.kt | 1 + .../edziennik/ui/home/HomeConfigDialog.kt | 2 + .../edziennik/ui/home/HomeFragment.kt | 4 +- .../edziennik/ui/home/cards/HomeEventsCard.kt | 6 +- .../edziennik/ui/home/cards/HomeNotesCard.kt | 122 + .../ui/homework/HomeworkListFragment.kt | 6 +- .../ui/messages/list/MessageViewHolder.kt | 6 +- .../ui/messages/list/MessagesAdapter.kt | 3 +- .../ui/messages/list/MessagesListFragment.kt | 4 +- .../ui/messages/single/MessageFragment.kt | 8 + .../ui/notes/NoteCategoryViewHolder.kt | 52 + .../edziennik/ui/notes/NoteDetailsDialog.kt | 85 + .../edziennik/ui/notes/NoteEditorDialog.kt | 197 ++ .../edziennik/ui/notes/NoteListAdapter.kt | 52 + .../edziennik/ui/notes/NoteListDialog.kt | 104 + .../edziennik/ui/notes/NoteViewHolder.kt | 84 + .../edziennik/ui/notes/NotesExtensions.kt | 47 + .../edziennik/ui/notes/NotesFragment.kt | 156 ++ .../edziennik/ui/search/SearchFilter.kt | 7 +- .../ui/settings/cards/SettingsRegisterCard.kt | 62 +- .../ui/timetable/LessonDetailsDialog.kt | 19 +- .../ui/timetable/TimetableDayFragment.kt | 11 +- .../edziennik/utils/managers/EventManager.kt | 11 +- .../edziennik/utils/managers/NoteManager.kt | 307 +++ .../utils/managers/TextStylingManager.kt | 30 + .../edziennik/utils/models/Date.java | 53 +- app/src/main/res/drawable/ic_announcement.xml | 21 + app/src/main/res/drawable/ic_attendance.xml | 33 + app/src/main/res/drawable/ic_behavior.xml | 18 + app/src/main/res/drawable/ic_calendar_day.xml | 15 + .../main/res/drawable/ic_calendar_event.xml | 27 + app/src/main/res/drawable/ic_grade.xml | 12 + app/src/main/res/drawable/ic_message.xml | 12 + app/src/main/res/drawable/ic_note.xml | 18 + app/src/main/res/drawable/ic_timetable.xml | 36 +- .../res/layout/attendance_details_dialog.xml | 17 + .../res/layout/attendance_item_attendance.xml | 2 +- app/src/main/res/layout/card_home_notes.xml | 45 + app/src/main/res/layout/dialog_day.xml | 24 +- .../main/res/layout/dialog_event_details.xml | 14 +- .../main/res/layout/dialog_grade_details.xml | 17 + .../main/res/layout/dialog_lesson_details.xml | 18 +- app/src/main/res/layout/grades_item_grade.xml | 2 +- app/src/main/res/layout/message_fragment.xml | 10 +- .../main/res/layout/messages_list_item.xml | 2 +- .../main/res/layout/note_details_dialog.xml | 143 + .../main/res/layout/note_dialog_header.xml | 42 + .../main/res/layout/note_dialog_subtitle.xml | 42 + .../main/res/layout/note_editor_dialog.xml | 138 + .../res/layout/note_list_category_item.xml | 21 + app/src/main/res/layout/note_list_dialog.xml | 76 + app/src/main/res/layout/note_list_item.xml | 60 + app/src/main/res/layout/notes_fragment.xml | 45 + app/src/main/res/layout/timetable_lesson.xml | 2 +- app/src/main/res/values/strings.xml | 49 + 109 files changed, 5504 insertions(+), 188 deletions(-) create mode 100644 app/schemas/pl.szczodrzynski.edziennik.data.db.AppDb/97.json create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/api/szkolny/request/NoteShareRequest.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/db/dao/NoteDao.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Note.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Noteable.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/data/db/migration/Migration97.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeNotesCard.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteCategoryViewHolder.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteDetailsDialog.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteEditorDialog.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteListAdapter.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteListDialog.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteViewHolder.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NotesExtensions.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NotesFragment.kt create mode 100644 app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/NoteManager.kt create mode 100644 app/src/main/res/drawable/ic_announcement.xml create mode 100644 app/src/main/res/drawable/ic_attendance.xml create mode 100644 app/src/main/res/drawable/ic_behavior.xml create mode 100644 app/src/main/res/drawable/ic_calendar_day.xml create mode 100644 app/src/main/res/drawable/ic_calendar_event.xml create mode 100644 app/src/main/res/drawable/ic_grade.xml create mode 100644 app/src/main/res/drawable/ic_message.xml create mode 100644 app/src/main/res/drawable/ic_note.xml create mode 100644 app/src/main/res/layout/card_home_notes.xml create mode 100644 app/src/main/res/layout/note_details_dialog.xml create mode 100644 app/src/main/res/layout/note_dialog_header.xml create mode 100644 app/src/main/res/layout/note_dialog_subtitle.xml create mode 100644 app/src/main/res/layout/note_editor_dialog.xml create mode 100644 app/src/main/res/layout/note_list_category_item.xml create mode 100644 app/src/main/res/layout/note_list_dialog.xml create mode 100644 app/src/main/res/layout/note_list_item.xml create mode 100644 app/src/main/res/layout/notes_fragment.xml diff --git a/app/build.gradle b/app/build.gradle index 6b0569e7..66dec0c2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -31,6 +31,12 @@ android { cppFlags "-std=c++11" } } + + kapt { + arguments { + arg("room.schemaLocation", "$projectDir/schemas") + } + } } buildTypes { 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/src/main/java/pl/szczodrzynski/edziennik/App.kt b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt index 77a2af31..78f27450 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/App.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/App.kt @@ -35,6 +35,7 @@ import okhttp3.OkHttpClient import org.greenrobot.eventbus.EventBus 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 @@ -66,6 +67,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { var devMode = false } + val api by lazy { SzkolnyApi(this) } val notificationChannelsManager by lazy { NotificationChannelsManager(this) } val userActionManager by lazy { UserActionManager(this) } val gradesManager by lazy { GradesManager(this) } @@ -77,6 +79,7 @@ class App : MultiDexApplication(), Configuration.Provider, CoroutineScope { val availabilityManager by lazy { AvailabilityManager(this) } val textStylingManager by lazy { TextStylingManager(this) } val messageManager by lazy { MessageManager(this) } + val noteManager by lazy { NoteManager(this) } val db get() = App.db 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/MainActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt index 0f934603..7197ea10 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/MainActivity.kt @@ -72,6 +72,7 @@ import pl.szczodrzynski.edziennik.ui.login.LoginActivity 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 @@ -118,6 +119,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope { const val DRAWER_ITEM_NOTIFICATIONS = 20 const val DRAWER_ITEM_MORE = 21 const val DRAWER_ITEM_TEACHERS = 22 + const val DRAWER_ITEM_NOTES = 23 const val DRAWER_ITEM_SETTINGS = 101 const val DRAWER_ITEM_DEBUG = 102 @@ -134,6 +136,13 @@ class MainActivity : AppCompatActivity(), CoroutineScope { val list: MutableList = mutableListOf() val moreList: MutableList = mutableListOf() + moreList += NavTarget( + id = DRAWER_ITEM_NOTES, + name = R.string.menu_notes, + fragmentClass = NotesFragment::class) + .withIcon(CommunityMaterial.Icon3.cmd_text_box_multiple_outline) + .isStatic(true) + moreList += NavTarget(DRAWER_ITEM_TEACHERS, R.string.menu_teachers, TeachersListFragment::class) 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..6482634c 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/config/ProfileConfig.kt @@ -18,7 +18,7 @@ import kotlin.coroutines.CoroutineContext class ProfileConfig(val db: AppDb, val profileId: Int, rawEntries: List) : CoroutineScope, AbstractConfig { companion object { - const val DATA_VERSION = 2 + const val DATA_VERSION = 3 } private val job = Job() 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..62122a37 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 @@ -7,6 +7,8 @@ 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.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 @@ -33,5 +35,13 @@ class ProfileConfigMigration(config: ProfileConfig) { dataVersion = 2 } + + if (dataVersion < 3) { + ui.homeCards = ui.homeCards.toMutableList().also { + it.add(HomeCardModel(config.profileId, HomeCard.CARD_NOTES)) + } + + dataVersion = 3 + } }} } 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 b5d34604..c86ae235 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 @@ -23,10 +23,7 @@ import pl.szczodrzynski.edziennik.data.api.szkolny.interceptor.SignatureIntercep 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.* -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.db.entity.* import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.ext.keys import pl.szczodrzynski.edziennik.ext.md5 @@ -199,7 +196,12 @@ class SzkolnyApi(val app: App) : CoroutineScope { } @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 -> @@ -225,7 +227,7 @@ class SzkolnyApi(val app: App) : CoroutineScope { lastSync = lastSyncTime, notifications = notifications.map { ServerSyncRequest.Notification(it.profileName ?: "", it.type, it.text) } )).execute() - val (events, hasBrowsers) = parseResponse(response, updateDeviceHash = true) + val (events, notes, hasBrowsers) = parseResponse(response, updateDeviceHash = true) hasBrowsers?.let { app.config.sync.webPushEnabled = it @@ -237,6 +239,7 @@ class SzkolnyApi(val app: App) : CoroutineScope { } val eventList = mutableListOf() + val noteList = mutableListOf() events.forEach { event -> // skip blacklisted events @@ -247,9 +250,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 @@ -261,42 +268,107 @@ class SzkolnyApi(val app: App) : CoroutineScope { if (profile.userCode == event.sharedBy) { sharedBy = "self" addedManually = true + } else { + sharedBy = 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(), - sharedByName = event.sharedByName, - unshareTeamCode = team.code, - eventId = event.id + 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) { + 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.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 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.shareNote(NoteShareRequest( + deviceId = app.deviceId, + device = getDevice(), + userCode = profile.userCode, + studentNameLong = profile.studentNameLong, + unshareTeamCode = team.code, + noteId = note.id, + )).execute() + parseResponse(response) + } + /*fun eventEditRequest(requesterName: String, event: Event): ApiResponse? { }*/ 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 0e86a7bd..5c60c3ff 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 @@ -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> 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/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/task/AppSync.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/AppSync.kt index f0c2f72e..0b13c867 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 @@ -30,7 +30,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,6 +40,10 @@ 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 -> @@ -54,6 +58,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/SzkolnyTask.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/api/task/SzkolnyTask.kt index 7b4931bc..39dd7e15 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 @@ -31,7 +31,7 @@ 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 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 61a7420b..93758366 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,8 +42,9 @@ import pl.szczodrzynski.edziennik.data.db.migration.* ConfigEntry::class, LibrusLesson::class, TimetableManual::class, + Note::class, Metadata::class -], version = 96) +], version = 97) @TypeConverters( ConverterTime::class, ConverterDate::class, @@ -82,6 +83,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 { @@ -182,6 +184,7 @@ abstract class AppDb : RoomDatabase() { Migration94(), Migration95(), Migration96(), + Migration97(), ).allowMainThreadQueries().build() } } 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/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/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..052596b3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/data/db/entity/Noteable.kt @@ -0,0 +1,34 @@ +/* + * 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 + + 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 0a049216..e57e8efa 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 @@ -56,6 +56,7 @@ data class Notification( const val TYPE_FEEDBACK_MESSAGE = 16 const val TYPE_AUTO_ARCHIVING = 17 const val TYPE_TEACHER_ABSENCE = 19 + const val TYPE_NEW_SHARED_NOTE = 20 fun buildId(profileId: Int, type: Int, itemId: Long): Long { return 1000000000000 + profileId*10000000000 + type*100000000 + itemId; @@ -112,6 +113,7 @@ data class Notification( TYPE_NEW_ATTENDANCE -> CommunityMaterial.Icon.cmd_calendar_remove_outline TYPE_LUCKY_NUMBER -> CommunityMaterial.Icon.cmd_emoticon_excited_outline TYPE_NEW_ANNOUNCEMENT -> CommunityMaterial.Icon.cmd_bullhorn_outline + TYPE_NEW_SHARED_NOTE -> CommunityMaterial.Icon3.cmd_playlist_edit else -> CommunityMaterial.Icon.cmd_bell_ring_outline } } 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 d34eda1e..9fc0521a 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 @@ -145,6 +145,9 @@ open class Profile( else -> "unknown" } + 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 { 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 bfa6954b..3eb681f2 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 @@ -4,8 +4,11 @@ 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.ui.search.Searchable import pl.szczodrzynski.edziennik.utils.html.BetterHtml import pl.szczodrzynski.edziennik.utils.models.Date @@ -19,7 +22,7 @@ class EventFull( profileId, id, date, time, topic, color, type, teacherId, subjectId, teamId, addedDate -), Searchable { +), Searchable, Noteable { constructor(event: Event, metadata: Metadata? = null) : this( event.profileId, event.id, event.date, event.time, event.topic, event.color, event.type, @@ -109,4 +112,10 @@ class EventFull( 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 } 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..a2982a30 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,18 @@ 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.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 +136,10 @@ class LessonFull( // metadata var seen: Boolean = false var notified: Boolean = false + + @Relation(parentColumn = "id", entityColumn = "noteOwnerId", entity = Note::class) + override lateinit var notes: MutableList + override fun getNoteType() = Note.OwnerType.LESSON + override fun getNoteOwnerProfileId() = profileId + override fun getNoteOwnerId() = id } 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 c3bf8543..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,8 @@ 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 @@ -18,7 +20,7 @@ class MessageFull( profileId, id, type, subject, body, senderId, addedDate -), Searchable { +), Searchable, Noteable { var senderName: String? = null @Relation(parentColumn = "messageId", entityColumn = "messageId", entity = MessageRecipient::class) var recipients: MutableList? = null @@ -83,4 +85,10 @@ class MessageFull( // 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/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/firebase/SzkolnyAppFirebase.kt b/app/src/main/java/pl/szczodrzynski/edziennik/data/firebase/SzkolnyAppFirebase.kt index da822462..c1a6d781 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 @@ -47,6 +47,15 @@ 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") ?: "", @@ -119,7 +128,7 @@ 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, @@ -186,7 +195,7 @@ 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 @@ -209,4 +218,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 = Notification.TYPE_NEW_SHARED_NOTE + val notificationFilter = app.config.getFor(note.profileId).sync.notificationFilter + + if (!notificationFilter.contains(type) && note.sharedBy != "self") { + val notification = Notification( + id = Notification.buildId(note.profileId, type, note.id), + title = app.getNotificationTitle(type), + text = message, + type = type, + profileId = profile.id, + profileName = profile.name, + viewId = MainActivity.DRAWER_ITEM_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/ext/DataExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/DataExtensions.kt index 663a58c4..b1e99c43 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/DataExtensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/DataExtensions.kt @@ -63,6 +63,7 @@ fun Context.getNotificationTitle(type: Int): String { 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 + Notification.TYPE_NEW_SHARED_NOTE -> R.string.notification_type_new_shared_note else -> R.string.notification_type_general }) } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ext/DialogExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/DialogExtensions.kt index a918e60b..61de4f77 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/DialogExtensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/DialogExtensions.kt @@ -4,13 +4,22 @@ 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( @@ -20,7 +29,7 @@ fun MaterialAlertDialogBuilder.input( value: CharSequence? = null, changeListener: ((editText: TextInputEditText, input: String) -> Boolean)? = null, positiveButton: Int? = null, - positiveListener: ((editText: TextInputEditText, input: String) -> Boolean)? = null + positiveListener: ((editText: TextInputEditText, input: String) -> Boolean)? = null, ): MaterialAlertDialogBuilder { val b = DialogEditTextBinding.inflate(LayoutInflater.from(context), null, false) b.title.text = message @@ -43,12 +52,53 @@ fun MaterialAlertDialogBuilder.input( return this } -fun MaterialAlertDialogBuilder.setTitle(@StringRes resId: Int, vararg formatArgs: Any): MaterialAlertDialogBuilder { +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 { +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/GraphicsExtensions.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ext/GraphicsExtensions.kt index 43000d20..69cbfc33 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ext/GraphicsExtensions.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ext/GraphicsExtensions.kt @@ -10,10 +10,7 @@ import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter import android.graphics.drawable.Drawable import android.util.TypedValue -import androidx.annotation.AttrRes -import androidx.annotation.ColorInt -import androidx.annotation.ColorRes -import androidx.annotation.DrawableRes +import androidx.annotation.* import androidx.core.content.res.ResourcesCompat import com.mikepenz.iconics.typeface.IIcon import pl.szczodrzynski.navlib.ImageHolder @@ -60,6 +57,12 @@ fun @receiver:AttrRes Int.resolveAttr(context: Context?): Int { 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) 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 index e888c82e..9da4beda 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/DayDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/DayDialog.kt @@ -26,7 +26,9 @@ 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 @@ -36,6 +38,7 @@ class DayDialog( private val profileId: Int, private val date: Date, private val eventTypeId: Long? = null, + private val showNotes: Boolean = true, onShowListener: ((tag: String) -> Unit)? = null, onDismissListener: ((tag: String) -> Unit)? = null, ) : BindingDialog(activity, onShowListener, onDismissListener) { @@ -151,7 +154,7 @@ class DayDialog( showTime = true, showSubject = true, markAsSeen = true, - onItemClick = { + onEventClick = { EventDetailsDialog( activity, it, @@ -171,6 +174,10 @@ class DayDialog( ) app.db.eventDao().getAllByDate(profileId, date).observe(activity) { events -> + events.forEach { + it.filterNotes() + } + adapter.setAllItems( if (eventTypeId != null) events.filter { it.type == eventTypeId } @@ -197,5 +204,15 @@ class DayDialog( b.eventsNoData.visibility = View.VISIBLE } } + + b.notesButton.isVisible = showNotes + b.notesButton.setupNotesButton( + activity = activity, + owner = date, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ) + if (showNotes) + NoteManager.setLegendText(date, b.legend) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/lessonchanges/LessonChangesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/lessonchanges/LessonChangesAdapter.kt index 4712e073..cf6b0cae 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/lessonchanges/LessonChangesAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/agenda/lessonchanges/LessonChangesAdapter.kt @@ -16,11 +16,13 @@ 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 LessonChangesAdapter( val context: Context, - private val onItemClick: ((lesson: LessonFull) -> Unit)? = null + private val showNotes: Boolean = true, + private val onLessonClick: ((lesson: LessonFull) -> Unit)? = null ) : RecyclerView.Adapter() { var items = listOf() @@ -39,8 +41,10 @@ class LessonChangesAdapter( 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 @@ -84,7 +88,8 @@ class LessonChangesAdapter( 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 @@ -93,6 +98,9 @@ class LessonChangesAdapter( 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 index 9e755046..50e972a6 100644 --- 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 @@ -35,7 +35,7 @@ class LessonChangesDialog( val adapter = LessonChangesAdapter( activity, - onItemClick = { + onLessonClick = { LessonDetailsDialog( activity, it, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/announcements/AnnouncementsAdapter.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/announcements/AnnouncementsAdapter.java index 73156558..d50d6670 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/announcements/AnnouncementsAdapter.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/announcements/AnnouncementsAdapter.java @@ -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; @@ -24,13 +25,14 @@ public class AnnouncementsAdapter extends RecyclerView.Adapter 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/announcements/AnnouncementsFragment.java b/app/src/main/java/pl/szczodrzynski/edziennik/ui/announcements/AnnouncementsFragment.java index c9c1032b..3eadad95 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/announcements/AnnouncementsFragment.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/announcements/AnnouncementsFragment.java @@ -107,6 +107,10 @@ public class AnnouncementsFragment extends Fragment { 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); diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceAdapter.kt index 6daaebcd..00a1b361 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceAdapter.kt @@ -28,6 +28,7 @@ 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/attendance/AttendanceDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceDetailsDialog.kt index 4d33687d..3ebb9a9d 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceDetailsDialog.kt @@ -7,17 +7,21 @@ 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) { @@ -48,5 +52,15 @@ class AttendanceDetailsDialog( onActionSelected = dialog::dismiss ) } + + b.notesButton.isVisible = showNotes + b.notesButton.setupNotesButton( + activity = activity, + owner = attendance, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ) + if (showNotes) + NoteManager.setLegendText(attendance, b.legend) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceListFragment.kt index 6b056103..acc19715 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceListFragment.kt @@ -66,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) { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceSummaryFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceSummaryFragment.kt index c8f0c226..828e5a95 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceSummaryFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/AttendanceSummaryFragment.kt @@ -75,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() } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/AttendanceViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/AttendanceViewHolder.kt index e030b288..ab6a0b92 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/AttendanceViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/attendance/viewholder/AttendanceViewHolder.kt @@ -19,6 +19,7 @@ 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( @@ -38,7 +39,10 @@ class AttendanceViewHolder( 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/base/CrashActivity.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/CrashActivity.kt index 5a209020..398134b3 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/CrashActivity.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/base/CrashActivity.kt @@ -19,7 +19,6 @@ 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 @@ -116,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.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/dialogs/settings/AgendaConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/AgendaConfigDialog.kt index 3042a9ef..05012c32 100644 --- 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 @@ -9,7 +9,6 @@ import androidx.appcompat.app.AppCompatActivity import com.google.android.material.dialog.MaterialAlertDialogBuilder import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.data.db.entity.Profile -import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.REGISTRATION_ENABLED import pl.szczodrzynski.edziennik.databinding.DialogConfigAgendaBinding import pl.szczodrzynski.edziennik.ext.onChange import pl.szczodrzynski.edziennik.ui.dialogs.base.ConfigDialog @@ -40,9 +39,9 @@ class AgendaConfigDialog( b.isAgendaMode = profileConfig.agendaViewType == Profile.AGENDA_DEFAULT b.eventSharingEnabled.isChecked = - app.profile.enableSharedEvents && app.profile.registration == REGISTRATION_ENABLED + app.profile.enableSharedEvents && app.profile.canShare b.eventSharingEnabled.onChange { _, isChecked -> - if (isChecked && app.profile.registration != REGISTRATION_ENABLED) { + if (isChecked && !app.profile.canShare) { b.eventSharingEnabled.isChecked = false val dialog = RegistrationConfigDialog( activity, diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/RegistrationConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/RegistrationConfigDialog.kt index f5b45320..51b2a0e1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/RegistrationConfigDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/dialogs/settings/RegistrationConfigDialog.kt @@ -19,7 +19,7 @@ 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,6 +57,21 @@ 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) diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventDetailsDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventDetailsDialog.kt index 302685c4..6c906c27 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventDetailsDialog.kt @@ -27,6 +27,7 @@ import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.databinding.DialogEventDetailsBinding import pl.szczodrzynski.edziennik.ext.* 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 @@ -36,6 +37,7 @@ class 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) { @@ -96,6 +98,8 @@ class EventDetailsDialog( manager.markAsSeen(event) } + event.filterNotes() + val bullet = " • " val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity) @@ -104,7 +108,7 @@ class EventDetailsDialog( } catch (_: Exception) {} - manager.setLegendText(b.legend, event) + manager.setLegendText(b.legend, event, showNotes) b.typeColor.background?.setTintColor(event.eventColor) @@ -257,6 +261,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) 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 index a7df3c95..f991eeef 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventListAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventListAdapter.kt @@ -22,12 +22,14 @@ class EventListAdapter( val simpleMode: Boolean = false, val showWeekDay: Boolean = false, val showDate: Boolean = false, + val showColor: Boolean = true, val showType: Boolean = true, val showTime: Boolean = true, val showSubject: Boolean = true, val markAsSeen: Boolean = true, + val showNotes: Boolean = true, isReversed: Boolean = false, - val onItemClick: ((event: EventFull) -> Unit)? = null, + val onEventClick: ((event: EventFull) -> Unit)? = null, val onEventEditClick: ((event: EventFull) -> Unit)? = null, ) : SearchableAdapter(isReversed), CoroutineScope { companion object { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventManualDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventManualDialog.kt index 6410b255..70526899 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventManualDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventManualDialog.kt @@ -13,9 +13,6 @@ 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 com.mikepenz.iconics.IconicsDrawable -import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial -import com.mikepenz.iconics.utils.sizeDp import kotlinx.coroutines.* import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe @@ -31,7 +28,6 @@ import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.LessonFull import pl.szczodrzynski.edziennik.databinding.DialogEventManualV2Binding import pl.szczodrzynski.edziennik.ext.* -import pl.szczodrzynski.edziennik.ui.dialogs.StyledTextDialog 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 @@ -123,20 +119,13 @@ class EventManualDialog( } } - b.topicLayout.endIconDrawable = IconicsDrawable(activity, CommunityMaterial.Icon3.cmd_open_in_new).apply { - sizeDp = 24 - } - b.topicLayout.setEndIconOnClickListener { - StyledTextDialog( - activity, - initialText = b.topic.text, - onSuccess = { - b.topic.text = it - }, - onShowListener, - onDismissListener - ).show() - } + textStylingManager.attachToField( + activity = activity, + textLayout = b.topicLayout, + textEdit = b.topic, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ) stylingConfig = StylingConfigBase(editText = b.topic, htmlMode = SIMPLE) @@ -414,7 +403,7 @@ class EventManualDialog( val share = b.shareSwitch.isChecked - if (share && profile.registration != Profile.REGISTRATION_ENABLED) { + if (share && !profile.canShare) { RegistrationConfigDialog(activity, profile, onChangeListener = { enabled -> if (enabled) saveEvent() 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 index 8170f335..bee5e305 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/event/EventViewHolder.kt @@ -36,14 +36,16 @@ class EventViewHolder( ) { val manager = app.eventManager - b.root.onClick { - adapter.onItemClick?.invoke(item) - if (!item.seen) { - manager.markAsSeen(item) - } - if (item.showAsUnseen == true) { - item.showAsUnseen = false - adapter.notifyItemChanged(item) + 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) + } } } @@ -52,7 +54,7 @@ class EventViewHolder( b.simpleMode = adapter.simpleMode - manager.setEventTopic(b.topic, item, showType = false) + manager.setEventTopic(b.topic, item, showType = false, showNotes = adapter.showNotes) b.topic.text = adapter.highlightSearchText( item = item, text = b.topic.text, @@ -67,13 +69,13 @@ class EventViewHolder( if (adapter.showDate) item.date.getRelativeString(activity, 7) ?: item.date.formattedStringShort else null, - if (adapter.showType) + 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) + if (adapter.showSubject && item.subjectLongName != null) adapter.highlightSearchText( item = item, text = item.subjectLongName ?: "", @@ -111,13 +113,19 @@ class EventViewHolder( b.attachmentIcon.isVisible = item.hasAttachments b.typeColor.background?.setTintColor(item.eventColor) - b.typeColor.isVisible = adapter.showType + b.typeColor.isVisible = adapter.showType && adapter.showColor - b.editButton.isVisible = !adapter.simpleMode && item.addedManually && !item.isDone - b.editButton.onClick { - adapter.onEventEditClick?.invoke(item) + 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) } - b.editButton.attachToastHint(R.string.hint_edit_event) if (item.showAsUnseen == null) item.showAsUnseen = !item.seen 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 index 80d0dfef..0c19319a 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradeDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradeDetailsDialog.kt @@ -15,12 +15,15 @@ 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) { @@ -71,23 +74,34 @@ class GradeDetailsDialog( val historyList = withContext(Dispatchers.Default) { app.db.gradeDao().getByParentIdNow(App.profileId, grade.id) } - if (historyList.isEmpty()) { - b.historyVisible = false - return - } - b.historyVisible = true - //b.gradeHistoryNest.isNestedScrollingEnabled = false - b.gradeHistoryList.adapter = GradesAdapter(activity, { - GradeDetailsDialog(activity, it).show() - }).also { - it.items = historyList.toMutableList() + historyList.forEach { + it.filterNotes() } - b.gradeHistoryList.apply { - setHasFixedSize(true) - layoutManager = LinearLayoutManager(context) - addItemDecoration(SimpleDividerItemDecoration(context)) + 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, + ) + if (showNotes) + NoteManager.setLegendText(grade, b.legend) } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesAdapter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesAdapter.kt index ae8dca9c..6d661c6f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesAdapter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesAdapter.kt @@ -25,6 +25,7 @@ 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 { @@ -218,7 +219,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/grades/GradesListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesListFragment.kt index b7ad447d..c6cf9474 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/GradesListFragment.kt @@ -76,6 +76,10 @@ 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 } else -> grades diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/GradeViewHolder.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/GradeViewHolder.kt index b8cab8b7..85684886 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/GradeViewHolder.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/grades/viewholder/GradeViewHolder.kt @@ -15,6 +15,7 @@ import pl.szczodrzynski.edziennik.data.db.full.GradeFull import pl.szczodrzynski.edziennik.databinding.GradesItemGradeBinding 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/home/HomeCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeCard.kt index af59bbb6..59bb24f1 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeCard.kt @@ -24,6 +24,7 @@ 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 diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeConfigDialog.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeConfigDialog.kt index 443eac32..73367c99 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeConfigDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeConfigDialog.kt @@ -12,6 +12,7 @@ import pl.szczodrzynski.edziennik.ui.dialogs.base.BaseDialog import pl.szczodrzynski.edziennik.ui.home.HomeCard.Companion.CARD_EVENTS import pl.szczodrzynski.edziennik.ui.home.HomeCard.Companion.CARD_GRADES import pl.szczodrzynski.edziennik.ui.home.HomeCard.Companion.CARD_LUCKY_NUMBER +import pl.szczodrzynski.edziennik.ui.home.HomeCard.Companion.CARD_NOTES import pl.szczodrzynski.edziennik.ui.home.HomeCard.Companion.CARD_TIMETABLE class HomeConfigDialog( @@ -32,6 +33,7 @@ class HomeConfigDialog( 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() = diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeFragment.kt index d720e31f..78ac63e7 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/HomeFragment.kt @@ -144,7 +144,8 @@ class HomeFragment : Fragment(), CoroutineScope { 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) + HomeCardModel(app.profile.id, HomeCard.CARD_GRADES), + HomeCardModel(app.profile.id, HomeCard.CARD_NOTES), ) app.config.forProfile().ui.homeCards = app.config.forProfile().ui.homeCards.toMutableList().also { it.addAll(cards) } } @@ -157,6 +158,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? } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeEventsCard.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeEventsCard.kt index 8c20e967..96534719 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeEventsCard.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeEventsCard.kt @@ -66,7 +66,7 @@ class HomeEventsCard( showTime = false, showSubject = false, markAsSeen = false, - onItemClick = { + onEventClick = { EventDetailsDialog( activity, it @@ -82,6 +82,10 @@ class HomeEventsCard( ) app.db.eventDao().getNearestNotDone(profile.id, Date.getToday(), 4).observe(activity, Observer { events -> + events.forEach { + it.filterNotes() + } + adapter.setAllItems(events) if (b.eventsView.adapter == null) { b.eventsView.adapter = adapter 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..c1884f00 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/home/cards/HomeNotesCard.kt @@ -0,0 +1,122 @@ +/* + * 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.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.loadTarget(MainActivity.DRAWER_ITEM_AGENDA) + } + }} + + override fun unbind(position: Int, holder: HomeCardAdapter.ViewHolder) = Unit +} diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/homework/HomeworkListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/homework/HomeworkListFragment.kt index f691ec4e..4c354801 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/homework/HomeworkListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/homework/HomeworkListFragment.kt @@ -64,7 +64,7 @@ class HomeworkListFragment : LazyFragment(), CoroutineScope { showSubject = true, markAsSeen = true, isReversed = homeworkDate == HomeworkDate.PAST, - onItemClick = { + onEventClick = { EventDetailsDialog( activity, it @@ -82,6 +82,10 @@ class HomeworkListFragment : LazyFragment(), CoroutineScope { app.db.eventDao().getAllByType(App.profileId, Event.TYPE_HOMEWORK, filter).observe(this@HomeworkListFragment, Observer { events -> if (!isAdded) return@Observer + events.forEach { + it.filterNotes() + } + // show/hide relevant views setSwipeToRefresh(events.isEmpty()) b.progressBar.isVisible = false 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 index 904eb62b..9e7cf3c4 100644 --- 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 @@ -19,6 +19,7 @@ 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( @@ -73,7 +74,10 @@ class MessageViewHolder( color = colorHighlight ) - adapter.onItemClick?.let { listener -> + if (adapter.showNotes) + NoteManager.prependIcon(item, b.messageSubject) + + adapter.onMessageClick?.let { listener -> b.root.onClick { listener(item) } } adapter.onStarClick?.let { listener -> 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 index 05bcb5a9..3273340d 100644 --- 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 @@ -13,7 +13,8 @@ import pl.szczodrzynski.edziennik.ui.search.SearchableAdapter class MessagesAdapter( val activity: AppCompatActivity, val teachers: List, - val onItemClick: ((item: MessageFull) -> Unit)? = null, + val showNotes: Boolean = true, + val onMessageClick: ((item: MessageFull) -> Unit)? = null, val onStarClick: ((item: MessageFull) -> Unit)? = null, ) : SearchableAdapter() { companion object { diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/list/MessagesListFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/list/MessagesListFragment.kt index b15f58c9..fb6edc56 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/list/MessagesListFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/list/MessagesListFragment.kt @@ -62,7 +62,7 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { app.db.teacherDao().getAllNow(App.profileId) } - adapter = MessagesAdapter(activity, teachers, onItemClick = { + adapter = MessagesAdapter(activity, teachers, onMessageClick = { val (target, args) = if (it.isDraft) { TARGET_MESSAGES_COMPOSE to Bundle("message" to app.gson.toJson(it)) @@ -81,6 +81,8 @@ class MessagesListFragment : LazyFragment(), CoroutineScope { return@Observer messages.forEach { message -> + message.filterNotes() + // uh oh, so these are the workarounds ?? message.recipients?.removeAll { it.profileId != message.profileId } message.recipients?.forEach { recipient -> diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/single/MessageFragment.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/single/MessageFragment.kt index 5cd27edc..27c90f4f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/single/MessageFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/messages/single/MessageFragment.kt @@ -30,6 +30,7 @@ import pl.szczodrzynski.edziennik.ext.* 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 @@ -266,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() { 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..ea77e2d3 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/notes/NoteEditorDialog.kt @@ -0,0 +1,197 @@ +/* + * 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, note, 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) + + b.ownerType = owner?.getNoteType() ?: Note.OwnerType.NONE + b.editingNote = editingNote + + 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/search/SearchFilter.kt b/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/SearchFilter.kt index a14a01a6..3a76400e 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/SearchFilter.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/search/SearchFilter.kt @@ -59,14 +59,15 @@ class SearchFilter>( } val newItems = allItems.mapNotNull { item -> - if (item is SearchField) { + // 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 - // get all keyword sets from the entity - val searchKeywords = item.searchKeywords // a temporary variable for the loops below var matchWeight: Int 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 index d274e748..3b6b51f9 100644 --- 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 @@ -10,7 +10,6 @@ import eu.szkolny.font.SzkolnyFont import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_LIBRUS -import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.REGISTRATION_ENABLED import pl.szczodrzynski.edziennik.ext.after import pl.szczodrzynski.edziennik.ui.dialogs.settings.* import pl.szczodrzynski.edziennik.ui.settings.SettingsCard @@ -83,36 +82,39 @@ class SettingsRegisterCard(util: SettingsUtil) : SettingsCard(util) { AttendanceConfigDialog(activity, reloadOnDismiss = false).show() }, - 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.archived) + null + else + 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, sharedEventsItem) + } else { + card.items.remove(sharedEventsItem) + } + util.refresh() + }) + if (value) + dialog.showEnableDialog() + else + dialog.showDisableDialog() + false + } + ) { _, _ -> }, - if (app.profile.registration == REGISTRATION_ENABLED) + if (app.profile.canShare) sharedEventsItem else null 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 index a2e2906b..e2dc1e21 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/LessonDetailsDialog.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/LessonDetailsDialog.kt @@ -26,8 +26,10 @@ 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 @@ -35,6 +37,7 @@ 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) { @@ -186,7 +189,7 @@ class LessonDetailsDialog( showTime = true, showSubject = true, markAsSeen = true, - onItemClick = { + onEventClick = { EventDetailsDialog( activity, it, @@ -210,6 +213,10 @@ class LessonDetailsDialog( lessonDate, lessonTime ).observe(activity) { events -> + events.forEach { + it.filterNotes() + } + adapter.setAllItems(events) if (b.eventsView.adapter == null) { b.eventsView.adapter = adapter @@ -244,5 +251,15 @@ class LessonDetailsDialog( onActionSelected = dialog::dismiss ) } + + b.notesButton.isVisible = showNotes + b.notesButton.setupNotesButton( + activity = activity, + owner = lesson, + onShowListener = onShowListener, + onDismissListener = onDismissListener, + ) + 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 index fcc2ad26..8ea7909f 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/ui/timetable/TimetableDayFragment.kt @@ -33,6 +33,7 @@ import pl.szczodrzynski.edziennik.ext.* 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.managers.NoteManager import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Time import java.util.* @@ -103,6 +104,10 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { // 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) } @@ -281,7 +286,9 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { lb.lessonNumber = lesson.displayLessonNumber - lb.subjectName.text = lesson.displaySubjectName?.let { + val lessonText = + lesson.getNoteSubstituteText(showNotes = true) ?: lesson.displaySubjectName + lb.subjectName.text = lessonText?.let { if (lesson.type == Lesson.TYPE_CANCELLED || lesson.type == Lesson.TYPE_SHIFTED_SOURCE) it.asStrikethroughSpannable().asColoredSpannable(colorSecondary) else @@ -290,6 +297,8 @@ class TimetableDayFragment : LazyFragment(), CoroutineScope { 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) 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 98c88835..62ca88a1 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 @@ -48,12 +48,16 @@ class EventManager(val app: App) : CoroutineScope { title: TextView, event: EventFull, showType: Boolean = true, + showNotes: Boolean = true, doneIconColor: Int? = null ) { - val topicSpan = event.topicHtml + val topicSpan = event.getNoteSubstituteText(showNotes) ?: event.topicHtml + val hasReplacingNotes = event.hasReplacingNotes() title.text = listOfNotNull( if (event.addedManually) "{cmd-clipboard-edit-outline} " 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, topicSpan, ).concat() @@ -70,10 +74,11 @@ class EventManager(val app: App) : CoroutineScope { ) } - fun setLegendText(legend: IconicsTextView, event: EventFull) { + 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.isDone) R.string.legend_event_is_done 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/NoteManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/NoteManager.kt new file mode 100644 index 00000000..7c25c129 --- /dev/null +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/NoteManager.kt @@ -0,0 +1,307 @@ +/* + * 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().getByIdNow(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, wasShared: Boolean): Boolean { + val success = when { + !note.isShared && wasShared -> unshareNote(activity, note) + note.isShared -> shareNote(activity, note) + 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): Boolean { + return app.api.runCatching(activity) { + shareNote(note) + } != 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, + showColor = 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/TextStylingManager.kt b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/TextStylingManager.kt index 74e7d2f0..54cf2b38 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/TextStylingManager.kt +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/managers/TextStylingManager.kt @@ -13,16 +13,22 @@ 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.* @@ -271,4 +277,28 @@ class TextStylingManager(private val app: App) { it.button.isEnabled = enable } } + + fun attachToField( + activity: AppCompatActivity, + textLayout: TextInputLayout, + textEdit: TextInputKeyboardEdit, + onShowListener: ((tag: String) -> Unit)? = null, + onDismissListener: ((tag: String) -> Unit)? = null, + ) { + textLayout.endIconDrawable = IconicsDrawable( + activity, + CommunityMaterial.Icon3.cmd_open_in_new + ).apply { sizeDp = 24 } + textLayout.setEndIconOnClickListener { + StyledTextDialog( + activity, + initialText = textEdit.text, + onSuccess = { + textEdit.text = it + }, + onShowListener, + onDismissListener + ).show() + } + } } diff --git a/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java b/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java index eb239ca9..71296b85 100644 --- a/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java +++ b/app/src/main/java/pl/szczodrzynski/edziennik/utils/models/Date.java @@ -6,14 +6,18 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.text.DateFormat; +import java.util.ArrayList; import java.util.Calendar; +import java.util.List; import java.util.Locale; import java.util.TimeZone; 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.ext.TextExtensionsKt; -public class Date implements Comparable { +public class Date implements Comparable, Noteable { public int year = 0; public int month = 0; public int day = 0; @@ -374,4 +378,51 @@ public class Date implements Comparable { result = 31 * result + day; return result; } + + @NonNull + @Override + public Note.OwnerType getNoteType() { + return Note.OwnerType.DAY; + } + + @Override + public int getNoteOwnerProfileId() { + return 0; + } + + @Override + public long getNoteOwnerId() { + return 0; + } + + @Nullable + @Override + public CharSequence getNoteSubstituteText(boolean showNotes) { + return null; + } + + @NonNull + @Override + public List getNotes() { + return new ArrayList(); + } + + @Override + public void setNotes(@NonNull List notes) { + } + + @Override + public void filterNotes() { + Noteable.DefaultImpls.filterNotes(this); + } + + @Override + public boolean hasNotes() { + return Noteable.DefaultImpls.hasNotes(this); + } + + @Override + public boolean hasReplacingNotes() { + return Noteable.DefaultImpls.hasReplacingNotes(this); + } } diff --git a/app/src/main/res/drawable/ic_announcement.xml b/app/src/main/res/drawable/ic_announcement.xml new file mode 100644 index 00000000..96e798dd --- /dev/null +++ b/app/src/main/res/drawable/ic_announcement.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_attendance.xml b/app/src/main/res/drawable/ic_attendance.xml new file mode 100644 index 00000000..b1474616 --- /dev/null +++ b/app/src/main/res/drawable/ic_attendance.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_behavior.xml b/app/src/main/res/drawable/ic_behavior.xml new file mode 100644 index 00000000..e688f85a --- /dev/null +++ b/app/src/main/res/drawable/ic_behavior.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_calendar_day.xml b/app/src/main/res/drawable/ic_calendar_day.xml new file mode 100644 index 00000000..43466c71 --- /dev/null +++ b/app/src/main/res/drawable/ic_calendar_day.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_calendar_event.xml b/app/src/main/res/drawable/ic_calendar_event.xml new file mode 100644 index 00000000..9917d977 --- /dev/null +++ b/app/src/main/res/drawable/ic_calendar_event.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/ic_grade.xml b/app/src/main/res/drawable/ic_grade.xml new file mode 100644 index 00000000..ed0551d5 --- /dev/null +++ b/app/src/main/res/drawable/ic_grade.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_message.xml b/app/src/main/res/drawable/ic_message.xml new file mode 100644 index 00000000..84c61e25 --- /dev/null +++ b/app/src/main/res/drawable/ic_message.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_note.xml b/app/src/main/res/drawable/ic_note.xml new file mode 100644 index 00000000..a00cf012 --- /dev/null +++ b/app/src/main/res/drawable/ic_note.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_timetable.xml b/app/src/main/res/drawable/ic_timetable.xml index be92d448..b51a0123 100644 --- a/app/src/main/res/drawable/ic_timetable.xml +++ b/app/src/main/res/drawable/ic_timetable.xml @@ -1,30 +1,30 @@ + android:viewportWidth="48" + android:viewportHeight="48"> + android:pathData="M42,38c0,2.209 -1.791,4 -4,4H10c-2.209,0 -4,-1.791 -4,-4V10c0,-2.209 1.791,-4 4,-4h28c2.209,0 4,1.791 4,4V38z" + android:fillColor="#CFD8DC"/> + android:pathData="M11,18H16V23H11zM18,18H23V23H18zM25,18H30V23H25zM32,18H37V23H32zM32,25H37V30H32zM11,25H16V30H11zM11,32H16V37H11zM18,25H23V30H18zM25,25H30V30H25zM18,32H23V37H18zM25,32H30V37H25zM32,32H37V37H32z" + android:fillColor="#90A4AE"/> + android:pathData="M11,11H16V16H11zM18,11H23V16H18zM25,11H30V16H25zM32,11H37V16H32z" + android:fillColor="#F44336"/> + android:pathData="M38,26c-0.338,0 -0.669,0.023 -1,0.05V30h-5v-2.382c-1.817,1.052 -3.329,2.565 -4.382,4.382H30v5h-3.95C26.023,37.331 26,37.662 26,38c0,1.405 0.254,2.747 0.697,4H38c2.209,0 4,-1.791 4,-4V26.697C40.747,26.254 39.405,26 38,26z" + android:fillColor="#ECEFF1"/> + android:pathData="M37,30v-3.95c-1.812,0.15 -3.506,0.703 -5,1.568V30H37zM30,32h-2.382c-0.865,1.494 -1.418,3.188 -1.568,5H30V32z" + android:fillColor="#CFD8DC"/> + android:pathData="M48,38c0,5.5 -4.5,10 -10,10s-10,-4.5 -10,-10s4.5,-10 10,-10S48,32.5 48,38" + android:fillColor="#F44336"/> + android:pathData="M45,38c0,3.9 -3.1,7 -7,7s-7,-3.1 -7,-7s3.1,-7 7,-7S45,34.1 45,38" + android:fillColor="#EEEEEE"/> + android:fillColor="#FF000000" + android:pathData="M42.4,41.1l-2.9,-2.9c0,-0.1 0,-0.1 0,-0.2c0,-0.4 -0.2,-0.8 -0.5,-1.1V33h-2v3.9c-0.3,0.3 -0.5,0.7 -0.5,1.1c0,0.8 0.7,1.5 1.5,1.5h0.1l2.9,2.9L42.4,41.1z"/> diff --git a/app/src/main/res/layout/attendance_details_dialog.xml b/app/src/main/res/layout/attendance_details_dialog.xml index b18e1c89..b3c46095 100644 --- a/app/src/main/res/layout/attendance_details_dialog.xml +++ b/app/src/main/res/layout/attendance_details_dialog.xml @@ -86,6 +86,14 @@ + + + + diff --git a/app/src/main/res/layout/attendance_item_attendance.xml b/app/src/main/res/layout/attendance_item_attendance.xml index 8ca3f486..362b3a79 100644 --- a/app/src/main/res/layout/attendance_item_attendance.xml +++ b/app/src/main/res/layout/attendance_item_attendance.xml @@ -52,7 +52,7 @@ tools:text="Nieobecność nieusprawiedliwiona" /> - + + + + + + + + + + + + diff --git a/app/src/main/res/layout/dialog_day.xml b/app/src/main/res/layout/dialog_day.xml index 8bbc7b64..9fd14e1e 100644 --- a/app/src/main/res/layout/dialog_day.xml +++ b/app/src/main/res/layout/dialog_day.xml @@ -31,13 +31,24 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginHorizontal="8dp" - android:layout_marginBottom="8dp" android:textAppearance="@style/NavView.TextView.Helper" android:textIsSelectable="true" android:visibility="gone" tools:text="8:00 - 14:20 (7 lekcji, 6 godzin, 20 minut)" tools:visibility="visible" /> + + + + @@ -106,5 +118,13 @@ android:clipToPadding="false" tools:listitem="@layout/event_list_item" tools:visibility="visible" /> + + diff --git a/app/src/main/res/layout/dialog_event_details.xml b/app/src/main/res/layout/dialog_event_details.xml index 7e8d610c..af817e1d 100644 --- a/app/src/main/res/layout/dialog_event_details.xml +++ b/app/src/main/res/layout/dialog_event_details.xml @@ -156,7 +156,7 @@ @@ -172,7 +172,7 @@ android:id="@+id/bodyTitle" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="4dp" + android:layout_marginTop="8dp" android:text="@string/dialog_event_details_body" android:textAppearance="@style/NavView.TextView.Helper" /> @@ -197,7 +197,7 @@ android:id="@+id/attachmentsTitle" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginTop="4dp" + android:layout_marginTop="8dp" android:text="@string/dialog_event_details_attachments" android:textAppearance="@style/NavView.TextView.Helper" /> @@ -279,6 +279,14 @@ android:text="\uf436" android:textSize="20sp" /> + + diff --git a/app/src/main/res/layout/dialog_grade_details.xml b/app/src/main/res/layout/dialog_grade_details.xml index bebfe40f..a6d2caec 100644 --- a/app/src/main/res/layout/dialog_grade_details.xml +++ b/app/src/main/res/layout/dialog_grade_details.xml @@ -113,6 +113,14 @@ + + + + diff --git a/app/src/main/res/layout/dialog_lesson_details.xml b/app/src/main/res/layout/dialog_lesson_details.xml index 7b4d3d6e..03a3c843 100644 --- a/app/src/main/res/layout/dialog_lesson_details.xml +++ b/app/src/main/res/layout/dialog_lesson_details.xml @@ -155,6 +155,14 @@ + + @@ -390,6 +399,13 @@ tools:visibility="visible" tools:listitem="@layout/event_list_item" /> + diff --git a/app/src/main/res/layout/grades_item_grade.xml b/app/src/main/res/layout/grades_item_grade.xml index 383d56d1..a87f8cf3 100644 --- a/app/src/main/res/layout/grades_item_grade.xml +++ b/app/src/main/res/layout/grades_item_grade.xml @@ -36,7 +36,7 @@ android:gravity="center_vertical" android:orientation="horizontal"> - @@ -260,6 +260,14 @@ android:visibility="gone" tools:drawableTop="@android:drawable/stat_sys_download" /> + + diff --git a/app/src/main/res/layout/messages_list_item.xml b/app/src/main/res/layout/messages_list_item.xml index ab365ba0..51f53a9d 100644 --- a/app/src/main/res/layout/messages_list_item.xml +++ b/app/src/main/res/layout/messages_list_item.xml @@ -34,7 +34,7 @@ tools:text="JP" tools:visibility="visible" /> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/note_dialog_header.xml b/app/src/main/res/layout/note_dialog_header.xml new file mode 100644 index 00000000..a6f21049 --- /dev/null +++ b/app/src/main/res/layout/note_dialog_header.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/note_dialog_subtitle.xml b/app/src/main/res/layout/note_dialog_subtitle.xml new file mode 100644 index 00000000..4c38dbfa --- /dev/null +++ b/app/src/main/res/layout/note_dialog_subtitle.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/note_editor_dialog.xml b/app/src/main/res/layout/note_editor_dialog.xml new file mode 100644 index 00000000..f52b3bb0 --- /dev/null +++ b/app/src/main/res/layout/note_editor_dialog.xml @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/note_list_category_item.xml b/app/src/main/res/layout/note_list_category_item.xml new file mode 100644 index 00000000..da6bd6b4 --- /dev/null +++ b/app/src/main/res/layout/note_list_category_item.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/layout/note_list_dialog.xml b/app/src/main/res/layout/note_list_dialog.xml new file mode 100644 index 00000000..38730de1 --- /dev/null +++ b/app/src/main/res/layout/note_list_dialog.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/note_list_item.xml b/app/src/main/res/layout/note_list_item.xml new file mode 100644 index 00000000..910d06ab --- /dev/null +++ b/app/src/main/res/layout/note_list_item.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/notes_fragment.xml b/app/src/main/res/layout/notes_fragment.xml new file mode 100644 index 00000000..0234f947 --- /dev/null +++ b/app/src/main/res/layout/notes_fragment.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/timetable_lesson.xml b/app/src/main/res/layout/timetable_lesson.xml index 4233a58e..491788b5 100644 --- a/app/src/main/res/layout/timetable_lesson.xml +++ b/app/src/main/res/layout/timetable_lesson.xml @@ -54,7 +54,7 @@ android:paddingVertical="4dp"> - Pobieranie udostępnionych wydarzeń… Rejestracja jest automatyczna, jeśli ta opcja jest włączona. Pozwala na tworzenie i odbieranie wydarzeń udostępnionych innym uczniom z Twojej klasy. Dzięki temu, można dodawać do dziennika pozycje nie zapisane przez nauczyciela.\n\nKontynuując, oświadczasz przeczytanie i akceptację postanowień Polityki prywatności. Aby móc udostępnić wydarzenie, należy włączyć opcję rejestracji na serwerze. Pozwala to na tworzenie i odbieranie wydarzeń udostępnionych w Twojej klasie.\n\nPo kliknięciu OK zostanie ona automatycznie włączona.\n\nKontynuując, oświadczasz przeczytanie i akceptację postanowień Polityki prywatności. + Aby móc udostępnić notatkę, należy włączyć opcję rejestracji na serwerze. Pozwala to na tworzenie i odbieranie notatek udostępnionych w Twojej klasie.\n\nPo kliknięciu OK zostanie ona automatycznie włączona.\n\nKontynuując, oświadczasz przeczytanie i akceptację postanowień Polityki prywatności. Udostępnianie wydarzeń + Udostępnianie notatek Rejestracja aplikacji Szkolny.eu Usuń Usunięto @@ -1498,4 +1500,51 @@ Edytuj tekst Wyślij wiadomość Kod QR nie wygląda na prawidłowy + Notatki + Notatki + Nie dodano żadnych notatek + Możesz dodać notatki oraz udostępnić je w klasie, używając przycisku Dodaj. + {cmd-playlist-edit} dodano notatki + {cmd-playlist-edit} dodano notatki\n{cmd-swap-horizontal} zastąpiono treść notatką + Dodano %1$s + Ogłoszenie szkolne + Frekwencja + Zachowanie + Dzień + Wydarzenie + Ocena + Lekcja + Wiadomość + ID notatki + ID właściciela + Notatka + Edytuj notatkę + Tytuł notatki (opcjonalnie) + Treść notatki + Zastąp oryginalną treść + Osoby z klasy zobaczą Twoją notatkę przy tym elemencie. + Treść notatki będzie widoczna zamiast oryginalnej treści elementu. + Podaj treść notatki + Kolor + Udostępnianie notatki… + Usuwanie udostępnionej notatki… + Udostępniono notatkę + Czy chcesz usunąć tą notatkę?\n\nZostanie ona usunięta na Twoim urządzeniu, oraz u pozostałych osób z klasy. + Notatki + Dodaj notatkę + Brak + Czerwony + Pomarańczowy + Żółty + Zielony + Morski + Niebieski + Ciemnoniebieski + Fioletowy + Różowy + Brązowy + Szary + Czarny + Notatki + Najnowsze notatki