[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.
This commit is contained in:
Kuba Szczodrzyński 2021-10-28 22:35:30 +02:00 committed by GitHub
parent 8745d7d526
commit 7c925cb88a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
109 changed files with 5504 additions and 188 deletions

View File

@ -31,6 +31,12 @@ android {
cppFlags "-std=c++11"
}
}
kapt {
arguments {
arg("room.schemaLocation", "$projectDir/schemas")
}
}
}
buildTypes {

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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,
)
}

View File

@ -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<NavTarget> = mutableListOf()
val moreList: MutableList<NavTarget> = 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)

View File

@ -18,7 +18,7 @@ import kotlin.coroutines.CoroutineContext
class ProfileConfig(val db: AppDb, val profileId: Int, rawEntries: List<ConfigEntry>) : CoroutineScope, AbstractConfig {
companion object {
const val DATA_VERSION = 2
const val DATA_VERSION = 3
}
private val job = Job()

View File

@ -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
}
}}
}

View File

@ -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<Profile>, notifications: List<Notification>, blacklistedIds: List<Long>, lastSyncTime: Long): List<EventFull> {
fun getEvents(
profiles: List<Profile>,
notifications: List<Notification>,
blacklistedIds: List<Long>,
lastSyncTime: Long,
): Pair<List<EventFull>, List<Note>> {
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<EventFull>()
val noteList = mutableListOf<Note>()
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<Nothing>? {
}*/

View File

@ -18,6 +18,9 @@ interface SzkolnyService {
@POST("share")
fun shareEvent(@Body request: EventShareRequest): Call<ApiResponse<Unit>>
@POST("share")
fun shareNote(@Body request: NoteShareRequest): Call<ApiResponse<Unit>>
@POST("webPush")
fun webPush(@Body request: WebPushRequest): Call<ApiResponse<WebPushResponse>>

View File

@ -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,

View File

@ -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
)

View File

@ -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<EventFull>,
val notes: List<Note>,
val hasBrowsers: Boolean? = null
)

View File

@ -30,7 +30,7 @@ class AppSync(val app: App, val notifications: MutableList<Notification>, 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<Notification>, 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<Notification>, val pr
})
return app.db.eventDao().upsertAll(events).size
}
return 0;
return 0
}
}

View File

@ -31,7 +31,7 @@ class SzkolnyTask(val app: App, val syncingProfiles: List<Profile>) : 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

View File

@ -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()
}
}

View File

@ -12,15 +12,19 @@ import pl.szczodrzynski.edziennik.data.db.entity.Keepable
@Dao
interface BaseDao<T : Keepable, F : T> {
@Transaction
@RawQuery
fun getRaw(query: SupportSQLiteQuery): LiveData<List<F>>
fun getRaw(query: String) = getRaw(SimpleSQLiteQuery(query))
@Transaction
@RawQuery
fun getOne(query: SupportSQLiteQuery): LiveData<F?>
fun getOne(query: String) = getOne(SimpleSQLiteQuery(query))
@Transaction
@RawQuery
fun getRawNow(query: SupportSQLiteQuery): List<F>
fun getRawNow(query: String) = getRawNow(SimpleSQLiteQuery(query))
@Transaction
@RawQuery
fun getOneNow(query: SupportSQLiteQuery): F?
fun getOneNow(query: String) = getOneNow(SimpleSQLiteQuery(query))

View File

@ -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<Note>)
@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<Note?>
@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<List<Note>>
@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<List<Note>>
@Query("SELECT * FROM notes WHERE profileId = :profileId AND noteOwnerType IS NULL $ORDER_BY")
fun getAllNoOwner(profileId: Int): LiveData<List<Note>>
}

View File

@ -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<Note> {
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
}
}
}

View File

@ -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<Note>
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
}
}

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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<Note>
override fun getNoteType() = Note.OwnerType.ANNOUNCEMENT
override fun getNoteOwnerProfileId() = profileId
override fun getNoteOwnerId() = id
}

View File

@ -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<Note>
override fun getNoteType() = Note.OwnerType.ATTENDANCE
override fun getNoteOwnerProfileId() = profileId
override fun getNoteOwnerId() = id
}

View File

@ -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<EventFull> {
), Searchable<EventFull>, 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<Note>
override fun getNoteType() = Note.OwnerType.EVENT
override fun getNoteOwnerProfileId() = profileId
override fun getNoteOwnerId() = id
}

View File

@ -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<Note>
override fun getNoteType() = Note.OwnerType.GRADE
override fun getNoteOwnerProfileId() = profileId
override fun getNoteOwnerId() = id
}

View File

@ -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<Note>
override fun getNoteType() = Note.OwnerType.LESSON
override fun getNoteOwnerProfileId() = profileId
override fun getNoteOwnerId() = id
}

View File

@ -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<MessageFull> {
), Searchable<MessageFull>, Noteable {
var senderName: String? = null
@Relation(parentColumn = "messageId", entityColumn = "messageId", entity = MessageRecipient::class)
var recipients: MutableList<MessageRecipientFull>? = 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<Note>
override fun getNoteType() = Note.OwnerType.MESSAGE
override fun getNoteOwnerProfileId() = profileId
override fun getNoteOwnerId() = id
}

View File

@ -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<Note>
override fun getNoteType() = Note.OwnerType.BEHAVIOR
override fun getNoteOwnerProfileId() = profileId
override fun getNoteOwnerId() = id
}

View File

@ -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);")
}
}

View File

@ -47,6 +47,15 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, 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<Profile>, 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<Profile>, 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<Profile>, 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<Note>()
val notificationList = mutableListOf<Notification>()
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)
}
}
}

View File

@ -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
})
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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<DialogDayBinding>(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)
}
}

View File

@ -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<LessonChangesAdapter.ViewHolder>() {
var items = listOf<LessonFull>()
@ -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 -> {

View File

@ -35,7 +35,7 @@ class LessonChangesDialog(
val adapter = LessonChangesAdapter(
activity,
onItemClick = {
onLessonClick = {
LessonDetailsDialog(
activity,
it,

View File

@ -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<AnnouncementsAdap
private Context context;
public List<AnnouncementFull> announcementList;
@Nullable
public OnAnnouncementClickListener onClick;
public interface OnAnnouncementClickListener {
void onClick(View v, AnnouncementFull announcement);
}
public AnnouncementsAdapter(Context context, List<AnnouncementFull> announcementList, OnAnnouncementClickListener onClick) {
public AnnouncementsAdapter(Context context, List<AnnouncementFull> announcementList, @Nullable OnAnnouncementClickListener onClick) {
//setHasStableIds(true);
this.context = context;
@ -54,11 +56,12 @@ public class AnnouncementsAdapter extends RecyclerView.Adapter<AnnouncementsAdap
AnnouncementFull item = announcementList.get(groupPosition);
RowAnnouncementsItemBinding b = holder.b;
b.announcementsItem.setOnClickListener((v -> {
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());

View File

@ -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);

View File

@ -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<RecyclerView.ViewHolder>(), 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) {

View File

@ -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<AttendanceDetailsDialogBinding>(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)
}
}

View File

@ -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) {

View File

@ -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() }

View File

@ -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,

View File

@ -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(), "<font color='#4caf50'>$packageName</font>")
content = content.replace("\n".toRegex(), "<br>")
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

View File

@ -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,

View File

@ -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)

View File

@ -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<DialogEventDetailsBinding>(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)

View File

@ -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<EventFull>(isReversed), CoroutineScope {
companion object {

View File

@ -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()

View File

@ -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

View File

@ -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<DialogGradeDetailsBinding>(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)
}
}

View File

@ -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<RecyclerView.ViewHolder>(), 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) {

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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() =

View File

@ -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?
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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 ->

View File

@ -13,7 +13,8 @@ import pl.szczodrzynski.edziennik.ui.search.SearchableAdapter
class MessagesAdapter(
val activity: AppCompatActivity,
val teachers: List<Teacher>,
val onItemClick: ((item: MessageFull) -> Unit)? = null,
val showNotes: Boolean = true,
val onMessageClick: ((item: MessageFull) -> Unit)? = null,
val onStarClick: ((item: MessageFull) -> Unit)? = null,
) : SearchableAdapter<MessageFull>() {
companion object {

View File

@ -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 ->

View File

@ -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() {

View File

@ -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<Note, NoteListAdapter> {
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)
}
}

View File

@ -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<NoteDetailsDialogBinding>(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 ?: "",
)
}
}

View File

@ -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<NoteEditorDialogBinding>(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<Boolean> { 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,
)
}
}

View File

@ -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<Note>() {
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)
}
}

View File

@ -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<NoteListDialogBinding>(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()
}
}
}
}

View File

@ -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<Note, NoteListAdapter> {
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<CharSequence>(
"{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)
}
}
}

View File

@ -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<LinearLayout.LayoutParams> {
gravity = Gravity.CENTER_HORIZONTAL
}
updatePadding(left = 12.dp)
onClick {
NoteListDialog(
activity = activity,
owner = owner,
onShowListener = onShowListener,
onDismissListener = onDismissListener,
).show()
}
}

View File

@ -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)
}
}
}

View File

@ -59,14 +59,15 @@ class SearchFilter<T : Searchable<T>>(
}
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

View File

@ -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

View File

@ -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<DialogLessonDetailsBinding>(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)
}
}

View File

@ -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)

View File

@ -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()
}

View File

@ -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,
)
}
}

View File

@ -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()
}
}
}

View File

@ -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<Date> {
public class Date implements Comparable<Date>, Noteable {
public int year = 0;
public int month = 0;
public int day = 0;
@ -374,4 +378,51 @@ public class Date implements Comparable<Date> {
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<Note> getNotes() {
return new ArrayList();
}
@Override
public void setNotes(@NonNull List<Note> 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);
}
}

View File

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M17.4,33H15v-4h4l0.4,1.5C19.7,31.8 18.7,33 17.4,33zM37,36c0,0 -11.8,-7 -18,-7V15c5.8,0 18,-7 18,-7V36z"
android:fillColor="#90CAF9"/>
<path
android:pathData="M9,17A5,5 0,1 0,9 27,5 5,0 1,0 9,17zM40,19h-3v6h3c1.7,0 3,-1.3 3,-3S41.7,19 40,19z"
android:fillColor="#283593"/>
<path
android:pathData="M18.6,41.2c-0.9,0.6 -2.5,1.2 -4.6,1.4c-0.6,0.1 -1.2,-0.3 -1.4,-1L8.2,27.9c0,0 8.8,-6.2 8.8,1.1c0,5.5 1.5,8.4 2.2,9.5c0.5,0.7 0.5,1.6 0,2.3C19,41 18.8,41.1 18.6,41.2z"
android:fillColor="#283593"/>
<path
android:pathData="M9,29h10V15H9c-1.1,0 -2,0.9 -2,2v10C7,28.1 7.9,29 9,29z"
android:fillColor="#3F51B5"/>
<path
android:pathData="M38,38L38,38c-1.1,0 -2,-0.9 -2,-2V8c0,-1.1 0.9,-2 2,-2h0c1.1,0 2,0.9 2,2v28C40,37.1 39.1,38 38,38z"
android:fillColor="#42A5F5"/>
</vector>

View File

@ -0,0 +1,33 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M40,45L8,45 8,3 30,3 40,13z"
android:fillColor="#90CAF9"/>
<path
android:pathData="M38.5,14L29,14 29,4.5z"
android:fillColor="#E1F5FE"/>
<path
android:pathData="M13,28H25V30H13zM13,20H25V22H13zM13,36H25V38H13z"
android:fillColor="#1976D2"/>
<path
android:pathData="M29,40h6v-6h-6V40z"
android:fillColor="#0D47A1"/>
<path
android:pathData="M31,36H33V38H31z"
android:fillColor="#BBDEFB"/>
<path
android:pathData="M32.91,30.59L31.5,32 28,28.5 29.41,27.09 31.5,29.18z"
android:fillColor="#0D47A1"/>
<path
android:pathData="M35.972,27.528L31.5,32 30.09,30.59 31.5,29.18 34.562,26.118z"
android:fillColor="#0D47A1"/>
<path
android:pathData="M28.9997,19.414L30.4139,17.9998L34.9996,22.5853L33.5854,23.9995z"
android:fillColor="#0D47A1"/>
<path
android:pathData="M28.9998,22.5861L33.5853,18.0004L34.9995,19.4146L30.414,24.0003z"
android:fillColor="#0D47A1"/>
</vector>

View File

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M44,24c0,11 -9,20 -20,20S4,35 4,24S13,4 24,4S44,13 44,24z"
android:fillColor="#FFCA28"/>
<path
android:pathData="M33,27c0,0.86 -0.27,4.11 -1.69,7.01H16.65c-1.42,-2.9 -1.7,-6.15 -1.65,-7.01H33z"
android:fillColor="#5D4037"/>
<path
android:pathData="M13.8,21.6l-1.6,-1.2C12.3,20.3 14,18 17,18s4.7,2.3 4.8,2.4l-1.6,1.2c0,0 -1.3,-1.6 -3.2,-1.6S13.8,21.6 13.8,21.6zM27.8,21.6l-1.6,-1.2C26.3,20.3 28,18 31,18s4.7,2.3 4.8,2.4l-1.6,1.2c0,0 -1.3,-1.6 -3.2,-1.6S27.8,21.6 27.8,21.6z"
android:fillColor="#B76C09"/>
<path
android:pathData="M31.31,34.01C30.04,36.65 27.81,39 24,39s-6.05,-2.35 -7.35,-4.99c0,0 1.975,-2.01 7.35,-2.01S31.31,34.01 31.31,34.01z"
android:fillColor="#C62828"/>
</vector>

View File

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M43,38c0,2.209 -1.791,4 -4,4H9c-2.209,0 -4,-1.791 -4,-4V10c0,-2.209 1.791,-4 4,-4h30c2.209,0 4,1.791 4,4V38z"
android:fillColor="#CFD8DC"/>
<path
android:pathData="M10,10H38V14H10z"
android:fillColor="#F44336"/>
<path
android:pathData="M16,22H38V26H16zM16,28H38V32H16zM16,34H38V38H16zM10,22H14V26H10zM10,28H14V32H10zM10,34H14V38H10zM16,16H38V20H16zM10,16H14V20H10z"
android:fillColor="#90A4AE"/>
</vector>

View File

@ -0,0 +1,27 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M5,39V15h38v24c0,2.2 -1.8,4 -4,4H9C6.8,43 5,41.2 5,39"
android:fillColor="#CFD8DC"/>
<path
android:pathData="M5,25h38v8H5V25z"
android:fillColor="#ECEFF1"/>
<path
android:pathData="M13,27h4v4h-4V27zM19,27h4v4h-4V27zM31,27h4v4h-4V27z"
android:fillColor="#B0BEC5"/>
<path
android:pathData="M13,21h4v4h-4V21zM19,21h4v4h-4V21zM25,21h4v4h-4V21zM31,21h4v4h-4V21zM13,33h4v4h-4V33zM19,33h4v4h-4V33zM25,33h4v4h-4V33zM31,33h4v4h-4V33z"
android:fillColor="#90A4AE"/>
<path
android:pathData="M25,27h4v4h-4V27zM43,11v6H5v-6c0,-2.2 1.8,-4 4,-4h30C41.2,7 43,8.8 43,11"
android:fillColor="#F44336"/>
<path
android:pathData="M36,11c0,1.7 -1.3,3 -3,3s-3,-1.3 -3,-3s1.3,-3 3,-3C34.7,8 36,9.3 36,11M18,11c0,1.7 -1.3,3 -3,3s-3,-1.3 -3,-3s1.3,-3 3,-3S18,9.3 18,11"
android:fillColor="#B71C1C"/>
<path
android:pathData="M33,4c-1.1,0 -2,0.9 -2,2v5c0,1.1 0.9,2 2,2s2,-0.9 2,-2V6C35,4.9 34.1,4 33,4M15,4c-1.1,0 -2,0.9 -2,2v5c0,1.1 0.9,2 2,2s2,-0.9 2,-2V6C17,4.9 16.1,4 15,4"
android:fillColor="#B0BEC5"/>
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M44,24c0,11.045 -8.955,20 -20,20S4,35.045 4,24S12.955,4 24,4S44,12.955 44,24z"
android:fillColor="#F44336"/>
<path
android:pathData="M24,11l3.898,7.898l8.703,1.301l-6.301,6.102l1.5,8.699L24,30.898L16.199,35l1.5,-8.699l-6.301,-6.102l8.703,-1.301L24,11z"
android:fillColor="#FFCA28"/>
</vector>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M40,10H8c-2.209,0 -4,1.791 -4,4v20c0,2.209 1.791,4 4,4h32c2.209,0 4,-1.791 4,-4V14C44,11.791 42.209,10 40,10z"
android:fillColor="#2196F3"/>
<path
android:pathData="M44,14.025c0,-0.465 -0.095,-0.904 -0.24,-1.32L24,27.025L4.241,12.705C4.095,13.121 4,13.561 4,14.025V15l20,14.495L44,15V14.025z"
android:fillColor="#0D47A1"/>
</vector>

View File

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="64dp"
android:height="64dp"
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="M8,39.001v-30c0,-2.2 1.8,-4 4,-4h24c2.2,0 4,1.8 4,4v30c0,2.2 -1.8,4 -4,4H12C9.8,43.001 8,41.201 8,39.001z"
android:fillColor="#42a5f5"/>
<path
android:pathData="M14,8.001c0,-1.105 -0.895,-2 -2,-2s-2,0.895 -2,2s0.895,2 2,2S14,9.105 14,8.001zM22,8.001c0,-1.105 -0.895,-2 -2,-2s-2,0.895 -2,2s0.895,2 2,2S22,9.105 22,8.001zM30,8.001c0,-1.105 -0.895,-2 -2,-2s-2,0.895 -2,2s0.895,2 2,2S30,9.105 30,8.001zM38,8.001c0,-1.105 -0.895,-2 -2,-2s-2,0.895 -2,2s0.895,2 2,2S38,9.105 38,8.001z"
android:fillColor="#1e88e5"/>
<path
android:pathData="M11,8.001v-4c0,-0.6 0.4,-1 1,-1l0,0c0.6,0 1,0.4 1,1v4c0,0.6 -0.4,1 -1,1l0,0C11.4,9.001 11,8.601 11,8.001zM19,8.001v-4c0,-0.6 0.4,-1 1,-1l0,0c0.6,0 1,0.4 1,1v4c0,0.6 -0.4,1 -1,1l0,0C19.4,9.001 19,8.601 19,8.001zM27,8.001v-4c0,-0.6 0.4,-1 1,-1l0,0c0.6,0 1,0.4 1,1v4c0,0.6 -0.4,1 -1,1l0,0C27.4,9.001 27,8.601 27,8.001zM35,8.001v-4c0,-0.6 0.4,-1 1,-1l0,0c0.6,0 1,0.4 1,1v4c0,0.6 -0.4,1 -1,1l0,0C35.4,9.001 35,8.601 35,8.001z"
android:fillColor="#cfd8dc"/>
<path
android:pathData="M14,19.001h19.993v2H14V19.001zM14.007,15.001H34v2H14.007V15.001zM14,30.001h17v2H14V30.001zM27,34.001h6.993v2H27V34.001zM14,34.001h11v2H14V34.001zM21,23.001h-7v2h7V23.001z"
android:fillColor="#1565c0"/>
</vector>

View File

@ -1,30 +1,30 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
android:viewportWidth="48"
android:viewportHeight="48">
<path
android:pathData="m18,100v-60h92v60c0,5.523 -4.477,10 -10,10h-72c-5.523,0 -10,-4.477 -10,-10z"
android:fillColor="#ffc662"/>
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"/>
<path
android:pathData="m110,29.602v16.398h-92v-16.398c0,-5.309 4.332,-9.602 9.684,-9.602h72.633c5.352,0 9.684,4.293 9.684,9.602"
android:fillColor="#ff634f"/>
android:pathData="M11,18H16V23H11zM18,18H23V23H18zM25,18H30V23H25zM32,18H37V23H32zM32,25H37V30H32zM11,25H16V30H11zM11,32H16V37H11zM18,25H23V30H18zM25,25H30V30H25zM18,32H23V37H18zM25,32H30V37H25zM32,32H37V37H32z"
android:fillColor="#90A4AE"/>
<path
android:pathData="m43,34c-2.75,0 -5,-2.25 -5,-5v-12c0,-2.75 2.25,-5 5,-5s5,2.25 5,5v12c0,2.75 -2.25,5 -5,5z"
android:fillColor="#888"/>
android:pathData="M11,11H16V16H11zM18,11H23V16H18zM25,11H30V16H25zM32,11H37V16H32z"
android:fillColor="#F44336"/>
<path
android:pathData="m85,34c-2.75,0 -5,-2.25 -5,-5v-12c0,-2.75 2.25,-5 5,-5s5,2.25 5,5v12c0,2.75 -2.25,5 -5,5z"
android:fillColor="#888"/>
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"/>
<path
android:pathData="m48,74h-8c-2.211,0 -4,-1.789 -4,-4v-8c0,-2.211 1.789,-4 4,-4h8c2.211,0 4,1.789 4,4v8c0,2.211 -1.789,4 -4,4zM72,70v-8c0,-2.211 -1.789,-4 -4,-4h-8c-2.211,0 -4,1.789 -4,4v8c0,2.211 1.789,4 4,4h8c2.211,0 4,-1.789 4,-4zM92,70v-8c0,-2.211 -1.789,-4 -4,-4h-8c-2.211,0 -4,1.789 -4,4v8c0,2.211 1.789,4 4,4h8c2.211,0 4,-1.789 4,-4zM52,90v-8c0,-2.211 -1.789,-4 -4,-4h-8c-2.211,0 -4,1.789 -4,4v8c0,2.211 1.789,4 4,4h8c2.211,0 4,-1.789 4,-4zM72,90v-8c0,-2.211 -1.789,-4 -4,-4h-8c-2.211,0 -4,1.789 -4,4v8c0,2.211 1.789,4 4,4h8c2.211,0 4,-1.789 4,-4zM92,90v-8c0,-2.211 -1.789,-4 -4,-4h-8c-2.211,0 -4,1.789 -4,4v8c0,2.211 1.789,4 4,4h8c2.211,0 4,-1.789 4,-4z"
android:fillColor="#ffe79f"/>
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"/>
<path
android:pathData="m124,104c0,11.047 -8.953,20 -20,20 -11.047,0 -20,-8.953 -20,-20 0,-11.047 8.953,-20 20,-20 11.047,0 20,8.953 20,20z"
android:fillColor="#fff"/>
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"/>
<path
android:pathData="m104,80c-13.254,0 -24,10.746 -24,24 0,13.254 10.746,24 24,24 13.254,0 24,-10.746 24,-24 0,-13.254 -10.746,-24 -24,-24zM104,120c-8.836,0 -16,-7.164 -16,-16 0,-8.836 7.164,-16 16,-16 8.836,0 16,7.164 16,16 0,8.836 -7.164,16 -16,16z"
android:fillColor="#1f80e5"/>
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"/>
<path
android:pathData="m113.41,110.59 l-5.563,-5.563c0.086,-0.328 0.148,-0.668 0.148,-1.023 0,-1.477 -0.809,-2.754 -2,-3.445v-6.555c0,-1.105 -0.895,-2 -2,-2s-2,0.895 -2,2v6.555c-1.191,0.691 -2,1.969 -2,3.445 0,2.211 1.789,4 4,4 0.355,0 0.695,-0.063 1.023,-0.148l5.563,5.563c0.391,0.391 0.902,0.586 1.414,0.586s1.023,-0.195 1.414,-0.586c0.781,-0.781 0.781,-2.047 0,-2.828z"
android:fillColor="#919191"/>
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"/>
</vector>

View File

@ -86,6 +86,14 @@
</LinearLayout>
</LinearLayout>
<com.mikepenz.iconics.view.IconicsTextView
android:id="@+id/legend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/NavView.TextView.Helper"
tools:text="[ - ] dodano notatki" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -230,6 +238,15 @@
tools:text="12345" />
</LinearLayout>
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/notesButton"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:text="@string/notes_button" />
</LinearLayout>
</ScrollView>
</layout>

View File

@ -52,7 +52,7 @@
tools:text="Nieobecność nieusprawiedliwiona" />
<TextView
<com.mikepenz.iconics.view.IconicsTextView
android:id="@+id/subjectName"
android:layout_width="0dp"
android:layout_height="wrap_content"

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2021-10-28.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:layout_margin="8dp">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/card_notes_header_title"
android:textAppearance="@style/NavView.TextView.Title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/noData"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="16dp"
android:fontFamily="sans-serif-light"
android:text="@string/notes_no_data"
android:textSize="16sp" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:clipToPadding="false"
tools:itemCount="3"
tools:listitem="@layout/note_list_item"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton
android:id="@+id/addNote"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/notes_action_add" />
</LinearLayout>

View File

@ -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" />
<com.mikepenz.iconics.view.IconicsTextView
android:id="@+id/legend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:textAppearance="@style/NavView.TextView.Helper"
tools:text="[ - ] dodano notatki" />
<View
android:layout_width="match_parent"
android:layout_height="8dp" />
<FrameLayout
android:id="@+id/lessonChangesFrame"
android:layout_width="match_parent"
@ -74,7 +85,8 @@
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:orientation="vertical"
android:paddingVertical="16dp"
android:paddingTop="16dp"
android:paddingBottom="8dp"
android:visibility="gone"
tools:visibility="visible">
@ -106,5 +118,13 @@
android:clipToPadding="false"
tools:listitem="@layout/event_list_item"
tools:visibility="visible" />
<com.google.android.material.button.MaterialButton
android:id="@+id/notesButton"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/notes_button" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -156,7 +156,7 @@
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:layout_marginTop="8dp"
android:textAppearance="@style/NavView.TextView.Helper"
android:text="@string/dialog_event_details_topic"/>
@ -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" />
</com.google.android.flexbox.FlexboxLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/notesButton"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/notes_button" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</layout>

View File

@ -113,6 +113,14 @@
</LinearLayout>
</LinearLayout>
<com.mikepenz.iconics.view.IconicsTextView
android:id="@+id/legend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textAppearance="@style/NavView.TextView.Helper"
tools:text="[ - ] dodano notatki" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -307,6 +315,15 @@
android:minHeight="0dp"
android:text="@string/configure" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/notesButton"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="8dp"
android:text="@string/notes_button" />
</LinearLayout>
</ScrollView>
</layout>

View File

@ -155,6 +155,14 @@
</LinearLayout>
<com.mikepenz.iconics.view.IconicsTextView
android:id="@+id/legend"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:textAppearance="@style/NavView.TextView.Helper"
tools:text="[ - ] dodano notatki" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
@ -356,8 +364,9 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:paddingVertical="16dp"
android:orientation="vertical"
android:paddingTop="16dp"
android:paddingBottom="8dp"
android:visibility="gone"
tools:visibility="visible">
@ -390,6 +399,13 @@
tools:visibility="visible"
tools:listitem="@layout/event_list_item" />
<com.google.android.material.button.MaterialButton
android:id="@+id/notesButton"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/notes_button" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</layout>

View File

@ -36,7 +36,7 @@
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
<com.mikepenz.iconics.view.IconicsTextView
android:id="@+id/gradeDescription"
android:layout_width="0dp"
android:layout_height="wrap_content"

View File

@ -198,7 +198,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginHorizontal="8dp"
android:layout_marginVertical="8dp"
android:layout_marginTop="8dp"
android:orientation="horizontal"
android:visibility="visible"
tools:visibility="visible">
@ -260,6 +260,14 @@
android:visibility="gone"
tools:drawableTop="@android:drawable/stat_sys_download" />
</LinearLayout>
<com.google.android.material.button.MaterialButton
android:id="@+id/notesButton"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:text="@string/notes_button" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@ -34,7 +34,7 @@
tools:text="JP"
tools:visibility="visible" />
<TextView
<com.mikepenz.iconics.view.IconicsTextView
android:id="@+id/messageSubject"
android:layout_width="0dp"
android:layout_height="wrap_content"

View File

@ -0,0 +1,143 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2021-10-23.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="pl.szczodrzynski.edziennik.data.db.entity.Note" />
<variable
name="note"
type="Note" />
</data>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp">
<include
android:id="@+id/header"
layout="@layout/note_dialog_header" />
<include
layout="@layout/note_dialog_subtitle"
app:text="@{@string/notes_details_dialog_title}" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginHorizontal="8dp"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dialog_event_details_added_by"
android:textAppearance="@style/NavView.TextView.Helper" />
<com.mikepenz.iconics.view.IconicsTextView
android:id="@+id/addedBy"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textIsSelectable="true"
tools:text="18 grudnia, 23:17 przez Janósz Kowalski" />
<LinearLayout
android:id="@+id/idsLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:baselineAligned="false"
android:orientation="horizontal"
android:visibility="gone">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/notes_details_id"
android:textAppearance="@style/NavView.TextView.Helper" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{String.valueOf(note.id)}"
android:textIsSelectable="true"
tools:text="1635073956543" />
</LinearLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/notes_details_owner_id"
android:textAppearance="@style/NavView.TextView.Helper" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{String.valueOf(note.ownerId)}"
android:textIsSelectable="true"
tools:text="3875623" />
</LinearLayout>
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:isVisible="@{note.topic != null}"
android:text="@string/dialog_event_details_topic"
android:textAppearance="@style/NavView.TextView.Helper" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:isVisible="@{note.topic != null}"
android:text="@{note.topicHtml}"
android:textAppearance="@style/NavView.TextView.Medium"
android:textIsSelectable="true"
tools:text="Rozdział II: Panowanie Piastów i Jagiellonów.Przeniesiony z 11 grudnia." />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/dialog_event_details_body"
android:textAppearance="@style/NavView.TextView.Helper" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{note.bodyHtml}"
android:textAppearance="@style/NavView.TextView.Medium"
android:textIsSelectable="true"
tools:text="Rozdział II: Panowanie Piastów i Jagiellonów.Przeniesiony z 11 grudnia." />
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</layout>

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