Compare commits
27 Commits
v1.1.1+for
...
v1.1.1+for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1e67c4f73 | ||
|
|
e0e48f87eb | ||
|
|
b2db64022f | ||
|
|
0ec14fe8fa | ||
|
|
01a2f1d95c | ||
|
|
67b3e85837 | ||
|
|
9f4d330ab1 | ||
|
|
25092fbcfb | ||
|
|
705e98729d | ||
|
|
108d16a157 | ||
|
|
e55ca6cc05 | ||
|
|
b8be1f184d | ||
|
|
aa96ec54a3 | ||
|
|
e8b43c7179 | ||
|
|
b51b4a10ee | ||
|
|
f2b9ede27c | ||
|
|
a8c7d891f1 | ||
|
|
195c4d7b6d | ||
|
|
d280dc31e8 | ||
|
|
eb0925c524 | ||
|
|
968de3664d | ||
|
|
12f7336392 | ||
|
|
3a4d13b1c6 | ||
|
|
273c841d9a | ||
|
|
0186b7f8da | ||
|
|
d33654c793 | ||
|
|
d844a77e65 |
20
README.md
@@ -1,8 +1,6 @@
|
||||
# Forked Mastodon for Android
|
||||

|
||||
|
||||
This is the repository for an officially forked Android app for Mastodon.
|
||||
|
||||
Learn more about the official app in the [blog post](https://blog.joinmastodon.org/2022/02/official-mastodon-for-android-app-is-coming-soon/).
|
||||
# Mastodon for Android Fork
|
||||
|
||||
## Changes
|
||||
|
||||
@@ -16,6 +14,20 @@ Learn more about the official app in the [blog post](https://blog.joinmastodon.o
|
||||
* [Always preserve content warnings when replying](https://github.com/sk22/mastodon-android-fork/tree/feature/always-preserve-cw) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/113))
|
||||
* [Make back button return to the home tab before exiting the app](https://github.com/sk22/mastodon-android-fork/tree/feature/back-returns-home) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/118))
|
||||
* [Implement a bookmark button and list](https://github.com/sk22/mastodon-android-fork/tree/feature/bookmarks) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/22))
|
||||
* [Implement deleting and re-drafting](https://github.com/sk22/mastodon-android-fork/tree/feature/delete-redraft) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/21))
|
||||
|
||||
## Fork-specific changes
|
||||
|
||||
* Custom app name
|
||||
* Custom icon: Modulate upstream icon's hue by `161%` using ImageMagick
|
||||
|
||||
```bash
|
||||
mogrify -modulate 100,100,161 mastodon/src/main/res/mipmap-*/ic_launcher*.png
|
||||
```
|
||||
|
||||
* Custom primary color: Hue of all `primary` colors in `colors.xml` is rotated
|
||||
by `109.8°` (equivalent of `161%`, done by hand using
|
||||
[PineTools](https://pinetools.com/shift-hue-color))
|
||||
|
||||
## Building
|
||||
|
||||
|
||||
@@ -9,8 +9,8 @@ android {
|
||||
applicationId "org.joinmastodon.android.sk"
|
||||
minSdk 23
|
||||
targetSdk 31
|
||||
versionCode 13
|
||||
versionName '1.1.1+fork.13'
|
||||
versionCode 19
|
||||
versionName '1.1.1+fork.19'
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
|
||||
5
mastodon/proguard-rules.pro
vendored
@@ -47,3 +47,8 @@
|
||||
-keep class org.joinmastodon.android.AppCenterWrapper { *; }
|
||||
|
||||
-keepattributes LineNumberTable
|
||||
|
||||
# Parceler library
|
||||
-keep interface org.parceler.Parcel
|
||||
-keep @org.parceler.Parcel class * { *; }
|
||||
-keep class **$$Parcelable { *; }
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.joinmastodon.android.api;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class AvatarResizedImageRequestBody extends ResizedImageRequestBody{
|
||||
public AvatarResizedImageRequestBody(Uri uri, ProgressListener progressListener) throws IOException{
|
||||
super(uri, 0, progressListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected int[] getTargetSize(int srcWidth, int srcHeight){
|
||||
float factor=400f/Math.min(srcWidth, srcHeight);
|
||||
return new int[]{Math.round(srcWidth*factor), Math.round(srcHeight*factor)};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean needResize(int srcWidth, int srcHeight){
|
||||
return srcHeight>400 || srcWidth!=srcHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean needCrop(int srcWidth, int srcHeight){
|
||||
return srcWidth!=srcHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Rect getCropBounds(int srcWidth, int srcHeight){
|
||||
Rect rect=new Rect();
|
||||
if(srcWidth>srcHeight){
|
||||
rect.set(srcWidth/2-srcHeight/2, 0, srcWidth/2-srcHeight/2+srcHeight, srcHeight);
|
||||
}else{
|
||||
rect.set(0, srcHeight/2-srcWidth/2, srcWidth, srcHeight/2-srcWidth/2+srcWidth);
|
||||
}
|
||||
return rect;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import android.os.Build;
|
||||
import android.provider.OpenableColumns;
|
||||
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
@@ -30,62 +31,105 @@ public class ResizedImageRequestBody extends CountingRequestBody{
|
||||
private File tempFile;
|
||||
private Uri uri;
|
||||
private String contentType;
|
||||
private int maxSize;
|
||||
|
||||
public ResizedImageRequestBody(Uri uri, int maxSize, ProgressListener progressListener) throws IOException{
|
||||
super(progressListener);
|
||||
this.uri=uri;
|
||||
contentType=MastodonApp.context.getContentResolver().getType(uri);
|
||||
this.maxSize=maxSize;
|
||||
BitmapFactory.Options opts=new BitmapFactory.Options();
|
||||
opts.inJustDecodeBounds=true;
|
||||
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
|
||||
BitmapFactory.decodeStream(in, null, opts);
|
||||
if("file".equals(uri.getScheme())){
|
||||
BitmapFactory.decodeFile(uri.getPath(), opts);
|
||||
contentType=UiUtils.getFileMediaType(new File(uri.getPath())).type();
|
||||
}else{
|
||||
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
|
||||
BitmapFactory.decodeStream(in, null, opts);
|
||||
}
|
||||
contentType=MastodonApp.context.getContentResolver().getType(uri);
|
||||
}
|
||||
if(opts.outWidth*opts.outHeight>maxSize){
|
||||
if(needResize(opts.outWidth, opts.outHeight) || needCrop(opts.outWidth, opts.outHeight)){
|
||||
Bitmap bitmap;
|
||||
if(Build.VERSION.SDK_INT>=29){
|
||||
bitmap=ImageDecoder.decodeBitmap(ImageDecoder.createSource(MastodonApp.context.getContentResolver(), uri), (decoder, info, source)->{
|
||||
int targetWidth=Math.round((float)Math.sqrt((float)maxSize*((float)info.getSize().getWidth()/info.getSize().getHeight())));
|
||||
int targetHeight=Math.round((float)Math.sqrt((float)maxSize*((float)info.getSize().getHeight()/info.getSize().getWidth())));
|
||||
if(Build.VERSION.SDK_INT>=28){
|
||||
ImageDecoder.Source source;
|
||||
if("file".equals(uri.getScheme())){
|
||||
source=ImageDecoder.createSource(new File(uri.getPath()));
|
||||
}else{
|
||||
source=ImageDecoder.createSource(MastodonApp.context.getContentResolver(), uri);
|
||||
}
|
||||
bitmap=ImageDecoder.decodeBitmap(source, (decoder, info, _source)->{
|
||||
int[] size=getTargetSize(info.getSize().getWidth(), info.getSize().getHeight());
|
||||
decoder.setAllocator(ImageDecoder.ALLOCATOR_SOFTWARE);
|
||||
decoder.setTargetSize(targetWidth, targetHeight);
|
||||
decoder.setTargetSize(size[0], size[1]);
|
||||
// Breaks images in mysterious ways
|
||||
// if(needCrop(size[0], size[1]))
|
||||
// decoder.setCrop(getCropBounds(size[0], size[1]));
|
||||
});
|
||||
if(needCrop(bitmap.getWidth(), bitmap.getHeight())){
|
||||
Rect crop=getCropBounds(bitmap.getWidth(), bitmap.getHeight());
|
||||
bitmap=Bitmap.createBitmap(bitmap, crop.left, crop.top, crop.width(), crop.height());
|
||||
}
|
||||
}else{
|
||||
int targetWidth=Math.round((float)Math.sqrt((float)maxSize*((float)opts.outWidth/opts.outHeight)));
|
||||
int targetHeight=Math.round((float)Math.sqrt((float)maxSize*((float)opts.outHeight/opts.outWidth)));
|
||||
int[] size=getTargetSize(opts.outWidth, opts.outHeight);
|
||||
int targetWidth=size[0];
|
||||
int targetHeight=size[1];
|
||||
float factor=opts.outWidth/(float)targetWidth;
|
||||
opts=new BitmapFactory.Options();
|
||||
opts.inSampleSize=(int)factor;
|
||||
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
|
||||
bitmap=BitmapFactory.decodeStream(in, null, opts);
|
||||
if("file".equals(uri.getScheme())){
|
||||
bitmap=BitmapFactory.decodeFile(uri.getPath(), opts);
|
||||
}else{
|
||||
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
|
||||
bitmap=BitmapFactory.decodeStream(in, null, opts);
|
||||
}
|
||||
}
|
||||
if(factor%1f!=0f){
|
||||
Bitmap scaled=Bitmap.createBitmap(targetWidth, targetHeight, Bitmap.Config.ARGB_8888);
|
||||
new Canvas(scaled).drawBitmap(bitmap, null, new Rect(0, 0, targetWidth, targetHeight), new Paint(Paint.FILTER_BITMAP_FLAG));
|
||||
boolean needCrop=needCrop(targetWidth, targetHeight);
|
||||
if(factor%1f!=0f || needCrop){
|
||||
Rect srcBounds=null;
|
||||
Rect dstBounds;
|
||||
if(needCrop){
|
||||
Rect crop=getCropBounds(targetWidth, targetHeight);
|
||||
dstBounds=new Rect(0, 0, crop.width(), crop.height());
|
||||
srcBounds=new Rect(
|
||||
Math.round(crop.left/(float)targetWidth*bitmap.getWidth()),
|
||||
Math.round(crop.top/(float)targetHeight*bitmap.getHeight()),
|
||||
Math.round(crop.right/(float)targetWidth*bitmap.getWidth()),
|
||||
Math.round(crop.bottom/(float)targetHeight*bitmap.getHeight())
|
||||
);
|
||||
}else{
|
||||
dstBounds=new Rect(0, 0, targetWidth, targetHeight);
|
||||
}
|
||||
Bitmap scaled=Bitmap.createBitmap(dstBounds.width(), dstBounds.height(), Bitmap.Config.ARGB_8888);
|
||||
new Canvas(scaled).drawBitmap(bitmap, srcBounds, dstBounds, new Paint(Paint.FILTER_BITMAP_FLAG));
|
||||
bitmap=scaled;
|
||||
}
|
||||
if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
int rotation;
|
||||
int orientation=0;
|
||||
if("file".equals(uri.getScheme())){
|
||||
ExifInterface exif=new ExifInterface(uri.getPath());
|
||||
orientation=exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||
}else if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.N){
|
||||
try(InputStream in=MastodonApp.context.getContentResolver().openInputStream(uri)){
|
||||
ExifInterface exif=new ExifInterface(in);
|
||||
int orientation=exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||
rotation=switch(orientation){
|
||||
case ExifInterface.ORIENTATION_ROTATE_90 -> 90;
|
||||
case ExifInterface.ORIENTATION_ROTATE_180 -> 180;
|
||||
case ExifInterface.ORIENTATION_ROTATE_270 -> 270;
|
||||
default -> 0;
|
||||
};
|
||||
}
|
||||
if(rotation!=0){
|
||||
Matrix matrix=new Matrix();
|
||||
matrix.setRotate(rotation);
|
||||
bitmap=Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
|
||||
orientation=exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||
}
|
||||
}
|
||||
int rotation=switch(orientation){
|
||||
case ExifInterface.ORIENTATION_ROTATE_90 -> 90;
|
||||
case ExifInterface.ORIENTATION_ROTATE_180 -> 180;
|
||||
case ExifInterface.ORIENTATION_ROTATE_270 -> 270;
|
||||
default -> 0;
|
||||
};
|
||||
if(rotation!=0){
|
||||
Matrix matrix=new Matrix();
|
||||
matrix.setRotate(rotation);
|
||||
bitmap=Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
|
||||
}
|
||||
}
|
||||
|
||||
tempFile=new File(MastodonApp.context.getCacheDir(), "tmp_upload_image");
|
||||
boolean isPNG="image/png".equals(contentType);
|
||||
tempFile=File.createTempFile("mastodon_tmp_resized", null);
|
||||
try(FileOutputStream out=new FileOutputStream(tempFile)){
|
||||
if("image/png".equals(contentType)){
|
||||
if(isPNG){
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out);
|
||||
}else{
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 97, out);
|
||||
@@ -94,9 +138,13 @@ public class ResizedImageRequestBody extends CountingRequestBody{
|
||||
}
|
||||
length=tempFile.length();
|
||||
}else{
|
||||
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){
|
||||
cursor.moveToFirst();
|
||||
length=cursor.getInt(0);
|
||||
if("file".equals(uri.getScheme())){
|
||||
length=new File(uri.getPath()).length();
|
||||
}else{
|
||||
try(Cursor cursor=MastodonApp.context.getContentResolver().query(uri, new String[]{OpenableColumns.SIZE}, null, null, null)){
|
||||
cursor.moveToFirst();
|
||||
length=cursor.getInt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -125,4 +173,22 @@ public class ResizedImageRequestBody extends CountingRequestBody{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected int[] getTargetSize(int srcWidth, int srcHeight){
|
||||
int targetWidth=Math.round((float)Math.sqrt((float)maxSize*((float)srcWidth/srcHeight)));
|
||||
int targetHeight=Math.round((float)Math.sqrt((float)maxSize*((float)srcHeight/srcWidth)));
|
||||
return new int[]{targetWidth, targetHeight};
|
||||
}
|
||||
|
||||
protected boolean needResize(int srcWidth, int srcHeight){
|
||||
return srcWidth*srcHeight>maxSize;
|
||||
}
|
||||
|
||||
protected boolean needCrop(int srcWidth, int srcHeight){
|
||||
return false;
|
||||
}
|
||||
|
||||
protected Rect getCropBounds(int srcWidth, int srcHeight){
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,16 @@ package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import org.joinmastodon.android.api.AvatarResizedImageRequestBody;
|
||||
import org.joinmastodon.android.api.ContentUriRequestBody;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.ResizedImageRequestBody;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.AccountField;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import okhttp3.MultipartBody;
|
||||
@@ -39,21 +42,21 @@ public class UpdateAccountCredentials extends MastodonAPIRequest<Account>{
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestBody getRequestBody(){
|
||||
public RequestBody getRequestBody() throws IOException{
|
||||
MultipartBody.Builder bldr=new MultipartBody.Builder()
|
||||
.setType(MultipartBody.FORM)
|
||||
.addFormDataPart("display_name", displayName)
|
||||
.addFormDataPart("note", bio);
|
||||
|
||||
if(avatar!=null){
|
||||
bldr.addFormDataPart("avatar", UiUtils.getFileName(avatar), new ContentUriRequestBody(avatar, null));
|
||||
bldr.addFormDataPart("avatar", UiUtils.getFileName(avatar), new AvatarResizedImageRequestBody(avatar, null));
|
||||
}else if(avatarFile!=null){
|
||||
bldr.addFormDataPart("avatar", avatarFile.getName(), RequestBody.create(UiUtils.getFileMediaType(avatarFile), avatarFile));
|
||||
bldr.addFormDataPart("avatar", avatarFile.getName(), new AvatarResizedImageRequestBody(Uri.fromFile(avatarFile), null));
|
||||
}
|
||||
if(cover!=null){
|
||||
bldr.addFormDataPart("header", UiUtils.getFileName(cover), new ContentUriRequestBody(cover, null));
|
||||
bldr.addFormDataPart("header", UiUtils.getFileName(cover), new ResizedImageRequestBody(cover, 1500*500, null));
|
||||
}else if(coverFile!=null){
|
||||
bldr.addFormDataPart("header", coverFile.getName(), RequestBody.create(UiUtils.getFileMediaType(coverFile), coverFile));
|
||||
bldr.addFormDataPart("header", coverFile.getName(), new ResizedImageRequestBody(Uri.fromFile(coverFile), 1500*500, null));
|
||||
}
|
||||
if(fields.isEmpty()){
|
||||
bldr.addFormDataPart("fields_attributes[0][name]", "").addFormDataPart("fields_attributes[0][value]", "");
|
||||
|
||||
@@ -11,7 +11,7 @@ public class CreateOAuthApp extends MastodonAPIRequest<Application>{
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public String clientName="Mastodon for Android (Fork)";
|
||||
public String clientName="Mastodon for Android Fork";
|
||||
public String redirectUris=AccountSessionManager.REDIRECT_URI;
|
||||
public String scopes=AccountSessionManager.SCOPE;
|
||||
public String website="https://github.com/sk22/mastodon-android-fork";
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.text.Layout;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -174,6 +175,14 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
private Instance instance;
|
||||
private boolean attachmentsErrorShowing;
|
||||
|
||||
public static DraftMediaAttachment redraftAttachment(Attachment att) {
|
||||
DraftMediaAttachment draft=new DraftMediaAttachment();
|
||||
draft.serverAttachment=att;
|
||||
draft.description=att.description;
|
||||
draft.uri=Uri.parse(att.url);
|
||||
return draft;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -204,6 +213,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
replyTo=Parcels.unwrap(getArguments().getParcelable("replyTo"));
|
||||
statusVisibility=replyTo.visibility;
|
||||
}
|
||||
|
||||
if(getArguments().containsKey("visibility")){
|
||||
statusVisibility=(StatusPrivacy) getArguments().getSerializable("visibility");
|
||||
}
|
||||
|
||||
if(savedInstanceState!=null){
|
||||
statusVisibility=(StatusPrivacy) savedInstanceState.getSerializable("visibility");
|
||||
}
|
||||
@@ -286,11 +300,18 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
pollDurationView.setOnClickListener(v->showPollDurationMenu());
|
||||
|
||||
pollOptions.clear();
|
||||
if(savedInstanceState!=null && savedInstanceState.containsKey("pollOptions")){
|
||||
ArrayList<String> restoredPollOptions=(savedInstanceState!=null ? savedInstanceState : getArguments())
|
||||
.getStringArrayList("pollOptions");
|
||||
if(restoredPollOptions!=null){
|
||||
if(savedInstanceState==null){
|
||||
// restoring from arguments
|
||||
pollDuration=getArguments().getInt("pollDuration");
|
||||
pollDurationStr=DateUtils.formatElapsedTime(pollDuration); // getResources().getQuantityString(R.plurals.x_hours, pollDuration/3600);
|
||||
}
|
||||
pollBtn.setSelected(true);
|
||||
mediaBtn.setEnabled(false);
|
||||
pollWrap.setVisibility(View.VISIBLE);
|
||||
for(String oldText:savedInstanceState.getStringArrayList("pollOptions")){
|
||||
for(String oldText:restoredPollOptions){
|
||||
DraftPollOption opt=createDraftPollOption();
|
||||
opt.edit.setText(oldText);
|
||||
}
|
||||
@@ -310,8 +331,9 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
spoilerBtn.setSelected(true);
|
||||
}
|
||||
|
||||
if(savedInstanceState!=null && savedInstanceState.containsKey("attachments")){
|
||||
ArrayList<Parcelable> serializedAttachments=savedInstanceState.getParcelableArrayList("attachments");
|
||||
ArrayList<Parcelable> serializedAttachments=(savedInstanceState!=null ? savedInstanceState : getArguments())
|
||||
.getParcelableArrayList("attachments");
|
||||
if(serializedAttachments!=null){
|
||||
for(Parcelable a:serializedAttachments){
|
||||
DraftMediaAttachment att=Parcels.unwrap(a);
|
||||
attachmentsView.addView(createMediaAttachmentView(att));
|
||||
@@ -456,10 +478,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
// TODO: setting for preserving cw always / only when replying to own posts
|
||||
// && AccountSessionManager.getInstance().isSelf(accountID, replyTo.account)
|
||||
if(!TextUtils.isEmpty(replyTo.spoilerText)){
|
||||
insertSpoiler(replyTo.spoilerText);
|
||||
hasSpoiler=true;
|
||||
spoilerEdit.setVisibility(View.VISIBLE);
|
||||
spoilerEdit.setText(replyTo.spoilerText);
|
||||
spoilerBtn.setSelected(true);
|
||||
}
|
||||
}
|
||||
}else{
|
||||
@@ -472,6 +492,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
mainEditText.setSelection(mainEditText.length());
|
||||
initialText=prefilledText;
|
||||
}
|
||||
String spoilerText=getArguments().getString("spoilerText");
|
||||
if(!TextUtils.isEmpty(spoilerText)) insertSpoiler(spoilerText);
|
||||
ArrayList<Uri> mediaUris=getArguments().getParcelableArrayList("mediaAttachments");
|
||||
if(mediaUris!=null && !mediaUris.isEmpty()){
|
||||
for(Uri uri:mediaUris){
|
||||
@@ -481,6 +503,13 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
}
|
||||
|
||||
private void insertSpoiler(String text) {
|
||||
hasSpoiler=true;
|
||||
if (text!=null) spoilerEdit.setText(text);
|
||||
spoilerEdit.setVisibility(View.VISIBLE);
|
||||
spoilerBtn.setSelected(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
publishButton=new Button(getActivity());
|
||||
@@ -549,8 +578,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
if(opt.edit.length()>0)
|
||||
nonEmptyPollOptionsCount++;
|
||||
}
|
||||
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit && uploadingAttachment==null && failedAttachments.isEmpty() && queuedAttachments.isEmpty()
|
||||
&& (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
|
||||
if(publishButton!=null){
|
||||
publishButton.setEnabled((trimmedCharCount>0 || !attachments.isEmpty()) && charCount<=charLimit
|
||||
&& uploadingAttachment==null && failedAttachments.isEmpty() && queuedAttachments.isEmpty()
|
||||
&& (pollOptions.isEmpty() || nonEmptyPollOptionsCount>1));
|
||||
}
|
||||
}
|
||||
|
||||
private void onCustomEmojiClick(Emoji emoji){
|
||||
@@ -637,8 +669,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
boolean pollFieldsHaveContent=false;
|
||||
for(DraftPollOption opt:pollOptions)
|
||||
pollFieldsHaveContent|=opt.edit.length()>0;
|
||||
return (mainEditText.length()>0 && !mainEditText.getText().toString().equals(initialText)) || !attachments.isEmpty()
|
||||
|| uploadingAttachment!=null || !queuedAttachments.isEmpty() || !failedAttachments.isEmpty() || pollFieldsHaveContent;
|
||||
return getArguments().getBoolean("hasDraft", false)
|
||||
|| (mainEditText.length()>0 && !mainEditText.getText().toString().equals(initialText))
|
||||
|| !attachments.isEmpty() || uploadingAttachment!=null || !queuedAttachments.isEmpty()
|
||||
|| !failedAttachments.isEmpty() || pollFieldsHaveContent;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -137,6 +137,8 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
int id=menuItem.getItemId();
|
||||
if(id==R.id.delete){
|
||||
UiUtils.confirmDeletePost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
|
||||
}else if(id==R.id.delete_and_redraft) {
|
||||
UiUtils.confirmDeleteAndRedraftPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, s->{});
|
||||
}else if(id==R.id.pin || id==R.id.unpin){
|
||||
UiUtils.confirmPinPost(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), item.status, !item.status.pinned, s->{});
|
||||
}else if(id==R.id.mute){
|
||||
@@ -252,6 +254,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
Menu menu=optionsMenu.getMenu();
|
||||
boolean isOwnPost=AccountSessionManager.getInstance().isSelf(item.parentFragment.getAccountID(), account);
|
||||
menu.findItem(R.id.delete).setVisible(item.status!=null && isOwnPost);
|
||||
menu.findItem(R.id.delete_and_redraft).setVisible(item.status!=null && isOwnPost);
|
||||
menu.findItem(R.id.pin).setVisible(item.status!=null && isOwnPost && !item.status.pinned);
|
||||
menu.findItem(R.id.unpin).setVisible(item.status!=null && isOwnPost && item.status.pinned);
|
||||
menu.findItem(R.id.open_in_browser).setVisible(item.status!=null);
|
||||
|
||||
@@ -19,6 +19,7 @@ import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Parcelable;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
@@ -46,6 +47,8 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
|
||||
import org.joinmastodon.android.events.StatusDeletedEvent;
|
||||
import org.joinmastodon.android.events.StatusUnpinnedEvent;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.ProfileFragment;
|
||||
import org.joinmastodon.android.fragments.ThreadFragment;
|
||||
@@ -53,8 +56,10 @@ import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Emoji;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.StatusPrivacy;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.text.CustomEmojiSpan;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.text.SpacerSpan;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
@@ -64,6 +69,8 @@ import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -76,6 +83,10 @@ import androidx.annotation.StringRes;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
@@ -377,7 +388,63 @@ public class UiUtils{
|
||||
})
|
||||
.wrapProgress(activity, pinned ? R.string.pinning : R.string.unpinning, false)
|
||||
.exec(accountID);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static void confirmDeleteAndRedraftPost(Activity activity, String accountID, Status status, Consumer<Status> resultCallback){
|
||||
showConfirmationAlert(activity, R.string.confirm_delete_and_redraft_title, R.string.confirm_delete_and_redraft, R.string.delete_and_redraft, ()->{
|
||||
new DeleteStatus(status.id)
|
||||
.setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
resultCallback.accept(result);
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getCacheController().deleteStatus(status.id);
|
||||
E.post(new StatusDeletedEvent(status.id, accountID));
|
||||
UiUtils.redraftStatus(status, accountID, activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(activity);
|
||||
}
|
||||
})
|
||||
.wrapProgress(activity, R.string.deleting, false)
|
||||
.exec(accountID);
|
||||
});
|
||||
}
|
||||
|
||||
public static void redraftStatus(Status status, String accountID, Activity activity) {
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putBoolean("hasDraft", true);
|
||||
args.putString("prefilledText", HtmlParser.parse(status.content, status.emojis, status.mentions, status.tags, accountID).toString());
|
||||
args.putString("spoilerText", status.spoilerText);
|
||||
args.putSerializable("visibility", status.visibility);
|
||||
if(status.poll!=null){
|
||||
args.putInt("pollDuration", (int)status.poll.expiresAt.minus(status.createdAt.getEpochSecond(), ChronoUnit.SECONDS).getEpochSecond());
|
||||
ArrayList<String> opts=status.poll.options.stream().map(o -> o.title).collect(Collectors.toCollection(ArrayList::new));
|
||||
args.putStringArrayList("pollOptions", opts);
|
||||
}
|
||||
if(!status.mediaAttachments.isEmpty()){
|
||||
ArrayList<Parcelable> serializedAttachments=status.mediaAttachments.stream()
|
||||
.map(att -> Parcels.wrap(ComposeFragment.redraftAttachment(att)))
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
args.putParcelableArrayList("attachments", serializedAttachments);
|
||||
}
|
||||
Callback<Status> cb=new Callback<>(){
|
||||
@Override public void onError(ErrorResponse error) {
|
||||
onSuccess(null);
|
||||
error.showToast(activity);
|
||||
}
|
||||
@Override public void onSuccess(Status status) {
|
||||
if (status!=null) args.putParcelable("replyTo", Parcels.wrap(status));
|
||||
Nav.go(activity, ComposeFragment.class, args);
|
||||
}
|
||||
};
|
||||
|
||||
if(status.inReplyToId!=null) new GetStatusByID(status.inReplyToId).setCallback(cb).exec(accountID);
|
||||
else cb.onSuccess(null);
|
||||
}
|
||||
|
||||
public static void setRelationshipToActionButton(Relationship relationship, Button button){
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:id="@+id/delete" android:title="@string/delete"/>
|
||||
<item android:id="@+id/delete_and_redraft" android:title="@string/delete_and_redraft"/>
|
||||
<item android:id="@+id/pin" android:title="@string/pin_post"/>
|
||||
<item android:id="@+id/unpin" android:title="@string/unpin_post"/>
|
||||
<item android:id="@+id/mute" android:title="@string/mute_user"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 5.2 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 8.3 KiB |
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 96 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 154 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 34 KiB |
@@ -122,8 +122,11 @@
|
||||
<string name="action_vote">Abstimmen</string>
|
||||
<string name="tap_to_reveal">Zum Anzeigen tippen</string>
|
||||
<string name="delete">Löschen</string>
|
||||
<string name="delete_and_redraft">Löschen und neu erstellen</string>
|
||||
<string name="confirm_delete_title">Beitrag löschen</string>
|
||||
<string name="confirm_delete_and_redraft_title">Beitrag löschen und neu erstellen</string>
|
||||
<string name="confirm_delete">Bist du dir sicher, dass du den Beitrag löschen möchtest?</string>
|
||||
<string name="confirm_delete_and_redraft">Bist du dir sicher, dass du den Beitrag löschen und neu erstellen möchtest?</string>
|
||||
<string name="deleting">Wird gelöscht…</string>
|
||||
<string name="pin_post">An Profil anheften</string>
|
||||
<string name="confirm_pin_post_title">Beitrag an Profil anheften</string>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">Mastodon</string>
|
||||
<string name="get_started">开始使用</string>
|
||||
<string name="log_in">登录</string>
|
||||
<string name="next">下一步</string>
|
||||
@@ -8,12 +7,15 @@
|
||||
<string name="error">错误</string>
|
||||
<string name="not_a_mastodon_instance">%s 似乎不是Mastodon实例。</string>
|
||||
<string name="ok">确定</string>
|
||||
<string name="preparing_auth">正在跳转...</string>
|
||||
<string name="finishing_auth">正在完成身份验证…</string>
|
||||
<string name="in_reply_to">回复给 %s</string>
|
||||
<string name="preparing_auth">正在准备身份认证…</string>
|
||||
<string name="finishing_auth">正在完成身份认证…</string>
|
||||
<string name="user_boosted">%s 转发了</string>
|
||||
<string name="in_reply_to">回复 %s</string>
|
||||
<string name="notifications">通知</string>
|
||||
<string name="user_followed_you">关注了你</string>
|
||||
<string name="user_sent_follow_request">向您发送了关注请求</string>
|
||||
<string name="user_sent_follow_request">向你发送了关注请求</string>
|
||||
<string name="user_favorited">喜欢了你的嘟文</string>
|
||||
<string name="notification_boosted">转发了你的嘟文</string>
|
||||
<string name="poll_ended">投票已结束</string>
|
||||
<string name="time_seconds">%d 秒前</string>
|
||||
<string name="time_minutes">%d 分钟前</string>
|
||||
@@ -22,7 +24,7 @@
|
||||
<string name="share_toot_title">分享</string>
|
||||
<string name="settings">设置</string>
|
||||
<string name="publish">发布</string>
|
||||
<string name="discard_draft">舍弃草稿?</string>
|
||||
<string name="discard_draft">丢弃草稿?</string>
|
||||
<string name="discard">丢弃</string>
|
||||
<string name="cancel">取消</string>
|
||||
<plurals name="followers">
|
||||
@@ -32,175 +34,302 @@
|
||||
<item quantity="other">正在关注</item>
|
||||
</plurals>
|
||||
<plurals name="posts">
|
||||
<item quantity="other">帖子</item>
|
||||
<item quantity="other">嘟文</item>
|
||||
</plurals>
|
||||
<string name="posts">帖子</string>
|
||||
<string name="posts_and_replies">帖子与回复</string>
|
||||
<string name="media">媒体文件</string>
|
||||
<string name="posts">嘟文</string>
|
||||
<string name="posts_and_replies">嘟文和回复</string>
|
||||
<string name="pinned_posts">置顶</string>
|
||||
<string name="media">媒体</string>
|
||||
<string name="profile_about">关于</string>
|
||||
<string name="button_follow">关注</string>
|
||||
<string name="button_following">正在关注</string>
|
||||
<string name="edit_profile">修改个人资料</string>
|
||||
<string name="mention_user">提及 %s</string>
|
||||
<string name="share_user">分享 %s</string>
|
||||
<string name="mute_user">静音 %s</string>
|
||||
<string name="unmute_user">不再静音 %s</string>
|
||||
<string name="mute_user">隐藏 %s</string>
|
||||
<string name="unmute_user">不再隐藏 %s</string>
|
||||
<string name="block_user">屏蔽 %s</string>
|
||||
<string name="unblock_user">解除屏蔽 %s</string>
|
||||
<string name="unblock_user">不再屏蔽 %s</string>
|
||||
<string name="report_user">举报 %s</string>
|
||||
<string name="block_domain">屏蔽 %s</string>
|
||||
<string name="unblock_domain">解除屏蔽 %s</string>
|
||||
<string name="block_domain">屏蔽 %s 实例</string>
|
||||
<string name="unblock_domain">不再屏蔽 %s 实例</string>
|
||||
<plurals name="x_posts">
|
||||
<item quantity="other">%d 个帖子</item>
|
||||
<item quantity="other">%d 条嘟文</item>
|
||||
</plurals>
|
||||
<string name="profile_joined">加入于</string>
|
||||
<string name="done">完成</string>
|
||||
<string name="loading">加载中...</string>
|
||||
<string name="loading">正在加载…</string>
|
||||
<string name="field_label">标签</string>
|
||||
<string name="saving">保存中...</string>
|
||||
<string name="post_from_user">来自 %s 的帖子</string>
|
||||
<string name="poll_option_hint">选项 %d</string>
|
||||
<string name="field_content">内容</string>
|
||||
<string name="saving">正在保存…</string>
|
||||
<string name="post_from_user">来自 %s 的嘟文</string>
|
||||
<string name="poll_option_hint">"选项 %d"</string>
|
||||
<plurals name="x_minutes">
|
||||
<item quantity="other">%d分钟</item>
|
||||
<item quantity="other">%d 分钟</item>
|
||||
</plurals>
|
||||
<plurals name="x_hours">
|
||||
<item quantity="other">%d小时</item>
|
||||
<item quantity="other">%d 小时</item>
|
||||
</plurals>
|
||||
<plurals name="x_days">
|
||||
<item quantity="other">%d天</item>
|
||||
<item quantity="other">%d 天</item>
|
||||
</plurals>
|
||||
<string name="compose_poll_duration">时长: %s</string>
|
||||
<string name="compose_poll_duration">时长:%s</string>
|
||||
<plurals name="x_seconds_left">
|
||||
<item quantity="other">剩余 %d 秒</item>
|
||||
<item quantity="other">%d 秒后结束</item>
|
||||
</plurals>
|
||||
<plurals name="x_minutes_left">
|
||||
<item quantity="other">剩余 %d 分钟</item>
|
||||
<item quantity="other">%d 分钟后结束</item>
|
||||
</plurals>
|
||||
<plurals name="x_hours_left">
|
||||
<item quantity="other">%d 小时后结束</item>
|
||||
</plurals>
|
||||
<plurals name="x_days_left">
|
||||
<item quantity="other">%d 天后结束</item>
|
||||
</plurals>
|
||||
<plurals name="x_voters">
|
||||
<item quantity="other">%d 人已投票</item>
|
||||
</plurals>
|
||||
<string name="poll_closed">已关闭</string>
|
||||
<string name="tap_to_reveal">点击以显示</string>
|
||||
<string name="confirm_mute_title">隐藏用户</string>
|
||||
<string name="confirm_mute">确定要隐藏 %s 吗?</string>
|
||||
<string name="do_mute">确定</string>
|
||||
<string name="confirm_unmute_title">取消隐藏</string>
|
||||
<string name="confirm_unmute">确定不再隐藏 %s 吗?</string>
|
||||
<string name="do_unmute">确定</string>
|
||||
<string name="confirm_block_title">屏蔽用户</string>
|
||||
<string name="confirm_block_domain_title">屏蔽域名</string>
|
||||
<string name="confirm_block">确定要屏蔽 %s 吗?</string>
|
||||
<string name="do_block">确定</string>
|
||||
<string name="confirm_unblock_title">解除屏蔽</string>
|
||||
<string name="confirm_unblock_domain_title">解除屏蔽</string>
|
||||
<string name="confirm_unblock">确定不再屏蔽 %s 吗?</string>
|
||||
<string name="do_unblock">确定</string>
|
||||
<string name="button_muted">已隐藏</string>
|
||||
<string name="button_blocked">已屏蔽</string>
|
||||
<string name="action_vote">投票</string>
|
||||
<string name="tap_to_reveal">点击显示</string>
|
||||
<string name="delete">删除</string>
|
||||
<string name="delete_and_redraft">删除以重新编辑</string>
|
||||
<string name="confirm_delete_title">删除嘟文</string>
|
||||
<string name="confirm_delete_and_redraft_title">删除嘟文并重新编辑</string>
|
||||
<string name="confirm_delete">确定要删除这条嘟文吗?</string>
|
||||
<string name="confirm_delete_and_redraft">确定要删除这条嘟文并重新编辑吗?</string>
|
||||
<string name="deleting">正在删除…</string>
|
||||
<string name="pin_post">置顶</string>
|
||||
<string name="confirm_pin_post_title">置顶嘟文</string>
|
||||
<string name="confirm_pin_post">确定要在你的资料页置顶这条嘟文吗?</string>
|
||||
<string name="pinning">正在置顶嘟文…</string>
|
||||
<string name="unpin_post">取消置顶</string>
|
||||
<string name="confirm_unpin_post_title">取消嘟文置顶</string>
|
||||
<string name="confirm_unpin_post">确定不再置顶这条嘟文吗?</string>
|
||||
<string name="unpinning">正在取消置顶…</string>
|
||||
<string name="notification_channel_audio_player">音频播放</string>
|
||||
<string name="play">播放</string>
|
||||
<string name="pause">暂停</string>
|
||||
<string name="log_out">登出</string>
|
||||
<string name="log_out">退出账户</string>
|
||||
<string name="add_account">添加账户</string>
|
||||
<string name="search_hint">搜索</string>
|
||||
<string name="hashtags">话题</string>
|
||||
<string name="for_you">推荐</string>
|
||||
<string name="news">新闻</string>
|
||||
<string name="for_you">推荐关注</string>
|
||||
<string name="all_notifications">全部</string>
|
||||
<string name="mentions">提及我</string>
|
||||
<string name="mentions">提及</string>
|
||||
<plurals name="x_people_talking">
|
||||
<item quantity="other">%d 人在讨论</item>
|
||||
</plurals>
|
||||
<plurals name="discussed_x_times">
|
||||
<item quantity="other">讨论了 %d 次</item>
|
||||
</plurals>
|
||||
<string name="report_title">举报 %s</string>
|
||||
<string name="report_choose_reason">这个帖子有什么问题?</string>
|
||||
<string name="report_choose_reason">这条嘟文有什么问题?</string>
|
||||
<string name="report_choose_reason_account">%s 有什么问题?</string>
|
||||
<string name="report_choose_reason_subtitle">选择最佳匹配</string>
|
||||
<string name="report_choose_reason_subtitle">选择最匹配的理由</string>
|
||||
<string name="report_reason_personal">我不喜欢它</string>
|
||||
<string name="report_reason_personal_subtitle">这不是你想看到的东西</string>
|
||||
<string name="report_reason_spam">它是垃圾信息</string>
|
||||
<string name="report_reason_spam_subtitle">恶意链接,虚假互动或重复回复</string>
|
||||
<string name="report_reason_violation">它违反了服务器规则</string>
|
||||
<string name="report_reason_violation_subtitle">你知道它会破坏特定规则</string>
|
||||
<string name="report_reason_spam">垃圾信息</string>
|
||||
<string name="report_reason_spam_subtitle">恶意链接、虚假互动或重复回复</string>
|
||||
<string name="report_reason_violation">违反社区规则</string>
|
||||
<string name="report_reason_violation_subtitle">你发现它违反了特定的规则</string>
|
||||
<string name="report_reason_other">其他原因</string>
|
||||
<string name="report_reason_other_subtitle">该问题不符合其他类别</string>
|
||||
<string name="report_reason_other_subtitle">以上理由都不适用</string>
|
||||
<string name="report_choose_rule">违反了哪些规则?</string>
|
||||
<string name="report_choose_rule_subtitle">选择所有适用项</string>
|
||||
<string name="report_choose_posts">是否有任何帖子支持此报告?</string>
|
||||
<string name="report_choose_posts">是否有任何嘟文支持此举报?</string>
|
||||
<string name="report_choose_posts_subtitle">选择所有适用项</string>
|
||||
<string name="report_comment_title">还有什么你认为我们应该知道的吗?</string>
|
||||
<string name="report_comment_title">还有什么要告诉我们的吗?</string>
|
||||
<string name="report_comment_hint">备注</string>
|
||||
<string name="sending_report">报告发送中...</string>
|
||||
<string name="report_sent_title">感谢提交举报,我们将会进行处理。</string>
|
||||
<string name="report_sent_subtitle">当我们审查这个问题时,你可以对 %s 采取行动。</string>
|
||||
<string name="unfollow_user">取消关注 %s</string>
|
||||
<string name="sending_report">正在提交举报…</string>
|
||||
<string name="report_sent_title">感谢举报,我们将会处理。</string>
|
||||
<string name="report_sent_subtitle">我们审查此问题期间,你也可以对 %s 采取行动。</string>
|
||||
<string name="unfollow_user">不再关注 %s</string>
|
||||
<string name="unfollow">取消关注</string>
|
||||
<string name="mute_user_explain">你不会在你的主页里看到他们的帖子或重新博客。他们不会知道他们被静音了。</string>
|
||||
<string name="block_user_explain">他们将不再能够关注或看到你的帖子,但他们可以看到他们是否被阻止。</string>
|
||||
<string name="report_personal_title">不想看到这个内容?</string>
|
||||
<string name="report_personal_subtitle">当您在Mastodon看到您不喜欢的东西时,您可以从您的体验中移除该人。</string>
|
||||
<string name="mute_user_explain">你不会在你的主页里看到他们的嘟文和转发,他们也不会知道你隐藏了他们。</string>
|
||||
<string name="block_user_explain">他们不能再关注你或看到你的嘟文,但他们能知道他们是否被屏蔽。</string>
|
||||
<string name="report_personal_title">不想看到这个?</string>
|
||||
<string name="report_personal_subtitle">如果在Mastodon看到不喜欢的东西,可以尝试移除它。</string>
|
||||
<string name="back">返回</string>
|
||||
<string name="instance_catalog_title">Mastodon由来自不同服务器的用户组成。</string>
|
||||
<string name="instance_catalog_subtitle">根据你的兴趣、地区或其他目的挑选一个服务器。无论你选择哪个服务器,你都可以跟所有人交流。</string>
|
||||
<string name="search_communities">搜索或输入网址</string>
|
||||
<string name="instance_rules_title">一些基本规则</string>
|
||||
<string name="instance_rules_subtitle">请花一分钟来审阅规则设置,并由 %s 管理员执行。</string>
|
||||
<string name="signup_title">让我们让您在 %s 上设置</string>
|
||||
<string name="instance_rules_subtitle">请花一分钟来审阅规则,这是由管理员 %s 设置和执行的。</string>
|
||||
<string name="signup_title">让我们在 %s 上开始</string>
|
||||
<string name="edit_photo">编辑</string>
|
||||
<string name="display_name">昵称</string>
|
||||
<string name="username">用户名</string>
|
||||
<string name="email">电子邮箱</string>
|
||||
<string name="password">密码</string>
|
||||
<string name="password_note">包括大写字母、特殊字符和数字以增加您的密码强度。</string>
|
||||
<string name="password_note">包括大写字母、特殊字符和数字以增加你的密码强度。</string>
|
||||
<string name="category_academia">学术</string>
|
||||
<string name="category_activism">行动主义</string>
|
||||
<string name="category_all">全部</string>
|
||||
<string name="category_art">艺术</string>
|
||||
<string name="category_food">食品</string>
|
||||
<string name="category_food">美食</string>
|
||||
<string name="category_furry">兽迷</string>
|
||||
<string name="category_games">游戏</string>
|
||||
<string name="category_general">通用</string>
|
||||
<string name="category_journalism">新闻</string>
|
||||
<string name="category_lgbt">性少数</string>
|
||||
<string name="category_music">音乐</string>
|
||||
<string name="category_regional">地区</string>
|
||||
<string name="category_tech">科技</string>
|
||||
<string name="confirm_email_title">还有一件事</string>
|
||||
<string name="confirm_email_subtitle">点击我们发送给你的链接来验证你的账户。</string>
|
||||
<string name="resend">重新发送</string>
|
||||
<string name="open_email_app">打开电子邮件应用</string>
|
||||
<string name="resent_email">确认邮件已发送</string>
|
||||
<string name="resent_email">邮件已发送</string>
|
||||
<string name="compose_hint">写下你的想法</string>
|
||||
<string name="content_warning">内容警告</string>
|
||||
<string name="image_description">图片描述</string>
|
||||
<string name="add_image_description">添加图片描述…</string>
|
||||
<string name="retry_upload">重新上传</string>
|
||||
<string name="image_upload_failed">图片上传失败</string>
|
||||
<string name="video_upload_failed">视频上传失败</string>
|
||||
<string name="edit_image">编辑图片</string>
|
||||
<string name="save">保存</string>
|
||||
<string name="add_alt_text">添加备注</string>
|
||||
<string name="alt_text_subtitle">备注可以为视障人士描述你的图片,请尽量只包含足以理解内容的信息。</string>
|
||||
<string name="alt_text_hint">例如,镜头前有一只狗眯着眼睛警惕地看着四周。</string>
|
||||
<string name="visibility_public">公开</string>
|
||||
<string name="visibility_unlisted">不公开</string>
|
||||
<string name="visibility_followers_only">仅关注者</string>
|
||||
<string name="visibility_private">仅我提到的人</string>
|
||||
<string name="visibility_private">仅提及的人</string>
|
||||
<string name="search_all">全部</string>
|
||||
<string name="search_people">用户</string>
|
||||
<string name="recent_searches">最近搜索</string>
|
||||
<string name="step_x_of_n">第 %1$d 步(共 %2$d 步)</string>
|
||||
<string name="skip">跳过</string>
|
||||
<string name="notification_type_follow">新关注者</string>
|
||||
<string name="notification_type_favorite">喜欢</string>
|
||||
<string name="notification_type_reblog">转发</string>
|
||||
<string name="notification_type_mention">提及</string>
|
||||
<string name="notification_type_poll">投票</string>
|
||||
<string name="choose_account">选择账户</string>
|
||||
<string name="err_not_logged_in">请先登陆到Mastodon</string>
|
||||
<plurals name="cant_add_more_than_x_attachments">
|
||||
<item quantity="other">你不能添加超过 %d 个媒体附件</item>
|
||||
</plurals>
|
||||
<string name="media_attachment_unsupported_type">文件 %s 是不支持的类型</string>
|
||||
<string name="media_attachment_too_big">文件 %1$s 的大小超出了 %2$s MB的限制</string>
|
||||
<string name="settings_theme">外观</string>
|
||||
<string name="theme_auto">自动</string>
|
||||
<string name="theme_light">浅色</string>
|
||||
<string name="theme_dark">深色</string>
|
||||
<string name="theme_true_black">纯黑模式</string>
|
||||
<string name="settings_behavior">行为</string>
|
||||
<string name="settings_gif">播放动态头像和表情</string>
|
||||
<string name="settings_custom_tabs">使用内置浏览器</string>
|
||||
<string name="settings_notifications">通知</string>
|
||||
<string name="notify_me_when">接收通知的范围</string>
|
||||
<string name="notify_anyone">任何人</string>
|
||||
<string name="notify_follower">关注者</string>
|
||||
<string name="notify_followed">我关注的</string>
|
||||
<string name="notify_none">没有人</string>
|
||||
<string name="notify_favorites">喜欢我的帖子</string>
|
||||
<string name="notify_none">关闭通知</string>
|
||||
<string name="notify_favorites">喜欢我的嘟文</string>
|
||||
<string name="notify_follow">关注我</string>
|
||||
<string name="notify_reblog">转发我的帖子</string>
|
||||
<string name="notify_reblog">转发我的嘟文</string>
|
||||
<string name="notify_mention">提及我</string>
|
||||
<string name="settings_boring">潜水区</string>
|
||||
<string name="settings_account">帐户设置</string>
|
||||
<string name="settings_boring">更多</string>
|
||||
<string name="settings_account">账户设置</string>
|
||||
<string name="settings_contribute">贡献给Mastodon</string>
|
||||
<string name="settings_tos">使用条款</string>
|
||||
<string name="settings_privacy_policy">隐私政策</string>
|
||||
<string name="settings_spicy">The Spicy Zone</string>
|
||||
<string name="settings_clear_cache">清除图片缓存</string>
|
||||
<string name="settings_spicy">危险地带</string>
|
||||
<string name="settings_clear_cache">清除媒体缓存</string>
|
||||
<string name="settings_app_version">Mastodon for Android v%1$s (%2$d)</string>
|
||||
<string name="media_cache_cleared">媒体缓存已清除</string>
|
||||
<string name="confirm_log_out">您确定要退出吗?</string>
|
||||
<string name="confirm_log_out">确定要退出账户吗?</string>
|
||||
<string name="sensitive_content">敏感内容</string>
|
||||
<string name="sensitive_content_explain">作者将此媒体标记为敏感。点击以显示。</string>
|
||||
<string name="media_hidden">点击以显示</string>
|
||||
<string name="avatar_description">跳转到 %s 的资料</string>
|
||||
<string name="sensitive_content_explain">作者将此媒体标记为敏感内容,点击显示。</string>
|
||||
<string name="media_hidden">点击显示</string>
|
||||
<string name="avatar_description">跳转到 %s 的资料页</string>
|
||||
<string name="more_options">更多选项</string>
|
||||
<string name="reveal_content">显示内容</string>
|
||||
<string name="hide_content">隐藏内容</string>
|
||||
<string name="new_post">新帖子</string>
|
||||
<string name="new_post">新嘟文</string>
|
||||
<string name="button_reply">回复</string>
|
||||
<string name="button_reblog">转发</string>
|
||||
<string name="button_favorite">喜欢</string>
|
||||
<string name="button_share">分享</string>
|
||||
<string name="button_bookmark">添加到书签</string>
|
||||
<string name="bookmarks">书签</string>
|
||||
<string name="media_no_description">没有描述的媒体</string>
|
||||
<string name="add_media">添加媒体</string>
|
||||
<string name="add_poll">添加投票</string>
|
||||
<string name="add_poll">发起投票</string>
|
||||
<string name="emoji">表情</string>
|
||||
<string name="post_visibility">帖子可见性</string>
|
||||
<string name="post_visibility">嘟文可见性</string>
|
||||
<string name="home_timeline">主页时间轴</string>
|
||||
<string name="my_profile">我的资料</string>
|
||||
<string name="media_viewer">媒体查看器</string>
|
||||
<string name="follow_user">关注 %s</string>
|
||||
<string name="unfollowed_user">取消关注 %s</string>
|
||||
<string name="followed_user">您正在关注 %s</string>
|
||||
<string name="followed_user">你正在关注 %s</string>
|
||||
<string name="open_in_browser">在浏览器中打开</string>
|
||||
<string name="signup_reason">加入的理由是?</string>
|
||||
<string name="signup_reason_note">这会有助于我们处理你的申请.</string>
|
||||
<string name="hide_boosts_from_user">隐藏来自 %s 的转发</string>
|
||||
<string name="show_boosts_from_user">不再隐藏来自 %s 的转发</string>
|
||||
<string name="signup_reason">为什么想要加入?</string>
|
||||
<string name="signup_reason_note">这会帮助我们评估你的申请。</string>
|
||||
<string name="clear">清除</string>
|
||||
<string name="profile_header">顶部图片</string>
|
||||
<string name="profile_picture">个人资料照片</string>
|
||||
<string name="profile_header">资料页横幅图片</string>
|
||||
<string name="profile_picture">头像</string>
|
||||
<string name="reorder">重新排序</string>
|
||||
<string name="download">下载</string>
|
||||
<string name="permission_required">需要相应权限</string>
|
||||
<string name="trending_posts_info_banner">这些是在你的 Mastodon 宇宙中备受关注的内容。</string>
|
||||
<string name="trending_hashtags_info_banner">这些是在你的 Mastodon 宇宙中备受关注的话题</string>
|
||||
<string name="local_timeline_info_banner">这些是与您使用相同 Mastodon 服务器的人的最新帖子。</string>
|
||||
<string name="follows_you">关注了你</string>
|
||||
<string name="current_account">当前账号</string>
|
||||
<string name="permission_required">需要权限</string>
|
||||
<string name="storage_permission_to_download">应用程序需要存储权限才能保存文件。</string>
|
||||
<string name="open_settings">打开「设置」</string>
|
||||
<string name="error_saving_file">保存文件时出错</string>
|
||||
<string name="file_saved">文件已保存</string>
|
||||
<string name="downloading">正在下载…</string>
|
||||
<string name="no_app_to_handle_action">未找到可处理此操作的应用</string>
|
||||
<string name="local_timeline">本站时间轴</string>
|
||||
<string name="federated_timeline">联邦时间轴</string>
|
||||
<string name="trending_posts_info_banner">这是在你的Mastodon角落备受关注的内容。</string>
|
||||
<string name="trending_hashtags_info_banner">这是在你的Mastodon角落备受关注的话题。</string>
|
||||
<string name="trending_links_info_banner">这是在你的Mastodon角落分享最多的新闻故事。</string>
|
||||
<string name="local_timeline_info_banner">这是你所在的Mastodon服务器上的用户发布的最新嘟文。</string>
|
||||
<string name="federated_timeline_info_banner">这是在你的联邦宇宙中最新发布的嘟文。</string>
|
||||
<string name="dismiss">驳回</string>
|
||||
<string name="see_new_posts">查看新嘟文</string>
|
||||
<string name="load_missing_posts">加载嘟文</string>
|
||||
<string name="follow_back">关注</string>
|
||||
<string name="button_follow_pending">已发送关注请求</string>
|
||||
<string name="follows_you">已关注你</string>
|
||||
<string name="manually_approves_followers">手动批准关注请求</string>
|
||||
<string name="current_account">当前账户</string>
|
||||
<string name="log_out_account">退出 %s</string>
|
||||
<!-- translators: %,d is a valid placeholder, it formats the number with locale-dependent grouping separators -->
|
||||
<plurals name="x_followers">
|
||||
<item quantity="other">%,d 名关注者</item>
|
||||
</plurals>
|
||||
<plurals name="x_following">
|
||||
<item quantity="other">关注 %,d 人</item>
|
||||
</plurals>
|
||||
<plurals name="x_favorites">
|
||||
<item quantity="other">%,d 次喜欢</item>
|
||||
</plurals>
|
||||
<plurals name="x_reblogs">
|
||||
<item quantity="other">%,d 次转发</item>
|
||||
</plurals>
|
||||
<string name="timestamp_via_app">%1$s 来自 %2$s</string>
|
||||
<string name="time_now">刚刚</string>
|
||||
</resources>
|
||||
|
||||
@@ -19,17 +19,17 @@
|
||||
<color name="gray_800t">#CC282C37</color>
|
||||
<color name="gray_900">#101828</color>
|
||||
|
||||
<color name="primary_25">#FAFDFF</color>
|
||||
<color name="primary_50">#EAF4FB</color>
|
||||
<color name="primary_100">#D5E9F7</color>
|
||||
<color name="primary_200">#BFDEF4</color>
|
||||
<color name="primary_300">#AAD3F0</color>
|
||||
<color name="primary_400">#95C8EC</color>
|
||||
<color name="primary_500">#80BCE8</color>
|
||||
<color name="primary_600">#55A6E1</color>
|
||||
<color name="primary_700">#2B90D9</color>
|
||||
<color name="primary_800">#2273AE</color>
|
||||
<color name="primary_900">#16486D</color>
|
||||
<color name="primary_25">#fffafd</color>
|
||||
<color name="primary_50">#fbeaf6</color>
|
||||
<color name="primary_100">#f7d4ee</color>
|
||||
<color name="primary_200">#f4bfe7</color>
|
||||
<color name="primary_300">#f0aade</color>
|
||||
<color name="primary_400">#ec94d6</color>
|
||||
<color name="primary_500">#e780cd</color>
|
||||
<color name="primary_600">#e055bd</color>
|
||||
<color name="primary_700">#d92aad</color>
|
||||
<color name="primary_800">#ae218a</color>
|
||||
<color name="primary_900">#6d1556</color>
|
||||
<color name="error_25">#FFFBFA</color>
|
||||
<color name="error_50">#FEF3F2</color>
|
||||
<color name="error_100">#FEE4E2</color>
|
||||
|
||||
@@ -127,8 +127,11 @@
|
||||
<string name="action_vote">Vote</string>
|
||||
<string name="tap_to_reveal">Tap to reveal</string>
|
||||
<string name="delete">Delete</string>
|
||||
<string name="delete_and_redraft">Delete and re-draft</string>
|
||||
<string name="confirm_delete_title">Delete Post</string>
|
||||
<string name="confirm_delete_and_redraft_title">Delete and re-draft Post</string>
|
||||
<string name="confirm_delete">Are you sure you want to delete this post?</string>
|
||||
<string name="confirm_delete_and_redraft">Are you sure you want to delete and re-draft this post?</string>
|
||||
<string name="deleting">Deleting…</string>
|
||||
<string name="pin_post">Pin to profile</string>
|
||||
<string name="confirm_pin_post_title">Pin post to profile</string>
|
||||
|
||||