mirror of
https://github.com/sudoxnym/habitica-android.git
synced 2026-04-14 19:56:32 +00:00
Implement basic offline task creation and updating
This commit is contained in:
parent
9cf2b1a71b
commit
43db91293b
15 changed files with 171 additions and 11 deletions
|
|
@ -2,7 +2,7 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.habitrpg.android.habitica"
|
||||
android:versionCode="1988"
|
||||
android:versionCode="1990"
|
||||
android:versionName="1.5"
|
||||
android:screenOrientation="portrait"
|
||||
android:installLocation="auto" >
|
||||
|
|
|
|||
|
|
@ -147,6 +147,23 @@
|
|||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/syncing_view"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginLeft="@dimen/spacing_small"
|
||||
android:layout_marginRight="@dimen/spacing_small"
|
||||
style="@style/Widget.AppCompat.ProgressBar"/>
|
||||
<ImageButton
|
||||
android:id="@+id/error_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="@color/transparent"
|
||||
android:src="@drawable/ic_warning_black"
|
||||
android:layout_marginLeft="@dimen/spacing_small"
|
||||
android:layout_marginRight="@dimen/spacing_small"/>
|
||||
<LinearLayout
|
||||
android:id="@+id/checklistIndicatorWrapper"
|
||||
android:layout_width="@dimen/checklist_wrapper_width"
|
||||
|
|
|
|||
|
|
@ -143,6 +143,23 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/syncing_view"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginLeft="@dimen/spacing_small"
|
||||
android:layout_marginRight="@dimen/spacing_small"
|
||||
style="@style/Widget.AppCompat.ProgressBar"/>
|
||||
<ImageButton
|
||||
android:id="@+id/error_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="@color/transparent"
|
||||
android:src="@drawable/ic_warning_black"
|
||||
android:layout_marginLeft="@dimen/spacing_small"
|
||||
android:layout_marginRight="@dimen/spacing_small"/>
|
||||
<FrameLayout
|
||||
android:id="@+id/btnMinusWrapper"
|
||||
android:layout_width="@dimen/button_width"
|
||||
|
|
|
|||
|
|
@ -66,6 +66,23 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/syncing_view"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginLeft="@dimen/spacing_small"
|
||||
android:layout_marginRight="@dimen/spacing_small"
|
||||
style="@style/Widget.AppCompat.ProgressBar"/>
|
||||
<ImageButton
|
||||
android:id="@+id/error_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="@color/transparent"
|
||||
android:src="@drawable/ic_warning_black"
|
||||
android:layout_marginLeft="@dimen/spacing_small"
|
||||
android:layout_marginRight="@dimen/spacing_small"/>
|
||||
<LinearLayout
|
||||
android:id="@+id/buyButton"
|
||||
android:layout_width="48dp"
|
||||
|
|
|
|||
|
|
@ -135,6 +135,23 @@
|
|||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/syncing_view"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginLeft="@dimen/spacing_small"
|
||||
android:layout_marginRight="@dimen/spacing_small"
|
||||
style="@style/Widget.AppCompat.ProgressBar"/>
|
||||
<ImageButton
|
||||
android:id="@+id/error_icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="@color/transparent"
|
||||
android:src="@drawable/ic_warning_black"
|
||||
android:layout_marginLeft="@dimen/spacing_small"
|
||||
android:layout_marginRight="@dimen/spacing_small"/>
|
||||
<LinearLayout
|
||||
android:id="@+id/checklistIndicatorWrapper"
|
||||
android:layout_width="@dimen/checklist_wrapper_width"
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import com.habitrpg.android.habitica.models.tasks.TasksOrder
|
|||
import com.habitrpg.android.habitica.models.user.User
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.Single
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmResults
|
||||
import java.util.*
|
||||
|
|
@ -56,4 +57,5 @@ interface TaskRepository : BaseRepository {
|
|||
|
||||
fun updateDailiesIsDue(date: Date): Flowable<TaskList>
|
||||
fun retrieveCompletedTodos(userId: String): Flowable<TaskList>
|
||||
fun syncErroredTasks(): Single<List<Task>>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,8 +9,10 @@ import com.habitrpg.android.habitica.models.responses.TaskDirection
|
|||
import com.habitrpg.android.habitica.models.responses.TaskScoringResult
|
||||
import com.habitrpg.android.habitica.models.tasks.*
|
||||
import com.habitrpg.android.habitica.models.user.User
|
||||
import com.playseeds.android.sdk.inappmessaging.Log
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.functions.Consumer
|
||||
import io.realm.Realm
|
||||
import io.realm.RealmList
|
||||
|
|
@ -162,12 +164,26 @@ class TaskRepositoryImpl(localRepository: TaskLocalRepository, apiClient: ApiCli
|
|||
}
|
||||
}
|
||||
|
||||
task.isSaving = true
|
||||
task.isCreating = true
|
||||
task.hasErrored = false
|
||||
task.userId = userID
|
||||
if (task.id == null) {
|
||||
task.id = UUID.randomUUID().toString()
|
||||
}
|
||||
localRepository.saveTask(task)
|
||||
|
||||
return apiClient.createTask(task)
|
||||
.map { task1 ->
|
||||
task1.dateCreated = Date()
|
||||
task1
|
||||
}
|
||||
.doOnNext { localRepository.saveTask(it) }
|
||||
.doOnError {
|
||||
task.hasErrored = true
|
||||
task.isSaving = false
|
||||
localRepository.save(task)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
|
|
@ -178,6 +194,10 @@ class TaskRepositoryImpl(localRepository: TaskLocalRepository, apiClient: ApiCli
|
|||
}
|
||||
lastTaskAction = now
|
||||
val id = task.id ?: return Maybe.just(task)
|
||||
|
||||
task.isSaving = true
|
||||
task.hasErrored = false
|
||||
localRepository.saveTask(task)
|
||||
return localRepository.getTaskCopy(id).firstElement()
|
||||
.flatMap { task1 -> apiClient.updateTask(id, task1).singleElement() }
|
||||
.map { task1 ->
|
||||
|
|
@ -185,6 +205,11 @@ class TaskRepositoryImpl(localRepository: TaskLocalRepository, apiClient: ApiCli
|
|||
task1
|
||||
}
|
||||
.doOnSuccess { localRepository.saveTask(it) }
|
||||
.doOnError {
|
||||
task.hasErrored = true
|
||||
task.isSaving = false
|
||||
localRepository.save(task)
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteTask(taskId: String): Flowable<Void> {
|
||||
|
|
@ -241,4 +266,17 @@ class TaskRepositoryImpl(localRepository: TaskLocalRepository, apiClient: ApiCli
|
|||
return apiClient.getTasks("dailys", formatter.format(date))
|
||||
.flatMapMaybe { localRepository.updateIsdue(it) }
|
||||
}
|
||||
|
||||
override fun syncErroredTasks(): Single<List<Task>> {
|
||||
return localRepository.getErroredTasks(userID).firstElement()
|
||||
.flatMapPublisher { Flowable.fromIterable(it) }
|
||||
.map { localRepository.getUnmanagedCopy(it) }
|
||||
.flatMap {
|
||||
return@flatMap if (it.isCreating) {
|
||||
createTask(it)
|
||||
} else {
|
||||
updateTask(it).toFlowable()
|
||||
}
|
||||
}.toList()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,4 +34,5 @@ interface TaskLocalRepository : BaseLocalRepository {
|
|||
|
||||
fun updateTaskPositions(taskOrder: List<String>)
|
||||
fun saveCompletedTodos(userId: String, tasks: MutableCollection<Task>)
|
||||
fun getErroredTasks(userID: String): Flowable<RealmResults<Task>>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -203,4 +203,14 @@ class RealmTaskLocalRepository(realm: Realm) : RealmBaseLocalRepository(realm),
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getErroredTasks(userID: String): Flowable<RealmResults<Task>> {
|
||||
return realm.where(Task::class.java)
|
||||
.equalTo("userId", userID)
|
||||
.equalTo("hasErrored", true)
|
||||
.sort("position")
|
||||
.findAll()
|
||||
.asFlowable()
|
||||
.filter { it.isLoaded }
|
||||
.retry(1) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ import org.json.JSONException
|
|||
import java.util.*
|
||||
|
||||
open class Task : RealmObject, Parcelable {
|
||||
@PrimaryKey
|
||||
@SerializedName("_id")
|
||||
var id: String? = null
|
||||
var userId: String = ""
|
||||
var priority: Float = 0.0f
|
||||
var text: String = ""
|
||||
|
|
@ -60,9 +63,6 @@ open class Task : RealmObject, Parcelable {
|
|||
var parsedText: CharSequence? = null
|
||||
@Ignore
|
||||
var parsedNotes: CharSequence? = null
|
||||
@PrimaryKey
|
||||
@SerializedName("_id")
|
||||
var id: String? = null
|
||||
set(value) {
|
||||
field = value
|
||||
repeat?.taskId = id
|
||||
|
|
@ -73,6 +73,11 @@ open class Task : RealmObject, Parcelable {
|
|||
var nextDue: Date? = null
|
||||
var yesterDaily: Boolean? = null
|
||||
|
||||
//Needed for offline creating/updating
|
||||
var isSaving: Boolean = false
|
||||
var hasErrored: Boolean = false
|
||||
var isCreating: Boolean = false
|
||||
|
||||
private var daysOfMonthString: String? = null
|
||||
private var weeksOfMonthString: String? = null
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,11 @@ import android.view.ViewGroup
|
|||
import com.habitrpg.android.habitica.helpers.TaskFilterHelper
|
||||
import com.habitrpg.android.habitica.models.tasks.Task
|
||||
import com.habitrpg.android.habitica.ui.viewHolders.tasks.BaseTaskViewHolder
|
||||
import io.reactivex.BackpressureStrategy
|
||||
import io.reactivex.Flowable
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.functions.Action
|
||||
import io.reactivex.subjects.PublishSubject
|
||||
import io.realm.OrderedRealmCollection
|
||||
import io.realm.OrderedRealmCollectionChangeListener
|
||||
import io.realm.RealmList
|
||||
|
|
@ -45,6 +50,8 @@ abstract class RealmBaseTasksRecyclerViewAdapter<VH : BaseTaskViewHolder>(privat
|
|||
var data: OrderedRealmCollection<Task>? = null
|
||||
private set
|
||||
|
||||
private var errorButtonEventsSubject = PublishSubject.create<String>()
|
||||
|
||||
private val isDataValid: Boolean
|
||||
get() = data?.isValid ?: false
|
||||
|
||||
|
|
@ -78,8 +85,6 @@ abstract class RealmBaseTasksRecyclerViewAdapter<VH : BaseTaskViewHolder>(privat
|
|||
|
||||
fun getItem(index: Int): Task? = if (isDataValid) data?.get(index) else null
|
||||
|
||||
|
||||
|
||||
override fun updateData(data: OrderedRealmCollection<Task>?) {
|
||||
if (hasAutoUpdates) {
|
||||
if (isDataValid) {
|
||||
|
|
@ -132,6 +137,9 @@ abstract class RealmBaseTasksRecyclerViewAdapter<VH : BaseTaskViewHolder>(privat
|
|||
val item = getItem(position)
|
||||
if (item != null) {
|
||||
holder.bindHolder(item, position)
|
||||
holder.errorButtonClicked = Action {
|
||||
errorButtonEventsSubject.onNext("")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -158,4 +166,8 @@ abstract class RealmBaseTasksRecyclerViewAdapter<VH : BaseTaskViewHolder>(privat
|
|||
override fun getTaskIDAt(position: Int): String {
|
||||
return data?.get(position)?.id ?: ""
|
||||
}
|
||||
|
||||
override fun getErrorButtonEvents(): Flowable<String> {
|
||||
return errorButtonEventsSubject.toFlowable(BackpressureStrategy.DROP)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ import com.habitrpg.android.habitica.models.user.User;
|
|||
import com.habitrpg.android.habitica.ui.viewHolders.ShopItemViewHolder;
|
||||
import com.habitrpg.android.habitica.ui.viewHolders.tasks.RewardViewHolder;
|
||||
|
||||
import io.reactivex.BackpressureStrategy;
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.functions.Action;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
import io.realm.OrderedRealmCollection;
|
||||
|
||||
public class RewardsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements TaskRecyclerViewAdapter {
|
||||
|
|
@ -30,6 +34,8 @@ public class RewardsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVie
|
|||
@Nullable
|
||||
private User user;
|
||||
|
||||
private PublishSubject<String> errorButtonEvents = PublishSubject.create();
|
||||
|
||||
public RewardsRecyclerViewAdapter(@Nullable OrderedRealmCollection<Task> data, Context context, int layoutResource, @Nullable User user) {
|
||||
this.context = context;
|
||||
this.layoutResource = layoutResource;
|
||||
|
|
@ -91,6 +97,11 @@ public class RewardsRecyclerViewAdapter extends RecyclerView.Adapter<RecyclerVie
|
|||
updateData(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Flowable<String> getErrorButtonEvents() {
|
||||
return errorButtonEvents.toFlowable(BackpressureStrategy.DROP);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
int rewardCount = getCustomRewardCount();
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import com.habitrpg.android.habitica.models.tasks.Task;
|
|||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import io.reactivex.Flowable;
|
||||
import io.reactivex.Observable;
|
||||
import io.realm.OrderedRealmCollection;
|
||||
import io.realm.RealmResults;
|
||||
|
||||
|
|
@ -22,4 +24,6 @@ public interface TaskRecyclerViewAdapter {
|
|||
boolean getIgnoreUpdates();
|
||||
|
||||
void updateUnfilteredData(@Nullable OrderedRealmCollection<Task> data);
|
||||
|
||||
Flowable<String> getErrorButtonEvents();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,9 +83,13 @@ open class TaskRecyclerViewFragment : BaseFragment(), View.OnClickListener, Swip
|
|||
allowReordering()
|
||||
}
|
||||
|
||||
recyclerAdapter = adapter as TaskRecyclerViewAdapter
|
||||
recyclerAdapter = adapter as? TaskRecyclerViewAdapter
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
recyclerAdapter?.errorButtonEvents?.subscribe(Consumer {
|
||||
taskRepository.syncErroredTasks().subscribe(Consumer {}, RxErrorHandler.handleEmptyError())
|
||||
}, RxErrorHandler.handleEmptyError())
|
||||
|
||||
if (this.classType != null) {
|
||||
taskRepository.getTasks(this.classType ?: "", userID).firstElement().subscribe(Consumer { this.recyclerAdapter?.updateUnfilteredData(it)
|
||||
this.recyclerAdapter?.filter()
|
||||
|
|
|
|||
|
|
@ -4,10 +4,7 @@ import android.content.Context
|
|||
import android.support.v7.widget.RecyclerView
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.*
|
||||
import com.habitrpg.android.habitica.R
|
||||
import com.habitrpg.android.habitica.events.TaskTappedEvent
|
||||
import com.habitrpg.android.habitica.ui.helpers.bindView
|
||||
|
|
@ -20,6 +17,7 @@ import com.habitrpg.android.habitica.ui.helpers.bindOptionalView
|
|||
import com.habitrpg.android.habitica.ui.views.EllipsisTextView
|
||||
import io.reactivex.Single
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.functions.Action
|
||||
import io.reactivex.functions.Consumer
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import net.pherth.android.emoji_library.EmojiTextView
|
||||
|
|
@ -29,6 +27,7 @@ abstract class BaseTaskViewHolder constructor(itemView: View) : RecyclerView.Vie
|
|||
|
||||
|
||||
var task: Task? = null
|
||||
var errorButtonClicked: Action? = null
|
||||
protected var context: Context
|
||||
private val titleTextView: EllipsisTextView by bindView(itemView, R.id.checkedTextView)
|
||||
private val notesTextView: EllipsisTextView by bindView(itemView, R.id.notesTextView)
|
||||
|
|
@ -40,6 +39,8 @@ abstract class BaseTaskViewHolder constructor(itemView: View) : RecyclerView.Vie
|
|||
private val taskIconWrapper: LinearLayout? by bindView(itemView, R.id.taskIconWrapper)
|
||||
private val approvalRequiredTextView: TextView? by bindView(itemView, R.id.approvalRequiredTextField)
|
||||
private val expandNotesButton: Button by bindView(R.id.expand_notes_button)
|
||||
private val syncingView: ProgressBar by bindView(R.id.syncing_view)
|
||||
private val errorIconView: ImageButton by bindView(R.id.error_icon)
|
||||
protected val taskGray: Int by bindColor(itemView.context, R.color.task_gray)
|
||||
|
||||
private var openTaskDisabled: Boolean = false
|
||||
|
|
@ -73,6 +74,8 @@ abstract class BaseTaskViewHolder constructor(itemView: View) : RecyclerView.Vie
|
|||
itemView.setOnClickListener { onClick(it) }
|
||||
itemView.isClickable = true
|
||||
|
||||
errorIconView.setOnClickListener { errorButtonClicked?.run()}
|
||||
|
||||
//Re enable when we find a way to only react when a link is tapped.
|
||||
//notesTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||
//titleTextView.movementMethod = LinkMovementMethod.getInstance()
|
||||
|
|
@ -163,6 +166,8 @@ abstract class BaseTaskViewHolder constructor(itemView: View) : RecyclerView.Vie
|
|||
approvalRequiredTextView?.visibility = View.GONE
|
||||
}
|
||||
|
||||
syncingView.visibility = if (task?.isSaving == true) View.VISIBLE else View.GONE
|
||||
errorIconView.visibility = if (task?.hasErrored == true) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue