Compare commits

...

13 Commits

Author SHA1 Message Date
da48c059ec [4.0-rc.4] Update build.gradle, signing and changelog. 2020-03-30 23:29:34 +02:00
ee5566d1ef [Events] Add toast hint to mark as done button. 2020-03-30 23:28:50 +02:00
b794b30346 [UI] Fix disabling pull to refresh when changing page using tab layout. 2020-03-30 23:16:35 +02:00
0db6393bb0 [Events] Add showing green check when event is done. Hide done events from homework current list. 2020-03-30 23:02:19 +02:00
fcc3c55110 [Events] Fix preserving isDone value. Improve DataRemoveModel method of keeping items. 2020-03-30 22:37:48 +02:00
328c07eaf4 [Messages] Fix Librus attachment downloading. Add option to force (re)download an attachment. 2020-03-30 19:48:56 +02:00
b004ec048e [UI] Refactor Grades, Notifications, Homework fragments to better match unified templates. 2020-03-30 18:55:28 +02:00
b9f83875a0 [Event] Add isDone attribute and marking events as done. 2020-03-30 18:19:19 +02:00
8c869d082b [UI] Add pager fragment templates. Move all templates to 'template' module. Fix swipe to refresh with pager fragments. 2020-03-30 12:50:21 +02:00
043f8210ba [UI] Add lazy loading to fragments with view pager. 2020-03-29 23:11:17 +02:00
41a79caf83 [API/Mobidziennik] Change data remove model to include only possible types. 2020-03-29 21:06:39 +02:00
0427fa6087 [Events] Add support for selective updates and upserting. 2020-03-29 18:05:56 +02:00
2f3c912dbe [Config] Disable teacher absence notifications by default. Add missing migration values. 2020-03-29 16:27:05 +02:00
82 changed files with 2016 additions and 667 deletions

1
annotation/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

29
annotation/build.gradle Normal file
View File

@ -0,0 +1,29 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-28.
*/
apply plugin: 'java-library'
apply plugin: 'kotlin'
apply plugin: 'kotlin-kapt'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
}
sourceCompatibility = "7"
targetCompatibility = "7"
repositories {
mavenCentral()
}
compileKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}
compileTestKotlin {
kotlinOptions {
jvmTarget = "1.8"
}
}

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-28.
*/
package pl.szczodrzynski.edziennik.annotation
import kotlin.reflect.KClass
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class SelectiveDao(
val db: KClass<*>
)

View File

@ -0,0 +1,13 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-28.
*/
package pl.szczodrzynski.edziennik.annotation
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
@MustBeDocumented
annotation class UpdateSelective(
val primaryKeys: Array<String>,
val skippedColumns: Array<String> = []
)

View File

@ -193,6 +193,9 @@ dependencies {
implementation "io.coil-kt:coil:0.9.2" implementation "io.coil-kt:coil:0.9.2"
implementation 'com.github.kuba2k2:NumberSlidingPicker:2921225f76' implementation 'com.github.kuba2k2:NumberSlidingPicker:2921225f76'
implementation project(":annotation")
kapt project(":codegen")
} }
repositories { repositories {
mavenCentral() mavenCentral()

View File

@ -1,4 +1,10 @@
<h3>Wersja 4.0-rc.3, 2020-03-29</h3> <h3>Wersja 4.0-rc.4, 2020-03-30</h3>
<ul>
<li>Poprawione pobieranie załączników w Librusie.</li>
<li>Nowy widok zadań domowych</li>
<li>Możliwość oznaczania zadań domowych i wydarzeń jako wykonane.</li>
</ul>
<!--<h3>Wersja 4.0-rc.3, 2020-03-29</h3>
<ul> <ul>
<li><b><u>Wysyłanie wiadomości</u></b> - funkcja, na którą czekał każdy. Od teraz w Szkolnym można wysyłać oraz odpowiadać na wiadomości do nauczycieli &#x1F44F;</li> <li><b><u>Wysyłanie wiadomości</u></b> - funkcja, na którą czekał każdy. Od teraz w Szkolnym można wysyłać oraz odpowiadać na wiadomości do nauczycieli &#x1F44F;</li>
<li><b>Przebudowaliśmy cały moduł synchronizacji</b>, co oznacza większą stabilność aplikacji, szybkość oraz poprawność pobieranych danych</li> <li><b>Przebudowaliśmy cały moduł synchronizacji</b>, co oznacza większą stabilność aplikacji, szybkość oraz poprawność pobieranych danych</li>
@ -22,7 +28,7 @@
<li>Poprawiliśmy synchronizację w tle na niektórych telefonach</li> <li>Poprawiliśmy synchronizację w tle na niektórych telefonach</li>
<li>Usunąłem denerwujący brak zaznaczenia w lewym menu</li> <li>Usunąłem denerwujący brak zaznaczenia w lewym menu</li>
<li>Znaczna ilość błędów z poprzednich wersji już nie występuje</li> <li>Znaczna ilość błędów z poprzednich wersji już nie występuje</li>
</ul> </ul>-->
<br> <br>
<br> <br>
Dzięki za korzystanie ze Szkolnego!<br> Dzięki za korzystanie ze Szkolnego!<br>

View File

@ -9,7 +9,7 @@
/*secret password - removed for source code publication*/ /*secret password - removed for source code publication*/
static toys AES_IV[16] = { static toys AES_IV[16] = {
0xa7, 0x84, 0xf9, 0xdc, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }; 0x1c, 0x15, 0x0f, 0x1c, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff };
unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat); unsigned char *agony(unsigned int laugh, unsigned char *box, unsigned char *heat);

View File

@ -40,6 +40,9 @@ import androidx.core.util.forEach
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.viewpager.widget.ViewPager
import com.google.android.gms.security.ProviderInstaller import com.google.android.gms.security.ProviderInstaller
import com.google.gson.JsonArray import com.google.gson.JsonArray
import com.google.gson.JsonElement import com.google.gson.JsonElement
@ -141,6 +144,10 @@ fun CharSequence?.isNotNullNorEmpty(): Boolean {
return this != null && this.isNotEmpty() return this != null && this.isNotEmpty()
} }
fun <T> Collection<T>?.isNotNullNorEmpty(): Boolean {
return this != null && this.isNotEmpty()
}
fun CharSequence?.isNotNullNorBlank(): Boolean { fun CharSequence?.isNotNullNorBlank(): Boolean {
return this != null && this.isNotBlank() return this != null && this.isNotBlank()
} }
@ -722,6 +729,13 @@ inline fun <T : View> T.onClick(crossinline onClickListener: (v: T) -> Unit) {
} }
} }
@Suppress("UNCHECKED_CAST")
inline fun <T : View> T.onLongClick(crossinline onLongClickListener: (v: T) -> Boolean) {
setOnLongClickListener { v: View ->
onLongClickListener(v as T)
}
}
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
inline fun <T : CompoundButton> T.onChange(crossinline onChangeListener: (v: T, isChecked: Boolean) -> Unit) { inline fun <T : CompoundButton> T.onChange(crossinline onChangeListener: (v: T, isChecked: Boolean) -> Unit) {
setOnCheckedChangeListener { buttonView, isChecked -> setOnCheckedChangeListener { buttonView, isChecked ->
@ -1167,3 +1181,19 @@ fun TextView.getTextPosition(range: IntRange): Rect {
return parentTextViewRect return parentTextViewRect
} }
inline fun ViewPager.addOnPageSelectedListener(crossinline block: (position: Int) -> Unit) = addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
override fun onPageSelected(position: Int) { block(position) }
})
val SwipeRefreshLayout.onScrollListener: RecyclerView.OnScrollListener
get() = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (recyclerView.canScrollVertically(-1))
this@onScrollListener.isEnabled = false
if (!recyclerView.canScrollVertically(-1) && newState == RecyclerView.SCROLL_STATE_IDLE)
this@onScrollListener.isEnabled = true
}
}

View File

@ -61,7 +61,7 @@ import pl.szczodrzynski.edziennik.ui.modules.behaviour.BehaviourFragment
import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar import pl.szczodrzynski.edziennik.ui.modules.error.ErrorSnackbar
import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackFragment import pl.szczodrzynski.edziennik.ui.modules.feedback.FeedbackFragment
import pl.szczodrzynski.edziennik.ui.modules.feedback.HelpFragment import pl.szczodrzynski.edziennik.ui.modules.feedback.HelpFragment
import pl.szczodrzynski.edziennik.ui.modules.grades.GradesFragment import pl.szczodrzynski.edziennik.ui.modules.grades.GradesListFragment
import pl.szczodrzynski.edziennik.ui.modules.grades.editor.GradesEditorFragment import pl.szczodrzynski.edziennik.ui.modules.grades.editor.GradesEditorFragment
import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment import pl.szczodrzynski.edziennik.ui.modules.home.HomeFragment
import pl.szczodrzynski.edziennik.ui.modules.homework.HomeworkFragment import pl.szczodrzynski.edziennik.ui.modules.homework.HomeworkFragment
@ -70,9 +70,10 @@ import pl.szczodrzynski.edziennik.ui.modules.messages.MessageFragment
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesComposeFragment import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesComposeFragment
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesListFragment import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesListFragment
import pl.szczodrzynski.edziennik.ui.modules.notifications.NotificationsFragment import pl.szczodrzynski.edziennik.ui.modules.notifications.NotificationsListFragment
import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsNewFragment import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsNewFragment
import pl.szczodrzynski.edziennik.ui.modules.template.TemplateFragment
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment
import pl.szczodrzynski.edziennik.ui.modules.webpush.WebPushFragment import pl.szczodrzynski.edziennik.ui.modules.webpush.WebPushFragment
import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoTouch import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoTouch
@ -129,6 +130,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
const val TARGET_MESSAGES_DETAILS = 503 const val TARGET_MESSAGES_DETAILS = 503
const val TARGET_MESSAGES_COMPOSE = 504 const val TARGET_MESSAGES_COMPOSE = 504
const val TARGET_WEB_PUSH = 140 const val TARGET_WEB_PUSH = 140
const val TARGET_TEMPLATE = 1000
const val HOME_ID = DRAWER_ITEM_HOME const val HOME_ID = DRAWER_ITEM_HOME
@ -153,7 +155,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
.withBadgeTypeId(TYPE_EVENT) .withBadgeTypeId(TYPE_EVENT)
.isInDrawer(true) .isInDrawer(true)
list += NavTarget(DRAWER_ITEM_GRADES, R.string.menu_grades, GradesFragment::class) list += NavTarget(DRAWER_ITEM_GRADES, R.string.menu_grades, GradesListFragment::class)
.withIcon(CommunityMaterial.Icon2.cmd_numeric_5_box_outline) .withIcon(CommunityMaterial.Icon2.cmd_numeric_5_box_outline)
.withBadgeTypeId(TYPE_GRADE) .withBadgeTypeId(TYPE_GRADE)
.isInDrawer(true) .isInDrawer(true)
@ -185,7 +187,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
// static drawer items // static drawer items
list += NavTarget(DRAWER_ITEM_NOTIFICATIONS, R.string.menu_notifications, NotificationsFragment::class) list += NavTarget(DRAWER_ITEM_NOTIFICATIONS, R.string.menu_notifications, NotificationsListFragment::class)
.withIcon(CommunityMaterial.Icon.cmd_bell_ring_outline) .withIcon(CommunityMaterial.Icon.cmd_bell_ring_outline)
.isInDrawer(true) .isInDrawer(true)
.isStatic(true) .isStatic(true)
@ -227,6 +229,13 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
list += NavTarget(TARGET_MESSAGES_COMPOSE, R.string.menu_message_compose, MessagesComposeFragment::class) list += NavTarget(TARGET_MESSAGES_COMPOSE, R.string.menu_message_compose, MessagesComposeFragment::class)
list += NavTarget(TARGET_WEB_PUSH, R.string.menu_web_push, WebPushFragment::class) list += NavTarget(TARGET_WEB_PUSH, R.string.menu_web_push, WebPushFragment::class)
list += NavTarget(DRAWER_ITEM_DEBUG, R.string.menu_debug, DebugFragment::class) list += NavTarget(DRAWER_ITEM_DEBUG, R.string.menu_debug, DebugFragment::class)
if (App.devMode) {
list += NavTarget(TARGET_TEMPLATE, R.string.menu_template, TemplateFragment::class)
.withIcon(CommunityMaterial.Icon2.cmd_test_tube_empty)
.isInDrawer(true)
.isBelowSeparator(true)
.isStatic(true)
}
list list
} }
@ -1068,6 +1077,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
.also { if (target.icon != null) it.withIcon(target.icon!!) } .also { if (target.icon != null) it.withIcon(target.icon!!) }
.also { if (target.title != null) it.withAppTitle(getString(target.title!!)) } .also { if (target.title != null) it.withAppTitle(getString(target.title!!)) }
.also { if (target.badgeTypeId != null) it.withBadgeStyle(drawer.badgeStyle)} .also { if (target.badgeTypeId != null) it.withBadgeStyle(drawer.badgeStyle)}
.withSelectedBackgroundAnimated(false)
if (target.badgeTypeId != null) if (target.badgeTypeId != null)
drawer.addUnreadCounterType(target.badgeTypeId!!, target.id) drawer.addUnreadCounterType(target.badgeTypeId!!, target.id)

View File

@ -22,7 +22,7 @@ import kotlin.coroutines.CoroutineContext
class Config(val db: AppDb) : CoroutineScope, AbstractConfig { class Config(val db: AppDb) : CoroutineScope, AbstractConfig {
companion object { companion object {
const val DATA_VERSION = 11 const val DATA_VERSION = 12
} }
private val job = Job() private val job = Job()

View File

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

View File

@ -64,11 +64,25 @@ class ConfigMigration(app: App, config: Config) {
dataVersion = 2 dataVersion = 2
} }
if (dataVersion < 3) {
update = null
privacyPolicyAccepted = false
debugMode = false
devModePassword = null
appInstalledTime = 0L
appRateSnackbarTime = 0L
dataVersion = 3
}
if (dataVersion < 10) { if (dataVersion < 10) {
ui.openDrawerOnBackPressed = false ui.openDrawerOnBackPressed = false
ui.snowfall = false ui.snowfall = false
ui.bottomSheetOpened = false ui.bottomSheetOpened = false
sync.dontShowAppManagerDialog = false sync.dontShowAppManagerDialog = false
sync.webPushEnabled = true
sync.lastAppSync = 0L
dataVersion = 10 dataVersion = 10
} }

View File

@ -5,6 +5,7 @@
package pl.szczodrzynski.edziennik.config.utils package pl.szczodrzynski.edziennik.config.utils
import pl.szczodrzynski.edziennik.config.ProfileConfig import pl.szczodrzynski.edziennik.config.ProfileConfig
import pl.szczodrzynski.edziennik.data.db.entity.Notification
import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.AGENDA_DEFAULT import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.AGENDA_DEFAULT
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.COLOR_MODE_WEIGHTED
import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_ALL_GRADES import pl.szczodrzynski.edziennik.utils.managers.GradesManager.Companion.YEAR_ALL_GRADES
@ -14,11 +15,23 @@ class ProfileConfigMigration(config: ProfileConfig) {
if (dataVersion < 1) { if (dataVersion < 1) {
grades.colorMode = COLOR_MODE_WEIGHTED grades.colorMode = COLOR_MODE_WEIGHTED
grades.dontCountEnabled = false
grades.yearAverageMode = YEAR_ALL_GRADES grades.yearAverageMode = YEAR_ALL_GRADES
grades.hideImproved = false
grades.averageWithoutWeight = true
grades.plusValue = null
grades.minusValue = null
grades.dontCountEnabled = false
grades.dontCountGrades = listOf()
ui.agendaViewType = AGENDA_DEFAULT ui.agendaViewType = AGENDA_DEFAULT
// no migration for ui.homeCards
dataVersion = 1 dataVersion = 1
} }
if (dataVersion < 2) {
sync.notificationFilter = sync.notificationFilter + Notification.TYPE_TEACHER_ABSENCE
dataVersion = 2
}
}} }}
} }

View File

@ -54,7 +54,7 @@ class LibrusMessagesGetAttachment(override val data: DataLibrus,
downloadAttachment("${LIBRUS_SANDBOX_URL}CSDownload&singleUseKey=$attachmentKey", method = POST) downloadAttachment("${LIBRUS_SANDBOX_URL}CSDownload&singleUseKey=$attachmentKey", method = POST)
} }
} else { } else {
downloadAttachment(downloadLink, method = GET) downloadAttachment("$downloadLink/get", method = GET)
} }
} }
} }

View File

@ -75,6 +75,8 @@ class MobidziennikApiEvents(val data: DataMobidziennik, rows: List<String>) {
} }
} }
data.toRemove.add(DataRemoveModel.Events.futureExceptType(Event.TYPE_HOMEWORK)) data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_DEFAULT))
data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_EXAM))
data.toRemove.add(DataRemoveModel.Events.futureWithType(Event.TYPE_SHORT_QUIZ))
} }
} }

View File

@ -284,7 +284,7 @@ abstract class Data(val app: App, val profile: Profile?, val loginStore: LoginSt
db.gradeDao().addAll(gradeList) db.gradeDao().addAll(gradeList)
} }
if (eventList.isNotEmpty()) { if (eventList.isNotEmpty()) {
db.eventDao().addAll(eventList) db.eventDao().upsertAll(eventList, removeNotKept = true)
} }
if (noticeList.isNotEmpty()) { if (noticeList.isNotEmpty()) {
db.noticeDao().clear(profile.id) db.noticeDao().clear(profile.id)

View File

@ -56,9 +56,9 @@ open class DataRemoveModel {
} }
fun commit(profileId: Int, dao: EventDao) { fun commit(profileId: Int, dao: EventDao) {
type?.let { dao.removeFutureWithType(profileId, Date.getToday(), it) } type?.let { dao.dontKeepFutureWithType(profileId, Date.getToday(), it) }
exceptType?.let { dao.removeFutureExceptType(profileId, Date.getToday(), it) } exceptType?.let { dao.dontKeepFutureExceptType(profileId, Date.getToday(), it) }
exceptTypes?.let { dao.removeFutureExceptTypes(profileId, Date.getToday(), it) } exceptTypes?.let { dao.dontKeepFutureExceptTypes(profileId, Date.getToday(), it) }
} }
} }

View File

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

View File

@ -44,7 +44,7 @@ class AppSync(val app: App, val notifications: MutableList<Notification>, val pr
event.addedDate event.addedDate
) )
}) })
return app.db.eventDao().addAll(events).size return app.db.eventDao().upsertAll(events).size
} }
return 0; return 0;
} }

View File

@ -43,7 +43,7 @@ import pl.szczodrzynski.edziennik.data.db.migration.*
LibrusLesson::class, LibrusLesson::class,
TimetableManual::class, TimetableManual::class,
Metadata::class Metadata::class
], version = 81) ], version = 83)
@TypeConverters( @TypeConverters(
ConverterTime::class, ConverterTime::class,
ConverterDate::class, ConverterDate::class,
@ -166,7 +166,9 @@ abstract class AppDb : RoomDatabase() {
Migration78(), Migration78(),
Migration79(), Migration79(),
Migration80(), Migration80(),
Migration81() Migration81(),
Migration82(),
Migration83()
).allowMainThreadQueries().build() ).allowMainThreadQueries().build()
} }
} }

View File

@ -5,15 +5,13 @@
package pl.szczodrzynski.edziennik.data.db.dao package pl.szczodrzynski.edziennik.data.db.dao
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.room.Dao import androidx.room.*
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.RawQuery
import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery
import pl.szczodrzynski.edziennik.data.db.entity.Keepable
@Dao @Dao
interface BaseDao<T, F> { interface BaseDao<T : Keepable, F : T> {
@RawQuery @RawQuery
fun getRaw(query: SupportSQLiteQuery): LiveData<List<F>> fun getRaw(query: SupportSQLiteQuery): LiveData<List<F>>
fun getRaw(query: String) = getRaw(SimpleSQLiteQuery(query)) fun getRaw(query: String) = getRaw(SimpleSQLiteQuery(query))
@ -24,11 +22,79 @@ interface BaseDao<T, F> {
fun getOneNow(query: SupportSQLiteQuery): F? fun getOneNow(query: SupportSQLiteQuery): F?
fun getOneNow(query: String) = getOneNow(SimpleSQLiteQuery(query)) fun getOneNow(query: String) = getOneNow(SimpleSQLiteQuery(query))
@Insert(onConflict = OnConflictStrategy.REPLACE) @Query("DELETE FROM events WHERE keep = 0")
fun add(item: T): Long fun removeNotKept()
@Insert(onConflict = OnConflictStrategy.REPLACE) /**
* INSERT an [item] into the database,
* ignoring any conflicts.
*/
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun add(item: T): Long
/**
* INSERT [items] into the database,
* ignoring any conflicts.
*/
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun addAll(items: List<T>): LongArray fun addAll(items: List<T>): LongArray
/**
* REPLACE an [item] in the database,
* removing any conflicting rows.
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun replace(item: T)
/**
* REPLACE [items] in the database,
* removing any conflicting rows.
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun replaceAll(items: List<T>)
/**
* Selective UPDATE an [item] in the database.
* Do nothing if a matching item does not exist.
*/
fun update(item: T): Long
/**
* Selective UPDATE [items] in the database.
* Do nothing for those items which do not exist.
*/
fun updateAll(items: List<T>): LongArray
/**
* Remove all items from the database,
* that match the given [profileId].
*/
fun clear(profileId: Int) fun clear(profileId: Int)
/**
* INSERT an [item] into the database,
* doing a selective [update] on conflicts.
* @return the newly inserted item's ID or -1L if the item was updated instead
*/
@Transaction
fun upsert(item: T): Long {
val id = add(item)
if (id == -1L) update(item)
return id
}
/**
* INSERT [items] into the database,
* doing a selective [update] on conflicts.
* @return a [LongArray] of IDs of newly inserted items or -1L if the item existed before
*/
@Transaction
fun upsertAll(items: List<T>, removeNotKept: Boolean = false): LongArray {
val insertResult = addAll(items)
val updateList = mutableListOf<T>()
insertResult.forEachIndexed { index, result ->
if (result == -1L) updateList.add(items[index])
}
if (updateList.isNotEmpty()) updateAll(items)
if (removeNotKept) removeNotKept()
return insertResult
}
} }

View File

@ -10,6 +10,10 @@ import androidx.room.RawQuery
import androidx.room.Transaction import androidx.room.Transaction
import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery import androidx.sqlite.db.SupportSQLiteQuery
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.annotation.SelectiveDao
import pl.szczodrzynski.edziennik.annotation.UpdateSelective
import pl.szczodrzynski.edziennik.data.db.AppDb
import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.EventFull
@ -17,6 +21,7 @@ import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Time import pl.szczodrzynski.edziennik.utils.models.Time
@Dao @Dao
@SelectiveDao(db = AppDb::class)
abstract class EventDao : BaseDao<Event, EventFull> { abstract class EventDao : BaseDao<Event, EventFull> {
companion object { companion object {
private const val QUERY = """ private const val QUERY = """
@ -35,19 +40,91 @@ abstract class EventDao : BaseDao<Event, EventFull> {
private const val ORDER_BY = """GROUP BY eventId ORDER BY eventDate, eventTime, addedDate ASC""" private const val ORDER_BY = """GROUP BY eventId ORDER BY eventDate, eventTime, addedDate ASC"""
private const val NOT_BLACKLISTED = """events.eventBlacklisted = 0""" private const val NOT_BLACKLISTED = """events.eventBlacklisted = 0"""
private const val NOT_DONE = """events.eventIsDone = 0"""
} }
//abstract fun queryRaw(query: SupportSQLiteQuery) private val selective by lazy { EventDaoSelective(App.db) }
//private fun queryRaw(query: String) = queryRaw(SimpleSQLiteQuery(query))
@RawQuery(observedEntities = [Event::class])
abstract override fun getRaw(query: SupportSQLiteQuery): LiveData<List<EventFull>>
// SELECTIVE UPDATE
@UpdateSelective(primaryKeys = ["profileId", "eventId"], skippedColumns = ["eventIsDone", "eventBlacklisted", "homeworkBody", "attachmentIds", "attachmentNames"])
override fun update(item: Event) = selective.update(item)
override fun updateAll(items: List<Event>) = selective.updateAll(items)
// CLEAR
@Query("DELETE FROM events WHERE profileId = :profileId") @Query("DELETE FROM events WHERE profileId = :profileId")
abstract override fun clear(profileId: Int) abstract override fun clear(profileId: Int)
/*fun update(event: Event) = // GET ALL - LIVE DATA
queryRaw("""UPDATE events SET fun getAll(profileId: Int) =
eventDate = '${event.date.stringY_m_d}', getRaw("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId $ORDER_BY")
eventTime = ${event.time?.stringValue}, fun getAllByType(profileId: Int, type: Long, filter: String = "1") =
eventTopic = '${event.topic}'""")*/ getRaw("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId AND eventType = $type AND $filter $ORDER_BY")
fun getAllByDate(profileId: Int, date: Date) =
getRaw("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId AND eventDate = '${date.stringY_m_d}' $ORDER_BY")
fun getAllByDateTime(profileId: Int, date: Date, time: Time) =
getRaw("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId AND eventDate = '${date.stringY_m_d}' AND eventTime = '${time.stringValue}'")
fun getNearestNotDone(profileId: Int, today: Date, limit: Int) =
getRaw("$QUERY WHERE $NOT_BLACKLISTED AND $NOT_DONE AND events.profileId = $profileId AND eventDate >= '${today.stringY_m_d}' $ORDER_BY LIMIT $limit")
// GET ALL - NOW
fun getAllNow(profileId: Int) =
getRawNow("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId $ORDER_BY")
fun getNotNotifiedNow() =
getRawNow("$QUERY WHERE $NOT_BLACKLISTED AND notified = 0 $ORDER_BY")
fun getNotNotifiedNow(profileId: Int) =
getRawNow("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId AND notified = 0 $ORDER_BY")
fun getAllByDateNow(profileId: Int, date: Date) =
getRawNow("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId AND eventDate = '${date.stringY_m_d}' $ORDER_BY")
// GET ONE - NOW
fun getByIdNow(profileId: Int, id: Long) =
getOneNow("$QUERY WHERE events.profileId = $profileId AND eventId = $id")
@Query("SELECT eventId FROM events WHERE profileId = :profileId AND eventBlacklisted = 1")
abstract fun getBlacklistedIds(profileId: Int): List<Long>
@get:Query("SELECT eventId FROM events WHERE eventBlacklisted = 1")
abstract val blacklistedIds: List<Long>
/*@Query("UPDATE events SET eventAddedManually = 1 WHERE profileId = :profileId AND eventDate < :date")
abstract fun convertOlderToManual(profileId: Int, date: Date?)
@Query("DELETE FROM events WHERE teamId = :teamId AND eventId = :id")
abstract fun removeByTeamId(teamId: Long, id: Long)
@Query("DELETE FROM events WHERE profileId = :profileId AND eventAddedManually = 0")
abstract fun removeNotManual(profileId: Int)*/
@RawQuery
abstract fun dontKeepFuture(query: SupportSQLiteQuery?): Long
@Transaction
open fun dontKeepFuture(profileId: Int, todayDate: Date, filter: String) {
dontKeepFuture(SimpleSQLiteQuery("UPDATE events SET keep = 0 WHERE profileId = " + profileId
+ " AND eventAddedManually = 0 AND eventDate >= '" + todayDate.stringY_m_d + "'" +
" AND " + filter))
}
@Query("UPDATE events SET keep = 0 WHERE profileId = :profileId AND eventAddedManually = 0 AND eventDate >= :todayDate AND eventType = :type")
abstract fun dontKeepFutureWithType(profileId: Int, todayDate: Date, type: Long)
@Query("UPDATE events SET keep = 0 WHERE profileId = :profileId AND eventAddedManually = 0 AND eventDate >= :todayDate AND eventType != :exceptType")
abstract fun dontKeepFutureExceptType(profileId: Int, todayDate: Date, exceptType: Long)
@Transaction
open fun dontKeepFutureExceptTypes(profileId: Int, todayDate: Date, exceptTypes: List<Long>) {
dontKeepFuture(profileId, todayDate, "eventType NOT IN " + exceptTypes.toString().replace('[', '(').replace(']', ')'))
}
@Query("UPDATE metadata SET seen = :seen WHERE profileId = :profileId AND (thingType = " + Metadata.TYPE_EVENT + " OR thingType = " + Metadata.TYPE_LESSON_CHANGE + " OR thingType = " + Metadata.TYPE_HOMEWORK + ") AND thingId IN (SELECT eventId FROM events WHERE profileId = :profileId AND eventDate = :date)")
abstract fun setSeenByDate(profileId: Int, date: Date, seen: Boolean)
@Query("UPDATE events SET eventBlacklisted = :blacklisted WHERE profileId = :profileId AND eventId = :eventId")
abstract fun setBlacklisted(profileId: Int, eventId: Long, blacklisted: Boolean)
@Query("DELETE FROM events WHERE profileId = :profileId AND eventId = :id") @Query("DELETE FROM events WHERE profileId = :profileId AND eventId = :id")
abstract fun remove(profileId: Int, id: Long) abstract fun remove(profileId: Int, id: Long)
@ -71,80 +148,4 @@ abstract class EventDao : BaseDao<Event, EventFull> {
remove(profileId, event.type, event.id) remove(profileId, event.type, event.id)
} }
@RawQuery(observedEntities = [Event::class])
abstract override fun getRaw(query: SupportSQLiteQuery): LiveData<List<EventFull>>
fun getAll(profileId: Int) =
getRaw("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId $ORDER_BY")
fun getAllByType(profileId: Int, type: Long, filter: String = "1") =
getRaw("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId AND eventType = $type AND $filter $ORDER_BY")
fun getAllByDate(profileId: Int, date: Date) =
getRaw("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId AND eventDate = '${date.stringY_m_d}' $ORDER_BY")
fun getAllByDateTime(profileId: Int, date: Date, time: Time) =
getRaw("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId AND eventDate = '${date.stringY_m_d}' AND eventTime = ${time.stringValue}")
fun getAllNearest(profileId: Int, today: Date, limit: Int) =
getRaw("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId AND eventDate >= '${today.stringY_m_d}' $ORDER_BY LIMIT $limit")
fun getAllNow(profileId: Int) =
getRawNow("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId $ORDER_BY")
fun getNotNotifiedNow() =
getRawNow("$QUERY WHERE $NOT_BLACKLISTED AND notified = 0 $ORDER_BY")
fun getNotNotifiedNow(profileId: Int) =
getRawNow("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId AND notified = 0 $ORDER_BY")
fun getAllByDateNow(profileId: Int, date: Date) =
getRawNow("$QUERY WHERE $NOT_BLACKLISTED AND events.profileId = $profileId AND eventDate = '${date.stringY_m_d}' $ORDER_BY")
fun getByIdNow(profileId: Int, id: Long) =
getOneNow("$QUERY WHERE events.profileId = $profileId AND eventId = $id")
@Query("SELECT eventId FROM events WHERE profileId = :profileId AND eventBlacklisted = 1")
abstract fun getBlacklistedIds(profileId: Int): List<Long>
@get:Query("SELECT eventId FROM events WHERE eventBlacklisted = 1")
abstract val blacklistedIds: List<Long>
@Query("UPDATE events SET eventAddedManually = 1 WHERE profileId = :profileId AND eventDate < :date")
abstract fun convertOlderToManual(profileId: Int, date: Date?)
@Query("DELETE FROM events WHERE teamId = :teamId AND eventId = :id")
abstract fun removeByTeamId(teamId: Long, id: Long)
@Query("DELETE FROM events WHERE profileId = :profileId AND eventAddedManually = 0")
abstract fun removeNotManual(profileId: Int)
@RawQuery
abstract fun removeFuture(query: SupportSQLiteQuery?): Long
@Transaction
open fun removeFuture(profileId: Int, todayDate: Date, filter: String) {
removeFuture(SimpleSQLiteQuery("DELETE FROM events WHERE profileId = " + profileId
+ " AND eventAddedManually = 0 AND eventDate >= '" + todayDate.stringY_m_d + "'" +
" AND " + filter))
}
@Query("DELETE FROM events WHERE profileId = :profileId AND eventAddedManually = 0 AND eventDate >= :todayDate AND eventType = :type")
abstract fun removeFutureWithType(profileId: Int, todayDate: Date, type: Long)
@Query("DELETE FROM events WHERE profileId = :profileId AND eventAddedManually = 0 AND eventDate >= :todayDate AND eventType != :exceptType")
abstract fun removeFutureExceptType(profileId: Int, todayDate: Date, exceptType: Long)
@Transaction
open fun removeFutureExceptTypes(profileId: Int, todayDate: Date, exceptTypes: List<Long>) {
removeFuture(profileId, todayDate, "eventType NOT IN " + exceptTypes.toString().replace('[', '(').replace(']', ')'))
}
@Query("UPDATE metadata SET seen = :seen WHERE profileId = :profileId AND (thingType = " + Metadata.TYPE_EVENT + " OR thingType = " + Metadata.TYPE_LESSON_CHANGE + " OR thingType = " + Metadata.TYPE_HOMEWORK + ") AND thingId IN (SELECT eventId FROM events WHERE profileId = :profileId AND eventDate = :date)")
abstract fun setSeenByDate(profileId: Int, date: Date, seen: Boolean)
@Query("UPDATE events SET eventBlacklisted = :blacklisted WHERE profileId = :profileId AND eventId = :eventId")
abstract fun setBlacklisted(profileId: Int, eventId: Long, blacklisted: Boolean)
} }

View File

@ -42,7 +42,7 @@ open class Event(
var teacherId: Long, var teacherId: Long,
var subjectId: Long, var subjectId: Long,
var teamId: Long var teamId: Long
) { ) : Keepable() {
companion object { companion object {
const val TYPE_UNDEFINED = -2L const val TYPE_UNDEFINED = -2L
const val TYPE_HOMEWORK = -1L const val TYPE_HOMEWORK = -1L
@ -79,6 +79,8 @@ open class Event(
var sharedByName: String? = null var sharedByName: String? = null
@ColumnInfo(name = "eventBlacklisted") @ColumnInfo(name = "eventBlacklisted")
var blacklisted: Boolean = false var blacklisted: Boolean = false
@ColumnInfo(name = "eventIsDone")
var isDone: Boolean = false
var homeworkBody: String? = null var homeworkBody: String? = null
var attachmentIds: List<Long>? = null var attachmentIds: List<Long>? = null

View File

@ -0,0 +1,9 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-30.
*/
package pl.szczodrzynski.edziennik.data.db.entity
abstract class Keepable {
var keep: Boolean = true
}

View File

@ -0,0 +1,10 @@
package pl.szczodrzynski.edziennik.data.db.migration
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration82 : Migration(81, 82) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE events ADD COLUMN eventIsDone INTEGER DEFAULT 0 NOT NULL")
}
}

View File

@ -0,0 +1,14 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-30.
*/
package pl.szczodrzynski.edziennik.data.db.migration
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration83 : Migration(82, 83) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE events ADD COLUMN keep INTEGER NOT NULL DEFAULT 1")
}
}

View File

@ -152,7 +152,7 @@ class SzkolnyAppFirebase(val app: App, val profiles: List<Profile>, val message:
events += event events += event
metadataList += metadata metadataList += metadata
} }
app.db.eventDao().addAll(events) app.db.eventDao().upsertAll(events)
app.db.metadataDao().addAllReplace(metadataList) app.db.metadataDao().addAllReplace(metadataList)
if (notificationList.isNotEmpty()) { if (notificationList.isNotEmpty()) {
app.db.notificationDao().addAll(notificationList) app.db.notificationDao().addAll(notificationList)

View File

@ -1,50 +0,0 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-12-19.
*/
package pl.szczodrzynski.edziennik.ui
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.databinding.TemplateListItemBinding
import pl.szczodrzynski.edziennik.onClick
class TemplateAdapter(
val context: Context,
val onItemClick: ((item: TemplateItem) -> Unit)? = null,
val onItemButtonClick: ((item: TemplateItem) -> Unit)? = null
) : RecyclerView.Adapter<TemplateAdapter.ViewHolder>() {
private val app by lazy { context.applicationContext as App }
var items = listOf<TemplateItem>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = TemplateListItemBinding.inflate(inflater, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
val b = holder.b
onItemClick?.let { listener ->
b.root.onClick { listener(item) }
}
/*b.someButton.visibility = if (buttonVisible) View.VISIBLE else View.GONE
onItemButtonClick?.let { listener ->
b.someButton.onClick { listener(item) }
}*/
}
override fun getItemCount() = items.size
class ViewHolder(val b: TemplateListItemBinding) : RecyclerView.ViewHolder(b.root)
data class TemplateItem(val text: String)
}

View File

@ -146,6 +146,11 @@ class DayDialog(
adapter = EventListAdapter( adapter = EventListAdapter(
activity, activity,
showWeekDay = false,
showDate = false,
showType = true,
showTime = true,
showSubject = true,
onItemClick = { onItemClick = {
EventDetailsDialog( EventDetailsDialog(
activity, activity,

View File

@ -148,6 +148,10 @@ class EventDetailsDialog(
openInCalendar() openInCalendar()
} }
b.checkDoneButton.setOnLongClickListener {
Toast.makeText(activity, R.string.hint_mark_as_done, Toast.LENGTH_SHORT).show()
true
}
b.goToTimetableButton.setOnLongClickListener { b.goToTimetableButton.setOnLongClickListener {
Toast.makeText(activity, R.string.hint_go_to_timetable, Toast.LENGTH_SHORT).show() Toast.makeText(activity, R.string.hint_go_to_timetable, Toast.LENGTH_SHORT).show()
true true
@ -161,6 +165,14 @@ class EventDetailsDialog(
true true
} }
b.checkDoneButton.isChecked = event.isDone
b.checkDoneButton.addOnCheckedChangeListener { _, isChecked ->
event.isDone = isChecked
launch(Dispatchers.Default) {
app.db.eventDao().replace(event)
}
}
b.topic.text = event.topic b.topic.text = event.topic
BetterLink.attach(b.topic) { BetterLink.attach(b.topic) {
dialog.dismiss() dialog.dismiss()

View File

@ -6,9 +6,9 @@ package pl.szczodrzynski.edziennik.ui.dialogs.event
import android.content.Context import android.content.Context
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.full.EventFull import pl.szczodrzynski.edziennik.data.db.full.EventFull
@ -19,8 +19,11 @@ import pl.szczodrzynski.edziennik.utils.models.Week
class EventListAdapter( class EventListAdapter(
val context: Context, val context: Context,
val simpleMode: Boolean = false, val simpleMode: Boolean = false,
val showDate: Boolean = false,
val showWeekDay: Boolean = false, val showWeekDay: Boolean = false,
val showDate: Boolean = false,
val showType: Boolean = true,
val showTime: Boolean = true,
val showSubject: Boolean = true,
val onItemClick: ((event: EventFull) -> Unit)? = null, val onItemClick: ((event: EventFull) -> Unit)? = null,
val onEventEditClick: ((event: EventFull) -> Unit)? = null val onEventEditClick: ((event: EventFull) -> Unit)? = null
) : RecyclerView.Adapter<EventListAdapter.ViewHolder>() { ) : RecyclerView.Adapter<EventListAdapter.ViewHolder>() {
@ -48,13 +51,14 @@ class EventListAdapter(
b.simpleMode = simpleMode b.simpleMode = simpleMode
b.topic.text = event.topic b.topic.text = event.topic
b.topic.maxLines = if (simpleMode) 2 else 3
b.details.text = mutableListOf<CharSequence?>( b.details.text = mutableListOf<CharSequence?>(
if (showWeekDay) Week.getFullDayName(event.date.weekDay) else null, if (showWeekDay) Week.getFullDayName(event.date.weekDay) else null,
if (showDate) event.date.getRelativeString(context, 7) ?: event.date.formattedStringShort else null, if (showDate) event.date.getRelativeString(context, 7) ?: event.date.formattedStringShort else null,
event.typeName, if (showType) event.typeName else null,
if (simpleMode) null else event.time?.stringHM ?: app.getString(R.string.event_all_day), if (showTime) event.time?.stringHM ?: app.getString(R.string.event_all_day) else null,
if (simpleMode) null else event.subjectLongName if (showSubject) event.subjectLongName else null
).concat(bullet) ).concat(bullet)
b.addedBy.setText( b.addedBy.setText(
@ -73,12 +77,15 @@ class EventListAdapter(
) )
b.typeColor.background?.setTintColor(event.eventColor) b.typeColor.background?.setTintColor(event.eventColor)
b.typeColor.isVisible = showType
b.editButton.visibility = if (event.addedManually && !simpleMode) View.VISIBLE else View.GONE b.editButton.isVisible = !simpleMode && event.addedManually && !event.isDone
b.editButton.onClick { b.editButton.onClick {
onEventEditClick?.invoke(event) onEventEditClick?.invoke(event)
} }
b.isDone.isVisible = event.isDone
b.editButton.setOnLongClickListener { b.editButton.setOnLongClickListener {
Toast.makeText(context, R.string.hint_edit_event, Toast.LENGTH_SHORT).show() Toast.makeText(context, R.string.hint_edit_event, Toast.LENGTH_SHORT).show()
true true

View File

@ -587,7 +587,7 @@ class EventManualDialog(
private fun finishAdding(eventObject: Event, metadataObject: Metadata) { private fun finishAdding(eventObject: Event, metadataObject: Metadata) {
launch { launch {
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
app.db.eventDao().add(eventObject) app.db.eventDao().upsert(eventObject)
app.db.metadataDao().add(metadataObject) app.db.metadataDao().add(metadataObject)
} }
} }

View File

@ -170,6 +170,11 @@ class LessonDetailsDialog(
adapter = EventListAdapter( adapter = EventListAdapter(
activity, activity,
showWeekDay = false,
showDate = false,
showType = true,
showTime = true,
showSubject = true,
onItemClick = { onItemClick = {
EventDetailsDialog( EventDetailsDialog(
activity, activity,

View File

@ -1,28 +0,0 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-2-22.
*/
package pl.szczodrzynski.edziennik.ui.modules.base
import androidx.fragment.app.Fragment
abstract class PagerFragment : Fragment() {
private var isPageCreated = false
/**
* Called when the page is first shown, or if previous
* [onPageCreated] returned false
*
* @return true if the view is set up
* @return false if the setup failed. The method may be then called
* again, when page becomes visible.
*/
abstract fun onPageCreated(): Boolean
override fun onResume() {
if (!isPageCreated) {
isPageCreated = onPageCreated()
}
super.onResume()
}
}

View File

@ -0,0 +1,18 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-30.
*/
package pl.szczodrzynski.edziennik.ui.modules.base.lazypager
import androidx.fragment.app.FragmentManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
class FragmentLazyPagerAdapter(
fragmentManager: FragmentManager,
swipeRefreshLayout: SwipeRefreshLayout,
private val fragments: List<Pair<LazyFragment, CharSequence>>
) : LazyPagerAdapter(fragmentManager, swipeRefreshLayout) {
override fun getPage(position: Int) = fragments[position].first
override fun getPageTitle(position: Int) = fragments[position].second
override fun getCount() = fragments.size
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-29.
*/
package pl.szczodrzynski.edziennik.ui.modules.base.lazypager
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
abstract class LazyFragment : Fragment() {
private var isPageCreated = false
internal var position = -1
internal var swipeRefreshLayoutCallback: ((position: Int, isEnabled: Boolean) -> Unit)? = null
/**
* Called when the page is first shown, or if previous
* [onPageCreated] returned false
*
* @return true if the view is set up
* @return false if the setup failed. The method may be then called
* again, when page becomes visible.
*/
abstract fun onPageCreated(): Boolean
fun enableSwipeToRefresh() = swipeRefreshLayoutCallback?.invoke(position, true)
fun disableSwipeToRefresh() = swipeRefreshLayoutCallback?.invoke(position, false)
fun setSwipeToRefresh(enabled: Boolean) = swipeRefreshLayoutCallback?.invoke(position, enabled)
val onScrollListener: RecyclerView.OnScrollListener
get() = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (recyclerView.canScrollVertically(-1))
disableSwipeToRefresh()
if (!recyclerView.canScrollVertically(-1) && newState == RecyclerView.SCROLL_STATE_IDLE)
enableSwipeToRefresh()
}
}
internal fun createPage() {
if (!isPageCreated && isAdded) {
isPageCreated = onPageCreated()
}
}
override fun onDestroyView() {
isPageCreated = false
super.onDestroyView()
}
override fun onResume() {
createPage()
super.onResume()
}
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-29.
*/
package pl.szczodrzynski.edziennik.ui.modules.base.lazypager
import android.util.SparseBooleanArray
import androidx.core.util.set
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
abstract class LazyPagerAdapter(fragmentManager: FragmentManager, val swipeRefreshLayout: SwipeRefreshLayout? = null) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) {
internal val enabledList = SparseBooleanArray()
private val refreshLayoutCallback: (position: Int, isEnabled: Boolean) -> Unit = { position, isEnabled ->
swipeRefreshLayout?.isEnabled = isEnabled
if (position > -1)
enabledList[position] = isEnabled
}
final override fun getItem(position: Int): LazyFragment {
return getPage(position).also {
it.position = position
it.swipeRefreshLayoutCallback = refreshLayoutCallback
}
}
abstract fun getPage(position: Int): LazyFragment
abstract override fun getPageTitle(position: Int): CharSequence
}

View File

@ -0,0 +1,42 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-29.
*/
package pl.szczodrzynski.edziennik.ui.modules.base.lazypager
import android.content.Context
import android.util.AttributeSet
import androidx.viewpager.widget.ViewPager
class LazyViewPager @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null
) : ViewPager(context, attrs) {
private var pageSelection = -1
private var scrollState = 0
init {
addOnPageChangeListener(object : OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {
scrollState = state
(adapter as? LazyPagerAdapter)?.let {
it.swipeRefreshLayout?.isEnabled = state == SCROLL_STATE_IDLE && it.enabledList[pageSelection, true]
}
}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
}
override fun onPageSelected(position: Int) {
pageSelection = position
(adapter as? LazyPagerAdapter)?.let {
it.swipeRefreshLayout?.isEnabled = scrollState == SCROLL_STATE_IDLE && it.enabledList[pageSelection, true]
val fragment = adapter?.instantiateItem(this@LazyViewPager, position)
val lazyFragment = fragment as? LazyFragment
lazyFragment?.createPage()
}
}
})
}
}

View File

@ -10,11 +10,10 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial.Icon2 import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial.Icon2
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -23,7 +22,7 @@ import pl.szczodrzynski.edziennik.MainActivity.Companion.TARGET_GRADES_EDITOR
import pl.szczodrzynski.edziennik.data.db.entity.Grade import pl.szczodrzynski.edziennik.data.db.entity.Grade
import pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_GRADE import pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_GRADE
import pl.szczodrzynski.edziennik.data.db.full.GradeFull import pl.szczodrzynski.edziennik.data.db.full.GradeFull
import pl.szczodrzynski.edziennik.databinding.GradesFragmentBinding import pl.szczodrzynski.edziennik.databinding.GradesListFragmentBinding
import pl.szczodrzynski.edziennik.ui.dialogs.grade.GradeDetailsDialog import pl.szczodrzynski.edziennik.ui.dialogs.grade.GradeDetailsDialog
import pl.szczodrzynski.edziennik.ui.dialogs.settings.GradesConfigDialog import pl.szczodrzynski.edziennik.ui.dialogs.settings.GradesConfigDialog
import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesAverages import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesAverages
@ -36,24 +35,20 @@ import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.math.max import kotlin.math.max
class GradesListFragment : Fragment(), CoroutineScope {
class GradesFragment : Fragment(), CoroutineScope {
companion object { companion object {
private const val TAG = "GradesFragment" private const val TAG = "GradesFragment"
} }
private lateinit var app: App private lateinit var app: App
private lateinit var activity: MainActivity private lateinit var activity: MainActivity
private lateinit var b: GradesFragmentBinding private lateinit var b: GradesListFragmentBinding
private val job: Job = Job() private val job: Job = Job()
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main get() = job + Dispatchers.Main
// local/private variables go here // local/private variables go here
private val adapter by lazy {
GradesAdapter(activity)
}
private val manager by lazy { app.gradesManager } private val manager by lazy { app.gradesManager }
private val dontCountEnabled by lazy { manager.dontCountEnabled } private val dontCountEnabled by lazy { manager.dontCountEnabled }
private val dontCountGrades by lazy { manager.dontCountGrades } private val dontCountGrades by lazy { manager.dontCountGrades }
@ -63,51 +58,49 @@ class GradesFragment : Fragment(), CoroutineScope {
activity = (getActivity() as MainActivity?) ?: return null activity = (getActivity() as MainActivity?) ?: return null
context ?: return null context ?: return null
app = activity.application as App app = activity.application as App
b = GradesFragmentBinding.inflate(inflater) b = GradesListFragmentBinding.inflate(inflater)
b.refreshLayout.setParent(activity.swipeRefreshLayout) b.refreshLayout.setParent(activity.swipeRefreshLayout)
return b.root return b.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { startCoroutineTimer(100L) {
if (!isAdded) if (!isAdded) return@startCoroutineTimer
return
expandSubjectId = arguments?.getLong("gradesSubjectId") ?: 0L expandSubjectId = arguments?.getLong("gradesSubjectId") ?: 0L
app.db.gradeDao() val adapter = GradesAdapter(activity)
.getAllOrderBy(App.profileId, app.gradesManager.getOrderByString()) var firstRun = true
.observe(this, Observer { grades ->
if (b.gradesRecyclerView.adapter == null) {
b.gradesRecyclerView.adapter = adapter
b.gradesRecyclerView.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(context)
//addItemDecoration(SimpleDividerItemDecoration(context))
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
if (recyclerView.canScrollVertically(-1)) {
b.refreshLayout.isEnabled = false
}
if (!recyclerView.canScrollVertically(-1) && newState == SCROLL_STATE_IDLE) {
b.refreshLayout.isEnabled = true
}
}
})
}
}
launch(Dispatchers.Default) { app.db.gradeDao().getAllOrderBy(App.profileId, app.gradesManager.getOrderByString()).observe(this, Observer { items -> launch {
processGrades(grades) if (!isAdded) return@launch
}
if (grades != null && grades.isNotEmpty()) { // load & configure the adapter
b.gradesRecyclerView.visibility = View.VISIBLE adapter.items = withContext(Dispatchers.Default) { processGrades(items) }
b.gradesNoData.visibility = View.GONE if (items.isNotNullNorEmpty() && b.list.adapter == null) {
} else { b.list.adapter = adapter
b.gradesRecyclerView.visibility = View.GONE b.list.apply {
b.gradesNoData.visibility = View.VISIBLE setHasFixedSize(true)
} layoutManager = LinearLayoutManager(context)
}) addOnScrollListener(b.refreshLayout.onScrollListener)
}
}
adapter.notifyDataSetChanged()
if (firstRun) {
expandSubject(adapter)
firstRun = false
}
// show/hide relevant views
b.progressBar.isVisible = false
if (items.isNullOrEmpty()) {
b.list.isVisible = false
b.noData.isVisible = true
} else {
b.list.isVisible = true
b.noData.isVisible = false
}
}})
adapter.onGradeClick = { adapter.onGradeClick = {
GradeDetailsDialog(activity, it) GradeDetailsDialog(activity, it)
@ -153,10 +146,30 @@ class GradesFragment : Fragment(), CoroutineScope {
}) })
) )
activity.gainAttention() activity.gainAttention()
}}
private fun expandSubject(adapter: GradesAdapter) {
var expandSubjectModel: GradesSubject? = null
if (expandSubjectId != 0L) {
expandSubjectModel = adapter.items.firstOrNull { it is GradesSubject && it.subjectId == expandSubjectId } as? GradesSubject
adapter.expandModel(
model = expandSubjectModel,
view = null,
notifyAdapter = false
)
}
startCoroutineTimer(500L) {
if (expandSubjectModel != null) {
b.list.smoothScrollToPosition(
adapter.items.indexOf(expandSubjectModel) + expandSubjectModel.semesters.size + (expandSubjectModel.semesters.firstOrNull()?.grades?.size ?: 0)
)
}
}
} }
@Suppress("SuspendFunctionOnCoroutineScope") @Suppress("SuspendFunctionOnCoroutineScope")
private suspend fun processGrades(grades: List<GradeFull>) { private fun processGrades(grades: List<GradeFull>): MutableList<Any> {
val items = mutableListOf<GradesSubject>() val items = mutableListOf<GradesSubject>()
var subjectId = -1L var subjectId = -1L
@ -284,30 +297,7 @@ class GradesFragment : Fragment(), CoroutineScope {
GradesManager.ORDER_BY_DATE_ASC -> items.sortBy { it.lastAddedDate } GradesManager.ORDER_BY_DATE_ASC -> items.sortBy { it.lastAddedDate }
} }
adapter.items = items.toMutableList() return (items + stats).toMutableList()
adapter.items.add(stats)
var expandSubjectModel: GradesSubject? = null
if (expandSubjectId != 0L) {
expandSubjectModel = items.firstOrNull { it.subjectId == expandSubjectId }
adapter.expandModel(
model = expandSubjectModel,
view = null,
notifyAdapter = false
)
}
withContext(Dispatchers.Main) {
adapter.notifyDataSetChanged()
}
startCoroutineTimer(500L, 0L) {
if (expandSubjectModel != null) {
b.gradesRecyclerView.smoothScrollToPosition(
items.indexOf(expandSubjectModel) + expandSubjectModel.semesters.size + (expandSubjectModel.semesters.firstOrNull()?.grades?.size ?: 0)
)
}
}
} }
private fun countGrade(grade: Grade, averages: GradesAverages) { private fun countGrade(grade: Grade, averages: GradesAverages) {

View File

@ -60,8 +60,11 @@ class HomeEventsCard(
adapter = EventListAdapter( adapter = EventListAdapter(
activity, activity,
simpleMode = true, simpleMode = true,
showDate = true,
showWeekDay = true, showWeekDay = true,
showDate = true,
showType = true,
showTime = false,
showSubject = false,
onItemClick = { onItemClick = {
EventDetailsDialog( EventDetailsDialog(
activity, activity,
@ -77,7 +80,7 @@ class HomeEventsCard(
} }
) )
app.db.eventDao().getAllNearest(profile.id, Date.getToday(), 4).observe(activity, Observer { events -> app.db.eventDao().getNearestNotDone(profile.id, Date.getToday(), 4).observe(activity, Observer { events ->
adapter.items = events adapter.items = events
if (b.eventsView.adapter == null) { if (b.eventsView.adapter == null) {
b.eventsView.adapter = adapter b.eventsView.adapter = adapter

View File

@ -1,3 +1,7 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-30.
*/
package pl.szczodrzynski.edziennik.ui.modules.homework package pl.szczodrzynski.edziennik.ui.modules.homework
import android.os.AsyncTask import android.os.AsyncTask
@ -7,46 +11,48 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Toast import android.widget.Toast
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.viewpager.widget.ViewPager
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.typeface.library.szkolny.font.SzkolnyFont import com.mikepenz.iconics.typeface.library.szkolny.font.SzkolnyFont
import pl.szczodrzynski.edziennik.App import kotlinx.coroutines.CoroutineScope
import pl.szczodrzynski.edziennik.MainActivity import kotlinx.coroutines.Dispatchers
import pl.szczodrzynski.edziennik.R import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.data.db.entity.Metadata import pl.szczodrzynski.edziennik.data.db.entity.Metadata
import pl.szczodrzynski.edziennik.databinding.FragmentHomeworkBinding import pl.szczodrzynski.edziennik.databinding.HomeworkFragmentBinding
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.FragmentLazyPagerAdapter
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
import kotlin.coroutines.CoroutineContext
class HomeworkFragment : Fragment() { class HomeworkFragment : Fragment(), CoroutineScope {
companion object { companion object {
private const val TAG = "HomeworkFragment"
var pageSelection = 0 var pageSelection = 0
} }
private lateinit var app: App private lateinit var app: App
private lateinit var activity: MainActivity private lateinit var activity: MainActivity
private lateinit var b: FragmentHomeworkBinding private lateinit var b: HomeworkFragmentBinding
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local/private variables go here
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null activity = (getActivity() as MainActivity?) ?: return null
if (context == null) context ?: return null
return null
app = activity.application as App app = activity.application as App
context!!.theme.applyStyle(Themes.appTheme, true) b = HomeworkFragmentBinding.inflate(inflater)
// activity, context and profile is valid
b = FragmentHomeworkBinding.inflate(inflater)
b.refreshLayout.setParent(activity.swipeRefreshLayout) b.refreshLayout.setParent(activity.swipeRefreshLayout)
return b.root return b.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// TODO check if app, activity, b can be null if (!isAdded) return
if (app.profile == null || !isAdded)
return
activity.bottomSheet.prependItems( activity.bottomSheet.prependItems(
BottomSheetPrimaryItem(true) BottomSheetPrimaryItem(true)
@ -67,34 +73,29 @@ class HomeworkFragment : Fragment() {
Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show() Toast.makeText(activity, R.string.main_menu_mark_as_read_success, Toast.LENGTH_SHORT).show()
})) }))
b.viewPager.adapter = MessagesFragment.Adapter(childFragmentManager).also { adapter -> val pagerAdapter = FragmentLazyPagerAdapter(
adapter.addFragment(HomeworkListFragment().also { fragment -> fragmentManager ?: return,
fragment.arguments = Bundle().also { args -> b.refreshLayout,
args.putInt("homeworkDate", HomeworkDate.CURRENT) listOf(
} HomeworkListFragment().apply {
}, getString(R.string.homework_tab_current)) arguments = Bundle("homeworkDate" to HomeworkDate.CURRENT)
} to getString(R.string.homework_tab_current),
adapter.addFragment(HomeworkListFragment().also { fragment -> HomeworkListFragment().apply {
fragment.arguments = Bundle().also { args -> arguments = Bundle("homeworkDate" to HomeworkDate.PAST)
args.putInt("homeworkDate", HomeworkDate.PAST) } to getString(R.string.homework_tab_past)
} )
}, getString(R.string.homework_tab_past)) )
b.viewPager.apply {
offscreenPageLimit = 1
adapter = pagerAdapter
currentItem = pageSelection
addOnPageSelectedListener {
pageSelection = it
}
b.tabLayout.setupWithViewPager(this)
} }
b.viewPager.currentItem = pageSelection
b.viewPager.clearOnPageChangeListeners()
b.viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) {
b.refreshLayout.isEnabled = state == ViewPager.SCROLL_STATE_IDLE
}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
override fun onPageSelected(position: Int) {
pageSelection = position
}
})
b.tabLayout.setupWithViewPager(b.viewPager)
activity.navView.apply { activity.navView.apply {
bottomBar.apply { bottomBar.apply {
fabEnable = true fabEnable = true

View File

@ -4,67 +4,106 @@ import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.core.view.isVisible
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import pl.szczodrzynski.edziennik.App import kotlinx.coroutines.CoroutineScope
import pl.szczodrzynski.edziennik.MainActivity import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.entity.Event import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.databinding.HomeworkListBinding import pl.szczodrzynski.edziennik.databinding.HomeworkListFragmentBinding
import pl.szczodrzynski.edziennik.getInt import pl.szczodrzynski.edziennik.ui.dialogs.event.EventDetailsDialog
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventListAdapter
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import kotlin.coroutines.CoroutineContext
class HomeworkListFragment : Fragment() { class HomeworkListFragment : LazyFragment(), CoroutineScope {
companion object {
private const val TAG = "HomeworkListFragment"
}
private lateinit var app: App private lateinit var app: App
private lateinit var activity: MainActivity private lateinit var activity: MainActivity
private lateinit var b: HomeworkListBinding private lateinit var b: HomeworkListFragmentBinding
private var homeworkDate = HomeworkDate.CURRENT private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local/private variables go here
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null activity = (getActivity() as MainActivity?) ?: return null
context ?: return null context ?: return null
app = activity.application as App app = activity.application as App
b = HomeworkListBinding.inflate(inflater) b = HomeworkListFragmentBinding.inflate(inflater)
return b.root return b.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onPageCreated(): Boolean { startCoroutineTimer(100L) {
// TODO check if app, activity, b can be null val homeworkDate = arguments.getInt("homeworkDate", HomeworkDate.CURRENT)
if (!isAdded)
return
if (arguments != null) {
homeworkDate = arguments.getInt("homeworkDate", HomeworkDate.CURRENT)
}
val layoutManager = LinearLayoutManager(context)
layoutManager.reverseLayout = homeworkDate == HomeworkDate.PAST
layoutManager.stackFromEnd = homeworkDate == HomeworkDate.PAST
b.homeworkView.setHasFixedSize(true)
b.homeworkView.layoutManager = layoutManager
val today = Date.getToday()
val filter = when(homeworkDate) { val filter = when(homeworkDate) {
HomeworkDate.CURRENT -> "eventDate >= '" + Date.getToday().stringY_m_d + "'" HomeworkDate.CURRENT -> "eventDate >= '${today.stringY_m_d}' AND eventIsDone = 0"
else -> "eventDate < '" + Date.getToday().stringY_m_d + "'" else -> "eventDate < '${today.stringY_m_d}' OR eventIsDone = 1"
} }
app.db.eventDao() val adapter = EventListAdapter(
.getAllByType(App.profileId, Event.TYPE_HOMEWORK, filter) activity,
.observe(this, Observer { homeworkList -> showWeekDay = true,
if (!isAdded) return@Observer showDate = true,
showType = false,
showTime = true,
showSubject = true,
onItemClick = {
EventDetailsDialog(
activity,
it
)
},
onEventEditClick = {
EventManualDialog(
activity,
it.profileId,
editingEvent = it
)
}
)
if (homeworkList != null && homeworkList.size > 0) { app.db.eventDao().getAllByType(App.profileId, Event.TYPE_HOMEWORK, filter).observe(this@HomeworkListFragment, Observer { items ->
val adapter = HomeworkAdapter(context, homeworkList) if (!isAdded) return@Observer
b.homeworkView.adapter = adapter
b.homeworkView.visibility = View.VISIBLE // load & configure the adapter
b.homeworkNoData.visibility = View.GONE adapter.items = items
} else { if (items.isNotNullNorEmpty() && b.list.adapter == null) {
b.homeworkView.visibility = View.GONE b.list.adapter = adapter
b.homeworkNoData.visibility = View.VISIBLE b.list.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(context).apply {
reverseLayout = homeworkDate == HomeworkDate.PAST
stackFromEnd = homeworkDate == HomeworkDate.PAST
} }
}) addItemDecoration(SimpleDividerItemDecoration(context))
} addOnScrollListener(onScrollListener)
}
}
adapter.notifyDataSetChanged()
setSwipeToRefresh(false) // TODO
// show/hide relevant views
b.progressBar.isVisible = false
if (items.isNullOrEmpty()) {
b.list.isVisible = false
b.noData.isVisible = true
} else {
b.list.isVisible = true
b.noData.isVisible = false
}
})
}; return true }
} }

View File

@ -15,6 +15,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.FrameLayout import android.widget.FrameLayout
import android.widget.ProgressBar import android.widget.ProgressBar
import androidx.appcompat.widget.PopupMenu
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import com.mikepenz.iconics.IconicsColor import com.mikepenz.iconics.IconicsColor
@ -62,7 +63,7 @@ class MessageFragment : Fragment(), CoroutineScope {
private lateinit var activity: MainActivity private lateinit var activity: MainActivity
private lateinit var b: MessageFragmentBinding private lateinit var b: MessageFragmentBinding
private lateinit var job: Job private val job: Job = Job()
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main get() = job + Dispatchers.Main
@ -73,16 +74,12 @@ class MessageFragment : Fragment(), CoroutineScope {
activity = (getActivity() as MainActivity?) ?: return null activity = (getActivity() as MainActivity?) ?: return null
context ?: return null context ?: return null
app = activity.application as App app = activity.application as App
context!!.theme.applyStyle(Themes.appTheme, true)
b = MessageFragmentBinding.inflate(inflater) b = MessageFragmentBinding.inflate(inflater)
job = Job()
return b.root return b.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// TODO check if app, activity, b can be null if (!isAdded) return
if (app.profile == null || !isAdded)
return
b.closeButton.setImageDrawable( b.closeButton.setImageDrawable(
IconicsDrawable(activity, CommunityMaterial.Icon2.cmd_window_close) IconicsDrawable(activity, CommunityMaterial.Icon2.cmd_window_close)
@ -260,16 +257,35 @@ class MessageFragment : Fragment(), CoroutineScope {
MessagesFragment.pageSelection = min(message.type, 1) MessagesFragment.pageSelection = min(message.type, 1)
} }
private val attachmentOnClick = { v: View ->
if (v.tag is Int) {
downloadAttachment(v.tag as Int)
}
}
private val attachmentOnLongClick = { v: View ->
(v.tag as? Int)?.let { tag ->
val popupMenu = PopupMenu(v.context, v)
popupMenu.menu.add(0, tag, 0, R.string.messages_attachment_download_again)
popupMenu.setOnMenuItemClickListener {
downloadAttachment(it.itemId, forceDownload = true)
true
}
popupMenu.show()
}
true
}
private fun showAttachments() { private fun showAttachments() {
if (message.attachmentIds != null) { if (message.attachmentIds != null) {
val insertPoint = b.attachments val insertPoint = b.attachments
insertPoint.removeAllViews() insertPoint.removeAllViews()
val chipLayoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) val chipLayoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
chipLayoutParams.setMargins(0, Utils.dpToPx(8), 0, Utils.dpToPx(8)) chipLayoutParams.setMargins(0, 8.dp, 0, 0)
val progressLayoutParams = FrameLayout.LayoutParams(Utils.dpToPx(18), Utils.dpToPx(18)) val progressLayoutParams = FrameLayout.LayoutParams(18.dp, 18.dp)
progressLayoutParams.setMargins(Utils.dpToPx(8), 0, Utils.dpToPx(8), 0) progressLayoutParams.setMargins(8.dp, 0, 8.dp, 0)
progressLayoutParams.gravity = END or CENTER_VERTICAL progressLayoutParams.gravity = END or CENTER_VERTICAL
// CREATE VIEWS AND AN OBJECT FOR EVERY ATTACHMENT // CREATE VIEWS AND AN OBJECT FOR EVERY ATTACHMENT
@ -280,12 +296,13 @@ class MessageFragment : Fragment(), CoroutineScope {
val size = message.attachmentSizes[index] val size = message.attachmentSizes[index]
// create the parent // create the parent
val attachmentLayout = FrameLayout(b.root.context) val attachmentLayout = FrameLayout(b.root.context)
attachmentLayout.setPadding(Utils.dpToPx(16), 0, Utils.dpToPx(16), 0) attachmentLayout.setPadding(16.dp, 0, 16.dp, 0)
val attachmentChip = Chip(attachmentLayout.context) val attachmentChip = Chip(attachmentLayout.context)
//attachmentChip.setChipBackgroundColorResource(ThemeUtils.getChipColorRes()); //attachmentChip.setChipBackgroundColorResource(ThemeUtils.getChipColorRes());
attachmentChip.layoutParams = chipLayoutParams attachmentChip.layoutParams = chipLayoutParams
attachmentChip.height = Utils.dpToPx(40) attachmentChip.chipMinHeight = 40.dp.toFloat()
//attachmentChip.height = Utils.dpToPx(40)
// show the file size or not // show the file size or not
if (size == -1L) if (size == -1L)
@ -312,11 +329,8 @@ class MessageFragment : Fragment(), CoroutineScope {
attachmentChip.isCloseIconVisible = false attachmentChip.isCloseIconVisible = false
// set the object's index in the attachmentList as the tag // set the object's index in the attachmentList as the tag
attachmentChip.tag = index attachmentChip.tag = index
attachmentChip.setOnClickListener { v -> attachmentChip.onClick(attachmentOnClick)
if (v.tag is Int) { attachmentChip.onLongClick(attachmentOnLongClick)
downloadAttachment(v.tag as Int)
}
}
attachmentLayout.addView(attachmentChip) attachmentLayout.addView(attachmentChip)
val attachmentProgress = ProgressBar(attachmentLayout.context) val attachmentProgress = ProgressBar(attachmentLayout.context)
@ -338,10 +352,10 @@ class MessageFragment : Fragment(), CoroutineScope {
} }
} }
private fun downloadAttachment(index: Int) { private fun downloadAttachment(index: Int, forceDownload: Boolean = false) {
val attachment = attachmentList[index] val attachment = attachmentList[index]
if (attachment.downloaded != null) { if (!forceDownload && attachment.downloaded != null) {
Utils.openFile(activity, File(attachment.downloaded)) Utils.openFile(activity, File(attachment.downloaded))
return return
} }

View File

@ -6,7 +6,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.viewpager.widget.ViewPager import androidx.viewpager.widget.ViewPager
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
@ -14,8 +14,9 @@ import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.db.entity.Message import pl.szczodrzynski.edziennik.data.db.entity.Message
import pl.szczodrzynski.edziennik.databinding.FragmentMessagesBinding import pl.szczodrzynski.edziennik.databinding.FragmentMessagesBinding
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyPagerAdapter
import pl.szczodrzynski.edziennik.utils.Themes import pl.szczodrzynski.edziennik.utils.Themes
import java.util.*
class MessagesFragment : Fragment() { class MessagesFragment : Fragment() {
companion object { companion object {
@ -54,7 +55,7 @@ class MessagesFragment : Fragment() {
return return
} }
b.viewPager.adapter = Adapter(childFragmentManager).also { adapter -> b.viewPager.adapter = Adapter(childFragmentManager, b.refreshLayout).also { adapter ->
adapter.addFragment(MessagesListFragment().also { fragment -> adapter.addFragment(MessagesListFragment().also { fragment ->
fragment.arguments = Bundle().also { args -> fragment.arguments = Bundle().also { args ->
@ -71,13 +72,8 @@ class MessagesFragment : Fragment() {
} }
b.viewPager.currentItem = pageSelection b.viewPager.currentItem = pageSelection
b.viewPager.clearOnPageChangeListeners()
b.viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener { b.viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
override fun onPageScrollStateChanged(state: Int) { override fun onPageScrollStateChanged(state: Int) {}
if (b.refreshLayout != null) {
b.refreshLayout.isEnabled = state == ViewPager.SCROLL_STATE_IDLE
}
}
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {} override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {}
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
pageSelection = position pageSelection = position
@ -126,11 +122,11 @@ class MessagesFragment : Fragment() {
}*/ }*/
} }
internal class Adapter(manager: FragmentManager) : FragmentPagerAdapter(manager) { internal class Adapter(manager: FragmentManager, swipeRefreshLayout: SwipeRefreshLayout) : LazyPagerAdapter(manager, swipeRefreshLayout) {
private val mFragmentList = ArrayList<Fragment>() private val mFragmentList = mutableListOf<LazyFragment>()
private val mFragmentTitleList = ArrayList<String>() private val mFragmentTitleList = mutableListOf<String>()
override fun getItem(position: Int): Fragment { override fun getPage(position: Int): LazyFragment {
return mFragmentList[position] return mFragmentList[position]
} }
@ -138,12 +134,12 @@ class MessagesFragment : Fragment() {
return mFragmentList.size return mFragmentList.size
} }
fun addFragment(fragment: Fragment, title: String) { fun addFragment(fragment: LazyFragment, title: String) {
mFragmentList.add(fragment) mFragmentList.add(fragment)
mFragmentTitleList.add(title) mFragmentTitleList.add(title)
} }
override fun getPageTitle(position: Int): CharSequence? { override fun getPageTitle(position: Int): CharSequence {
return mFragmentTitleList[position] return mFragmentTitleList[position]
} }
} }

View File

@ -10,11 +10,10 @@ import android.view.ViewGroup;
import android.view.animation.Interpolator; import android.view.animation.Interpolator;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.databinding.DataBindingUtil; import androidx.databinding.DataBindingUtil;
import androidx.fragment.app.Fragment;
import androidx.interpolator.view.animation.FastOutSlowInInterpolator; import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -26,13 +25,15 @@ import pl.szczodrzynski.edziennik.data.db.entity.Message;
import pl.szczodrzynski.edziennik.data.db.full.MessageFull; import pl.szczodrzynski.edziennik.data.db.full.MessageFull;
import pl.szczodrzynski.edziennik.data.db.full.MessageRecipientFull; import pl.szczodrzynski.edziennik.data.db.full.MessageRecipientFull;
import pl.szczodrzynski.edziennik.databinding.MessagesListBinding; import pl.szczodrzynski.edziennik.databinding.MessagesListBinding;
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment;
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration; import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration;
import pl.szczodrzynski.edziennik.utils.Themes; import pl.szczodrzynski.edziennik.utils.Themes;
import static androidx.recyclerview.widget.RecyclerView.NO_POSITION; import static androidx.recyclerview.widget.RecyclerView.NO_POSITION;
import static androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE;
import static pl.szczodrzynski.edziennik.utils.Utils.d; import static pl.szczodrzynski.edziennik.utils.Utils.d;
public class MessagesListFragment extends Fragment { public class MessagesListFragment extends LazyFragment {
private App app = null; private App app = null;
private MainActivity activity = null; private MainActivity activity = null;
@ -65,9 +66,9 @@ public class MessagesListFragment extends Fragment {
} }
@Override @Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { public boolean onPageCreated() {
if (app == null || activity == null || b == null || !isAdded()) if (app == null || activity == null || b == null || !isAdded())
return; return false;
long messageId = -1; long messageId = -1;
if (getArguments() != null) { if (getArguments() != null) {
@ -78,7 +79,7 @@ public class MessagesListFragment extends Fragment {
args.putLong("messageId", messageId); args.putLong("messageId", messageId);
getArguments().remove("messageId"); getArguments().remove("messageId");
activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, args); activity.loadTarget(MainActivity.TARGET_MESSAGES_DETAILS, args);
return; return false;
} }
if (getArguments() != null) { if (getArguments() != null) {
@ -161,11 +162,22 @@ public class MessagesListFragment extends Fragment {
// TODO ANIMATION // TODO ANIMATION
//postponeEnterTransition(); //postponeEnterTransition();
viewParent = (ViewGroup) view.getParent(); viewParent = (ViewGroup) getView().getParent();
b.emailList.setLayoutManager(new LinearLayoutManager(view.getContext())); b.emailList.setLayoutManager(new LinearLayoutManager(getView().getContext()));
b.emailList.addItemDecoration(new SimpleDividerItemDecoration(view.getContext())); b.emailList.addItemDecoration(new SimpleDividerItemDecoration(getView().getContext()));
b.emailList.setAdapter(messagesAdapter); b.emailList.setAdapter(messagesAdapter);
b.emailList.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (b.emailList.canScrollVertically(-1)) {
setSwipeToRefresh(false);
}
if (!b.emailList.canScrollVertically(-1) && newState == SCROLL_STATE_IDLE) {
setSwipeToRefresh(true);
}
}
});
if (messageType == Message.TYPE_RECEIVED) { if (messageType == Message.TYPE_RECEIVED) {
App.db.messageDao().getReceived(App.Companion.getProfileId()).observe(this, messageFulls -> { App.db.messageDao().getReceived(App.Companion.getProfileId()).observe(this, messageFulls -> {
@ -215,7 +227,7 @@ public class MessagesListFragment extends Fragment {
}); });
} }
return true;
} }
private void createMessageList(List<MessageFull> messageFulls) { private void createMessageList(List<MessageFull> messageFulls) {

View File

@ -1,71 +1,62 @@
package pl.szczodrzynski.edziennik.ui.modules.notifications package pl.szczodrzynski.edziennik.ui.modules.notifications
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.TextView import androidx.appcompat.app.AppCompatActivity
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.* import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.entity.Notification import pl.szczodrzynski.edziennik.data.db.entity.Notification
import pl.szczodrzynski.edziennik.utils.Utils.d import pl.szczodrzynski.edziennik.databinding.NotificationsListItemBinding
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import kotlin.coroutines.CoroutineContext
class NotificationsAdapter( class NotificationsAdapter(
private val context: Context private val activity: AppCompatActivity,
) : RecyclerView.Adapter<NotificationsAdapter.ViewHolder>() { val onItemClick: ((item: Notification) -> Unit)? = null
) : RecyclerView.Adapter<NotificationsAdapter.ViewHolder>(), CoroutineScope {
companion object { companion object {
private const val TAG = "NotificationsAdapter" private const val TAG = "NotificationsAdapter"
} }
private val app = activity.applicationContext as App
// optional: place the manager here
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
var items = listOf<Notification>() var items = listOf<Notification>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(context) val inflater = LayoutInflater.from(activity)
val view = inflater.inflate(R.layout.row_notifications_item, parent, false) val view = NotificationsListItemBinding.inflate(inflater, parent, false)
return ViewHolder(view) return ViewHolder(view)
} }
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val app = context.applicationContext as App val item = items[position]
val b = holder.b
val notification = items[position] val date = Date.fromMillis(item.addedDate).formattedString
val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity)
val date = Date.fromMillis(notification.addedDate).formattedString b.title.text = item.text
val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(context) b.profileDate.text = listOf(
item.profileName ?: "",
holder.title.text = notification.text
holder.profileDate.text = listOf(
notification.profileName ?: "",
"", "",
date.asColoredSpannable(colorSecondary) date
).concat() ).concat().asColoredSpannable(colorSecondary)
holder.type.text = context.getNotificationTitle(notification.type) b.type.text = activity.getNotificationTitle(item.type)
holder.root.onClick { onItemClick?.let { listener ->
val intent = Intent("android.intent.action.MAIN") b.root.onClick { listener(item) }
notification.fillIntent(intent)
d(TAG, "notification with item " + notification.viewId + " extras " + if (intent.extras == null) "null" else intent.extras!!.toString())
//Log.d(TAG, "Got date "+intent.getLongExtra("timetableDate", 0));
if (notification.profileId != null && notification.profileId != -1 && notification.profileId != app.profile.id && context is Activity) {
Toast.makeText(app, app.getString(R.string.toast_changing_profile), Toast.LENGTH_LONG).show()
}
app.sendBroadcast(intent)
} }
} }
override fun getItemCount() = items.size override fun getItemCount() = items.size
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { class ViewHolder(val b: NotificationsListItemBinding) : RecyclerView.ViewHolder(b.root)
var root = itemView
var title: TextView = itemView.findViewById(R.id.title)
var profileDate: TextView = itemView.findViewById(R.id.profileDate)
var type: TextView = itemView.findViewById(R.id.type)
}
} }

View File

@ -1,87 +0,0 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-22.
*/
package pl.szczodrzynski.edziennik.ui.modules.notifications
import android.os.AsyncTask
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.FragmentNotificationsBinding
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
class NotificationsFragment : Fragment() {
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: FragmentNotificationsBinding
private val adapter by lazy {
NotificationsAdapter(activity)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
context ?: return null
app = activity.application as App
context!!.theme.applyStyle(Themes.appTheme, true)
if (app.profile == null)
return inflater.inflate(R.layout.fragment_loading, container, false)
// activity, context and profile is valid
b = FragmentNotificationsBinding.inflate(inflater)
return b.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// TODO check if app, activity, b can be null
if (app.profile == null || !isAdded)
return
activity.bottomSheet.prependItems(
BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_remove_notifications)
.withIcon(CommunityMaterial.Icon.cmd_delete_sweep_outline)
.withOnClickListener(View.OnClickListener {
activity.bottomSheet.close()
AsyncTask.execute { app.db.notificationDao().clearAll() }
Toast.makeText(activity, R.string.menu_remove_notifications_success, Toast.LENGTH_SHORT).show()
}))
app.db.notificationDao()
.getAll()
.observe(this, Observer { notifications ->
if (app.profile == null || !isAdded) return@Observer
adapter.items = notifications
if (b.notificationsView.adapter == null) {
b.notificationsView.adapter = adapter
b.notificationsView.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(context)
addItemDecoration(SimpleDividerItemDecoration(context))
}
}
adapter.notifyDataSetChanged()
if (notifications != null && notifications.isNotEmpty()) {
b.notificationsView.visibility = View.VISIBLE
b.notificationsNoData.visibility = View.GONE
} else {
b.notificationsView.visibility = View.GONE
b.notificationsNoData.visibility = View.VISIBLE
}
})
}
}

View File

@ -0,0 +1,101 @@
/*
* Copyright (c) Kuba Szczodrzyński 2019-11-22.
*/
package pl.szczodrzynski.edziennik.ui.modules.notifications
import android.app.Activity
import android.content.Intent
import android.os.AsyncTask
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.databinding.NotificationsListFragmentBinding
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
import pl.szczodrzynski.edziennik.utils.Utils
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
import kotlin.coroutines.CoroutineContext
class NotificationsListFragment : Fragment(), CoroutineScope {
companion object {
private const val TAG = "NotificationsListFragment"
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: NotificationsListFragmentBinding
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
context ?: return null
app = activity.application as App
b = NotificationsListFragmentBinding.inflate(inflater)
return b.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { startCoroutineTimer(100L) {
if (!isAdded) return@startCoroutineTimer
activity.bottomSheet.prependItems(
BottomSheetPrimaryItem(true)
.withTitle(R.string.menu_remove_notifications)
.withIcon(CommunityMaterial.Icon.cmd_delete_sweep_outline)
.withOnClickListener(View.OnClickListener {
activity.bottomSheet.close()
AsyncTask.execute { app.db.notificationDao().clearAll() }
Toast.makeText(activity, R.string.menu_remove_notifications_success, Toast.LENGTH_SHORT).show()
}))
val adapter = NotificationsAdapter(activity) { notification ->
val intent = Intent("android.intent.action.MAIN")
notification.fillIntent(intent)
Utils.d(TAG, "notification with item " + notification.viewId + " extras " + if (intent.extras == null) "null" else intent.extras!!.toString())
if (notification.profileId != null && notification.profileId != -1 && notification.profileId != app.profile.id && context is Activity) {
Toast.makeText(app, app.getString(R.string.toast_changing_profile), Toast.LENGTH_LONG).show()
}
app.sendBroadcast(intent)
}
app.db.notificationDao().getAll().observe(this, Observer { items ->
if (!isAdded) return@Observer
// load & configure the adapter
adapter.items = items
if (items.isNotNullNorEmpty() && b.list.adapter == null) {
b.list.adapter = adapter
b.list.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(context)
addItemDecoration(SimpleDividerItemDecoration(context))
}
}
adapter.notifyDataSetChanged()
// show/hide relevant views
b.progressBar.isVisible = false
if (items.isNullOrEmpty()) {
b.list.isVisible = false
b.noData.isVisible = true
} else {
b.list.isVisible = true
b.noData.isVisible = false
}
})
}}
}

View File

@ -0,0 +1,66 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-30.
*/
package pl.szczodrzynski.edziennik.ui.modules.template
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.entity.Notification
import pl.szczodrzynski.edziennik.databinding.TemplateListItemBinding
import pl.szczodrzynski.edziennik.utils.models.Date
import kotlin.coroutines.CoroutineContext
class TemplateAdapter(
val activity: AppCompatActivity,
val onItemClick: ((item: Notification) -> Unit)? = null
) : RecyclerView.Adapter<TemplateAdapter.ViewHolder>(), CoroutineScope {
companion object {
private const val TAG = "TemplateAdapter"
}
private val app = activity.applicationContext as App
// optional: place the manager here
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
var items = listOf<Notification>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(parent.context)
val view = TemplateListItemBinding.inflate(inflater, parent, false)
return ViewHolder(view)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val item = items[position]
val b = holder.b
val date = Date.fromMillis(item.addedDate).formattedString
val colorSecondary = android.R.attr.textColorSecondary.resolveAttr(activity)
b.title.text = item.text
b.profileDate.text = listOf(
item.profileName ?: "",
"",
date
).concat().asColoredSpannable(colorSecondary)
b.type.text = activity.getNotificationTitle(item.type)
onItemClick?.let { listener ->
b.root.onClick { listener(item) }
}
}
override fun getItemCount() = items.size
class ViewHolder(val b: TemplateListItemBinding) : RecyclerView.ViewHolder(b.root)
}

View File

@ -1,8 +1,8 @@
/* /*
* Copyright (c) Kuba Szczodrzyński 2020-1-8. * Copyright (c) Kuba Szczodrzyński 2020-3-30.
*/ */
package pl.szczodrzynski.edziennik.ui.dialogs package pl.szczodrzynski.edziennik.ui.modules.template
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity

View File

@ -0,0 +1,75 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-30.
*/
package pl.szczodrzynski.edziennik.ui.modules.template
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.addOnPageSelectedListener
import pl.szczodrzynski.edziennik.databinding.TemplateFragmentBinding
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.FragmentLazyPagerAdapter
import pl.szczodrzynski.edziennik.ui.modules.homework.HomeworkFragment
import kotlin.coroutines.CoroutineContext
class TemplateFragment : Fragment(), CoroutineScope {
companion object {
private const val TAG = "TemplateFragment"
var pageSelection = 0
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: TemplateFragmentBinding
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local/private variables go here
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
context ?: return null
app = activity.application as App
b = TemplateFragmentBinding.inflate(inflater)
b.refreshLayout.setParent(activity.swipeRefreshLayout)
return b.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (!isAdded) return
val pagerAdapter = FragmentLazyPagerAdapter(
fragmentManager ?: return,
b.refreshLayout,
listOf(
TemplatePageFragment() to "Pager 0",
TemplatePageFragment() to "Pager 1",
TemplatePageFragment() to "Pager 2",
TemplatePageFragment() to "Pager 3",
TemplateListPageFragment() to "Pager 4",
TemplateListPageFragment() to "Pager 5",
TemplateListPageFragment() to "Pager 6",
TemplateListPageFragment() to "Pager 7"
)
)
b.viewPager.apply {
offscreenPageLimit = 1
adapter = pagerAdapter
currentItem = pageSelection
addOnPageSelectedListener {
HomeworkFragment.pageSelection = it
}
b.tabLayout.setupWithViewPager(this)
}
}
}

View File

@ -0,0 +1,80 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-30.
*/
package pl.szczodrzynski.edziennik.ui.modules.template
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.databinding.TemplateListFragmentBinding
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
import kotlin.coroutines.CoroutineContext
class TemplateListFragment : Fragment(), CoroutineScope {
companion object {
private const val TAG = "TemplateListFragment"
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: TemplateListFragmentBinding
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local/private variables go here
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
context ?: return null
app = activity.application as App
b = TemplateListFragmentBinding.inflate(inflater)
b.refreshLayout.setParent(activity.swipeRefreshLayout)
return b.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { startCoroutineTimer(100L) {
if (!isAdded) return@startCoroutineTimer
val adapter = TemplateAdapter(activity)
app.db.notificationDao().getAll().observe(this, Observer { items ->
if (!isAdded) return@Observer
// load & configure the adapter
adapter.items = items
if (items.isNotNullNorEmpty() && b.list.adapter == null) {
b.list.adapter = adapter
b.list.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(context)
addItemDecoration(SimpleDividerItemDecoration(context))
addOnScrollListener(b.refreshLayout.onScrollListener)
}
}
adapter.notifyDataSetChanged()
b.refreshLayout.isEnabled = false // TODO
// show/hide relevant views
b.progressBar.isVisible = false
if (items.isNullOrEmpty()) {
b.list.isVisible = false
b.noData.isVisible = true
} else {
b.list.isVisible = true
b.noData.isVisible = false
}
})
}}
}

View File

@ -0,0 +1,80 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-30.
*/
package pl.szczodrzynski.edziennik.ui.modules.template
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.databinding.TemplateListPageFragmentBinding
import pl.szczodrzynski.edziennik.isNotNullNorEmpty
import pl.szczodrzynski.edziennik.startCoroutineTimer
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
import kotlin.coroutines.CoroutineContext
class TemplateListPageFragment : LazyFragment(), CoroutineScope {
companion object {
private const val TAG = "TemplateListPagerFragment"
}
private lateinit var app: App
private lateinit var activity: MainActivity
private lateinit var b: TemplateListPageFragmentBinding
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local/private variables go here
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
context ?: return null
app = activity.application as App
b = TemplateListPageFragmentBinding.inflate(inflater)
return b.root
}
override fun onPageCreated(): Boolean { startCoroutineTimer(100L) {
val adapter = TemplateAdapter(activity)
app.db.notificationDao().getAll().observe(this, Observer { items ->
if (!isAdded) return@Observer
// load & configure the adapter
adapter.items = items
if (items.isNotNullNorEmpty() && b.list.adapter == null) {
b.list.adapter = adapter
b.list.apply {
setHasFixedSize(true)
layoutManager = LinearLayoutManager(context)
addItemDecoration(SimpleDividerItemDecoration(context))
addOnScrollListener(onScrollListener)
}
}
adapter.notifyDataSetChanged()
setSwipeToRefresh(false) // TODO
// show/hide relevant views
b.progressBar.isVisible = false
if (items.isNullOrEmpty()) {
b.list.isVisible = false
b.noData.isVisible = true
} else {
b.list.isVisible = true
b.noData.isVisible = false
}
})
}; return true }
}

View File

@ -1,26 +1,30 @@
package pl.szczodrzynski.edziennik.ui.modules.base /*
* Copyright (c) Kuba Szczodrzyński 2020-3-30.
*/
package pl.szczodrzynski.edziennik.ui.modules.template
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.fragment.app.Fragment
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.databinding.FragmentTemplateBinding import pl.szczodrzynski.edziennik.databinding.TemplatePageFragmentBinding
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
class TemplateFragment : Fragment(), CoroutineScope { class TemplatePageFragment : LazyFragment(), CoroutineScope {
companion object { companion object {
private const val TAG = "TemplateFragment" private const val TAG = "TemplatePagerFragment"
} }
private lateinit var app: App private lateinit var app: App
private lateinit var activity: MainActivity private lateinit var activity: MainActivity
private lateinit var b: FragmentTemplateBinding private lateinit var b: TemplatePageFragmentBinding
private val job: Job = Job() private val job: Job = Job()
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
@ -32,15 +36,16 @@ class TemplateFragment : Fragment(), CoroutineScope {
activity = (getActivity() as MainActivity?) ?: return null activity = (getActivity() as MainActivity?) ?: return null
context ?: return null context ?: return null
app = activity.application as App app = activity.application as App
b = FragmentTemplateBinding.inflate(inflater) b = TemplatePageFragmentBinding.inflate(inflater)
b.refreshLayout.setParent(activity.swipeRefreshLayout)
return b.root return b.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onPageCreated(): Boolean {
if (!isAdded) b.text.text = "Fragment $position"
return
b.button.addOnCheckedChangeListener { button, isChecked ->
setSwipeToRefresh(isChecked)
}
return true
} }
} }

View File

@ -0,0 +1,15 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-30.
*/
package pl.szczodrzynski.edziennik.ui.modules.template
import androidx.fragment.app.FragmentManager
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyPagerAdapter
class TemplatePagerAdapter(fragmentManager: FragmentManager, swipeRefreshLayout: SwipeRefreshLayout) : LazyPagerAdapter(fragmentManager, swipeRefreshLayout) {
override fun getPage(position: Int) = TemplatePageFragment()
override fun getPageTitle(position: Int) = "Page $position"
override fun getCount() = 10
}

View File

@ -27,7 +27,7 @@ import pl.szczodrzynski.edziennik.data.db.full.LessonFull
import pl.szczodrzynski.edziennik.databinding.TimetableLessonBinding import pl.szczodrzynski.edziennik.databinding.TimetableLessonBinding
import pl.szczodrzynski.edziennik.databinding.TimetableNoTimetableBinding import pl.szczodrzynski.edziennik.databinding.TimetableNoTimetableBinding
import pl.szczodrzynski.edziennik.ui.dialogs.timetable.LessonDetailsDialog import pl.szczodrzynski.edziennik.ui.dialogs.timetable.LessonDetailsDialog
import pl.szczodrzynski.edziennik.ui.modules.base.PagerFragment import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment.Companion.DEFAULT_END_HOUR import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment.Companion.DEFAULT_END_HOUR
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment.Companion.DEFAULT_START_HOUR import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment.Companion.DEFAULT_START_HOUR
import pl.szczodrzynski.edziennik.utils.ListenerScrollView import pl.szczodrzynski.edziennik.utils.ListenerScrollView
@ -36,7 +36,7 @@ import java.util.*
import kotlin.coroutines.CoroutineContext import kotlin.coroutines.CoroutineContext
import kotlin.math.min import kotlin.math.min
class TimetableDayFragment : PagerFragment(), CoroutineScope { class TimetableDayFragment : LazyFragment(), CoroutineScope {
companion object { companion object {
private const val TAG = "TimetableDayFragment" private const val TAG = "TimetableDayFragment"
} }
@ -104,9 +104,6 @@ class TimetableDayFragment : PagerFragment(), CoroutineScope {
} }
override fun onPageCreated(): Boolean { override fun onPageCreated(): Boolean {
if (!isAdded)
return false
// observe lesson database // observe lesson database
app.db.timetableDao().getForDate(App.profileId, date).observe(this, Observer { lessons -> app.db.timetableDao().getForDate(App.profileId, date).observe(this, Observer { lessons ->
launch { launch {

View File

@ -5,9 +5,9 @@
package pl.szczodrzynski.edziennik.ui.modules.timetable package pl.szczodrzynski.edziennik.ui.modules.timetable
import android.os.Bundle import android.os.Bundle
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentStatePagerAdapter import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyPagerAdapter
import pl.szczodrzynski.edziennik.utils.models.Date import pl.szczodrzynski.edziennik.utils.models.Date
import pl.szczodrzynski.edziennik.utils.models.Week import pl.szczodrzynski.edziennik.utils.models.Week
@ -16,7 +16,7 @@ class TimetablePagerAdapter(
private val items: List<Date>, private val items: List<Date>,
private val startHour: Int, private val startHour: Int,
private val endHour: Int private val endHour: Int
) : FragmentStatePagerAdapter(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { ) : LazyPagerAdapter(fragmentManager, null) {
companion object { companion object {
private const val TAG = "TimetablePagerAdapter" private const val TAG = "TimetablePagerAdapter"
} }
@ -25,7 +25,7 @@ class TimetablePagerAdapter(
private val weekStart by lazy { today.weekStart } private val weekStart by lazy { today.weekStart }
private val weekEnd by lazy { weekStart.clone().stepForward(0, 0, 6) } private val weekEnd by lazy { weekStart.clone().stepForward(0, 0, 6) }
override fun getItem(position: Int): Fragment { override fun getPage(position: Int): LazyFragment {
return TimetableDayFragment().apply { return TimetableDayFragment().apply {
arguments = Bundle().apply { arguments = Bundle().apply {
putInt("date", items[position].value) putInt("date", items[position].value)
@ -39,7 +39,7 @@ class TimetablePagerAdapter(
return items.size return items.size
} }
override fun getPageTitle(position: Int): CharSequence? { override fun getPageTitle(position: Int): CharSequence {
val date = items[position] val date = items[position]
val pageTitle = StringBuilder(Week.getFullDayName(date.weekDay)) val pageTitle = StringBuilder(Week.getFullDayName(date.weekDay))
if (date > weekEnd || date < weekStart) { if (date > weekEnd || date < weekStart) {

View File

@ -137,7 +137,7 @@ public class Date implements Comparable<Date> {
} }
public static int diffDays(Date d1, Date d2) { public static int diffDays(Date d1, Date d2) {
return (int) ((d1.getInMillis() - d2.getInMillis()) / (24 * 60 * 60 * 1000)); return Math.round((d1.getInMillis() - d2.getInMillis()) / (24 * 60 * 60 * 1000f));
} }
public static boolean isToday(Date date) { public static boolean isToday(Date date) {

View File

@ -0,0 +1,34 @@
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-3-30.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<path
android:pathData="m102,24h-74c-4.42,0 -8,3.58 -8,8v72c0,4.42 3.58,8 8,8h74c4.42,0 8,-3.58 8,-8v-72c0,-4.42 -3.58,-8 -8,-8z"
android:fillColor="#70d0f3"/>
<path
android:pathData="m20,32v8h90v-8c0,-4.42 -3.58,-8 -8,-8h-74c-4.42,0 -8,3.58 -8,8z"
android:fillColor="#3281e6"/>
<path
android:pathData="m88,92h-30c-2.21,0 -4,-1.79 -4,-4 0,-2.21 1.79,-4 4,-4h30c2.21,0 4,1.79 4,4 0,2.21 -1.79,4 -4,4zM92,74c0,-2.21 -1.79,-4 -4,-4h-30c-2.21,0 -4,1.79 -4,4 0,2.21 1.79,4 4,4h30c2.21,0 4,-1.79 4,-4zM92,60c0,-2.21 -1.79,-4 -4,-4h-30c-2.21,0 -4,1.79 -4,4 0,2.21 1.79,4 4,4h30c2.21,0 4,-1.79 4,-4zM48,88c0,-2.21 -1.79,-4 -4,-4h-2c-2.21,0 -4,1.79 -4,4 0,2.21 1.79,4 4,4h2c2.21,0 4,-1.79 4,-4zM48,74c0,-2.21 -1.79,-4 -4,-4h-2c-2.21,0 -4,1.79 -4,4 0,2.21 1.79,4 4,4h2c2.21,0 4,-1.79 4,-4zM48,60c0,-2.21 -1.79,-4 -4,-4h-2c-2.21,0 -4,1.79 -4,4 0,2.21 1.79,4 4,4h2c2.21,0 4,-1.79 4,-4z"
android:fillColor="#2b83d5"/>
<path
android:pathData="m127,86.3 l-7.42,-7.42c-1.12,-1.13 -2.96,-1.13 -4.09,0l-3.5,3.49 11.5,11.5 3.49,-3.5c1.13,-1.13 1.13,-2.96 0,-4.09"
android:fillColor="#fc657c"/>
<path
android:pathData="m93.4,124 l-11.5,-11.5 24.5,-24.5 11.5,11.5z"
android:fillColor="#ffa859"/>
<path
android:pathData="m118,99.6 l-11.5,-11.5 5.75,-5.76 11.5,11.5z"
android:fillColor="#c8c8c8"/>
<path
android:pathData="m81.9,113 l-3.88,15.4 15.4,-3.88z"
android:fillColor="#ffcd98"/>
<path
android:pathData="m80,120 l-1.96,7.78 7.78,-1.96z"
android:fillColor="#818181"/>
</vector>

View File

@ -195,6 +195,18 @@
android:text="\uFCDA" android:text="\uFCDA"
android:textSize="20sp" android:textSize="20sp"
android:fontFamily="@font/community_material_font_v3_5_95_1" /> android:fontFamily="@font/community_material_font_v3_5_95_1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/checkDoneButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:minWidth="0dp"
android:checkable="true"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:text="\uFCE1"
android:textSize="20sp"
android:fontFamily="@font/community_material_font_v3_5_95_1" />
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>
</androidx.core.widget.NestedScrollView> </androidx.core.widget.NestedScrollView>

View File

@ -4,7 +4,8 @@
--> -->
<layout xmlns:tools="http://schemas.android.com/tools" <layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data> <data>
<import type="android.view.View"/> <import type="android.view.View"/>
@ -54,22 +55,35 @@
android:id="@+id/topic" android:id="@+id/topic"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_marginRight="4dp"
android:layout_weight="1" android:layout_weight="1"
android:textAppearance="@style/NavView.TextView.Medium"
android:maxLines="@{simpleMode ? 2 : 3}"
android:ellipsize="end" android:ellipsize="end"
tools:maxLines="3" android:maxLines="3"
android:textAppearance="@style/NavView.TextView.Medium"
tools:text="Rozdział II: Panowanie Piastów i Jagiellonów.Przeniesiony z 11 grudnia. Nie wiem co się dzieje w tym roku nie będzie już religii w szkołach podstawowych w Polsce i Europie zachodniej Afryki" /> tools:text="Rozdział II: Panowanie Piastów i Jagiellonów.Przeniesiony z 11 grudnia. Nie wiem co się dzieje w tym roku nie będzie już religii w szkołach podstawowych w Polsce i Europie zachodniej Afryki" />
<com.google.android.material.button.MaterialButton <com.google.android.material.button.MaterialButton
android:id="@+id/editButton" android:id="@+id/editButton"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/community_material_font_v3_5_95_1"
android:minWidth="0dp" android:minWidth="0dp"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:text="\uFC92" android:text="\uFC92"
android:textSize="20sp" android:textSize="20sp"
android:fontFamily="@font/community_material_font_v3_5_95_1"/> tools:visibility="gone" />
<com.mikepenz.iconics.view.IconicsImageView
android:id="@+id/isDone"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_gravity="top"
android:layout_marginLeft="4dp"
android:layout_marginRight="4dp"
app:iiv_color="@color/md_green_500"
app:iiv_icon="cmd-check"
tools:background="@sample/check" />
</LinearLayout> </LinearLayout>

View File

@ -21,7 +21,7 @@
app:tabSelectedTextColor="?colorPrimary" app:tabSelectedTextColor="?colorPrimary"
app:tabTextColor="?android:textColorPrimary"/> app:tabTextColor="?android:textColorPrimary"/>
<androidx.viewpager.widget.ViewPager <pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyViewPager
android:id="@+id/viewPager" android:id="@+id/viewPager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -29,4 +29,4 @@
</LinearLayout> </LinearLayout>
</pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator> </pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator>
</layout> </layout>

View File

@ -28,7 +28,7 @@
android:id="@+id/tabLayout" android:id="@+id/tabLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="48dp" android:layout_height="48dp"
android:background="@color/colorSurface_6dp" android:background="@color/colorSurface_1dp"
app:rtl_tabIndicatorColor="?colorPrimary" app:rtl_tabIndicatorColor="?colorPrimary"
app:rtl_tabMaxWidth="300dp" app:rtl_tabMaxWidth="300dp"
app:rtl_tabMinWidth="90dp" app:rtl_tabMinWidth="90dp"
@ -40,7 +40,7 @@
app:rtl_tabTextAppearance="@style/rtl_RecyclerTabLayout.Tab" /> app:rtl_tabTextAppearance="@style/rtl_RecyclerTabLayout.Tab" />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager <pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyViewPager
android:id="@+id/viewPager" android:id="@+id/viewPager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@ -94,4 +94,4 @@
</FrameLayout> </FrameLayout>
<!--</pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator>--> <!--</pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator>-->
</layout> </layout>

View File

@ -16,8 +16,14 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/gradesNoData" android:id="@+id/noData"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
@ -25,13 +31,17 @@
android:fontFamily="sans-serif-light" android:fontFamily="sans-serif-light"
android:text="@string/grades_no_data" android:text="@string/grades_no_data"
android:textSize="24sp" android:textSize="24sp"
app:drawableTopCompat="@drawable/ic_no_grades" /> android:visibility="gone"
app:drawableTopCompat="@drawable/ic_no_grades"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/gradesRecyclerView" android:id="@+id/list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:listitem="@layout/grades_item_subject" /> android:visibility="gone"
tools:listitem="@layout/grades_item_subject"
tools:visibility="visible" />
</FrameLayout> </FrameLayout>
</pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator> </pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator>
</layout> </layout>

View File

@ -1,7 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-3-30.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto">
xmlns:tools="http://schemas.android.com/tools">
<pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator <pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator
android:id="@+id/refreshLayout" android:id="@+id/refreshLayout"
@ -23,12 +26,11 @@
app:tabSelectedTextColor="?colorPrimary" app:tabSelectedTextColor="?colorPrimary"
app:tabTextColor="?android:textColorPrimary" /> app:tabTextColor="?android:textColorPrimary" />
<androidx.viewpager.widget.ViewPager <pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyViewPager
android:id="@+id/viewPager" android:id="@+id/viewPager"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" /> app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</LinearLayout> </LinearLayout>
</pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator> </pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator>
</layout> </layout>

View File

@ -1,47 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/homeworkView"
android:layout_width="match_parent"
android:layout_height="match_parent">
</androidx.recyclerview.widget.RecyclerView>
<LinearLayout
android:id="@+id/homeworkNoData"
android:layout_width="match_parent"
android:orientation="vertical"
android:gravity="center"
android:layout_height="match_parent"
android:visibility="gone"
tools:visibility="visible">
<com.mikepenz.iconics.view.IconicsImageView
android:layout_width="match_parent"
android:layout_height="92dp"
app:iiv_color="?android:textColorPrimary"
app:iiv_icon="szf-file-document-edit"
app:iiv_size="92dp" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:fontFamily="sans-serif-medium"
android:gravity="center"
android:text="@string/homework_no_data"
android:textSize="18sp"
android:textStyle="italic" />
</LinearLayout>
</LinearLayout>
</layout>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-3-30.
-->
<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">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/noData"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:text="@string/homework_no_data"
android:textSize="24sp"
android:visibility="gone"
app:drawableTopCompat="@drawable/ic_no_homework"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:listitem="@layout/event_list_item"
tools:visibility="visible" />
</FrameLayout>
</layout>

View File

@ -231,8 +231,7 @@
android:ellipsize="middle" android:ellipsize="middle"
android:text="Wyniki sprawdzianu z matematyki.pdf" android:text="Wyniki sprawdzianu z matematyki.pdf"
android:visibility="visible" android:visibility="visible"
app:chipIcon="@drawable/googleg_standard_color_18" app:chipIcon="@drawable/googleg_standard_color_18" />
app:chipMinHeight="36dp" />
<ProgressBar <ProgressBar
style="?android:attr/progressBarStyle" style="?android:attr/progressBarStyle"

View File

@ -1,30 +1,41 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2019-11-22.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android" <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:tools="http://schemas.android.com/tools">
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView <ProgressBar
android:id="@+id/notificationsView" android:id="@+id/progressBar"
android:layout_width="match_parent" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="wrap_content"
tools:listitem="@layout/row_notifications_item" android:layout_gravity="center" />
tools:visibility="gone" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
android:id="@+id/notificationsNoData" android:id="@+id/noData"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="center" android:layout_gravity="center"
app:drawableTopCompat="@drawable/ic_no_notifications"
android:drawablePadding="16dp" android:drawablePadding="16dp"
android:fontFamily="sans-serif-light" android:fontFamily="sans-serif-light"
android:text="@string/notifications_no_data" android:text="@string/notifications_no_data"
android:textSize="24sp" android:textSize="24sp"
android:visibility="gone" android:visibility="gone"
app:drawableTopCompat="@drawable/ic_no_notifications"
tools:visibility="visible" /> tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:listitem="@layout/notifications_list_item"
tools:visibility="visible" />
</FrameLayout> </FrameLayout>
</layout> </layout>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:orientation="vertical"
android:padding="8dp">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/NavView.TextView.Medium"
tools:text="Dzisiaj 1 to szczęśliwy numerek" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/profileDate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
tools:text="Władca Androida • 22 listopada" />
<TextView
android:id="@+id/type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Szczęśliwy numerek" />
</LinearLayout>
</LinearLayout>
</layout>

View File

@ -1,35 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<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"
android:padding="8dp"
android:background="?selectableItemBackground">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/NavView.TextView.Medium"
tools:text="Dzisiaj 1 to szczęśliwy numerek" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/profileDate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
tools:text="Władca Androida • 22 listopada" />
<TextView
android:id="@+id/type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Szczęśliwy numerek" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-3-30.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorSurface_6dp"
app:tabIndicatorColor="?colorPrimary"
app:tabMode="auto"
app:tabSelectedTextColor="?colorPrimary"
app:tabTextColor="?android:textColorPrimary" />
<pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</LinearLayout>
</pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator>
</layout>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-3-30.
-->
<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">
<pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator
android:id="@+id/refreshLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/noData"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:text="@string/grades_no_data"
android:textSize="24sp"
android:visibility="gone"
app:drawableTopCompat="@drawable/ic_no_grades"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:listitem="@layout/grades_item_subject"
tools:visibility="visible" />
</FrameLayout>
</pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoIndicator>
</layout>

View File

@ -3,16 +3,39 @@
~ Copyright (c) Kuba Szczodrzyński 2019-12-19. ~ Copyright (c) Kuba Szczodrzyński 2019-12-19.
--> -->
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:padding="8dp" android:background="?selectableItemBackground"
android:orientation="vertical" android:orientation="vertical"
android:background="?selectableItemBackground"> android:padding="8dp">
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAppearance="@style/NavView.TextView.Medium"
tools:text="Dzisiaj 1 to szczęśliwy numerek" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/profileDate"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
tools:text="Władca Androida • 22 listopada" />
<TextView
android:id="@+id/type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Szczęśliwy numerek" />
</LinearLayout>
</LinearLayout> </LinearLayout>
</layout> </layout>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-3-30.
-->
<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">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/noData"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawablePadding="16dp"
android:fontFamily="sans-serif-light"
android:text="@string/grades_no_data"
android:textSize="24sp"
android:visibility="gone"
app:drawableTopCompat="@drawable/ic_no_grades"
tools:visibility="visible" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
tools:listitem="@layout/grades_item_subject"
tools:visibility="visible" />
</FrameLayout>
</layout>

View File

@ -0,0 +1,30 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2020-3-30.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:text="Fragment 1" />
<com.google.android.material.button.MaterialButton
android:id="@+id/button"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:checkable="true"
android:text="Enable/disable Swipe to refresh"/>
</LinearLayout>
</layout>

View File

@ -1278,4 +1278,7 @@
<string name="registration_enable_dialog_title">Rejestracja na serwerze</string> <string name="registration_enable_dialog_title">Rejestracja na serwerze</string>
<string name="registration_enable_dialog_text">Rejestracja jest automatyczna, jeśli ta opcja jest włączona. Pozwala na tworzenie i odbieranie wydarzeń udostępnionych innym uczniom z Twojej klasy. Dzięki temu, można dodawać do dziennika pozycje nie zapisane przez nauczyciela.\n\nUpewnij się, że zapoznałeś się z warunkami <a href="http://szkolny.eu/privacy-policy">Polityki prywatności</a> i akceptujesz jej postanowienia.</string> <string name="registration_enable_dialog_text">Rejestracja jest automatyczna, jeśli ta opcja jest włączona. Pozwala na tworzenie i odbieranie wydarzeń udostępnionych innym uczniom z Twojej klasy. Dzięki temu, można dodawać do dziennika pozycje nie zapisane przez nauczyciela.\n\nUpewnij się, że zapoznałeś się z warunkami <a href="http://szkolny.eu/privacy-policy">Polityki prywatności</a> i akceptujesz jej postanowienia.</string>
<string name="menu_add_remove_cards">Dodaj lub usuń karty</string> <string name="menu_add_remove_cards">Dodaj lub usuń karty</string>
<string name="menu_template">Template</string>
<string name="messages_attachment_download_again">Pobierz ponownie</string>
<string name="hint_mark_as_done">Oznacz jako wykonane</string>
</resources> </resources>

View File

@ -5,8 +5,8 @@ buildscript {
kotlin_version = '1.3.61' kotlin_version = '1.3.61'
release = [ release = [
versionName: "4.0-rc.3", versionName: "4.0-rc.4",
versionCode: 4000039 versionCode: 4000049
] ]
setup = [ setup = [

1
codegen/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

39
codegen/build.gradle Normal file
View File

@ -0,0 +1,39 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-28.
*/
apply plugin: 'java-library'
apply plugin: 'kotlin'
apply plugin: 'kotlin-kapt'
kapt {
generateStubs = true
}
sourceSets {
main {
java {
srcDir "${buildDir.absolutePath}/tmp/kapt/main/kotlinGenerated/"
}
}
}
dependencies {
kapt project(":annotation")
compileOnly project(':annotation')
implementation fileTree(include: ['*.jar'], dir: 'libs')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
// configuration generator for service providers
implementation "com.google.auto.service:auto-service:1.0-rc4"
kapt "com.google.auto.service:auto-service:1.0-rc4"
kapt "androidx.room:room-compiler:${versions.room}"
implementation "androidx.room:room-runtime:${versions.room}"
implementation "com.squareup:kotlinpoet:1.5.0"
implementation "androidx.sqlite:sqlite:2.1.0@aar"
}
sourceCompatibility = "7"
targetCompatibility = "7"

View File

@ -0,0 +1,339 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-28.
*/
package pl.szczodrzynski.edziennik.codegen
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.TypeConverters
import com.google.auto.service.AutoService
import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import pl.szczodrzynski.edziennik.annotation.SelectiveDao
import pl.szczodrzynski.edziennik.annotation.UpdateSelective
import java.io.File
import javax.annotation.processing.*
import javax.lang.model.SourceVersion
import javax.lang.model.element.*
import javax.lang.model.type.*
import javax.lang.model.util.ElementFilter
import javax.tools.Diagnostic
import kotlin.reflect.KClass
@Suppress("unused")
@AutoService(Processor::class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedOptions(FileGenerator.KAPT_KOTLIN_GENERATED_OPTION_NAME)
class FileGenerator : AbstractProcessor() {
companion object {
const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "kapt.kotlin.generated"
}
private data class TypeConverter(val dataType: TypeMirror, val converterType: TypeElement, val methodName: Name, val returnType: TypeMirror)
private inline fun <reified T : Annotation> Element.getAnnotationClassValue(f: T.() -> KClass<*>) = try {
getAnnotation(T::class.java).f()
throw Exception("Expected to get a MirroredTypeException")
} catch (e: MirroredTypeException) {
e.typeMirror
}
private inline fun <reified T : Annotation> Element.getAnnotationClassValues(f: T.() -> Array<KClass<*>>) = try {
getAnnotation(T::class.java).f()
throw Exception("Expected to get a MirroredTypesException")
} catch (e: MirroredTypesException) {
e.typeMirrors
}
override fun process(set: MutableSet<out TypeElement>?, roundEnvironment: RoundEnvironment?): Boolean {
roundEnvironment?.getElementsAnnotatedWith(SelectiveDao::class.java)?.forEach { it ->
if (it.kind != ElementKind.CLASS) {
processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, "Can only be applied to classes, element: $it")
return false
}
val generatedSourcesRoot = processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME]
if (generatedSourcesRoot?.isEmpty() != false) {
processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, "Can't find the target directory for generated Kotlin files.")
return false
}
val file = File(generatedSourcesRoot)
file.mkdirs()
val dao = it as TypeElement
processClass(dao, file)
//processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, "package = $packageName, className = $className, methodName = $methodName, tableName = $tableName, paramName = $paramName, paramClass = $paramClass")
}
return true
}
private fun processClass(dao: TypeElement, file: File) {
val daoName = dao.simpleName.toString()
val packageName = processingEnv.elementUtils.getPackageOf(dao).toString()
val dbType = processingEnv.typeUtils.asElement(dao.getAnnotationClassValue<SelectiveDao> { db }) as TypeElement
val typeConverters = dbType.getAnnotationClassValues<TypeConverters> { value }.map {
processingEnv.typeUtils.asElement(it) as TypeElement
}.map { type ->
processingEnv.elementUtils.getAllMembers(type).mapNotNull { element ->
if (element is ExecutableElement) {
if (element.returnType.toString() == "java.lang.String"
|| element.returnType.toString() == "java.lang.Long"
|| element.returnType.toString() == "java.lang.Integer"
|| element.returnType.kind.isPrimitive) {
if (element.simpleName.startsWith("to") && element.parameters.isNotEmpty())
return@mapNotNull TypeConverter(element.parameters.first().asType(), type, element.simpleName, element.returnType)
}
}
null
}
}.flatten()
//processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, "c = ${typeConverters.joinToString()}")
val roomDatabase = ClassName("androidx.room", "RoomDatabase")
val selective = TypeSpec.classBuilder("${daoName}Selective")
.primaryConstructor(FunSpec.constructorBuilder()
.addParameter("__db", roomDatabase, KModifier.PRIVATE)
.build())
.addProperty(PropertySpec.builder("__db", roomDatabase)
.initializer("__db")
.addModifiers(KModifier.PRIVATE)
.build())
val usedTypeConverters = mutableSetOf<TypeConverter>()
processingEnv.elementUtils.getAllMembers(dao).forEach { element ->
if (element.kind != ElementKind.METHOD)
return@forEach
val method = element as ExecutableElement
val annotation = method.getAnnotation(UpdateSelective::class.java) ?: return@forEach
usedTypeConverters.addAll(processMethod(selective, method, annotation, typeConverters))
}
usedTypeConverters.forEach { converter ->
selective.addProperty(PropertySpec.builder("__${converter.converterType.simpleName}", converter.converterType.asType().asTypeName(), KModifier.PRIVATE)
.delegate(CodeBlock.builder()
.beginControlFlow("lazy")
.addStatement("%T()", converter.converterType.asType().asTypeName())
.endControlFlow()
.build())
.build())
}
FileSpec.builder(packageName, "${daoName}Selective")
.addType(selective.build())
.build()
.writeTo(file)
}
private fun VariableElement.name() = getAnnotation(ColumnInfo::class.java)?.name ?: simpleName.toString()
private fun processMethod(cls: TypeSpec.Builder, method: ExecutableElement, annotation: UpdateSelective, typeConverters: List<TypeConverter>): List<TypeConverter> {
val methodName = method.simpleName.toString()
val parameter = method.parameters.first()
val paramName = parameter.simpleName.toString()
val paramTypeElement = processingEnv.typeUtils.asElement(parameter.asType()) as TypeElement
val paramTypeAnnotation = paramTypeElement.getAnnotation(Entity::class.java)
val tableName = paramTypeAnnotation.tableName
val primaryKeys = annotation.primaryKeys
val skippedColumns = annotation.skippedColumns
var members = processingEnv.elementUtils.getAllMembers(paramTypeElement)
val allFields = ElementFilter.fieldsIn(members)
// check all super classes
var superType = paramTypeElement.superclass
while (superType !is NoType) {
val superTypeElement = processingEnv.typeUtils.asElement(superType) as TypeElement
members = processingEnv.elementUtils.getAllMembers(superTypeElement)
allFields += ElementFilter.fieldsIn(members)
superType = superTypeElement.superclass
}
allFields.removeAll { skippedColumns.contains(it.name()) }
allFields.removeAll { it.getAnnotation(Ignore::class.java) != null }
allFields.removeAll { field -> field.modifiers.any { it == Modifier.STATIC || it == Modifier.FINAL } }
val allFieldsDistinct = allFields.distinct()
val fields = allFieldsDistinct.filterNot { primaryKeys.contains(it.name()) }
val primaryFields = allFieldsDistinct.filter { primaryKeys.contains(it.name()) }
val fieldNames = fields.map { it.name() }
val primaryFieldNames = primaryFields.map { it.name() }
val fieldNamesQuery = fieldNames.joinToString { "$it = ?" }
val primaryFieldNamesQuery = primaryFieldNames.joinToString(" AND ") { "$it = ?" }
val query = "\"\"\"UPDATE $tableName SET $fieldNamesQuery WHERE $primaryFieldNamesQuery\"\"\""
val entityInsertionAdapter = ClassName("androidx.room", "EntityInsertionAdapter")
val supportSQLiteStatement = ClassName("androidx.sqlite.db", "SupportSQLiteStatement")
val usedTypeConverters = mutableListOf<TypeConverter>()
val bind = CodeBlock.builder()
(fields+primaryFields).forEachIndexed { i, field ->
val index = i+1
val fieldName = field.simpleName.toString()
val name = "${paramName}_$fieldName"
val realName = "${paramName}.$fieldName"
val nullable = field.getAnnotation(org.jetbrains.annotations.Nullable::class.java) != null
var param = when (field.asType().kind) {
TypeKind.BOOLEAN -> "if ($name) 1L else 0L"
TypeKind.BYTE,
TypeKind.SHORT,
TypeKind.INT -> "$name.toLong()"
TypeKind.CHAR -> "$name.toString()"
TypeKind.FLOAT -> "$name.toDouble()"
else -> when (field.asType().toString()) {
"java.lang.String" -> name
"java.lang.Boolean" -> "if ($name == true) 1L else 0L"
"java.lang.Byte",
"java.lang.Short",
"java.lang.Integer" -> "$name.toLong()"
"java.lang.Long" -> name
"java.lang.Char" -> "$name.toString()"
"java.lang.Float" -> "$name.toDouble()"
"java.lang.Double" -> name
else -> name
}
}
var isConvert = false
val bindMethod = when (field.asType().kind) {
TypeKind.BOOLEAN -> "bindLong"
TypeKind.BYTE -> "bindLong"
TypeKind.SHORT -> "bindLong"
TypeKind.INT -> "bindLong"
TypeKind.LONG -> "bindLong"
TypeKind.CHAR -> "bindString"
TypeKind.FLOAT -> "bindDouble"
TypeKind.DOUBLE -> "bindDouble"
else -> when (field.asType().toString()) {
"java.lang.String" -> "bindString"
"java.lang.Boolean" -> "bindLong"
"java.lang.Byte" -> "bindLong"
"java.lang.Short" -> "bindLong"
"java.lang.Integer" -> "bindLong"
"java.lang.Long" -> "bindLong"
"java.lang.Char" -> "bindString"
"java.lang.Float" -> "bindDouble"
"java.lang.Double" -> "bindDouble"
else -> {
val converter = typeConverters.firstOrNull {
it.dataType.toString() == field.asType().toString()
}
if (converter != null) {
param = "__${converter.converterType.simpleName}.${converter.methodName}($realName)"
param = when (converter.returnType.toString()) {
"java.lang.Integer", "int",
"java.lang.Short", "short",
"java.lang.Byte", "byte" -> "$param.toLong()"
"java.lang.Boolean", "boolean" -> "if ($param) 1L else 0L"
"java.lang.Char", "char" -> "$param.toString()"
"java.lang.Float", "float" -> "$param.toDouble()"
else -> param
}
isConvert = true
usedTypeConverters += converter
when (converter.returnType.toString()) {
"java.lang.Integer", "int",
"java.lang.Short", "short",
"java.lang.Byte", "byte",
"java.lang.Boolean", "boolean" -> "bindLong"
"java.lang.Char", "char" -> "bindString"
"java.lang.Float", "float" -> "bindDouble"
else -> "bindString"
}
}
else "bind${field.asType()}"
}
}
}
if (!isConvert) {
bind.addStatement("val $name = $realName")
}
else {
bind.addStatement("val $name = $param")
param = name
}
if (nullable) {
bind.beginControlFlow("if ($name == null)")
.addStatement("stmt.bindNull($index)")
.endControlFlow()
.beginControlFlow("else")
.addStatement("stmt.$bindMethod($index, $param)")
.endControlFlow()
}
else {
bind.addStatement("stmt.$bindMethod($index, $param)")
}
}
val adapterName = "__insertionAdapterOf$methodName"
val delegate = CodeBlock.builder().add("""
|lazy {
| object : EntityInsertionAdapter<%T>(__db) {
| override fun createQuery() = $query
| override fun bind(stmt: %T, $paramName: %T) {
|${bind.indent().indent().indent().build()}
| }
| }
|}""".trimMargin(), paramTypeElement.asClassName(), supportSQLiteStatement, paramTypeElement.asClassName())
cls.addProperty(PropertySpec.builder(adapterName, entityInsertionAdapter.parameterizedBy(paramTypeElement.asClassName()), KModifier.PRIVATE)
.delegate(delegate.build())
.build())
val list = ClassName("kotlin.collections", "List")
val longArray = ClassName("kotlin", "LongArray")
val function = FunSpec.builder(methodName)
.addModifiers(KModifier.INTERNAL)
.addParameter("item", parameter.asType().asTypeName())
.returns(Long::class.java)
.addStatement("__db.assertNotSuspendingTransaction()")
.addStatement("__db.beginTransaction()")
.addCode("""
|try {
| val _result = $adapterName.insertAndReturnId(item)
| __db.setTransactionSuccessful()
| return _result
|} finally {
| __db.endTransaction()
|}
""".trimMargin())
.build()
val functionAll = FunSpec.builder(methodName+"All")
.addModifiers(KModifier.INTERNAL)
.addParameter("items", list.parameterizedBy(parameter.asType().asTypeName()))
.returns(longArray)
.addStatement("__db.assertNotSuspendingTransaction()")
.addStatement("__db.beginTransaction()")
.addCode("""
|try {
| val _result = $adapterName.insertAndReturnIdsArray(items)
| __db.setTransactionSuccessful()
| return _result
|} finally {
| __db.endTransaction()
|}
""".trimMargin())
.build()
cls.addFunction(function)
cls.addFunction(functionAll)
return usedTypeConverters
}
override fun getSupportedAnnotationTypes(): MutableSet<String> {
return mutableSetOf(SelectiveDao::class.java.canonicalName)
}
}

View File

@ -1,3 +1,5 @@
include ':codegen'
include ':annotation'
rootProject.name='Szkolny.eu' rootProject.name='Szkolny.eu'
include ':app', ':agendacalendarview', ':mhttp', ':material-about-library', ':cafebar', ':szkolny-font', ':nachos' include ':app', ':agendacalendarview', ':mhttp', ':material-about-library', ':cafebar', ':szkolny-font', ':nachos'
/* /*