mirror of https://github.com/tuskyapp/Tusky.git
Browse Source
* Scheduled toot * Hide scheduled toot button if version < 2.7.0 * Fix timeline reloading after toot * Add edit icon to ComposeScheduleView * Add button to reset scheduled toot * Close bottom sheet and change button color after time a was selected * Fix edit icon's size * List of scheduled toots * Fix instance version check * Use MaterialDatePicker * Set date and time consecutively * Add licensespull/1513/head
23 changed files with 933 additions and 56 deletions
@ -0,0 +1,166 @@ |
|||||||
|
package com.keylesspalace.tusky |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
import android.content.Intent |
||||||
|
import android.os.Bundle |
||||||
|
import android.view.View |
||||||
|
import androidx.appcompat.widget.Toolbar |
||||||
|
import androidx.lifecycle.Lifecycle |
||||||
|
import androidx.recyclerview.widget.DividerItemDecoration |
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager |
||||||
|
import com.keylesspalace.tusky.adapter.ScheduledTootAdapter |
||||||
|
import com.keylesspalace.tusky.appstore.EventHub |
||||||
|
import com.keylesspalace.tusky.appstore.StatusScheduledEvent |
||||||
|
import com.keylesspalace.tusky.di.Injectable |
||||||
|
import com.keylesspalace.tusky.entity.ScheduledStatus |
||||||
|
import com.keylesspalace.tusky.network.MastodonApi |
||||||
|
import com.keylesspalace.tusky.util.hide |
||||||
|
import com.keylesspalace.tusky.util.show |
||||||
|
import com.uber.autodispose.AutoDispose.autoDisposable |
||||||
|
import com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from |
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers |
||||||
|
import kotlinx.android.synthetic.main.activity_scheduled_toot.* |
||||||
|
import okhttp3.ResponseBody |
||||||
|
import retrofit2.Call |
||||||
|
import retrofit2.Callback |
||||||
|
import retrofit2.Response |
||||||
|
import javax.inject.Inject |
||||||
|
|
||||||
|
|
||||||
|
class ScheduledTootActivity : BaseActivity(), ScheduledTootAdapter.ScheduledTootAction, Injectable { |
||||||
|
|
||||||
|
companion object { |
||||||
|
@JvmStatic |
||||||
|
fun newIntent(context: Context): Intent { |
||||||
|
return Intent(context, ScheduledTootActivity::class.java) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
lateinit var adapter: ScheduledTootAdapter |
||||||
|
|
||||||
|
@Inject |
||||||
|
lateinit var mastodonApi: MastodonApi |
||||||
|
@Inject |
||||||
|
lateinit var eventHub: EventHub |
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) { |
||||||
|
super.onCreate(savedInstanceState) |
||||||
|
setContentView(R.layout.activity_scheduled_toot) |
||||||
|
|
||||||
|
val toolbar = findViewById<Toolbar>(R.id.toolbar) |
||||||
|
|
||||||
|
setSupportActionBar(toolbar) |
||||||
|
val bar = supportActionBar |
||||||
|
if (bar != null) { |
||||||
|
bar.title = getString(R.string.title_scheduled_toot) |
||||||
|
bar.setDisplayHomeAsUpEnabled(true) |
||||||
|
bar.setDisplayShowHomeEnabled(true) |
||||||
|
} |
||||||
|
|
||||||
|
swipe_refresh_layout.setOnRefreshListener(this::refreshStatuses) |
||||||
|
|
||||||
|
scheduled_toot_list.setHasFixedSize(true) |
||||||
|
val layoutManager = LinearLayoutManager(this) |
||||||
|
scheduled_toot_list.layoutManager = layoutManager |
||||||
|
val divider = DividerItemDecoration(this, layoutManager.orientation) |
||||||
|
scheduled_toot_list.addItemDecoration(divider) |
||||||
|
adapter = ScheduledTootAdapter(this) |
||||||
|
scheduled_toot_list.adapter = adapter |
||||||
|
|
||||||
|
loadStatuses() |
||||||
|
|
||||||
|
eventHub.events |
||||||
|
.observeOn(AndroidSchedulers.mainThread()) |
||||||
|
.`as`(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY))) |
||||||
|
.subscribe { event -> |
||||||
|
if (event is StatusScheduledEvent) { |
||||||
|
refreshStatuses() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun loadStatuses() { |
||||||
|
progress_bar.visibility = View.VISIBLE |
||||||
|
mastodonApi.scheduledStatuses() |
||||||
|
.enqueue(object : Callback<List<ScheduledStatus>> { |
||||||
|
override fun onResponse(call: Call<List<ScheduledStatus>>, response: Response<List<ScheduledStatus>>) { |
||||||
|
progress_bar.visibility = View.GONE |
||||||
|
if (response.body().isNullOrEmpty()) { |
||||||
|
errorMessageView.show() |
||||||
|
errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, |
||||||
|
null) |
||||||
|
} else { |
||||||
|
show(response.body()!!) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override fun onFailure(call: Call<List<ScheduledStatus>>, t: Throwable) { |
||||||
|
progress_bar.visibility = View.GONE |
||||||
|
errorMessageView.show() |
||||||
|
errorMessageView.setup(R.drawable.elephant_error, R.string.error_generic) { |
||||||
|
errorMessageView.hide() |
||||||
|
loadStatuses() |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
private fun refreshStatuses() { |
||||||
|
swipe_refresh_layout.isRefreshing = true |
||||||
|
mastodonApi.scheduledStatuses() |
||||||
|
.enqueue(object : Callback<List<ScheduledStatus>> { |
||||||
|
override fun onResponse(call: Call<List<ScheduledStatus>>, response: Response<List<ScheduledStatus>>) { |
||||||
|
swipe_refresh_layout.isRefreshing = false |
||||||
|
if (response.body().isNullOrEmpty()) { |
||||||
|
errorMessageView.show() |
||||||
|
errorMessageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty, |
||||||
|
null) |
||||||
|
} else { |
||||||
|
show(response.body()!!) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override fun onFailure(call: Call<List<ScheduledStatus>>, t: Throwable) { |
||||||
|
swipe_refresh_layout.isRefreshing = false |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
fun show(statuses: List<ScheduledStatus>) { |
||||||
|
adapter.setItems(statuses) |
||||||
|
adapter.notifyDataSetChanged() |
||||||
|
} |
||||||
|
|
||||||
|
override fun edit(position: Int, item: ScheduledStatus?) { |
||||||
|
if (item == null) { |
||||||
|
return |
||||||
|
} |
||||||
|
val intent = ComposeActivity.IntentBuilder() |
||||||
|
.tootText(item.params.text) |
||||||
|
.contentWarning(item.params.spoilerText) |
||||||
|
.mediaAttachments(item.mediaAttachments) |
||||||
|
.inReplyToId(item.params.inReplyToId) |
||||||
|
.visibility(item.params.visibility) |
||||||
|
.scheduledAt(item.scheduledAt) |
||||||
|
.sensitive(item.params.sensitive) |
||||||
|
.build(this) |
||||||
|
startActivity(intent) |
||||||
|
delete(position, item) |
||||||
|
} |
||||||
|
|
||||||
|
override fun delete(position: Int, item: ScheduledStatus?) { |
||||||
|
if (item == null) { |
||||||
|
return |
||||||
|
} |
||||||
|
mastodonApi.deleteScheduledStatus(item.id) |
||||||
|
.enqueue(object : Callback<ResponseBody> { |
||||||
|
override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) { |
||||||
|
adapter.removeItem(position) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onFailure(call: Call<ResponseBody>, t: Throwable) { |
||||||
|
|
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,125 @@ |
|||||||
|
/* 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.adapter; |
||||||
|
|
||||||
|
import android.content.Context; |
||||||
|
import android.view.LayoutInflater; |
||||||
|
import android.view.View; |
||||||
|
import android.view.ViewGroup; |
||||||
|
import android.widget.ImageButton; |
||||||
|
import android.widget.TextView; |
||||||
|
|
||||||
|
import androidx.annotation.NonNull; |
||||||
|
import androidx.annotation.Nullable; |
||||||
|
import androidx.recyclerview.widget.RecyclerView; |
||||||
|
|
||||||
|
import com.keylesspalace.tusky.R; |
||||||
|
import com.keylesspalace.tusky.entity.ScheduledStatus; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
public class ScheduledTootAdapter extends RecyclerView.Adapter { |
||||||
|
private List<ScheduledStatus> list; |
||||||
|
private ScheduledTootAction handler; |
||||||
|
|
||||||
|
public ScheduledTootAdapter(Context context) { |
||||||
|
super(); |
||||||
|
list = new ArrayList<>(); |
||||||
|
handler = (ScheduledTootAction) context; |
||||||
|
} |
||||||
|
|
||||||
|
@NonNull |
||||||
|
@Override |
||||||
|
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { |
||||||
|
View view = LayoutInflater.from(parent.getContext()) |
||||||
|
.inflate(R.layout.item_scheduled_toot, parent, false); |
||||||
|
return new TootViewHolder(view); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) { |
||||||
|
TootViewHolder holder = (TootViewHolder) viewHolder; |
||||||
|
holder.bind(getItem(position)); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int getItemCount() { |
||||||
|
return list.size(); |
||||||
|
} |
||||||
|
|
||||||
|
public void setItems(List<ScheduledStatus> newToot) { |
||||||
|
list = new ArrayList<>(); |
||||||
|
list.addAll(newToot); |
||||||
|
} |
||||||
|
|
||||||
|
@Nullable |
||||||
|
public ScheduledStatus removeItem(int position) { |
||||||
|
if (position < 0 || position >= list.size()) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
ScheduledStatus toot = list.remove(position); |
||||||
|
notifyItemRemoved(position); |
||||||
|
return toot; |
||||||
|
} |
||||||
|
|
||||||
|
private ScheduledStatus getItem(int position) { |
||||||
|
if (position >= 0 && position < list.size()) { |
||||||
|
return list.get(position); |
||||||
|
} |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
public interface ScheduledTootAction { |
||||||
|
void edit(int position, ScheduledStatus item); |
||||||
|
|
||||||
|
void delete(int position, ScheduledStatus item); |
||||||
|
} |
||||||
|
|
||||||
|
private class TootViewHolder extends RecyclerView.ViewHolder { |
||||||
|
View view; |
||||||
|
TextView text; |
||||||
|
ImageButton edit; |
||||||
|
ImageButton delete; |
||||||
|
|
||||||
|
TootViewHolder(View view) { |
||||||
|
super(view); |
||||||
|
this.view = view; |
||||||
|
this.text = view.findViewById(R.id.text); |
||||||
|
this.edit = view.findViewById(R.id.edit); |
||||||
|
this.delete = view.findViewById(R.id.delete); |
||||||
|
} |
||||||
|
|
||||||
|
void bind(final ScheduledStatus item) { |
||||||
|
edit.setEnabled(true); |
||||||
|
delete.setEnabled(true); |
||||||
|
|
||||||
|
if (item != null) { |
||||||
|
text.setText(item.getParams().getText()); |
||||||
|
|
||||||
|
edit.setOnClickListener(v -> { |
||||||
|
v.setEnabled(false); |
||||||
|
handler.edit(getAdapterPosition(), item); |
||||||
|
}); |
||||||
|
|
||||||
|
delete.setOnClickListener(v -> { |
||||||
|
v.setEnabled(false); |
||||||
|
handler.delete(getAdapterPosition(), item); |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,25 @@ |
|||||||
|
/* 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.entity |
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName |
||||||
|
|
||||||
|
data class ScheduledStatus( |
||||||
|
val id: String, |
||||||
|
@SerializedName("scheduled_at") val scheduledAt: String, |
||||||
|
val params: StatusParams, |
||||||
|
@SerializedName("media_attachments") val mediaAttachments: ArrayList<Attachment> |
||||||
|
) |
||||||
@ -0,0 +1,26 @@ |
|||||||
|
/* 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.entity |
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName |
||||||
|
|
||||||
|
data class StatusParams( |
||||||
|
val text: String, |
||||||
|
val sensitive: Boolean, |
||||||
|
val visibility: Status.Visibility, |
||||||
|
@SerializedName("spoiler_text") val spoilerText: String, |
||||||
|
@SerializedName("in_reply_to_id") val inReplyToId: String? |
||||||
|
) |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
/* 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.fragment; |
||||||
|
|
||||||
|
import android.app.Dialog; |
||||||
|
import android.app.TimePickerDialog; |
||||||
|
import android.os.Bundle; |
||||||
|
|
||||||
|
import androidx.annotation.NonNull; |
||||||
|
import androidx.fragment.app.DialogFragment; |
||||||
|
|
||||||
|
import com.keylesspalace.tusky.ComposeActivity; |
||||||
|
|
||||||
|
import java.util.Calendar; |
||||||
|
import java.util.TimeZone; |
||||||
|
|
||||||
|
public class TimePickerFragment extends DialogFragment { |
||||||
|
|
||||||
|
public static final String PICKER_TIME_HOUR = "picker_time_hour"; |
||||||
|
public static final String PICKER_TIME_MINUTE = "picker_time_minute"; |
||||||
|
|
||||||
|
@Override |
||||||
|
@NonNull |
||||||
|
public Dialog onCreateDialog(Bundle savedInstanceState) { |
||||||
|
Bundle args = getArguments(); |
||||||
|
Calendar calendar = Calendar.getInstance(TimeZone.getDefault()); |
||||||
|
if (args != null) { |
||||||
|
calendar.set(Calendar.HOUR_OF_DAY, args.getInt(PICKER_TIME_HOUR)); |
||||||
|
calendar.set(Calendar.MINUTE, args.getInt(PICKER_TIME_MINUTE)); |
||||||
|
} |
||||||
|
|
||||||
|
return new TimePickerDialog(getContext(), |
||||||
|
android.R.style.Theme_DeviceDefault_Dialog, |
||||||
|
(ComposeActivity) getActivity(), |
||||||
|
calendar.get(Calendar.HOUR_OF_DAY), |
||||||
|
calendar.get(Calendar.MINUTE), |
||||||
|
true); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,42 @@ |
|||||||
|
/* 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.util; |
||||||
|
|
||||||
|
import java.util.regex.Matcher; |
||||||
|
import java.util.regex.Pattern; |
||||||
|
|
||||||
|
public class VersionUtils { |
||||||
|
|
||||||
|
private int major; |
||||||
|
private int minor; |
||||||
|
private int patch; |
||||||
|
|
||||||
|
public VersionUtils(String versionString) { |
||||||
|
String regex = "([0-9]+)\\.([0-9]+)\\.([0-9]+).*"; |
||||||
|
Pattern pattern = Pattern.compile(regex); |
||||||
|
Matcher matcher = pattern.matcher(versionString); |
||||||
|
if (matcher.find()) { |
||||||
|
major = Integer.parseInt(matcher.group(1)); |
||||||
|
minor = Integer.parseInt(matcher.group(2)); |
||||||
|
patch = Integer.parseInt(matcher.group(3)); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public boolean supportsScheduledToots() { |
||||||
|
return (major == 2) ? ( (minor == 7) ? (patch >= 0) : (minor > 7) ) : (major > 2); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,187 @@ |
|||||||
|
/* 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.view; |
||||||
|
|
||||||
|
import android.content.Context; |
||||||
|
import android.graphics.drawable.Drawable; |
||||||
|
import android.os.Bundle; |
||||||
|
import android.util.AttributeSet; |
||||||
|
import android.widget.Button; |
||||||
|
import android.widget.TextView; |
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity; |
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout; |
||||||
|
|
||||||
|
import com.google.android.material.datepicker.CalendarConstraints; |
||||||
|
import com.google.android.material.datepicker.DateValidatorPointForward; |
||||||
|
import com.google.android.material.datepicker.MaterialDatePicker; |
||||||
|
import com.keylesspalace.tusky.R; |
||||||
|
import com.keylesspalace.tusky.fragment.TimePickerFragment; |
||||||
|
|
||||||
|
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 { |
||||||
|
|
||||||
|
private DateFormat dateFormat; |
||||||
|
private DateFormat timeFormat; |
||||||
|
private SimpleDateFormat iso8601; |
||||||
|
|
||||||
|
private Button resetScheduleButton; |
||||||
|
private TextView scheduledDateTimeView; |
||||||
|
|
||||||
|
private Calendar scheduleDateTime; |
||||||
|
|
||||||
|
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); |
||||||
|
|
||||||
|
scheduledDateTimeView.setOnClickListener(v -> openPickDateDialog()); |
||||||
|
|
||||||
|
scheduleDateTime = null; |
||||||
|
|
||||||
|
setScheduledDateTime(); |
||||||
|
|
||||||
|
setEditIcons(); |
||||||
|
} |
||||||
|
|
||||||
|
private void setScheduledDateTime() { |
||||||
|
if (scheduleDateTime == null) { |
||||||
|
scheduledDateTimeView.setText(R.string.hint_configure_scheduled_toot); |
||||||
|
} else { |
||||||
|
scheduledDateTimeView.setText(String.format("%s %s", |
||||||
|
dateFormat.format(scheduleDateTime.getTime()), |
||||||
|
timeFormat.format(scheduleDateTime.getTime()))); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void setEditIcons() { |
||||||
|
final int size = scheduledDateTimeView.getLineHeight(); |
||||||
|
|
||||||
|
Drawable icon = getContext().getDrawable(R.drawable.ic_create_24dp); |
||||||
|
if (icon == null) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
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(); |
||||||
|
} |
||||||
|
|
||||||
|
private void openPickDateDialog() { |
||||||
|
long yesterday = Calendar.getInstance().getTimeInMillis() - 24 * 60 * 60 * 1000; |
||||||
|
CalendarConstraints calendarConstraints = new CalendarConstraints.Builder() |
||||||
|
.setValidator(new DateValidatorPointForward(yesterday)) |
||||||
|
.build(); |
||||||
|
if (scheduleDateTime == null) { |
||||||
|
scheduleDateTime = Calendar.getInstance(TimeZone.getDefault()); |
||||||
|
} |
||||||
|
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() { |
||||||
|
TimePickerFragment picker = new TimePickerFragment(); |
||||||
|
if (scheduleDateTime != null) { |
||||||
|
Bundle args = new Bundle(); |
||||||
|
args.putInt(TimePickerFragment.PICKER_TIME_HOUR, scheduleDateTime.get(Calendar.HOUR_OF_DAY)); |
||||||
|
args.putInt(TimePickerFragment.PICKER_TIME_MINUTE, scheduleDateTime.get(Calendar.MINUTE)); |
||||||
|
picker.setArguments(args); |
||||||
|
} |
||||||
|
picker.show(((AppCompatActivity) getContext()).getSupportFragmentManager(), "time_picker"); |
||||||
|
} |
||||||
|
|
||||||
|
public void setDateTime(String scheduledAt) { |
||||||
|
Date date; |
||||||
|
try { |
||||||
|
date = iso8601.parse(scheduledAt); |
||||||
|
} catch (ParseException e) { |
||||||
|
return; |
||||||
|
} |
||||||
|
if (scheduleDateTime == null) { |
||||||
|
scheduleDateTime = Calendar.getInstance(TimeZone.getDefault()); |
||||||
|
} |
||||||
|
scheduleDateTime.setTime(date); |
||||||
|
setScheduledDateTime(); |
||||||
|
} |
||||||
|
|
||||||
|
private void onDateSet(long selection) { |
||||||
|
if (scheduleDateTime == null) { |
||||||
|
scheduleDateTime = Calendar.getInstance(TimeZone.getDefault()); |
||||||
|
} |
||||||
|
Calendar newDate = Calendar.getInstance(TimeZone.getDefault()); |
||||||
|
newDate.setTimeInMillis(selection); |
||||||
|
scheduleDateTime.set(newDate.get(Calendar.YEAR), newDate.get(Calendar.MONTH), newDate.get(Calendar.DATE)); |
||||||
|
openPickTimeDialog(); |
||||||
|
} |
||||||
|
|
||||||
|
public void onTimeSet(int hourOfDay, int minute) { |
||||||
|
if (scheduleDateTime == null) { |
||||||
|
scheduleDateTime = Calendar.getInstance(TimeZone.getDefault()); |
||||||
|
} |
||||||
|
scheduleDateTime.set(Calendar.HOUR_OF_DAY, hourOfDay); |
||||||
|
scheduleDateTime.set(Calendar.MINUTE, minute); |
||||||
|
setScheduledDateTime(); |
||||||
|
} |
||||||
|
|
||||||
|
public String getTime() { |
||||||
|
if (scheduleDateTime == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return iso8601.format(scheduleDateTime.getTime()); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,9 @@ |
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
android:width="24dp" |
||||||
|
android:height="24dp" |
||||||
|
android:viewportWidth="24.0" |
||||||
|
android:viewportHeight="24.0"> |
||||||
|
<path |
||||||
|
android:fillColor="#FF000000" |
||||||
|
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/> |
||||||
|
</vector> |
||||||
@ -0,0 +1,53 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||||
|
xmlns:tools="http://schemas.android.com/tools" |
||||||
|
android:id="@+id/activity_view_thread" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="match_parent" |
||||||
|
tools:context="com.keylesspalace.tusky.AccountListActivity"> |
||||||
|
|
||||||
|
<include layout="@layout/toolbar_basic" /> |
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="match_parent" |
||||||
|
app:layout_behavior="@string/appbar_scrolling_view_behavior"> |
||||||
|
|
||||||
|
<ProgressBar |
||||||
|
android:id="@+id/progress_bar" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
app:layout_constraintBottom_toBottomOf="parent" |
||||||
|
app:layout_constraintLeft_toLeftOf="parent" |
||||||
|
app:layout_constraintRight_toRightOf="parent" |
||||||
|
app:layout_constraintTop_toTopOf="parent" /> |
||||||
|
|
||||||
|
<com.keylesspalace.tusky.view.BackgroundMessageView |
||||||
|
android:id="@+id/errorMessageView" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:src="@android:color/transparent" |
||||||
|
android:visibility="gone" |
||||||
|
app:layout_constraintBottom_toBottomOf="parent" |
||||||
|
app:layout_constraintLeft_toLeftOf="parent" |
||||||
|
app:layout_constraintRight_toRightOf="parent" |
||||||
|
app:layout_constraintTop_toTopOf="parent" |
||||||
|
tools:src="@drawable/elephant_error" |
||||||
|
tools:visibility="visible" /> |
||||||
|
|
||||||
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout |
||||||
|
android:id="@+id/swipe_refresh_layout" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="match_parent"> |
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView |
||||||
|
android:id="@+id/scheduled_toot_list" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="match_parent" /> |
||||||
|
|
||||||
|
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout> |
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout> |
||||||
|
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout> |
||||||
@ -0,0 +1,40 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:orientation="horizontal"> |
||||||
|
|
||||||
|
<androidx.emoji.widget.EmojiTextView |
||||||
|
android:id="@+id/text" |
||||||
|
android:layout_width="0dp" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:layout_weight="0.91" |
||||||
|
android:padding="8dp" |
||||||
|
android:textSize="?attr/status_text_medium" /> |
||||||
|
|
||||||
|
<ImageButton |
||||||
|
android:id="@+id/edit" |
||||||
|
style="?attr/image_button_style" |
||||||
|
android:layout_width="32dp" |
||||||
|
android:layout_height="32dp" |
||||||
|
android:layout_gravity="center_vertical" |
||||||
|
android:layout_margin="12dp" |
||||||
|
android:background="?attr/selectableItemBackgroundBorderless" |
||||||
|
android:contentDescription="@string/action_edit" |
||||||
|
android:padding="4dp" |
||||||
|
app:srcCompat="@drawable/ic_create_24dp" /> |
||||||
|
|
||||||
|
<ImageButton |
||||||
|
android:id="@+id/delete" |
||||||
|
style="?attr/image_button_style" |
||||||
|
android:layout_width="32dp" |
||||||
|
android:layout_height="32dp" |
||||||
|
android:layout_gravity="center_vertical" |
||||||
|
android:layout_margin="12dp" |
||||||
|
android:background="?attr/selectableItemBackgroundBorderless" |
||||||
|
android:contentDescription="@string/action_delete" |
||||||
|
android:padding="4dp" |
||||||
|
app:srcCompat="@drawable/ic_clear_24dp" /> |
||||||
|
|
||||||
|
</LinearLayout> |
||||||
@ -0,0 +1,31 @@ |
|||||||
|
<merge 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" |
||||||
|
tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> |
||||||
|
|
||||||
|
<Button |
||||||
|
android:id="@+id/resetScheduleButton" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:paddingStart="16dp" |
||||||
|
android:paddingEnd="16dp" |
||||||
|
android:text="@string/action_reset_schedule" |
||||||
|
app:layout_constraintStart_toStartOf="parent" |
||||||
|
app:layout_constraintBottom_toBottomOf="parent" /> |
||||||
|
|
||||||
|
<TextView |
||||||
|
android:id="@+id/scheduledDateTime" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:paddingBottom="16dp" |
||||||
|
android:paddingEnd="16dp" |
||||||
|
android:paddingStart="4dp" |
||||||
|
android:paddingTop="4dp" |
||||||
|
android:textColor="?android:textColorTertiary" |
||||||
|
android:textSize="?attr/status_text_medium" |
||||||
|
android:drawablePadding="4dp" |
||||||
|
app:layout_constraintBottom_toBottomOf="parent" |
||||||
|
app:layout_constraintEnd_toEndOf="parent" |
||||||
|
tools:text="2020/01/01 00:00:00" /> |
||||||
|
|
||||||
|
</merge> |
||||||
Loading…
Reference in new issue