[streaming] Home timeline streaming
This commit is contained in:
parent
54dfc854d4
commit
9bb15e943d
|
@ -16,6 +16,7 @@
|
||||||
package com.keylesspalace.tusky;
|
package com.keylesspalace.tusky;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
@ -25,6 +26,7 @@ import android.preference.PreferenceManager;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.WindowManager;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
@ -118,6 +120,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
||||||
private Drawer drawer;
|
private Drawer drawer;
|
||||||
private TabLayout tabLayout;
|
private TabLayout tabLayout;
|
||||||
private ViewPager viewPager;
|
private ViewPager viewPager;
|
||||||
|
private SharedPreferences defPrefs;
|
||||||
|
|
||||||
private int notificationTabPosition;
|
private int notificationTabPosition;
|
||||||
private MainPagerAdapter adapter;
|
private MainPagerAdapter adapter;
|
||||||
|
@ -204,12 +207,14 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
||||||
|
|
||||||
setupTabs(showNotificationTab);
|
setupTabs(showNotificationTab);
|
||||||
|
|
||||||
|
defPrefs = PreferenceManager.getDefaultSharedPreferences(this);
|
||||||
|
|
||||||
int pageMargin = getResources().getDimensionPixelSize(R.dimen.tab_page_margin);
|
int pageMargin = getResources().getDimensionPixelSize(R.dimen.tab_page_margin);
|
||||||
viewPager.setPageMargin(pageMargin);
|
viewPager.setPageMargin(pageMargin);
|
||||||
Drawable pageMarginDrawable = ThemeUtils.getDrawable(this, R.attr.tab_page_margin_drawable,
|
Drawable pageMarginDrawable = ThemeUtils.getDrawable(this, R.attr.tab_page_margin_drawable,
|
||||||
R.drawable.tab_page_margin_dark);
|
R.drawable.tab_page_margin_dark);
|
||||||
viewPager.setPageMarginDrawable(pageMarginDrawable);
|
viewPager.setPageMarginDrawable(pageMarginDrawable);
|
||||||
if(PreferenceManager.getDefaultSharedPreferences(this).getBoolean("viewPagerOffScreenLimit", false)) {
|
if (defPrefs.getBoolean("viewPagerOffScreenLimit", false)) {
|
||||||
viewPager.setOffscreenPageLimit(9);
|
viewPager.setOffscreenPageLimit(9);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -270,6 +275,28 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
startStreaming();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startStreaming() {
|
||||||
|
if (defPrefs.getBoolean("useHTLStream", false)) {
|
||||||
|
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
stopStreaming();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopStreaming() {
|
||||||
|
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
if (drawer != null && drawer.isDrawerOpen()) {
|
if (drawer != null && drawer.isDrawerOpen()) {
|
||||||
|
@ -538,6 +565,8 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
||||||
SFragment.flushFilters();
|
SFragment.flushFilters();
|
||||||
accountManager.setActiveAccount(newSelectedId);
|
accountManager.setActiveAccount(newSelectedId);
|
||||||
|
|
||||||
|
stopStreaming();
|
||||||
|
|
||||||
Intent intent = new Intent(this, MainActivity.class);
|
Intent intent = new Intent(this, MainActivity.class);
|
||||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||||
if (forward != null) {
|
if (forward != null) {
|
||||||
|
|
|
@ -17,4 +17,5 @@ data class PreferenceChangedEvent(val preferenceKey: String) : Dispatchable
|
||||||
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable
|
data class MainTabsChangedEvent(val newTabs: List<TabData>) : Dispatchable
|
||||||
data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
|
data class PollVoteEvent(val statusId: String, val poll: Poll) : Dispatchable
|
||||||
data class DomainMuteEvent(val instance: String): Dispatchable
|
data class DomainMuteEvent(val instance: String): Dispatchable
|
||||||
data class QuickReplyEvent(val status: Status) : Dispatchable
|
data class QuickReplyEvent(val status: Status) : Dispatchable
|
||||||
|
data class StreamUpdateEvent(val status: Status, val first: Boolean) : Dispatchable
|
|
@ -0,0 +1,20 @@
|
||||||
|
package com.keylesspalace.tusky.entity
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
data class StreamEvent(
|
||||||
|
var event: EventType,
|
||||||
|
var payload: String
|
||||||
|
) {
|
||||||
|
|
||||||
|
enum class EventType(val num: Int) {
|
||||||
|
UNKNOWN(0),
|
||||||
|
@SerializedName("update")
|
||||||
|
UPDATE(1),
|
||||||
|
@SerializedName("notification")
|
||||||
|
NOTIFICATION(2),
|
||||||
|
@SerializedName("delete")
|
||||||
|
DELETE(3);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -45,7 +45,9 @@ import com.keylesspalace.tusky.appstore.QuickReplyEvent;
|
||||||
import com.keylesspalace.tusky.appstore.ReblogEvent;
|
import com.keylesspalace.tusky.appstore.ReblogEvent;
|
||||||
import com.keylesspalace.tusky.appstore.StatusComposedEvent;
|
import com.keylesspalace.tusky.appstore.StatusComposedEvent;
|
||||||
import com.keylesspalace.tusky.appstore.StatusDeletedEvent;
|
import com.keylesspalace.tusky.appstore.StatusDeletedEvent;
|
||||||
|
import com.keylesspalace.tusky.appstore.StreamUpdateEvent;
|
||||||
import com.keylesspalace.tusky.appstore.UnfollowEvent;
|
import com.keylesspalace.tusky.appstore.UnfollowEvent;
|
||||||
|
import com.keylesspalace.tusky.db.AccountEntity;
|
||||||
import com.keylesspalace.tusky.db.AccountManager;
|
import com.keylesspalace.tusky.db.AccountManager;
|
||||||
import com.keylesspalace.tusky.di.Injectable;
|
import com.keylesspalace.tusky.di.Injectable;
|
||||||
import com.keylesspalace.tusky.entity.Filter;
|
import com.keylesspalace.tusky.entity.Filter;
|
||||||
|
@ -71,8 +73,11 @@ import com.keylesspalace.tusky.view.BackgroundMessageView;
|
||||||
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
import com.keylesspalace.tusky.view.EndlessOnScrollListener;
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
|
|
||||||
|
import net.accelf.yuito.TimelineStreamingListener;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -104,6 +109,9 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
import kotlin.Unit;
|
import kotlin.Unit;
|
||||||
import kotlin.collections.CollectionsKt;
|
import kotlin.collections.CollectionsKt;
|
||||||
import kotlin.jvm.functions.Function1;
|
import kotlin.jvm.functions.Function1;
|
||||||
|
import okhttp3.OkHttpClient;
|
||||||
|
import okhttp3.Request;
|
||||||
|
import okhttp3.WebSocket;
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
@ -178,6 +186,8 @@ public class TimelineFragment extends SFragment implements
|
||||||
private boolean reduceTimelineRoading;
|
private boolean reduceTimelineRoading;
|
||||||
private boolean checkMobileNetwork;
|
private boolean checkMobileNetwork;
|
||||||
|
|
||||||
|
private WebSocket webSocket;
|
||||||
|
|
||||||
private PairedList<Either<Placeholder, Status>, StatusViewData> statuses =
|
private PairedList<Either<Placeholder, Status>, StatusViewData> statuses =
|
||||||
new PairedList<>(new Function<Either<Placeholder, Status>, StatusViewData>() {
|
new PairedList<>(new Function<Either<Placeholder, Status>, StatusViewData>() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -263,6 +273,56 @@ public class TimelineFragment extends SFragment implements
|
||||||
return rootView;
|
return rootView;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
startStreaming();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
stopStreaming();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startStreaming() {
|
||||||
|
if (preferences.getBoolean("useHTLStream", false) && kind == Kind.HOME) {
|
||||||
|
connectWebsocket(buildStreamingUrl());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildStreamingUrl() {
|
||||||
|
AccountEntity activeAccount = accountManager.getActiveAccount();
|
||||||
|
if (activeAccount != null) {
|
||||||
|
return "wss://" + activeAccount.getDomain() + "/api/v1/streaming/?" + "stream=user" + "&" + "access_token" + "=" + activeAccount.getAccessToken();
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void connectWebsocket(String endpoint) {
|
||||||
|
if (webSocket != null) {
|
||||||
|
stopStreaming();
|
||||||
|
}
|
||||||
|
|
||||||
|
Request request = new Request.Builder()
|
||||||
|
.url(endpoint)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
OkHttpClient client = new OkHttpClient.Builder()
|
||||||
|
.build();
|
||||||
|
|
||||||
|
webSocket = client.newWebSocket(request, new TimelineStreamingListener(eventHub));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopStreaming() {
|
||||||
|
if (webSocket == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
webSocket.close(1000, null);
|
||||||
|
webSocket = null;
|
||||||
|
}
|
||||||
|
|
||||||
private void sendInitialRequest() {
|
private void sendInitialRequest() {
|
||||||
if (this.kind == Kind.HOME) {
|
if (this.kind == Kind.HOME) {
|
||||||
this.tryCache();
|
this.tryCache();
|
||||||
|
@ -535,6 +595,10 @@ public class TimelineFragment extends SFragment implements
|
||||||
handleStatusComposeEvent(status);
|
handleStatusComposeEvent(status);
|
||||||
} else if (event instanceof PreferenceChangedEvent) {
|
} else if (event instanceof PreferenceChangedEvent) {
|
||||||
onPreferenceChanged(((PreferenceChangedEvent) event).getPreferenceKey());
|
onPreferenceChanged(((PreferenceChangedEvent) event).getPreferenceKey());
|
||||||
|
} else if (event instanceof StreamUpdateEvent) {
|
||||||
|
if (kind == Kind.HOME) {
|
||||||
|
handleStreamUpdateEvent((StreamUpdateEvent) event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
eventRegistered = true;
|
eventRegistered = true;
|
||||||
|
@ -1222,6 +1286,24 @@ public class TimelineFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addStatus(Either<Placeholder, Status> item) {
|
||||||
|
if (item.isRight()) {
|
||||||
|
Status status = item.asRight();
|
||||||
|
if (!((status.getInReplyToId() != null && filterRemoveReplies)
|
||||||
|
|| (status.getReblog() != null && filterRemoveReblogs)
|
||||||
|
|| shouldFilterStatus(status))) {
|
||||||
|
if (findStatusOrReblogPositionById(status.getId()) < 0) {
|
||||||
|
statuses.add(0, item);
|
||||||
|
updateAdapter();
|
||||||
|
timelineRepo.addSingleStatusToDb(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
statuses.add(0, item);
|
||||||
|
updateAdapter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* For certain requests we don't want to see placeholders, they will be removed some other way
|
* For certain requests we don't want to see placeholders, they will be removed some other way
|
||||||
*/
|
*/
|
||||||
|
@ -1308,6 +1390,9 @@ public class TimelineFragment extends SFragment implements
|
||||||
private void handleStatusComposeEvent(@NonNull Status status) {
|
private void handleStatusComposeEvent(@NonNull Status status) {
|
||||||
switch (kind) {
|
switch (kind) {
|
||||||
case HOME:
|
case HOME:
|
||||||
|
if (preferences.getBoolean("useHTLStream", false)){
|
||||||
|
return;
|
||||||
|
}
|
||||||
case PUBLIC_FEDERATED:
|
case PUBLIC_FEDERATED:
|
||||||
case PUBLIC_LOCAL:
|
case PUBLIC_LOCAL:
|
||||||
break;
|
break;
|
||||||
|
@ -1343,6 +1428,16 @@ public class TimelineFragment extends SFragment implements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleStreamUpdateEvent(StreamUpdateEvent event) {
|
||||||
|
Status status = event.getStatus();
|
||||||
|
if (event.getFirst() && statuses.get(0).isRight()) {
|
||||||
|
Placeholder placeholder = new Placeholder(statuses.get(0).asRight().getId() + 1);
|
||||||
|
updateStatuses(Arrays.asList(new Either.Right<>(status), new Either.Left<>(placeholder)), false);
|
||||||
|
} else {
|
||||||
|
addStatus(new Either.Right<>(status));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private List<Either<Placeholder, Status>> liftStatusList(List<Status> list) {
|
private List<Either<Placeholder, Status>> liftStatusList(List<Status> list) {
|
||||||
return CollectionsKt.map(list, statusLifter);
|
return CollectionsKt.map(list, statusLifter);
|
||||||
}
|
}
|
||||||
|
@ -1357,11 +1452,14 @@ public class TimelineFragment extends SFragment implements
|
||||||
if (isAdded()) {
|
if (isAdded()) {
|
||||||
adapter.notifyItemRangeInserted(position, count);
|
adapter.notifyItemRangeInserted(position, count);
|
||||||
Context context = getContext();
|
Context context = getContext();
|
||||||
if (position == 0 && context != null) {
|
if (position == 0 && context != null && layoutManager.findFirstVisibleItemPosition() == 0) {
|
||||||
if (isSwipeToRefreshEnabled)
|
if (count == 1) {
|
||||||
|
jumpToTop();
|
||||||
|
} else if (isSwipeToRefreshEnabled) {
|
||||||
recyclerView.scrollBy(0, Utils.dpToPx(context, -30));
|
recyclerView.scrollBy(0, Utils.dpToPx(context, -30));
|
||||||
else
|
} else {
|
||||||
recyclerView.scrollToPosition(0);
|
recyclerView.scrollToPosition(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,6 +30,7 @@ enum class TimelineRequestMode {
|
||||||
interface TimelineRepository {
|
interface TimelineRepository {
|
||||||
fun getStatuses(maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int,
|
fun getStatuses(maxId: String?, sinceId: String?, sincedIdMinusOne: String?, limit: Int,
|
||||||
requestMode: TimelineRequestMode): Single<out List<TimelineStatus>>
|
requestMode: TimelineRequestMode): Single<out List<TimelineStatus>>
|
||||||
|
fun addSingleStatusToDb(status: Status)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val CLEANUP_INTERVAL = TimeUnit.DAYS.toMillis(14)
|
val CLEANUP_INTERVAL = TimeUnit.DAYS.toMillis(14)
|
||||||
|
@ -61,6 +62,17 @@ class TimelineRepositoryImpl(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun addSingleStatusToDb(status: Status) {
|
||||||
|
val acc = accountManager.activeAccount ?: throw IllegalStateException()
|
||||||
|
val accountId = acc.id
|
||||||
|
|
||||||
|
timelineDao.insertInTransaction(
|
||||||
|
status.toEntity(accountId, htmlConverter, gson),
|
||||||
|
status.account.toEntity(accountId, gson),
|
||||||
|
status.reblog?.account?.toEntity(accountId, gson)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getStatusesFromNetwork(maxId: String?, sinceId: String?,
|
private fun getStatusesFromNetwork(maxId: String?, sinceId: String?,
|
||||||
sinceIdMinusOne: String?, limit: Int,
|
sinceIdMinusOne: String?, limit: Int,
|
||||||
accountId: Long, requestMode: TimelineRequestMode
|
accountId: Long, requestMode: TimelineRequestMode
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
package net.accelf.yuito;
|
||||||
|
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
import com.google.gson.GsonBuilder;
|
||||||
|
import com.keylesspalace.tusky.appstore.EventHub;
|
||||||
|
import com.keylesspalace.tusky.appstore.StatusDeletedEvent;
|
||||||
|
import com.keylesspalace.tusky.appstore.StreamUpdateEvent;
|
||||||
|
import com.keylesspalace.tusky.entity.Status;
|
||||||
|
import com.keylesspalace.tusky.entity.StreamEvent;
|
||||||
|
import com.keylesspalace.tusky.json.SpannedTypeAdapter;
|
||||||
|
|
||||||
|
import okhttp3.Response;
|
||||||
|
import okhttp3.WebSocket;
|
||||||
|
import okhttp3.WebSocketListener;
|
||||||
|
|
||||||
|
public class TimelineStreamingListener extends WebSocketListener {
|
||||||
|
|
||||||
|
private Gson gson = buildGson();
|
||||||
|
private boolean isFirstStatus = true;
|
||||||
|
|
||||||
|
private EventHub eventHub;
|
||||||
|
|
||||||
|
private static Gson buildGson() {
|
||||||
|
return new GsonBuilder()
|
||||||
|
.registerTypeAdapter(Spanned.class, new SpannedTypeAdapter())
|
||||||
|
.create();
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimelineStreamingListener(EventHub eventHub) {
|
||||||
|
this.eventHub = eventHub;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOpen(WebSocket webSocket, Response response) {
|
||||||
|
Log.d("StreamingListener", "Stream connected.");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onMessage(WebSocket webSocket, String text) {
|
||||||
|
StreamEvent event = gson.fromJson(text, StreamEvent.class);
|
||||||
|
|
||||||
|
String payload = event.getPayload();
|
||||||
|
switch (event.getEvent()) {
|
||||||
|
case UPDATE:
|
||||||
|
Status status = gson.fromJson(payload, Status.class);
|
||||||
|
eventHub.dispatch(new StreamUpdateEvent(status, isFirstStatus));
|
||||||
|
isFirstStatus = false;
|
||||||
|
break;
|
||||||
|
case DELETE:
|
||||||
|
eventHub.dispatch(new StatusDeletedEvent(payload));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onClosed(WebSocket webSocket, int code, String reason) {
|
||||||
|
Log.d("StreamingListener", "Stream closed.");
|
||||||
|
}
|
||||||
|
}
|
|
@ -175,6 +175,7 @@
|
||||||
<string name="pref_title_limited_bandwidth_mobile">モバイルネットワークでのみ有効</string>
|
<string name="pref_title_limited_bandwidth_mobile">モバイルネットワークでのみ有効</string>
|
||||||
<string name="pref_title_limited_bandwidth_timeline">トゥート後のタイムラインの自動更新をしない</string>
|
<string name="pref_title_limited_bandwidth_timeline">トゥート後のタイムラインの自動更新をしない</string>
|
||||||
<string name="pref_title_experimental_viewpager_offscreen">ViewPagerのoffscreenPageLimitを増やす</string>
|
<string name="pref_title_experimental_viewpager_offscreen">ViewPagerのoffscreenPageLimitを増やす</string>
|
||||||
|
<string name="pref_title_experimental_htl_streaming">HTLのストリーミングを利用する</string>
|
||||||
<string name="app_them_dark">ダーク</string>
|
<string name="app_them_dark">ダーク</string>
|
||||||
<string name="app_theme_light">ライト</string>
|
<string name="app_theme_light">ライト</string>
|
||||||
<string name="app_theme_black">ブラック</string>
|
<string name="app_theme_black">ブラック</string>
|
||||||
|
|
|
@ -223,6 +223,7 @@
|
||||||
<string name="pref_title_limited_bandwidth_timeline">Reduce timeline auto reload after toot</string>
|
<string name="pref_title_limited_bandwidth_timeline">Reduce timeline auto reload after toot</string>
|
||||||
<string name="pref_title_experimental">Experimental Settings</string>
|
<string name="pref_title_experimental">Experimental Settings</string>
|
||||||
<string name="pref_title_experimental_viewpager_offscreen">Increase offscreenPageLimit of ViewPager</string>
|
<string name="pref_title_experimental_viewpager_offscreen">Increase offscreenPageLimit of ViewPager</string>
|
||||||
|
<string name="pref_title_experimental_htl_streaming">Use home timeline streaming</string>
|
||||||
|
|
||||||
<string name="app_them_dark">Dark</string>
|
<string name="app_them_dark">Dark</string>
|
||||||
<string name="app_theme_light">Light</string>
|
<string name="app_theme_light">Light</string>
|
||||||
|
|
|
@ -119,5 +119,10 @@
|
||||||
android:defaultValue="false"
|
android:defaultValue="false"
|
||||||
android:key="viewPagerOffScreenLimit"
|
android:key="viewPagerOffScreenLimit"
|
||||||
android:title="@string/pref_title_experimental_viewpager_offscreen" />
|
android:title="@string/pref_title_experimental_viewpager_offscreen" />
|
||||||
|
|
||||||
|
<CheckBoxPreference
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="useHTLStream"
|
||||||
|
android:title="@string/pref_title_experimental_htl_streaming" />
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
</androidx.preference.PreferenceScreen>
|
</androidx.preference.PreferenceScreen>
|
||||||
|
|
Loading…
Reference in New Issue