Archived
FetcherActivity.kt now has a bottom app bar with useful actions and extends FragmentActivity instead of AppCompatActivity to show a tutorial bottom sheet
This commit is contained in:
@@ -5,12 +5,13 @@ import android.os.Bundle
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import com.denizk0461.studip.R
|
||||
import com.denizk0461.studip.data.showErrorSnackBar
|
||||
import com.denizk0461.studip.data.showToast
|
||||
import com.denizk0461.studip.databinding.ActivityFetcherBinding
|
||||
import com.denizk0461.studip.sheet.TextSheet
|
||||
import com.denizk0461.studip.viewmodel.FetcherViewModel
|
||||
import java.io.IOException
|
||||
import java.net.URLDecoder
|
||||
@@ -19,7 +20,7 @@ import java.net.URLDecoder
|
||||
* Activity for allowing the user to log in to access and fetch their schedule from their Stud.IP
|
||||
* profile. Launched through a button in the app's settings.
|
||||
*/
|
||||
class FetcherActivity : AppCompatActivity() {
|
||||
class FetcherActivity : FragmentActivity() {
|
||||
|
||||
// View binding
|
||||
private lateinit var binding: ActivityFetcherBinding
|
||||
@@ -54,13 +55,40 @@ class FetcherActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
// Set up bottom app bar for various actions
|
||||
binding.bottomAppBar.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.close -> {
|
||||
// Close the activity
|
||||
finish()
|
||||
true
|
||||
}
|
||||
R.id.refresh -> {
|
||||
// Refresh the currently opened page
|
||||
binding.webview.reload()
|
||||
true
|
||||
}
|
||||
R.id.help -> {
|
||||
// Present the user with a small tutorial on how to fetch their timetable
|
||||
TextSheet().also { sheet ->
|
||||
val bundle = Bundle()
|
||||
bundle.putString("header", getString(R.string.fetch_bar_help_sheet_title))
|
||||
bundle.putString("content", getString(R.string.fetch_bar_help_sheet_content))
|
||||
sheet.arguments = bundle
|
||||
}.show(supportFragmentManager, TextSheet::class.java.simpleName)
|
||||
true
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Attempt to load the user-set Stud.IP front page. Redirects to login page if user is
|
||||
* not logged in.
|
||||
*/
|
||||
binding.webview.loadUrl("https://elearning.uni-bremen.de/index.php?again=yes")
|
||||
|
||||
binding.fab.setOnClickListener { view ->
|
||||
binding.fab.setOnClickListener {
|
||||
|
||||
if (binding.webview.url?.contains(
|
||||
"https://elearning.uni-bremen.de/dispatch.php/calendar/schedule"
|
||||
@@ -78,20 +106,19 @@ class FetcherActivity : AppCompatActivity() {
|
||||
viewModel.parse(URLDecoder.decode(p0, "UTF-8"))
|
||||
|
||||
// Notify the user that the fetch was successful
|
||||
Toast.makeText(
|
||||
this, R.string.toast_fetch_finished, Toast.LENGTH_SHORT
|
||||
).show()
|
||||
showToast(this, getString(R.string.toast_fetch_finished))
|
||||
|
||||
// Close the activity
|
||||
finish()
|
||||
|
||||
} catch (e: IOException) {
|
||||
// Let the user know that an error occurred
|
||||
theme.showErrorSnackBar(binding.rootView, getString(R.string.fetch_error_snack))
|
||||
theme.showErrorSnackBar(binding.rootView, getString(R.string.fetch_error_snack), binding.bottomAppBar)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
theme.showErrorSnackBar(binding.rootView, getString(R.string.fetch_error_webpage_snack))
|
||||
// Tell the user that they must navigate to their timetable
|
||||
theme.showErrorSnackBar(binding.rootView, getString(R.string.fetch_error_webpage_snack), binding.bottomAppBar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
@@ -51,13 +52,15 @@ fun showToast(context: Context, text: String) {
|
||||
Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
fun Resources.Theme.showErrorSnackBar(view: CoordinatorLayout, text: String) {
|
||||
Snackbar
|
||||
fun Resources.Theme.showErrorSnackBar(view: CoordinatorLayout, text: String, anchor: View? = null) {
|
||||
val s = Snackbar
|
||||
.make(view, text, Snackbar.LENGTH_SHORT)
|
||||
// Set colours to signify an error
|
||||
.setBackgroundTint(getThemedColor(R.attr.colorErrorContainer))
|
||||
.setTextColor(getThemedColor(R.attr.colorOnErrorContainer))
|
||||
.show()
|
||||
|
||||
anchor?.let { a -> s.setAnchorView(a) }
|
||||
s.show()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,7 +24,7 @@ class AppRepository(app: Application) {
|
||||
private val dietaryPrefString = "prefs_obj"
|
||||
|
||||
// Blank dietary preference regular expression
|
||||
private val blankDietaryPrefs: String = DietaryPreferences.C_FALSE.toString().repeat(10)
|
||||
private val blankDietaryPrefs: String = DietaryPreferences.TEMPLATE_EMPTY
|
||||
|
||||
/**
|
||||
* Used to observe just to register an update of the user-set dietary preferences.
|
||||
|
||||
@@ -7,20 +7,18 @@ import android.os.Bundle
|
||||
import android.widget.TimePicker
|
||||
|
||||
/**
|
||||
* Dialogue used for letting the user pick a timestamp.
|
||||
* Dialogue used for letting the user pick a timestamp. TODO crashes on config change (multitask)
|
||||
*
|
||||
* @param timestamp timestamp that is to be edited
|
||||
* @param listener listener for when the user finishes setting a time
|
||||
* @param isEventStart whether the timestamp to be edited is the start of the event
|
||||
*/
|
||||
class TimePickerFragment(
|
||||
private val timestamp: String,
|
||||
private val listener: OnTimeSetListener,
|
||||
private val isEventStart: Boolean,
|
||||
) : DialogFragment(), TimePickerDialog.OnTimeSetListener {
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val timestampSplit = timestamp.split(":")
|
||||
val timestampSplit = (arguments?.getString("timestamp") ?: "").split(":")
|
||||
return TimePickerDialog(
|
||||
activity,
|
||||
this,
|
||||
|
||||
@@ -121,7 +121,7 @@ class CanteenFragment : AppFragment() {
|
||||
chip.isChecked = getPreference(pref)
|
||||
|
||||
// Set a listener for when the user clicks the chip
|
||||
chip.setOnCheckedChangeListener { buttonView, newValue ->
|
||||
chip.setOnCheckedChangeListener { _, newValue ->
|
||||
// Save the preference change to persistent storage
|
||||
setPreference(pref, newValue)
|
||||
}
|
||||
|
||||
@@ -141,7 +141,6 @@ class SettingsFragment : AppFragment() {
|
||||
intent.putExtras(bundle)
|
||||
})
|
||||
}
|
||||
10 -> showToast(context, getString(R.string.settings_app_version_10))
|
||||
15 -> showToast(context, getString(R.string.settings_app_version_15))
|
||||
20 -> showToast(context, getString(R.string.settings_app_version_20))
|
||||
30 -> showToast(context, getString(R.string.settings_app_version_30))
|
||||
@@ -151,7 +150,10 @@ class SettingsFragment : AppFragment() {
|
||||
200 -> showToast(context, getString(R.string.settings_app_version_200))
|
||||
222 -> {
|
||||
startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(mysteryLink)))
|
||||
appVersionClick = -1
|
||||
}
|
||||
238 -> { // after 222 has been reached, this loops every 16 clicks
|
||||
showToast(context, resources.getStringArray(R.array.cheesy).random())
|
||||
appVersionClick = 222
|
||||
}
|
||||
}
|
||||
appVersionClick += 1
|
||||
|
||||
@@ -1,65 +1,38 @@
|
||||
package com.denizk0461.studip.model
|
||||
|
||||
// TODO KDoc
|
||||
enum class AllergenPreferences {
|
||||
GLUTEN_WHEAT,
|
||||
GLUTEN_RYE,
|
||||
GLUTEN_BARLEY,
|
||||
GLUTEN_OATS,
|
||||
GLUTEN_SPELT,
|
||||
GLUTEN_KAMUT,
|
||||
CRUSTACEANS,
|
||||
EGGS,
|
||||
FISH,
|
||||
PEANUTS,
|
||||
SOY,
|
||||
DAIRY,
|
||||
ALMONDS,
|
||||
HAZELNUTS,
|
||||
WALNUTS,
|
||||
CASHEW_NUTS,
|
||||
PECANS,
|
||||
BRAZIL_NUTS,
|
||||
PISTACHIOS,
|
||||
MACADAMIA,
|
||||
CELERY,
|
||||
MUSTARD,
|
||||
SULPHIDES,
|
||||
LUPINS,
|
||||
SESAME,
|
||||
MOLLUSCS,
|
||||
;
|
||||
class AllergenPreferences {
|
||||
|
||||
/**
|
||||
* Object that holds allergy preferences. Can be used to hold both the user-set values as well
|
||||
* as the values of an individual canteen item.
|
||||
*
|
||||
* @param hasWheat
|
||||
* @param hasRye
|
||||
* @param hasBarley
|
||||
* @param hasOats
|
||||
* @param hasSpelt
|
||||
* @param hasKamut
|
||||
* @param hasCrustaceans
|
||||
* @param hasEggs
|
||||
* @param hasFish
|
||||
* @param hasPeanuts
|
||||
* @param hasSoy
|
||||
* @param hasDairy
|
||||
* @param hasAlmonds
|
||||
* @param hasHazelnuts
|
||||
* @param hasWalnuts
|
||||
* @param hasCashewNuts
|
||||
* @param hasPecans
|
||||
* @param hasBrazilNuts
|
||||
* @param hasPistachios
|
||||
* @param hasMacadamia
|
||||
* @param hasCelery
|
||||
* @param hasMustard
|
||||
* @param hasSulphides
|
||||
* @param hasLupins
|
||||
* @param hasSesame
|
||||
* @param hasMolluscs
|
||||
* @param hasWheat whether the offer contains gluten through wheat
|
||||
* @param hasRye whether the offer contains gluten through rye
|
||||
* @param hasBarley whether the offer contains gluten through barley
|
||||
* @param hasOats whether the offer contains gluten through oats
|
||||
* @param hasSpelt whether the offer contains gluten through spelt
|
||||
* @param hasKamut whether the offer contains gluten through kamut
|
||||
* @param hasCrustaceans whether the offer contains crustaceans
|
||||
* @param hasEggs whether the offer contains eggs
|
||||
* @param hasFish whether the offer contains fish
|
||||
* @param hasPeanuts whether the offer contains peanuts
|
||||
* @param hasSoy whether the offer contains soy
|
||||
* @param hasDairy whether the offer contains dairy
|
||||
* @param hasAlmonds whether the offer contains almonds
|
||||
* @param hasHazelnuts whether the offer contains hazelnuts
|
||||
* @param hasWalnuts whether the offer contains walnuts
|
||||
* @param hasCashewNuts whether the offer contains cashew nuts
|
||||
* @param hasPecans whether the offer contains pecan nuts
|
||||
* @param hasBrazilNuts whether the offer contains brazil nuts
|
||||
* @param hasPistachios whether the offer contains pistachios
|
||||
* @param hasMacadamia whether the offer contains macadamia nuts
|
||||
* @param hasCelery whether the offer contains celery
|
||||
* @param hasMustard whether the offer contains mustard
|
||||
* @param hasSulphides whether the offer contains highly concentrated sulphides
|
||||
* @param hasLupins whether the offer contains lupins
|
||||
* @param hasSesame whether the offer contains sesame
|
||||
* @param hasMolluscs whether the offer contains molluscs
|
||||
*/
|
||||
data class Object(
|
||||
val hasWheat: Boolean,
|
||||
@@ -130,15 +103,16 @@ enum class AllergenPreferences {
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val C_TRUE: Char = 'y'
|
||||
const val C_FALSE: Char = 'n'
|
||||
|
||||
/**
|
||||
* Template string representing no allergens.
|
||||
*/
|
||||
const val TEMPLATE: String = ""
|
||||
|
||||
/**
|
||||
* Constructs an AllergenPreferenceObject from a string.
|
||||
*
|
||||
* @param input string that must be 26 characters long
|
||||
* @param input string that must be 26 characters long. This will NOT be checked
|
||||
* @return instance of AllergenPreferences.Object with preferences inserted
|
||||
*/
|
||||
fun construct(input: String): Object {
|
||||
@@ -172,37 +146,5 @@ enum class AllergenPreferences {
|
||||
elements.contains("n"),
|
||||
)
|
||||
}
|
||||
// String(
|
||||
// charArrayOf(
|
||||
// charFor(obj.hasWheat),
|
||||
// charFor(obj.hasRye),
|
||||
// charFor(obj.hasBarley),
|
||||
// charFor(obj.hasOats),
|
||||
// charFor(obj.hasSpelt),
|
||||
// charFor(obj.hasKamut),
|
||||
// charFor(obj.hasCrustaceans),
|
||||
// charFor(obj.hasEggs),
|
||||
// charFor(obj.hasFish),
|
||||
// charFor(obj.hasPeanuts),
|
||||
// charFor(obj.hasSoy),
|
||||
// charFor(obj.hasDairy),
|
||||
// charFor(obj.hasAlmonds),
|
||||
// charFor(obj.hasHazelnuts),
|
||||
// charFor(obj.hasWalnuts),
|
||||
// charFor(obj.hasCashewNuts),
|
||||
// charFor(obj.hasPecans),
|
||||
// charFor(obj.hasBrazilNuts),
|
||||
// charFor(obj.hasPistachios),
|
||||
// charFor(obj.hasMacadamia),
|
||||
// charFor(obj.hasCelery),
|
||||
// charFor(obj.hasMustard),
|
||||
// charFor(obj.hasSulphides),
|
||||
// charFor(obj.hasLupins),
|
||||
// charFor(obj.hasSesame),
|
||||
// charFor(obj.hasMolluscs),
|
||||
// )
|
||||
// )
|
||||
|
||||
private fun charFor(pref: Boolean) = if (pref) C_TRUE else C_FALSE
|
||||
}
|
||||
}
|
||||
@@ -128,17 +128,23 @@ enum class DietaryPreferences(val value: String) {
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Char used to construct a regex to denote that a preference is met
|
||||
* Char used to construct a regex to denote that a preference is met.
|
||||
*/
|
||||
const val C_TRUE: Char = 't'
|
||||
|
||||
/**
|
||||
* Char used to construct a regex to denote that a preference is not met
|
||||
* Char used to construct a regex to denote that a preference is not met.
|
||||
*/
|
||||
const val C_FALSE: Char = '.'
|
||||
|
||||
/**
|
||||
* Default value for preferences if none are set.
|
||||
*/
|
||||
const val TEMPLATE_EMPTY: String = ".........."
|
||||
|
||||
/**
|
||||
* Object denoting that no dietary preferences apply.
|
||||
*/
|
||||
val NONE_MET: Object = construct(TEMPLATE_EMPTY)
|
||||
|
||||
/**
|
||||
|
||||
@@ -37,6 +37,9 @@ class ScheduleUpdateSheet : AppSheet(R.layout.sheet_schedule_update), TimePicker
|
||||
// View model
|
||||
private val viewModel: ScheduleUpdateViewModel by viewModels()
|
||||
|
||||
// Whether the user has once clicked the delete button; used to prevent an accidental click
|
||||
private var hasClickedDelete: Boolean = false
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
@@ -54,6 +57,7 @@ class ScheduleUpdateSheet : AppSheet(R.layout.sheet_schedule_update), TimePicker
|
||||
binding.editTextLecturers.setText(event.lecturer)
|
||||
binding.editTextRoom.setText(event.room)
|
||||
|
||||
|
||||
/*
|
||||
* Show academic quarter suggestion if it is applicable. For this to be the case, both the
|
||||
* start timestamp and the end timestamp must match up in a pattern that would make it
|
||||
@@ -62,63 +66,63 @@ class ScheduleUpdateSheet : AppSheet(R.layout.sheet_schedule_update), TimePicker
|
||||
*
|
||||
* 13:00 - 15:30 -> likely no academic quarter intended
|
||||
*/
|
||||
binding.buttonAcademicQuarter.visibility = if (
|
||||
timeslotsAcademicQuarter.contains(timeslotStart) &&
|
||||
timeslotsAcademicQuarter.contains(timeslotEnd)
|
||||
) {
|
||||
binding.buttonAcademicQuarter.setOnClickListener {
|
||||
try {
|
||||
// Retrieve new timestamps with the academic quarter applied, or throw a tantrum
|
||||
val newStart = getTimestampAcademicQuarterStart(timeslotStart)
|
||||
val newEnd = getTimestampAcademicQuarterEnd(timeslotEnd)
|
||||
binding.buttonAcademicQuarter.setOnClickListener {
|
||||
try {
|
||||
// Retrieve new timestamps with the academic quarter applied, or throw a tantrum
|
||||
val newStart = getTimestampAcademicQuarterStart(timeslotStart)
|
||||
val newEnd = getTimestampAcademicQuarterEnd(timeslotEnd)
|
||||
|
||||
// Set the buttons to the new timesatmps
|
||||
timeslotStart = newStart
|
||||
binding.buttonTimeStart.text = newStart
|
||||
timeslotEnd = newEnd
|
||||
binding.buttonTimeEnd.text = newEnd
|
||||
// Set the buttons to the new timesatmps
|
||||
timeslotStart = newStart
|
||||
binding.buttonTimeStart.text = newStart
|
||||
timeslotEnd = newEnd
|
||||
binding.buttonTimeEnd.text = newEnd
|
||||
|
||||
/*
|
||||
/*
|
||||
* Attempt to fix the issue of the bottom sheet jumping up when hiding the
|
||||
* button.
|
||||
*/
|
||||
TransitionManager.beginDelayedTransition(binding.sheet.parent as ViewGroup)
|
||||
binding.buttonAcademicQuarter.visibility = View.GONE
|
||||
TransitionManager.beginDelayedTransition(binding.sheet.parent as ViewGroup)
|
||||
binding.buttonAcademicQuarter.visibility = View.GONE
|
||||
|
||||
// Catch if a timeslot couldn't be found in the list, and tell the user
|
||||
} catch (e: AcademicQuarterNotApplicableException) {
|
||||
showToast(
|
||||
context,
|
||||
getString(R.string.sheet_schedule_update_hint_quarter_error)
|
||||
)
|
||||
}
|
||||
// Catch if a timeslot couldn't be found in the list, and tell the user
|
||||
} catch (e: AcademicQuarterNotApplicableException) {
|
||||
showToast(
|
||||
context,
|
||||
getString(R.string.sheet_schedule_update_hint_quarter_error)
|
||||
)
|
||||
}
|
||||
|
||||
// If the pattern applies, show the button to the user
|
||||
View.VISIBLE
|
||||
} else {
|
||||
// Hide the button if the action is not necessary or applicable
|
||||
View.GONE
|
||||
}
|
||||
|
||||
binding.buttonAcademicQuarter.visibility = getVisibilityForAcademicQuarter(
|
||||
timeslotsAcademicQuarter.contains(timeslotStart),
|
||||
timeslotsAcademicQuarter.contains(timeslotEnd),
|
||||
)
|
||||
|
||||
// Prepare timestamp buttons
|
||||
binding.buttonTimeStart.text = timeslotStart
|
||||
binding.buttonTimeStart.setOnClickListener {
|
||||
// Launch a time picker dialogue to pick a new start timestamp
|
||||
TimePickerFragment(
|
||||
binding.buttonTimeStart.text.toString(),
|
||||
this,
|
||||
true,
|
||||
).show((context as FragmentActivity).supportFragmentManager, "timePicker")
|
||||
).also { sheet ->
|
||||
val bundle = Bundle()
|
||||
bundle.putString("timestamp", binding.buttonTimeStart.text.toString())
|
||||
sheet.arguments = bundle
|
||||
}.show((context as FragmentActivity).supportFragmentManager, "timePicker")
|
||||
}
|
||||
binding.buttonTimeEnd.text = timeslotEnd
|
||||
binding.buttonTimeEnd.setOnClickListener {
|
||||
// Launch a time picker dialogue to pick a new end timestamp
|
||||
TimePickerFragment(
|
||||
binding.buttonTimeEnd.text.toString(),
|
||||
this,
|
||||
false,
|
||||
).show((context as FragmentActivity).supportFragmentManager, "timePicker")
|
||||
).also { sheet ->
|
||||
val bundle = Bundle()
|
||||
bundle.putString("timestamp", binding.buttonTimeEnd.text.toString())
|
||||
sheet.arguments = bundle
|
||||
}.show((context as FragmentActivity).supportFragmentManager, "timePicker")
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -134,9 +138,20 @@ class ScheduleUpdateSheet : AppSheet(R.layout.sheet_schedule_update), TimePicker
|
||||
|
||||
// Prepare delete button
|
||||
binding.buttonDelete.setOnClickListener {
|
||||
viewModel.delete(event)
|
||||
// Dismiss the sheet upon deletion
|
||||
dismiss()
|
||||
/*
|
||||
* Check if the user has already clicked the button before; this is done to prevent
|
||||
* accidentally deleting an event.
|
||||
*/
|
||||
if (hasClickedDelete) {
|
||||
viewModel.delete(event)
|
||||
// Dismiss the sheet upon deletion
|
||||
dismiss()
|
||||
} else {
|
||||
// Update the text to show the user that they clicked the button once
|
||||
TransitionManager.beginDelayedTransition(binding.buttonContainer as ViewGroup)
|
||||
binding.buttonDelete.text = getString(R.string.sheet_schedule_update_delete_confirm)
|
||||
hasClickedDelete = true
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare update/save button
|
||||
@@ -193,6 +208,26 @@ class ScheduleUpdateSheet : AppSheet(R.layout.sheet_schedule_update), TimePicker
|
||||
timeslotEnd = newTimestamp
|
||||
binding.buttonTimeEnd.text = newTimestamp
|
||||
}
|
||||
|
||||
// Set appropriate visibility to the academic quarter button
|
||||
TransitionManager.beginDelayedTransition(binding.sheet.parent as ViewGroup)
|
||||
binding.buttonAcademicQuarter.visibility = getVisibilityForAcademicQuarter(
|
||||
timeslotsAcademicQuarter.contains(timeslotStart),
|
||||
timeslotsAcademicQuarter.contains(timeslotEnd),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which visibility the academic quarter button should have given the timestamps.
|
||||
*
|
||||
* @param isStartValid whether the starting timestamp can be converted to an academic quarter
|
||||
* @param isEndValid whether the ending timestamp can be converted to an academic quarter
|
||||
*/
|
||||
private fun getVisibilityForAcademicQuarter(isStartValid: Boolean, isEndValid: Boolean): Int =
|
||||
if (isStartValid && isEndValid) {
|
||||
View.VISIBLE
|
||||
} else {
|
||||
View.GONE
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user