mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-19 23:29:16 +01:00
Remove custom presenter class
This commit is contained in:
parent
72f8c4d5e2
commit
96a39f5c54
@ -4,6 +4,7 @@ import android.os.Bundle
|
|||||||
import eu.kanade.tachiyomi.data.backup.BackupManager
|
import eu.kanade.tachiyomi.data.backup.BackupManager
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
@ -48,13 +49,13 @@ class BackupPresenter : BasePresenter<BackupFragment>() {
|
|||||||
* @param file the path where the file will be saved.
|
* @param file the path where the file will be saved.
|
||||||
*/
|
*/
|
||||||
fun createBackup(file: File) {
|
fun createBackup(file: File) {
|
||||||
if (isUnsubscribed(backupSubscription)) {
|
if (backupSubscription.isNullOrUnsubscribed()) {
|
||||||
backupSubscription = getBackupObservable(file)
|
backupSubscription = getBackupObservable(file)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeFirst(
|
.subscribeFirst(
|
||||||
{ view, result -> view.onBackupCompleted(file) },
|
{ view, result -> view.onBackupCompleted(file) },
|
||||||
{ view, error -> view.onBackupError(error) })
|
BackupFragment::onBackupError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -64,13 +65,13 @@ class BackupPresenter : BasePresenter<BackupFragment>() {
|
|||||||
* @param stream the input stream of the backup file.
|
* @param stream the input stream of the backup file.
|
||||||
*/
|
*/
|
||||||
fun restoreBackup(stream: InputStream) {
|
fun restoreBackup(stream: InputStream) {
|
||||||
if (isUnsubscribed(restoreSubscription)) {
|
if (restoreSubscription.isNullOrUnsubscribed()) {
|
||||||
restoreSubscription = getRestoreObservable(stream)
|
restoreSubscription = getRestoreObservable(stream)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribeFirst(
|
.subscribeFirst(
|
||||||
{ view, result -> view.onRestoreCompleted() },
|
{ view, result -> view.onRestoreCompleted() },
|
||||||
{ view, error -> view.onRestoreError(error) })
|
BackupFragment::onRestoreError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.presenter
|
package eu.kanade.tachiyomi.ui.base.presenter
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import nucleus.presenter.RxPresenter
|
||||||
import nucleus.view.ViewWithPresenter
|
import nucleus.view.ViewWithPresenter
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
|
||||||
|
@ -1,492 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.base.presenter;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.annotation.CallSuper;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import nucleus.presenter.Presenter;
|
|
||||||
import nucleus.presenter.delivery.DeliverFirst;
|
|
||||||
import nucleus.presenter.delivery.DeliverLatestCache;
|
|
||||||
import nucleus.presenter.delivery.DeliverReplay;
|
|
||||||
import nucleus.presenter.delivery.Delivery;
|
|
||||||
import rx.Observable;
|
|
||||||
import rx.Subscription;
|
|
||||||
import rx.functions.Action1;
|
|
||||||
import rx.functions.Action2;
|
|
||||||
import rx.functions.Func0;
|
|
||||||
import rx.internal.util.SubscriptionList;
|
|
||||||
import rx.subjects.BehaviorSubject;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is an extension of {@link Presenter} which provides RxJava functionality.
|
|
||||||
*
|
|
||||||
* @param <View> a type of view.
|
|
||||||
*/
|
|
||||||
public class RxPresenter<View> extends Presenter<View> {
|
|
||||||
|
|
||||||
private static final String REQUESTED_KEY = RxPresenter.class.getName() + "#requested";
|
|
||||||
|
|
||||||
private final BehaviorSubject<View> views = BehaviorSubject.create();
|
|
||||||
private final SubscriptionList subscriptions = new SubscriptionList();
|
|
||||||
|
|
||||||
private final HashMap<Integer, Func0<Subscription>> restartables = new HashMap<>();
|
|
||||||
private final HashMap<Integer, Subscription> restartableSubscriptions = new HashMap<>();
|
|
||||||
private final ArrayList<Integer> requested = new ArrayList<>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an {@link rx.Observable} that emits the current attached view or null.
|
|
||||||
* See {@link BehaviorSubject} for more information.
|
|
||||||
*
|
|
||||||
* @return an observable that emits the current attached view or null.
|
|
||||||
*/
|
|
||||||
public Observable<View> view() {
|
|
||||||
return views;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Registers a subscription to automatically unsubscribe it during onDestroy.
|
|
||||||
* See {@link SubscriptionList#add(Subscription) for details.}
|
|
||||||
*
|
|
||||||
* @param subscription a subscription to add.
|
|
||||||
*/
|
|
||||||
public void add(Subscription subscription) {
|
|
||||||
subscriptions.add(subscription);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes and unsubscribes a subscription that has been registered with {@link #add} previously.
|
|
||||||
* See {@link SubscriptionList#remove(Subscription)} for details.
|
|
||||||
*
|
|
||||||
* @param subscription a subscription to remove.
|
|
||||||
*/
|
|
||||||
public void remove(Subscription subscription) {
|
|
||||||
subscriptions.remove(subscription);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A restartable is any RxJava observable that can be started (subscribed) and
|
|
||||||
* should be automatically restarted (re-subscribed) after a process restart if
|
|
||||||
* it was still subscribed at the moment of saving presenter's state.
|
|
||||||
*
|
|
||||||
* Registers a factory. Re-subscribes the restartable after the process restart.
|
|
||||||
*
|
|
||||||
* @param restartableId id of the restartable
|
|
||||||
* @param factory factory of the restartable
|
|
||||||
*/
|
|
||||||
public void restartable(int restartableId, Func0<Subscription> factory) {
|
|
||||||
restartables.put(restartableId, factory);
|
|
||||||
if (requested.contains(restartableId))
|
|
||||||
start(restartableId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts the given restartable.
|
|
||||||
*
|
|
||||||
* @param restartableId id of the restartable
|
|
||||||
*/
|
|
||||||
public void start(int restartableId) {
|
|
||||||
stop(restartableId);
|
|
||||||
requested.add(restartableId);
|
|
||||||
restartableSubscriptions.put(restartableId, restartables.get(restartableId).call());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unsubscribes a restartable
|
|
||||||
*
|
|
||||||
* @param restartableId id of a restartable.
|
|
||||||
*/
|
|
||||||
public void stop(int restartableId) {
|
|
||||||
requested.remove((Integer) restartableId);
|
|
||||||
Subscription subscription = restartableSubscriptions.get(restartableId);
|
|
||||||
if (subscription != null)
|
|
||||||
subscription.unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a restartable is unsubscribed.
|
|
||||||
*
|
|
||||||
* @param restartableId id of the restartable.
|
|
||||||
* @return true if the subscription is null or unsubscribed, false otherwise.
|
|
||||||
*/
|
|
||||||
public boolean isUnsubscribed(int restartableId) {
|
|
||||||
return isUnsubscribed(restartableSubscriptions.get(restartableId));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a subscription is unsubscribed.
|
|
||||||
*
|
|
||||||
* @param subscription the subscription to check.
|
|
||||||
* @return true if the subscription is null or unsubscribed, false otherwise.
|
|
||||||
*/
|
|
||||||
public boolean isUnsubscribed(@Nullable Subscription subscription) {
|
|
||||||
return subscription == null || subscription.isUnsubscribed();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut that can be used instead of combining together
|
|
||||||
* {@link #restartable(int, Func0)},
|
|
||||||
* {@link #deliverFirst()},
|
|
||||||
* {@link #split(Action2, Action2)}.
|
|
||||||
*
|
|
||||||
* @param restartableId an id of the restartable.
|
|
||||||
* @param observableFactory a factory that should return an Observable when the restartable should run.
|
|
||||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
|
||||||
* @param onError a callback that will be called if the source observable emits onError.
|
|
||||||
* @param <T> the type of the observable.
|
|
||||||
*/
|
|
||||||
public <T> void restartableFirst(int restartableId, final Func0<Observable<T>> observableFactory,
|
|
||||||
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
|
||||||
|
|
||||||
restartable(restartableId, new Func0<Subscription>() {
|
|
||||||
@Override
|
|
||||||
public Subscription call() {
|
|
||||||
return observableFactory.call()
|
|
||||||
.compose(RxPresenter.this.<T>deliverFirst())
|
|
||||||
.subscribe(split(onNext, onError));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut for calling {@link #restartableFirst(int, Func0, Action2, Action2)} with the last parameter = null.
|
|
||||||
*/
|
|
||||||
public <T> void restartableFirst(int restartableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
|
|
||||||
restartableFirst(restartableId, observableFactory, onNext, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut that can be used instead of combining together
|
|
||||||
* {@link #restartable(int, Func0)},
|
|
||||||
* {@link #deliverLatestCache()},
|
|
||||||
* {@link #split(Action2, Action2)}.
|
|
||||||
*
|
|
||||||
* @param restartableId an id of the restartable.
|
|
||||||
* @param observableFactory a factory that should return an Observable when the restartable should run.
|
|
||||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
|
||||||
* @param onError a callback that will be called if the source observable emits onError.
|
|
||||||
* @param <T> the type of the observable.
|
|
||||||
*/
|
|
||||||
public <T> void restartableLatestCache(int restartableId, final Func0<Observable<T>> observableFactory,
|
|
||||||
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
|
||||||
|
|
||||||
restartable(restartableId, new Func0<Subscription>() {
|
|
||||||
@Override
|
|
||||||
public Subscription call() {
|
|
||||||
return observableFactory.call()
|
|
||||||
.compose(RxPresenter.this.<T>deliverLatestCache())
|
|
||||||
.subscribe(split(onNext, onError));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut for calling {@link #restartableLatestCache(int, Func0, Action2, Action2)} with the last parameter = null.
|
|
||||||
*/
|
|
||||||
public <T> void restartableLatestCache(int restartableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
|
|
||||||
restartableLatestCache(restartableId, observableFactory, onNext, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut that can be used instead of combining together
|
|
||||||
* {@link #restartable(int, Func0)},
|
|
||||||
* {@link #deliverReplay()},
|
|
||||||
* {@link #split(Action2, Action2)}.
|
|
||||||
*
|
|
||||||
* @param restartableId an id of the restartable.
|
|
||||||
* @param observableFactory a factory that should return an Observable when the restartable should run.
|
|
||||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
|
||||||
* @param onError a callback that will be called if the source observable emits onError.
|
|
||||||
* @param <T> the type of the observable.
|
|
||||||
*/
|
|
||||||
public <T> void restartableReplay(int restartableId, final Func0<Observable<T>> observableFactory,
|
|
||||||
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
|
||||||
|
|
||||||
restartable(restartableId, new Func0<Subscription>() {
|
|
||||||
@Override
|
|
||||||
public Subscription call() {
|
|
||||||
return observableFactory.call()
|
|
||||||
.compose(RxPresenter.this.<T>deliverReplay())
|
|
||||||
.subscribe(split(onNext, onError));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut for calling {@link #restartableReplay(int, Func0, Action2, Action2)} with the last parameter = null.
|
|
||||||
*/
|
|
||||||
public <T> void restartableReplay(int restartableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
|
|
||||||
restartableReplay(restartableId, observableFactory, onNext, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A startable behaves the same as a restartable but it does not resubscribe on process restart
|
|
||||||
*
|
|
||||||
* @param startableId an id of the restartable.
|
|
||||||
* @param observableFactory a factory that should return an Observable when the startable should run.
|
|
||||||
*/
|
|
||||||
public <T> void startable(int startableId, final Func0<Observable<T>> observableFactory) {
|
|
||||||
restartables.put(startableId, new Func0<Subscription>() {
|
|
||||||
@Override
|
|
||||||
public Subscription call() {return observableFactory.call().subscribe();}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A startable behaves the same as a restartable but it does not resubscribe on process restart
|
|
||||||
*
|
|
||||||
* @param startableId an id of the restartable.
|
|
||||||
* @param observableFactory a factory that should return an Observable when the startable should run.
|
|
||||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
|
||||||
* @param onError a callback that will be called if the source observable emits onError.
|
|
||||||
*/
|
|
||||||
public <T> void startable(int startableId, final Func0<Observable<T>> observableFactory,
|
|
||||||
final Action1<T> onNext, final Action1<Throwable> onError) {
|
|
||||||
|
|
||||||
restartables.put(startableId, new Func0<Subscription>() {
|
|
||||||
@Override
|
|
||||||
public Subscription call() {return observableFactory.call().subscribe(onNext, onError);}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A startable behaves the same as a restartable but it does not resubscribe on process restart
|
|
||||||
*
|
|
||||||
* @param startableId an id of the restartable.
|
|
||||||
* @param observableFactory a factory that should return an Observable when the startable should run.
|
|
||||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
|
||||||
*/
|
|
||||||
public <T> void startable(int startableId, final Func0<Observable<T>> observableFactory, final Action1<T> onNext) {
|
|
||||||
restartables.put(startableId, new Func0<Subscription>() {
|
|
||||||
@Override
|
|
||||||
public Subscription call() {return observableFactory.call().subscribe(onNext);}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut that can be used instead of combining together
|
|
||||||
* {@link #startable(int, Func0)},
|
|
||||||
* {@link #deliverFirst()},
|
|
||||||
* {@link #split(Action2, Action2)}.
|
|
||||||
*
|
|
||||||
* @param startableId an id of the startable.
|
|
||||||
* @param observableFactory a factory that should return an Observable when the startable should run.
|
|
||||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
|
||||||
* @param onError a callback that will be called if the source observable emits onError.
|
|
||||||
* @param <T> the type of the observable.
|
|
||||||
*/
|
|
||||||
public <T> void startableFirst(int startableId, final Func0<Observable<T>> observableFactory,
|
|
||||||
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
|
||||||
|
|
||||||
restartables.put(startableId, new Func0<Subscription>() {
|
|
||||||
@Override
|
|
||||||
public Subscription call() {
|
|
||||||
return observableFactory.call()
|
|
||||||
.compose(RxPresenter.this.<T>deliverFirst())
|
|
||||||
.subscribe(split(onNext, onError));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut for calling {@link #startableFirst(int, Func0, Action2, Action2)} with the last parameter = null.
|
|
||||||
*/
|
|
||||||
public <T> void startableFirst(int startableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
|
|
||||||
startableFirst(startableId, observableFactory, onNext, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut that can be used instead of combining together
|
|
||||||
* {@link #startable(int, Func0)},
|
|
||||||
* {@link #deliverLatestCache()},
|
|
||||||
* {@link #split(Action2, Action2)}.
|
|
||||||
*
|
|
||||||
* @param startableId an id of the startable.
|
|
||||||
* @param observableFactory a factory that should return an Observable when the startable should run.
|
|
||||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
|
||||||
* @param onError a callback that will be called if the source observable emits onError.
|
|
||||||
* @param <T> the type of the observable.
|
|
||||||
*/
|
|
||||||
public <T> void startableLatestCache(int startableId, final Func0<Observable<T>> observableFactory,
|
|
||||||
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
|
||||||
|
|
||||||
restartables.put(startableId, new Func0<Subscription>() {
|
|
||||||
@Override
|
|
||||||
public Subscription call() {
|
|
||||||
return observableFactory.call()
|
|
||||||
.compose(RxPresenter.this.<T>deliverLatestCache())
|
|
||||||
.subscribe(split(onNext, onError));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut for calling {@link #startableLatestCache(int, Func0, Action2, Action2)} with the last parameter = null.
|
|
||||||
*/
|
|
||||||
public <T> void startableLatestCache(int startableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
|
|
||||||
startableLatestCache(startableId, observableFactory, onNext, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut that can be used instead of combining together
|
|
||||||
* {@link #startable(int, Func0)},
|
|
||||||
* {@link #deliverReplay()},
|
|
||||||
* {@link #split(Action2, Action2)}.
|
|
||||||
*
|
|
||||||
* @param startableId an id of the startable.
|
|
||||||
* @param observableFactory a factory that should return an Observable when the startable should run.
|
|
||||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
|
||||||
* @param onError a callback that will be called if the source observable emits onError.
|
|
||||||
* @param <T> the type of the observable.
|
|
||||||
*/
|
|
||||||
public <T> void startableReplay(int startableId, final Func0<Observable<T>> observableFactory,
|
|
||||||
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
|
||||||
|
|
||||||
restartables.put(startableId, new Func0<Subscription>() {
|
|
||||||
@Override
|
|
||||||
public Subscription call() {
|
|
||||||
return observableFactory.call()
|
|
||||||
.compose(RxPresenter.this.<T>deliverReplay())
|
|
||||||
.subscribe(split(onNext, onError));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut for calling {@link #startableReplay(int, Func0, Action2, Action2)} with the last parameter = null.
|
|
||||||
*/
|
|
||||||
public <T> void startableReplay(int startableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
|
|
||||||
startableReplay(startableId, observableFactory, onNext, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by
|
|
||||||
* the source {@link rx.Observable}.
|
|
||||||
*
|
|
||||||
* {@link #deliverLatestCache} keeps the latest onNext value and emits it each time a new view gets attached.
|
|
||||||
* If a new onNext value appears while a view is attached, it will be delivered immediately.
|
|
||||||
*
|
|
||||||
* @param <T> the type of source observable emissions
|
|
||||||
*/
|
|
||||||
public <T> DeliverLatestCache<View, T> deliverLatestCache() {
|
|
||||||
return new DeliverLatestCache<>(views);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by
|
|
||||||
* the source {@link rx.Observable}.
|
|
||||||
*
|
|
||||||
* {@link #deliverFirst} delivers only the first onNext value that has been emitted by the source observable.
|
|
||||||
*
|
|
||||||
* @param <T> the type of source observable emissions
|
|
||||||
*/
|
|
||||||
public <T> DeliverFirst<View, T> deliverFirst() {
|
|
||||||
return new DeliverFirst<>(views);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by
|
|
||||||
* the source {@link rx.Observable}.
|
|
||||||
*
|
|
||||||
* {@link #deliverReplay} keeps all onNext values and emits them each time a new view gets attached.
|
|
||||||
* If a new onNext value appears while a view is attached, it will be delivered immediately.
|
|
||||||
*
|
|
||||||
* @param <T> the type of source observable emissions
|
|
||||||
*/
|
|
||||||
public <T> DeliverReplay<View, T> deliverReplay() {
|
|
||||||
return new DeliverReplay<>(views);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a method that can be used for manual restartable chain build. It returns an Action1 that splits
|
|
||||||
* a received {@link Delivery} into two {@link Action2} onNext and onError calls.
|
|
||||||
*
|
|
||||||
* @param onNext a method that will be called if the delivery contains an emitted onNext value.
|
|
||||||
* @param onError a method that will be called if the delivery contains an onError throwable.
|
|
||||||
* @param <T> a type on onNext value.
|
|
||||||
* @return an Action1 that splits a received {@link Delivery} into two {@link Action2} onNext and onError calls.
|
|
||||||
*/
|
|
||||||
public <T> Action1<Delivery<View, T>> split(final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
|
||||||
return new Action1<Delivery<View, T>>() {
|
|
||||||
@Override
|
|
||||||
public void call(Delivery<View, T> delivery) {
|
|
||||||
delivery.split(onNext, onError);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is a shortcut for calling {@link #split(Action2, Action2)} when the second parameter is null.
|
|
||||||
*/
|
|
||||||
public <T> Action1<Delivery<View, T>> split(Action2<View, T> onNext) {
|
|
||||||
return split(onNext, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@CallSuper
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedState) {
|
|
||||||
if (savedState != null)
|
|
||||||
requested.addAll(savedState.getIntegerArrayList(REQUESTED_KEY));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@CallSuper
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
views.onCompleted();
|
|
||||||
subscriptions.unsubscribe();
|
|
||||||
for (Map.Entry<Integer, Subscription> entry : restartableSubscriptions.entrySet())
|
|
||||||
entry.getValue().unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@CallSuper
|
|
||||||
@Override
|
|
||||||
protected void onSave(Bundle state) {
|
|
||||||
for (int i = requested.size() - 1; i >= 0; i--) {
|
|
||||||
int restartableId = requested.get(i);
|
|
||||||
Subscription subscription = restartableSubscriptions.get(restartableId);
|
|
||||||
if (subscription != null && subscription.isUnsubscribed())
|
|
||||||
requested.remove(i);
|
|
||||||
}
|
|
||||||
state.putIntegerArrayList(REQUESTED_KEY, requested);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@CallSuper
|
|
||||||
@Override
|
|
||||||
protected void onTakeView(View view) {
|
|
||||||
views.onNext(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@CallSuper
|
|
||||||
@Override
|
|
||||||
protected void onDropView() {
|
|
||||||
views.onNext(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Please, use restartableXX and deliverXX methods for pushing data from RxPresenter into View.
|
|
||||||
*/
|
|
||||||
@Deprecated
|
|
||||||
@Nullable
|
|
||||||
@Override
|
|
||||||
public View getView() {
|
|
||||||
return super.getView();
|
|
||||||
}
|
|
||||||
}
|
|
@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
|||||||
import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent
|
import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent
|
||||||
import eu.kanade.tachiyomi.ui.manga.info.MangaFavoriteEvent
|
import eu.kanade.tachiyomi.ui.manga.info.MangaFavoriteEvent
|
||||||
import eu.kanade.tachiyomi.util.SharedData
|
import eu.kanade.tachiyomi.util.SharedData
|
||||||
|
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
import rx.Subscription
|
import rx.Subscription
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@ -44,10 +45,10 @@ class MangaPresenter : BasePresenter<MangaActivity>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun setMangaEvent(event: MangaEvent) {
|
fun setMangaEvent(event: MangaEvent) {
|
||||||
if (isUnsubscribed(mangaSubscription)) {
|
if (mangaSubscription.isNullOrUnsubscribed()) {
|
||||||
manga = event.manga
|
manga = event.manga
|
||||||
mangaSubscription = Observable.just(manga)
|
mangaSubscription = Observable.just(manga)
|
||||||
.subscribeLatestCache({ view, manga -> view.onSetManga(manga) })
|
.subscribeLatestCache(MangaActivity::onSetManga)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import com.jakewharton.rxrelay.PublishRelay
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
@ -15,11 +16,12 @@ import eu.kanade.tachiyomi.ui.manga.MangaEvent
|
|||||||
import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent
|
import eu.kanade.tachiyomi.ui.manga.info.ChapterCountEvent
|
||||||
import eu.kanade.tachiyomi.ui.manga.info.MangaFavoriteEvent
|
import eu.kanade.tachiyomi.ui.manga.info.MangaFavoriteEvent
|
||||||
import eu.kanade.tachiyomi.util.SharedData
|
import eu.kanade.tachiyomi.util.SharedData
|
||||||
|
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import rx.subjects.PublishSubject
|
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
@ -69,8 +71,8 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
/**
|
/**
|
||||||
* Subject of list of chapters to allow updating the view without going to DB.
|
* Subject of list of chapters to allow updating the view without going to DB.
|
||||||
*/
|
*/
|
||||||
val chaptersSubject: PublishSubject<List<ChapterModel>>
|
val chaptersRelay: PublishRelay<List<ChapterModel>>
|
||||||
by lazy { PublishSubject.create<List<ChapterModel>>() }
|
by lazy { PublishRelay.create<List<ChapterModel>>() }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether the chapter list has been requested to the source.
|
* Whether the chapter list has been requested to the source.
|
||||||
@ -78,56 +80,33 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
var hasRequested = false
|
var hasRequested = false
|
||||||
private set
|
private set
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
/**
|
||||||
* Id of the restartable which sends a filtered and ordered list of chapters to the view.
|
* Subscription to retrieve the new list of chapters from the source.
|
||||||
*/
|
*/
|
||||||
private const val GET_CHAPTERS = 1
|
private var fetchChaptersSubscription: Subscription? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Id of the restartable which requests an updated list of chapters to the source.
|
* Subscription to observe download status changes.
|
||||||
*/
|
*/
|
||||||
private const val FETCH_CHAPTERS = 2
|
private var observeDownloadsSubscription: Subscription? = null
|
||||||
|
|
||||||
/**
|
|
||||||
* Id of the restartable which listens for download status changes.
|
|
||||||
*/
|
|
||||||
private const val CHAPTER_STATUS_CHANGES = 3
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
startableLatestCache(GET_CHAPTERS,
|
|
||||||
// On each subject emission, apply filters and sort then update the view.
|
|
||||||
{ chaptersSubject
|
|
||||||
.flatMap { applyChapterFilters(it) }
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
}, ChaptersFragment::onNextChapters)
|
|
||||||
|
|
||||||
startableFirst(FETCH_CHAPTERS,
|
|
||||||
{ getRemoteChaptersObservable() },
|
|
||||||
{ view, result -> view.onFetchChaptersDone() },
|
|
||||||
ChaptersFragment::onFetchChaptersError)
|
|
||||||
|
|
||||||
startableLatestCache(CHAPTER_STATUS_CHANGES,
|
|
||||||
{ getChapterStatusObservable() },
|
|
||||||
ChaptersFragment::onChapterStatusChange,
|
|
||||||
{ view, error -> Timber.e(error) })
|
|
||||||
|
|
||||||
// Find the active manga from the shared data or return.
|
// Find the active manga from the shared data or return.
|
||||||
manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
|
manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
|
||||||
|
source = sourceManager.get(manga.source)!!
|
||||||
Observable.just(manga)
|
Observable.just(manga)
|
||||||
.subscribeLatestCache(ChaptersFragment::onNextManga)
|
.subscribeLatestCache(ChaptersFragment::onNextManga)
|
||||||
|
|
||||||
// Find the source for this manga.
|
// Prepare the relay.
|
||||||
source = sourceManager.get(manga.source)!!
|
chaptersRelay.flatMap { applyChapterFilters(it) }
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
// Prepare the publish subject.
|
.subscribeLatestCache(ChaptersFragment::onNextChapters,
|
||||||
start(GET_CHAPTERS)
|
{ view, error -> Timber.e(error) })
|
||||||
|
|
||||||
// Add the subscription that retrieves the chapters from the database, keeps subscribed to
|
// Add the subscription that retrieves the chapters from the database, keeps subscribed to
|
||||||
// changes, and sends the list of chapters to the publish subject.
|
// changes, and sends the list of chapters to the relay.
|
||||||
add(db.getChapters(manga).asRxObservable()
|
add(db.getChapters(manga).asRxObservable()
|
||||||
.map { chapters ->
|
.map { chapters ->
|
||||||
// Convert every chapter to a model.
|
// Convert every chapter to a model.
|
||||||
@ -141,12 +120,22 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
this.chapters = chapters
|
this.chapters = chapters
|
||||||
|
|
||||||
// Listen for download status changes
|
// Listen for download status changes
|
||||||
start(CHAPTER_STATUS_CHANGES)
|
observeDownloads()
|
||||||
|
|
||||||
// Emit the number of chapters to the info tab.
|
// Emit the number of chapters to the info tab.
|
||||||
SharedData.get(ChapterCountEvent::class.java)?.emit(chapters.size)
|
SharedData.get(ChapterCountEvent::class.java)?.emit(chapters.size)
|
||||||
}
|
}
|
||||||
.subscribe { chaptersSubject.onNext(it) })
|
.subscribe { chaptersRelay.call(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun observeDownloads() {
|
||||||
|
observeDownloadsSubscription?.let { remove(it) }
|
||||||
|
observeDownloadsSubscription = downloadManager.queue.getStatusObservable()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.filter { download -> download.manga.id == manga.id }
|
||||||
|
.doOnNext { onDownloadStatusChange(it) }
|
||||||
|
.subscribeLatestCache(ChaptersFragment::onChapterStatusChange,
|
||||||
|
{ view, error -> Timber.e(error) })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -186,34 +175,24 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
*/
|
*/
|
||||||
fun fetchChaptersFromSource() {
|
fun fetchChaptersFromSource() {
|
||||||
hasRequested = true
|
hasRequested = true
|
||||||
start(FETCH_CHAPTERS)
|
|
||||||
|
if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return
|
||||||
|
fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribeFirst({ view, chapters ->
|
||||||
|
view.onFetchChaptersDone()
|
||||||
|
}, ChaptersFragment::onFetchChaptersError)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the UI after applying the filters.
|
* Updates the UI after applying the filters.
|
||||||
*/
|
*/
|
||||||
private fun refreshChapters() {
|
private fun refreshChapters() {
|
||||||
chaptersSubject.onNext(chapters)
|
chaptersRelay.call(chapters)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable that updates the chapter list with the latest from the source.
|
|
||||||
*/
|
|
||||||
fun getRemoteChaptersObservable(): Observable<Pair<List<Chapter>, List<Chapter>>> =
|
|
||||||
Observable.defer { source.fetchChapterList(manga) }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable that listens to download queue status changes.
|
|
||||||
*/
|
|
||||||
fun getChapterStatusObservable(): Observable<Download> =
|
|
||||||
downloadManager.queue.getStatusObservable()
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.filter { download -> download.manga.id == manga.id }
|
|
||||||
.doOnNext { onDownloadStatusChange(it) }
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies the view filters to the list of chapters obtained from the database.
|
* Applies the view filters to the list of chapters obtained from the database.
|
||||||
* @param chapters the list of chapters from the database
|
* @param chapters the list of chapters from the database
|
||||||
@ -224,7 +203,7 @@ class ChaptersPresenter : BasePresenter<ChaptersFragment>() {
|
|||||||
if (onlyUnread()) {
|
if (onlyUnread()) {
|
||||||
observable = observable.filter { !it.read }
|
observable = observable.filter { !it.read }
|
||||||
}
|
}
|
||||||
if (onlyRead()) {
|
else if (onlyRead()) {
|
||||||
observable = observable.filter { it.read }
|
observable = observable.filter { it.read }
|
||||||
}
|
}
|
||||||
if (onlyDownloaded()) {
|
if (onlyDownloaded()) {
|
||||||
|
@ -9,7 +9,9 @@ import eu.kanade.tachiyomi.data.source.SourceManager
|
|||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaEvent
|
import eu.kanade.tachiyomi.ui.manga.MangaEvent
|
||||||
import eu.kanade.tachiyomi.util.SharedData
|
import eu.kanade.tachiyomi.util.SharedData
|
||||||
|
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import rx.Subscription
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
import rx.schedulers.Schedulers
|
import rx.schedulers.Schedulers
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
@ -49,32 +51,21 @@ class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
|
|||||||
val coverCache: CoverCache by injectLazy()
|
val coverCache: CoverCache by injectLazy()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The id of the restartable.
|
* Subscription to send the manga to the view.
|
||||||
*/
|
*/
|
||||||
private val GET_MANGA = 1
|
private var viewMangaSubcription: Subscription? = null
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The id of the restartable.
|
* Subscription to update the manga from the source.
|
||||||
*/
|
*/
|
||||||
private val FETCH_MANGA_INFO = 2
|
private var fetchMangaSubscription: Subscription? = null
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
override fun onCreate(savedState: Bundle?) {
|
||||||
super.onCreate(savedState)
|
super.onCreate(savedState)
|
||||||
|
|
||||||
// Notify the view a manga is available or has changed.
|
|
||||||
startableLatestCache(GET_MANGA,
|
|
||||||
{ Observable.just(manga) },
|
|
||||||
{ view, manga -> view.onNextManga(manga, source) })
|
|
||||||
|
|
||||||
// Fetch manga info from source.
|
|
||||||
startableFirst(FETCH_MANGA_INFO,
|
|
||||||
{ fetchMangaObs() },
|
|
||||||
{ view, manga -> view.onFetchMangaDone() },
|
|
||||||
{ view, error -> view.onFetchMangaError() })
|
|
||||||
|
|
||||||
manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
|
manga = SharedData.get(MangaEvent::class.java)?.manga ?: return
|
||||||
source = sourceManager.get(manga.source)!!
|
source = sourceManager.get(manga.source)!!
|
||||||
refreshManga()
|
sendMangaToView()
|
||||||
|
|
||||||
// Update chapter count
|
// Update chapter count
|
||||||
SharedData.get(ChapterCountEvent::class.java)?.observable
|
SharedData.get(ChapterCountEvent::class.java)?.observable
|
||||||
@ -88,30 +79,34 @@ class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch manga information from source.
|
* Sends the active manga to the view.
|
||||||
*/
|
*/
|
||||||
fun fetchMangaFromSource() {
|
fun sendMangaToView() {
|
||||||
if (isUnsubscribed(FETCH_MANGA_INFO)) {
|
viewMangaSubcription?.let { remove(it) }
|
||||||
start(FETCH_MANGA_INFO)
|
viewMangaSubcription = Observable.just(manga)
|
||||||
}
|
.subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) })
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch manga information from source.
|
* Fetch manga information from source.
|
||||||
*
|
|
||||||
* @return manga information.
|
|
||||||
*/
|
*/
|
||||||
private fun fetchMangaObs(): Observable<Manga> {
|
fun fetchMangaFromSource() {
|
||||||
return Observable.defer { source.fetchMangaDetails(manga) }
|
if (!fetchMangaSubscription.isNullOrUnsubscribed()) return
|
||||||
.flatMap { networkManga ->
|
fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) }
|
||||||
|
.map { networkManga ->
|
||||||
manga.copyFrom(networkManga)
|
manga.copyFrom(networkManga)
|
||||||
manga.initialized = true
|
manga.initialized = true
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
Observable.just<Manga>(manga)
|
manga
|
||||||
}
|
}
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.doOnNext { refreshManga() }
|
.doOnNext { sendMangaToView() }
|
||||||
|
.subscribeFirst({ view, manga ->
|
||||||
|
view.onFetchMangaDone()
|
||||||
|
}, { view, error ->
|
||||||
|
view.onFetchMangaError()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -123,19 +118,14 @@ class MangaInfoPresenter : BasePresenter<MangaInfoFragment>() {
|
|||||||
coverCache.deleteFromCache(manga.thumbnail_url)
|
coverCache.deleteFromCache(manga.thumbnail_url)
|
||||||
}
|
}
|
||||||
db.insertManga(manga).executeAsBlocking()
|
db.insertManga(manga).executeAsBlocking()
|
||||||
refreshManga()
|
sendMangaToView()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setFavorite(favorite: Boolean) {
|
private fun setFavorite(favorite: Boolean) {
|
||||||
if (manga.favorite == favorite)
|
if (manga.favorite == favorite) {
|
||||||
return
|
return
|
||||||
|
}
|
||||||
toggleFavorite()
|
toggleFavorite()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh MangaInfo view.
|
|
||||||
*/
|
|
||||||
private fun refreshManga() {
|
|
||||||
start(GET_MANGA)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user