Convert BezelImageView, EndlessOnScrollListener, ComposeScheduleView, ProgressImageView to Kotlin (#3147)
* Convert BezelImageView to Kotlin * Convert EndlessOnScrollListener to Kotlin * Convert ComposeScheduleView to use view binding * Convert ComposeScheduleView to Kotlin * Convert ProgressImageView to Kotlin * Apply reviewer feedback
This commit is contained in:
parent
aa96d02923
commit
e5e076b0d3
9 changed files with 410 additions and 485 deletions
|
@ -1244,7 +1244,7 @@ class ComposeActivity :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onTimeSet(time: String) {
|
override fun onTimeSet(time: String?) {
|
||||||
viewModel.updateScheduledAt(time)
|
viewModel.updateScheduledAt(time)
|
||||||
if (verifyScheduledTime()) {
|
if (verifyScheduledTime()) {
|
||||||
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||||
|
|
|
@ -1,248 +0,0 @@
|
||||||
/* Copyright 2019 kyori19
|
|
||||||
*
|
|
||||||
* This file is a part of Tusky.
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
|
||||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
||||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
||||||
* Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
|
||||||
* see <http://www.gnu.org/licenses>. */
|
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.compose.view;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
|
|
||||||
import com.google.android.material.datepicker.CalendarConstraints;
|
|
||||||
import com.google.android.material.datepicker.DateValidatorPointForward;
|
|
||||||
import com.google.android.material.datepicker.MaterialDatePicker;
|
|
||||||
import com.google.android.material.timepicker.MaterialTimePicker;
|
|
||||||
import com.google.android.material.timepicker.TimeFormat;
|
|
||||||
import com.keylesspalace.tusky.R;
|
|
||||||
|
|
||||||
import java.text.DateFormat;
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.TimeZone;
|
|
||||||
|
|
||||||
public class ComposeScheduleView extends ConstraintLayout {
|
|
||||||
|
|
||||||
public interface OnTimeSetListener {
|
|
||||||
void onTimeSet(String time);
|
|
||||||
}
|
|
||||||
|
|
||||||
private OnTimeSetListener listener;
|
|
||||||
|
|
||||||
private DateFormat dateFormat;
|
|
||||||
private DateFormat timeFormat;
|
|
||||||
private SimpleDateFormat iso8601;
|
|
||||||
|
|
||||||
private Button resetScheduleButton;
|
|
||||||
private TextView scheduledDateTimeView;
|
|
||||||
private TextView invalidScheduleWarningView;
|
|
||||||
|
|
||||||
private Calendar scheduleDateTime;
|
|
||||||
public static int MINIMUM_SCHEDULED_SECONDS = 330; // Minimum is 5 minutes, pad 30 seconds for posting
|
|
||||||
|
|
||||||
public ComposeScheduleView(Context context) {
|
|
||||||
super(context);
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ComposeScheduleView(Context context, AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ComposeScheduleView(Context context, AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void init() {
|
|
||||||
inflate(getContext(), R.layout.view_compose_schedule, this);
|
|
||||||
|
|
||||||
dateFormat = SimpleDateFormat.getDateInstance();
|
|
||||||
timeFormat = SimpleDateFormat.getTimeInstance();
|
|
||||||
iso8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.getDefault());
|
|
||||||
iso8601.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
||||||
|
|
||||||
resetScheduleButton = findViewById(R.id.resetScheduleButton);
|
|
||||||
scheduledDateTimeView = findViewById(R.id.scheduledDateTime);
|
|
||||||
invalidScheduleWarningView = findViewById(R.id.invalidScheduleWarning);
|
|
||||||
|
|
||||||
scheduledDateTimeView.setOnClickListener(v -> openPickDateDialog());
|
|
||||||
invalidScheduleWarningView.setText(R.string.warning_scheduling_interval);
|
|
||||||
|
|
||||||
scheduleDateTime = null;
|
|
||||||
|
|
||||||
setScheduledDateTime();
|
|
||||||
|
|
||||||
setEditIcons();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setListener(OnTimeSetListener listener) {
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setScheduledDateTime() {
|
|
||||||
if (scheduleDateTime == null) {
|
|
||||||
scheduledDateTimeView.setText("");
|
|
||||||
invalidScheduleWarningView.setVisibility(GONE);
|
|
||||||
} else {
|
|
||||||
Date scheduled = scheduleDateTime.getTime();
|
|
||||||
scheduledDateTimeView.setText(String.format("%s %s",
|
|
||||||
dateFormat.format(scheduled),
|
|
||||||
timeFormat.format(scheduled)));
|
|
||||||
verifyScheduledTime(scheduled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setEditIcons() {
|
|
||||||
Drawable icon = ContextCompat.getDrawable(getContext(), R.drawable.ic_create_24dp);
|
|
||||||
if (icon == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final int size = scheduledDateTimeView.getLineHeight();
|
|
||||||
|
|
||||||
icon.setBounds(0, 0, size, size);
|
|
||||||
|
|
||||||
scheduledDateTimeView.setCompoundDrawables(null, null, icon, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setResetOnClickListener(OnClickListener listener) {
|
|
||||||
resetScheduleButton.setOnClickListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void resetSchedule() {
|
|
||||||
scheduleDateTime = null;
|
|
||||||
setScheduledDateTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void openPickDateDialog() {
|
|
||||||
long yesterday = Calendar.getInstance().getTimeInMillis() - 24 * 60 * 60 * 1000;
|
|
||||||
CalendarConstraints calendarConstraints = new CalendarConstraints.Builder()
|
|
||||||
.setValidator(
|
|
||||||
DateValidatorPointForward.from(yesterday))
|
|
||||||
.build();
|
|
||||||
initializeSuggestedTime();
|
|
||||||
MaterialDatePicker<Long> picker = MaterialDatePicker.Builder
|
|
||||||
.datePicker()
|
|
||||||
.setSelection(scheduleDateTime.getTimeInMillis())
|
|
||||||
.setCalendarConstraints(calendarConstraints)
|
|
||||||
.build();
|
|
||||||
picker.addOnPositiveButtonClickListener(this::onDateSet);
|
|
||||||
picker.show(((AppCompatActivity) getContext()).getSupportFragmentManager(), "date_picker");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void openPickTimeDialog() {
|
|
||||||
MaterialTimePicker.Builder pickerBuilder = new MaterialTimePicker.Builder();
|
|
||||||
if (scheduleDateTime != null) {
|
|
||||||
pickerBuilder.setHour(scheduleDateTime.get(Calendar.HOUR_OF_DAY))
|
|
||||||
.setMinute(scheduleDateTime.get(Calendar.MINUTE));
|
|
||||||
}
|
|
||||||
if (android.text.format.DateFormat.is24HourFormat(this.getContext())) {
|
|
||||||
pickerBuilder.setTimeFormat(TimeFormat.CLOCK_24H);
|
|
||||||
} else {
|
|
||||||
pickerBuilder.setTimeFormat(TimeFormat.CLOCK_12H);
|
|
||||||
}
|
|
||||||
|
|
||||||
MaterialTimePicker picker = pickerBuilder.build();
|
|
||||||
picker.addOnPositiveButtonClickListener(v -> onTimeSet(picker.getHour(), picker.getMinute()));
|
|
||||||
|
|
||||||
picker.show(((AppCompatActivity) getContext()).getSupportFragmentManager(), "time_picker");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Date getDateTime(String scheduledAt) {
|
|
||||||
if (scheduledAt != null) {
|
|
||||||
try {
|
|
||||||
return iso8601.parse(scheduledAt);
|
|
||||||
} catch (ParseException e) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDateTime(String scheduledAt) {
|
|
||||||
Date date;
|
|
||||||
try {
|
|
||||||
date = iso8601.parse(scheduledAt);
|
|
||||||
} catch (ParseException e) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
initializeSuggestedTime();
|
|
||||||
scheduleDateTime.setTime(date);
|
|
||||||
setScheduledDateTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean verifyScheduledTime(@Nullable Date scheduledTime) {
|
|
||||||
boolean valid;
|
|
||||||
if (scheduledTime != null) {
|
|
||||||
Calendar minimumScheduledTime = getCalendar();
|
|
||||||
minimumScheduledTime.add(Calendar.SECOND, MINIMUM_SCHEDULED_SECONDS);
|
|
||||||
valid = scheduledTime.after(minimumScheduledTime.getTime());
|
|
||||||
} else {
|
|
||||||
valid = true;
|
|
||||||
}
|
|
||||||
invalidScheduleWarningView.setVisibility(valid ? GONE : VISIBLE);
|
|
||||||
return valid;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onDateSet(long selection) {
|
|
||||||
initializeSuggestedTime();
|
|
||||||
Calendar newDate = getCalendar();
|
|
||||||
// working around bug in DatePicker where date is UTC #1720
|
|
||||||
// see https://github.com/material-components/material-components-android/issues/882
|
|
||||||
newDate.setTimeZone(TimeZone.getTimeZone("UTC"));
|
|
||||||
newDate.setTimeInMillis(selection);
|
|
||||||
scheduleDateTime.set(newDate.get(Calendar.YEAR), newDate.get(Calendar.MONTH), newDate.get(Calendar.DATE));
|
|
||||||
openPickTimeDialog();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onTimeSet(int hourOfDay, int minute) {
|
|
||||||
initializeSuggestedTime();
|
|
||||||
scheduleDateTime.set(Calendar.HOUR_OF_DAY, hourOfDay);
|
|
||||||
scheduleDateTime.set(Calendar.MINUTE, minute);
|
|
||||||
setScheduledDateTime();
|
|
||||||
if (listener != null) {
|
|
||||||
listener.onTimeSet(getTime());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTime() {
|
|
||||||
if (scheduleDateTime == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return iso8601.format(scheduleDateTime.getTime());
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
public static Calendar getCalendar() {
|
|
||||||
return Calendar.getInstance(TimeZone.getDefault());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeSuggestedTime() {
|
|
||||||
if (scheduleDateTime == null) {
|
|
||||||
scheduleDateTime = getCalendar();
|
|
||||||
scheduleDateTime.add(Calendar.MINUTE, 15);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,210 @@
|
||||||
|
/* Copyright 2019 kyori19
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
package com.keylesspalace.tusky.components.compose.view
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.google.android.material.datepicker.CalendarConstraints
|
||||||
|
import com.google.android.material.datepicker.DateValidatorPointForward
|
||||||
|
import com.google.android.material.datepicker.MaterialDatePicker
|
||||||
|
import com.google.android.material.timepicker.MaterialTimePicker
|
||||||
|
import com.google.android.material.timepicker.TimeFormat
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.databinding.ViewComposeScheduleBinding
|
||||||
|
import java.text.ParseException
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Calendar
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
|
class ComposeScheduleView
|
||||||
|
@JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : ConstraintLayout(context, attrs, defStyleAttr) {
|
||||||
|
interface OnTimeSetListener {
|
||||||
|
fun onTimeSet(time: String?)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var binding = ViewComposeScheduleBinding.inflate(
|
||||||
|
(context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater),
|
||||||
|
this
|
||||||
|
)
|
||||||
|
private var listener: OnTimeSetListener? = null
|
||||||
|
private var dateFormat = SimpleDateFormat.getDateInstance()
|
||||||
|
private var timeFormat = SimpleDateFormat.getTimeInstance()
|
||||||
|
private var iso8601 = SimpleDateFormat(
|
||||||
|
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'",
|
||||||
|
Locale.getDefault()
|
||||||
|
).apply {
|
||||||
|
timeZone = TimeZone.getTimeZone("UTC")
|
||||||
|
}
|
||||||
|
private var scheduleDateTime: Calendar? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
binding.scheduledDateTime.setOnClickListener { openPickDateDialog() }
|
||||||
|
binding.invalidScheduleWarning.setText(R.string.warning_scheduling_interval)
|
||||||
|
updateScheduleUi()
|
||||||
|
setEditIcons()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setListener(listener: OnTimeSetListener?) {
|
||||||
|
this.listener = listener
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateScheduleUi() {
|
||||||
|
if (scheduleDateTime == null) {
|
||||||
|
binding.scheduledDateTime.text = ""
|
||||||
|
binding.invalidScheduleWarning.visibility = GONE
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val scheduled = scheduleDateTime!!.time
|
||||||
|
binding.scheduledDateTime.text = String.format(
|
||||||
|
"%s %s",
|
||||||
|
dateFormat.format(scheduled),
|
||||||
|
timeFormat.format(scheduled)
|
||||||
|
)
|
||||||
|
verifyScheduledTime(scheduled)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setEditIcons() {
|
||||||
|
val icon = ContextCompat.getDrawable(context, R.drawable.ic_create_24dp) ?: return
|
||||||
|
val size = binding.scheduledDateTime.lineHeight
|
||||||
|
icon.setBounds(0, 0, size, size)
|
||||||
|
binding.scheduledDateTime.setCompoundDrawables(null, null, icon, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setResetOnClickListener(listener: OnClickListener?) {
|
||||||
|
binding.resetScheduleButton.setOnClickListener(listener)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resetSchedule() {
|
||||||
|
scheduleDateTime = null
|
||||||
|
updateScheduleUi()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun openPickDateDialog() {
|
||||||
|
val yesterday = Calendar.getInstance().timeInMillis - 24 * 60 * 60 * 1000
|
||||||
|
val calendarConstraints = CalendarConstraints.Builder()
|
||||||
|
.setValidator(
|
||||||
|
DateValidatorPointForward.from(yesterday)
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
initializeSuggestedTime()
|
||||||
|
val picker = MaterialDatePicker.Builder
|
||||||
|
.datePicker()
|
||||||
|
.setSelection(scheduleDateTime!!.timeInMillis)
|
||||||
|
.setCalendarConstraints(calendarConstraints)
|
||||||
|
.build()
|
||||||
|
picker.addOnPositiveButtonClickListener { selection: Long -> onDateSet(selection) }
|
||||||
|
picker.show((context as AppCompatActivity).supportFragmentManager, "date_picker")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getTimeFormat(context: Context): Int {
|
||||||
|
return if (android.text.format.DateFormat.is24HourFormat(context)) {
|
||||||
|
TimeFormat.CLOCK_24H
|
||||||
|
} else {
|
||||||
|
TimeFormat.CLOCK_12H
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun openPickTimeDialog() {
|
||||||
|
val pickerBuilder = MaterialTimePicker.Builder()
|
||||||
|
scheduleDateTime?.let {
|
||||||
|
pickerBuilder.setHour(it[Calendar.HOUR_OF_DAY])
|
||||||
|
.setMinute(it[Calendar.MINUTE])
|
||||||
|
}
|
||||||
|
|
||||||
|
pickerBuilder.setTimeFormat(getTimeFormat(context))
|
||||||
|
|
||||||
|
val picker = pickerBuilder.build()
|
||||||
|
picker.addOnPositiveButtonClickListener { onTimeSet(picker.hour, picker.minute) }
|
||||||
|
picker.show((context as AppCompatActivity).supportFragmentManager, "time_picker")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDateTime(scheduledAt: String?): Date? {
|
||||||
|
scheduledAt?.let {
|
||||||
|
try {
|
||||||
|
return iso8601.parse(it)
|
||||||
|
} catch (_: ParseException) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDateTime(scheduledAt: String?) {
|
||||||
|
val date = getDateTime(scheduledAt) ?: return
|
||||||
|
initializeSuggestedTime()
|
||||||
|
scheduleDateTime!!.time = date
|
||||||
|
updateScheduleUi()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun verifyScheduledTime(scheduledTime: Date?): Boolean {
|
||||||
|
val valid: Boolean = if (scheduledTime != null) {
|
||||||
|
val minimumScheduledTime = calendar()
|
||||||
|
minimumScheduledTime.add(
|
||||||
|
Calendar.SECOND,
|
||||||
|
MINIMUM_SCHEDULED_SECONDS
|
||||||
|
)
|
||||||
|
scheduledTime.after(minimumScheduledTime.time)
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
binding.invalidScheduleWarning.visibility = if (valid) GONE else VISIBLE
|
||||||
|
return valid
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onDateSet(selection: Long) {
|
||||||
|
initializeSuggestedTime()
|
||||||
|
val newDate = calendar()
|
||||||
|
// working around bug in DatePicker where date is UTC #1720
|
||||||
|
// see https://github.com/material-components/material-components-android/issues/882
|
||||||
|
newDate.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
|
newDate.timeInMillis = selection
|
||||||
|
scheduleDateTime!![newDate[Calendar.YEAR], newDate[Calendar.MONTH]] = newDate[Calendar.DATE]
|
||||||
|
openPickTimeDialog()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onTimeSet(hourOfDay: Int, minute: Int) {
|
||||||
|
initializeSuggestedTime()
|
||||||
|
scheduleDateTime?.set(Calendar.HOUR_OF_DAY, hourOfDay)
|
||||||
|
scheduleDateTime?.set(Calendar.MINUTE, minute)
|
||||||
|
updateScheduleUi()
|
||||||
|
listener?.onTimeSet(time)
|
||||||
|
}
|
||||||
|
|
||||||
|
val time: String?
|
||||||
|
get() = scheduleDateTime?.time?.let { iso8601.format(it) }
|
||||||
|
|
||||||
|
private fun initializeSuggestedTime() {
|
||||||
|
if (scheduleDateTime == null) {
|
||||||
|
scheduleDateTime = calendar().apply {
|
||||||
|
add(Calendar.MINUTE, 15)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
var MINIMUM_SCHEDULED_SECONDS = 330 // Minimum is 5 minutes, pad 30 seconds for posting
|
||||||
|
fun calendar(): Calendar = Calendar.getInstance(TimeZone.getDefault())
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,121 +0,0 @@
|
||||||
/* Copyright 2017 Andrew Dawson
|
|
||||||
*
|
|
||||||
* This file is a part of Tusky.
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
|
||||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
||||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
||||||
* Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
|
||||||
* see <http://www.gnu.org/licenses>. */
|
|
||||||
|
|
||||||
package com.keylesspalace.tusky.components.compose.view;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.graphics.Paint;
|
|
||||||
import android.graphics.PorterDuff;
|
|
||||||
import android.graphics.PorterDuffXfermode;
|
|
||||||
import android.graphics.RectF;
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import androidx.annotation.Nullable;
|
|
||||||
import androidx.core.content.ContextCompat;
|
|
||||||
import androidx.appcompat.content.res.AppCompatResources;
|
|
||||||
import androidx.appcompat.widget.AppCompatImageView;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
|
||||||
import com.keylesspalace.tusky.view.MediaPreviewImageView;
|
|
||||||
import at.connyduck.sparkbutton.helpers.Utils;
|
|
||||||
|
|
||||||
public final class ProgressImageView extends MediaPreviewImageView {
|
|
||||||
|
|
||||||
private int progress = -1;
|
|
||||||
private final RectF progressRect = new RectF();
|
|
||||||
private final RectF biggerRect = new RectF();
|
|
||||||
private final Paint circlePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
||||||
private final Paint clearPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
||||||
private final Paint markBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
|
||||||
private Drawable captionDrawable;
|
|
||||||
|
|
||||||
public ProgressImageView(Context context) {
|
|
||||||
super(context);
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProgressImageView(Context context, @Nullable AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
public ProgressImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
|
||||||
super(context, attrs, defStyleAttr);
|
|
||||||
init();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void init() {
|
|
||||||
circlePaint.setColor(getContext().getColor(R.color.tusky_blue));
|
|
||||||
circlePaint.setStrokeWidth(Utils.dpToPx(getContext(), 4));
|
|
||||||
circlePaint.setStyle(Paint.Style.STROKE);
|
|
||||||
|
|
||||||
clearPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
|
|
||||||
|
|
||||||
markBgPaint.setStyle(Paint.Style.FILL);
|
|
||||||
markBgPaint.setColor(getContext().getColor(R.color.tusky_grey_10));
|
|
||||||
captionDrawable = AppCompatResources.getDrawable(getContext(), R.drawable.spellcheck);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setProgress(int progress) {
|
|
||||||
this.progress = progress;
|
|
||||||
if (progress != -1) {
|
|
||||||
setColorFilter(Color.rgb(123, 123, 123), PorterDuff.Mode.MULTIPLY);
|
|
||||||
} else {
|
|
||||||
clearColorFilter();
|
|
||||||
}
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setChecked(boolean checked) {
|
|
||||||
this.markBgPaint.setColor(getContext().getColor(checked ? R.color.tusky_blue : R.color.tusky_grey_10));
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDraw(Canvas canvas) {
|
|
||||||
super.onDraw(canvas);
|
|
||||||
|
|
||||||
float angle = (progress / 100f) * 360 - 90;
|
|
||||||
float halfWidth = getWidth() / 2;
|
|
||||||
float halfHeight = getHeight() / 2;
|
|
||||||
progressRect.set(halfWidth * 0.75f, halfHeight * 0.75f, halfWidth * 1.25f, halfHeight * 1.25f);
|
|
||||||
biggerRect.set(progressRect);
|
|
||||||
int margin = 8;
|
|
||||||
biggerRect.set(progressRect.left - margin, progressRect.top - margin, progressRect.right + margin, progressRect.bottom + margin);
|
|
||||||
canvas.saveLayer(biggerRect, null, Canvas.ALL_SAVE_FLAG);
|
|
||||||
if (progress != -1) {
|
|
||||||
canvas.drawOval(progressRect, circlePaint);
|
|
||||||
canvas.drawArc(biggerRect, angle, 360 - angle - 90, true, clearPaint);
|
|
||||||
}
|
|
||||||
canvas.restore();
|
|
||||||
|
|
||||||
int circleRadius = Utils.dpToPx(getContext(), 14);
|
|
||||||
int circleMargin = Utils.dpToPx(getContext(), 14);
|
|
||||||
|
|
||||||
int circleY = getHeight() - circleMargin - circleRadius / 2;
|
|
||||||
int circleX = getWidth() - circleMargin - circleRadius / 2;
|
|
||||||
|
|
||||||
canvas.drawCircle(circleX, circleY, circleRadius, markBgPaint);
|
|
||||||
|
|
||||||
captionDrawable.setBounds(getWidth() - circleMargin - circleRadius,
|
|
||||||
getHeight() - circleMargin - circleRadius,
|
|
||||||
getWidth() - circleMargin,
|
|
||||||
getHeight() - circleMargin);
|
|
||||||
captionDrawable.setTint(Color.WHITE);
|
|
||||||
captionDrawable.draw(canvas);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
/* Copyright 2017 Andrew Dawson
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
package com.keylesspalace.tusky.components.compose.view
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.PorterDuff
|
||||||
|
import android.graphics.PorterDuffXfermode
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import androidx.appcompat.content.res.AppCompatResources
|
||||||
|
import at.connyduck.sparkbutton.helpers.Utils
|
||||||
|
import com.keylesspalace.tusky.R
|
||||||
|
import com.keylesspalace.tusky.view.MediaPreviewImageView
|
||||||
|
|
||||||
|
class ProgressImageView
|
||||||
|
@JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyleAttr: Int = 0
|
||||||
|
) : MediaPreviewImageView(context, attrs, defStyleAttr) {
|
||||||
|
private var progress = -1
|
||||||
|
private val progressRect = RectF()
|
||||||
|
private val biggerRect = RectF()
|
||||||
|
private val circlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
color = context.getColor(R.color.tusky_blue)
|
||||||
|
strokeWidth = Utils.dpToPx(context, 4).toFloat()
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
}
|
||||||
|
private val clearPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT)
|
||||||
|
}
|
||||||
|
private val markBgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
color = context.getColor(R.color.tusky_grey_10)
|
||||||
|
}
|
||||||
|
private val captionDrawable = AppCompatResources.getDrawable(
|
||||||
|
context,
|
||||||
|
R.drawable.spellcheck
|
||||||
|
)!!.apply {
|
||||||
|
setTint(Color.WHITE)
|
||||||
|
}
|
||||||
|
private val circleRadius = Utils.dpToPx(context, 14)
|
||||||
|
private val circleMargin = Utils.dpToPx(context, 14)
|
||||||
|
|
||||||
|
fun setProgress(progress: Int) {
|
||||||
|
this.progress = progress
|
||||||
|
if (progress != -1) {
|
||||||
|
setColorFilter(Color.rgb(123, 123, 123), PorterDuff.Mode.MULTIPLY)
|
||||||
|
} else {
|
||||||
|
clearColorFilter()
|
||||||
|
}
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setChecked(checked: Boolean) {
|
||||||
|
markBgPaint.color =
|
||||||
|
context.getColor(if (checked) R.color.tusky_blue else R.color.tusky_grey_10)
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDraw(canvas: Canvas) {
|
||||||
|
super.onDraw(canvas)
|
||||||
|
val angle = progress / 100f * 360 - 90
|
||||||
|
val halfWidth = width / 2f
|
||||||
|
val halfHeight = height / 2f
|
||||||
|
progressRect[halfWidth * 0.75f, halfHeight * 0.75f, halfWidth * 1.25f] = halfHeight * 1.25f
|
||||||
|
biggerRect.set(progressRect)
|
||||||
|
val margin = 8
|
||||||
|
biggerRect[progressRect.left - margin, progressRect.top - margin, progressRect.right + margin] =
|
||||||
|
progressRect.bottom + margin
|
||||||
|
canvas.saveLayer(biggerRect, null)
|
||||||
|
if (progress != -1) {
|
||||||
|
canvas.drawOval(progressRect, circlePaint)
|
||||||
|
canvas.drawArc(biggerRect, angle, 360 - angle - 90, true, clearPaint)
|
||||||
|
}
|
||||||
|
canvas.restore()
|
||||||
|
val circleY = height - circleMargin - circleRadius / 2
|
||||||
|
val circleX = width - circleMargin - circleRadius / 2
|
||||||
|
canvas.drawCircle(circleX.toFloat(), circleY.toFloat(), circleRadius.toFloat(), markBgPaint)
|
||||||
|
captionDrawable.setBounds(
|
||||||
|
width - circleMargin - circleRadius,
|
||||||
|
height - circleMargin - circleRadius,
|
||||||
|
width - circleMargin,
|
||||||
|
height - circleMargin
|
||||||
|
)
|
||||||
|
captionDrawable.draw(canvas)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,61 +0,0 @@
|
||||||
/* Copyright 2019 Tusky contributors
|
|
||||||
*
|
|
||||||
* This file is a part of Tusky.
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
|
||||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
||||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
||||||
* Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
|
||||||
* see <http://www.gnu.org/licenses>. */
|
|
||||||
|
|
||||||
package com.keylesspalace.tusky.view;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Outline;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewOutlineProvider;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* override BezelImageView from MaterialDrawer library to provide custom outline
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class BezelImageView extends com.mikepenz.materialdrawer.view.BezelImageView {
|
|
||||||
public BezelImageView(Context context) {
|
|
||||||
this(context, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BezelImageView(Context context, AttributeSet attrs) {
|
|
||||||
this(context, attrs, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BezelImageView(Context context, AttributeSet attrs, int defStyle) {
|
|
||||||
super(context, attrs, defStyle);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSizeChanged(int w, int h, int old_w, int old_h) {
|
|
||||||
setOutlineProvider(new CustomOutline(w, h));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class CustomOutline extends ViewOutlineProvider {
|
|
||||||
|
|
||||||
int width;
|
|
||||||
int height;
|
|
||||||
|
|
||||||
CustomOutline(int width, int height) {
|
|
||||||
this.width = width;
|
|
||||||
this.height = height;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void getOutline(View view, Outline outline) {
|
|
||||||
outline.setRoundRect(0, 0, width, height, width < height ? width / 8f : height / 8f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
/* Copyright 2019 Tusky contributors
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
package com.keylesspalace.tusky.view
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Outline
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewOutlineProvider
|
||||||
|
import com.mikepenz.materialdrawer.view.BezelImageView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* override BezelImageView from MaterialDrawer library to provide custom outline
|
||||||
|
*/
|
||||||
|
class BezelImageView @JvmOverloads constructor(
|
||||||
|
context: Context,
|
||||||
|
attrs: AttributeSet? = null,
|
||||||
|
defStyle: Int = 0
|
||||||
|
) : BezelImageView(context, attrs, defStyle) {
|
||||||
|
override fun onSizeChanged(w: Int, h: Int, old_w: Int, old_h: Int) {
|
||||||
|
outlineProvider = CustomOutline(w, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CustomOutline(var width: Int, var height: Int) :
|
||||||
|
ViewOutlineProvider() {
|
||||||
|
override fun getOutline(view: View, outline: Outline) {
|
||||||
|
outline.setRoundRect(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
if (width < height) width / 8f else height / 8f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,54 +0,0 @@
|
||||||
/* Copyright 2017 Andrew Dawson
|
|
||||||
*
|
|
||||||
* This file is a part of Tusky.
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
|
||||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
||||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
||||||
* Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
|
||||||
* see <http://www.gnu.org/licenses>. */
|
|
||||||
|
|
||||||
package com.keylesspalace.tusky.view;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
public abstract class EndlessOnScrollListener extends RecyclerView.OnScrollListener {
|
|
||||||
private static final int VISIBLE_THRESHOLD = 15;
|
|
||||||
private int previousTotalItemCount;
|
|
||||||
private LinearLayoutManager layoutManager;
|
|
||||||
|
|
||||||
public EndlessOnScrollListener(LinearLayoutManager layoutManager) {
|
|
||||||
this.layoutManager = layoutManager;
|
|
||||||
previousTotalItemCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onScrolled(@NonNull RecyclerView view, int dx, int dy) {
|
|
||||||
int totalItemCount = layoutManager.getItemCount();
|
|
||||||
int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition();
|
|
||||||
if (totalItemCount < previousTotalItemCount) {
|
|
||||||
previousTotalItemCount = totalItemCount;
|
|
||||||
|
|
||||||
}
|
|
||||||
if (totalItemCount != previousTotalItemCount) {
|
|
||||||
previousTotalItemCount = totalItemCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastVisibleItemPosition + VISIBLE_THRESHOLD > totalItemCount) {
|
|
||||||
onLoadMore(totalItemCount, view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void reset() {
|
|
||||||
previousTotalItemCount = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract void onLoadMore(int totalItemsCount, RecyclerView view);
|
|
||||||
}
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
/* Copyright 2017 Andrew Dawson
|
||||||
|
*
|
||||||
|
* This file is a part of Tusky.
|
||||||
|
*
|
||||||
|
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||||
|
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
||||||
|
* see <http://www.gnu.org/licenses>. */
|
||||||
|
package com.keylesspalace.tusky.view
|
||||||
|
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
|
||||||
|
abstract class EndlessOnScrollListener(private val layoutManager: LinearLayoutManager) :
|
||||||
|
RecyclerView.OnScrollListener() {
|
||||||
|
private var previousTotalItemCount = 0
|
||||||
|
|
||||||
|
override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) {
|
||||||
|
val totalItemCount = layoutManager.itemCount
|
||||||
|
val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition()
|
||||||
|
|
||||||
|
if (totalItemCount < previousTotalItemCount) {
|
||||||
|
previousTotalItemCount = totalItemCount
|
||||||
|
}
|
||||||
|
if (totalItemCount != previousTotalItemCount) {
|
||||||
|
previousTotalItemCount = totalItemCount
|
||||||
|
}
|
||||||
|
if (lastVisibleItemPosition + VISIBLE_THRESHOLD > totalItemCount) {
|
||||||
|
onLoadMore(totalItemCount, view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
previousTotalItemCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract fun onLoadMore(totalItemsCount: Int, view: RecyclerView)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val VISIBLE_THRESHOLD = 15
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue