Compare commits

...

27 Commits

Author SHA1 Message Date
sk
c1e67c4f73 bump version 2022-06-08 21:44:39 +02:00
sk
e0e48f87eb Merge branch 'master' into fork 2022-06-08 21:42:42 +02:00
Grishka
b2db64022f Add pre-upload avatar and header resizing 2022-06-06 16:45:56 +03:00
Samuel Kaiser
0ec14fe8fa Merge pull request #20 from Y32Gcnte8z/fork
fix simplified Chinese strings
2022-06-05 12:03:08 +02:00
Y32Gcnte8z
01a2f1d95c fix simplified Chinese strings 2022-06-04 13:20:56 +08:00
Samuel Kaiser
67b3e85837 Merge pull request #19 from Y32Gcnte8z/fork
New translations strings.xml (Chinese Simplified)
2022-06-03 11:31:19 +02:00
Y32Gcnte8z
9f4d330ab1 New translations strings.xml (Chinese Simplified) 2022-06-03 13:55:49 +08:00
sk
25092fbcfb add icon to readme 2022-05-31 17:04:58 +02:00
sk
705e98729d initial pink branding 2022-05-31 16:51:05 +02:00
sk
108d16a157 bump version 2022-05-26 22:44:31 +02:00
sk
e55ca6cc05 Merge branch 'feature/delete-redraft' into fork 2022-05-26 22:42:02 +02:00
sk
b8be1f184d hide redraft button when not applicable 2022-05-26 22:38:56 +02:00
sk
aa96ec54a3 Merge branch 'feature/delete-redraft' into fork 2022-05-26 22:09:39 +02:00
sk
e8b43c7179 preserve visibility when re-drafting 2022-05-26 22:09:02 +02:00
sk
b51b4a10ee Merge branch 'feature/delete-redraft' into fork 2022-05-26 21:44:07 +02:00
sk
f2b9ede27c Add proguard rules for parceler
according to http://parceler.org/
fixes issue where parceler can't find parcelable class
2022-05-26 21:43:39 +02:00
sk
a8c7d891f1 Merge branch 'feature/delete-redraft' into fork 2022-05-26 19:45:52 +02:00
sk
195c4d7b6d remove unused imports 2022-05-26 19:45:10 +02:00
sk
d280dc31e8 bump version 2022-05-26 19:35:36 +02:00
sk
eb0925c524 Merge branch 'feature/delete-redraft' into fork 2022-05-26 19:30:52 +02:00
sk
968de3664d fix german strings 2022-05-26 19:30:23 +02:00
sk
12f7336392 Merge branch 'feature/delete-redraft' into fork 2022-05-26 19:28:09 +02:00
sk
3a4d13b1c6 implement deleting and re-drafting 2022-05-26 19:19:42 +02:00
sk
273c841d9a Merge branch 'master' into feature/delete-redraft 2022-05-26 15:35:12 +02:00
sk
0186b7f8da Merge remote-tracking branch 'origin/fork' into fork 2022-05-22 02:14:06 +02:00
sk
d33654c793 bump version 2022-05-22 02:08:01 +02:00
sk
d844a77e65 add ui items for redraft 2022-05-11 17:25:00 +02:00
35 changed files with 514 additions and 149 deletions

View File

@@ -1,8 +1,6 @@
# Forked Mastodon for Android
![Pink version of the Mastodon for Android launcher icon](mastodon/src/main/res/mipmap-xhdpi/ic_launcher_round.png)
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

View File

@@ -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"
}

View File

@@ -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 { *; }

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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]", "");

View File

@@ -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";

View File

@@ -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

View File

@@ -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);

View File

@@ -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){

View File

@@ -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"/>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>