Merge branch 'master' into feature_collapse_status
This commit is contained in:
commit
f66d689c57
|
@ -1,4 +1,4 @@
|
||||||
[![Translate - with Stringlate](https://img.shields.io/badge/translate%20with-stringlate-green.svg)](https://lonamiwebs.github.io/stringlate/translate?git=https%3A%2F%2Fgithub.com%2Ftuskyapp%2FTusky) [![Build Status](https://app.bitrise.io/app/55b2f0c77c4bba74/status.svg?token=elUl9fieM5K34iLRL0rpoA&branch=master)](https://app.bitrise.io/app/55b2f0c77c4bba74) [![CircleCI](https://circleci.com/gh/tuskyapp/Tusky.svg?style=svg)](https://circleci.com/gh/tuskyapp/Tusky)
|
[![Translate - with Stringlate](https://img.shields.io/badge/translate%20with-stringlate-green.svg)](https://lonamiwebs.github.io/stringlate/translate?git=https%3A%2F%2Fgithub.com%2Ftuskyapp%2FTusky) [![Build Status](https://app.bitrise.io/app/a3e773c3c57a894c/status.svg?token=qLu_Ti4Gp2LWcYT4eo2INQ&branch=master)](https://app.bitrise.io/app/a3e773c3c57a894c#/builds) [![CircleCI](https://circleci.com/gh/tuskyapp/Tusky.svg?style=svg)](https://circleci.com/gh/tuskyapp/Tusky)
|
||||||
# Tusky
|
# Tusky
|
||||||
|
|
||||||
![](/fastlane/metadata/android/en-US/images/icon.png)
|
![](/fastlane/metadata/android/en-US/images/icon.png)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
apply plugin: 'com.android.application'
|
apply plugin: 'com.android.application'
|
||||||
apply plugin: 'kotlin-kapt'
|
|
||||||
apply plugin: 'kotlin-android'
|
apply plugin: 'kotlin-android'
|
||||||
apply plugin: 'kotlin-android-extensions'
|
apply plugin: 'kotlin-android-extensions'
|
||||||
|
apply plugin: 'kotlin-kapt'
|
||||||
|
|
||||||
def getGitSha = { ->
|
def getGitSha = { ->
|
||||||
def stdout = new ByteArrayOutputStream()
|
def stdout = new ByteArrayOutputStream()
|
||||||
|
@ -19,8 +19,8 @@ android {
|
||||||
applicationId "com.keylesspalace.tusky"
|
applicationId "com.keylesspalace.tusky"
|
||||||
minSdkVersion 19
|
minSdkVersion 19
|
||||||
targetSdkVersion 27
|
targetSdkVersion 27
|
||||||
versionCode 48
|
versionCode 49
|
||||||
versionName "3.0"
|
versionName "3.1"
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables.useSupportLibrary true
|
vectorDrawables.useSupportLibrary true
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
ext.supportLibraryVersion = '27.1.1'
|
ext.supportLibraryVersion = '27.1.1'
|
||||||
ext.daggerVersion = '2.16'
|
ext.daggerVersion = '2.17'
|
||||||
|
|
||||||
|
|
||||||
// if libraries are changed here, they should also be changed in LicenseActivity
|
// if libraries are changed here, they should also be changed in LicenseActivity
|
||||||
|
@ -82,13 +82,13 @@ dependencies {
|
||||||
implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'
|
implementation 'com.squareup.okhttp3:logging-interceptor:3.11.0'
|
||||||
implementation 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0'
|
implementation 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0'
|
||||||
implementation 'com.github.connyduck:sparkbutton:1.0.1'
|
implementation 'com.github.connyduck:sparkbutton:1.0.1'
|
||||||
implementation 'com.github.chrisbanes:PhotoView:2.1.3'
|
implementation 'com.github.chrisbanes:PhotoView:2.1.4'
|
||||||
implementation 'com.mikepenz:google-material-typeface:3.0.1.2.original@aar'
|
implementation 'com.mikepenz:google-material-typeface:3.0.1.2.original@aar'
|
||||||
implementation('com.theartofdev.edmodo:android-image-cropper:2.7.0') {
|
implementation('com.theartofdev.edmodo:android-image-cropper:2.7.0') {
|
||||||
exclude group: 'com.android.support'
|
exclude group: 'com.android.support'
|
||||||
}
|
}
|
||||||
implementation 'com.evernote:android-job:1.2.6'
|
implementation 'com.evernote:android-job:1.2.6'
|
||||||
implementation 'com.android.support.constraint:constraint-layout:1.1.2'
|
implementation 'com.android.support.constraint:constraint-layout:1.1.3'
|
||||||
// EmojiCompat
|
// EmojiCompat
|
||||||
implementation "com.android.support:support-emoji:$supportLibraryVersion"
|
implementation "com.android.support:support-emoji:$supportLibraryVersion"
|
||||||
implementation "com.android.support:support-emoji-appcompat:$supportLibraryVersion"
|
implementation "com.android.support:support-emoji-appcompat:$supportLibraryVersion"
|
||||||
|
@ -111,8 +111,8 @@ dependencies {
|
||||||
exclude group: 'com.android.support', module: 'support-annotations'
|
exclude group: 'com.android.support', module: 'support-annotations'
|
||||||
})
|
})
|
||||||
debugImplementation 'im.dino:dbinspector:3.4.1@aar'
|
debugImplementation 'im.dino:dbinspector:3.4.1@aar'
|
||||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
|
implementation 'io.reactivex.rxjava2:rxjava:2.2.1'
|
||||||
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
|
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
|
||||||
implementation 'com.uber.autodispose:autodispose-android-archcomponents:0.8.0'
|
implementation 'com.uber.autodispose:autodispose-android-archcomponents:1.0.0-RC2'
|
||||||
implementation 'com.uber.autodispose:autodispose-kotlin:0.8.0'
|
implementation 'com.uber.autodispose:autodispose-ktx:1.0.0-RC2'
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ package com.keylesspalace.tusky;
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.ProgressDialog;
|
import android.app.ProgressDialog;
|
||||||
|
import android.arch.lifecycle.Lifecycle;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
|
@ -100,7 +101,6 @@ import com.keylesspalace.tusky.network.ProgressRequestBody;
|
||||||
import com.keylesspalace.tusky.service.SendTootService;
|
import com.keylesspalace.tusky.service.SendTootService;
|
||||||
import com.keylesspalace.tusky.util.CountUpDownLatch;
|
import com.keylesspalace.tusky.util.CountUpDownLatch;
|
||||||
import com.keylesspalace.tusky.util.DownsizeImageTask;
|
import com.keylesspalace.tusky.util.DownsizeImageTask;
|
||||||
import com.keylesspalace.tusky.util.IOUtils;
|
|
||||||
import com.keylesspalace.tusky.util.ListUtils;
|
import com.keylesspalace.tusky.util.ListUtils;
|
||||||
import com.keylesspalace.tusky.util.MediaUtils;
|
import com.keylesspalace.tusky.util.MediaUtils;
|
||||||
import com.keylesspalace.tusky.util.MentionTokenizer;
|
import com.keylesspalace.tusky.util.MentionTokenizer;
|
||||||
|
@ -123,7 +123,6 @@ import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
@ -135,12 +134,20 @@ import java.util.Locale;
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import at.connyduck.sparkbutton.helpers.Utils;
|
import at.connyduck.sparkbutton.helpers.Utils;
|
||||||
|
import io.reactivex.Single;
|
||||||
|
import io.reactivex.SingleObserver;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.disposables.Disposable;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
import okhttp3.MediaType;
|
import okhttp3.MediaType;
|
||||||
import okhttp3.MultipartBody;
|
import okhttp3.MultipartBody;
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
import retrofit2.Response;
|
import retrofit2.Response;
|
||||||
|
|
||||||
|
import static com.uber.autodispose.AutoDispose.autoDisposable;
|
||||||
|
import static com.uber.autodispose.android.lifecycle.AndroidLifecycleScopeProvider.from;
|
||||||
|
|
||||||
public final class ComposeActivity
|
public final class ComposeActivity
|
||||||
extends BaseActivity
|
extends BaseActivity
|
||||||
implements ComposeOptionsListener,
|
implements ComposeOptionsListener,
|
||||||
|
@ -150,8 +157,9 @@ public final class ComposeActivity
|
||||||
|
|
||||||
private static final String TAG = "ComposeActivity"; // logging tag
|
private static final String TAG = "ComposeActivity"; // logging tag
|
||||||
static final int STATUS_CHARACTER_LIMIT = 500;
|
static final int STATUS_CHARACTER_LIMIT = 500;
|
||||||
private static final int STATUS_MEDIA_SIZE_LIMIT = 8388608; // 8MiB
|
private static final int STATUS_IMAGE_SIZE_LIMIT = 8388608; // 8MiB
|
||||||
private static final int STATUS_MEDIA_PIXEL_SIZE_LIMIT = 16777216; // 4096^2 Pixels
|
private static final int STATUS_VIDEO_SIZE_LIMIT = 41943040; // 40MiB
|
||||||
|
private static final int STATUS_IMAGE_PIXEL_SIZE_LIMIT = 16777216; // 4096^2 Pixels
|
||||||
private static final int MEDIA_PICK_RESULT = 1;
|
private static final int MEDIA_PICK_RESULT = 1;
|
||||||
private static final int MEDIA_TAKE_PHOTO_RESULT = 2;
|
private static final int MEDIA_TAKE_PHOTO_RESULT = 2;
|
||||||
private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1;
|
private static final int PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1;
|
||||||
|
@ -986,8 +994,8 @@ public final class ComposeActivity
|
||||||
@NonNull
|
@NonNull
|
||||||
private File createNewImageFile() throws IOException {
|
private File createNewImageFile() throws IOException {
|
||||||
// Create an image file name
|
// Create an image file name
|
||||||
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date());
|
String randomId = StringUtils.randomAlphanumericString(12);
|
||||||
String imageFileName = "Tusky_" + timeStamp + "_";
|
String imageFileName = "Tusky_" + randomId + "_";
|
||||||
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
|
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
|
||||||
return File.createTempFile(
|
return File.createTempFile(
|
||||||
imageFileName, /* prefix */
|
imageFileName, /* prefix */
|
||||||
|
@ -1079,12 +1087,12 @@ public final class ComposeActivity
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (type == QueuedMedia.Type.IMAGE &&
|
if (type == QueuedMedia.Type.IMAGE &&
|
||||||
(mediaSize > STATUS_MEDIA_SIZE_LIMIT || MediaUtils.getImageSquarePixels(getContentResolver(), item.uri) > STATUS_MEDIA_PIXEL_SIZE_LIMIT)) {
|
(mediaSize > STATUS_IMAGE_SIZE_LIMIT || MediaUtils.getImageSquarePixels(getContentResolver(), item.uri) > STATUS_IMAGE_PIXEL_SIZE_LIMIT)) {
|
||||||
downsizeMedia(item);
|
downsizeMedia(item);
|
||||||
} else {
|
} else {
|
||||||
uploadMedia(item);
|
uploadMedia(item);
|
||||||
}
|
}
|
||||||
} catch (FileNotFoundException e) {
|
} catch (IOException e) {
|
||||||
onUploadFailure(item, false);
|
onUploadFailure(item, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1121,11 +1129,24 @@ public final class ComposeActivity
|
||||||
DisplayMetrics displayMetrics = new DisplayMetrics();
|
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||||
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
|
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
|
||||||
|
|
||||||
Picasso.with(this)
|
Single.fromCallable(() ->
|
||||||
.load(item.uri)
|
MediaUtils.getSampledBitmap(getContentResolver(), item.uri, displayMetrics.widthPixels, displayMetrics.heightPixels))
|
||||||
.resize(displayMetrics.widthPixels, displayMetrics.heightPixels)
|
.subscribeOn(Schedulers.computation())
|
||||||
.onlyScaleDown()
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.into(imageView);
|
.as(autoDisposable(from(this, Lifecycle.Event.ON_DESTROY)))
|
||||||
|
.subscribe(new SingleObserver<Bitmap>() {
|
||||||
|
@Override
|
||||||
|
public void onSubscribe(Disposable d) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSuccess(Bitmap bitmap) {
|
||||||
|
imageView.setImageBitmap(bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onError(Throwable e) { }
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
int margin = Utils.dpToPx(this, 4);
|
int margin = Utils.dpToPx(this, 4);
|
||||||
dialogLayout.addView(imageView);
|
dialogLayout.addView(imageView);
|
||||||
|
@ -1201,14 +1222,17 @@ public final class ComposeActivity
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void downsizeMedia(final QueuedMedia item) {
|
private void downsizeMedia(final QueuedMedia item) throws IOException {
|
||||||
item.readyStage = QueuedMedia.ReadyStage.DOWNSIZING;
|
item.readyStage = QueuedMedia.ReadyStage.DOWNSIZING;
|
||||||
|
|
||||||
new DownsizeImageTask(STATUS_MEDIA_SIZE_LIMIT, getContentResolver(),
|
new DownsizeImageTask(STATUS_IMAGE_SIZE_LIMIT, getContentResolver(), createNewImageFile(),
|
||||||
new DownsizeImageTask.Listener() {
|
new DownsizeImageTask.Listener() {
|
||||||
@Override
|
@Override
|
||||||
public void onSuccess(List<byte[]> contentList) {
|
public void onSuccess(File tempFile) {
|
||||||
item.content = contentList.get(0);
|
item.uri = FileProvider.getUriForFile(
|
||||||
|
ComposeActivity.this,
|
||||||
|
BuildConfig.APPLICATION_ID+".fileprovider",
|
||||||
|
tempFile);
|
||||||
uploadMedia(item);
|
uploadMedia(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1220,7 +1244,7 @@ public final class ComposeActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onMediaDownsizeFailure(QueuedMedia item) {
|
private void onMediaDownsizeFailure(QueuedMedia item) {
|
||||||
displayTransientError(R.string.error_media_upload_size);
|
displayTransientError(R.string.error_image_upload_size);
|
||||||
removeMediaFromQueue(item);
|
removeMediaFromQueue(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1236,32 +1260,20 @@ public final class ComposeActivity
|
||||||
StringUtils.randomAlphanumericString(10),
|
StringUtils.randomAlphanumericString(10),
|
||||||
fileExtension);
|
fileExtension);
|
||||||
|
|
||||||
byte[] content = item.content;
|
InputStream stream;
|
||||||
|
|
||||||
if (content == null) {
|
try {
|
||||||
InputStream stream;
|
stream = getContentResolver().openInputStream(item.uri);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
try {
|
Log.w(TAG, e);
|
||||||
stream = getContentResolver().openInputStream(item.uri);
|
return;
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
Log.d(TAG, Log.getStackTraceString(e));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
content = MediaUtils.inputStreamGetBytes(stream);
|
|
||||||
IOUtils.closeQuietly(stream);
|
|
||||||
|
|
||||||
if (content == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mimeType == null) mimeType = "multipart/form-data";
|
if (mimeType == null) mimeType = "multipart/form-data";
|
||||||
|
|
||||||
item.preview.setProgress(0);
|
item.preview.setProgress(0);
|
||||||
|
|
||||||
ProgressRequestBody fileBody = new ProgressRequestBody(content, MediaType.parse(mimeType),
|
ProgressRequestBody fileBody = new ProgressRequestBody(stream, MediaUtils.getMediaSize(getContentResolver(), item.uri), MediaType.parse(mimeType),
|
||||||
false, // If request body logging is enabled, pass true
|
|
||||||
new ProgressRequestBody.UploadCallback() { // may reference activity longer than I would like to
|
new ProgressRequestBody.UploadCallback() { // may reference activity longer than I would like to
|
||||||
int lastProgress = -1;
|
int lastProgress = -1;
|
||||||
|
|
||||||
|
@ -1356,8 +1368,8 @@ public final class ComposeActivity
|
||||||
String topLevelType = mimeType.substring(0, mimeType.indexOf('/'));
|
String topLevelType = mimeType.substring(0, mimeType.indexOf('/'));
|
||||||
switch (topLevelType) {
|
switch (topLevelType) {
|
||||||
case "video": {
|
case "video": {
|
||||||
if (mediaSize > STATUS_MEDIA_SIZE_LIMIT) {
|
if (mediaSize > STATUS_VIDEO_SIZE_LIMIT) {
|
||||||
displayTransientError(R.string.error_media_upload_size);
|
displayTransientError(R.string.error_image_upload_size);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mediaQueued.size() > 0
|
if (mediaQueued.size() > 0
|
||||||
|
@ -1521,7 +1533,6 @@ public final class ComposeActivity
|
||||||
String id;
|
String id;
|
||||||
Call<Attachment> uploadRequest;
|
Call<Attachment> uploadRequest;
|
||||||
ReadyStage readyStage;
|
ReadyStage readyStage;
|
||||||
byte[] content;
|
|
||||||
long mediaSize;
|
long mediaSize;
|
||||||
String description;
|
String description;
|
||||||
|
|
||||||
|
|
|
@ -53,8 +53,8 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val AVATAR_SIZE = 400
|
const val AVATAR_SIZE = 400
|
||||||
const val HEADER_WIDTH = 700
|
const val HEADER_WIDTH = 1500
|
||||||
const val HEADER_HEIGHT = 335
|
const val HEADER_HEIGHT = 500
|
||||||
|
|
||||||
private const val AVATAR_PICK_RESULT = 1
|
private const val AVATAR_PICK_RESULT = 1
|
||||||
private const val HEADER_PICK_RESULT = 2
|
private const val HEADER_PICK_RESULT = 2
|
||||||
|
|
|
@ -420,7 +420,7 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
||||||
|
|
||||||
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);
|
||||||
startActivityWithSlideInAnimation(intent);
|
startActivity(intent);
|
||||||
finishWithoutSlideOutAnimation();
|
finishWithoutSlideOutAnimation();
|
||||||
|
|
||||||
overridePendingTransition(R.anim.explode, R.anim.explode);
|
overridePendingTransition(R.anim.explode, R.anim.explode);
|
||||||
|
@ -510,21 +510,15 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
||||||
|
|
||||||
List<AccountEntity> allAccounts = accountManager.getAllAccountsOrderedByActive();
|
List<AccountEntity> allAccounts = accountManager.getAllAccountsOrderedByActive();
|
||||||
|
|
||||||
// reuse the already existing "add account" item
|
|
||||||
List<IProfile> profiles = new ArrayList<>(allAccounts.size()+1);
|
List<IProfile> profiles = new ArrayList<>(allAccounts.size()+1);
|
||||||
for (IProfile profile: headerResult.getProfiles()) {
|
|
||||||
if (profile.getIdentifier() == DRAWER_ITEM_ADD_ACCOUNT) {
|
|
||||||
profiles.add(profile);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (AccountEntity acc : allAccounts) {
|
for (AccountEntity acc : allAccounts) {
|
||||||
CharSequence emojifiedName = CustomEmojiHelper.emojifyString(acc.getDisplayName(), acc.getEmojis(), headerResult.getView());
|
CharSequence emojifiedName = CustomEmojiHelper.emojifyString(acc.getDisplayName(), acc.getEmojis(), headerResult.getView());
|
||||||
emojifiedName = EmojiCompat.get().process(emojifiedName);
|
emojifiedName = EmojiCompat.get().process(emojifiedName);
|
||||||
|
|
||||||
profiles.add(0,
|
profiles.add(
|
||||||
new ProfileDrawerItem()
|
new ProfileDrawerItem()
|
||||||
|
.withSetSelected(acc.isActive())
|
||||||
.withName(emojifiedName)
|
.withName(emojifiedName)
|
||||||
.withIcon(acc.getProfilePictureUrl())
|
.withIcon(acc.getProfilePictureUrl())
|
||||||
.withNameShown(true)
|
.withNameShown(true)
|
||||||
|
@ -532,6 +526,15 @@ public final class MainActivity extends BottomSheetActivity implements ActionBut
|
||||||
.withEmail(acc.getFullName()));
|
.withEmail(acc.getFullName()));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reuse the already existing "add account" item
|
||||||
|
for (IProfile profile: headerResult.getProfiles()) {
|
||||||
|
if (profile.getIdentifier() == DRAWER_ITEM_ADD_ACCOUNT) {
|
||||||
|
profiles.add(profile);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
headerResult.clear();
|
||||||
headerResult.setProfiles(profiles);
|
headerResult.setProfiles(profiles);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,7 +61,7 @@ public class PreferencesActivity extends BaseActivity
|
||||||
|
|
||||||
preferences.registerOnSharedPreferenceChangeListener(this);
|
preferences.registerOnSharedPreferenceChangeListener(this);
|
||||||
|
|
||||||
if(savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
currentPreferences = R.xml.preferences;
|
currentPreferences = R.xml.preferences;
|
||||||
currentTitle = R.string.action_view_preferences;
|
currentTitle = R.string.action_view_preferences;
|
||||||
} else {
|
} else {
|
||||||
|
@ -124,6 +124,10 @@ public class PreferencesActivity extends BaseActivity
|
||||||
restartActivitiesOnExit = true;
|
restartActivitiesOnExit = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "absoluteTimeView": {
|
||||||
|
restartActivitiesOnExit = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "notificationsEnabled": {
|
case "notificationsEnabled": {
|
||||||
boolean enabled = sharedPreferences.getBoolean("notificationsEnabled", true);
|
boolean enabled = sharedPreferences.getBoolean("notificationsEnabled", true);
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
@ -145,14 +149,14 @@ public class PreferencesActivity extends BaseActivity
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
//if we are not on the top level, show the top level. Else exit the activity
|
//if we are not on the top level, show the top level. Else exit the activity
|
||||||
if(currentPreferences != R.xml.preferences) {
|
if (currentPreferences != R.xml.preferences) {
|
||||||
showFragment(R.xml.preferences, R.string.action_view_preferences);
|
showFragment(R.xml.preferences, R.string.action_view_preferences);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
/* Switching themes won't actually change the theme of activities on the back stack.
|
/* Switching themes won't actually change the theme of activities on the back stack.
|
||||||
* Either the back stack activities need to all be recreated, or do the easier thing, which
|
* Either the back stack activities need to all be recreated, or do the easier thing, which
|
||||||
* is hijack the back button press and use it to launch a new MainActivity and clear the
|
* is hijack the back button press and use it to launch a new MainActivity and clear the
|
||||||
* back stack. */
|
* back stack. */
|
||||||
if (restartActivitiesOnExit) {
|
if (restartActivitiesOnExit) {
|
||||||
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);
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.adapter;
|
package com.keylesspalace.tusky.adapter;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
|
||||||
|
@ -26,55 +27,40 @@ import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public abstract class AccountAdapter extends RecyclerView.Adapter {
|
public abstract class AccountAdapter extends RecyclerView.Adapter {
|
||||||
|
static final int VIEW_TYPE_ACCOUNT = 0;
|
||||||
|
static final int VIEW_TYPE_FOOTER = 1;
|
||||||
|
|
||||||
|
|
||||||
List<Account> accountList;
|
List<Account> accountList;
|
||||||
AccountActionListener accountActionListener;
|
AccountActionListener accountActionListener;
|
||||||
FooterViewHolder.State footerState;
|
private boolean bottomLoading;
|
||||||
|
|
||||||
private String topId;
|
|
||||||
private String bottomId;
|
|
||||||
|
|
||||||
AccountAdapter(AccountActionListener accountActionListener) {
|
AccountAdapter(AccountActionListener accountActionListener) {
|
||||||
super();
|
this.accountList = new ArrayList<>();
|
||||||
accountList = new ArrayList<>();
|
|
||||||
this.accountActionListener = accountActionListener;
|
this.accountActionListener = accountActionListener;
|
||||||
footerState = FooterViewHolder.State.END;
|
bottomLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemCount() {
|
public int getItemCount() {
|
||||||
return accountList.size() + 1;
|
return accountList.size() + (bottomLoading ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void update(@Nullable List<Account> newAccounts, @Nullable String fromId,
|
@Override
|
||||||
@Nullable String uptoId) {
|
public int getItemViewType(int position) {
|
||||||
if (newAccounts == null || newAccounts.isEmpty()) {
|
if (position == accountList.size() && bottomLoading) {
|
||||||
return;
|
return VIEW_TYPE_FOOTER;
|
||||||
}
|
|
||||||
|
|
||||||
bottomId = fromId;
|
|
||||||
topId = uptoId;
|
|
||||||
|
|
||||||
if (accountList.isEmpty()) {
|
|
||||||
accountList = ListUtils.removeDuplicates(newAccounts);
|
|
||||||
} else {
|
} else {
|
||||||
int index = accountList.indexOf(newAccounts.get(newAccounts.size() - 1));
|
return VIEW_TYPE_ACCOUNT;
|
||||||
for (int i = 0; i < index; i++) {
|
|
||||||
accountList.remove(0);
|
|
||||||
}
|
|
||||||
int newIndex = newAccounts.indexOf(accountList.get(0));
|
|
||||||
if (newIndex == -1) {
|
|
||||||
accountList.addAll(0, newAccounts);
|
|
||||||
} else {
|
|
||||||
accountList.addAll(0, newAccounts.subList(0, newIndex));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void update(@NonNull List<Account> newAccounts) {
|
||||||
|
accountList = ListUtils.removeDuplicates(newAccounts);
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addItems(List<Account> newAccounts, @Nullable String fromId) {
|
public void addItems(List<Account> newAccounts) {
|
||||||
if (fromId != null) {
|
|
||||||
bottomId = fromId;
|
|
||||||
}
|
|
||||||
int end = accountList.size();
|
int end = accountList.size();
|
||||||
Account last = accountList.get(end - 1);
|
Account last = accountList.get(end - 1);
|
||||||
if (last != null && !findAccount(newAccounts, last.getId())) {
|
if (last != null && !findAccount(newAccounts, last.getId())) {
|
||||||
|
@ -83,6 +69,19 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setBottomLoading(boolean loading) {
|
||||||
|
boolean wasLoading = bottomLoading;
|
||||||
|
if(wasLoading == loading) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
bottomLoading = loading;
|
||||||
|
if(loading) {
|
||||||
|
notifyItemInserted(accountList.size());
|
||||||
|
} else {
|
||||||
|
notifyItemRemoved(accountList.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean findAccount(List<Account> accounts, String id) {
|
private static boolean findAccount(List<Account> accounts, String id) {
|
||||||
for (Account account : accounts) {
|
for (Account account : accounts) {
|
||||||
if (account.getId().equals(id)) {
|
if (account.getId().equals(id)) {
|
||||||
|
@ -110,25 +109,5 @@ public abstract class AccountAdapter extends RecyclerView.Adapter {
|
||||||
notifyItemInserted(position);
|
notifyItemInserted(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public Account getItem(int position) {
|
|
||||||
if (position >= 0 && position < accountList.size()) {
|
|
||||||
return accountList.get(position);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFooterState(FooterViewHolder.State newFooterState) {
|
|
||||||
footerState = newFooterState;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public String getBottomId() {
|
|
||||||
return bottomId;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
public String getTopId() {
|
|
||||||
return topId;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,8 +31,6 @@ import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||||
import com.squareup.picasso.Picasso;
|
import com.squareup.picasso.Picasso;
|
||||||
|
|
||||||
public class BlocksAdapter extends AccountAdapter {
|
public class BlocksAdapter extends AccountAdapter {
|
||||||
private static final int VIEW_TYPE_BLOCKED_USER = 0;
|
|
||||||
private static final int VIEW_TYPE_FOOTER = 1;
|
|
||||||
|
|
||||||
public BlocksAdapter(AccountActionListener accountActionListener) {
|
public BlocksAdapter(AccountActionListener accountActionListener) {
|
||||||
super(accountActionListener);
|
super(accountActionListener);
|
||||||
|
@ -43,7 +41,7 @@ public class BlocksAdapter extends AccountAdapter {
|
||||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
switch (viewType) {
|
switch (viewType) {
|
||||||
default:
|
default:
|
||||||
case VIEW_TYPE_BLOCKED_USER: {
|
case VIEW_TYPE_ACCOUNT: {
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.item_blocked_user, parent, false);
|
.inflate(R.layout.item_blocked_user, parent, false);
|
||||||
return new BlockedUserViewHolder(view);
|
return new BlockedUserViewHolder(view);
|
||||||
|
@ -51,29 +49,17 @@ public class BlocksAdapter extends AccountAdapter {
|
||||||
case VIEW_TYPE_FOOTER: {
|
case VIEW_TYPE_FOOTER: {
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.item_footer, parent, false);
|
.inflate(R.layout.item_footer, parent, false);
|
||||||
return new FooterViewHolder(view);
|
return new LoadingFooterViewHolder(view);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||||
if (position < accountList.size()) {
|
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
||||||
BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder;
|
BlockedUserViewHolder holder = (BlockedUserViewHolder) viewHolder;
|
||||||
holder.setupWithAccount(accountList.get(position));
|
holder.setupWithAccount(accountList.get(position));
|
||||||
holder.setupActionListener(accountActionListener);
|
holder.setupActionListener(accountActionListener);
|
||||||
} else {
|
|
||||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
|
||||||
holder.setState(footerState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemViewType(int position) {
|
|
||||||
if (position == accountList.size()) {
|
|
||||||
return VIEW_TYPE_FOOTER;
|
|
||||||
} else {
|
|
||||||
return VIEW_TYPE_BLOCKED_USER;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,8 +26,6 @@ import com.keylesspalace.tusky.interfaces.AccountActionListener;
|
||||||
|
|
||||||
/** Both for follows and following lists. */
|
/** Both for follows and following lists. */
|
||||||
public class FollowAdapter extends AccountAdapter {
|
public class FollowAdapter extends AccountAdapter {
|
||||||
private static final int VIEW_TYPE_ACCOUNT = 0;
|
|
||||||
private static final int VIEW_TYPE_FOOTER = 1;
|
|
||||||
|
|
||||||
public FollowAdapter(AccountActionListener accountActionListener) {
|
public FollowAdapter(AccountActionListener accountActionListener) {
|
||||||
super(accountActionListener);
|
super(accountActionListener);
|
||||||
|
@ -46,29 +44,18 @@ public class FollowAdapter extends AccountAdapter {
|
||||||
case VIEW_TYPE_FOOTER: {
|
case VIEW_TYPE_FOOTER: {
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.item_footer, parent, false);
|
.inflate(R.layout.item_footer, parent, false);
|
||||||
return new FooterViewHolder(view);
|
return new LoadingFooterViewHolder(view);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||||
if (position < accountList.size()) {
|
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
||||||
AccountViewHolder holder = (AccountViewHolder) viewHolder;
|
AccountViewHolder holder = (AccountViewHolder) viewHolder;
|
||||||
holder.setupWithAccount(accountList.get(position));
|
holder.setupWithAccount(accountList.get(position));
|
||||||
holder.setupActionListener(accountActionListener);
|
holder.setupActionListener(accountActionListener);
|
||||||
} else {
|
|
||||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
|
||||||
holder.setState(footerState);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemViewType(int position) {
|
|
||||||
if (position == accountList.size()) {
|
|
||||||
return VIEW_TYPE_FOOTER;
|
|
||||||
} else {
|
|
||||||
return VIEW_TYPE_ACCOUNT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,8 +31,6 @@ import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||||
import com.squareup.picasso.Picasso;
|
import com.squareup.picasso.Picasso;
|
||||||
|
|
||||||
public class FollowRequestsAdapter extends AccountAdapter {
|
public class FollowRequestsAdapter extends AccountAdapter {
|
||||||
private static final int VIEW_TYPE_FOLLOW_REQUEST = 0;
|
|
||||||
private static final int VIEW_TYPE_FOOTER = 1;
|
|
||||||
|
|
||||||
public FollowRequestsAdapter(AccountActionListener accountActionListener) {
|
public FollowRequestsAdapter(AccountActionListener accountActionListener) {
|
||||||
super(accountActionListener);
|
super(accountActionListener);
|
||||||
|
@ -43,7 +41,7 @@ public class FollowRequestsAdapter extends AccountAdapter {
|
||||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
switch (viewType) {
|
switch (viewType) {
|
||||||
default:
|
default:
|
||||||
case VIEW_TYPE_FOLLOW_REQUEST: {
|
case VIEW_TYPE_ACCOUNT: {
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.item_follow_request, parent, false);
|
.inflate(R.layout.item_follow_request, parent, false);
|
||||||
return new FollowRequestViewHolder(view);
|
return new FollowRequestViewHolder(view);
|
||||||
|
@ -51,29 +49,17 @@ public class FollowRequestsAdapter extends AccountAdapter {
|
||||||
case VIEW_TYPE_FOOTER: {
|
case VIEW_TYPE_FOOTER: {
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.item_footer, parent, false);
|
.inflate(R.layout.item_footer, parent, false);
|
||||||
return new FooterViewHolder(view);
|
return new LoadingFooterViewHolder(view);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||||
if (position < accountList.size()) {
|
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
||||||
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
|
FollowRequestViewHolder holder = (FollowRequestViewHolder) viewHolder;
|
||||||
holder.setupWithAccount(accountList.get(position));
|
holder.setupWithAccount(accountList.get(position));
|
||||||
holder.setupActionListener(accountActionListener);
|
holder.setupActionListener(accountActionListener);
|
||||||
} else {
|
|
||||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
|
||||||
holder.setState(footerState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemViewType(int position) {
|
|
||||||
if (position == accountList.size()) {
|
|
||||||
return VIEW_TYPE_FOOTER;
|
|
||||||
} else {
|
|
||||||
return VIEW_TYPE_FOLLOW_REQUEST;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
/* Copyright 2017 Andrew Dawson
|
|
||||||
*
|
|
||||||
* This file is a part of Tusky.
|
|
||||||
*
|
|
||||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
|
||||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
|
||||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
|
||||||
* Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License along with Tusky; if not,
|
|
||||||
* see <http://www.gnu.org/licenses>. */
|
|
||||||
|
|
||||||
package com.keylesspalace.tusky.adapter;
|
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable;
|
|
||||||
import android.support.v7.content.res.AppCompatResources;
|
|
||||||
import android.support.v7.widget.RecyclerView;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.support.v7.widget.RecyclerView.LayoutParams;
|
|
||||||
|
|
||||||
import com.keylesspalace.tusky.R;
|
|
||||||
|
|
||||||
public class FooterViewHolder extends RecyclerView.ViewHolder {
|
|
||||||
public enum State {
|
|
||||||
EMPTY,
|
|
||||||
END,
|
|
||||||
LOADING
|
|
||||||
}
|
|
||||||
|
|
||||||
private View container;
|
|
||||||
private ProgressBar progressBar;
|
|
||||||
private TextView endMessage;
|
|
||||||
|
|
||||||
FooterViewHolder(View itemView) {
|
|
||||||
super(itemView);
|
|
||||||
container = itemView.findViewById(R.id.footer_container);
|
|
||||||
progressBar = itemView.findViewById(R.id.footer_progress_bar);
|
|
||||||
endMessage = itemView.findViewById(R.id.footer_end_message);
|
|
||||||
Drawable top = AppCompatResources.getDrawable(itemView.getContext(),
|
|
||||||
R.drawable.elephant_friend_empty);
|
|
||||||
endMessage.setCompoundDrawablesWithIntrinsicBounds(null, top, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setState(State state) {
|
|
||||||
switch (state) {
|
|
||||||
case LOADING: {
|
|
||||||
RecyclerView.LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
|
|
||||||
LayoutParams.MATCH_PARENT);
|
|
||||||
container.setLayoutParams(layoutParams);
|
|
||||||
container.setVisibility(View.VISIBLE);
|
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
|
||||||
endMessage.setVisibility(View.GONE);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case END: {
|
|
||||||
RecyclerView.LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
|
|
||||||
LayoutParams.WRAP_CONTENT);
|
|
||||||
container.setLayoutParams(layoutParams);
|
|
||||||
container.setVisibility(View.GONE);
|
|
||||||
progressBar.setVisibility(View.GONE);
|
|
||||||
endMessage.setVisibility(View.GONE);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case EMPTY: {
|
|
||||||
RecyclerView.LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
|
|
||||||
LayoutParams.MATCH_PARENT);
|
|
||||||
container.setLayoutParams(layoutParams);
|
|
||||||
container.setVisibility(View.VISIBLE);
|
|
||||||
progressBar.setVisibility(View.GONE);
|
|
||||||
endMessage.setVisibility(View.VISIBLE);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,21 @@
|
||||||
|
/* Copyright 2018 Conny Duck
|
||||||
|
*
|
||||||
|
* 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.support.v7.widget.RecyclerView
|
||||||
|
import android.view.View
|
||||||
|
|
||||||
|
class LoadingFooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
|
@ -16,8 +16,6 @@ import com.keylesspalace.tusky.util.CustomEmojiHelper;
|
||||||
import com.squareup.picasso.Picasso;
|
import com.squareup.picasso.Picasso;
|
||||||
|
|
||||||
public class MutesAdapter extends AccountAdapter {
|
public class MutesAdapter extends AccountAdapter {
|
||||||
private static final int VIEW_TYPE_MUTED_USER = 0;
|
|
||||||
private static final int VIEW_TYPE_FOOTER = 1;
|
|
||||||
|
|
||||||
public MutesAdapter(AccountActionListener accountActionListener) {
|
public MutesAdapter(AccountActionListener accountActionListener) {
|
||||||
super(accountActionListener);
|
super(accountActionListener);
|
||||||
|
@ -28,7 +26,7 @@ public class MutesAdapter extends AccountAdapter {
|
||||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
switch (viewType) {
|
switch (viewType) {
|
||||||
default:
|
default:
|
||||||
case VIEW_TYPE_MUTED_USER: {
|
case VIEW_TYPE_ACCOUNT: {
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.item_muted_user, parent, false);
|
.inflate(R.layout.item_muted_user, parent, false);
|
||||||
return new MutesAdapter.MutedUserViewHolder(view);
|
return new MutesAdapter.MutedUserViewHolder(view);
|
||||||
|
@ -36,31 +34,20 @@ public class MutesAdapter extends AccountAdapter {
|
||||||
case VIEW_TYPE_FOOTER: {
|
case VIEW_TYPE_FOOTER: {
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.item_footer, parent, false);
|
.inflate(R.layout.item_footer, parent, false);
|
||||||
return new FooterViewHolder(view);
|
return new LoadingFooterViewHolder(view);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, int position) {
|
||||||
if (position < accountList.size()) {
|
if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) {
|
||||||
MutedUserViewHolder holder = (MutedUserViewHolder) viewHolder;
|
MutedUserViewHolder holder = (MutedUserViewHolder) viewHolder;
|
||||||
holder.setupWithAccount(accountList.get(position));
|
holder.setupWithAccount(accountList.get(position));
|
||||||
holder.setupActionListener(accountActionListener);
|
holder.setupActionListener(accountActionListener);
|
||||||
} else {
|
|
||||||
FooterViewHolder holder = (FooterViewHolder) viewHolder;
|
|
||||||
holder.setState(footerState);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemViewType(int position) {
|
|
||||||
if (position == accountList.size()) {
|
|
||||||
return VIEW_TYPE_FOOTER;
|
|
||||||
} else {
|
|
||||||
return VIEW_TYPE_MUTED_USER;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static class MutedUserViewHolder extends RecyclerView.ViewHolder {
|
static class MutedUserViewHolder extends RecyclerView.ViewHolder {
|
||||||
private ImageView avatar;
|
private ImageView avatar;
|
||||||
|
|
|
@ -52,21 +52,23 @@ import com.keylesspalace.tusky.viewdata.NotificationViewData;
|
||||||
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
import com.squareup.picasso.Picasso;
|
import com.squareup.picasso.Picasso;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
public class NotificationsAdapter extends RecyclerView.Adapter {
|
public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
private static final int VIEW_TYPE_MENTION = 0;
|
private static final int VIEW_TYPE_MENTION = 0;
|
||||||
private static final int VIEW_TYPE_FOOTER = 1;
|
private static final int VIEW_TYPE_STATUS_NOTIFICATION = 1;
|
||||||
private static final int VIEW_TYPE_STATUS_NOTIFICATION = 2;
|
private static final int VIEW_TYPE_FOLLOW = 2;
|
||||||
private static final int VIEW_TYPE_FOLLOW = 3;
|
private static final int VIEW_TYPE_PLACEHOLDER = 3;
|
||||||
private static final int VIEW_TYPE_PLACEHOLDER = 4;
|
|
||||||
|
|
||||||
private List<NotificationViewData> notifications;
|
private List<NotificationViewData> notifications;
|
||||||
private StatusActionListener statusListener;
|
private StatusActionListener statusListener;
|
||||||
private NotificationActionListener notificationActionListener;
|
private NotificationActionListener notificationActionListener;
|
||||||
private boolean mediaPreviewEnabled;
|
private boolean mediaPreviewEnabled;
|
||||||
|
private boolean useAbsoluteTime;
|
||||||
private BidiFormatter bidiFormatter;
|
private BidiFormatter bidiFormatter;
|
||||||
|
|
||||||
public NotificationsAdapter(StatusActionListener statusListener,
|
public NotificationsAdapter(StatusActionListener statusListener,
|
||||||
|
@ -76,6 +78,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
this.statusListener = statusListener;
|
this.statusListener = statusListener;
|
||||||
this.notificationActionListener = notificationActionListener;
|
this.notificationActionListener = notificationActionListener;
|
||||||
mediaPreviewEnabled = true;
|
mediaPreviewEnabled = true;
|
||||||
|
useAbsoluteTime = false;
|
||||||
bidiFormatter = BidiFormatter.getInstance();
|
bidiFormatter = BidiFormatter.getInstance();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,17 +90,12 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
case VIEW_TYPE_MENTION: {
|
case VIEW_TYPE_MENTION: {
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.item_status, parent, false);
|
.inflate(R.layout.item_status, parent, false);
|
||||||
return new StatusViewHolder(view);
|
return new StatusViewHolder(view, useAbsoluteTime);
|
||||||
}
|
|
||||||
case VIEW_TYPE_FOOTER: {
|
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
|
||||||
.inflate(R.layout.item_footer, parent, false);
|
|
||||||
return new FooterViewHolder(view);
|
|
||||||
}
|
}
|
||||||
case VIEW_TYPE_STATUS_NOTIFICATION: {
|
case VIEW_TYPE_STATUS_NOTIFICATION: {
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.item_status_notification, parent, false);
|
.inflate(R.layout.item_status_notification, parent, false);
|
||||||
return new StatusNotificationViewHolder(view);
|
return new StatusNotificationViewHolder(view, useAbsoluteTime);
|
||||||
}
|
}
|
||||||
case VIEW_TYPE_FOLLOW: {
|
case VIEW_TYPE_FOLLOW: {
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
|
@ -138,7 +136,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
StatusNotificationViewHolder holder = (StatusNotificationViewHolder) viewHolder;
|
StatusNotificationViewHolder holder = (StatusNotificationViewHolder) viewHolder;
|
||||||
StatusViewData.Concrete statusViewData = concreteNotificaton.getStatusViewData();
|
StatusViewData.Concrete statusViewData = concreteNotificaton.getStatusViewData();
|
||||||
|
|
||||||
if(statusViewData == null) {
|
if (statusViewData == null) {
|
||||||
holder.showNotificationContent(false);
|
holder.showNotificationContent(false);
|
||||||
} else {
|
} else {
|
||||||
holder.showNotificationContent(true);
|
holder.showNotificationContent(true);
|
||||||
|
@ -174,31 +172,28 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemViewType(int position) {
|
public int getItemViewType(int position) {
|
||||||
if (position == notifications.size()) {
|
NotificationViewData notification = notifications.get(position);
|
||||||
return VIEW_TYPE_FOOTER;
|
if (notification instanceof NotificationViewData.Concrete) {
|
||||||
} else {
|
NotificationViewData.Concrete concrete = ((NotificationViewData.Concrete) notification);
|
||||||
NotificationViewData notification = notifications.get(position);
|
switch (concrete.getType()) {
|
||||||
if (notification instanceof NotificationViewData.Concrete) {
|
default:
|
||||||
NotificationViewData.Concrete concrete = ((NotificationViewData.Concrete) notification);
|
case MENTION: {
|
||||||
switch (concrete.getType()) {
|
return VIEW_TYPE_MENTION;
|
||||||
default:
|
}
|
||||||
case MENTION: {
|
case FAVOURITE:
|
||||||
return VIEW_TYPE_MENTION;
|
case REBLOG: {
|
||||||
}
|
return VIEW_TYPE_STATUS_NOTIFICATION;
|
||||||
case FAVOURITE:
|
}
|
||||||
case REBLOG: {
|
case FOLLOW: {
|
||||||
return VIEW_TYPE_STATUS_NOTIFICATION;
|
return VIEW_TYPE_FOLLOW;
|
||||||
}
|
|
||||||
case FOLLOW: {
|
|
||||||
return VIEW_TYPE_FOLLOW;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (notification instanceof NotificationViewData.Placeholder) {
|
|
||||||
return VIEW_TYPE_PLACEHOLDER;
|
|
||||||
} else {
|
|
||||||
throw new AssertionError("Unknown notification type");
|
|
||||||
}
|
}
|
||||||
|
} else if (notification instanceof NotificationViewData.Placeholder) {
|
||||||
|
return VIEW_TYPE_PLACEHOLDER;
|
||||||
|
} else {
|
||||||
|
throw new AssertionError("Unknown notification type");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void update(@Nullable List<NotificationViewData> newNotifications) {
|
public void update(@Nullable List<NotificationViewData> newNotifications) {
|
||||||
|
@ -239,6 +234,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
return mediaPreviewEnabled;
|
return mediaPreviewEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setUseAbsoluteTime(boolean useAbsoluteTime) {
|
||||||
|
this.useAbsoluteTime = useAbsoluteTime;
|
||||||
|
}
|
||||||
|
|
||||||
public interface NotificationActionListener {
|
public interface NotificationActionListener {
|
||||||
void onViewAccount(String id);
|
void onViewAccount(String id);
|
||||||
|
|
||||||
|
@ -326,7 +325,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
private NotificationActionListener notificationActionListener;
|
private NotificationActionListener notificationActionListener;
|
||||||
private StatusViewData.Concrete statusViewData;
|
private StatusViewData.Concrete statusViewData;
|
||||||
|
|
||||||
StatusNotificationViewHolder(View itemView) {
|
private boolean useAbsoluteTime;
|
||||||
|
private SimpleDateFormat shortSdf;
|
||||||
|
private SimpleDateFormat longSdf;
|
||||||
|
|
||||||
|
StatusNotificationViewHolder(View itemView, boolean useAbsoluteTime) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
message = itemView.findViewById(R.id.notification_top_text);
|
message = itemView.findViewById(R.id.notification_top_text);
|
||||||
statusNameBar = itemView.findViewById(R.id.status_name_bar);
|
statusNameBar = itemView.findViewById(R.id.status_name_bar);
|
||||||
|
@ -349,6 +352,10 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
message.setOnClickListener(this);
|
message.setOnClickListener(this);
|
||||||
statusContent.setOnClickListener(this);
|
statusContent.setOnClickListener(this);
|
||||||
contentWarningButton.setOnCheckedChangeListener(this);
|
contentWarningButton.setOnCheckedChangeListener(this);
|
||||||
|
|
||||||
|
this.useAbsoluteTime = useAbsoluteTime;
|
||||||
|
shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
|
||||||
|
longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showNotificationContent(boolean show) {
|
private void showNotificationContent(boolean show) {
|
||||||
|
@ -372,26 +379,40 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
username.setText(usernameText);
|
username.setText(usernameText);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setCreatedAt(@Nullable Date createdAt) {
|
protected void setCreatedAt(@Nullable Date createdAt) {
|
||||||
// This is the visible timestampInfo.
|
if (useAbsoluteTime) {
|
||||||
String readout;
|
String time;
|
||||||
/* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m"
|
if (createdAt != null) {
|
||||||
* as 17 meters instead of minutes. */
|
if (System.currentTimeMillis() - createdAt.getTime() > 86400000L) {
|
||||||
CharSequence readoutAloud;
|
time = longSdf.format(createdAt);
|
||||||
if (createdAt != null) {
|
} else {
|
||||||
long then = createdAt.getTime();
|
time = shortSdf.format(createdAt);
|
||||||
long now = new Date().getTime();
|
}
|
||||||
readout = DateUtils.getRelativeTimeSpanString(timestampInfo.getContext(), then, now);
|
} else {
|
||||||
readoutAloud = android.text.format.DateUtils.getRelativeTimeSpanString(then, now,
|
time = "??:??:??";
|
||||||
android.text.format.DateUtils.SECOND_IN_MILLIS,
|
}
|
||||||
android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE);
|
timestampInfo.setText(time);
|
||||||
} else {
|
} else {
|
||||||
// unknown minutes~
|
// This is the visible timestampInfo.
|
||||||
readout = "?m";
|
String readout;
|
||||||
readoutAloud = "? minutes";
|
/* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m"
|
||||||
|
* as 17 meters instead of minutes. */
|
||||||
|
CharSequence readoutAloud;
|
||||||
|
if (createdAt != null) {
|
||||||
|
long then = createdAt.getTime();
|
||||||
|
long now = new Date().getTime();
|
||||||
|
readout = DateUtils.getRelativeTimeSpanString(timestampInfo.getContext(), then, now);
|
||||||
|
readoutAloud = android.text.format.DateUtils.getRelativeTimeSpanString(then, now,
|
||||||
|
android.text.format.DateUtils.SECOND_IN_MILLIS,
|
||||||
|
android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE);
|
||||||
|
} else {
|
||||||
|
// unknown minutes~
|
||||||
|
readout = "?m";
|
||||||
|
readoutAloud = "? minutes";
|
||||||
|
}
|
||||||
|
timestampInfo.setText(readout);
|
||||||
|
timestampInfo.setContentDescription(readoutAloud);
|
||||||
}
|
}
|
||||||
timestampInfo.setText(readout);
|
|
||||||
timestampInfo.setContentDescription(readoutAloud);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener, BidiFormatter bidiFormatter) {
|
void setMessage(NotificationViewData.Concrete notificationViewData, LinkListener listener, BidiFormatter bidiFormatter) {
|
||||||
|
@ -417,7 +438,7 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
}
|
}
|
||||||
case REBLOG: {
|
case REBLOG: {
|
||||||
icon = ContextCompat.getDrawable(context, R.drawable.ic_repeat_24dp);
|
icon = ContextCompat.getDrawable(context, R.drawable.ic_repeat_24dp);
|
||||||
if(icon != null) {
|
if (icon != null) {
|
||||||
icon.setColorFilter(ContextCompat.getColor(context,
|
icon.setColorFilter(ContextCompat.getColor(context,
|
||||||
R.color.color_accent_dark), PorterDuff.Mode.SRC_ATOP);
|
R.color.color_accent_dark), PorterDuff.Mode.SRC_ATOP);
|
||||||
}
|
}
|
||||||
|
@ -478,10 +499,12 @@ public class NotificationsAdapter extends RecyclerView.Adapter {
|
||||||
switch (v.getId()) {
|
switch (v.getId()) {
|
||||||
case R.id.notification_container:
|
case R.id.notification_container:
|
||||||
case R.id.notification_content:
|
case R.id.notification_content:
|
||||||
if (notificationActionListener != null) notificationActionListener.onViewStatusForNotificationId(notificationId);
|
if (notificationActionListener != null)
|
||||||
|
notificationActionListener.onViewStatusForNotificationId(notificationId);
|
||||||
break;
|
break;
|
||||||
case R.id.notification_top_text:
|
case R.id.notification_top_text:
|
||||||
if (notificationActionListener != null) notificationActionListener.onViewAccount(accountId);
|
if (notificationActionListener != null)
|
||||||
|
notificationActionListener.onViewAccount(accountId);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ public class SearchResultsAdapter extends RecyclerView.Adapter {
|
||||||
private boolean mediaPreviewsEnabled;
|
private boolean mediaPreviewsEnabled;
|
||||||
private boolean alwaysShowSensitiveMedia;
|
private boolean alwaysShowSensitiveMedia;
|
||||||
private boolean collapseLongStatusContent;
|
private boolean collapseLongStatusContent;
|
||||||
|
private boolean useAbsoluteTime;
|
||||||
|
|
||||||
private LinkListener linkListener;
|
private LinkListener linkListener;
|
||||||
private StatusActionListener statusListener;
|
private StatusActionListener statusListener;
|
||||||
|
@ -57,7 +58,8 @@ public class SearchResultsAdapter extends RecyclerView.Adapter {
|
||||||
boolean alwaysShowSensitiveMedia,
|
boolean alwaysShowSensitiveMedia,
|
||||||
boolean collapseLongStatusContent,
|
boolean collapseLongStatusContent,
|
||||||
LinkListener linkListener,
|
LinkListener linkListener,
|
||||||
StatusActionListener statusListener) {
|
StatusActionListener statusListener,
|
||||||
|
boolean useAbsoluteTime) {
|
||||||
|
|
||||||
this.accountList = Collections.emptyList();
|
this.accountList = Collections.emptyList();
|
||||||
this.statusList = Collections.emptyList();
|
this.statusList = Collections.emptyList();
|
||||||
|
@ -67,6 +69,7 @@ public class SearchResultsAdapter extends RecyclerView.Adapter {
|
||||||
this.mediaPreviewsEnabled = mediaPreviewsEnabled;
|
this.mediaPreviewsEnabled = mediaPreviewsEnabled;
|
||||||
this.alwaysShowSensitiveMedia = alwaysShowSensitiveMedia;
|
this.alwaysShowSensitiveMedia = alwaysShowSensitiveMedia;
|
||||||
this.collapseLongStatusContent = collapseLongStatusContent;
|
this.collapseLongStatusContent = collapseLongStatusContent;
|
||||||
|
this.useAbsoluteTime = useAbsoluteTime;
|
||||||
|
|
||||||
this.linkListener = linkListener;
|
this.linkListener = linkListener;
|
||||||
this.statusListener = statusListener;
|
this.statusListener = statusListener;
|
||||||
|
@ -91,7 +94,7 @@ public class SearchResultsAdapter extends RecyclerView.Adapter {
|
||||||
case VIEW_TYPE_STATUS: {
|
case VIEW_TYPE_STATUS: {
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.item_status, parent, false);
|
.inflate(R.layout.item_status, parent, false);
|
||||||
return new StatusViewHolder(view);
|
return new StatusViewHolder(view, useAbsoluteTime);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,8 +32,10 @@ import com.keylesspalace.tusky.viewdata.StatusViewData;
|
||||||
import com.mikepenz.iconics.utils.Utils;
|
import com.mikepenz.iconics.utils.Utils;
|
||||||
import com.squareup.picasso.Picasso;
|
import com.squareup.picasso.Picasso;
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import at.connyduck.sparkbutton.SparkButton;
|
import at.connyduck.sparkbutton.SparkButton;
|
||||||
import at.connyduck.sparkbutton.SparkEventListener;
|
import at.connyduck.sparkbutton.SparkEventListener;
|
||||||
|
@ -67,7 +69,11 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
TextView content;
|
TextView content;
|
||||||
TextView contentWarningDescription;
|
TextView contentWarningDescription;
|
||||||
|
|
||||||
StatusBaseViewHolder(View itemView) {
|
private boolean useAbsoluteTime;
|
||||||
|
private SimpleDateFormat shortSdf;
|
||||||
|
private SimpleDateFormat longSdf;
|
||||||
|
|
||||||
|
StatusBaseViewHolder(View itemView, boolean useAbsoluteTime) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
container = itemView.findViewById(R.id.status_container);
|
container = itemView.findViewById(R.id.status_container);
|
||||||
displayName = itemView.findViewById(R.id.status_display_name);
|
displayName = itemView.findViewById(R.id.status_display_name);
|
||||||
|
@ -95,6 +101,10 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
contentWarningDescription = itemView.findViewById(R.id.status_content_warning_description);
|
contentWarningDescription = itemView.findViewById(R.id.status_content_warning_description);
|
||||||
contentWarningButton = itemView.findViewById(R.id.status_content_warning_button);
|
contentWarningButton = itemView.findViewById(R.id.status_content_warning_button);
|
||||||
contentCollapseButton = itemView.findViewById(R.id.button_toggle_content);
|
contentCollapseButton = itemView.findViewById(R.id.button_toggle_content);
|
||||||
|
|
||||||
|
this.useAbsoluteTime = useAbsoluteTime;
|
||||||
|
shortSdf = new SimpleDateFormat("HH:mm:ss", Locale.getDefault());
|
||||||
|
longSdf = new SimpleDateFormat("MM/dd HH:mm:ss", Locale.getDefault());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract int getMediaPreviewHeight(Context context);
|
protected abstract int getMediaPreviewHeight(Context context);
|
||||||
|
@ -130,25 +140,39 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setCreatedAt(@Nullable Date createdAt) {
|
protected void setCreatedAt(@Nullable Date createdAt) {
|
||||||
// This is the visible timestampInfo.
|
if (useAbsoluteTime) {
|
||||||
String readout;
|
String time;
|
||||||
/* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m"
|
if (createdAt != null) {
|
||||||
* as 17 meters instead of minutes. */
|
if (System.currentTimeMillis() - createdAt.getTime() > 86400000L) {
|
||||||
CharSequence readoutAloud;
|
time = longSdf.format(createdAt);
|
||||||
if (createdAt != null) {
|
} else {
|
||||||
long then = createdAt.getTime();
|
time = shortSdf.format(createdAt);
|
||||||
long now = new Date().getTime();
|
}
|
||||||
readout = DateUtils.getRelativeTimeSpanString(timestampInfo.getContext(), then, now);
|
} else {
|
||||||
readoutAloud = android.text.format.DateUtils.getRelativeTimeSpanString(then, now,
|
time = "??:??:??";
|
||||||
android.text.format.DateUtils.SECOND_IN_MILLIS,
|
}
|
||||||
android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE);
|
timestampInfo.setText(time);
|
||||||
} else {
|
} else {
|
||||||
// unknown minutes~
|
// This is the visible timestampInfo.
|
||||||
readout = "?m";
|
String readout;
|
||||||
readoutAloud = "? minutes";
|
/* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m"
|
||||||
|
* as 17 meters instead of minutes. */
|
||||||
|
CharSequence readoutAloud;
|
||||||
|
if (createdAt != null) {
|
||||||
|
long then = createdAt.getTime();
|
||||||
|
long now = new Date().getTime();
|
||||||
|
readout = DateUtils.getRelativeTimeSpanString(timestampInfo.getContext(), then, now);
|
||||||
|
readoutAloud = android.text.format.DateUtils.getRelativeTimeSpanString(then, now,
|
||||||
|
android.text.format.DateUtils.SECOND_IN_MILLIS,
|
||||||
|
android.text.format.DateUtils.FORMAT_ABBREV_RELATIVE);
|
||||||
|
} else {
|
||||||
|
// unknown minutes~
|
||||||
|
readout = "?m";
|
||||||
|
readoutAloud = "? minutes";
|
||||||
|
}
|
||||||
|
timestampInfo.setText(readout);
|
||||||
|
timestampInfo.setContentDescription(readoutAloud);
|
||||||
}
|
}
|
||||||
timestampInfo.setText(readout);
|
|
||||||
timestampInfo.setContentDescription(readoutAloud);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void showContent(boolean show) {
|
protected void showContent(boolean show) {
|
||||||
|
@ -260,7 +284,7 @@ abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
final int urlIndex = i;
|
final int urlIndex = i;
|
||||||
previews[i].setOnClickListener(v -> {
|
previews[i].setOnClickListener(v -> {
|
||||||
if(getAdapterPosition() != RecyclerView.NO_POSITION) {
|
if (getAdapterPosition() != RecyclerView.NO_POSITION) {
|
||||||
listener.onViewMedia(getAdapterPosition(), urlIndex, v);
|
listener.onViewMedia(getAdapterPosition(), urlIndex, v);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -41,7 +41,7 @@ class StatusDetailedViewHolder extends StatusBaseViewHolder {
|
||||||
private TextView cardUrl;
|
private TextView cardUrl;
|
||||||
|
|
||||||
StatusDetailedViewHolder(View view) {
|
StatusDetailedViewHolder(View view) {
|
||||||
super(view);
|
super(view, false);
|
||||||
reblogs = view.findViewById(R.id.status_reblogs);
|
reblogs = view.findViewById(R.id.status_reblogs);
|
||||||
favourites = view.findViewById(R.id.status_favourites);
|
favourites = view.findViewById(R.id.status_favourites);
|
||||||
cardView = view.findViewById(R.id.card_view);
|
cardView = view.findViewById(R.id.card_view);
|
||||||
|
|
|
@ -34,8 +34,8 @@ public class StatusViewHolder extends StatusBaseViewHolder {
|
||||||
private ImageView avatarReblog;
|
private ImageView avatarReblog;
|
||||||
private TextView rebloggedBar;
|
private TextView rebloggedBar;
|
||||||
|
|
||||||
StatusViewHolder(View itemView) {
|
StatusViewHolder(View itemView, boolean useAbsoluteTime) {
|
||||||
super(itemView);
|
super(itemView, useAbsoluteTime);
|
||||||
avatarReblog = itemView.findViewById(R.id.status_avatar_reblog);
|
avatarReblog = itemView.findViewById(R.id.status_avatar_reblog);
|
||||||
rebloggedBar = itemView.findViewById(R.id.status_reblogged);
|
rebloggedBar = itemView.findViewById(R.id.status_reblogged);
|
||||||
//workaround because Android < API 21 does not support setting drawableLeft from xml when it is a vector image
|
//workaround because Android < API 21 does not support setting drawableLeft from xml when it is a vector image
|
||||||
|
|
|
@ -36,12 +36,14 @@ public class ThreadAdapter extends RecyclerView.Adapter {
|
||||||
private List<StatusViewData.Concrete> statuses;
|
private List<StatusViewData.Concrete> statuses;
|
||||||
private StatusActionListener statusActionListener;
|
private StatusActionListener statusActionListener;
|
||||||
private boolean mediaPreviewEnabled;
|
private boolean mediaPreviewEnabled;
|
||||||
|
private boolean useAbsoluteTime;
|
||||||
private int detailedStatusPosition;
|
private int detailedStatusPosition;
|
||||||
|
|
||||||
public ThreadAdapter(StatusActionListener listener) {
|
public ThreadAdapter(StatusActionListener listener) {
|
||||||
this.statusActionListener = listener;
|
this.statusActionListener = listener;
|
||||||
this.statuses = new ArrayList<>();
|
this.statuses = new ArrayList<>();
|
||||||
mediaPreviewEnabled = true;
|
mediaPreviewEnabled = true;
|
||||||
|
useAbsoluteTime = false;
|
||||||
detailedStatusPosition = RecyclerView.NO_POSITION;
|
detailedStatusPosition = RecyclerView.NO_POSITION;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +55,7 @@ public class ThreadAdapter extends RecyclerView.Adapter {
|
||||||
case VIEW_TYPE_STATUS: {
|
case VIEW_TYPE_STATUS: {
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.item_status, parent, false);
|
.inflate(R.layout.item_status, parent, false);
|
||||||
return new StatusViewHolder(view);
|
return new StatusViewHolder(view, useAbsoluteTime);
|
||||||
}
|
}
|
||||||
case VIEW_TYPE_STATUS_DETAILED: {
|
case VIEW_TYPE_STATUS_DETAILED: {
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
View view = LayoutInflater.from(parent.getContext())
|
||||||
|
@ -149,6 +151,10 @@ public class ThreadAdapter extends RecyclerView.Adapter {
|
||||||
mediaPreviewEnabled = enabled;
|
mediaPreviewEnabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setUseAbsoluteTime(boolean useAbsoluteTime) {
|
||||||
|
this.useAbsoluteTime = useAbsoluteTime;
|
||||||
|
}
|
||||||
|
|
||||||
public void setDetailedStatusPosition(int position) {
|
public void setDetailedStatusPosition(int position) {
|
||||||
if (position != detailedStatusPosition
|
if (position != detailedStatusPosition
|
||||||
&& detailedStatusPosition != RecyclerView.NO_POSITION) {
|
&& detailedStatusPosition != RecyclerView.NO_POSITION) {
|
||||||
|
|
|
@ -39,6 +39,7 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
|
||||||
private final AdapterDataSource<StatusViewData> dataSource;
|
private final AdapterDataSource<StatusViewData> dataSource;
|
||||||
private final StatusActionListener statusListener;
|
private final StatusActionListener statusListener;
|
||||||
private boolean mediaPreviewEnabled;
|
private boolean mediaPreviewEnabled;
|
||||||
|
private boolean useAbsoluteTime;
|
||||||
|
|
||||||
public TimelineAdapter(AdapterDataSource<StatusViewData> dataSource,
|
public TimelineAdapter(AdapterDataSource<StatusViewData> dataSource,
|
||||||
StatusActionListener statusListener) {
|
StatusActionListener statusListener) {
|
||||||
|
@ -46,6 +47,7 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
|
||||||
this.dataSource = dataSource;
|
this.dataSource = dataSource;
|
||||||
this.statusListener = statusListener;
|
this.statusListener = statusListener;
|
||||||
mediaPreviewEnabled = true;
|
mediaPreviewEnabled = true;
|
||||||
|
useAbsoluteTime = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -56,7 +58,7 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
|
||||||
case VIEW_TYPE_STATUS: {
|
case VIEW_TYPE_STATUS: {
|
||||||
View view = LayoutInflater.from(viewGroup.getContext())
|
View view = LayoutInflater.from(viewGroup.getContext())
|
||||||
.inflate(R.layout.item_status, viewGroup, false);
|
.inflate(R.layout.item_status, viewGroup, false);
|
||||||
return new StatusViewHolder(view);
|
return new StatusViewHolder(view, useAbsoluteTime);
|
||||||
}
|
}
|
||||||
case VIEW_TYPE_PLACEHOLDER: {
|
case VIEW_TYPE_PLACEHOLDER: {
|
||||||
View view = LayoutInflater.from(viewGroup.getContext())
|
View view = LayoutInflater.from(viewGroup.getContext())
|
||||||
|
@ -97,6 +99,10 @@ public final class TimelineAdapter extends RecyclerView.Adapter {
|
||||||
mediaPreviewEnabled = enabled;
|
mediaPreviewEnabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setUseAbsoluteTime(boolean useAbsoluteTime){
|
||||||
|
this.useAbsoluteTime=useAbsoluteTime;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean getMediaPreviewEnabled() {
|
public boolean getMediaPreviewEnabled() {
|
||||||
return mediaPreviewEnabled;
|
return mediaPreviewEnabled;
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ package com.keylesspalace.tusky.di
|
||||||
import com.keylesspalace.tusky.TuskyApplication
|
import com.keylesspalace.tusky.TuskyApplication
|
||||||
import dagger.BindsInstance
|
import dagger.BindsInstance
|
||||||
import dagger.Component
|
import dagger.Component
|
||||||
import dagger.android.AndroidInjectionModule
|
import dagger.android.support.AndroidSupportInjectionModule
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ import javax.inject.Singleton
|
||||||
@Component(modules = [
|
@Component(modules = [
|
||||||
AppModule::class,
|
AppModule::class,
|
||||||
NetworkModule::class,
|
NetworkModule::class,
|
||||||
AndroidInjectionModule::class,
|
AndroidSupportInjectionModule::class,
|
||||||
ActivitiesModule::class,
|
ActivitiesModule::class,
|
||||||
ServicesModule::class,
|
ServicesModule::class,
|
||||||
BroadcastReceiverModule::class,
|
BroadcastReceiverModule::class,
|
||||||
|
|
|
@ -36,7 +36,6 @@ import com.keylesspalace.tusky.adapter.AccountAdapter;
|
||||||
import com.keylesspalace.tusky.adapter.BlocksAdapter;
|
import com.keylesspalace.tusky.adapter.BlocksAdapter;
|
||||||
import com.keylesspalace.tusky.adapter.FollowAdapter;
|
import com.keylesspalace.tusky.adapter.FollowAdapter;
|
||||||
import com.keylesspalace.tusky.adapter.FollowRequestsAdapter;
|
import com.keylesspalace.tusky.adapter.FollowRequestsAdapter;
|
||||||
import com.keylesspalace.tusky.adapter.FooterViewHolder;
|
|
||||||
import com.keylesspalace.tusky.adapter.MutesAdapter;
|
import com.keylesspalace.tusky.adapter.MutesAdapter;
|
||||||
import com.keylesspalace.tusky.di.Injectable;
|
import com.keylesspalace.tusky.di.Injectable;
|
||||||
import com.keylesspalace.tusky.entity.Account;
|
import com.keylesspalace.tusky.entity.Account;
|
||||||
|
@ -79,10 +78,8 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||||
private RecyclerView recyclerView;
|
private RecyclerView recyclerView;
|
||||||
private EndlessOnScrollListener scrollListener;
|
private EndlessOnScrollListener scrollListener;
|
||||||
private AccountAdapter adapter;
|
private AccountAdapter adapter;
|
||||||
private boolean bottomLoading;
|
private boolean fetching = false;
|
||||||
private int bottomFetches;
|
private String bottomId;
|
||||||
private boolean topLoading;
|
|
||||||
private int topFetches;
|
|
||||||
|
|
||||||
public static AccountListFragment newInstance(Type type) {
|
public static AccountListFragment newInstance(Type type) {
|
||||||
Bundle arguments = new Bundle();
|
Bundle arguments = new Bundle();
|
||||||
|
@ -140,10 +137,6 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||||
}
|
}
|
||||||
recyclerView.setAdapter(adapter);
|
recyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
bottomLoading = false;
|
|
||||||
bottomFetches = 0;
|
|
||||||
topLoading = false;
|
|
||||||
topFetches = 0;
|
|
||||||
|
|
||||||
return rootView;
|
return rootView;
|
||||||
}
|
}
|
||||||
|
@ -155,13 +148,13 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||||
scrollListener = new EndlessOnScrollListener(layoutManager) {
|
scrollListener = new EndlessOnScrollListener(layoutManager) {
|
||||||
@Override
|
@Override
|
||||||
public void onLoadMore(int totalItemsCount, RecyclerView view) {
|
public void onLoadMore(int totalItemsCount, RecyclerView view) {
|
||||||
AccountListFragment.this.onLoadMore(view);
|
AccountListFragment.this.onLoadMore();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
recyclerView.addOnScrollListener(scrollListener);
|
recyclerView.addOnScrollListener(scrollListener);
|
||||||
|
|
||||||
fetchAccounts(null, null, FetchEnd.BOTTOM);
|
fetchAccounts(null);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,14 +169,6 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMute(final boolean mute, final String id, final int position) {
|
public void onMute(final boolean mute, final String id, final int position) {
|
||||||
if (api == null) {
|
|
||||||
/* If somehow an unmute button is clicked after onCreateView but before
|
|
||||||
* onActivityCreated, then this would get called with a null api object, so this eats
|
|
||||||
* that input. */
|
|
||||||
Log.d(TAG, "MastodonApi isn't initialised so this mute can't occur.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Callback<Relationship> callback = new Callback<Relationship>() {
|
Callback<Relationship> callback = new Callback<Relationship>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<Relationship> call, @NonNull Response<Relationship> response) {
|
public void onResponse(@NonNull Call<Relationship> call, @NonNull Response<Relationship> response) {
|
||||||
|
@ -237,14 +222,6 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBlock(final boolean block, final String id, final int position) {
|
public void onBlock(final boolean block, final String id, final int position) {
|
||||||
if (api == null) {
|
|
||||||
/* If somehow an unblock button is clicked after onCreateView but before
|
|
||||||
* onActivityCreated, then this would get called with a null api object, so this eats
|
|
||||||
* that input. */
|
|
||||||
Log.d(TAG, "MastodonApi isn't initialised so this block can't occur.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Callback<Relationship> cb = new Callback<Relationship>() {
|
Callback<Relationship> cb = new Callback<Relationship>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(@NonNull Call<Relationship> call, @NonNull Response<Relationship> response) {
|
public void onResponse(@NonNull Call<Relationship> call, @NonNull Response<Relationship> response) {
|
||||||
|
@ -299,13 +276,6 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||||
@Override
|
@Override
|
||||||
public void onRespondToFollowRequest(final boolean accept, final String accountId,
|
public void onRespondToFollowRequest(final boolean accept, final String accountId,
|
||||||
final int position) {
|
final int position) {
|
||||||
if (api == null) {
|
|
||||||
/* If somehow an response button is clicked after onCreateView but before
|
|
||||||
* onActivityCreated, then this would get called with a null api object, so this eats
|
|
||||||
* that input. */
|
|
||||||
Log.d(TAG, "MastodonApi isn't initialised, so follow requests can't be responded to.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Callback<Relationship> callback = new Callback<Relationship>() {
|
Callback<Relationship> callback = new Callback<Relationship>() {
|
||||||
@Override
|
@Override
|
||||||
|
@ -349,44 +319,30 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||||
Log.e(TAG, message);
|
Log.e(TAG, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private enum FetchEnd {
|
private Call<List<Account>> getFetchCallByListType(Type type, String fromId) {
|
||||||
TOP,
|
|
||||||
BOTTOM
|
|
||||||
}
|
|
||||||
|
|
||||||
private Call<List<Account>> getFetchCallByListType(Type type, String fromId, String uptoId) {
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
default:
|
default:
|
||||||
case FOLLOWS:
|
case FOLLOWS:
|
||||||
return api.accountFollowing(accountId, fromId, uptoId, null);
|
return api.accountFollowing(accountId, fromId, null, null);
|
||||||
case FOLLOWERS:
|
case FOLLOWERS:
|
||||||
return api.accountFollowers(accountId, fromId, uptoId, null);
|
return api.accountFollowers(accountId, fromId, null, null);
|
||||||
case BLOCKS:
|
case BLOCKS:
|
||||||
return api.blocks(fromId, uptoId, null);
|
return api.blocks(fromId, null, null);
|
||||||
case MUTES:
|
case MUTES:
|
||||||
return api.mutes(fromId, uptoId, null);
|
return api.mutes(fromId, null, null);
|
||||||
case FOLLOW_REQUESTS:
|
case FOLLOW_REQUESTS:
|
||||||
return api.followRequests(fromId, uptoId, null);
|
return api.followRequests(fromId, null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void fetchAccounts(String fromId, String uptoId, final FetchEnd fetchEnd) {
|
private void fetchAccounts(String id) {
|
||||||
/* If there is a fetch already ongoing, record however many fetches are requested and
|
if (fetching) {
|
||||||
* fulfill them after it's complete. */
|
|
||||||
if (fetchEnd == FetchEnd.TOP && topLoading) {
|
|
||||||
topFetches++;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (fetchEnd == FetchEnd.BOTTOM && bottomLoading) {
|
|
||||||
bottomFetches++;
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
fetching = true;
|
||||||
|
|
||||||
if (fromId != null || adapter.getItemCount() <= 1) {
|
if (id != null) {
|
||||||
/* When this is called by the EndlessScrollListener it cannot refresh the footer state
|
recyclerView.post(() -> adapter.setBottomLoading(true));
|
||||||
* using adapter.notifyItemChanged. So its necessary to postpone doing so until a
|
|
||||||
* convenient time for the UI thread using a Runnable. */
|
|
||||||
recyclerView.post(() -> adapter.setFooterState(FooterViewHolder.State.LOADING));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Callback<List<Account>> cb = new Callback<List<Account>>() {
|
Callback<List<Account>> cb = new Callback<List<Account>>() {
|
||||||
|
@ -394,99 +350,55 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
||||||
public void onResponse(@NonNull Call<List<Account>> call, @NonNull Response<List<Account>> response) {
|
public void onResponse(@NonNull Call<List<Account>> call, @NonNull Response<List<Account>> response) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
String linkHeader = response.headers().get("Link");
|
String linkHeader = response.headers().get("Link");
|
||||||
onFetchAccountsSuccess(response.body(), linkHeader, fetchEnd);
|
onFetchAccountsSuccess(response.body(), linkHeader);
|
||||||
} else {
|
} else {
|
||||||
onFetchAccountsFailure(new Exception(response.message()), fetchEnd);
|
onFetchAccountsFailure(new Exception(response.message()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(@NonNull Call<List<Account>> call, @NonNull Throwable t) {
|
public void onFailure(@NonNull Call<List<Account>> call, @NonNull Throwable t) {
|
||||||
onFetchAccountsFailure((Exception) t, fetchEnd);
|
onFetchAccountsFailure((Exception) t);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Call<List<Account>> listCall = getFetchCallByListType(type, fromId, uptoId);
|
Call<List<Account>> listCall = getFetchCallByListType(type, id);
|
||||||
callList.add(listCall);
|
callList.add(listCall);
|
||||||
listCall.enqueue(cb);
|
listCall.enqueue(cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onFetchAccountsSuccess(List<Account> accounts, String linkHeader,
|
private void onFetchAccountsSuccess(List<Account> accounts, String linkHeader) {
|
||||||
FetchEnd fetchEnd) {
|
adapter.setBottomLoading(false);
|
||||||
|
|
||||||
|
|
||||||
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
|
List<HttpHeaderLink> links = HttpHeaderLink.parse(linkHeader);
|
||||||
switch (fetchEnd) {
|
HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next");
|
||||||
case TOP: {
|
String fromId = null;
|
||||||
HttpHeaderLink previous = HttpHeaderLink.findByRelationType(links, "prev");
|
if (next != null) {
|
||||||
String uptoId = null;
|
fromId = next.uri.getQueryParameter("max_id");
|
||||||
if (previous != null) {
|
|
||||||
uptoId = previous.uri.getQueryParameter("since_id");
|
|
||||||
}
|
|
||||||
adapter.update(accounts, null, uptoId);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case BOTTOM: {
|
|
||||||
HttpHeaderLink next = HttpHeaderLink.findByRelationType(links, "next");
|
|
||||||
String fromId = null;
|
|
||||||
if (next != null) {
|
|
||||||
fromId = next.uri.getQueryParameter("max_id");
|
|
||||||
}
|
|
||||||
if (adapter.getItemCount() > 1) {
|
|
||||||
adapter.addItems(accounts, fromId);
|
|
||||||
} else {
|
|
||||||
/* If this is the first fetch, also save the id from the "previous" link and
|
|
||||||
* treat this operation as a refresh so the scroll position doesn't get pushed
|
|
||||||
* down to the end. */
|
|
||||||
HttpHeaderLink previous = HttpHeaderLink.findByRelationType(links, "prev");
|
|
||||||
String uptoId = null;
|
|
||||||
if (previous != null) {
|
|
||||||
uptoId = previous.uri.getQueryParameter("since_id");
|
|
||||||
}
|
|
||||||
adapter.update(accounts, fromId, uptoId);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
fulfillAnyQueuedFetches(fetchEnd);
|
if (adapter.getItemCount() > 1) {
|
||||||
if (accounts.size() == 0 && adapter.getItemCount() == 1) {
|
adapter.addItems(accounts);
|
||||||
adapter.setFooterState(FooterViewHolder.State.EMPTY);
|
|
||||||
} else {
|
} else {
|
||||||
adapter.setFooterState(FooterViewHolder.State.END);
|
adapter.update(accounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bottomId = fromId;
|
||||||
|
|
||||||
|
fetching = false;
|
||||||
|
|
||||||
|
adapter.setBottomLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onFetchAccountsFailure(Exception exception, FetchEnd fetchEnd) {
|
private void onFetchAccountsFailure(Exception exception) {
|
||||||
|
fetching = false;
|
||||||
Log.e(TAG, "Fetch failure: " + exception.getMessage());
|
Log.e(TAG, "Fetch failure: " + exception.getMessage());
|
||||||
fulfillAnyQueuedFetches(fetchEnd);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onRefresh() {
|
private void onLoadMore() {
|
||||||
fetchAccounts(null, adapter.getTopId(), FetchEnd.TOP);
|
if(bottomId == null) {
|
||||||
}
|
return;
|
||||||
|
|
||||||
private void onLoadMore(RecyclerView recyclerView) {
|
|
||||||
AccountAdapter adapter = (AccountAdapter) recyclerView.getAdapter();
|
|
||||||
//if we do not have a bottom id, we know we do not need to load more
|
|
||||||
if (adapter.getBottomId() == null) return;
|
|
||||||
fetchAccounts(adapter.getBottomId(), null, FetchEnd.BOTTOM);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fulfillAnyQueuedFetches(FetchEnd fetchEnd) {
|
|
||||||
switch (fetchEnd) {
|
|
||||||
case BOTTOM: {
|
|
||||||
bottomLoading = false;
|
|
||||||
if (bottomFetches > 0) {
|
|
||||||
bottomFetches--;
|
|
||||||
onLoadMore(recyclerView);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case TOP: {
|
|
||||||
topLoading = false;
|
|
||||||
if (topFetches > 0) {
|
|
||||||
topFetches--;
|
|
||||||
onRefresh();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
fetchAccounts(bottomId);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -202,6 +202,8 @@ public class NotificationsFragment extends SFragment implements
|
||||||
collapseLongStatusContent = preferences.getBoolean("collapseLongStatuses", true);
|
collapseLongStatusContent = preferences.getBoolean("collapseLongStatuses", true);
|
||||||
boolean mediaPreviewEnabled = preferences.getBoolean("mediaPreviewEnabled", true);
|
boolean mediaPreviewEnabled = preferences.getBoolean("mediaPreviewEnabled", true);
|
||||||
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
||||||
|
boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false);
|
||||||
|
adapter.setUseAbsoluteTime(useAbsoluteTime);
|
||||||
recyclerView.setAdapter(adapter);
|
recyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
notifications.clear();
|
notifications.clear();
|
||||||
|
@ -753,19 +755,23 @@ public class NotificationsFragment extends SFragment implements
|
||||||
private void saveNewestNotificationId(List<Notification> notifications) {
|
private void saveNewestNotificationId(List<Notification> notifications) {
|
||||||
|
|
||||||
AccountEntity account = accountManager.getActiveAccount();
|
AccountEntity account = accountManager.getActiveAccount();
|
||||||
BigInteger lastNoti = new BigInteger(account.getLastNotificationId());
|
if(account != null) {
|
||||||
|
BigInteger lastNoti = new BigInteger(account.getLastNotificationId());
|
||||||
|
|
||||||
for (Notification noti : notifications) {
|
for (Notification noti : notifications) {
|
||||||
BigInteger a = new BigInteger(noti.getId());
|
BigInteger a = new BigInteger(noti.getId());
|
||||||
if (isBiggerThan(a, lastNoti)) {
|
if (isBiggerThan(a, lastNoti)) {
|
||||||
lastNoti = a;
|
lastNoti = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String lastNotificationId = lastNoti.toString();
|
||||||
|
if(!account.getLastNotificationId().equals(lastNotificationId)) {
|
||||||
|
Log.d(TAG, "saving newest noti id: " + lastNotificationId);
|
||||||
|
account.setLastNotificationId(lastNotificationId);
|
||||||
|
accountManager.saveAccount(account);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(TAG, "saving newest noti id: " + lastNoti);
|
|
||||||
|
|
||||||
account.setLastNotificationId(lastNoti.toString());
|
|
||||||
accountManager.saveAccount(account);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isBiggerThan(BigInteger newId, BigInteger lastShownNotificationId) {
|
private boolean isBiggerThan(BigInteger newId, BigInteger lastShownNotificationId) {
|
||||||
|
|
|
@ -51,7 +51,7 @@ class SearchFragment : SFragment(), StatusActionListener, Injectable {
|
||||||
private var alwaysShowSensitiveMedia = false
|
private var alwaysShowSensitiveMedia = false
|
||||||
private var mediaPreviewEnabled = true
|
private var mediaPreviewEnabled = true
|
||||||
private var collapseLongStatusContent = true;
|
private var collapseLongStatusContent = true;
|
||||||
|
private var useAbsoluteTime = false
|
||||||
|
|
||||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||||
return inflater.inflate(R.layout.fragment_search, container, false)
|
return inflater.inflate(R.layout.fragment_search, container, false)
|
||||||
|
@ -61,6 +61,7 @@ class SearchFragment : SFragment(), StatusActionListener, Injectable {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(view.context)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(view.context)
|
||||||
alwaysShowSensitiveMedia = preferences.getBoolean("alwaysShowSensitiveMedia", false)
|
alwaysShowSensitiveMedia = preferences.getBoolean("alwaysShowSensitiveMedia", false)
|
||||||
mediaPreviewEnabled = preferences.getBoolean("mediaPreviewEnabled", true)
|
mediaPreviewEnabled = preferences.getBoolean("mediaPreviewEnabled", true)
|
||||||
|
useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false)
|
||||||
|
|
||||||
collapseLongStatusContent = preferences.getBoolean("collapseLongStatuses", true);
|
collapseLongStatusContent = preferences.getBoolean("collapseLongStatuses", true);
|
||||||
|
|
||||||
|
@ -71,8 +72,7 @@ class SearchFragment : SFragment(), StatusActionListener, Injectable {
|
||||||
alwaysShowSensitiveMedia,
|
alwaysShowSensitiveMedia,
|
||||||
collapseLongStatusContent,
|
collapseLongStatusContent,
|
||||||
this,
|
this,
|
||||||
this
|
useAbsoluteTime)
|
||||||
)
|
|
||||||
searchRecyclerView.adapter = searchAdapter
|
searchRecyclerView.adapter = searchAdapter
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -249,6 +249,8 @@ public class TimelineFragment extends SFragment implements
|
||||||
alwaysShowSensitiveMedia = preferences.getBoolean("alwaysShowSensitiveMedia", false);
|
alwaysShowSensitiveMedia = preferences.getBoolean("alwaysShowSensitiveMedia", false);
|
||||||
boolean mediaPreviewEnabled = preferences.getBoolean("mediaPreviewEnabled", true);
|
boolean mediaPreviewEnabled = preferences.getBoolean("mediaPreviewEnabled", true);
|
||||||
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
||||||
|
boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false);
|
||||||
|
adapter.setUseAbsoluteTime(useAbsoluteTime);
|
||||||
|
|
||||||
boolean filter = preferences.getBoolean("tabFilterHomeReplies", true);
|
boolean filter = preferences.getBoolean("tabFilterHomeReplies", true);
|
||||||
filterRemoveReplies = kind == Kind.HOME && !filter;
|
filterRemoveReplies = kind == Kind.HOME && !filter;
|
||||||
|
@ -306,6 +308,9 @@ public class TimelineFragment extends SFragment implements
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if(statuses.size() == 0) {
|
||||||
|
nothingMessageView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -635,7 +640,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
case "mediaPreviewEnabled": {
|
case "mediaPreviewEnabled": {
|
||||||
boolean enabled = sharedPreferences.getBoolean("mediaPreviewEnabled", true);
|
boolean enabled = sharedPreferences.getBoolean("mediaPreviewEnabled", true);
|
||||||
boolean oldMediaPreviewEnabled = adapter.getMediaPreviewEnabled();
|
boolean oldMediaPreviewEnabled = adapter.getMediaPreviewEnabled();
|
||||||
if(enabled != oldMediaPreviewEnabled) {
|
if (enabled != oldMediaPreviewEnabled) {
|
||||||
adapter.setMediaPreviewEnabled(enabled);
|
adapter.setMediaPreviewEnabled(enabled);
|
||||||
fullyRefresh();
|
fullyRefresh();
|
||||||
}
|
}
|
||||||
|
@ -858,11 +863,13 @@ public class TimelineFragment extends SFragment implements
|
||||||
swipeRefreshLayout.setRefreshing(false);
|
swipeRefreshLayout.setRefreshing(false);
|
||||||
if (this.statuses.size() == 0) {
|
if (this.statuses.size() == 0) {
|
||||||
nothingMessageView.setVisibility(View.VISIBLE);
|
nothingMessageView.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
nothingMessageView.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd, int position) {
|
private void onFetchTimelineFailure(Exception exception, FetchEnd fetchEnd, int position) {
|
||||||
if(isAdded()) {
|
if (isAdded()) {
|
||||||
swipeRefreshLayout.setRefreshing(false);
|
swipeRefreshLayout.setRefreshing(false);
|
||||||
|
|
||||||
if (fetchEnd == FetchEnd.MIDDLE && !statuses.get(position).isRight()) {
|
if (fetchEnd == FetchEnd.MIDDLE && !statuses.get(position).isRight()) {
|
||||||
|
@ -1084,7 +1091,7 @@ public class TimelineFragment extends SFragment implements
|
||||||
private final ListUpdateCallback listUpdateCallback = new ListUpdateCallback() {
|
private final ListUpdateCallback listUpdateCallback = new ListUpdateCallback() {
|
||||||
@Override
|
@Override
|
||||||
public void onInserted(int position, int count) {
|
public void onInserted(int position, int count) {
|
||||||
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) {
|
||||||
|
|
|
@ -163,6 +163,8 @@ public final class ViewThreadFragment extends SFragment implements
|
||||||
collapseLongStatusContent = preferences.getBoolean("collapseLongStatuses", true);
|
collapseLongStatusContent = preferences.getBoolean("collapseLongStatuses", true);
|
||||||
boolean mediaPreviewEnabled = preferences.getBoolean("mediaPreviewEnabled", true);
|
boolean mediaPreviewEnabled = preferences.getBoolean("mediaPreviewEnabled", true);
|
||||||
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
adapter.setMediaPreviewEnabled(mediaPreviewEnabled);
|
||||||
|
boolean useAbsoluteTime = preferences.getBoolean("absoluteTimeView", false);
|
||||||
|
adapter.setUseAbsoluteTime(useAbsoluteTime);
|
||||||
recyclerView.setAdapter(adapter);
|
recyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
statuses.clear();
|
statuses.clear();
|
||||||
|
|
|
@ -17,18 +17,18 @@ package com.keylesspalace.tusky.network;
|
||||||
|
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
|
||||||
import okhttp3.MediaType;
|
import okhttp3.MediaType;
|
||||||
import okhttp3.RequestBody;
|
import okhttp3.RequestBody;
|
||||||
import okio.BufferedSink;
|
import okio.BufferedSink;
|
||||||
|
|
||||||
public final class ProgressRequestBody extends RequestBody {
|
public final class ProgressRequestBody extends RequestBody {
|
||||||
private final byte[] content;
|
private final InputStream content;
|
||||||
private final UploadCallback mListener;
|
private final long contentLength;
|
||||||
|
private final UploadCallback uploadListener;
|
||||||
private final MediaType mediaType;
|
private final MediaType mediaType;
|
||||||
private boolean shouldIgnoreThisPass;
|
|
||||||
|
|
||||||
private static final int DEFAULT_BUFFER_SIZE = 2048;
|
private static final int DEFAULT_BUFFER_SIZE = 2048;
|
||||||
|
|
||||||
|
@ -36,11 +36,11 @@ public final class ProgressRequestBody extends RequestBody {
|
||||||
void onProgressUpdate(int percentage);
|
void onProgressUpdate(int percentage);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProgressRequestBody(final byte[] content, final MediaType mediaType, boolean shouldIgnoreFirst, final UploadCallback listener) {
|
public ProgressRequestBody(final InputStream content, long contentLength, final MediaType mediaType, final UploadCallback listener) {
|
||||||
this.content = content;
|
this.content = content;
|
||||||
|
this.contentLength = contentLength;
|
||||||
this.mediaType = mediaType;
|
this.mediaType = mediaType;
|
||||||
mListener = listener;
|
this.uploadListener = listener;
|
||||||
shouldIgnoreThisPass = shouldIgnoreFirst;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -50,29 +50,25 @@ public final class ProgressRequestBody extends RequestBody {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long contentLength() {
|
public long contentLength() {
|
||||||
return content.length;
|
return contentLength;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeTo(@NonNull BufferedSink sink) throws IOException {
|
public void writeTo(@NonNull BufferedSink sink) throws IOException {
|
||||||
long length = content.length;
|
|
||||||
|
|
||||||
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
|
byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
|
||||||
ByteArrayInputStream in = new ByteArrayInputStream(content);
|
|
||||||
long uploaded = 0;
|
long uploaded = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
int read;
|
int read;
|
||||||
while ((read = in.read(buffer)) != -1) {
|
while ((read = content.read(buffer)) != -1) {
|
||||||
if (!shouldIgnoreThisPass) {
|
uploadListener.onProgressUpdate((int)(100 * uploaded / contentLength));
|
||||||
mListener.onProgressUpdate((int)(100 * uploaded / length));
|
|
||||||
}
|
|
||||||
uploaded += read;
|
uploaded += read;
|
||||||
sink.write(buffer, 0, read);
|
sink.write(buffer, 0, read);
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
in.close();
|
content.close();
|
||||||
}
|
}
|
||||||
shouldIgnoreThisPass = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -18,122 +18,40 @@ package com.keylesspalace.tusky.util;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
import android.graphics.Matrix;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.media.ExifInterface;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.FileOutputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.io.OutputStream;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reduces the file size of images to fit under a given limit by resizing them, maintaining both
|
* Reduces the file size of images to fit under a given limit by resizing them, maintaining both
|
||||||
* aspect ratio and orientation.
|
* aspect ratio and orientation.
|
||||||
*/
|
*/
|
||||||
public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
||||||
private static final String TAG = "DownsizeImageTask";
|
|
||||||
private int sizeLimit;
|
private int sizeLimit;
|
||||||
private ContentResolver contentResolver;
|
private ContentResolver contentResolver;
|
||||||
private Listener listener;
|
private Listener listener;
|
||||||
private List<byte[]> resultList;
|
private File tempFile;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param sizeLimit the maximum number of bytes each image can take
|
* @param sizeLimit the maximum number of bytes each image can take
|
||||||
* @param contentResolver to resolve the specified images' URIs
|
* @param contentResolver to resolve the specified images' URIs
|
||||||
|
* @param tempFile the file where the result will be stored
|
||||||
* @param listener to whom the results are given
|
* @param listener to whom the results are given
|
||||||
*/
|
*/
|
||||||
public DownsizeImageTask(int sizeLimit, ContentResolver contentResolver, Listener listener) {
|
public DownsizeImageTask(int sizeLimit, ContentResolver contentResolver, File tempFile, Listener listener) {
|
||||||
this.sizeLimit = sizeLimit;
|
this.sizeLimit = sizeLimit;
|
||||||
this.contentResolver = contentResolver;
|
this.contentResolver = contentResolver;
|
||||||
|
this.tempFile = tempFile;
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private static Bitmap reorientBitmap(Bitmap bitmap, int orientation) {
|
|
||||||
Matrix matrix = new Matrix();
|
|
||||||
switch (orientation) {
|
|
||||||
default:
|
|
||||||
case ExifInterface.ORIENTATION_NORMAL: {
|
|
||||||
return bitmap;
|
|
||||||
}
|
|
||||||
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: {
|
|
||||||
matrix.setScale(-1, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ExifInterface.ORIENTATION_ROTATE_180: {
|
|
||||||
matrix.setRotate(180);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ExifInterface.ORIENTATION_FLIP_VERTICAL: {
|
|
||||||
matrix.setRotate(180);
|
|
||||||
matrix.postScale(-1, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ExifInterface.ORIENTATION_TRANSPOSE: {
|
|
||||||
matrix.setRotate(90);
|
|
||||||
matrix.postScale(-1, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ExifInterface.ORIENTATION_ROTATE_90: {
|
|
||||||
matrix.setRotate(90);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ExifInterface.ORIENTATION_TRANSVERSE: {
|
|
||||||
matrix.setRotate(-90);
|
|
||||||
matrix.postScale(-1, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case ExifInterface.ORIENTATION_ROTATE_270: {
|
|
||||||
matrix.setRotate(-90);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
|
|
||||||
bitmap.getHeight(), matrix, true);
|
|
||||||
if (!bitmap.sameAs(result)) {
|
|
||||||
bitmap.recycle();
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
} catch (OutOfMemoryError e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static int getOrientation(Uri uri, ContentResolver contentResolver) {
|
|
||||||
InputStream inputStream;
|
|
||||||
try {
|
|
||||||
inputStream = contentResolver.openInputStream(uri);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
Log.d(TAG, Log.getStackTraceString(e));
|
|
||||||
return ExifInterface.ORIENTATION_UNDEFINED;
|
|
||||||
}
|
|
||||||
if (inputStream == null) {
|
|
||||||
return ExifInterface.ORIENTATION_UNDEFINED;
|
|
||||||
}
|
|
||||||
ExifInterface exifInterface;
|
|
||||||
try {
|
|
||||||
exifInterface = new ExifInterface(inputStream);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.d(TAG, Log.getStackTraceString(e));
|
|
||||||
IOUtils.closeQuietly(inputStream);
|
|
||||||
return ExifInterface.ORIENTATION_UNDEFINED;
|
|
||||||
}
|
|
||||||
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION,
|
|
||||||
ExifInterface.ORIENTATION_NORMAL);
|
|
||||||
IOUtils.closeQuietly(inputStream);
|
|
||||||
return orientation;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Boolean doInBackground(Uri... uris) {
|
protected Boolean doInBackground(Uri... uris) {
|
||||||
resultList = new ArrayList<>();
|
|
||||||
for (Uri uri : uris) {
|
for (Uri uri : uris) {
|
||||||
InputStream inputStream;
|
InputStream inputStream;
|
||||||
try {
|
try {
|
||||||
|
@ -147,9 +65,7 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
||||||
BitmapFactory.decodeStream(inputStream, null, options);
|
BitmapFactory.decodeStream(inputStream, null, options);
|
||||||
IOUtils.closeQuietly(inputStream);
|
IOUtils.closeQuietly(inputStream);
|
||||||
// Get EXIF data, for orientation info.
|
// Get EXIF data, for orientation info.
|
||||||
int orientation = getOrientation(uri, contentResolver);
|
int orientation = MediaUtils.getImageOrientation(uri, contentResolver);
|
||||||
// Then use that information to determine how much to compress.
|
|
||||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
|
||||||
/* Unfortunately, there isn't a determined worst case compression ratio for image
|
/* Unfortunately, there isn't a determined worst case compression ratio for image
|
||||||
* formats. So, the only way to tell if they're too big is to compress them and
|
* formats. So, the only way to tell if they're too big is to compress them and
|
||||||
* test, and keep trying at smaller sizes. The initial estimate should be good for
|
* test, and keep trying at smaller sizes. The initial estimate should be good for
|
||||||
|
@ -157,7 +73,12 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
||||||
* sure it gets downsized to below the limit. */
|
* sure it gets downsized to below the limit. */
|
||||||
int scaledImageSize = 1024;
|
int scaledImageSize = 1024;
|
||||||
do {
|
do {
|
||||||
stream.reset();
|
OutputStream stream;
|
||||||
|
try {
|
||||||
|
stream = new FileOutputStream(tempFile);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
inputStream = contentResolver.openInputStream(uri);
|
inputStream = contentResolver.openInputStream(uri);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
|
@ -176,7 +97,7 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
||||||
if (scaledBitmap == null) {
|
if (scaledBitmap == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Bitmap reorientedBitmap = reorientBitmap(scaledBitmap, orientation);
|
Bitmap reorientedBitmap = MediaUtils.reorientBitmap(scaledBitmap, orientation);
|
||||||
if (reorientedBitmap == null) {
|
if (reorientedBitmap == null) {
|
||||||
scaledBitmap.recycle();
|
scaledBitmap.recycle();
|
||||||
return false;
|
return false;
|
||||||
|
@ -192,9 +113,8 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
||||||
reorientedBitmap.compress(format, 85, stream);
|
reorientedBitmap.compress(format, 85, stream);
|
||||||
reorientedBitmap.recycle();
|
reorientedBitmap.recycle();
|
||||||
scaledImageSize /= 2;
|
scaledImageSize /= 2;
|
||||||
} while (stream.size() > sizeLimit);
|
} while (tempFile.length() > sizeLimit);
|
||||||
|
|
||||||
resultList.add(stream.toByteArray());
|
|
||||||
if (isCancelled()) {
|
if (isCancelled()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -205,7 +125,7 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
||||||
@Override
|
@Override
|
||||||
protected void onPostExecute(Boolean successful) {
|
protected void onPostExecute(Boolean successful) {
|
||||||
if (successful) {
|
if (successful) {
|
||||||
listener.onSuccess(resultList);
|
listener.onSuccess(tempFile);
|
||||||
} else {
|
} else {
|
||||||
listener.onFailure();
|
listener.onFailure();
|
||||||
}
|
}
|
||||||
|
@ -214,7 +134,7 @@ public class DownsizeImageTask extends AsyncTask<Uri, Void, Boolean> {
|
||||||
|
|
||||||
/** Used to communicate the results of the task. */
|
/** Used to communicate the results of the task. */
|
||||||
public interface Listener {
|
public interface Listener {
|
||||||
void onSuccess(List<byte[]> contentList);
|
void onSuccess(File file);
|
||||||
void onFailure();
|
void onFailure();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -126,7 +126,7 @@ public class LinkHelper {
|
||||||
* @param context context
|
* @param context context
|
||||||
*/
|
*/
|
||||||
public static void openLink(String url, Context context) {
|
public static void openLink(String url, Context context) {
|
||||||
Uri uri = Uri.parse(url);
|
Uri uri = Uri.parse(url).normalizeScheme();
|
||||||
|
|
||||||
boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(context)
|
boolean useCustomTabs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
.getBoolean("customTabs", false);
|
.getBoolean("customTabs", false);
|
||||||
|
|
|
@ -20,6 +20,7 @@ import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.graphics.Matrix;
|
||||||
import android.media.MediaMetadataRetriever;
|
import android.media.MediaMetadataRetriever;
|
||||||
import android.media.ThumbnailUtils;
|
import android.media.ThumbnailUtils;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
@ -27,6 +28,7 @@ import android.provider.OpenableColumns;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.annotation.Px;
|
import android.support.annotation.Px;
|
||||||
|
import android.support.media.ExifInterface;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
@ -111,7 +113,9 @@ public class MediaUtils {
|
||||||
options.inJustDecodeBounds = false;
|
options.inJustDecodeBounds = false;
|
||||||
try {
|
try {
|
||||||
stream = contentResolver.openInputStream(uri);
|
stream = contentResolver.openInputStream(uri);
|
||||||
return BitmapFactory.decodeStream(stream, null, options);
|
Bitmap bitmap = BitmapFactory.decodeStream(stream, null, options);
|
||||||
|
int orientation = getImageOrientation(uri, contentResolver);
|
||||||
|
return reorientBitmap(bitmap, orientation);
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
return null;
|
return null;
|
||||||
|
@ -176,4 +180,81 @@ public class MediaUtils {
|
||||||
|
|
||||||
return inSampleSize;
|
return inSampleSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static Bitmap reorientBitmap(Bitmap bitmap, int orientation) {
|
||||||
|
Matrix matrix = new Matrix();
|
||||||
|
switch (orientation) {
|
||||||
|
default:
|
||||||
|
case ExifInterface.ORIENTATION_NORMAL: {
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL: {
|
||||||
|
matrix.setScale(-1, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_180: {
|
||||||
|
matrix.setRotate(180);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ExifInterface.ORIENTATION_FLIP_VERTICAL: {
|
||||||
|
matrix.setRotate(180);
|
||||||
|
matrix.postScale(-1, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ExifInterface.ORIENTATION_TRANSPOSE: {
|
||||||
|
matrix.setRotate(90);
|
||||||
|
matrix.postScale(-1, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_90: {
|
||||||
|
matrix.setRotate(90);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ExifInterface.ORIENTATION_TRANSVERSE: {
|
||||||
|
matrix.setRotate(-90);
|
||||||
|
matrix.postScale(-1, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_270: {
|
||||||
|
matrix.setRotate(-90);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Bitmap result = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
|
||||||
|
bitmap.getHeight(), matrix, true);
|
||||||
|
if (!bitmap.sameAs(result)) {
|
||||||
|
bitmap.recycle();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (OutOfMemoryError e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getImageOrientation(Uri uri, ContentResolver contentResolver) {
|
||||||
|
InputStream inputStream;
|
||||||
|
try {
|
||||||
|
inputStream = contentResolver.openInputStream(uri);
|
||||||
|
} catch (FileNotFoundException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
return ExifInterface.ORIENTATION_UNDEFINED;
|
||||||
|
}
|
||||||
|
if (inputStream == null) {
|
||||||
|
return ExifInterface.ORIENTATION_UNDEFINED;
|
||||||
|
}
|
||||||
|
ExifInterface exifInterface;
|
||||||
|
try {
|
||||||
|
exifInterface = new ExifInterface(inputStream);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.w(TAG, e);
|
||||||
|
IOUtils.closeQuietly(inputStream);
|
||||||
|
return ExifInterface.ORIENTATION_UNDEFINED;
|
||||||
|
}
|
||||||
|
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION,
|
||||||
|
ExifInterface.ORIENTATION_NORMAL);
|
||||||
|
IOUtils.closeQuietly(inputStream);
|
||||||
|
return orientation;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="@color/tab_page_margin_black" />
|
||||||
|
</shape>
|
|
@ -91,6 +91,26 @@
|
||||||
license:link="https://google.github.io/dagger/"
|
license:link="https://google.github.io/dagger/"
|
||||||
license:name="Dagger 2" />
|
license:name="Dagger 2" />
|
||||||
|
|
||||||
|
<com.keylesspalace.tusky.view.LicenseCard
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
license:license="@string/license_apache_2"
|
||||||
|
license:link="https://github.com/ReactiveX/RxJava"
|
||||||
|
license:name="RxJava" />
|
||||||
|
|
||||||
|
<com.keylesspalace.tusky.view.LicenseCard
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="12dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_marginTop="12dp"
|
||||||
|
license:license="@string/license_apache_2"
|
||||||
|
license:link="https://github.com/uber/AutoDispose"
|
||||||
|
license:name="AutoDispose" />
|
||||||
|
|
||||||
<com.keylesspalace.tusky.view.LicenseCard
|
<com.keylesspalace.tusky.view.LicenseCard
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
|
@ -1,24 +1,12 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/footer_container"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="72dp">
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/footer_progress_bar"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_centerInParent="true"
|
android:layout_gravity="center"
|
||||||
android:indeterminate="true" />
|
android:indeterminate="true" />
|
||||||
|
|
||||||
<TextView
|
</FrameLayout>
|
||||||
android:id="@+id/footer_end_message"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerInParent="true"
|
|
||||||
android:drawablePadding="32dp"
|
|
||||||
android:text="@string/footer_empty"
|
|
||||||
android:textAlignment="center"
|
|
||||||
android:textSize="?attr/status_text_medium" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="error_authorization_denied">تم رفض التصريح.</string>
|
<string name="error_authorization_denied">تم رفض التصريح.</string>
|
||||||
<string name="error_retrieving_oauth_token">فشل الحصول على رمز الدخول.</string>
|
<string name="error_retrieving_oauth_token">فشل الحصول على رمز الدخول.</string>
|
||||||
<string name="error_compose_character_limit">المنشور طويل جدا !</string>
|
<string name="error_compose_character_limit">المنشور طويل جدا !</string>
|
||||||
<string name="error_media_upload_size">يجب أن يكون حجم الملف أقل من 4 ميغابايت.</string>
|
<string name="error_image_upload_size">يجب أن يكون حجم الملف أقل من 4 ميغابايت.</string>
|
||||||
<string name="error_media_upload_type">لا يمكن رفع هذا النوع من الملفات.</string>
|
<string name="error_media_upload_type">لا يمكن رفع هذا النوع من الملفات.</string>
|
||||||
<string name="error_media_upload_opening">تعذر فتح ذاك الملف.</string>
|
<string name="error_media_upload_opening">تعذر فتح ذاك الملف.</string>
|
||||||
<string name="error_media_upload_permission">التصريح لازم لقراءة الوسائط</string>
|
<string name="error_media_upload_permission">التصريح لازم لقراءة الوسائط</string>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="error_authorization_denied">L\'autorització s\'ha denegat.</string>
|
<string name="error_authorization_denied">L\'autorització s\'ha denegat.</string>
|
||||||
<string name="error_retrieving_oauth_token">L\'obtenció del testimoni d\'inici de sessió ha fallat.</string>
|
<string name="error_retrieving_oauth_token">L\'obtenció del testimoni d\'inici de sessió ha fallat.</string>
|
||||||
<string name="error_compose_character_limit">L\'estat és massa llarg!</string>
|
<string name="error_compose_character_limit">L\'estat és massa llarg!</string>
|
||||||
<string name="error_media_upload_size">El fitxer ha de ser inferior a 8MB.</string>
|
<string name="error_image_upload_size">El fitxer ha de ser inferior a 8MB.</string>
|
||||||
<string name="error_media_upload_type">Aquest tipus de fitxer no es pot pujar.</string>
|
<string name="error_media_upload_type">Aquest tipus de fitxer no es pot pujar.</string>
|
||||||
<string name="error_media_upload_opening">Aquest tipus de fitxer no es pot obrir.</string>
|
<string name="error_media_upload_opening">Aquest tipus de fitxer no es pot obrir.</string>
|
||||||
<string name="error_media_upload_permission">Cal permís de lectura del mitjà.</string>
|
<string name="error_media_upload_permission">Cal permís de lectura del mitjà.</string>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="error_authorization_denied">Autorisierung fehlgeschlagen.</string>
|
<string name="error_authorization_denied">Autorisierung fehlgeschlagen.</string>
|
||||||
<string name="error_retrieving_oauth_token">Es konnte kein Login-Token abgerufen werden.</string>
|
<string name="error_retrieving_oauth_token">Es konnte kein Login-Token abgerufen werden.</string>
|
||||||
<string name="error_compose_character_limit">Der Beitrag ist zu lang!</string>
|
<string name="error_compose_character_limit">Der Beitrag ist zu lang!</string>
|
||||||
<string name="error_media_upload_size">Die Datei muss kleiner als 8MB sein.</string>
|
<string name="error_image_upload_size">Die Datei muss kleiner als 8MB sein.</string>
|
||||||
<string name="error_media_upload_type">Dieser Dateityp darf nicht hochgeladen werden.</string>
|
<string name="error_media_upload_type">Dieser Dateityp darf nicht hochgeladen werden.</string>
|
||||||
<string name="error_media_upload_opening">Die Datei konnte nicht geöffnet werden.</string>
|
<string name="error_media_upload_opening">Die Datei konnte nicht geöffnet werden.</string>
|
||||||
<string name="error_media_upload_permission">Eine Leseberechtigung wird für das Hochladen der Mediendatei benötigt.</string>
|
<string name="error_media_upload_permission">Eine Leseberechtigung wird für das Hochladen der Mediendatei benötigt.</string>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="error_authorization_denied">La autorización falló.</string>
|
<string name="error_authorization_denied">La autorización falló.</string>
|
||||||
<string name="error_retrieving_oauth_token">Fallo al obtener identificador de login.</string>
|
<string name="error_retrieving_oauth_token">Fallo al obtener identificador de login.</string>
|
||||||
<string name="error_compose_character_limit">¡El estado es demasiado largo!</string>
|
<string name="error_compose_character_limit">¡El estado es demasiado largo!</string>
|
||||||
<string name="error_media_upload_size">El archivo debe ser inferior a 8MB.</string>
|
<string name="error_image_upload_size">El archivo debe ser inferior a 8MB.</string>
|
||||||
<string name="error_media_upload_type">No se admite este tipo de archivo.</string>
|
<string name="error_media_upload_type">No se admite este tipo de archivo.</string>
|
||||||
<string name="error_media_upload_opening">No pudo abrirse el fichero.</string>
|
<string name="error_media_upload_opening">No pudo abrirse el fichero.</string>
|
||||||
<string name="error_media_upload_permission">Se requiere permiso para acceder al almacenamiento.</string>
|
<string name="error_media_upload_permission">Se requiere permiso para acceder al almacenamiento.</string>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="error_authorization_denied">Authentification refusée.</string>
|
<string name="error_authorization_denied">Authentification refusée.</string>
|
||||||
<string name="error_retrieving_oauth_token">Impossible de récupérer le jeton d’authentification.</string>
|
<string name="error_retrieving_oauth_token">Impossible de récupérer le jeton d’authentification.</string>
|
||||||
<string name="error_compose_character_limit">Votre pouet est trop long !</string>
|
<string name="error_compose_character_limit">Votre pouet est trop long !</string>
|
||||||
<string name="error_media_upload_size">Le fichier doit peser moins de 8 Mo.</string>
|
<string name="error_image_upload_size">Le fichier doit peser moins de 8 Mo.</string>
|
||||||
<string name="error_media_upload_type">Ce type de fichier n’est pas accepté.</string>
|
<string name="error_media_upload_type">Ce type de fichier n’est pas accepté.</string>
|
||||||
<string name="error_media_upload_opening">Le fichier ne peut pas être ouvert.</string>
|
<string name="error_media_upload_opening">Le fichier ne peut pas être ouvert.</string>
|
||||||
<string name="error_media_upload_permission">Permission requise pour lire ce média.</string>
|
<string name="error_media_upload_permission">Permission requise pour lire ce média.</string>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="error_authorization_denied">Engedélyezés letiltva.</string>
|
<string name="error_authorization_denied">Engedélyezés letiltva.</string>
|
||||||
<string name="error_retrieving_oauth_token">Bejelentkezési token megszerzése sikertelen.</string>
|
<string name="error_retrieving_oauth_token">Bejelentkezési token megszerzése sikertelen.</string>
|
||||||
<string name="error_compose_character_limit">Túl hosszú a tülkölés!</string>
|
<string name="error_compose_character_limit">Túl hosszú a tülkölés!</string>
|
||||||
<string name="error_media_upload_size">A fájl kisebb kell legyen mint 8MB.</string>
|
<string name="error_image_upload_size">A fájl kisebb kell legyen mint 8MB.</string>
|
||||||
<string name="error_media_upload_type">Fájl feltöltése sikertelen.</string>
|
<string name="error_media_upload_type">Fájl feltöltése sikertelen.</string>
|
||||||
<string name="error_media_upload_opening">Fájl megnyitása sikertelen.</string>
|
<string name="error_media_upload_opening">Fájl megnyitása sikertelen.</string>
|
||||||
<string name="error_media_upload_permission">Média olvasási engedély szükséges.</string>
|
<string name="error_media_upload_permission">Média olvasási engedély szükséges.</string>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="error_authorization_denied">L\'autorizzazione è stata negata.</string>
|
<string name="error_authorization_denied">L\'autorizzazione è stata negata.</string>
|
||||||
<string name="error_retrieving_oauth_token">Errore nell\'acquisizione del token di accesso.</string>
|
<string name="error_retrieving_oauth_token">Errore nell\'acquisizione del token di accesso.</string>
|
||||||
<string name="error_compose_character_limit">Lo stato è troppo lungo!</string>
|
<string name="error_compose_character_limit">Lo stato è troppo lungo!</string>
|
||||||
<string name="error_media_upload_size">La dimensione del file deve essere inferiore a 8MB.</string>
|
<string name="error_image_upload_size">La dimensione del file deve essere inferiore a 8MB.</string>
|
||||||
<string name="error_media_upload_type">Questo tipo di file non può essere caricato.</string>
|
<string name="error_media_upload_type">Questo tipo di file non può essere caricato.</string>
|
||||||
<string name="error_media_upload_opening">Questo file non può essere aperto.</string>
|
<string name="error_media_upload_opening">Questo file non può essere aperto.</string>
|
||||||
<string name="error_media_upload_permission">Il permesso di lettura della scheda sd è richiesto.</string>
|
<string name="error_media_upload_permission">Il permesso di lettura della scheda sd è richiesto.</string>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="error_authorization_denied">承認が拒否されました。</string>
|
<string name="error_authorization_denied">承認が拒否されました。</string>
|
||||||
<string name="error_retrieving_oauth_token">ログイントークンの取得に失敗しました。</string>
|
<string name="error_retrieving_oauth_token">ログイントークンの取得に失敗しました。</string>
|
||||||
<string name="error_compose_character_limit">投稿文が長すぎます!</string>
|
<string name="error_compose_character_limit">投稿文が長すぎます!</string>
|
||||||
<string name="error_media_upload_size">ファイルは4MB未満にしてください。</string>
|
<string name="error_image_upload_size">ファイルは4MB未満にしてください。</string>
|
||||||
<string name="error_media_upload_type">その形式のファイルはアップロードできません。</string>
|
<string name="error_media_upload_type">その形式のファイルはアップロードできません。</string>
|
||||||
<string name="error_media_upload_opening">ファイルを開けませんでした。</string>
|
<string name="error_media_upload_opening">ファイルを開けませんでした。</string>
|
||||||
<string name="error_media_upload_permission">メディアの読み取り許可が必要です。</string>
|
<string name="error_media_upload_permission">メディアの読み取り許可が必要です。</string>
|
||||||
|
@ -277,4 +277,6 @@
|
||||||
<string name="action_set_caption">説明を設定</string>
|
<string name="action_set_caption">説明を設定</string>
|
||||||
<string name="action_remove_media">消去</string>
|
<string name="action_remove_media">消去</string>
|
||||||
|
|
||||||
|
<string name="pref_title_absolute_time">絶対時間で表示</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<string name="error_authorization_denied">인증이 거부되었습니다.</string>
|
<string name="error_authorization_denied">인증이 거부되었습니다.</string>
|
||||||
<string name="error_retrieving_oauth_token">로그인 토큰을 가져오는 데 실패했습니다.</string>
|
<string name="error_retrieving_oauth_token">로그인 토큰을 가져오는 데 실패했습니다.</string>
|
||||||
<string name="error_compose_character_limit">툿이 너무 깁니다!</string>
|
<string name="error_compose_character_limit">툿이 너무 깁니다!</string>
|
||||||
<string name="error_media_upload_size">파일은 8MB보다 작아야 합니다.</string>
|
<string name="error_image_upload_size">파일은 8MB보다 작아야 합니다.</string>
|
||||||
<string name="error_media_upload_type">이 형태의 파일은 업로드될 수 없습니다.</string>
|
<string name="error_media_upload_type">이 형태의 파일은 업로드될 수 없습니다.</string>
|
||||||
<string name="error_media_upload_opening">그 파일은 열 수 없습니다.</string>
|
<string name="error_media_upload_opening">그 파일은 열 수 없습니다.</string>
|
||||||
<string name="error_media_upload_permission">미디어를 읽기 위한 권한이 필요합니다.</string>
|
<string name="error_media_upload_permission">미디어를 읽기 위한 권한이 필요합니다.</string>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="error_authorization_denied">Autorisatie werd geweigerd.</string>
|
<string name="error_authorization_denied">Autorisatie werd geweigerd.</string>
|
||||||
<string name="error_retrieving_oauth_token">Kon geen inlogsleutel verkrijgen.</string>
|
<string name="error_retrieving_oauth_token">Kon geen inlogsleutel verkrijgen.</string>
|
||||||
<string name="error_compose_character_limit">Tekst van deze toot is te lang!</string>
|
<string name="error_compose_character_limit">Tekst van deze toot is te lang!</string>
|
||||||
<string name="error_media_upload_size">Bestand moet kleiner zijn dan 8MB.</string>
|
<string name="error_image_upload_size">Bestand moet kleiner zijn dan 8MB.</string>
|
||||||
<string name="error_media_upload_type">Bestandstype kan niet worden geüpload.</string>
|
<string name="error_media_upload_type">Bestandstype kan niet worden geüpload.</string>
|
||||||
<string name="error_media_upload_opening">Bestand kon niet worden geopend.</string>
|
<string name="error_media_upload_opening">Bestand kon niet worden geopend.</string>
|
||||||
<string name="error_media_upload_permission">Er is toestemming nodig om deze media te lezen.</string>
|
<string name="error_media_upload_permission">Er is toestemming nodig om deze media te lezen.</string>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="error_authorization_denied">L\'autoritzacion es estada regetada.</string>
|
<string name="error_authorization_denied">L\'autoritzacion es estada regetada.</string>
|
||||||
<string name="error_retrieving_oauth_token">Fracàs de l’obtencion del testimoni d\'iniciacion de session.</string>
|
<string name="error_retrieving_oauth_token">Fracàs de l’obtencion del testimoni d\'iniciacion de session.</string>
|
||||||
<string name="error_compose_character_limit">L\'estatut es tròp long !</string>
|
<string name="error_compose_character_limit">L\'estatut es tròp long !</string>
|
||||||
<string name="error_media_upload_size">Lo fichièr a d’èsser inferior a 8Mo.</string>
|
<string name="error_image_upload_size">Lo fichièr a d’èsser inferior a 8Mo.</string>
|
||||||
<string name="error_media_upload_type">Aqueste tip de fichièr se pòt pas mandar.</string>
|
<string name="error_media_upload_type">Aqueste tip de fichièr se pòt pas mandar.</string>
|
||||||
<string name="error_media_upload_opening">Aqueste tip de fichièr se pòt pas dobrir.</string>
|
<string name="error_media_upload_opening">Aqueste tip de fichièr se pòt pas dobrir.</string>
|
||||||
<string name="error_media_upload_permission">Cal permís de lectura del mèdia.</string>
|
<string name="error_media_upload_permission">Cal permís de lectura del mèdia.</string>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="error_authorization_denied">Odmówiono autoryzacji.</string>
|
<string name="error_authorization_denied">Odmówiono autoryzacji.</string>
|
||||||
<string name="error_retrieving_oauth_token">Nie udało się uzyskać tokenu logowania.</string>
|
<string name="error_retrieving_oauth_token">Nie udało się uzyskać tokenu logowania.</string>
|
||||||
<string name="error_compose_character_limit">Zbyt długi wpis!</string>
|
<string name="error_compose_character_limit">Zbyt długi wpis!</string>
|
||||||
<string name="error_media_upload_size">Plik może mieć maksymalnie 8 MB.</string>
|
<string name="error_image_upload_size">Plik może mieć maksymalnie 8 MB.</string>
|
||||||
<string name="error_media_upload_type">Ten format pliku nie może zostać wysłany.</string>
|
<string name="error_media_upload_type">Ten format pliku nie może zostać wysłany.</string>
|
||||||
<string name="error_media_upload_opening">Nie można otworzyć tego pliku.</string>
|
<string name="error_media_upload_opening">Nie można otworzyć tego pliku.</string>
|
||||||
<string name="error_media_upload_permission">Wymagane jest pozwolenie na dostęp do plików z urządzenia.</string>
|
<string name="error_media_upload_permission">Wymagane jest pozwolenie na dostęp do plików z urządzenia.</string>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="error_authorization_denied">Autorização negada.</string>
|
<string name="error_authorization_denied">Autorização negada.</string>
|
||||||
<string name="error_retrieving_oauth_token">Falha ao adquirir token de entrada.</string>
|
<string name="error_retrieving_oauth_token">Falha ao adquirir token de entrada.</string>
|
||||||
<string name="error_compose_character_limit">A postagem é muito longa!</string>
|
<string name="error_compose_character_limit">A postagem é muito longa!</string>
|
||||||
<string name="error_media_upload_size">O arquivo deve ser menor que 8MB.</string>
|
<string name="error_image_upload_size">O arquivo deve ser menor que 8MB.</string>
|
||||||
<string name="error_media_upload_type">Esse tipo de arquivo não pode ser enviado.</string>
|
<string name="error_media_upload_type">Esse tipo de arquivo não pode ser enviado.</string>
|
||||||
<string name="error_media_upload_opening">Esse arquvo não pode ser aberto.</string>
|
<string name="error_media_upload_opening">Esse arquvo não pode ser aberto.</string>
|
||||||
<string name="error_media_upload_permission">Permissão para ler mídia é necessária.</string>
|
<string name="error_media_upload_permission">Permissão para ler mídia é necessária.</string>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="error_authorization_denied">Авторизация была отклонена.</string>
|
<string name="error_authorization_denied">Авторизация была отклонена.</string>
|
||||||
<string name="error_retrieving_oauth_token">Не удалось получить токен авторизации.</string>
|
<string name="error_retrieving_oauth_token">Не удалось получить токен авторизации.</string>
|
||||||
<string name="error_compose_character_limit">Статус слишком длинный!</string>
|
<string name="error_compose_character_limit">Статус слишком длинный!</string>
|
||||||
<string name="error_media_upload_size">Файл должен быть не больше 8 Мбайт.</string>
|
<string name="error_image_upload_size">Файл должен быть не больше 8 Мбайт.</string>
|
||||||
<string name="error_media_upload_type">Данный тип файла не может быть загружен.</string>
|
<string name="error_media_upload_type">Данный тип файла не может быть загружен.</string>
|
||||||
<string name="error_media_upload_opening">Файл не может быть открыт.</string>
|
<string name="error_media_upload_opening">Файл не может быть открыт.</string>
|
||||||
<string name="error_media_upload_permission">Необходимо разрешение на чтение медиаконтента.</string>
|
<string name="error_media_upload_permission">Необходимо разрешение на чтение медиаконтента.</string>
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
<string name="error_authorization_denied">Ingen behörighet.</string>
|
<string name="error_authorization_denied">Ingen behörighet.</string>
|
||||||
<string name="error_retrieving_oauth_token">Misslyckades med att få en inloggnings-token.</string>
|
<string name="error_retrieving_oauth_token">Misslyckades med att få en inloggnings-token.</string>
|
||||||
<string name="error_compose_character_limit">Statusen är för lång!</string>
|
<string name="error_compose_character_limit">Statusen är för lång!</string>
|
||||||
<string name="error_media_upload_size">Filen måste vara mindre än 8MB.</string>
|
<string name="error_image_upload_size">Filen måste vara mindre än 8MB.</string>
|
||||||
<string name="error_media_upload_type">Den typen av fil kan inte laddas upp.</string>
|
<string name="error_media_upload_type">Den typen av fil kan inte laddas upp.</string>
|
||||||
<string name="error_media_upload_opening">Den filen kunde inte öppnas.</string>
|
<string name="error_media_upload_opening">Den filen kunde inte öppnas.</string>
|
||||||
<string name="error_media_upload_permission">Tillstånd att läsa media krävs.</string>
|
<string name="error_media_upload_permission">Tillstånd att läsa media krävs.</string>
|
||||||
|
@ -285,7 +285,7 @@
|
||||||
<string name="abbreviated_seconds_ago">%ds</string>
|
<string name="abbreviated_seconds_ago">%ds</string>
|
||||||
|
|
||||||
<string name="follows_you">Följer dig</string>
|
<string name="follows_you">Följer dig</string>
|
||||||
<string name="pref_title_alway_show_sensitive_media">Visa alltid allt innehåll (nsfw)</string>
|
<string name="pref_title_alway_show_sensitive_media">Visa alltid allt innehåll (inkl. känsligt)</string>
|
||||||
<string name="title_media">Media</string>
|
<string name="title_media">Media</string>
|
||||||
<string name="replying_to">Svarar till @%s</string>
|
<string name="replying_to">Svarar till @%s</string>
|
||||||
<string name="load_more_placeholder_text">ladda mer</string>
|
<string name="load_more_placeholder_text">ladda mer</string>
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
<string name="error_authorization_denied">அங்கீகாரம் மறுக்கப்பட்டுள்ளது</string>
|
<string name="error_authorization_denied">அங்கீகாரம் மறுக்கப்பட்டுள்ளது</string>
|
||||||
<string name="error_retrieving_oauth_token">உள்நுழைவு டோக்கனைப் பெறுவதில் தோல்வி.</string>
|
<string name="error_retrieving_oauth_token">உள்நுழைவு டோக்கனைப் பெறுவதில் தோல்வி.</string>
|
||||||
<string name="error_compose_character_limit">நிலை மிக நீளமாக உள்ளது!</string>
|
<string name="error_compose_character_limit">நிலை மிக நீளமாக உள்ளது!</string>
|
||||||
<string name="error_media_upload_size">கோப்பு 4MB-க்கும் குறைவாக இருக்க வேண்டும்.</string>
|
<string name="error_image_upload_size">கோப்பு 4MB-க்கும் குறைவாக இருக்க வேண்டும்.</string>
|
||||||
<string name="error_media_upload_type">இந்த வகை கோப்பை பதிவேற்ற முடியாது.</string>
|
<string name="error_media_upload_type">இந்த வகை கோப்பை பதிவேற்ற முடியாது.</string>
|
||||||
<string name="error_media_upload_opening">அந்த கோப்பை திறக்க முடியவில்லை.</string>
|
<string name="error_media_upload_opening">அந்த கோப்பை திறக்க முடியவில்லை.</string>
|
||||||
<string name="error_media_upload_permission">ஊடகத்தை படிக்க அனுமதி தேவை.</string>
|
<string name="error_media_upload_permission">ஊடகத்தை படிக்க அனுமதி தேவை.</string>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="error_authorization_denied">Kimlik doğrulama reddedildi.</string>
|
<string name="error_authorization_denied">Kimlik doğrulama reddedildi.</string>
|
||||||
<string name="error_retrieving_oauth_token">Giriş jetonu alınamadı.</string>
|
<string name="error_retrieving_oauth_token">Giriş jetonu alınamadı.</string>
|
||||||
<string name="error_compose_character_limit">İleti fazlasıyla uzun!</string>
|
<string name="error_compose_character_limit">İleti fazlasıyla uzun!</string>
|
||||||
<string name="error_media_upload_size">Dosya 8MB\'ten küçük olmalı.</string>
|
<string name="error_image_upload_size">Dosya 8MB\'ten küçük olmalı.</string>
|
||||||
<string name="error_media_upload_type">O biçim dosya yüklenmez.</string>
|
<string name="error_media_upload_type">O biçim dosya yüklenmez.</string>
|
||||||
<string name="error_media_upload_opening">O dosya açılamadı.</string>
|
<string name="error_media_upload_opening">O dosya açılamadı.</string>
|
||||||
<string name="error_media_upload_permission">Medya okuma izni gerekiyor.</string>
|
<string name="error_media_upload_permission">Medya okuma izni gerekiyor.</string>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="error_authorization_denied">授权被拒绝。</string>
|
<string name="error_authorization_denied">授权被拒绝。</string>
|
||||||
<string name="error_retrieving_oauth_token">无法获取登录信息。</string>
|
<string name="error_retrieving_oauth_token">无法获取登录信息。</string>
|
||||||
<string name="error_compose_character_limit">嘟文太长了!</string>
|
<string name="error_compose_character_limit">嘟文太长了!</string>
|
||||||
<string name="error_media_upload_size">文件大小限制 8MB。</string>
|
<string name="error_image_upload_size">文件大小限制 8MB。</string>
|
||||||
<string name="error_media_upload_type">无法上传此类型的文件。</string>
|
<string name="error_media_upload_type">无法上传此类型的文件。</string>
|
||||||
<string name="error_media_upload_opening">此文件无法打开。</string>
|
<string name="error_media_upload_opening">此文件无法打开。</string>
|
||||||
<string name="error_media_upload_permission">需要授予 Tusky 读取媒体文件的权限。</string>
|
<string name="error_media_upload_permission">需要授予 Tusky 读取媒体文件的权限。</string>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="error_authorization_denied">授權被拒絕。</string>
|
<string name="error_authorization_denied">授權被拒絕。</string>
|
||||||
<string name="error_retrieving_oauth_token">無法獲取登錄信息。</string>
|
<string name="error_retrieving_oauth_token">無法獲取登錄信息。</string>
|
||||||
<string name="error_compose_character_limit">嘟文太長了!</string>
|
<string name="error_compose_character_limit">嘟文太長了!</string>
|
||||||
<string name="error_media_upload_size">文件大小限制 8MB。</string>
|
<string name="error_image_upload_size">文件大小限制 8MB。</string>
|
||||||
<string name="error_media_upload_type">無法上傳此類型的文件。</string>
|
<string name="error_media_upload_type">無法上傳此類型的文件。</string>
|
||||||
<string name="error_media_upload_opening">此文件無法打開。</string>
|
<string name="error_media_upload_opening">此文件無法打開。</string>
|
||||||
<string name="error_media_upload_permission">需要授予 Tusky 讀取媒體文件的權限。</string>
|
<string name="error_media_upload_permission">需要授予 Tusky 讀取媒體文件的權限。</string>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="error_authorization_denied">授權被拒絕。</string>
|
<string name="error_authorization_denied">授權被拒絕。</string>
|
||||||
<string name="error_retrieving_oauth_token">無法獲取登錄信息。</string>
|
<string name="error_retrieving_oauth_token">無法獲取登錄信息。</string>
|
||||||
<string name="error_compose_character_limit">嘟文太長了!</string>
|
<string name="error_compose_character_limit">嘟文太長了!</string>
|
||||||
<string name="error_media_upload_size">文件大小限制 8MB。</string>
|
<string name="error_image_upload_size">文件大小限制 8MB。</string>
|
||||||
<string name="error_media_upload_type">無法上傳此類型的文件。</string>
|
<string name="error_media_upload_type">無法上傳此類型的文件。</string>
|
||||||
<string name="error_media_upload_opening">此文件無法打開。</string>
|
<string name="error_media_upload_opening">此文件無法打開。</string>
|
||||||
<string name="error_media_upload_permission">需要授予 Tusky 讀取媒體文件的權限。</string>
|
<string name="error_media_upload_permission">需要授予 Tusky 讀取媒體文件的權限。</string>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="error_authorization_denied">授权被拒绝。</string>
|
<string name="error_authorization_denied">授权被拒绝。</string>
|
||||||
<string name="error_retrieving_oauth_token">无法获取登录信息。</string>
|
<string name="error_retrieving_oauth_token">无法获取登录信息。</string>
|
||||||
<string name="error_compose_character_limit">嘟文太长了!</string>
|
<string name="error_compose_character_limit">嘟文太长了!</string>
|
||||||
<string name="error_media_upload_size">文件大小限制 8MB。</string>
|
<string name="error_image_upload_size">文件大小限制 8MB。</string>
|
||||||
<string name="error_media_upload_type">无法上传此类型的文件。</string>
|
<string name="error_media_upload_type">无法上传此类型的文件。</string>
|
||||||
<string name="error_media_upload_opening">此文件无法打开。</string>
|
<string name="error_media_upload_opening">此文件无法打开。</string>
|
||||||
<string name="error_media_upload_permission">需要授予 Tusky 读取媒体文件的权限。</string>
|
<string name="error_media_upload_permission">需要授予 Tusky 读取媒体文件的权限。</string>
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<string name="error_authorization_denied">授權被拒絕。</string>
|
<string name="error_authorization_denied">授權被拒絕。</string>
|
||||||
<string name="error_retrieving_oauth_token">無法獲取登錄信息。</string>
|
<string name="error_retrieving_oauth_token">無法獲取登錄信息。</string>
|
||||||
<string name="error_compose_character_limit">嘟文太長了!</string>
|
<string name="error_compose_character_limit">嘟文太長了!</string>
|
||||||
<string name="error_media_upload_size">文件大小限制 8MB。</string>
|
<string name="error_image_upload_size">文件大小限制 8MB。</string>
|
||||||
<string name="error_media_upload_type">無法上傳此類型的文件。</string>
|
<string name="error_media_upload_type">無法上傳此類型的文件。</string>
|
||||||
<string name="error_media_upload_opening">此文件無法打開。</string>
|
<string name="error_media_upload_opening">此文件無法打開。</string>
|
||||||
<string name="error_media_upload_permission">需要授予 Tusky 讀取媒體文件的權限。</string>
|
<string name="error_media_upload_permission">需要授予 Tusky 讀取媒體文件的權限。</string>
|
||||||
|
|
|
@ -49,6 +49,7 @@
|
||||||
<color name="text_color_primary_black">#FFFFFF</color>
|
<color name="text_color_primary_black">#FFFFFF</color>
|
||||||
<color name="toolbar_background_black">#111111</color>
|
<color name="toolbar_background_black">#111111</color>
|
||||||
<color name="dialog_background_black">#111111</color>
|
<color name="dialog_background_black">#111111</color>
|
||||||
|
<color name="tab_page_margin_black">#000000</color>
|
||||||
<!--Light Theme Colors-->
|
<!--Light Theme Colors-->
|
||||||
<color name="color_primary_light">#dfdfdf</color>
|
<color name="color_primary_light">#dfdfdf</color>
|
||||||
<color name="color_primary_dark_light">#8f8f8f</color>
|
<color name="color_primary_dark_light">#8f8f8f</color>
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
<string name="error_authorization_denied">Authorization was denied.</string>
|
<string name="error_authorization_denied">Authorization was denied.</string>
|
||||||
<string name="error_retrieving_oauth_token">Failed getting a login token.</string>
|
<string name="error_retrieving_oauth_token">Failed getting a login token.</string>
|
||||||
<string name="error_compose_character_limit">The status is too long!</string>
|
<string name="error_compose_character_limit">The status is too long!</string>
|
||||||
<string name="error_media_upload_size">The file must be less than 8MB.</string>
|
<string name="error_image_upload_size">The file must be less than 8MB.</string>
|
||||||
|
<string name="error_video_upload_size">Video files must be less than 40MB.</string>
|
||||||
<string name="error_media_upload_type">That type of file cannot be uploaded.</string>
|
<string name="error_media_upload_type">That type of file cannot be uploaded.</string>
|
||||||
<string name="error_media_upload_opening">That file could not be opened.</string>
|
<string name="error_media_upload_opening">That file could not be opened.</string>
|
||||||
<string name="error_media_upload_permission">Permission to read media is required.</string>
|
<string name="error_media_upload_permission">Permission to read media is required.</string>
|
||||||
|
@ -354,4 +355,6 @@
|
||||||
<string name="profile_metadata_label_label">Label</string>
|
<string name="profile_metadata_label_label">Label</string>
|
||||||
<string name="profile_metadata_content_label">Content</string>
|
<string name="profile_metadata_content_label">Content</string>
|
||||||
|
|
||||||
|
<string name="pref_title_absolute_time">Use absolute time</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -172,6 +172,7 @@
|
||||||
<item name="material_drawer_selected">@color/color_primary_black</item>
|
<item name="material_drawer_selected">@color/color_primary_black</item>
|
||||||
<item name="material_drawer_selected_text">@color/text_color_primary_black</item>
|
<item name="material_drawer_selected_text">@color/text_color_primary_black</item>
|
||||||
<item name="material_drawer_header_selection_text">@color/text_color_primary_black</item>
|
<item name="material_drawer_header_selection_text">@color/text_color_primary_black</item>
|
||||||
|
<item name="tab_page_margin_drawable">@drawable/tab_page_margin_black</item>
|
||||||
</style>
|
</style>
|
||||||
<style name="TuskyBlackTheme" parent="TuskyBlackThemeBase"/>
|
<style name="TuskyBlackTheme" parent="TuskyBlackThemeBase"/>
|
||||||
|
|
||||||
|
|
|
@ -48,6 +48,12 @@
|
||||||
android:title="@string/pref_appearance_long_posts_title"
|
android:title="@string/pref_appearance_long_posts_title"
|
||||||
android:summaryOn="@string/pref_appearance_long_posts_enabled"
|
android:summaryOn="@string/pref_appearance_long_posts_enabled"
|
||||||
android:summaryOff="@string/pref_appearance_long_posts_disabled"/>
|
android:summaryOff="@string/pref_appearance_long_posts_disabled"/>
|
||||||
|
|
||||||
|
<CheckBoxPreference
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="absoluteTimeView"
|
||||||
|
android:title="@string/pref_title_absolute_time" />
|
||||||
|
|
||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
|
|
||||||
<PreferenceCategory android:title="@string/pref_publishing">
|
<PreferenceCategory android:title="@string/pref_publishing">
|
||||||
|
|
Loading…
Reference in New Issue