1
0
mirror of https://github.com/wulkanowy/wulkanowy.git synced 2024-09-19 20:39:08 -05:00

Add notes (#179)

This commit is contained in:
Mikołaj Pich 2018-11-11 17:45:58 +01:00 committed by Rafał Borcz
parent 240e61df0e
commit cb7e70471b
34 changed files with 922 additions and 26 deletions

1
.gitignore vendored
View File

@ -47,3 +47,4 @@ Thumbs.db
./app/key.p12
./app/upload-key.jks
*.log
.idea/assetWizardSettings.xml

View File

@ -78,7 +78,7 @@ ext.androidx_version = "1.0.0"
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation('com.github.wulkanowy:api:e829b094de') { exclude module: "threetenbp" }
implementation('com.github.wulkanowy:api:96be34a') { exclude module: "threetenbp" }
implementation "androidx.legacy:legacy-support-v4:$androidx_version"
implementation "androidx.appcompat:appcompat:$androidx_version"

View File

@ -67,4 +67,8 @@ internal class RepositoryModule {
@Singleton
@Provides
fun provideTimetableDao(database: AppDatabase) = database.timetableDao()
@Singleton
@Provides
fun provideNoteDao(database: AppDatabase) = database.noteDao()
}

View File

@ -5,23 +5,38 @@ import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import io.github.wulkanowy.data.db.dao.*
import io.github.wulkanowy.data.db.entities.*
import io.github.wulkanowy.data.db.dao.AttendanceDao
import io.github.wulkanowy.data.db.dao.ExamDao
import io.github.wulkanowy.data.db.dao.GradeDao
import io.github.wulkanowy.data.db.dao.GradeSummaryDao
import io.github.wulkanowy.data.db.dao.NoteDao
import io.github.wulkanowy.data.db.dao.SemesterDao
import io.github.wulkanowy.data.db.dao.StudentDao
import io.github.wulkanowy.data.db.dao.TimetableDao
import io.github.wulkanowy.data.db.entities.Attendance
import io.github.wulkanowy.data.db.entities.Exam
import io.github.wulkanowy.data.db.entities.Grade
import io.github.wulkanowy.data.db.entities.GradeSummary
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.db.entities.Student
import io.github.wulkanowy.data.db.entities.Timetable
import javax.inject.Singleton
@Singleton
@Database(
entities = [
Student::class,
Semester::class,
Exam::class,
Timetable::class,
Attendance::class,
Grade::class,
GradeSummary::class
],
version = 1,
exportSchema = false
entities = [
Student::class,
Semester::class,
Exam::class,
Timetable::class,
Attendance::class,
Grade::class,
GradeSummary::class,
Note::class
],
version = 1,
exportSchema = false
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
@ -29,7 +44,7 @@ abstract class AppDatabase : RoomDatabase() {
companion object {
fun newInstance(context: Context): AppDatabase {
return Room.databaseBuilder(context, AppDatabase::class.java, "wulkanowy_database")
.build()
.build()
}
}
@ -46,4 +61,6 @@ abstract class AppDatabase : RoomDatabase() {
abstract fun gradeDao(): GradeDao
abstract fun gradeSummaryDao(): GradeSummaryDao
abstract fun noteDao(): NoteDao
}

View File

@ -0,0 +1,31 @@
package io.github.wulkanowy.data.db.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import io.github.wulkanowy.data.db.entities.Note
import io.reactivex.Maybe
@Dao
interface NoteDao {
@Insert
fun insertAll(notes: List<Note>)
@Update
fun update(note: Note)
@Update
fun updateAll(notes: List<Note>)
@Delete
fun deleteAll(notes: List<Note>)
@Query("SELECT * FROM Notes WHERE semester_id = :semesterId AND student_id = :studentId")
fun getNotes(semesterId: Int, studentId: Int): Maybe<List<Note>>
@Query("SELECT * FROM Notes WHERE is_read = 0 AND semester_id = :semesterId AND student_id = :studentId")
fun getNewNotes(semesterId: Int, studentId: Int): Maybe<List<Note>>
}

View File

@ -0,0 +1,35 @@
package io.github.wulkanowy.data.db.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import org.threeten.bp.LocalDate
import java.io.Serializable
@Entity(tableName = "Notes")
data class Note(
@ColumnInfo(name = "semester_id")
var semesterId: Int,
@ColumnInfo(name = "student_id")
var studentId: Int,
var date: LocalDate,
var teacher: String,
var category: String,
var content: String
) : Serializable {
@PrimaryKey(autoGenerate = true)
var id: Long = 0
@ColumnInfo(name = "is_read")
var isRead: Boolean = false
@ColumnInfo(name = "is_notified")
var isNotified: Boolean = true
}

View File

@ -0,0 +1,52 @@
package io.github.wulkanowy.data.repositories
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import com.github.pwittchen.reactivenetwork.library.rx2.internet.observing.InternetObservingSettings
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.data.repositories.local.NoteLocal
import io.github.wulkanowy.data.repositories.remote.NoteRemote
import io.reactivex.Completable
import io.reactivex.Single
import java.net.UnknownHostException
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class NoteRepository @Inject constructor(
private val settings: InternetObservingSettings,
private val local: NoteLocal,
private val remote: NoteRemote
) {
fun getNotes(semester: Semester, forceRefresh: Boolean = false, notify: Boolean = false): Single<List<Note>> {
return local.getNotes(semester).filter { !forceRefresh }
.switchIfEmpty(ReactiveNetwork.checkInternetConnectivity(settings)
.flatMap {
if (it) remote.getNotes(semester)
else Single.error(UnknownHostException())
}.flatMap { new ->
local.getNotes(semester).toSingle(emptyList())
.doOnSuccess { old ->
local.deleteNotes(old - new)
local.saveNotes((new - old)
.onEach {
if (notify) it.isNotified = false
})
}
}.flatMap { local.getNotes(semester).toSingle(emptyList()) }
)
}
fun getNewNotes(semester: Semester): Single<List<Note>> {
return local.getNewNotes(semester).toSingle(emptyList())
}
fun updateNote(note: Note): Completable {
return local.updateNote(note)
}
fun updateNotes(notes: List<Note>): Completable {
return local.updateNotes(notes)
}
}

View File

@ -0,0 +1,37 @@
package io.github.wulkanowy.data.repositories.local
import io.github.wulkanowy.data.db.dao.NoteDao
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Semester
import io.reactivex.Completable
import io.reactivex.Maybe
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class NoteLocal @Inject constructor(private val noteDb: NoteDao) {
fun getNotes(semester: Semester): Maybe<List<Note>> {
return noteDb.getNotes(semester.semesterId, semester.studentId).filter { !it.isEmpty() }
}
fun getNewNotes(semester: Semester): Maybe<List<Note>> {
return noteDb.getNewNotes(semester.semesterId, semester.studentId)
}
fun saveNotes(notes: List<Note>) {
noteDb.insertAll(notes)
}
fun updateNote(note: Note): Completable {
return Completable.fromCallable { noteDb.update(note) }
}
fun updateNotes(notes: List<Note>): Completable {
return Completable.fromCallable { noteDb.updateAll(notes) }
}
fun deleteNotes(notes: List<Note>) {
noteDb.deleteAll(notes)
}
}

View File

@ -0,0 +1,34 @@
package io.github.wulkanowy.data.repositories.remote
import io.github.wulkanowy.api.Api
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.db.entities.Semester
import io.github.wulkanowy.utils.toLocalDate
import io.reactivex.Single
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class NoteRemote @Inject constructor(private val api: Api) {
fun getNotes(semester: Semester): Single<List<Note>> {
return Single.just(api.run {
if (diaryId != semester.diaryId) {
diaryId = semester.diaryId
notifyDataChanged()
}
}).flatMap { api.getNotes() }
.map { notes ->
notes.map {
Note(
semesterId = semester.semesterId,
studentId = semester.studentId,
date = it.date.toLocalDate(),
teacher = it.teacher,
category = it.category,
content = it.content
)
}
}
}
}

View File

@ -7,10 +7,12 @@ import io.github.wulkanowy.data.repositories.AttendanceRepository
import io.github.wulkanowy.data.repositories.ExamRepository
import io.github.wulkanowy.data.repositories.GradeRepository
import io.github.wulkanowy.data.repositories.GradeSummaryRepository
import io.github.wulkanowy.data.repositories.NoteRepository
import io.github.wulkanowy.data.repositories.PreferencesRepository
import io.github.wulkanowy.data.repositories.SessionRepository
import io.github.wulkanowy.data.repositories.TimetableRepository
import io.github.wulkanowy.services.notification.GradeNotification
import io.github.wulkanowy.services.notification.NoteNotification
import io.github.wulkanowy.utils.friday
import io.github.wulkanowy.utils.isHolidays
import io.github.wulkanowy.utils.monday
@ -40,6 +42,9 @@ class SyncWorker : SimpleJobService() {
@Inject
lateinit var timetable: TimetableRepository
@Inject
lateinit var note: NoteRepository
@Inject
lateinit var prefRepository: PreferencesRepository
@ -73,7 +78,8 @@ class SyncWorker : SimpleJobService() {
gradesSummary.getGradesSummary(it, true),
attendance.getAttendance(it, start, end, true),
exam.getExams(it, start, end, true),
timetable.getTimetable(it, start, end, true)
timetable.getTimetable(it, start, end, true),
note.getNotes(it, true, true)
)
)
}
@ -90,7 +96,12 @@ class SyncWorker : SimpleJobService() {
}
private fun sendNotifications() {
disposable.add(session.getSemesters(true)
sendGradeNotifications()
sendNoteNotification()
}
private fun sendGradeNotifications() {
disposable.add(session.getSemesters()
.map { it.single { semester -> semester.current } }
.flatMap { gradesDetails.getNewGrades(it) }
.map { it.filter { grade -> !grade.isNotified } }
@ -103,6 +114,20 @@ class SyncWorker : SimpleJobService() {
}) { Timber.e("Notifications sending failed") })
}
private fun sendNoteNotification() {
disposable.add(session.getSemesters()
.map { it.single { semester -> semester.current } }
.flatMap { note.getNewNotes(it) }
.map { it.filter { note -> !note.isNotified } }
.subscribe({
if (it.isNotEmpty()) {
Timber.d("Found ${it.size} unread notes")
NoteNotification(applicationContext).sendNotification(it)
note.updateNotes(it.map { note -> note.apply { isNotified = true } }).subscribe()
}
}) { Timber.e("Notifications sending failed") })
}
override fun onDestroy() {
super.onDestroy()
disposable.clear()

View File

@ -0,0 +1,58 @@
package io.github.wulkanowy.services.notification
import android.annotation.TargetApi
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.ui.modules.main.MainActivity
import timber.log.Timber
class NoteNotification(context: Context) : BaseNotification(context) {
private val channelId = "Note_Notify"
@TargetApi(26)
override fun createChannel(channelId: String) {
notificationManager.createNotificationChannel(NotificationChannel(
channelId, context.getString(R.string.notify_note_channel), NotificationManager.IMPORTANCE_HIGH
).apply {
enableLights(true)
enableVibration(true)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
})
}
fun sendNotification(items: List<Note>) {
notify(notificationBuilder(channelId)
.setContentTitle(context.resources.getQuantityString(R.plurals.note_new_items, items.size, items.size))
.setContentText(context.resources.getQuantityString(R.plurals.notify_note_new_items, items.size, items.size))
.setSmallIcon(R.drawable.ic_stat_notify_note)
.setAutoCancel(true)
.setDefaults(NotificationCompat.DEFAULT_ALL)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setColor(ContextCompat.getColor(context, R.color.colorPrimary))
.setContentIntent(
PendingIntent.getActivity(context, 0,
MainActivity.getStartIntent(context).putExtra(MainActivity.EXTRA_START_MENU_INDEX, 4),
PendingIntent.FLAG_UPDATE_CURRENT
)
)
.setStyle(NotificationCompat.InboxStyle().run {
setSummaryText(context.resources.getQuantityString(R.plurals.note_number_item, items.size, items.size))
items.forEach {
addLine("${it.teacher}: ${it.category}")
}
this
})
.build()
)
Timber.d("Notification sent")
}
}

View File

@ -18,7 +18,6 @@ import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.more.MoreFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
import io.github.wulkanowy.utils.getThemeAttrColor
import io.github.wulkanowy.utils.logLogin
import io.github.wulkanowy.utils.safelyPopFragment
import io.github.wulkanowy.utils.setOnViewChangeListener
import kotlinx.android.synthetic.main.activity_main.*

View File

@ -14,6 +14,7 @@ import io.github.wulkanowy.ui.modules.exam.ExamFragment
import io.github.wulkanowy.ui.modules.grade.GradeFragment
import io.github.wulkanowy.ui.modules.grade.GradeModule
import io.github.wulkanowy.ui.modules.more.MoreFragment
import io.github.wulkanowy.ui.modules.note.NoteFragment
import io.github.wulkanowy.ui.modules.settings.SettingsFragment
import io.github.wulkanowy.ui.modules.timetable.TimetableFragment
@ -57,4 +58,7 @@ abstract class MainModule {
@ContributesAndroidInjector
abstract fun bindSettingsFragment(): SettingsFragment
@ContributesAndroidInjector
abstract fun bindNoteFragment(): NoteFragment
}

View File

@ -25,6 +25,7 @@ class MainPresenter @Inject constructor(
when (initMenuIndex) {
1 -> logLogin("Grades")
3 -> logLogin("Timetable")
4 -> logLogin("More")
}
serviceHelper.startFullSyncService()

View File

@ -14,6 +14,7 @@ import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.about.AboutFragment
import io.github.wulkanowy.ui.modules.main.MainActivity
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.ui.modules.note.NoteFragment
import io.github.wulkanowy.ui.modules.settings.SettingsFragment
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_more.*
@ -34,11 +35,18 @@ class MoreFragment : BaseFragment(), MoreView, MainView.TitledView, MainView.Mai
override val titleStringId: Int
get() = R.string.more_title
override val noteRes: Pair<String, Drawable?>?
get() {
return context?.run {
getString(R.string.note_title) to ContextCompat.getDrawable(this, R.drawable.ic_menu_main_note_24dp)
}
}
override val settingsRes: Pair<String, Drawable?>?
get() {
return context?.run {
getString(R.string.settings_title) to
ContextCompat.getDrawable(this, R.drawable.ic_more_settings_24dp)
ContextCompat.getDrawable(this, R.drawable.ic_more_settings_24dp)
}
}
@ -46,7 +54,7 @@ class MoreFragment : BaseFragment(), MoreView, MainView.TitledView, MainView.Mai
get() {
return context?.run {
getString(R.string.about_title) to
ContextCompat.getDrawable(this, R.drawable.ic_more_about_24dp)
ContextCompat.getDrawable(this, R.drawable.ic_more_about_24dp)
}
}
@ -76,6 +84,10 @@ class MoreFragment : BaseFragment(), MoreView, MainView.TitledView, MainView.Mai
moreAdapter.updateDataSet(data)
}
override fun openNoteView() {
(activity as? MainActivity)?.pushView(NoteFragment.newInstance())
}
override fun openSettingsView() {
(activity as? MainActivity)?.pushView(SettingsFragment.newInstance())
}

View File

@ -5,8 +5,7 @@ import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.ui.base.BasePresenter
import javax.inject.Inject
class MorePresenter @Inject constructor(errorHandler: ErrorHandler)
: BasePresenter<MoreView>(errorHandler) {
class MorePresenter @Inject constructor(errorHandler: ErrorHandler) : BasePresenter<MoreView>(errorHandler) {
override fun onAttachView(view: MoreView) {
super.onAttachView(view)
@ -18,6 +17,7 @@ class MorePresenter @Inject constructor(errorHandler: ErrorHandler)
if (item is MoreItem) {
view?.run {
when (item.title) {
noteRes?.first -> openNoteView()
settingsRes?.first -> openSettingsView()
aboutRes?.first -> openAboutView()
}
@ -32,8 +32,10 @@ class MorePresenter @Inject constructor(errorHandler: ErrorHandler)
private fun loadData() {
view?.run {
updateData(listOfNotNull(
settingsRes?.let { MoreItem(it.first, it.second) },
aboutRes?.let { MoreItem(it.first, it.second) }))
noteRes?.let { MoreItem(it.first, it.second) },
settingsRes?.let { MoreItem(it.first, it.second) },
aboutRes?.let { MoreItem(it.first, it.second) })
)
}
}
}

View File

@ -5,6 +5,8 @@ import io.github.wulkanowy.ui.base.BaseView
interface MoreView : BaseView {
val noteRes: Pair<String, Drawable?>?
val settingsRes: Pair<String, Drawable?>?
val aboutRes: Pair<String, Drawable?>?
@ -18,4 +20,5 @@ interface MoreView : BaseView {
fun openAboutView()
fun popView()
fun openNoteView()
}

View File

@ -0,0 +1,48 @@
package io.github.wulkanowy.ui.modules.note
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.DialogFragment
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.synthetic.main.dialog_note.*
class NoteDialog : DialogFragment() {
private lateinit var note: Note
companion object {
private const val ARGUMENT_KEY = "Item"
fun newInstance(exam: Note): NoteDialog {
return NoteDialog().apply {
arguments = Bundle().apply { putSerializable(ARGUMENT_KEY, exam) }
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setStyle(STYLE_NO_TITLE, 0)
arguments?.run {
note = getSerializable(ARGUMENT_KEY) as Note
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.dialog_note, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
noteDialogDate.text = note.date.toFormattedString()
noteDialogCategory.text = note.category
noteDialogTeacher.text = note.teacher
noteDialogContent.text = note.content
noteDialogClose.setOnClickListener { dismiss() }
}
}

View File

@ -0,0 +1,95 @@
package io.github.wulkanowy.ui.modules.note
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.common.SmoothScrollLinearLayoutManager
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.ui.base.BaseFragment
import io.github.wulkanowy.ui.modules.main.MainView
import io.github.wulkanowy.utils.setOnItemClickListener
import kotlinx.android.synthetic.main.fragment_note.*
import javax.inject.Inject
class NoteFragment : BaseFragment(), NoteView, MainView.TitledView {
@Inject
lateinit var presenter: NotePresenter
@Inject
lateinit var noteAdapter: FlexibleAdapter<AbstractFlexibleItem<*>>
companion object {
fun newInstance() = NoteFragment()
}
override val titleStringId: Int
get() = R.string.note_title
override val isViewEmpty: Boolean
get() = noteAdapter.isEmpty
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.fragment_note, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
presenter.onAttachView(this)
}
override fun initView() {
noteAdapter.run {
setOnItemClickListener { presenter.onNoteItemSelected(getItem(it)) }
}
noteRecycler.run {
layoutManager = SmoothScrollLinearLayoutManager(context)
adapter = noteAdapter
}
noteSwipe.setOnRefreshListener { presenter.onSwipeRefresh() }
}
override fun showNoteDialog(note: Note) {
NoteDialog.newInstance(note).show(fragmentManager, note.toString())
}
override fun updateData(data: List<NoteItem>) {
noteAdapter.updateDataSet(data, true)
}
override fun updateItem(item: AbstractFlexibleItem<*>) {
noteAdapter.updateItem(item)
}
override fun clearData() {
noteAdapter.clear()
}
override fun showEmpty(show: Boolean) {
noteEmpty.visibility = if (show) VISIBLE else GONE
}
override fun showProgress(show: Boolean) {
noteProgress.visibility = if (show) VISIBLE else GONE
}
override fun showContent(show: Boolean) {
noteRecycler.visibility = if (show) VISIBLE else GONE
}
override fun hideRefresh() {
noteSwipe.isRefreshing = false
}
override fun onDestroyView() {
presenter.onDetachView()
super.onDestroyView()
}
}

View File

@ -0,0 +1,61 @@
package io.github.wulkanowy.ui.modules.note
import android.graphics.Typeface.BOLD
import android.graphics.Typeface.NORMAL
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import io.github.wulkanowy.R
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.utils.toFormattedString
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.item_note.*
class NoteItem(val note: Note) : AbstractFlexibleItem<NoteItem.ViewHolder>() {
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<*>>): NoteItem.ViewHolder {
return NoteItem.ViewHolder(view, adapter)
}
override fun getLayoutRes(): Int = R.layout.item_note
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<*>>,
holder: NoteItem.ViewHolder, position: Int, payloads: MutableList<Any>?
) {
holder.apply {
noteItemDate.apply {
text = note.date.toFormattedString()
setTypeface(null, if (note.isRead) NORMAL else BOLD)
}
noteItemType.apply {
text = note.category
setTypeface(null, if (note.isRead) NORMAL else BOLD)
}
noteItemTeacher.text = note.teacher
noteItemContent.text = note.content
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as NoteItem
if (note != other.note) return false
return true
}
override fun hashCode(): Int {
return note.hashCode()
}
class ViewHolder(val view: View, adapter: FlexibleAdapter<*>) : FlexibleViewHolder(view, adapter), LayoutContainer {
override val containerView: View
get() = contentView
}
}

View File

@ -0,0 +1,80 @@
package io.github.wulkanowy.ui.modules.note
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.ErrorHandler
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.data.repositories.NoteRepository
import io.github.wulkanowy.data.repositories.SessionRepository
import io.github.wulkanowy.ui.base.BasePresenter
import io.github.wulkanowy.utils.SchedulersProvider
import io.github.wulkanowy.utils.logEvent
import timber.log.Timber
import javax.inject.Inject
class NotePresenter @Inject constructor(
private val errorHandler: ErrorHandler,
private val schedulers: SchedulersProvider,
private val sessionRepository: SessionRepository,
private val noteRepository: NoteRepository
) : BasePresenter<NoteView>(errorHandler) {
override fun onAttachView(view: NoteView) {
super.onAttachView(view)
view.initView()
loadData()
}
fun onSwipeRefresh() {
loadData(true)
}
private fun loadData(forceRefresh: Boolean = false) {
disposable.add(sessionRepository.getSemesters()
.map { it.single { semester -> semester.current } }
.flatMap { noteRepository.getNotes(it, forceRefresh) }
.map { items -> items.map { NoteItem(it) } }
.map { items -> items.sortedByDescending { it.note.date } }
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.doFinally {
view?.run {
hideRefresh()
showProgress(false)
}
}.subscribe({
view?.apply {
updateData(it)
showEmpty(it.isEmpty())
showContent(it.isNotEmpty())
}
logEvent("Note load", mapOf("items" to it.size, "forceRefresh" to forceRefresh))
}, {
view?.run { showEmpty(isViewEmpty) }
errorHandler.proceed(it)
})
)
}
fun onNoteItemSelected(item: AbstractFlexibleItem<*>?) {
if (item is NoteItem) {
view?.run {
showNoteDialog(item.note)
if (!item.note.isRead) {
item.note.isRead = true
updateItem(item)
updateNote(item.note)
}
}
}
}
private fun updateNote(note: Note) {
disposable.add(noteRepository.updateNote(note)
.subscribeOn(schedulers.backgroundThread)
.observeOn(schedulers.mainThread)
.subscribe({
Timber.d("Note ${note.id} updated")
}) { error -> errorHandler.proceed(error) }
)
}
}

View File

@ -0,0 +1,28 @@
package io.github.wulkanowy.ui.modules.note
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import io.github.wulkanowy.data.db.entities.Note
import io.github.wulkanowy.ui.base.BaseView
interface NoteView : BaseView {
val isViewEmpty: Boolean
fun initView()
fun updateData(data: List<NoteItem>)
fun updateItem(item: AbstractFlexibleItem<*>)
fun clearData()
fun showEmpty(show: Boolean)
fun showProgress(show: Boolean)
fun showContent(show: Boolean)
fun hideRefresh()
fun showNoteDialog(note: Note)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 414 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 554 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,10 @@
<!--https://materialdesignicons.com/icon/trophy-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M20.2,2H19.5H18C17.1,2 16,3 16,4H8C8,3 6.9,2 6,2H4.5H3.8H2V11C2,12 3,13 4,13H6.2C6.6,15 7.9,16.7 11,17V19.1C8.8,19.3 8,20.4 8,21.7V22H16V21.7C16,20.4 15.2,19.3 13,19.1V17C16.1,16.7 17.4,15 17.8,13H20C21,13 22,12 22,11V2H20.2M4,11V4H6V6V11C5.1,11 4.3,11 4,11M20,11C19.7,11 18.9,11 18,11V6V4H20V11Z" />
</vector>

View File

@ -1,8 +1,8 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportHeight="24"
android:viewportWidth="24">
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M14 14H7v2h7m5 3H5V8h14m0-5h-1V1h-2v2H8V1H6v2H5a2

View File

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="300dp"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/note_dialog_details"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:layout_marginBottom="20dp"
android:text="@string/all_details"
android:textSize="20sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/all_category"
android:textSize="17sp" />
<TextView
android:id="@+id/noteDialogCategory"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="12sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/all_teacher"
android:textSize="17sp" />
<TextView
android:id="@+id/noteDialogTeacher"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="12sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/all_date"
android:textSize="17sp" />
<TextView
android:id="@+id/noteDialogDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="12sp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:text="@string/note_content"
android:textSize="17sp" />
<TextView
android:id="@+id/noteDialogContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="3dp"
android:lineSpacingMultiplier="1.2"
android:text="@string/all_no_data"
android:textIsSelectable="true"
android:textSize="12sp" />
<Button
android:id="@+id/noteDialogClose"
style="@style/Widget.AppCompat.Button.Borderless"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginTop="15dp"
android:text="@string/all_close"
android:textAllCaps="true"
android:textSize="15sp" />
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,51 @@
<FrameLayout 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"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/noteProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true" />
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/noteSwipe"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/noteRecycler"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
<LinearLayout
android:id="@+id/noteEmpty"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical"
android:padding="10dp"
android:visibility="gone">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minWidth="100dp"
android:minHeight="100dp"
app:srcCompat="@drawable/ic_menu_main_note_24dp"
app:tint="?android:attr/textColorPrimary"
tools:ignore="contentDescription" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:gravity="center"
android:text="@string/note_no_items"
android:textSize="20sp" />
</LinearLayout>
</FrameLayout>

View File

@ -0,0 +1,60 @@
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/note_subitem_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/ic_all_divider"
android:foreground="?attr/selectableItemBackgroundBorderless">
<TextView
android:id="@+id/noteItemDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="15dp"
android:layout_marginLeft="15dp"
android:layout_marginTop="10dp"
android:text="@string/all_date"
android:textSize="15sp" />
<TextView
android:id="@+id/noteItemTeacher"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_marginStart="10dp"
android:layout_marginLeft="10dp"
android:layout_marginTop="10dp"
android:layout_marginEnd="15dp"
android:layout_marginRight="15dp"
android:layout_toEndOf="@id/noteItemDate"
android:layout_toRightOf="@id/noteItemDate"
android:gravity="end"
android:text="@string/all_teacher"
android:textSize="13sp" />
<TextView
android:id="@+id/noteItemType"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/noteItemDate"
android:layout_alignStart="@id/noteItemDate"
android:layout_alignLeft="@id/noteItemDate"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:text="@string/exam_type"
android:textSize="13sp" />
<TextView
android:id="@+id/noteItemContent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/noteItemType"
android:layout_alignStart="@id/noteItemDate"
android:layout_alignLeft="@id/noteItemDate"
android:layout_marginEnd="15dp"
android:layout_marginRight="15dp"
android:layout_marginBottom="15dp"
android:lineSpacingMultiplier="1.2"
android:text="@string/all_description"
android:textSize="14sp" />
</RelativeLayout>

View File

@ -11,6 +11,7 @@
<string name="settings_title">Ustawienia</string>
<string name="more_title">Więcej</string>
<string name="about_title">O aplikacji</string>
<string name="note_title">Uwagi i osiągnięcia</string>
<!--Login form-->
@ -72,6 +73,16 @@
</plurals>
<!--Note notify-->
<string name="notify_note_channel">Nowe uwagi</string>
<plurals name="notify_note_new_items">
<item quantity="one">Dostałeś %1$d uwagę</item>
<item quantity="few">"Dostałeś %1$d uwagi</item>
<item quantity="many">Dostałeś %1$d uwag</item>
<item quantity="other">Dostałeś %1$d uwag</item>
</plurals>
<!--Timetable-->
<string name="timetable_lesson">Lekcja</string>
<string name="timetable_room">Sala</string>
@ -107,6 +118,24 @@
<string name="about_source_code">Kod źródłowy</string>
<string name="about_feedback">Zgłoś błąd</string>
<!--Note-->
<string name="note_no_items">Brak informacji o uwagach</string>
<string name="note_content">Treść</string>
<plurals name="note_number_item">
<item quantity="one">%d uwaga</item>
<item quantity="few">%d uwagi</item>
<item quantity="many">%d uwag</item>
<item quantity="other">%d uwag</item>
</plurals>
<plurals name="note_new_items">
<item quantity="one">Nowa uwaga</item>
<item quantity="few">Nowe uwagi</item>
<item quantity="many">Nowych uwag</item>
<item quantity="other">Nowych uwag</item>
</plurals>
<!--Generic-->
<string name="all_description">Opis</string>
<string name="all_no_description">Brak opisu</string>
@ -114,6 +143,7 @@
<string name="all_date">Data</string>
<string name="all_color">Kolor</string>
<string name="all_details">Szczegóły</string>
<string name="all_category">Kategoria</string>
<string name="all_close">Zamknij</string>
<string name="all_cancel">Anuluj</string>
<string name="all_no_data">Brak danych</string>

View File

@ -11,6 +11,7 @@
<string name="settings_title">Settings</string>
<string name="more_title">More</string>
<string name="about_title">About</string>
<string name="note_title">Notes and achievements</string>
<!--Login form-->
@ -64,6 +65,13 @@
<item quantity="other">You received %1$d grades</item>
</plurals>
<!--Note notify-->
<string name="notify_note_channel">New notes</string>
<plurals name="notify_note_new_items">
<item quantity="one">You received %1$d note</item>
<item quantity="other">You received %1$d notes</item>
</plurals>
<!--Timetable-->
<string name="timetable_lesson">Lesson</string>
@ -99,6 +107,19 @@
<string name="about_source_code">Source code</string>
<string name="about_feedback">Report a bug</string>
<!--Note-->
<string name="note_no_items">No info about notes</string>
<string name="note_content">Content</string>
<plurals name="note_number_item">
<item quantity="one">%d note</item>
<item quantity="other">%d notes</item>
</plurals>
<plurals name="note_new_items">
<item quantity="one">New note</item>
<item quantity="other">New notes</item>
</plurals>
<!--Generic-->
<string name="all_description">Description</string>
@ -107,6 +128,7 @@
<string name="all_date">Date</string>
<string name="all_color">Color</string>
<string name="all_details">Details</string>
<string name="all_category">Category</string>
<string name="all_close">Close</string>
<string name="all_cancel">Cancel</string>
<string name="all_no_data">No data</string>