Merge branch 'develop' into hotfix/android-4-fixes

This commit is contained in:
Kuba Szczodrzyński 2021-03-28 22:16:11 +02:00 committed by GitHub
commit f72f8228f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 2839 additions and 1840 deletions

View File

@ -130,7 +130,7 @@ dependencies {
implementation "com.mikepenz:iconics-core:5.2.8"
implementation "com.mikepenz:iconics-views:5.2.8"
implementation "com.mikepenz:community-material-typeface:5.8.55.0-kotlin@aar"
implementation "eu.szkolny:szkolny-font:1dab7d64ed"
implementation "eu.szkolny:szkolny-font:1.3"
// Other dependencies
implementation "cat.ereza:customactivityoncrash:2.3.0"
@ -140,6 +140,7 @@ dependencies {
implementation "com.daimajia.swipelayout:library:1.2.0@aar"
implementation "com.github.antonKozyriatskyi:CircularProgressIndicator:1.2.2"
implementation "com.github.bassaer:chatmessageview:2.0.1"
implementation "com.github.CanHub:Android-Image-Cropper:2.2.2"
implementation "com.github.ChuckerTeam.Chucker:library:3.0.1"
implementation "com.github.jetradarmobile:android-snowfall:1.2.0"
implementation "com.github.wulkanowy.uonet-request-signer:hebe-jvm:a99ca50a31"
@ -148,7 +149,6 @@ dependencies {
implementation "com.jaredrummler:colorpicker:1.1.0"
implementation "com.qifan.powerpermission:powerpermission-coroutines:1.3.0"
implementation "com.qifan.powerpermission:powerpermission:1.3.0"
implementation "com.theartofdev.edmodo:android-image-cropper:2.8.0"
implementation "com.wdullaer:materialdatetimepicker:4.2.3"
implementation "com.yuyh.json:jsonviewer:1.0.6"
implementation "io.coil-kt:coil:1.1.1"

View File

@ -143,8 +143,7 @@
<activity android:name=".ui.modules.settings.SettingsLicenseActivity"
android:configChanges="orientation|keyboardHidden"
android:theme="@style/AppTheme" />
<activity android:name=".ui.modules.webpush.QrScannerActivity" />
<activity android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
<activity android:name="com.canhub.cropper.CropImageActivity"
android:configChanges="orientation|keyboardHidden"
android:theme="@style/Base.Theme.AppCompat" />

View File

@ -1,12 +1,9 @@
package pl.szczodrzynski.edziennik
import android.Manifest
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.content.res.Resources
import android.database.Cursor
@ -29,7 +26,6 @@ import android.view.View
import android.view.WindowManager
import android.widget.*
import androidx.annotation.*
import androidx.core.app.ActivityCompat
import androidx.core.database.getIntOrNull
import androidx.core.database.getLongOrNull
import androidx.core.database.getStringOrNull
@ -295,19 +291,6 @@ fun colorFromCssName(name: String): Int {
fun List<Profile>.filterOutArchived() = this.filter { !it.archived }
fun Activity.isStoragePermissionGranted(): Boolean {
return if (Build.VERSION.SDK_INT >= 23) {
if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
true
} else {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), 1)
false
}
} else {
true
}
}
fun Response?.getUnixDate(): Long {
val rfcDate = this?.headers()?.get("date") ?: return currentTimeUnix()
val pattern = "EEE, dd MMM yyyy HH:mm:ss Z"
@ -1235,3 +1218,41 @@ operator fun <K, V> Iterable<Pair<K, V>>.get(key: K): V? {
}
fun ByteArray.toHexString() = joinToString("") { "%02x".format(it) }
fun <E> MutableList<E>.after(what: E, insert: E) {
val index = indexOf(what)
if (index != -1)
add(index + 1, insert)
}
fun <E> MutableList<E>.before(what: E, insert: E) {
val index = indexOf(what)
if (index != -1)
add(index, insert)
}
fun <E> MutableList<E>.after(what: E, insert: Collection<E>) {
val index = indexOf(what)
if (index != -1)
addAll(index + 1, insert)
}
fun <E> MutableList<E>.before(what: E, insert: Collection<E>) {
val index = indexOf(what)
if (index != -1)
addAll(index, insert)
}
fun Context.getSyncInterval(interval: Int): String {
val hours = interval / 60 / 60
val minutes = interval / 60 % 60
val hoursText = if (hours > 0)
plural(R.plurals.time_till_hours, hours)
else
null
val minutesText = if (minutes > 0)
plural(R.plurals.time_till_minutes, minutes)
else
""
return hoursText?.plus(" $minutesText") ?: minutesText
}

View File

@ -10,13 +10,11 @@ import android.graphics.BitmapFactory
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.Settings
import android.view.Gravity
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.PopupMenu
import androidx.core.graphics.ColorUtils
import androidx.core.view.isVisible
import androidx.lifecycle.Observer
@ -56,7 +54,7 @@ import pl.szczodrzynski.edziennik.ui.dialogs.ServerMessageDialog
import pl.szczodrzynski.edziennik.ui.dialogs.UpdateAvailableDialog
import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog
import pl.szczodrzynski.edziennik.ui.dialogs.event.EventManualDialog
import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileRemoveDialog
import pl.szczodrzynski.edziennik.ui.dialogs.profile.ProfileConfigDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncViewListDialog
import pl.szczodrzynski.edziennik.ui.modules.agenda.AgendaFragment
import pl.szczodrzynski.edziennik.ui.modules.announcements.AnnouncementsFragment
@ -79,7 +77,7 @@ import pl.szczodrzynski.edziennik.ui.modules.messages.MessagesFragment
import pl.szczodrzynski.edziennik.ui.modules.messages.compose.MessagesComposeFragment
import pl.szczodrzynski.edziennik.ui.modules.notifications.NotificationsListFragment
import pl.szczodrzynski.edziennik.ui.modules.settings.ProfileManagerFragment
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsNewFragment
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsFragment
import pl.szczodrzynski.edziennik.ui.modules.timetable.TimetableFragment
import pl.szczodrzynski.edziennik.ui.modules.webpush.WebPushFragment
import pl.szczodrzynski.edziennik.utils.SwipeRefreshLayoutNoTouch
@ -97,7 +95,6 @@ import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetPrimaryItem
import pl.szczodrzynski.navlib.bottomsheet.items.BottomSheetSeparatorItem
import pl.szczodrzynski.navlib.drawer.NavDrawer
import pl.szczodrzynski.navlib.drawer.items.DrawerPrimaryItem
import java.io.File
import java.io.IOException
import java.util.*
import kotlin.coroutines.CoroutineContext
@ -110,8 +107,6 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
const val TAG = "MainActivity"
const val REQUEST_LOGIN_ACTIVITY = 20222
const val DRAWER_PROFILE_ADD_NEW = 200
const val DRAWER_PROFILE_SYNC_ALL = 201
const val DRAWER_PROFILE_EXPORT_DATA = 202
@ -199,7 +194,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
.isStatic(true)
.isBelowSeparator(true)
list += NavTarget(DRAWER_ITEM_SETTINGS, R.string.menu_settings, SettingsNewFragment::class)
list += NavTarget(DRAWER_ITEM_SETTINGS, R.string.menu_settings, SettingsFragment::class)
.withIcon(CommunityMaterial.Icon.cmd_cog_outline)
.isInDrawer(true)
.isStatic(true)
@ -257,6 +252,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
val bottomSheet: NavBottomSheet by lazy { navView.bottomSheet }
val mainSnackbar: MainSnackbar by lazy { MainSnackbar(this) }
val errorSnackbar: ErrorSnackbar by lazy { ErrorSnackbar(this) }
val requestHandler by lazy { MainActivityRequestHandler(this) }
val swipeRefreshLayout: SwipeRefreshLayoutNoTouch by lazy { b.swipeRefreshLayout }
@ -395,7 +391,13 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}
drawerProfileLongClickListener = { _, profile, _, view ->
if (view != null && profile is ProfileDrawerItem) {
showProfileContextMenu(profile, view)
launch {
val appProfile = withContext(Dispatchers.IO) {
App.db.profileDao().getByIdNow(profile.identifier.toInt())
} ?: return@launch
drawer.close()
ProfileConfigDialog(this@MainActivity, appProfile)
}
true
}
else {
@ -475,28 +477,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}
// APP BACKGROUND
if (app.config.ui.appBackground != null) {
try {
app.config.ui.appBackground?.let {
var bg = it
val bgDir = File(Environment.getExternalStoragePublicDirectory("Szkolny.eu"), "bg")
if (bgDir.exists()) {
val files = bgDir.listFiles()
val r = Random()
val i = r.nextInt(files.size)
bg = files[i].toString()
}
val linearLayout = b.root
if (bg.endsWith(".gif")) {
linearLayout.background = GifDrawable(bg)
} else {
linearLayout.background = BitmapDrawable.createFromPath(bg)
}
}
} catch (e: IOException) {
e.printStackTrace()
}
}
setAppBackground()
// IT'S WINTER MY DUDES
val today = Date.getToday()
@ -578,7 +559,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
if (App.devMode) {
bottomSheet += BottomSheetPrimaryItem(false)
.withTitle(R.string.menu_debug)
.withIcon(CommunityMaterial.Icon.cmd_android_studio)
.withIcon(CommunityMaterial.Icon.cmd_android_debug_bridge)
.withOnClickListener(View.OnClickListener { loadTarget(DRAWER_ITEM_DEBUG) })
}
}
@ -586,7 +567,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
private var profileSettingClickListener = { id: Int, view: View? ->
when (id) {
DRAWER_PROFILE_ADD_NEW -> {
startActivityForResult(Intent(this, LoginActivity::class.java), REQUEST_LOGIN_ACTIVITY)
requestHandler.requestLogin()
}
DRAWER_PROFILE_SYNC_ALL -> {
EdziennikTask.sync().enqueue(this)
@ -834,7 +815,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
handleIntent(intent?.extras)
}
}
private fun handleIntent(extras: Bundle?) {
fun handleIntent(extras: Bundle?) {
d(TAG, "handleIntent() {")
extras?.keySet()?.forEach { key ->
@ -992,13 +973,7 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_LOGIN_ACTIVITY) {
if (!app.config.loginFinished)
finish()
else {
handleIntent(data?.extras)
}
}
requestHandler.handleResult(requestCode, resultCode, data)
}
/* _ _ _ _ _
@ -1224,6 +1199,19 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
}, 3000)
}
fun setAppBackground() {
try {
b.root.background = app.config.ui.appBackground?.let {
if (it.endsWith(".gif"))
GifDrawable(it)
else
BitmapDrawable.createFromPath(it)
}
} catch (e: IOException) {
e.printStackTrace()
}
}
/* _____ _ _
| __ \ (_) |
| | | |_ __ __ ___ _____ _ __ _| |_ ___ _ __ ___ ___
@ -1299,26 +1287,6 @@ class MainActivity : AppCompatActivity(), CoroutineScope {
drawer.addProfileSettings(*drawerProfiles.toTypedArray())
}
private fun showProfileContextMenu(profile: IProfile, view: View) {
val profileId = profile.identifier.toInt()
val popupMenu = PopupMenu(this, view)
popupMenu.menu.add(0, 1, 1, R.string.profile_menu_open_settings)
popupMenu.menu.add(0, 2, 2, R.string.profile_menu_remove)
popupMenu.setOnMenuItemClickListener { item ->
if (item.itemId == 1) {
if (profileId != app.profile.id) {
loadProfile(profileId, DRAWER_ITEM_SETTINGS)
return@setOnMenuItemClickListener true
}
loadTarget(DRAWER_ITEM_SETTINGS, null)
} else if (item.itemId == 2) {
ProfileRemoveDialog(this, profileId, profile.name?.getText(this) ?: "?")
}
true
}
popupMenu.show()
}
private val targetPopToHomeList = arrayListOf<Int>()
private var targetHomeId: Int = -1
override fun onBackPressed() {

View File

@ -0,0 +1,220 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-3-23.
*/
package pl.szczodrzynski.edziennik
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.provider.OpenableColumns
import com.canhub.cropper.CropImage
import com.canhub.cropper.CropImageView
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.ui.modules.login.LoginActivity
import java.io.File
import java.io.FileOutputStream
class MainActivityRequestHandler(val activity: MainActivity) {
companion object {
private const val REQUEST_LOGIN_ACTIVITY = 2000
private const val REQUEST_FILE_HEADER_BACKGROUND = 3000
private const val REQUEST_FILE_APP_BACKGROUND = 4000
private const val REQUEST_FILE_PROFILE_IMAGE = 5000
private const val REQUEST_CROP_HEADER_BACKGROUND = 3100
private const val REQUEST_CROP_APP_BACKGROUND = 4100
private const val REQUEST_CROP_PROFILE_IMAGE = 5100
}
private val app = activity.app
private val requestData = mutableMapOf<Int, Any?>()
private val listeners = mutableMapOf<Int, (data: Any?) -> Unit>()
private val manager
get() = app.permissionManager
fun requestLogin() = activity.startActivityForResult(
Intent(activity, LoginActivity::class.java),
REQUEST_LOGIN_ACTIVITY
)
fun requestHeaderBackground(listener: (Any?) -> Unit) =
manager.requestCameraPermission(
activity, 0, isRequired = false
) {
listeners[REQUEST_FILE_HEADER_BACKGROUND] = listener
activity.startActivityForResult(
CropImage.getPickImageChooserIntent(
activity,
activity.getString(R.string.pick_image_intent_chooser_title),
true,
true
),
REQUEST_FILE_HEADER_BACKGROUND
)
}
fun requestAppBackground(listener: (Any?) -> Unit) =
manager.requestCameraPermission(
activity, 0, isRequired = false
) {
listeners[REQUEST_FILE_APP_BACKGROUND] = listener
activity.startActivityForResult(
CropImage.getPickImageChooserIntent(
activity,
activity.getString(R.string.pick_image_intent_chooser_title),
true,
true
),
REQUEST_FILE_APP_BACKGROUND
)
}
fun requestProfileImage(profile: Profile, listener: (Any?) -> Unit) =
manager.requestCameraPermission(
activity, 0, isRequired = false
) {
listeners[REQUEST_FILE_PROFILE_IMAGE] = listener
requestData[REQUEST_FILE_PROFILE_IMAGE] = profile
activity.startActivityForResult(
CropImage.getPickImageChooserIntent(
activity,
activity.getString(R.string.pick_image_intent_chooser_title),
true,
true
),
REQUEST_FILE_PROFILE_IMAGE
)
}
private fun getFileInfo(uri: Uri): Pair<String, String?> {
if (uri.scheme == "file") {
return (uri.lastPathSegment ?: "unknown") to null
}
val cursor = activity.contentResolver.query(
uri,
null,
null,
null,
null,
null
)
return cursor?.use {
if (it.moveToFirst()) {
val name = it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
val mimeIndex = it.getColumnIndex("mime_type")
val mimeType = if (mimeIndex != -1) it.getString(mimeIndex) else null
name to mimeType
} else
null
} ?: "unknown" to null
}
private fun shouldCrop(uri: Uri): Boolean {
val (filename, mimeType) = getFileInfo(uri)
return !filename.endsWith(".gif") && mimeType?.endsWith("/gif") != true
}
private fun saveFile(uri: Uri, name: String): String {
val (filename, _) = getFileInfo(uri)
val extension = filename.substringAfterLast('.')
val file = File(activity.filesDir, "$name.$extension")
activity.contentResolver.openInputStream(uri)?.use { input ->
FileOutputStream(file).use { output ->
input.copyTo(output)
}
}
return file.absolutePath
}
fun handleResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode != Activity.RESULT_OK)
return
var uri = CropImage.getPickImageResultUri(activity, data)
when (requestCode) {
REQUEST_LOGIN_ACTIVITY -> {
if (!app.config.loginFinished)
activity.finish()
else {
activity.handleIntent(data?.extras)
}
}
REQUEST_FILE_HEADER_BACKGROUND -> {
if (uri == null)
return // TODO: 2021-03-24 if the app returns no data
if (shouldCrop(uri)) {
val intent = CropImage.activity(uri)
.setAspectRatio(512, 288)
.setGuidelines(CropImageView.Guidelines.ON_TOUCH)
.setAllowFlipping(true)
.setAllowRotation(true)
.setRequestedSize(512, 288)
.getIntent(activity)
activity.startActivityForResult(intent, REQUEST_CROP_HEADER_BACKGROUND)
} else {
val path = saveFile(uri, "header")
app.config.ui.headerBackground = path
listeners.remove(REQUEST_FILE_HEADER_BACKGROUND)?.invoke(path)
}
}
REQUEST_FILE_APP_BACKGROUND -> {
if (uri == null)
return
if (shouldCrop(uri)) {
val intent = CropImage.activity(uri)
.setGuidelines(CropImageView.Guidelines.ON_TOUCH)
.setAllowFlipping(true)
.setAllowRotation(true)
.getIntent(activity)
activity.startActivityForResult(intent, REQUEST_CROP_APP_BACKGROUND)
} else {
val path = saveFile(uri, "background")
app.config.ui.appBackground = path
listeners.remove(REQUEST_FILE_APP_BACKGROUND)?.invoke(path)
}
}
REQUEST_FILE_PROFILE_IMAGE -> {
if (uri == null)
return
if (shouldCrop(uri)) {
val intent = CropImage.activity(uri)
.setAspectRatio(1, 1)
.setCropShape(CropImageView.CropShape.OVAL)
.setGuidelines(CropImageView.Guidelines.ON_TOUCH)
.setAllowFlipping(true)
.setAllowRotation(true)
.setRequestedSize(512, 512)
.getIntent(activity)
activity.startActivityForResult(intent, REQUEST_CROP_PROFILE_IMAGE)
} else {
val profile =
requestData.remove(REQUEST_FILE_PROFILE_IMAGE) as? Profile ?: return
val path = saveFile(uri, "profile${profile.id}")
profile.image = path
listeners.remove(REQUEST_FILE_PROFILE_IMAGE)?.invoke(profile)
}
}
REQUEST_CROP_HEADER_BACKGROUND -> {
uri = CropImage.getActivityResult(data)?.uri ?: return
val path = saveFile(uri, "header")
app.config.ui.headerBackground = path
listeners.remove(REQUEST_FILE_HEADER_BACKGROUND)?.invoke(path)
}
REQUEST_CROP_APP_BACKGROUND -> {
uri = CropImage.getActivityResult(data)?.uri ?: return
val path = saveFile(uri, "background")
app.config.ui.appBackground = path
listeners.remove(REQUEST_FILE_APP_BACKGROUND)?.invoke(path)
}
REQUEST_CROP_PROFILE_IMAGE -> {
uri = CropImage.getActivityResult(data)?.uri ?: return
val profile = requestData.remove(REQUEST_FILE_PROFILE_IMAGE) as? Profile ?: return
val path = saveFile(uri, "profile${profile.id}")
profile.image = path
listeners.remove(REQUEST_FILE_PROFILE_IMAGE)?.invoke(profile)
}
}
}
}

View File

@ -0,0 +1,117 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-3-20.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.bell
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.core.widget.addTextChangedListener
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.utils.models.Time
import kotlin.coroutines.CoroutineContext
class BellSyncConfigDialog(
val activity: AppCompatActivity,
val onChangeListener: (() -> Unit)? = null,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) : CoroutineScope {
companion object {
private const val TAG = "BellSyncConfigDialog"
}
private lateinit var app: App
private lateinit var dialog: AlertDialog
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local variables go here
private fun parse(input: String): Pair<Time, Int>? {
if (input.length < 8) {
return null
}
if (input[2] != ':' || input[5] != ':') {
return null
}
val multiplier = when {
input[0] == '+' -> 1
input[0] == '-' -> -1
else -> return null
}
val time = Time.fromH_m_s("0" + input.substring(1))
return time to multiplier
}
init { run {
if (activity.isFinishing)
return@run
onShowListener?.invoke(TAG)
app = activity.applicationContext as App
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.bell_sync_title)
.setView(R.layout.dialog_edit_text)
.setPositiveButton(R.string.ok, null)
.setNegativeButton(R.string.cancel, null)
.setNeutralButton(R.string.reset) { _, _ ->
app.config.timetable.bellSyncDiff = null
app.config.timetable.bellSyncMultiplier = 0
onChangeListener?.invoke()
}
.setOnDismissListener {
onDismissListener?.invoke(TAG)
}
.show()
val message = dialog.findViewById<TextView>(android.R.id.title)
val editText = dialog.findViewById<TextInputEditText>(android.R.id.text1)
val textLayout = dialog.findViewById<TextInputLayout>(R.id.text_input_layout)
message?.setText(R.string.bell_sync_adjust_content)
editText?.hint = "±H:MM:SS"
editText?.setText(app.config.timetable.bellSyncDiff?.let {
(if (app.config.timetable.bellSyncMultiplier == -1) "-" else "+") + it.stringHMS
} ?: "+0:00:00")
editText?.addTextChangedListener { text ->
val input = text?.toString()
textLayout?.error =
if (input != null && parse(input) == null)
activity.getString(R.string.bell_sync_adjust_error)
else
null
}
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.onClick {
val input = editText?.text?.toString() ?: return@onClick
val parsed = parse(input)
if (parsed == null) {
Toast.makeText(activity, R.string.bell_sync_adjust_error, Toast.LENGTH_SHORT).show()
return@onClick
}
val (time, multiplier) = parsed
app.config.timetable.bellSyncDiff =
if (time.value == 0)
null
else
time
app.config.timetable.bellSyncMultiplier = multiplier
onChangeListener?.invoke()
dialog.dismiss()
}
}}
}

View File

@ -33,7 +33,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.data.db.full.EventFull
import pl.szczodrzynski.edziennik.data.db.full.LessonFull
import pl.szczodrzynski.edziennik.databinding.DialogEventManualV2Binding
import pl.szczodrzynski.edziennik.ui.dialogs.sync.RegistrationEnableDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.RegistrationConfigDialog
import pl.szczodrzynski.edziennik.ui.modules.views.TimeDropdown.Companion.DISPLAY_LESSONS
import pl.szczodrzynski.edziennik.utils.Anim
import pl.szczodrzynski.edziennik.utils.TextInputDropDown
@ -64,7 +64,7 @@ class EventManualDialog(
private val app by lazy { activity.application as App }
private lateinit var b: DialogEventManualV2Binding
private lateinit var dialog: AlertDialog
private var profile: Profile? = null
private lateinit var profile: Profile
private var customColor: Int? = null
private val editingShared = editingEvent?.sharedBy != null
@ -80,11 +80,11 @@ class EventManualDialog(
private var progressDialog: AlertDialog? = null
init { run {
init { launch {
if (activity.isFinishing)
return@run
return@launch
onShowListener?.invoke(TAG)
EventBus.getDefault().register(this)
EventBus.getDefault().register(this@EventManualDialog)
b = DialogEventManualV2Binding.inflate(activity.layoutInflater)
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.dialog_event_manual_title)
@ -236,8 +236,15 @@ class EventManualDialog(
progressDialog?.dismiss()
}
private fun loadLists() { launch {
profile = withContext(Dispatchers.Default) { app.db.profileDao().getByIdNow(profileId) }
private fun loadLists() = launch {
val profile = withContext(Dispatchers.Default) {
app.db.profileDao().getByIdNow(profileId)
}
if (profile == null) {
Toast.makeText(activity, R.string.event_manual_no_profile, Toast.LENGTH_SHORT).show()
return@launch
}
this@EventManualDialog.profile = profile
with (b.dateDropdown) {
db = app.db
@ -380,7 +387,7 @@ class EventManualDialog(
})
colorPickerDialog.show(activity.supportFragmentManager, "color-picker-dialog")
}
}}
}
private fun showRemoveEventDialog() {
val shareNotice = when {
@ -417,12 +424,11 @@ class EventManualDialog(
val share = b.shareSwitch.isChecked
if (share && profile?.registration != Profile.REGISTRATION_ENABLED) {
RegistrationEnableDialog(activity, profileId).showEventShareDialog {
if (it != null)
profile = it
saveEvent()
}
if (share && profile.registration != Profile.REGISTRATION_ENABLED) {
RegistrationConfigDialog(activity, profile, onChangeListener = { enabled ->
if (enabled)
saveEvent()
}).showEventShareDialog()
return
}

View File

@ -13,7 +13,6 @@ import pl.szczodrzynski.edziennik.data.db.full.GradeFull
import pl.szczodrzynski.edziennik.databinding.DialogGradeDetailsBinding
import pl.szczodrzynski.edziennik.onClick
import pl.szczodrzynski.edziennik.setTintColor
import pl.szczodrzynski.edziennik.ui.dialogs.settings.GradesConfigDialog
import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
import kotlin.coroutines.CoroutineContext

View File

@ -2,7 +2,7 @@
* Copyright (c) Kacper Ziubryniewicz 2020-1-16
*/
package pl.szczodrzynski.edziennik.ui.dialogs.settings
package pl.szczodrzynski.edziennik.ui.dialogs.grade
import android.annotation.SuppressLint
import androidx.appcompat.app.AlertDialog

View File

@ -0,0 +1,100 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-3-23.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.profile
import android.content.res.ColorStateList
import androidx.appcompat.app.AlertDialog
import androidx.core.widget.addTextChangedListener
import com.google.android.material.color.MaterialColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.shape.MaterialShapeDrawable
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.databinding.DialogProfileConfigBinding
import kotlin.coroutines.CoroutineContext
class ProfileConfigDialog(
val activity: MainActivity,
val profile: Profile,
val onProfileSaved: ((profile: Profile) -> Unit)? = null,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) : CoroutineScope {
companion object {
private const val TAG = "ProfileConfigDialog"
}
private lateinit var app: App
private lateinit var b: DialogProfileConfigBinding
private lateinit var dialog: AlertDialog
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local variables go here
private var profileChanged = false
private var profileRemoved = false
init { run {
if (activity.isFinishing)
return@run
onShowListener?.invoke(TAG)
app = activity.applicationContext as App
b = DialogProfileConfigBinding.inflate(activity.layoutInflater)
dialog = MaterialAlertDialogBuilder(activity)
.setView(b.root)
.setPositiveButton(R.string.close, null)
.setOnDismissListener {
if (!profileRemoved && profileChanged) {
app.profileSave(profile)
onProfileSaved?.invoke(profile)
}
onDismissListener?.invoke(TAG)
}
.show()
b.profile = profile
profile.applyImageTo(b.image)
// I can't believe how simple it is to get the dialog's background color !!
val shape = MaterialShapeDrawable(activity, null, R.attr.alertDialogStyle, R.style.MaterialAlertDialog_MaterialComponents)
val surface = MaterialColors.getColor(activity, R.attr.colorSurface, TAG)
shape.setCornerSize(18.dp.toFloat())
shape.initializeElevationOverlay(activity)
shape.fillColor = ColorStateList.valueOf(surface)
shape.elevation = 16.dp.toFloat()
b.circleView.background = shape
b.nameEdit.addTextChangedListener {
profileChanged = true
}
b.syncSwitch.onChange { _, _ ->
profileChanged = true
}
b.imageButton.onClick {
activity.requestHandler.requestProfileImage(profile) {
val profile = it as? Profile ?: return@requestProfileImage
if (this@ProfileConfigDialog.profile == profile) {
profileChanged = true
b.profile = profile
b.image.setImageDrawable(profile.getImageDrawable(activity))
}
}
}
b.logoutButton.onClick {
ProfileRemoveDialog(activity, profile.id, profile.name) {
profileRemoved = true
dialog.dismiss()
}
}
}}
}

View File

@ -2,7 +2,7 @@
* Copyright (c) Kuba Szczodrzyński 2019-11-13.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.settings
package pl.szczodrzynski.edziennik.ui.dialogs.profile
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
@ -18,7 +18,8 @@ class ProfileRemoveDialog(
val activity: MainActivity,
val profileId: Int,
val profileName: String,
val noProfileRemoval: Boolean = false
val noProfileRemoval: Boolean = false,
val onRemove: (() -> Unit)? = null
) : CoroutineScope {
companion object {
private const val TAG = "ProfileRemoveDialog"
@ -95,5 +96,6 @@ class ProfileRemoveDialog(
dialog.dismiss()
activity.reloadTarget()
Toast.makeText(activity, R.string.dialog_profile_remove_success, Toast.LENGTH_LONG).show()
onRemove?.invoke()
}}
}

View File

@ -0,0 +1,71 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-3-19.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.settings
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.*
import kotlin.coroutines.CoroutineContext
class AppLanguageDialog(
val activity: AppCompatActivity,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) : CoroutineScope {
companion object {
private const val TAG = "AppLanguageDialog"
}
private lateinit var app: App
private lateinit var dialog: AlertDialog
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local variables go here
init { run {
if (activity.isFinishing)
return@run
onShowListener?.invoke(TAG)
app = activity.applicationContext as App
val languages = mapOf(
null to R.string.language_system,
"pl" to R.string.language_polish,
"en" to R.string.language_english,
"de" to R.string.language_german
)
val languageIds = languages.map { it.key }
val languageNames = languages.map {
activity.getString(it.value)
}
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.app_language_dialog_title)
//.setMessage(R.string.settings_about_language_dialog_text)
.setSingleChoiceItems(
languageNames.toTypedArray(),
languageIds.indexOf(app.config.ui.language),
null
)
.setPositiveButton(R.string.ok) { _, _ ->
val which = dialog.listView.checkedItemPosition
app.config.ui.language = languageIds[which]
activity.recreate()
}
.setNegativeButton(R.string.cancel, null)
.setOnDismissListener {
onDismissListener?.invoke(TAG)
}
.show()
}}
}

View File

@ -0,0 +1,96 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-3-18.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.settings
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_AGENDA
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_ANNOUNCEMENTS
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_ATTENDANCE
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_BEHAVIOUR
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_GRADES
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_HOME
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_HOMEWORK
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_MESSAGES
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_NOTIFICATIONS
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_SETTINGS
import pl.szczodrzynski.edziennik.MainActivity.Companion.DRAWER_ITEM_TIMETABLE
import pl.szczodrzynski.edziennik.R
import kotlin.coroutines.CoroutineContext
class MiniMenuConfigDialog(
val activity: AppCompatActivity,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) : CoroutineScope {
companion object {
private const val TAG = "MiniMenuConfigDialog"
}
private lateinit var app: App
private lateinit var dialog: AlertDialog
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local variables go here
init { run {
if (activity.isFinishing)
return@run
onShowListener?.invoke(TAG)
app = activity.applicationContext as App
val buttons = mapOf(
DRAWER_ITEM_HOME to R.string.menu_home_page,
DRAWER_ITEM_TIMETABLE to R.string.menu_timetable,
DRAWER_ITEM_AGENDA to R.string.menu_agenda,
DRAWER_ITEM_GRADES to R.string.menu_grades,
DRAWER_ITEM_MESSAGES to R.string.menu_messages,
DRAWER_ITEM_HOMEWORK to R.string.menu_homework,
DRAWER_ITEM_BEHAVIOUR to R.string.menu_notices,
DRAWER_ITEM_ATTENDANCE to R.string.menu_attendance,
DRAWER_ITEM_ANNOUNCEMENTS to R.string.menu_announcements,
DRAWER_ITEM_NOTIFICATIONS to R.string.menu_notifications,
DRAWER_ITEM_SETTINGS to R.string.menu_settings
)
val miniMenuButtons = app.config.ui.miniMenuButtons
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.settings_theme_mini_drawer_buttons_dialog_title)
//.setMessage(R.string.settings_theme_mini_drawer_buttons_dialog_text)
.setMultiChoiceItems(
buttons.map { activity.getString(it.value) }.toTypedArray(),
buttons.map { it.key in miniMenuButtons }.toBooleanArray(),
null
)
.setPositiveButton(R.string.ok) { _, _ ->
app.config.ui.miniMenuButtons =
buttons.keys.mapIndexedNotNull { index, id ->
if (dialog.listView.checkedItemPositions[index])
id
else
null
}
if (activity is MainActivity) {
activity.setDrawerItems()
activity.drawer.updateBadges()
}
}
.setNegativeButton(R.string.cancel, null)
.setOnDismissListener {
onDismissListener?.invoke(TAG)
}
.show()
}}
}

View File

@ -0,0 +1,65 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-3-18.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.settings
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.utils.Themes
import kotlin.coroutines.CoroutineContext
class ThemeChooserDialog(
val activity: AppCompatActivity,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) : CoroutineScope {
companion object {
private const val TAG = "ThemeChooserDialog"
}
private lateinit var app: App
private lateinit var dialog: AlertDialog
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local variables go here
init { run {
if (activity.isFinishing)
return@run
onShowListener?.invoke(TAG)
app = activity.applicationContext as App
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.settings_theme_theme_text)
.setSingleChoiceItems(
Themes.getThemeNames(activity).toTypedArray(),
Themes.themeIndex,
null
)
.setPositiveButton(R.string.ok) { _, _ ->
val which = dialog.listView.checkedItemPosition
val theme = Themes.themeList[which]
if (app.config.ui.theme == theme.id)
return@setPositiveButton
app.config.ui.theme = theme.id
Themes.themeIndex = which
activity.recreate()
}
.setNegativeButton(R.string.cancel, null)
.setOnDismissListener {
onDismissListener?.invoke(TAG)
}
.show()
}}
}

View File

@ -0,0 +1,107 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-3-20.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.sync
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.utils.models.Time
import kotlin.coroutines.CoroutineContext
class QuietHoursConfigDialog(
val activity: AppCompatActivity,
val onChangeListener: (() -> Unit)? = null,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) : CoroutineScope {
companion object {
private const val TAG = "QuietHoursConfigDialog"
}
private lateinit var app: App
private lateinit var dialog: AlertDialog
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local variables go here
init { run {
if (activity.isFinishing)
return@run
onShowListener?.invoke(TAG)
app = activity.applicationContext as App
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.settings_sync_quiet_hours_dialog_title)
.setItems(arrayOf(
activity.getString(R.string.settings_sync_quiet_hours_set_beginning),
activity.getString(R.string.settings_sync_quiet_hours_set_end)
)) { dialog, which ->
when (which) {
0 -> configStartTime()
1 -> configEndTime()
}
dialog.dismiss()
}
.setNegativeButton(R.string.cancel, null)
.setOnDismissListener {
onDismissListener?.invoke(TAG)
}
.show()
}}
private fun configStartTime() {
onShowListener?.invoke(TAG + "Start")
val time = app.config.sync.quietHoursStart ?: return
val picker = MaterialTimePicker.Builder()
.setTitleText(R.string.settings_sync_quiet_hours_set_beginning)
.setTimeFormat(TimeFormat.CLOCK_24H)
.setHour(time.hour)
.setMinute(time.minute)
.build()
picker.show(activity.supportFragmentManager, TAG)
picker.addOnPositiveButtonClickListener {
app.config.sync.quietHoursEnabled = true
app.config.sync.quietHoursStart = Time(picker.hour, picker.minute, 0)
onChangeListener?.invoke()
}
picker.addOnDismissListener {
onDismissListener?.invoke(TAG + "Start")
}
}
private fun configEndTime() {
onShowListener?.invoke(TAG + "End")
val time = app.config.sync.quietHoursEnd ?: return
val picker = MaterialTimePicker.Builder()
.setTitleText(R.string.settings_sync_quiet_hours_set_end)
.setTimeFormat(TimeFormat.CLOCK_24H)
.setHour(time.hour)
.setMinute(time.minute)
.build()
picker.show(activity.supportFragmentManager, TAG)
picker.addOnPositiveButtonClickListener {
app.config.sync.quietHoursEnabled = true
app.config.sync.quietHoursEnd = Time(picker.hour, picker.minute, 0)
onChangeListener?.invoke()
}
picker.addOnDismissListener {
onDismissListener?.invoke(TAG + "End")
}
}
}

View File

@ -0,0 +1,149 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-15.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.sync
import android.text.Html
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.api.task.AppSync
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import kotlin.coroutines.CoroutineContext
class RegistrationConfigDialog(
val activity: AppCompatActivity,
val profile: Profile,
val onChangeListener: ((enabled: Boolean) -> Unit)? = null,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) : CoroutineScope {
companion object {
private const val TAG = "RegistrationEnableDialog"
}
private lateinit var app: App
private lateinit var dialog: AlertDialog
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local variables go here
init { run {
if (activity.isFinishing)
return@run
app = activity.applicationContext as App
}}
fun showEventShareDialog() {
onShowListener?.invoke(TAG + "EventShare")
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.registration_config_event_sharing_title)
.setMessage(R.string.registration_config_event_sharing_text)
.setPositiveButton(R.string.i_agree) { _, _ ->
enableRegistration()
}
.setNegativeButton(R.string.cancel, null)
.setOnDismissListener {
onDismissListener?.invoke(TAG + "EventShare")
}
.show()
}
fun showEnableDialog() {
onShowListener?.invoke(TAG + "Enable")
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.registration_config_title)
.setMessage(Html.fromHtml(app.getString(R.string.registration_config_enable_text)))
.setPositiveButton(R.string.i_agree) { _, _ ->
enableRegistration()
}
.setNegativeButton(R.string.i_disagree, null)
.setOnDismissListener {
onDismissListener?.invoke(TAG + "Enable")
}
.show()
}
fun showDisableDialog() {
onShowListener?.invoke(TAG + "Disable")
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.registration_config_title)
.setMessage(Html.fromHtml(app.getString(R.string.registration_config_disable_text)))
.setPositiveButton(R.string.ok) { _, _ ->
disableRegistration()
}
.setNegativeButton(R.string.cancel, null)
.setOnDismissListener {
onDismissListener?.invoke(TAG + "Disable")
}
.show()
}
private fun enableRegistration() = launch {
onShowListener?.invoke(TAG + "Enabling")
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.please_wait)
.setMessage(R.string.registration_config_enable_progress_text)
.setCancelable(false)
.setOnDismissListener {
onDismissListener?.invoke(TAG + "Enabling")
}
.show()
withContext(Dispatchers.Default) {
profile.registration = Profile.REGISTRATION_ENABLED
// force full registration of the user
App.config.getFor(profile.id).hash = ""
SzkolnyApi(app).runCatching(activity) {
AppSync(app, mutableListOf(), listOf(profile), this).run(
0L,
markAsSeen = true
)
}
app.db.profileDao().add(profile)
if (profile.id == App.profileId) {
App.profile.registration = profile.registration
}
}
dialog.dismiss()
onChangeListener?.invoke(true)
}
private fun disableRegistration() = launch {
onShowListener?.invoke(TAG + "Disabling")
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.please_wait)
.setMessage(R.string.registration_config_disable_progress_text)
.setCancelable(false)
.setOnDismissListener {
onDismissListener?.invoke(TAG + "Disabling")
}
.show()
withContext(Dispatchers.Default) {
profile.registration = Profile.REGISTRATION_DISABLED
SzkolnyApi(app).runCatching(activity) {
unregisterAppUser(profile.userCode)
}
app.db.profileDao().add(profile)
if (profile.id == App.profileId) {
App.profile.registration = profile.registration
}
}
dialog.dismiss()
onChangeListener?.invoke(false)
}
}

View File

@ -1,89 +0,0 @@
/*
* Copyright (c) Kuba Szczodrzyński 2020-3-15.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.sync
import android.text.Html
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.*
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.data.api.szkolny.SzkolnyApi
import pl.szczodrzynski.edziennik.data.api.task.AppSync
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import kotlin.coroutines.CoroutineContext
class RegistrationEnableDialog(
val activity: AppCompatActivity,
val profileId: Int
) : CoroutineScope {
companion object {
private const val TAG = "RegistrationEnableDialog"
}
private lateinit var app: App
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local variables go here
private var progressDialog: AlertDialog? = null
init { run {
if (activity.isFinishing)
return@run
app = activity.applicationContext as App
}}
fun showEventShareDialog(onSuccess: (profile: Profile?) -> Unit) {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.event_manual_need_registration_title)
.setMessage(R.string.event_manual_need_registration_text)
.setPositiveButton(R.string.ok) { dialog, which ->
enableRegistration(onSuccess)
}
.setNegativeButton(R.string.cancel, null)
.show()
}
fun showEnableDialog(onSuccess: (profile: Profile?) -> Unit) {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.registration_enable_dialog_title)
.setMessage(Html.fromHtml(app.getString(R.string.registration_enable_dialog_text)))
.setPositiveButton(R.string.ok) { dialog, which ->
enableRegistration(onSuccess)
}
.setNegativeButton(R.string.cancel, null)
.show()
}
private fun enableRegistration(onSuccess: (profile: Profile?) -> Unit) { launch {
progressDialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.please_wait)
.setMessage(R.string.registration_enable_progress_text)
.setCancelable(false)
.show()
val profile = withContext(Dispatchers.Default) {
val profile = app.db.profileDao().getByIdNow(profileId) ?: return@withContext null
profile.registration = Profile.REGISTRATION_ENABLED
// force full registration of the user
App.config.getFor(profile.id).hash = ""
AppSync(app, mutableListOf(), listOf(profile), SzkolnyApi(app)).run(0L, markAsSeen = true)
app.db.profileDao().add(profile)
if (profile.id == App.profileId) {
App.profile.registration = profile.registration
}
return@withContext profile
}
progressDialog?.dismiss()
onSuccess(profile)
}}
}

View File

@ -0,0 +1,77 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-3-20.
*/
package pl.szczodrzynski.edziennik.ui.dialogs.sync
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import pl.szczodrzynski.edziennik.*
import kotlin.coroutines.CoroutineContext
class SyncIntervalDialog(
val activity: AppCompatActivity,
val onChangeListener: (() -> Unit)? = null,
val onShowListener: ((tag: String) -> Unit)? = null,
val onDismissListener: ((tag: String) -> Unit)? = null
) : CoroutineScope {
companion object {
private const val TAG = "SyncIntervalDialog"
}
private lateinit var app: App
private lateinit var dialog: AlertDialog
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
// local variables go here
init { run {
if (activity.isFinishing)
return@run
onShowListener?.invoke(TAG)
app = activity.applicationContext as App
val intervals = listOf(
30 * MINUTE,
45 * MINUTE,
60 * MINUTE,
90 * MINUTE,
2 * HOUR,
3 * HOUR,
4 * HOUR,
6 * HOUR,
10 * HOUR
)
val intervalNames = intervals.map {
activity.getSyncInterval(it.toInt())
}
dialog = MaterialAlertDialogBuilder(activity)
.setTitle(R.string.settings_sync_sync_interval_dialog_title)
//.setMessage(R.string.settings_sync_sync_interval_dialog_text)
.setSingleChoiceItems(
intervalNames.toTypedArray(),
intervals.indexOf(app.config.sync.interval.toLong()),
null
)
.setPositiveButton(R.string.ok) { _, _ ->
val which = dialog.listView.checkedItemPosition
val interval = intervals[which]
app.config.sync.interval = interval.toInt()
onChangeListener?.invoke()
}
.setNegativeButton(R.string.cancel, null)
.setOnDismissListener {
onDismissListener?.invoke(TAG)
}
.show()
}}
}

View File

@ -16,7 +16,7 @@ import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.*
import pl.szczodrzynski.edziennik.data.db.entity.Event
import pl.szczodrzynski.edziennik.databinding.LabFragmentBinding
import pl.szczodrzynski.edziennik.ui.dialogs.settings.ProfileRemoveDialog
import pl.szczodrzynski.edziennik.ui.dialogs.profile.ProfileRemoveDialog
import pl.szczodrzynski.edziennik.ui.modules.base.lazypager.LazyFragment
import pl.szczodrzynski.edziennik.utils.TextInputDropDown
import pl.szczodrzynski.fslogin.decode

View File

@ -23,7 +23,7 @@ import pl.szczodrzynski.edziennik.data.db.entity.Metadata.TYPE_GRADE
import pl.szczodrzynski.edziennik.data.db.full.GradeFull
import pl.szczodrzynski.edziennik.databinding.GradesListFragmentBinding
import pl.szczodrzynski.edziennik.ui.dialogs.grade.GradeDetailsDialog
import pl.szczodrzynski.edziennik.ui.dialogs.settings.GradesConfigDialog
import pl.szczodrzynski.edziennik.ui.dialogs.grade.GradesConfigDialog
import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesAverages
import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesSemester
import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesStats

View File

@ -15,7 +15,7 @@ import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.databinding.GradesItemStatsBinding
import pl.szczodrzynski.edziennik.onClick
import pl.szczodrzynski.edziennik.ui.dialogs.settings.GradesConfigDialog
import pl.szczodrzynski.edziennik.ui.dialogs.grade.GradesConfigDialog
import pl.szczodrzynski.edziennik.ui.modules.grades.GradesAdapter
import pl.szczodrzynski.edziennik.ui.modules.grades.models.GradesStats
import java.text.DecimalFormat

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-3-17.
*/
package pl.szczodrzynski.edziennik.ui.modules.settings
import android.content.Context
import android.view.View
import com.danielstone.materialaboutlibrary.holders.MaterialAboutItemViewHolder
import com.danielstone.materialaboutlibrary.items.MaterialAboutTitleItem
class MaterialAboutProfileItem(item: MaterialAboutTitleItem) : MaterialAboutTitleItem(item) {
companion object {
fun getViewHolder(view: View): MaterialAboutItemViewHolder =
MaterialAboutTitleItem.getViewHolder(view)
fun setupItem(
holder: MaterialAboutTitleItemViewHolder,
item: MaterialAboutProfileItem,
context: Context
) = MaterialAboutTitleItem.setupItem(holder, item, context)
}
override fun getType(): Int {
return SettingsViewTypeManager.ItemType.PROFILE_ITEM
}
override fun getDetailString() = "MaterialAboutProfileItem{" +
"text=" + text +
", textRes=" + textRes +
", desc=" + desc +
", descRes=" + descRes +
", icon=" + icon +
", iconRes=" + iconRes +
", onClickAction=" + onClickAction +
", onLongClickAction=" + onLongClickAction +
'}'
override fun clone() = MaterialAboutProfileItem(this)
}

View File

@ -0,0 +1,28 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-3-17.
*/
package pl.szczodrzynski.edziennik.ui.modules.settings
import com.danielstone.materialaboutlibrary.items.MaterialAboutItem
import com.danielstone.materialaboutlibrary.model.MaterialAboutCard
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.MainActivity
abstract class SettingsCard(
protected val util: SettingsUtil,
) {
protected val app: App = util.activity.application as App
protected val activity: MainActivity = util.activity
protected val configGlobal by lazy { app.config }
protected val configProfile by lazy { app.config.forProfile() }
val card by lazy {
buildCard()
}
protected abstract fun buildCard(): MaterialAboutCard
protected abstract fun getItems(): List<MaterialAboutItem>
protected open fun getItemsMore(): List<MaterialAboutItem> = listOf()
}

View File

@ -0,0 +1,62 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-3-16.
*/
package pl.szczodrzynski.edziennik.ui.modules.settings
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.danielstone.materialaboutlibrary.MaterialAboutFragment
import com.danielstone.materialaboutlibrary.model.MaterialAboutList
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.ui.modules.settings.cards.*
import kotlin.coroutines.CoroutineContext
class SettingsFragment : MaterialAboutFragment(), CoroutineScope {
companion object {
private const val TAG = "SettingsFragment"
}
private lateinit var app: App
private lateinit var activity: MainActivity
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private val util by lazy {
SettingsUtil(activity) {
refreshMaterialAboutList()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
activity = (getActivity() as MainActivity?) ?: return null
app = activity.application as App
return super.onCreateView(inflater, container, savedInstanceState)
}
override fun getViewTypeManager() =
SettingsViewTypeManager()
override fun getMaterialAboutList(activityContext: Context?): MaterialAboutList {
return MaterialAboutList(
SettingsProfileCard(util).card,
SettingsThemeCard(util).card,
SettingsSyncCard(util).card,
SettingsRegisterCard(util).card,
SettingsAboutCard(util).card,
)
}
}

View File

@ -3,8 +3,8 @@ package pl.szczodrzynski.edziennik.ui.modules.settings
import android.content.Context
import android.net.Uri
import android.os.Bundle
import com.danielstone.materialaboutlibrary.ConvenienceBuilder
import com.danielstone.materialaboutlibrary.ConvenienceBuilder.createLicenseCard
import com.danielstone.materialaboutlibrary.MaterialAboutActivity
import com.danielstone.materialaboutlibrary.items.MaterialAboutActionItem
import com.danielstone.materialaboutlibrary.model.MaterialAboutCard
@ -14,169 +14,374 @@ import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.resolveColor
import pl.szczodrzynski.edziennik.utils.Themes
class SettingsLicenseActivity : MaterialAboutActivity() {
var foregroundColor: Int = 0
private val icon
get() = IconicsDrawable(this).apply {
icon = CommunityMaterial.Icon.cmd_book_outline
colorInt = foregroundColor
sizeDp = 18
}
override fun onCreate(savedInstanceState: Bundle?) {
val app = application as App
setTheme(Themes.appTheme)
foregroundColor = Themes.getPrimaryTextColor(this)
setTheme(
if (Themes.isDark)
R.style.Theme_MaterialComponents
else
R.style.Theme_MaterialComponents_Light
)
foregroundColor = if (Themes.isDark)
R.color.primaryTextDark.resolveColor(this)
else
R.color.primaryTextLight.resolveColor(this)
super.onCreate(savedInstanceState)
}
private fun createLicenseCard(
context: Context,
libraryTitle: CharSequence,
copyrightYear: CharSequence,
copyrightName: CharSequence,
license: OpenSourceLicense,
libraryUrl: String): MaterialAboutCard {
val licenseItem = MaterialAboutActionItem.Builder()
.icon(IconicsDrawable(this).apply {
icon = CommunityMaterial.Icon.cmd_book_outline
colorInt = foregroundColor
sizeDp = 18
})
.setIconGravity(MaterialAboutActionItem.GRAVITY_TOP)
.text(libraryTitle)
.subText(String.format(getString(license.resourceId), copyrightYear, copyrightName))
.setOnClickAction(ConvenienceBuilder.createWebsiteOnClickAction(context, Uri.parse(libraryUrl)))
.build()
return MaterialAboutCard.Builder().addItem(licenseItem).build()
private fun license(
title: String,
year: String,
copyright: String,
license: OpenSourceLicense,
url: String
): MaterialAboutCard {
return createLicenseCard(this, icon, title, year, copyright, license).also {
(it.items[0] as MaterialAboutActionItem).onClickAction =
ConvenienceBuilder.createWebsiteOnClickAction(
this,
Uri.parse(url)
)
}
}
override fun getMaterialAboutList(context: Context): MaterialAboutList {
override fun getMaterialAboutList(context: Context) = MaterialAboutList(
license(
"Kotlin",
"2000-2020",
"JetBrains s.r.o. and Kotlin Programming Language contributors.",
OpenSourceLicense.APACHE_2,
"https://github.com/JetBrains/kotlin"
),
return MaterialAboutList(
createLicenseCard(this,
"OkHttp",
"",
"square",
OpenSourceLicense.APACHE_2,
"https://github.com/square/okhttp/"),
createLicenseCard(this,
"MHttp",
"2018",
"Mot.",
OpenSourceLicense.APACHE_2,
"https://github.com/motcwang/MHttp/"),
createLicenseCard(this,
"AgendaCalendarView",
"2015",
"Thibault Guégan",
OpenSourceLicense.APACHE_2,
"https://github.com/Tibolte/AgendaCalendarView/"),
createLicenseCard(this,
"Material Calendar View",
"2017",
"Applandeo sp. z o.o.",
OpenSourceLicense.APACHE_2,
"https://github.com/Applandeo/Material-Calendar-View/"),
createLicenseCard(this,
"Custom Activity On Crash",
"",
"Eduard Ereza MartĂ­nez (Ereza)",
OpenSourceLicense.APACHE_2,
"https://github.com/Ereza/CustomActivityOnCrash/"),
createLicenseCard(this,
"Android-Iconics",
"2018",
"Mike Penz",
OpenSourceLicense.APACHE_2,
"https://github.com/mikepenz/Android-Iconics/"),
createLicenseCard(this,
"MaterialDrawer",
"2016",
"Mike Penz",
OpenSourceLicense.APACHE_2,
"https://github.com/mikepenz/MaterialDrawer/"),
createLicenseCard(this,
"Material Dialogs",
"2014-2016",
"Aidan Michael Follestad",
OpenSourceLicense.MIT,
"https://github.com/afollestad/material-dialogs/"),
createLicenseCard(this,
"MaterialDateTimePicker",
"2014",
"Wouter Dullaert",
OpenSourceLicense.APACHE_2,
"https://github.com/wdullaer/MaterialDateTimePicker/"),
createLicenseCard(this,
"ColorPicker",
"2016",
"Jared Rummler, 2015 Daniel Nilsson",
OpenSourceLicense.APACHE_2,
"https://github.com/jaredrummler/ColorPicker/"),
createLicenseCard(this,
"material-about-library",
"2016-2018",
"Daniel Stone",
OpenSourceLicense.APACHE_2,
"https://github.com/daniel-stoneuk/material-about-library/"),
createLicenseCard(this,
"material-intro",
"2017",
"Jan Heinrich Reimer",
OpenSourceLicense.MIT,
"https://github.com/heinrichreimer/material-intro/"),
createLicenseCard(this,
"JsonViewer",
"2017",
"smuyyh",
OpenSourceLicense.APACHE_2,
"https://github.com/smuyyh/JsonViewer/"),
createLicenseCard(this,
"ShortcutBadger",
"2014",
"Leo Lin",
OpenSourceLicense.APACHE_2,
"https://github.com/leolin310148/ShortcutBadger/"),
createLicenseCard(this,
"Android Image Cropper",
"2016",
"Arthur Teplitzki, 2013 Edmodo, Inc.",
OpenSourceLicense.APACHE_2,
"https://github.com/ArthurHub/Android-Image-Cropper/"),
createLicenseCard(this,
"Android Swipe Layout",
"2014",
"代码家 (daimajia)",
OpenSourceLicense.MIT,
"https://github.com/daimajia/AndroidSwipeLayout/"),
createLicenseCard(this,
"barcodescanner (ZXing)",
"2014",
"Dushyanth Maguluru",
OpenSourceLicense.APACHE_2,
"https://github.com/dm77/barcodescanner/"),
createLicenseCard(this,
"CircularProgressIndicator",
"2018",
"Anton Kozyriatskyi",
OpenSourceLicense.APACHE_2,
"https://github.com/antonKozyriatskyi/CircularProgressIndicator/")
license(
"Android Jetpack",
"",
"The Android Open Source Project",
OpenSourceLicense.APACHE_2,
"https://github.com/androidx/androidx"
),
license(
"Material Components for Android",
"2014-2020",
"Google, Inc.",
OpenSourceLicense.APACHE_2,
"https://github.com/material-components/material-components-android"
),
/*createLicenseCard(this,
"NoNonsense-FilePicker",
"",
"Jonas Kalderstam (spacecowboy)",
OpenSourceLicense.GNU_GPL_3,
"https://github.com/spacecowboy/NoNonsense-FilePicker/")*/
license(
"OkHttp",
"2019",
"Square, Inc.",
OpenSourceLicense.APACHE_2,
"https://github.com/square/okhttp"
),
license(
"Retrofit",
"2013",
"Square, Inc.",
OpenSourceLicense.APACHE_2,
"https://github.com/square/retrofit"
),
license(
"Gson",
"2008",
"Google Inc.",
OpenSourceLicense.APACHE_2,
"https://github.com/google/gson"
),
license(
"jsoup",
"2009-2021",
"Jonathan Hedley",
OpenSourceLicense.MIT,
"https://github.com/jhy/jsoup"
),
license(
"jspoon",
"2017",
"Droids On Roids",
OpenSourceLicense.MIT,
"https://github.com/DroidsOnRoids/jspoon"
),
license(
"AgendaCalendarView",
"2015",
"Thibault Guégan",
OpenSourceLicense.APACHE_2,
"https://github.com/szkolny-eu/agendacalendarview"
),
license(
"CafeBar",
"2017",
"Dani Mahardhika",
OpenSourceLicense.APACHE_2,
"https://github.com/szkolny-eu/cafebar"
),
license(
"FSLogin",
"2021",
"kuba2k2",
OpenSourceLicense.MIT,
"https://github.com/szkolny-eu/FSLogin"
),
license(
"material-about-library",
"2016-2020",
"Daniel Stone",
OpenSourceLicense.APACHE_2,
"https://github.com/szkolny-eu/material-about-library"
),
license(
"MHttp",
"2018",
"Mot.",
OpenSourceLicense.APACHE_2,
"https://github.com/szkolny-eu/mhttp"
),
license(
"Nachos for Android",
"2016",
"Hootsuite Media, Inc.",
OpenSourceLicense.APACHE_2,
"https://github.com/szkolny-eu/nachos"
),
license(
"Material Number Sliding Picker",
"2019",
"Alessandro Crugnola",
OpenSourceLicense.MIT,
"https://github.com/kuba2k2/NumberSlidingPicker"
),
license(
"RecyclerTabLayout",
"2017",
"nshmura",
OpenSourceLicense.APACHE_2,
"https://github.com/kuba2k2/RecyclerTabLayout"
),
license(
"Tachyon",
"2019",
"LinkedIn Corporation",
OpenSourceLicense.BSD,
"https://github.com/kuba2k2/Tachyon"
),
license(
"Android-Iconics",
"2021",
"Mike Penz",
OpenSourceLicense.APACHE_2,
"https://github.com/mikepenz/Android-Iconics"
),
license(
"Custom Activity On Crash library",
"2020",
"Eduard Ereza Martínez",
OpenSourceLicense.APACHE_2,
"https://github.com/Ereza/CustomActivityOnCrash"
),
license(
"Material-Calendar-View",
"2017",
"Applandeo sp. z o.o.",
OpenSourceLicense.APACHE_2,
"https://github.com/Applandeo/Material-Calendar-View"
),
license(
"Android Swipe Layout",
"2014",
"代码家",
OpenSourceLicense.MIT,
"https://github.com/daimajia/AndroidSwipeLayout"
),
license(
"CircularProgressIndicator",
"2018",
"Anton Kozyriatskyi",
OpenSourceLicense.APACHE_2,
"https://github.com/antonKozyriatskyi/CircularProgressIndicator"
),
license(
"ChatMessageView",
"2019",
"Tsubasa Nakayama",
OpenSourceLicense.APACHE_2,
"https://github.com/bassaer/ChatMessageView"
),
license(
"Android Image Cropper",
"2016 Arthur Teplitzki,",
"2013 Edmodo, Inc.",
OpenSourceLicense.APACHE_2,
"https://github.com/CanHub/Android-Image-Cropper"
),
license(
"Chucker",
"2018-2020 Chucker Team,",
"2017 Jeff Gilfelt",
OpenSourceLicense.APACHE_2,
"https://github.com/ChuckerTeam/chucker"
),
license(
"Android-Snowfall",
"2016",
"JetRadar",
OpenSourceLicense.APACHE_2,
"https://github.com/JetradarMobile/android-snowfall"
),
license(
"UONET+ Request Signer",
"2019",
"Wulkanowy",
OpenSourceLicense.MIT,
"https://github.com/wulkanowy/uonet-request-signer"
),
license(
"material-intro",
"2017",
"Jan Heinrich Reimer",
OpenSourceLicense.MIT,
"https://github.com/heinrichreimer/material-intro"
),
license(
"HyperLog Android",
"2018",
"HyperTrack",
OpenSourceLicense.MIT,
"https://github.com/hypertrack/hyperlog-android"
),
license(
"Color Picker",
"2016 Jared Rummler,",
"2015 Daniel Nilsson",
OpenSourceLicense.APACHE_2,
"https://github.com/jaredrummler/ColorPicker"
),
license(
"PowerPermission",
"2020",
"Qifan Yang",
OpenSourceLicense.APACHE_2,
"https://github.com/underwindfall/PowerPermission"
),
license(
"Material DateTime Picker",
"2015",
"Wouter Dullaert",
OpenSourceLicense.APACHE_2,
"https://github.com/wdullaer/MaterialDateTimePicker"
),
license(
"JsonViewer",
"2017",
"smuyyh",
OpenSourceLicense.APACHE_2,
"https://github.com/smuyyh/JsonViewer"
),
license(
"Coil",
"2021",
"Coil Contributors",
OpenSourceLicense.APACHE_2,
"https://github.com/coil-kt/coil"
),
license(
"Barcode Scanner (ZXing)",
"2014",
"Dushyanth Maguluru",
OpenSourceLicense.APACHE_2,
"https://github.com/dm77/barcodescanner"
),
license(
"AutoFitTextView",
"2014",
"Grantland Chew",
OpenSourceLicense.APACHE_2,
"https://github.com/grantland/android-autofittextview"
),
license(
"ShortcutBadger",
"2014",
"Leo Lin",
OpenSourceLicense.APACHE_2,
"https://github.com/leolin310148/ShortcutBadger"
),
license(
"EventBus",
"2012-2020",
"Markus Junginger, greenrobot",
OpenSourceLicense.APACHE_2,
"https://github.com/greenrobot/EventBus"
),
license(
"android-gif-drawable",
"2013 - present,",
"Karol Wrótniak, Droids on Roids LLC\n",
OpenSourceLicense.MIT,
"https://github.com/koral--/android-gif-drawable"
),
license(
"Android Debug Database",
"2019 Amit Shekhar,",
"2011 Android Open Source Project",
OpenSourceLicense.APACHE_2,
"https://github.com/amitshekhariitbhu/Android-Debug-Database"
)
}
)
override fun getActivityTitle(): CharSequence? {
override fun getActivityTitle(): CharSequence {
return getString(R.string.settings_about_licenses_text)
}
}

View File

@ -0,0 +1,189 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-3-17.
*/
package pl.szczodrzynski.edziennik.ui.modules.settings
import com.danielstone.materialaboutlibrary.items.*
import com.danielstone.materialaboutlibrary.model.MaterialAboutCard
import com.mikepenz.iconics.IconicsDrawable
import com.mikepenz.iconics.typeface.IIcon
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import com.mikepenz.iconics.utils.colorInt
import com.mikepenz.iconics.utils.sizeDp
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.after
import pl.szczodrzynski.edziennik.data.db.entity.Profile
import pl.szczodrzynski.edziennik.utils.Colors
import pl.szczodrzynski.edziennik.utils.Themes
class SettingsUtil(
val activity: MainActivity,
private val onRefresh: () -> Unit
) {
fun refresh() = onRefresh()
private fun IIcon.asDrawable(color: Int? = null, size: Int = 20) =
IconicsDrawable(activity).apply {
icon = this@asDrawable
sizeDp = size
colorInt = color ?: Themes.getPrimaryTextColor(activity)
}
fun createCard(
titleRes: Int?,
items: List<MaterialAboutItem>,
itemsMore: List<MaterialAboutItem>,
backgroundColor: Int? = null,
theme: Int? = null
): MaterialAboutCard {
val card = MaterialAboutCard.Builder()
.title(titleRes ?: 0)
.cardColor(backgroundColor ?: 0)
.theme(theme ?: 0)
.build()
card.items.addAll(items)
if (itemsMore.isNotEmpty()) {
card.items.add(createMoreItem(card, itemsMore))
}
return card
}
fun createMoreItem(
card: MaterialAboutCard,
items: List<MaterialAboutItem>
): MaterialAboutActionItem {
val iconColor = card.cardColor.let {
if (it == 0)
null
else
Colors.legibleTextColor(it)
}
val moreItem = MaterialAboutActionItem.Builder()
.text(R.string.settings_more_text)
.icon(CommunityMaterial.Icon.cmd_chevron_down.asDrawable(iconColor, size = 14))
.build()
moreItem.setOnClickAction {
card.items.after(moreItem, items)
card.items.remove(moreItem)
onRefresh()
}
return moreItem
}
fun createSectionItem(text: Int) = MaterialAboutSectionItem(text)
fun createActionItem(
text: Int,
subText: Int? = null,
icon: IIcon,
backgroundColor: Int? = null,
onClick: (item: MaterialAboutActionItem) -> Unit
): MaterialAboutActionItem {
val iconColor = backgroundColor?.let { Colors.legibleTextColor(it) }
val item = MaterialAboutActionItem.Builder()
.text(text)
.subText(subText ?: 0)
.icon(icon.asDrawable(iconColor))
.build()
item.setOnClickAction {
onClick(item)
}
return item
}
fun createPropertyItem(
text: Int,
subText: Int? = null,
subTextChecked: Int? = null,
icon: IIcon,
backgroundColor: Int? = null,
value: Boolean,
beforeChange: ((item: MaterialAboutSwitchItem, value: Boolean) -> Boolean)? = null,
onChange: (item: MaterialAboutSwitchItem, value: Boolean) -> Unit
): MaterialAboutSwitchItem {
val iconColor = backgroundColor?.let { Colors.legibleTextColor(it) }
val item = MaterialAboutSwitchItem.Builder()
.text(text)
.subText(subText ?: 0)
.subTextChecked(subTextChecked ?: 0)
.icon(icon.asDrawable(iconColor))
.setChecked(value)
.build()
item.setOnCheckedChangedAction { item, isChecked ->
if (beforeChange?.invoke(item as MaterialAboutSwitchItem, isChecked) == false)
return@setOnCheckedChangedAction false
onChange(item as MaterialAboutSwitchItem, isChecked)
true
}
return item
}
fun createPropertyActionItem(
text: Int,
subText: Int? = null,
subTextChecked: Int? = null,
icon: IIcon,
backgroundColor: Int? = null,
value: Boolean,
onChange: (item: MaterialAboutActionSwitchItem, value: Boolean) -> Unit,
onClick: (item: MaterialAboutActionSwitchItem) -> Unit
): MaterialAboutSwitchItem {
val iconColor = backgroundColor?.let { Colors.legibleTextColor(it) }
val item = MaterialAboutActionSwitchItem.Builder()
.text(text)
.subText(subText ?: 0)
.subTextChecked(subTextChecked ?: 0)
.icon(icon.asDrawable(iconColor))
.setChecked(value)
.build()
item.setOnClickAction {
onClick(item)
}
item.setOnCheckedChangedAction { item, isChecked ->
onChange(item as MaterialAboutActionSwitchItem, isChecked)
true
}
return item
}
fun createTitleItem(): MaterialAboutTitleItem =
MaterialAboutTitleItem.Builder()
.text(R.string.app_name)
.desc(R.string.settings_about_title_subtext)
.icon(R.mipmap.ic_splash)
.build()
fun createProfileItem(
profile: Profile,
onClick: (item: MaterialAboutProfileItem, profile: Profile) -> Unit
): MaterialAboutProfileItem {
val item = MaterialAboutProfileItem(
MaterialAboutTitleItem.Builder()
.text(profile.name)
.desc(profile.subname)
.icon(profile.getImageDrawable(activity))
.build()
)
item.setOnClickAction {
onClick(item, profile)
}
return item
}
}

View File

@ -0,0 +1,46 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-3-17.
*/
package pl.szczodrzynski.edziennik.ui.modules.settings
import android.content.Context
import android.view.View
import com.danielstone.materialaboutlibrary.holders.MaterialAboutItemViewHolder
import com.danielstone.materialaboutlibrary.items.MaterialAboutItem
import com.danielstone.materialaboutlibrary.items.MaterialAboutTitleItem.MaterialAboutTitleItemViewHolder
import com.danielstone.materialaboutlibrary.util.DefaultViewTypeManager
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsViewTypeManager.ItemType.Companion.PROFILE_ITEM
class SettingsViewTypeManager : DefaultViewTypeManager() {
class ItemType {
companion object {
const val PROFILE_ITEM = 10
}
}
override fun getLayout(itemType: Int) = when (itemType) {
PROFILE_ITEM -> R.layout.mal_material_about_profile_item
else -> super.getLayout(itemType)
}
override fun getViewHolder(itemType: Int, view: View): MaterialAboutItemViewHolder =
when (itemType) {
PROFILE_ITEM -> MaterialAboutProfileItem.getViewHolder(view)
else -> super.getViewHolder(itemType, view)
}
override fun setupItem(
itemType: Int,
holder: MaterialAboutItemViewHolder,
item: MaterialAboutItem,
context: Context
) = when (itemType) {
PROFILE_ITEM -> MaterialAboutProfileItem.setupItem(
holder as MaterialAboutTitleItemViewHolder,
item as MaterialAboutProfileItem, context
)
else -> super.setupItem(itemType, holder, item, context)
}
}

View File

@ -0,0 +1,150 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-3-18.
*/
package pl.szczodrzynski.edziennik.ui.modules.settings.cards
import android.content.Intent
import android.media.MediaPlayer
import android.widget.Toast
import com.danielstone.materialaboutlibrary.items.MaterialAboutItem
import com.danielstone.materialaboutlibrary.model.MaterialAboutCard
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import eu.szkolny.font.SzkolnyFont
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import pl.szczodrzynski.edziennik.App
import pl.szczodrzynski.edziennik.BuildConfig
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.dialogs.changelog.ChangelogDialog
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsCard
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsLicenseActivity
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsUtil
import pl.szczodrzynski.edziennik.utils.Utils
import kotlin.coroutines.CoroutineContext
class SettingsAboutCard(util: SettingsUtil) : SettingsCard(util), CoroutineScope {
private val job: Job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private var clickCounter = 0
private val mediaPlayer by lazy {
MediaPlayer.create(activity, R.raw.ogarnij_sie)
}
override fun buildCard(): MaterialAboutCard =
util.createCard(
null,
items = listOf(),
itemsMore = listOf(),
backgroundColor = 0xff1976d2.toInt(),
theme = R.style.AppTheme_Dark
).also {
it.items.addAll(getItems(it))
}
override fun getItems() = listOf<MaterialAboutItem>()
override fun getItemsMore() = listOf<MaterialAboutItem>()
private fun getItems(card: MaterialAboutCard) = listOf(
util.createTitleItem(),
util.createActionItem(
text = R.string.settings_about_version_text,
icon = CommunityMaterial.Icon2.cmd_information_outline,
onClick = { item ->
clickCounter++
if (clickCounter < 7)
Toast.makeText(activity, "\uD83D\uDE02", Toast.LENGTH_SHORT).show()
item.subText =
BuildConfig.VERSION_NAME + ", " + BuildConfig.BUILD_TYPE + " \uD83D\uDCA3"
util.refresh()
if (clickCounter >= 7) {
mediaPlayer.start()
clickCounter = 0
}
}
).also {
it.subText = BuildConfig.VERSION_NAME + ", " + BuildConfig.BUILD_TYPE
},
util.createMoreItem(card, items = listOf(
util.createActionItem(
text = R.string.settings_about_changelog_text,
icon = CommunityMaterial.Icon3.cmd_radar
) {
ChangelogDialog(activity)
},
util.createActionItem(
text = R.string.settings_about_update_text,
subText = R.string.settings_about_update_subtext,
icon = CommunityMaterial.Icon3.cmd_update
) {
launch {
UpdateWorker.runNow(app)
}
}
)),
util.createSectionItem(
text = R.string.see_also
),
util.createActionItem(
text = R.string.settings_about_privacy_policy_text,
icon = CommunityMaterial.Icon3.cmd_shield_outline
) {
Utils.openUrl(activity, "https://szkolny.eu/privacy-policy")
},
util.createActionItem(
text = R.string.settings_about_discord_text,
subText = R.string.settings_about_discord_subtext,
icon = SzkolnyFont.Icon.szf_discord_outline
) {
Utils.openUrl(activity, "https://szkolny.eu/discord")
},
util.createActionItem(
text = R.string.settings_about_github_text,
subText = R.string.settings_about_github_subtext,
icon = SzkolnyFont.Icon.szf_github_face
) {
Utils.openUrl(activity, "https://szkolny.eu/github/android")
},
util.createMoreItem(card, items = listOfNotNull(
util.createActionItem(
text = R.string.settings_about_homepage_text,
subText = R.string.settings_about_homepage_subtext,
icon = CommunityMaterial.Icon.cmd_earth
) {
Utils.openUrl(activity, "https://szkolny.eu/")
},
util.createActionItem(
text = R.string.settings_about_licenses_text,
icon = CommunityMaterial.Icon.cmd_code_braces
) {
activity.startActivity(Intent(activity, SettingsLicenseActivity::class.java))
},
if (App.devMode)
util.createActionItem(
text = R.string.settings_about_crash_text,
subText = R.string.settings_about_crash_subtext,
icon = CommunityMaterial.Icon.cmd_bug_outline
) {
throw RuntimeException("MANUAL CRASH")
}
else
null
))
)
}

View File

@ -0,0 +1,48 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-3-18.
*/
package pl.szczodrzynski.edziennik.ui.modules.settings.cards
import android.content.Intent
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.ui.dialogs.profile.ProfileConfigDialog
import pl.szczodrzynski.edziennik.ui.modules.login.LoginActivity
import pl.szczodrzynski.edziennik.ui.modules.settings.MaterialAboutProfileItem
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsCard
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsUtil
class SettingsProfileCard(util: SettingsUtil) : SettingsCard(util) {
override fun buildCard() = util.createCard(
null,
items = getItems(),
itemsMore = listOf()
)
private fun getProfileItem(): MaterialAboutProfileItem = util.createProfileItem(
profile = app.profile
) { item, profile ->
ProfileConfigDialog(activity, profile, onProfileSaved = {
val index = card.items.indexOf(item)
if (index == -1)
return@ProfileConfigDialog
card.items.remove(item)
card.items.add(index, getProfileItem())
util.refresh()
})
}
override fun getItems() = listOf(
getProfileItem(),
util.createActionItem(
text = R.string.settings_add_student_text,
subText = R.string.settings_add_student_subtext,
icon = CommunityMaterial.Icon.cmd_account_plus_outline
) {
activity.startActivity(Intent(activity, LoginActivity::class.java))
}
)
}

View File

@ -0,0 +1,152 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-3-18.
*/
package pl.szczodrzynski.edziennik.ui.modules.settings.cards
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import eu.szkolny.font.SzkolnyFont
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.after
import pl.szczodrzynski.edziennik.data.api.LOGIN_TYPE_LIBRUS
import pl.szczodrzynski.edziennik.data.db.entity.Profile.Companion.REGISTRATION_ENABLED
import pl.szczodrzynski.edziennik.ui.dialogs.bell.BellSyncConfigDialog
import pl.szczodrzynski.edziennik.ui.dialogs.grade.GradesConfigDialog
import pl.szczodrzynski.edziennik.ui.dialogs.settings.AttendanceConfigDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.RegistrationConfigDialog
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsCard
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsUtil
class SettingsRegisterCard(util: SettingsUtil) : SettingsCard(util) {
override fun buildCard() = util.createCard(
R.string.settings_card_register_title,
items = getItems(),
itemsMore = getItemsMore()
)
private fun getBellSync() =
configGlobal.timetable.bellSyncDiff?.let {
activity.getString(
R.string.settings_register_bell_sync_subtext_format,
(if (configGlobal.timetable.bellSyncMultiplier == -1) "-" else "+") + it.stringHMS
)
} ?: activity.getString(R.string.settings_register_bell_sync_subtext_disabled)
private val sharedEventsItem by lazy {
util.createPropertyItem(
text = R.string.settings_register_shared_events_text,
subText = R.string.settings_register_shared_events_subtext,
icon = CommunityMaterial.Icon3.cmd_share_outline,
value = app.profile.enableSharedEvents
) { _, value ->
app.profile.enableSharedEvents = value
app.profileSave()
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.event_sharing)
.setMessage(
if (value)
R.string.settings_register_shared_events_dialog_enabled_text
else
R.string.settings_register_shared_events_dialog_disabled_text
)
.setPositiveButton(R.string.ok, null)
.show()
}
}
override fun getItems() = listOfNotNull(
util.createActionItem(
text = R.string.menu_grades_config,
icon = CommunityMaterial.Icon3.cmd_numeric_5_box_outline
) {
GradesConfigDialog(activity, reloadOnDismiss = false)
},
util.createActionItem(
text = R.string.menu_attendance_config,
icon = CommunityMaterial.Icon.cmd_calendar_remove_outline
) {
AttendanceConfigDialog(activity, reloadOnDismiss = false)
},
util.createPropertyItem(
text = R.string.settings_register_allow_registration_text,
subText = R.string.settings_register_allow_registration_subtext,
icon = CommunityMaterial.Icon.cmd_account_circle_outline,
value = app.profile.registration == REGISTRATION_ENABLED,
beforeChange = { item, value ->
if (app.profile.registration == REGISTRATION_ENABLED == value)
// allow the switch to change - needed for util.refresh() to change the visual state
return@createPropertyItem true
val dialog =
RegistrationConfigDialog(activity, app.profile, onChangeListener = { enabled ->
if (item.isChecked == enabled)
return@RegistrationConfigDialog
item.isChecked = enabled
if (value) {
card.items.after(item, sharedEventsItem)
} else {
card.items.remove(sharedEventsItem)
}
util.refresh()
})
if (value)
dialog.showEnableDialog()
else
dialog.showDisableDialog()
false
}
) { _, _ -> },
if (app.profile.registration == REGISTRATION_ENABLED)
sharedEventsItem
else
null
)
override fun getItemsMore() = listOfNotNull(
util.createActionItem(
text = R.string.settings_register_bell_sync_text,
icon = SzkolnyFont.Icon.szf_alarm_bell_outline,
onClick = {
BellSyncConfigDialog(activity, onChangeListener = {
it.subText = getBellSync()
util.refresh()
})
}
).also {
it.subText = getBellSync()
},
util.createPropertyItem(
text = R.string.settings_register_count_in_seconds_text,
subText = R.string.settings_register_count_in_seconds_subtext,
icon = CommunityMaterial.Icon3.cmd_timer_outline,
value = configGlobal.timetable.countInSeconds
) { _, it ->
configGlobal.timetable.countInSeconds = it
},
if (app.profile.loginStoreType == LOGIN_TYPE_LIBRUS)
util.createPropertyItem(
text = R.string.settings_register_show_teacher_absences_text,
icon = CommunityMaterial.Icon.cmd_account_arrow_right_outline,
value = app.profile.getStudentData("showTeacherAbsences", true)
) { _, it ->
app.profile.putStudentData("showTeacherAbsences", it)
app.profileSave()
}
else
null,
util.createPropertyItem(
text = R.string.settings_register_hide_sticks_from_old,
icon = CommunityMaterial.Icon3.cmd_numeric_1_box_outline,
value = configProfile.grades.hideSticksFromOld
) { _, it ->
configProfile.grades.hideSticksFromOld = it
}
)
}

View File

@ -0,0 +1,200 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-3-18.
*/
package pl.szczodrzynski.edziennik.ui.modules.settings.cards
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.net.Uri
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES
import android.provider.Settings
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import pl.szczodrzynski.edziennik.MainActivity
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.after
import pl.szczodrzynski.edziennik.getSyncInterval
import pl.szczodrzynski.edziennik.sync.SyncWorker
import pl.szczodrzynski.edziennik.sync.UpdateWorker
import pl.szczodrzynski.edziennik.ui.dialogs.sync.NotificationFilterDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.QuietHoursConfigDialog
import pl.szczodrzynski.edziennik.ui.dialogs.sync.SyncIntervalDialog
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsCard
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsUtil
import pl.szczodrzynski.edziennik.utils.models.Time
class SettingsSyncCard(util: SettingsUtil) : SettingsCard(util) {
override fun buildCard() = util.createCard(
R.string.settings_card_sync_title,
items = getItems(),
itemsMore = getItemsMore()
)
private fun getQuietHours(): String {
if (configGlobal.sync.quietHoursStart == null) {
configGlobal.sync.quietHoursStart = Time(22, 30, 0)
}
if (configGlobal.sync.quietHoursEnd == null) {
configGlobal.sync.quietHoursEnd = Time(6, 30, 0)
}
return activity.getString(
if (configGlobal.sync.quietHoursStart!! > configGlobal.sync.quietHoursEnd!!)
R.string.settings_sync_quiet_hours_subtext_next_day_format
else
R.string.settings_sync_quiet_hours_subtext_format,
configGlobal.sync.quietHoursStart?.stringHM,
configGlobal.sync.quietHoursEnd?.stringHM
)
}
private val syncWifiItem by lazy {
util.createPropertyItem(
text = R.string.settings_sync_wifi_text,
subText = R.string.settings_sync_wifi_subtext,
icon = CommunityMaterial.Icon3.cmd_wifi_strength_2,
value = configGlobal.sync.onlyWifi
) { _, it ->
configGlobal.sync.onlyWifi = it
SyncWorker.rescheduleNext(app)
}
}
override fun getItems() = listOfNotNull(
util.createPropertyActionItem(
text = R.string.settings_sync_sync_interval_text,
subText = R.string.settings_sync_sync_interval_subtext_disabled,
icon = CommunityMaterial.Icon.cmd_download_outline,
value = configGlobal.sync.enabled,
onChange = { item, value ->
// When calling onChange from the onClick listener below
// a list refresh is requested, the adapter refreshes
// all view holders, changing the state of the switch
// view, thus calling onChange again, causing it to
// try to recursively refresh the list, therefore
// crashing the app. To avoid this, the method will
// continue only if the checked state is different
// from the saved value, which should only happen
// when clicking the switch manually or when called
// by onClick, **once** (because onClick doesn't
// update the config value - we let the switch
// listener do it). Then there comes a different problem,
// when onClick changes the subText and the onChange
// listener returns because the boolean value
// is unchanged, leaving the list not refreshed.
// To solve this, a list refresh is also requested
// in onClick, when the config value is the same
// as the new switch value, which would normally
// cause the onChange method to exit here.
if (value == configGlobal.sync.enabled)
return@createPropertyActionItem
if (value) {
card.items.after(item, syncWifiItem)
} else {
card.items.remove(syncWifiItem)
}
util.refresh()
configGlobal.sync.enabled = value
SyncWorker.rescheduleNext(app)
},
onClick = { item ->
SyncIntervalDialog(activity, onChangeListener = {
item.subTextChecked = activity.getSyncInterval(configGlobal.sync.interval)
item.isChecked = true
item.onCheckedChangedAction.onCheckedChanged(item, true)
if (configGlobal.sync.enabled)
util.refresh()
})
}
).also {
it.subTextChecked = activity.getSyncInterval(configGlobal.sync.interval)
},
if (configGlobal.sync.enabled)
syncWifiItem
else
null,
util.createActionItem(
text = R.string.settings_profile_notifications_text,
subText = R.string.settings_profile_notifications_subtext,
icon = CommunityMaterial.Icon2.cmd_filter_outline
) {
NotificationFilterDialog(activity)
},
util.createPropertyActionItem(
text = R.string.settings_sync_quiet_hours_text,
subText = R.string.settings_sync_quiet_hours_subtext_disabled,
icon = CommunityMaterial.Icon.cmd_bell_sleep_outline,
value = configGlobal.sync.quietHoursEnabled,
onChange = { _, value ->
configGlobal.sync.quietHoursEnabled = value
},
onClick = { item ->
QuietHoursConfigDialog(activity, onChangeListener = {
item.subTextChecked = getQuietHours()
item.isChecked = configGlobal.sync.quietHoursEnabled
util.refresh()
})
}
).also {
it.subTextChecked = getQuietHours()
},
util.createActionItem(
text = R.string.settings_sync_web_push_text,
subText = R.string.settings_sync_web_push_subtext,
icon = CommunityMaterial.Icon2.cmd_laptop
) {
activity.loadTarget(MainActivity.TARGET_WEB_PUSH)
}
)
override fun getItemsMore() = listOfNotNull(
util.createPropertyItem(
text = R.string.settings_sync_updates_text,
icon = CommunityMaterial.Icon.cmd_cellphone_arrow_down,
value = configGlobal.sync.notifyAboutUpdates
) { _, it ->
configGlobal.sync.notifyAboutUpdates = it
UpdateWorker.rescheduleNext(app)
},
if (SDK_INT >= VERSION_CODES.KITKAT)
util.createActionItem(
text = R.string.settings_sync_notifications_settings_text,
subText = R.string.settings_sync_notifications_settings_subtext,
icon = CommunityMaterial.Icon.cmd_cog_outline
) {
val channel = app.notificationChannelsManager.data.key
val intent = Intent().apply {
when {
SDK_INT >= VERSION_CODES.O -> {
action = Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS
putExtra(Settings.EXTRA_APP_PACKAGE, app.packageName)
putExtra(Settings.EXTRA_CHANNEL_ID, channel)
addFlags(FLAG_ACTIVITY_NEW_TASK)
}
SDK_INT >= VERSION_CODES.LOLLIPOP -> {
action = "android.settings.APP_NOTIFICATION_SETTINGS"
putExtra("app_package", app.packageName)
putExtra("app_uid", app.applicationInfo.uid)
}
else -> {
action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
addCategory(Intent.CATEGORY_DEFAULT)
data = Uri.parse("package:" + app.packageName)
}
}
}
activity.startActivity(intent)
}
else
null
)
}

View File

@ -0,0 +1,148 @@
/*
* Copyright (c) Kuba Szczodrzyński 2021-3-18.
*/
package pl.szczodrzynski.edziennik.ui.modules.settings.cards
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import pl.szczodrzynski.edziennik.R
import pl.szczodrzynski.edziennik.ui.dialogs.settings.AppLanguageDialog
import pl.szczodrzynski.edziennik.ui.dialogs.settings.MiniMenuConfigDialog
import pl.szczodrzynski.edziennik.ui.dialogs.settings.ThemeChooserDialog
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsCard
import pl.szczodrzynski.edziennik.ui.modules.settings.SettingsUtil
import pl.szczodrzynski.edziennik.utils.Themes
import pl.szczodrzynski.edziennik.utils.models.Date
class SettingsThemeCard(util: SettingsUtil) : SettingsCard(util) {
override fun buildCard() = util.createCard(
R.string.settings_card_theme_title,
items = getItems(),
itemsMore = getItemsMore()
)
override fun getItems() = listOfNotNull(
if (Date.getToday().month % 11 == 1) // cool math games
util.createPropertyItem(
text = R.string.settings_theme_snowfall_text,
subText = R.string.settings_theme_snowfall_subtext,
icon = CommunityMaterial.Icon3.cmd_snowflake,
value = configGlobal.ui.snowfall
) { _, it ->
configGlobal.ui.snowfall = it
activity.recreate()
}
else null,
util.createActionItem(
text = R.string.settings_theme_theme_text,
subText = Themes.getThemeNameRes(),
icon = CommunityMaterial.Icon3.cmd_palette_outline
) {
ThemeChooserDialog(activity)
},
util.createActionItem(
text = R.string.settings_about_language_text,
subText = R.string.settings_about_language_subtext,
icon = CommunityMaterial.Icon3.cmd_translate
) {
AppLanguageDialog(activity)
},
util.createPropertyItem(
text = R.string.settings_theme_mini_drawer_text,
subText = R.string.settings_theme_mini_drawer_subtext,
icon = CommunityMaterial.Icon.cmd_dots_vertical,
value = configGlobal.ui.miniMenuVisible
) { _, it ->
configGlobal.ui.miniMenuVisible = it
activity.navView.drawer.miniDrawerVisiblePortrait = it
}
)
override fun getItemsMore() = listOf(
util.createActionItem(
text = R.string.settings_theme_mini_drawer_buttons_text,
icon = CommunityMaterial.Icon2.cmd_format_list_checks
) {
MiniMenuConfigDialog(activity)
},
util.createActionItem(
text = R.string.settings_theme_drawer_header_text,
icon = CommunityMaterial.Icon2.cmd_image_outline
) {
if (app.config.ui.appBackground == null) {
setHeaderBackground()
return@createActionItem
}
MaterialAlertDialogBuilder(activity)
.setItems(
arrayOf(
activity.getString(R.string.settings_theme_drawer_header_dialog_set),
activity.getString(R.string.settings_theme_drawer_header_dialog_restore)
)
) { _, which ->
when (which) {
0 -> setHeaderBackground()
1 -> {
app.config.ui.headerBackground = null
activity.drawer.setAccountHeaderBackground(null)
activity.drawer.open()
}
}
}
.setNegativeButton(R.string.cancel, null)
.show()
},
util.createActionItem(
text = R.string.settings_theme_app_background_text,
subText = R.string.settings_theme_app_background_subtext,
icon = CommunityMaterial.Icon2.cmd_image_filter_hdr
) {
if (app.config.ui.appBackground == null) {
setAppBackground()
return@createActionItem
}
MaterialAlertDialogBuilder(activity)
.setItems(
arrayOf(
activity.getString(R.string.settings_theme_app_background_dialog_set),
activity.getString(R.string.settings_theme_app_background_dialog_restore)
)
) { _, which ->
when (which) {
0 -> setAppBackground()
1 -> {
app.config.ui.appBackground = null
activity.setAppBackground()
}
}
}
.setNegativeButton(R.string.cancel, null)
.show()
},
util.createPropertyItem(
text = R.string.settings_theme_open_drawer_on_back_pressed_text,
icon = CommunityMaterial.Icon3.cmd_menu_open,
value = configGlobal.ui.openDrawerOnBackPressed
) { _, it ->
configGlobal.ui.openDrawerOnBackPressed = it
}
)
private fun setHeaderBackground() = activity.requestHandler.requestHeaderBackground {
activity.drawer.setAccountHeaderBackground(null)
activity.drawer.setAccountHeaderBackground(app.config.ui.headerBackground)
activity.drawer.open()
}
private fun setAppBackground() = activity.requestHandler.requestAppBackground {
activity.setAppBackground()
}
}

View File

@ -1,75 +0,0 @@
package pl.szczodrzynski.edziennik.ui.modules.webpush;
import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import com.google.zxing.Result;
import me.dm7.barcodescanner.zxing.ZXingScannerView;
import pl.szczodrzynski.edziennik.R;
public class QrScannerActivity extends AppCompatActivity implements ZXingScannerView.ResultHandler {
private ZXingScannerView mScannerView;
public static ZXingScannerView.ResultHandler resultHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mScannerView = new ZXingScannerView(this); // Programmatically initialize the scanner view
setContentView(mScannerView);
int result = ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA);
if (result == PackageManager.PERMISSION_GRANTED) {
mScannerView.setResultHandler(this); // Register ourselves as a handler for scan results.
mScannerView.startCamera(); // Start camera on resume
} else {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, 1);
}
}
@Override
public void onResume() {
super.onResume();
mScannerView.setResultHandler(this); // Register ourselves as a handler for scan results.
mScannerView.startCamera(); // Start camera on resume
}
@Override
public void onPause() {
super.onPause();
mScannerView.stopCamera(); // Stop camera on pause
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case 1: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
mScannerView.startCamera();
} else {
// permission denied, boo! Disable the
// functionality that depends on this permission.
Toast.makeText(this, R.string.no_permissions, Toast.LENGTH_SHORT).show();
}
}
// other 'case' lines to check for other
// permissions this app might request
}
}
@Override
public void handleResult(Result rawResult) {
if (resultHandler != null) {
resultHandler.handleResult(rawResult);
}
finish();
}
}

View File

@ -4,15 +4,11 @@
package pl.szczodrzynski.edziennik.ui.modules.webpush
import android.Manifest
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.LinearLayoutManager
import kotlinx.coroutines.CoroutineScope
@ -25,7 +21,6 @@ import pl.szczodrzynski.edziennik.data.api.szkolny.response.WebPushResponse
import pl.szczodrzynski.edziennik.databinding.WebPushFragmentBinding
import pl.szczodrzynski.edziennik.ui.dialogs.QrScannerDialog
import pl.szczodrzynski.edziennik.utils.SimpleDividerItemDecoration
import pl.szczodrzynski.edziennik.utils.Themes
import kotlin.coroutines.CoroutineContext
class WebPushFragment : Fragment(), CoroutineScope {
@ -46,13 +41,13 @@ class WebPushFragment : Fragment(), CoroutineScope {
SzkolnyApi(app)
}
private val manager
get() = app.permissionManager
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
activity = (getActivity() as MainActivity?) ?: return null
context ?: return null
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 = WebPushFragmentBinding.inflate(inflater)
return b.root
@ -60,19 +55,15 @@ class WebPushFragment : Fragment(), CoroutineScope {
@SuppressLint("DefaultLocale")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// TODO check if app, activity, b can be null
if (app.profile == null || !isAdded)
if (!isAdded)
return
b.scanQrCode.onClick {
val result = ContextCompat.checkSelfPermission(activity, Manifest.permission.CAMERA)
if (result == PackageManager.PERMISSION_GRANTED) {
manager.requestCameraPermission(activity, R.string.permissions_qr_scanner) {
QrScannerDialog(activity, {
b.tokenEditText.setText(it.crc32().toString(36).toUpperCase())
pairBrowser(browserId = it)
})
} else {
ActivityCompat.requestPermissions(activity, arrayOf(Manifest.permission.CAMERA), 1)
}
}

View File

@ -1,28 +0,0 @@
package pl.szczodrzynski.edziennik.utils;
import android.content.Context;
import android.view.ViewGroup;
import com.google.android.material.snackbar.Snackbar;
import androidx.core.view.ViewCompat;
import pl.szczodrzynski.edziennik.R;
public class SnackbarHelper {
public static void configSnackbar(Context context, Snackbar snack) {
addMargins(snack);
setRoundBordersBg(context, snack);
ViewCompat.setElevation(snack.getView(), 6f);
}
private static void addMargins(Snackbar snack) {
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) snack.getView().getLayoutParams();
params.setMargins(12, 12, 12, 12);
snack.getView().setLayoutParams(params);
}
private static void setRoundBordersBg(Context context, Snackbar snackbar) {
snackbar.getView().setBackground(context.getResources().getDrawable(R.drawable.bg_snackbar));
}
}

View File

@ -42,6 +42,12 @@ object Themes {
theme = themeList[value]
}
var themeIndex
get() = themeList.indexOf(theme)
set(value) {
theme = themeList[value]
}
val appThemeNoDisplay: Int
get() = if (theme.isDark) R.style.AppTheme_Dark_NoDisplay else R.style.AppTheme_Light_NoDisplay
@ -61,38 +67,15 @@ object Themes {
return getColorFromAttr(context, android.R.attr.textColorSecondary)
}
/*public static int getChipColorRes() {
switch (themeInt) {
case THEME_LIGHT:
return R.color.chipBackgroundLight;
default:
case THEME_DARK:
return R.color.chipBackgroundDark;
case THEME_BLACK:
return R.color.chipBackgroundBlack;
case THEME_CHOCOLATE:
return R.color.chipBackgroundChocolate;
case THEME_BLUE:
return R.color.chipBackgroundBlue;
case THEME_PURPLE:
return R.color.chipBackgroundPurple;
case THEME_GREEN:
return R.color.chipBackgroundGreen;
case THEME_AMBER:
return R.color.chipBackgroundAmber;
case THEME_RED:
return R.color.chipBackgroundRed;
}
}*/
fun getThemeName(context: Context): String {
return context.getString(theme.name)
}
fun getThemeNames(context: Context): List<String> {
val list = mutableListOf<String>()
for (theme in themeList) {
list += context.getString(theme.name)
@StringRes
fun getThemeNameRes() = theme.name
fun getThemeNames(context: Context) =
themeList.map {
context.getString(it.name)
}
return list
}
}

View File

@ -31,49 +31,91 @@ class PermissionManager(val app: App) : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Main
private fun isStoragePermissionGranted() = if (Build.VERSION.SDK_INT >= 23) {
app.checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED
} else {
true
private fun isPermissionGranted(name: String) =
if (Build.VERSION.SDK_INT >= 23)
app.checkSelfPermission(name) == PackageManager.PERMISSION_GRANTED
else
true
private fun openPermissionSettings(activity: AppCompatActivity) {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", app.packageName, null)
intent.data = uri
activity.startActivity(intent)
}
fun requestStoragePermission(
activity: AppCompatActivity,
@StringRes permissionMessage: Int,
onSuccess: suspend CoroutineScope.() -> Unit
private fun requestPermission(
activity: AppCompatActivity,
@StringRes permissionMessage: Int,
isRequired: Boolean = true,
permissionName: String,
onSuccess: suspend CoroutineScope.() -> Unit
) {
launch {
if (isStoragePermissionGranted()) {
if (isPermissionGranted(permissionName)) {
onSuccess()
return@launch
}
val result = activity.awaitAskPermissions(Manifest.permission.WRITE_EXTERNAL_STORAGE)
val result = activity.awaitAskPermissions(permissionName)
when {
result.hasAllGranted() -> onSuccess()
result.hasRational() -> {
if (!isRequired) {
onSuccess()
return@launch
}
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.permissions_required)
.setMessage(permissionMessage)
.setPositiveButton(R.string.ok) { _, _ ->
requestStoragePermission(activity, permissionMessage, onSuccess)
}
.setNegativeButton(R.string.cancel, null)
.show()
.setTitle(R.string.permissions_required)
.setMessage(permissionMessage)
.setPositiveButton(R.string.ok) { _, _ ->
requestPermission(
activity,
permissionMessage,
isRequired,
permissionName,
onSuccess
)
}
.setNegativeButton(R.string.cancel, null)
.show()
}
result.hasPermanentDenied() -> {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.permissions_required)
.setMessage(R.string.permissions_denied)
.setPositiveButton(R.string.ok) { _, _ ->
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", app.packageName, null)
intent.data = uri
activity.startActivity(intent)
}
.setNegativeButton(R.string.cancel, null)
.show()
.setTitle(R.string.permissions_required)
.setMessage(R.string.permissions_denied)
.setPositiveButton(R.string.ok) { _, _ ->
openPermissionSettings(activity)
}
.setNegativeButton(R.string.cancel, null)
.show()
}
}
}
}
fun requestStoragePermission(
activity: AppCompatActivity,
@StringRes permissionMessage: Int,
isRequired: Boolean = true,
onSuccess: suspend CoroutineScope.() -> Unit
) = requestPermission(
activity,
permissionMessage,
isRequired,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
onSuccess
)
fun requestCameraPermission(
activity: AppCompatActivity,
@StringRes permissionMessage: Int,
isRequired: Boolean = true,
onSuccess: suspend CoroutineScope.() -> Unit
) = requestPermission(
activity,
permissionMessage,
isRequired,
Manifest.permission.CAMERA,
onSuccess
)
}

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="4dp" />
<stroke android:color="@color/dividerColor" android:width="1dp" />
<solid android:color="#DCDCDC" />
</shape>

View File

@ -1,5 +0,0 @@
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#323232" />
<corners android:radius="4dp" />
</shape>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright (c) Kuba Szczodrzyński 2021-3-19.
-->
<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:paddingStart="?attr/dialogPreferredPadding"
android:paddingTop="8dp"
android:paddingEnd="?attr/dialogPreferredPadding">
<TextView
android:id="@android:id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="4dp"
tools:text="This is a custom message of the dialog." />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_input_layout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.textfield.TextInputEditText
android:id="@android:id/text1"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) Kuba Szczodrzyński 2021-3-22.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<import type="pl.szczodrzynski.edziennik.data.db.entity.Profile" />
<variable
name="profile"
type="Profile" />
</data>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="24dp"
android:paddingTop="24dp">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
<com.mikepenz.materialdrawer.view.BezelImageView
android:id="@+id/image"
android:layout_width="120dp"
android:layout_height="120dp"
android:scaleType="centerCrop"
tools:src="@drawable/face_1" />
<View
android:id="@+id/circleView"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="bottom|end"
tools:background="?colorSurface" />
<com.mikepenz.iconics.view.IconicsImageView
android:id="@+id/imageButtonIcon"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="bottom|end"
app:iiv_color="?colorOnBackground"
app:iiv_icon="szf-image-plus-outline"
app:iiv_padding="5dp"
tools:background="@color/colorSurface_16dp" />
<View
android:id="@+id/imageButton"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="bottom|end"
android:background="?selectableItemBackgroundBorderless" />
</FrameLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/nameLayout"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/nameEdit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/profile_config_name_hint"
android:text="@={profile.name}"
tools:text="Paweł Informatyczny" />
</com.google.android.material.textfield.TextInputLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:gravity="center_horizontal"
android:textAppearance="@style/NavView.TextView.Subtitle"
android:textColor="?android:textColorSecondary"
android:text="@{profile.subname}"
tools:text="3b3t - 2020/2021" />
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/syncSwitch"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/profile_config_sync_enabled"
android:checked="@={profile.syncEnabled}" />
<com.google.android.material.button.MaterialButton
android:id="@+id/logoutButton"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/profile_config_logout"
android:textColor="@color/md_red_500"
app:rippleColor="@color/md_red_300"
app:strokeColor="@color/md_red_500" />
</LinearLayout>
</ScrollView>
</layout>

View File

@ -0,0 +1,48 @@
<?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="horizontal"
android:paddingHorizontal="@dimen/mal_baseline_half">
<com.mikepenz.materialdrawer.view.BezelImageView
android:id="@+id/mal_item_image"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_margin="@dimen/mal_baseline"
android:adjustViewBounds="false"
android:contentDescription="@null"
android:cropToPadding="false"
android:scaleType="centerCrop"
tools:background="@tools:sample/backgrounds/scenic" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginVertical="@dimen/mal_baseline"
android:layout_marginEnd="@dimen/mal_baseline"
android:layout_marginRight="@dimen/mal_baseline"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/mal_item_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
android:textColor="?android:textColorPrimary"
android:textSize="24sp"
tools:text="Władca Androida" />
<TextView
android:id="@+id/mal_item_desc"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textColor="?android:textColorSecondary"
android:textSize="14sp"
tools:text="kubasz" />
</LinearLayout>
</LinearLayout>

View File

@ -351,8 +351,6 @@
<string name="event_list_added_by_unknown_format">Hinzugefügt %1$s%3$s</string>
<string name="event_list_shared_by_format">{cmd-share-variant} %1$s von %2$s%3$s</string>
<string name="event_list_shared_by_self_format">{cmd-share-variant} %1$s von Ihnen%3$s</string>
<string name="event_manual_need_registration_text">Um das Ereignis freigeben zu können, müssen Sie die Serverregistrierungsoption aktivieren. Auf diese Weise können Sie Ereignisse erstellen und empfangen, die in Ihrer Klasse geteilt werden.\n\nWenn Sie auf OK klicken, wird diese automatisch aktiviert.\n\nStellen Sie sicher, dass Sie die Bedingungen lesen und akzeptieren.</string>
<string name="event_manual_need_registration_title">Ereignisse teilen</string>
<string name="event_manual_remove">Ereignis entfernen…</string>
<string name="event_manual_saving">Ereignis speichern…</string>
<string name="event_manual_share">Ereignis teilen…</string>
@ -830,9 +828,13 @@
<string name="rate_snackbar_positive">etzt bewerten</string>
<string name="rate_snackbar_text">Zeigen Sie, dass Ihnen Szkolny.eu gefällt - bewerten Sie die App und machen Sie sie noch besser!</string>
<string name="refresh">Aktualisieren</string>
<string name="registration_enable_dialog_text">Die Registrierung erfolgt automatisch, wenn diese Option aktiviert ist. Sie können Ereignisse erstellen und empfangen, die mit anderen Schülern in Ihrer Klasse geteilt werden. Dank dessen können Sie Elemente, die nicht vom Lehrer gespeichert wurden, zum Klassenbuch hinzufügen.\n\nStellen Sie sicher, dass Sie die Bestimmungen der <a href="http://szkolny.eu/privacy-policy"> Datenschutzrichtlinie </a> lesen und die Bestimmungen akzeptieren.</string>
<string name="registration_enable_dialog_title">Server-Registrierung</string>
<string name="registration_enable_progress_text">Gemeinsame Ereignisse herunterladen…</string>
<string name="registration_config_disable_progress_text">Benutzerregistrierung aufheben…</string>
<string name="registration_config_disable_text">Einige Funktionen wie Ereignisfreigabe oder Benachrichtigungsweiterleitung können nicht mehr verwendet werden.\n\nDie Funktion kann je nach ausgewähltem Profil aktiviert / deaktiviert werden.</string>
<string name="registration_config_enable_progress_text">Gemeinsame Ereignisse herunterladen…</string>
<string name="registration_config_enable_text">Die Registrierung erfolgt automatisch, wenn diese Option aktiviert ist. Sie können Ereignisse erstellen und empfangen, die mit anderen Schülern in Ihrer Klasse geteilt werden. Dank dessen können Sie Elemente, die nicht vom Lehrer gespeichert wurden, zum Klassenbuch hinzufügen.\n\nStellen Sie sicher, dass Sie die Bestimmungen der <a href="http://szkolny.eu/privacy-policy"> Datenschutzrichtlinie </a> lesen und die Bestimmungen akzeptieren.</string>
<string name="registration_config_event_sharing_text">Um das Ereignis freigeben zu können, müssen Sie die Serverregistrierungsoption aktivieren. Auf diese Weise können Sie Ereignisse erstellen und empfangen, die in Ihrer Klasse geteilt werden.\n\nWenn Sie auf OK klicken, wird diese automatisch aktiviert.\n\nStellen Sie sicher, dass Sie die Bedingungen lesen und akzeptieren.</string>
<string name="registration_config_event_sharing_title">Ereignisse teilen</string>
<string name="registration_config_title">Szkolny.eu Server-Registrierung</string>
<string name="remove">Löschen</string>
<string name="removed">gelöschtet</string>
<string name="report">Melden</string>
@ -847,13 +849,13 @@
<string name="settings_about_crash_text">Klicken Sie hier, um eine unerwartete Ausnahme auszulösen</string>
<string name="settings_about_discord_subtext">Treten Sie unserem Discord-Server bei!</string>
<string name="settings_about_discord_text">Discord Server</string>
<string name="settings_about_language_dialog_text">Hinweis. Diese Option funktioniert möglicherweise auf einigen Geräten und in einigen Teilen der App nicht.</string>
<string name="settings_about_language_dialog_title">App-Sprache ändern</string>
<string name="app_language_dialog_text">Hinweis. Diese Option funktioniert möglicherweise auf einigen Geräten und in einigen Teilen der App nicht.</string>
<string name="app_language_dialog_title">App-Sprache ändern</string>
<string name="settings_about_language_subtext">Deutsch</string>
<string name="settings_about_language_text">Sprache der App</string>
<string name="settings_about_licenses_text">Open-Source-Lizenzen</string>
<string name="settings_about_privacy_policy_text">Datenschutzrichtlinie</string>
<string name="settings_about_register_title_text">E-Klassenbuch</string>
<string name="settings_card_register_title">E-Klassenbuch</string>
<string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nSeptember 2018 - Februar 2021</string>
<string name="settings_about_update_subtext">Klicken Sie hier, um nach Aktualisierungen zu suchen</string>
<string name="settings_about_update_text">Aktualisierung</string>
@ -944,7 +946,7 @@
<string name="settings_sync_sync_interval_subtext_disabled">Deaktiviert</string>
<string name="settings_sync_sync_interval_subtext_format">Daten alle %s herunterladen</string>
<string name="settings_sync_sync_interval_text">Automatische Synchronisierung</string>
<string name="settings_sync_title_text">Synchronisation und Benachrichtigungen</string>
<string name="settings_card_sync_title">Synchronisation und Benachrichtigungen</string>
<string name="settings_sync_updates_text">Über App-Aktualisierungen benachrichtigen</string>
<string name="settings_sync_web_push_subtext">Benachrichtigungen auf Ihrem PC anzeigen</string>
<string name="settings_sync_web_push_text">Benachrichtigungsweiterleitung</string>
@ -968,7 +970,7 @@
<string name="settings_theme_theme_pink">Rosa</string>
<string name="settings_theme_theme_system">System</string>
<string name="settings_theme_theme_text">Thema</string>
<string name="settings_theme_title_text">Aussehen</string>
<string name="settings_card_theme_title">Aussehen</string>
<string name="share">Teilen</string>
<string name="share_intent">Teilen über…</string>
<string name="sharing_event">Daten Teilen…</string>

View File

@ -351,8 +351,6 @@
<string name="event_list_added_by_unknown_format">Adde %1$s%3$s</string>
<string name="event_list_shared_by_format">{cmd-share-variant} %1$s by %2$s%3$s</string>
<string name="event_list_shared_by_self_format">{cmd-share-variant} %1$s by you%3$s</string>
<string name="event_manual_need_registration_text">You need to turn on server registration, in order to share an event. This option lets you create and receive shared events in your class.\n\nIt will be turned on automatically by pressing OK.\n\nBefore turning it on, make sure you\'ve read the terms and accept them</string>
<string name="event_manual_need_registration_title">Sharing events</string>
<string name="event_manual_remove">Removing event…</string>
<string name="event_manual_saving">Saving event…</string>
<string name="event_manual_share">Sharing event…</string>
@ -832,9 +830,13 @@
<string name="rate_snackbar_positive">Rate now</string>
<string name="rate_snackbar_text">Show that you like Szkolny.eu - rate the app and make it even better!</string>
<string name="refresh">Refresh</string>
<string name="registration_enable_dialog_text">Registration is automatic if this option is enabled. It allows you to create and receive events shared with other students in your class. Thanks to this, you can add items not saved by the teacher to the journal.\n\nMake sure you read the terms and accept them.</string>
<string name="registration_enable_dialog_title">Server registration</string>
<string name="registration_enable_progress_text">Syncing shared events…</string>
<string name="registration_config_disable_progress_text">Unregistering the user…</string>
<string name="registration_config_disable_text">You won\'t be able to use some functions anymore, like Event sharing and Notification forwarding.\n\nThis feature may be enabled/disabled depending on the active profile.</string>
<string name="registration_config_enable_progress_text">Syncing shared events…</string>
<string name="registration_config_enable_text">Registration is automatic if this option is enabled. It allows you to create and receive events shared with other students in your class. Thanks to this, you can add items not saved by the teacher to the journal.\n\nContinuing means reading and accepting the <a href="https://szkolny.eu/privacy-policy">Privacy policy</a>.</string>
<string name="registration_config_event_sharing_text">You need to turn on server registration, in order to share an event. This option lets you create and receive shared events in your class.\n\nIt will be turned on automatically by pressing OK.\n\nContinuing means reading and accepting the <a href="https://szkolny.eu/privacy-policy">Privacy policy</a>.</string>
<string name="registration_config_event_sharing_title">Sharing events</string>
<string name="registration_config_title">Szkolny.eu server registration</string>
<string name="remove">Remove</string>
<string name="removed">Removed</string>
<string name="report">Report</string>
@ -849,13 +851,13 @@
<string name="settings_about_crash_text">Click to throw an unexpected exception</string>
<string name="settings_about_discord_subtext">Join our Discord community!</string>
<string name="settings_about_discord_text">Discord server</string>
<string name="settings_about_language_dialog_text">Notice. This feature may not work on some devices or in some parts of the app.</string>
<string name="settings_about_language_dialog_title">Change app language</string>
<string name="settings_about_language_subtext">"English "</string>
<string name="app_language_dialog_text">Notice. This feature may not work on some devices or in some parts of the app.</string>
<string name="app_language_dialog_title">Change app language</string>
<string name="settings_about_language_subtext">English</string>
<string name="settings_about_language_text">App language</string>
<string name="settings_about_licenses_text">Open-source licenses</string>
<string name="settings_about_privacy_policy_text">Privacy policy</string>
<string name="settings_about_register_title_text">E-register</string>
<string name="settings_card_register_title">E-register</string>
<string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nSeptember 2018 - February 2021</string>
<string name="settings_about_update_subtext">Click to check for updates</string>
<string name="settings_about_update_text">Update</string>
@ -946,7 +948,7 @@
<string name="settings_sync_sync_interval_subtext_disabled">Disabled</string>
<string name="settings_sync_sync_interval_subtext_format">Download data every %s</string>
<string name="settings_sync_sync_interval_text">Automatic sync</string>
<string name="settings_sync_title_text">Sync &amp; notifications</string>
<string name="settings_card_sync_title">Sync &amp; notifications</string>
<string name="settings_sync_updates_text">Notify about app updates</string>
<string name="settings_sync_web_push_subtext">Show notifications on your PC</string>
<string name="settings_sync_web_push_text">Notification forwarding</string>
@ -970,7 +972,7 @@
<string name="settings_theme_theme_pink">Pink</string>
<string name="settings_theme_theme_system">System</string>
<string name="settings_theme_theme_text">Theme</string>
<string name="settings_theme_title_text">Appearance</string>
<string name="settings_card_theme_title">Appearance</string>
<string name="share">Share</string>
<string name="share_intent">Share…</string>
<string name="sharing_event">Sharing events…</string>

View File

@ -386,8 +386,6 @@
<string name="event_list_added_by_unknown_format">Dodano %1$s%3$s</string>
<string name="event_list_shared_by_format">{cmd-share-variant} %1$s przez %2$s%3$s</string>
<string name="event_list_shared_by_self_format">{cmd-share-variant} %1$s przez Ciebie%3$s</string>
<string name="event_manual_need_registration_text">Aby móc udostępnić wydarzenie, należy włączyć opcję rejestracji na serwerze. Pozwala to na tworzenie i odbieranie wydarzeń udostępnionych w Twojej klasie.\n\nPo kliknięciu OK zostanie ona automatycznie włączona.\n\nUpewnij się, że zapoznałeś się z warunkami i akceptujesz jej postanowienia.</string>
<string name="event_manual_need_registration_title">Udostępnianie wydarzeń</string>
<string name="event_manual_remove">Usuwam wydarzenie…</string>
<string name="event_manual_saving">Zapisuję wydarzenie…</string>
<string name="event_manual_share">Udostępniam wydarzenie…</string>
@ -895,9 +893,13 @@
<string name="rate_snackbar_positive">Oceń teraz</string>
<string name="rate_snackbar_text">Pokaż, że podoba ci się Szkolny.eu - oceń aplikację i spraw, by była jeszcze lepsza!</string>
<string name="refresh">Odśwież</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_title">Rejestracja na serwerze</string>
<string name="registration_enable_progress_text">Pobieranie udostępnionych wydarzeń…</string>
<string name="registration_config_disable_progress_text">Trwa wyrejestrowywanie użytkownika…</string>
<string name="registration_config_disable_text">Stracisz możliwość korzystania z niektórych funkcji, takich jak Udostępnianie wydarzeń czy Przekazywanie powiadomień.\n\nFunkcja może być włączona/wyłączona w zależności od wybranego profilu.</string>
<string name="registration_config_enable_progress_text">Pobieranie udostępnionych wydarzeń…</string>
<string name="registration_config_enable_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\nKontynuując, oświadczasz przeczytanie i akceptację postanowień <a href="https://szkolny.eu/privacy-policy">Polityki prywatności</a>.</string>
<string name="registration_config_event_sharing_text">Aby móc udostępnić wydarzenie, należy włączyć opcję rejestracji na serwerze. Pozwala to na tworzenie i odbieranie wydarzeń udostępnionych w Twojej klasie.\n\nPo kliknięciu OK zostanie ona automatycznie włączona.\n\nKontynuując, oświadczasz przeczytanie i akceptację postanowień <a href="https://szkolny.eu/privacy-policy">Polityki prywatności</a>.</string>
<string name="registration_config_event_sharing_title">Udostępnianie wydarzeń</string>
<string name="registration_config_title">Rejestracja aplikacji Szkolny.eu</string>
<string name="remove">Usuń</string>
<string name="removed">Usunięto</string>
<string name="report">Zgłoś</string>
@ -912,13 +914,13 @@
<string name="settings_about_crash_text">Kliknij, aby wywołać nieoczekiwany wyjątek</string>
<string name="settings_about_discord_subtext">Dołącz do naszego serwera Discord!</string>
<string name="settings_about_discord_text">Serwer Discord</string>
<string name="settings_about_language_dialog_text">Uwaga. Ta opcja może nie działać na niektórych urządzeniach oraz w niektórych fragmentach aplikacji.</string>
<string name="settings_about_language_dialog_title">Zmień język aplikacji</string>
<string name="app_language_dialog_text">Uwaga. Ta opcja może nie działać na niektórych urządzeniach oraz w niektórych fragmentach aplikacji.</string>
<string name="app_language_dialog_title">Zmień język aplikacji</string>
<string name="settings_about_language_subtext">Polski</string>
<string name="settings_about_language_text">Język aplikacji</string>
<string name="settings_about_licenses_text">Licencje open-source</string>
<string name="settings_about_privacy_policy_text">Polityka prywatności</string>
<string name="settings_about_register_title_text">E-dziennik</string>
<string name="settings_card_register_title">E-dziennik</string>
<string name="settings_about_title_subtext">© Kuba Szczodrzyński &amp;&amp; Kacper Ziubryniewicz\nwrzesień 2018 - luty 2021</string>
<string name="settings_about_update_subtext">Kliknij, aby sprawdzić aktualizacje</string>
<string name="settings_about_update_text">Aktualizacja</string>
@ -1011,7 +1013,7 @@
<string name="settings_sync_sync_interval_subtext_disabled">Wyłączona</string>
<string name="settings_sync_sync_interval_subtext_format">Pobieraj dane co %s</string>
<string name="settings_sync_sync_interval_text">Synchronizacja automatyczna</string>
<string name="settings_sync_title_text">Synchronizacja i powiadomienia</string>
<string name="settings_card_sync_title">Synchronizacja i powiadomienia</string>
<string name="settings_sync_updates_text">Powiadamiaj o aktualizacjach aplikacji</string>
<string name="settings_sync_web_push_subtext">Pokazuj powiadomienia na swoim komputerze</string>
<string name="settings_sync_web_push_text">Przekazywanie powiadomień</string>
@ -1035,7 +1037,7 @@
<string name="settings_theme_theme_pink">Różowy</string>
<string name="settings_theme_theme_system">Systemowy</string>
<string name="settings_theme_theme_text">Motyw</string>
<string name="settings_theme_title_text">Wygląd</string>
<string name="settings_card_theme_title">Wygląd</string>
<string name="share">Udostępnij</string>
<string name="share_intent">Udostępnij przez…</string>
<string name="sharing_event">Udostępnianie danych…</string>
@ -1387,4 +1389,14 @@
<string name="login_chooser_mode_dev_only">{cmd-android-studio} Wersja deweloperska</string>
<string name="eggs">\???</string>
<string name="rate_snackbar_negative_message">Szkoda, opinie innych pomagają mi rozwijać aplikację.</string>
<string name="event_manual_no_profile">Nie znaleziono profilu ucznia.</string>
<string name="see_also">Zobacz także</string>
<string name="settings_about_homepage_text">Wejdź na stronę aplikacji</string>
<string name="settings_about_homepage_subtext">Uzyskaj pomoc lub wesprzyj autorów</string>
<string name="settings_about_github_text">Kod źródłowy</string>
<string name="settings_about_github_subtext">Pomóż w rozwoju aplikacji na GitHubie</string>
<string name="profile_config_name_hint">Nazwa profilu</string>
<string name="profile_config_sync_enabled">Synchronizuj ten profil</string>
<string name="profile_config_logout">Wyloguj się</string>
<string name="permissions_qr_scanner">Aby móc zeskanować kod QR musisz przyznać uprawnienia dostępu do kamery.\n\nKliknij OK, aby przyznać uprawnienia.</string>
</resources>