Compare commits

...

92 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
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
Samuel Kaiser
86d2312615 Update README.md 2022-05-22 02:07:07 +02:00
sk
d1083c331b Merge branch 'feature/bookmarks' into fork 2022-05-22 02:04:08 +02:00
sk
ed7242217a add missing icon 2022-05-22 02:03:54 +02:00
sk
8fddaa8c82 implement bookmarks list!! 2022-05-22 02:03:29 +02:00
sk
00affe6e3e update readme 2022-05-21 23:38:56 +02:00
sk
f21b647ee0 bump version 2022-05-21 23:35:10 +02:00
sk
2a628a3791 Merge branch 'feature/back-returns-home' into fork 2022-05-21 23:34:18 +02:00
sk
ecd568503d make back button return home before exiting 2022-05-21 23:33:53 +02:00
sk
f9d0632a85 add missing files 2022-05-21 19:42:29 +02:00
sk
11905513b7 add missing icons 2022-05-21 19:40:49 +02:00
sk
9c89abf1c4 implement bookmark button 2022-05-21 19:27:44 +02:00
sk
4d950e43ac update readme 2022-05-21 18:59:13 +02:00
sk
99405f307d bump version 2022-05-21 18:49:59 +02:00
sk
f1bfe05263 Merge branch 'feature/always-preserve-cw' into fork 2022-05-21 18:49:07 +02:00
sk
0f223159c0 always preserve cw when replying 2022-05-21 18:48:48 +02:00
sk
ad9518e87c re-add deleted strings 2022-05-21 18:08:14 +02:00
sk
1c16cfb09e bump version 2022-05-21 18:05:01 +02:00
sk
d4a4b10017 Merge branch 'feature/compose-image-description-full-image' into fork 2022-05-21 18:03:29 +02:00
sk
74ae5bd04e change app name 2022-05-21 18:03:15 +02:00
sk
9638cf079f set image view height to wrap_content 2022-05-21 17:48:53 +02:00
sk
a6d161c1b4 minor code style change
for grishka's code style must prevail
2022-05-21 17:42:40 +02:00
sk
1136e40eb4 obey image max width 2022-05-21 17:39:28 +02:00
sk
98de3a2984 don't crop image when composing alt text 2022-05-21 17:27:31 +02:00
sk
b08415ca8f bump version 2022-05-15 20:35:10 +02:00
sk
3639c69d36 Merge branch 'master' into fork 2022-05-15 20:34:14 +02:00
sk
31e3a8592f bump version 2022-05-14 13:49:49 +02:00
sk
39655d5278 Merge branch 'master' into fork 2022-05-14 13:48:03 +02:00
sk
d844a77e65 add ui items for redraft 2022-05-11 17:25:00 +02:00
sk
bde2e398a8 bump version and change app name 2022-05-06 22:10:45 +02:00
sk
8d443b2051 Merge branch 'feature/pin-posts' into fork 2022-05-06 21:50:08 +02:00
sk
33d4b678ed update posts' pinned states 2022-05-06 21:49:33 +02:00
sk
3becad1468 fix created posts being added to pinned 2022-05-06 21:18:48 +02:00
sk
fad3ba3eae bump version 2022-05-06 19:49:36 +02:00
sk
cb16f95878 Merge branch 'feature/pin-posts' into fork 2022-05-06 19:45:33 +02:00
sk
4e833490ff fix about section not being displayed 2022-05-06 19:45:09 +02:00
Samuel Kaiser
04a973f7b0 Update README.md 2022-05-06 19:28:43 +02:00
sk
0318169b74 Merge branch 'feature/pin-posts' into fork 2022-05-06 19:24:58 +02:00
sk
972fb1e241 translate "pinned" strings to german 2022-05-06 19:21:39 +02:00
sk
9beb04b01d implement pinning and unpinning posts 2022-05-06 19:07:51 +02:00
sk
a3bea6ad24 add profile tab for pinned toots 2022-05-06 18:09:00 +02:00
sk
7996e4ee4a change client name, versioning 2022-05-06 15:39:57 +02:00
sk
69c4bf4213 Merge branch 'feature/display-alt-text' into fork 2022-05-06 01:00:34 +02:00
sk
7cd5ca77f5 Merge remote-tracking branch 'origin/feature/display-alt-text' into feature/display-alt-text 2022-05-06 01:00:01 +02:00
sk
7e736d3cd3 clean up code 2022-05-06 00:59:43 +02:00
sk
13c2adba56 update version and readme 2022-05-06 00:34:08 +02:00
sk
010095a50e Merge branch 'master' into fork 2022-05-06 00:30:23 +02:00
Samuel Kaiser
f0cef2103f Merge branch 'mastodon:master' into feature/display-alt-text 2022-05-06 00:29:05 +02:00
sk
8ed731a48b edit application id for fork 2022-05-06 00:27:59 +02:00
sk
8660d43cb1 bump version 2022-05-06 00:21:08 +02:00
sk
0f495f620a Merge branch 'feature/display-alt-text' into fork 2022-05-06 00:17:07 +02:00
sk
ac81f10ea8 make button disappear when no description 2022-05-06 00:16:25 +02:00
sk
9aa95413e6 make text selectable 2022-05-06 00:15:56 +02:00
sk
a0a28a0cb7 implement scroll-to-close 2022-05-05 23:39:24 +02:00
sk
11d88aed27 implement alt text as bottom sheet 2022-05-05 23:26:48 +02:00
sk
899c9cdf21 implement alt text as toast messages 2022-05-05 19:28:27 +02:00
sk
919d5cffb5 Merge branch 'master' into fork 2022-05-05 16:06:28 +02:00
sk
12599db0ff change dev versioning 2022-05-02 23:00:55 +02:00
sk
c751c85c1c add version number for upstream changes 2022-05-02 22:43:05 +02:00
sk
f1331a0f6d start fork versioning 2022-05-02 21:35:51 +02:00
sk
c75c9b60f9 add fork readme 2022-05-02 21:35:42 +02:00
sk
eb3adf1dfd Merge branch 'feature/add-federated-timeline' into fork 2022-05-02 21:35:14 +02:00
sk
6533163fd0 Merge branch 'master' into fork 2022-05-02 21:17:04 +02:00
sk
1becad6016 Merge branch 'feature/enable-unlisted-as-default' into fork 2022-05-02 19:32:26 +02:00
sk
d34653750e set unlisted as default visibility 2022-05-02 19:31:43 +02:00
sk
705592aefd set unlisted as default 2022-05-02 19:17:25 +02:00
sk
583325d6e8 add unlisted visibility option 2022-05-02 19:16:53 +02:00
sk
318d271127 add federation tab and change tab order 2022-05-02 18:31:29 +02:00
62 changed files with 951 additions and 168 deletions

View File

@@ -1,11 +1,33 @@
# Mastodon for Android
[![Crowdin](https://badges.crowdin.net/mastodon-for-android/localized.svg)](https://crowdin.com/project/mastodon-for-android)
![Pink version of the Mastodon for Android launcher icon](mastodon/src/main/res/mipmap-xhdpi/ic_launcher_round.png)
<a href="https://play.google.com/store/apps/details?id=org.joinmastodon.android"><img src="img/google-play-badge.png" height="50"></a>
# Mastodon for Android Fork
This is the repository for the official Android app for Mastodon.
## Changes
Learn more about this app in the [blog post](https://blog.joinmastodon.org/2022/02/official-mastodon-for-android-app-is-coming-soon/).
* [Enable "Unlisted" as a visibility option](https://github.com/sk22/mastodon-android-fork/tree/feature/enable-unlisted)
([Pull request](https://github.com/mastodon/mastodon-android/pull/103)) and
[set as default](https://github.com/sk22/mastodon-android-fork/tree/feature/enable-unlisted-as-default)
* [Add "Federation" tab and change Discover tab order](https://github.com/sk22/mastodon-android-fork/tree/feature/add-federated-timeline) ([Fixes issue](https://github.com/mastodon/mastodon-android/issues/8))
* [Add image description button and viewer](https://github.com/sk22/mastodon-android-fork/tree/feature/display-alt-text) ([Pull request](https://github.com/mastodon/mastodon-android/pull/129))
* [Implement pinning posts and displaying pinned posts](https://github.com/sk22/mastodon-android-fork/tree/feature/pin-posts) ([Pull request](https://github.com/mastodon/mastodon-android/pull/140))
* [Display full image when adding image description](https://github.com/sk22/mastodon-android-fork/tree/feature/compose-image-description-full-image) ([Pull request](https://github.com/mastodon/mastodon-android/pull/182))
* [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
@@ -17,4 +39,4 @@ As this app is using Java 17 features, you need JDK 17 or newer to build it. Oth
## License
This project is released under the [GPL-3 License](./LICENSE).
This project is released under the [GPL-3 License](./LICENSE).

View File

@@ -6,11 +6,11 @@ plugins {
android {
compileSdk 31
defaultConfig {
applicationId "org.joinmastodon.android"
applicationId "org.joinmastodon.android.sk"
minSdk 23
targetSdk 31
versionCode 37
versionName "1.1.1"
versionCode 19
versionName '1.1.1+fork.19'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -46,4 +46,9 @@
-keep class org.joinmastodon.android.AppCenterWrapper { *; }
-keepattributes LineNumberTable
-keepattributes LineNumberTable
# Parceler library
-keep interface org.parceler.Parcel
-keep @org.parceler.Parcel class * { *; }
-keep class **$$Parcelable { *; }

View File

@@ -4,6 +4,7 @@ import android.os.Looper;
import org.joinmastodon.android.E;
import org.joinmastodon.android.MastodonApp;
import org.joinmastodon.android.api.requests.statuses.SetStatusBookmarked;
import org.joinmastodon.android.api.requests.statuses.SetStatusFavorited;
import org.joinmastodon.android.api.requests.statuses.SetStatusReblogged;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
@@ -18,6 +19,7 @@ public class StatusInteractionController{
private final String accountID;
private final HashMap<String, SetStatusFavorited> runningFavoriteRequests=new HashMap<>();
private final HashMap<String, SetStatusReblogged> runningReblogRequests=new HashMap<>();
private final HashMap<String, SetStatusBookmarked> runningBookmarkRequests=new HashMap<>();
public StatusInteractionController(String accountID){
this.accountID=accountID;
@@ -61,6 +63,36 @@ public class StatusInteractionController{
E.post(new StatusCountersUpdatedEvent(status));
}
public void setBookmarked(Status status, boolean bookmarked){
if(!Looper.getMainLooper().isCurrentThread())
throw new IllegalStateException("Can only be called from main thread");
SetStatusBookmarked current=runningBookmarkRequests.remove(status.id);
if(current!=null){
current.cancel();
}
SetStatusBookmarked req=(SetStatusBookmarked) new SetStatusBookmarked(status.id, bookmarked)
.setCallback(new Callback<>(){
@Override
public void onSuccess(Status result){
runningBookmarkRequests.remove(status.id);
E.post(new StatusCountersUpdatedEvent(result));
}
@Override
public void onError(ErrorResponse error){
runningBookmarkRequests.remove(status.id);
error.showToast(MastodonApp.context);
status.bookmarked=!bookmarked;
E.post(new StatusCountersUpdatedEvent(status));
}
})
.exec(accountID);
runningBookmarkRequests.put(status.id, req);
status.bookmarked=bookmarked;
E.post(new StatusCountersUpdatedEvent(status));
}
public void setReblogged(Status status, boolean reblogged){
if(!Looper.getMainLooper().isCurrentThread())
throw new IllegalStateException("Can only be called from main thread");

View File

@@ -21,6 +21,7 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
switch(filter){
case DEFAULT -> addQueryParameter("exclude_replies", "true");
case INCLUDE_REPLIES -> {}
case PINNED -> addQueryParameter("pinned", "true");
case MEDIA -> addQueryParameter("only_media", "true");
case NO_REBLOGS -> {
addQueryParameter("exclude_replies", "true");
@@ -32,6 +33,7 @@ public class GetAccountStatuses extends MastodonAPIRequest<List<Status>>{
public enum Filter{
DEFAULT,
INCLUDE_REPLIES,
PINNED,
MEDIA,
NO_REBLOGS
}

View File

@@ -0,0 +1,52 @@
package org.joinmastodon.android.api.requests.accounts;
import androidx.annotation.NonNull;
import com.google.gson.reflect.TypeToken;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import okhttp3.Response;
public class GetBookmarks extends MastodonAPIRequest<List<Status>>{
private String maxId;
public GetBookmarks(String maxID, String minID, int limit){
super(HttpMethod.GET, "/bookmarks", new TypeToken<>(){});
if(maxID!=null)
addQueryParameter("max_id", maxID);
if(minID!=null)
addQueryParameter("min_id", minID);
if(limit>0)
addQueryParameter("limit", ""+limit);
}
@Override
public void validateAndPostprocessResponse(List<Status> respObj, Response httpResponse) throws IOException {
super.validateAndPostprocessResponse(respObj, httpResponse);
// <https://mastodon.social/api/v1/bookmarks?max_id=268962>; rel="next",
// <https://mastodon.social/api/v1/bookmarks?min_id=268981>; rel="prev"
String link=httpResponse.header("link");
// parsing link header by hand; using a library would be cleaner
// (also, the functionality should be part of the max id logics and implemented in MastodonAPIRequest)
if(link==null) return;
String maxIdEq="max_id=";
for(String s : link.split(",")) {
if(s.contains("rel=\"next\"")) {
int start=s.indexOf(maxIdEq)+maxIdEq.length();
int end=s.indexOf('>');
if(start<0 || start>end) return;
this.maxId=s.substring(start, end);
}
}
}
public String getMaxId() {
return maxId;
}
}

View File

@@ -11,9 +11,9 @@ public class CreateOAuthApp extends MastodonAPIRequest<Application>{
}
private static class Request{
public String clientName="Mastodon for Android";
public String clientName="Mastodon for Android Fork";
public String redirectUris=AccountSessionManager.REDIRECT_URI;
public String scopes=AccountSessionManager.SCOPE;
public String website="https://app.joinmastodon.org/android";
public String website="https://github.com/sk22/mastodon-android-fork";
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
public class SetStatusBookmarked extends MastodonAPIRequest<Status>{
public SetStatusBookmarked(String id, boolean bookmarked){
super(HttpMethod.POST, "/statuses/"+id+"/"+(bookmarked ? "bookmark" : "unbookmark"), Status.class);
setRequestBody(new Object());
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.api.requests.statuses;
import org.joinmastodon.android.api.MastodonAPIRequest;
import org.joinmastodon.android.model.Status;
public class SetStatusPinned extends MastodonAPIRequest<Status>{
public SetStatusPinned(String id, boolean pinned){
super(HttpMethod.POST, "/statuses/"+id+"/"+(pinned ? "pin" : "unpin"), Status.class);
setRequestBody(new Object());
}
}

View File

@@ -5,7 +5,7 @@ import org.joinmastodon.android.model.Status;
public class StatusCountersUpdatedEvent{
public String id;
public int favorites, reblogs, replies;
public boolean favorited, reblogged;
public boolean favorited, reblogged, pinned;
public StatusCountersUpdatedEvent(Status s){
id=s.id;
@@ -14,5 +14,6 @@ public class StatusCountersUpdatedEvent{
replies=s.repliesCount;
favorited=s.favourited;
reblogged=s.reblogged;
pinned=s.pinned;
}
}

View File

@@ -0,0 +1,11 @@
package org.joinmastodon.android.events;
public class StatusUnpinnedEvent {
public final String id;
public final String accountID;
public StatusUnpinnedEvent(String id, String accountID){
this.id=id;
this.accountID=accountID;
}
}

View File

@@ -8,8 +8,10 @@ import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusUnpinnedEvent;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.HeaderStatusDisplayItem;
import org.parceler.Parcels;
import java.util.Collections;
@@ -76,6 +78,7 @@ public class AccountTimelineFragment extends StatusListFragment{
protected void onStatusCreated(StatusCreatedEvent ev){
if(!AccountSessionManager.getInstance().isSelf(accountID, ev.status.account))
return;
if(filter==GetAccountStatuses.Filter.PINNED) return;
if(filter==GetAccountStatuses.Filter.DEFAULT){
// Keep replies to self, discard all other replies
if(ev.status.inReplyToAccountId!=null && !ev.status.inReplyToAccountId.equals(AccountSessionManager.getInstance().getAccount(accountID).self.id))
@@ -86,4 +89,24 @@ public class AccountTimelineFragment extends StatusListFragment{
}
prependItems(Collections.singletonList(ev.status), true);
}
protected void onStatusUnpinned(StatusUnpinnedEvent ev){
if(!ev.accountID.equals(accountID) || filter!=GetAccountStatuses.Filter.PINNED)
return;
Status status=getStatusByID(ev.id);
data.remove(status);
preloadedData.remove(status);
HeaderStatusDisplayItem item=findItemOfType(ev.id, HeaderStatusDisplayItem.class);
if(item==null)
return;
int index=displayItems.indexOf(item);
int lastIndex;
for(lastIndex=index;lastIndex<displayItems.size();lastIndex++){
if(!displayItems.get(lastIndex).parentID.equals(ev.id))
break;
}
displayItems.subList(index, lastIndex).clear();
adapter.notifyItemRangeRemoved(index, lastIndex-index);
}
}

View File

@@ -0,0 +1,53 @@
package org.joinmastodon.android.fragments;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.requests.accounts.GetBookmarks;
import org.joinmastodon.android.api.session.AccountSession;
import org.joinmastodon.android.api.session.AccountSessionManager;
import org.joinmastodon.android.model.Account;
import org.joinmastodon.android.model.Status;
import java.util.List;
import me.grishka.appkit.api.SimpleCallback;
public class BookmarksListFragment extends StatusListFragment{
private String accountID;
private Account self;
private String lastMaxId=null;
@Override
public void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
accountID=getArguments().getString("account");
AccountSession session=AccountSessionManager.getInstance().getAccount(accountID);
self=session.self;
setTitle(R.string.bookmarks);
}
@Override
protected void onShown(){
super.onShown();
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading)
loadData();
}
@Override
protected void doLoadData(int offset, int count) {
GetBookmarks b=new GetBookmarks(offset>0 ? lastMaxId : null, null, count);
currentRequest=b.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
onDataLoaded(result, b.getMaxId()!=null);
lastMaxId=b.getMaxId();
}
})
.exec(accountID);
}
}

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;
@@ -167,13 +168,21 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
private ImageView sendError;
private View sendingOverlay;
private WindowManager wm;
private StatusPrivacy statusVisibility=StatusPrivacy.PUBLIC;
private StatusPrivacy statusVisibility=StatusPrivacy.UNLISTED;
private ComposeAutocompleteSpan currentAutocompleteSpan;
private FrameLayout mainEditTextWrap;
private ComposeAutocompleteViewController autocompleteViewController;
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));
@@ -453,11 +475,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
if(savedInstanceState==null){
mainEditText.setText(initialText);
mainEditText.setSelection(mainEditText.length());
if(!TextUtils.isEmpty(replyTo.spoilerText) && AccountSessionManager.getInstance().isSelf(accountID, replyTo.account)){
// 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{
@@ -470,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){
@@ -479,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());
@@ -547,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){
@@ -635,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
@@ -1033,7 +1069,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
UiUtils.enablePopupMenuIcons(getActivity(), menu);
m.setGroupCheckable(0, true, true);
m.findItem(switch(statusVisibility){
case PUBLIC, UNLISTED -> R.id.vis_public;
case PUBLIC -> R.id.vis_public;
case UNLISTED -> R.id.vis_unlisted;
case PRIVATE -> R.id.vis_followers;
case DIRECT -> R.id.vis_private;
}).setChecked(true);
@@ -1043,6 +1080,8 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
int id=item.getItemId();
if(id==R.id.vis_public){
statusVisibility=StatusPrivacy.PUBLIC;
}else if(id==R.id.vis_unlisted){
statusVisibility=StatusPrivacy.UNLISTED;
}else if(id==R.id.vis_followers){
statusVisibility=StatusPrivacy.PRIVATE;
}else if(id==R.id.vis_private){

View File

@@ -255,9 +255,14 @@ public class HomeFragment extends AppKitFragment implements OnBackPressedListene
@Override
public boolean onBackPressed(){
if(currentTab==R.id.tab_profile)
return profileFragment.onBackPressed();
if (profileFragment.onBackPressed()) return true;
if(currentTab==R.id.tab_search)
return searchFragment.onBackPressed();
if (searchFragment.onBackPressed()) return true;
if (currentTab!=R.id.tab_home) {
tabBar.selectTab(R.id.tab_home);
onTabSelected(R.id.tab_home);
return true;
}
return false;
}

View File

@@ -101,7 +101,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
private ProgressBarButton actionButton;
private ViewPager2 pager;
private NestedRecyclerScrollView scrollView;
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, mediaFragment;
private AccountTimelineFragment postsFragment, postsWithRepliesFragment, pinnedPostsFragment, mediaFragment;
private ProfileAboutFragment aboutFragment;
private TabLayout tabbar;
private SwipeRefreshLayout refreshLayout;
@@ -209,14 +209,15 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
}
};
tabViews=new FrameLayout[4];
tabViews=new FrameLayout[5];
for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity());
tabView.setId(switch(i){
case 0 -> R.id.profile_posts;
case 1 -> R.id.profile_posts_with_replies;
case 2 -> R.id.profile_media;
case 3 -> R.id.profile_about;
case 2 -> R.id.profile_pinned_posts;
case 3 -> R.id.profile_media;
case 4 -> R.id.profile_about;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
tabView.setVisibility(View.GONE);
@@ -224,7 +225,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
tabViews[i]=tabView;
}
pager.setOffscreenPageLimit(4);
pager.setOffscreenPageLimit(5);
pager.setAdapter(new ProfilePagerAdapter());
pager.getLayoutParams().height=getResources().getDisplayMetrics().heightPixels;
@@ -240,8 +241,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
tab.setText(switch(position){
case 0 -> R.string.posts;
case 1 -> R.string.posts_and_replies;
case 2 -> R.string.media;
case 3 -> R.string.profile_about;
case 2 -> R.string.pinned_posts;
case 3 -> R.string.media;
case 4 -> R.string.profile_about;
default -> throw new IllegalStateException();
});
}
@@ -298,6 +300,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
postsFragment.onRefresh();
if(postsWithRepliesFragment.loaded)
postsWithRepliesFragment.onRefresh();
if(pinnedPostsFragment.loaded)
pinnedPostsFragment.onRefresh();
if(mediaFragment.loaded)
mediaFragment.onRefresh();
}
@@ -322,6 +326,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(postsFragment==null){
postsFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.DEFAULT, true);
postsWithRepliesFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.INCLUDE_REPLIES, false);
pinnedPostsFragment =AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.PINNED, false);
mediaFragment=AccountTimelineFragment.newInstance(accountID, account, GetAccountStatuses.Filter.MEDIA, false);
aboutFragment=new ProfileAboutFragment();
aboutFragment.setFields(fields);
@@ -402,6 +407,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(postsFragment!=null && postsFragment.isAdded() && childInsets!=null){
postsFragment.onApplyWindowInsets(childInsets);
postsWithRepliesFragment.onApplyWindowInsets(childInsets);
pinnedPostsFragment.onApplyWindowInsets(childInsets);
mediaFragment.onApplyWindowInsets(childInsets);
}
}
@@ -516,7 +522,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
if(isOwnProfile){
for(int i=0;i<menu.size();i++){
MenuItem item=menu.getItem(i);
item.setVisible(item.getItemId()==R.id.share);
item.setVisible(item.getItemId()==R.id.share || item.getItemId()==R.id.bookmarks);
}
return;
}
@@ -536,11 +542,16 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public boolean onOptionsItemSelected(MenuItem item){
int id=item.getItemId();
if(id==R.id.share){
Intent intent=new Intent(Intent.ACTION_SEND);
if(id==R.id.share) {
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
intent.putExtra(Intent.EXTRA_TEXT, account.url);
startActivity(Intent.createChooser(intent, item.getTitle()));
}else if(id==R.id.bookmarks) {
Bundle args=new Bundle();
args.putString("account", accountID);
args.putParcelable("profileAccount", Parcels.wrap(account));
Nav.go(getActivity(), BookmarksListFragment.class, args);
}else if(id==R.id.mute){
confirmToggleMuted();
}else if(id==R.id.block){
@@ -644,8 +655,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
return switch(page){
case 0 -> postsFragment;
case 1 -> postsWithRepliesFragment;
case 2 -> mediaFragment;
case 3 -> aboutFragment;
case 2 -> pinnedPostsFragment;
case 3 -> mediaFragment;
case 4 -> aboutFragment;
default -> throw new IllegalStateException();
};
}
@@ -706,9 +718,9 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
invalidateOptionsMenu();
pager.setUserInputEnabled(false);
actionButton.setText(R.string.done);
pager.setCurrentItem(3);
pager.setCurrentItem(4);
ArrayList<Animator> animators=new ArrayList<>();
for(int i=0;i<3;i++){
for(int i=0;i<tabViews.length-1;i++){
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, .3f));
tabbar.getTabAt(i).view.setEnabled(false);
}
@@ -749,7 +761,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
invalidateOptionsMenu();
ArrayList<Animator> animators=new ArrayList<>();
actionButton.setText(R.string.edit_profile);
for(int i=0;i<3;i++){
for(int i=0;i<tabViews.length-1;i++){
animators.add(ObjectAnimator.ofFloat(tabbar.getTabAt(i).view, View.ALPHA, 1f));
}
animators.add(ObjectAnimator.ofInt(avatar.getForeground(), "alpha", 0));
@@ -767,7 +779,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
set.addListener(new AnimatorListenerAdapter(){
@Override
public void onAnimationEnd(Animator animation){
for(int i=0;i<3;i++){
for(int i=0;i<tabViews.length-1;i++){
tabbar.getTabAt(i).view.setEnabled(true);
}
pager.setUserInputEnabled(true);
@@ -944,7 +956,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
@Override
public int getItemCount(){
return loaded ? 4 : 0;
return loaded ? tabViews.length : 0;
}
@Override

View File

@@ -9,6 +9,7 @@ import org.joinmastodon.android.events.PollUpdatedEvent;
import org.joinmastodon.android.events.StatusCountersUpdatedEvent;
import org.joinmastodon.android.events.StatusCreatedEvent;
import org.joinmastodon.android.events.StatusDeletedEvent;
import org.joinmastodon.android.events.StatusUnpinnedEvent;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.displayitems.ExtendedFooterStatusDisplayItem;
import org.joinmastodon.android.ui.displayitems.FooterStatusDisplayItem;
@@ -60,6 +61,8 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
protected void onStatusCreated(StatusCreatedEvent ev){}
protected void onStatusUnpinned(StatusUnpinnedEvent ev){}
protected Status getContentStatusByID(String id){
Status s=getStatusByID(id);
return s==null ? null : s.getContentStatus();
@@ -135,6 +138,11 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>{
StatusListFragment.this.onStatusCreated(ev);
}
@Subscribe
public void onStatusUnpinned(StatusUnpinnedEvent ev){
StatusListFragment.this.onStatusUnpinned(ev);
}
@Subscribe
public void onPollUpdated(PollUpdatedEvent ev){
if(!ev.accountID.equals(accountID))

View File

@@ -51,6 +51,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
private DiscoverAccountsFragment accountsFragment;
private SearchFragment searchFragment;
private LocalTimelineFragment localTimelineFragment;
private FederatedTimelineFragment federatedTimelineFragment;
private String accountID;
private Runnable searchDebouncer=this::onSearchChangedDebounced;
@@ -72,15 +73,16 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
tabLayout=view.findViewById(R.id.tabbar);
pager=view.findViewById(R.id.pager);
tabViews=new FrameLayout[5];
tabViews=new FrameLayout[6];
for(int i=0;i<tabViews.length;i++){
FrameLayout tabView=new FrameLayout(getActivity());
tabView.setId(switch(i){
case 0 -> R.id.discover_posts;
case 1 -> R.id.discover_hashtags;
case 2 -> R.id.discover_news;
case 3 -> R.id.discover_local_timeline;
case 4 -> R.id.discover_users;
case 0 -> R.id.discover_local_timeline;
case 1 -> R.id.discover_federated_timeline;
case 2 -> R.id.discover_hashtags;
case 3 -> R.id.discover_posts;
case 4 -> R.id.discover_news;
case 5 -> R.id.discover_users;
default -> throw new IllegalStateException("Unexpected value: "+i);
});
tabView.setVisibility(View.GONE);
@@ -106,7 +108,7 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
}
});
if(postsFragment==null){
if(localTimelineFragment==null){
Bundle args=new Bundle();
args.putString("account", accountID);
args.putBoolean("__is_tab", true);
@@ -126,9 +128,13 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
localTimelineFragment=new LocalTimelineFragment();
localTimelineFragment.setArguments(args);
federatedTimelineFragment=new FederatedTimelineFragment();
federatedTimelineFragment.setArguments(args);
getChildFragmentManager().beginTransaction()
.add(R.id.discover_posts, postsFragment)
.add(R.id.discover_local_timeline, localTimelineFragment)
.add(R.id.discover_federated_timeline, federatedTimelineFragment)
.add(R.id.discover_hashtags, hashtagsFragment)
.add(R.id.discover_news, newsFragment)
.add(R.id.discover_users, accountsFragment)
@@ -139,11 +145,12 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
@Override
public void onConfigureTab(@NonNull TabLayout.Tab tab, int position){
tab.setText(switch(position){
case 0 -> R.string.posts;
case 1 -> R.string.hashtags;
case 2 -> R.string.news;
case 3 -> R.string.local_timeline;
case 4 -> R.string.for_you;
case 0 -> R.string.local_timeline;
case 1 -> R.string.federated_timeline;
case 2 -> R.string.hashtags;
case 3 -> R.string.posts;
case 4 -> R.string.news;
case 5 -> R.string.for_you;
default -> throw new IllegalStateException("Unexpected value: "+position);
});
tab.view.textView.setAllCaps(true);
@@ -229,8 +236,8 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
}
public void loadData(){
if(postsFragment!=null && !postsFragment.loaded && !postsFragment.dataLoading)
postsFragment.loadData();
if(localTimelineFragment!=null && !localTimelineFragment.loaded && !localTimelineFragment.dataLoading)
localTimelineFragment.loadData();
}
private void onSearchEditFocusChanged(View v, boolean hasFocus){
@@ -266,11 +273,12 @@ public class DiscoverFragment extends AppKitFragment implements ScrollableToTop,
private Fragment getFragmentForPage(int page){
return switch(page){
case 0 -> postsFragment;
case 1 -> hashtagsFragment;
case 2 -> newsFragment;
case 3 -> localTimelineFragment;
case 4 -> accountsFragment;
case 0 -> localTimelineFragment;
case 1 -> federatedTimelineFragment;
case 2 -> hashtagsFragment;
case 3 -> postsFragment;
case 4 -> newsFragment;
case 5 -> accountsFragment;
default -> throw new IllegalStateException("Unexpected value: "+page);
};
}

View File

@@ -0,0 +1,41 @@
package org.joinmastodon.android.fragments.discover;
import android.os.Bundle;
import android.view.View;
import org.joinmastodon.android.api.requests.timelines.GetPublicTimeline;
import org.joinmastodon.android.fragments.StatusListFragment;
import org.joinmastodon.android.model.Filter;
import org.joinmastodon.android.model.Status;
import org.joinmastodon.android.ui.utils.DiscoverInfoBannerHelper;
import org.joinmastodon.android.utils.StatusFilterPredicate;
import java.util.List;
import java.util.stream.Collectors;
import me.grishka.appkit.api.SimpleCallback;
public class FederatedTimelineFragment extends StatusListFragment{
private DiscoverInfoBannerHelper bannerHelper=new DiscoverInfoBannerHelper(DiscoverInfoBannerHelper.BannerType.FEDERATED_TIMELINE);
private String maxID;
@Override
protected void doLoadData(int offset, int count){
currentRequest=new GetPublicTimeline(false, false, refreshing ? null : maxID, count)
.setCallback(new SimpleCallback<>(this){
@Override
public void onSuccess(List<Status> result){
if(!result.isEmpty())
maxID=result.get(result.size()-1).id;
onDataLoaded(result.stream().filter(new StatusFilterPredicate(accountID, Filter.FilterContext.PUBLIC)).collect(Collectors.toList()), !result.isEmpty());
}
})
.exec(accountID);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState){
super.onViewCreated(view, savedInstanceState);
bannerHelper.maybeAddBanner(contentWrap);
}
}

View File

@@ -126,6 +126,7 @@ public class Status extends BaseModel implements DisplayItemsParent{
repliesCount=ev.replies;
favourited=ev.favorited;
reblogged=ev.reblogged;
pinned=ev.pinned;
}
public Status getContentStatus(){

View File

@@ -0,0 +1,87 @@
package org.joinmastodon.android.ui;
import android.app.Activity;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.LinearLayoutManager;
import org.joinmastodon.android.R;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.ui.utils.UiUtils;
import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
import me.grishka.appkit.utils.V;
import me.grishka.appkit.views.BottomSheet;
import me.grishka.appkit.views.UsableRecyclerView;
public class ImageDescriptionSheet extends BottomSheet{
private UsableRecyclerView list;
public ImageDescriptionSheet(@NonNull Activity activity, Attachment attachment){
super(activity);
View handleView=new View(activity);
handleView.setBackgroundResource(R.drawable.bg_bottom_sheet_handle);
ViewGroup handle=new FrameLayout(activity);
handle.addView(handleView);
handle.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, V.dp(24)));
TextView textView = new TextView(activity);
if (attachment.description == null || attachment.description.isEmpty()) {
textView.setText(R.string.media_no_description);
textView.setTypeface(null, Typeface.ITALIC);
} else {
textView.setText(attachment.description);
textView.setTextIsSelectable(true);
}
TextView heading=new TextView(activity);
heading.setText(R.string.image_description);
heading.setAllCaps(true);
heading.setTypeface(null, Typeface.BOLD);
heading.setPadding(0, V.dp(24), 0, V.dp(8));
LinearLayout linearLayout = new LinearLayout(activity);
linearLayout.setOrientation(LinearLayout.VERTICAL);
linearLayout.setPadding(V.dp(24), 0, V.dp(24), 0);
linearLayout.addView(heading);
linearLayout.addView(textView);
FrameLayout layout=new FrameLayout(activity);
layout.addView(handle);
layout.addView(linearLayout);
list=new UsableRecyclerView(activity);
list.setLayoutManager(new LinearLayoutManager(activity));
list.setBackgroundResource(R.drawable.bg_bottom_sheet);
list.setAdapter(new SingleViewRecyclerAdapter(layout));
list.setClipToPadding(false);
setContentView(list);
setNavigationBarBackground(new ColorDrawable(UiUtils.getThemeColor(activity, R.attr.colorWindowBackground)), !UiUtils.isDarkTheme());
}
@Override
protected void onWindowInsetsUpdated(WindowInsets insets){
if(Build.VERSION.SDK_INT>=29){
int tappableBottom=insets.getTappableElementInsets().bottom;
int insetBottom=insets.getSystemWindowInsetBottom();
if(tappableBottom==0 && insetBottom>0){
list.setPadding(0, 0, 0, V.dp(48)-insetBottom);
}else{
list.setPadding(0, 0, 0, V.dp(24));
}
}else{
list.setPadding(0, 0, 0, V.dp(24));
}
}
}

View File

@@ -43,7 +43,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
}
public static class Holder extends StatusDisplayItem.Holder<FooterStatusDisplayItem>{
private final TextView reply, boost, favorite;
private final TextView reply, boost, favorite, bookmark;
private final ImageView share;
private final View.AccessibilityDelegate buttonAccessibilityDelegate=new View.AccessibilityDelegate(){
@@ -60,22 +60,27 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
reply=findViewById(R.id.reply);
boost=findViewById(R.id.boost);
favorite=findViewById(R.id.favorite);
bookmark=findViewById(R.id.bookmark);
share=findViewById(R.id.share);
if(Build.VERSION.SDK_INT<Build.VERSION_CODES.N){
UiUtils.fixCompoundDrawableTintOnAndroid6(reply);
UiUtils.fixCompoundDrawableTintOnAndroid6(boost);
UiUtils.fixCompoundDrawableTintOnAndroid6(favorite);
UiUtils.fixCompoundDrawableTintOnAndroid6(bookmark);
}
View reply=findViewById(R.id.reply_btn);
View boost=findViewById(R.id.boost_btn);
View favorite=findViewById(R.id.favorite_btn);
View share=findViewById(R.id.share_btn);
View bookmark=findViewById(R.id.bookmark_btn);
reply.setOnClickListener(this::onReplyClick);
reply.setAccessibilityDelegate(buttonAccessibilityDelegate);
boost.setOnClickListener(this::onBoostClick);
boost.setAccessibilityDelegate(buttonAccessibilityDelegate);
favorite.setOnClickListener(this::onFavoriteClick);
favorite.setAccessibilityDelegate(buttonAccessibilityDelegate);
bookmark.setOnClickListener(this::onBookmarkClick);
bookmark.setAccessibilityDelegate(buttonAccessibilityDelegate);
share.setOnClickListener(this::onShareClick);
share.setAccessibilityDelegate(buttonAccessibilityDelegate);
}
@@ -87,6 +92,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
bindButton(favorite, item.status.favouritesCount);
boost.setSelected(item.status.reblogged);
favorite.setSelected(item.status.favourited);
bookmark.setSelected(item.status.bookmarked);
boost.setEnabled(item.status.visibility==StatusPrivacy.PUBLIC || item.status.visibility==StatusPrivacy.UNLISTED
|| (item.status.visibility==StatusPrivacy.PRIVATE && item.status.account.id.equals(AccountSessionManager.getInstance().getAccount(item.accountID).self.id)));
}
@@ -120,6 +126,11 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
bindButton(favorite, item.status.favouritesCount);
}
private void onBookmarkClick(View v){
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked);
bookmark.setSelected(item.status.bookmarked);
}
private void onShareClick(View v){
Intent intent=new Intent(Intent.ACTION_SEND);
intent.setType("text/plain");
@@ -134,6 +145,8 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
return R.string.button_reblog;
if(id==R.id.favorite_btn)
return R.string.button_favorite;
if(id==R.id.bookmark_btn)
return R.string.button_bookmark;
if(id==R.id.share_btn)
return R.string.button_share;
return 0;

View File

@@ -137,6 +137,10 @@ 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){
UiUtils.confirmToggleMuteUser(item.parentFragment.getActivity(), item.parentFragment.getAccountID(), account, relationship!=null && relationship.muting, r->{});
}else if(id==R.id.block){
@@ -250,6 +254,9 @@ 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);
MenuItem blockDomain=menu.findItem(R.id.block_domain);
MenuItem mute=menu.findItem(R.id.mute);

View File

@@ -19,6 +19,7 @@ import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.opengl.Visibility;
import android.os.Build;
import android.os.Environment;
import android.os.SystemClock;
@@ -48,6 +49,7 @@ import android.widget.Toolbar;
import org.joinmastodon.android.R;
import org.joinmastodon.android.api.MastodonAPIController;
import org.joinmastodon.android.model.Attachment;
import org.joinmastodon.android.ui.ImageDescriptionSheet;
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
import java.io.File;
@@ -97,6 +99,7 @@ public class PhotoViewer implements ZoomPanView.Listener{
private TextView videoTimeView;
private ImageButton videoPlayPauseButton;
private View videoControls;
private MenuItem imageDescriptionButton;
private boolean uiVisible=true;
private AudioManager.OnAudioFocusChangeListener audioFocusListener=this::onAudioFocusChanged;
private Runnable uiAutoHider=()->{
@@ -174,11 +177,24 @@ public class PhotoViewer implements ZoomPanView.Listener{
toolbarWrap=uiOverlay.findViewById(R.id.toolbar_wrap);
toolbar=uiOverlay.findViewById(R.id.toolbar);
toolbar.setNavigationOnClickListener(v->onStartSwipeToDismissTransition(0));
toolbar.getMenu().add(R.string.download).setIcon(R.drawable.ic_fluent_arrow_download_24_regular).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
toolbar.setOnMenuItemClickListener(item->{
saveCurrentFile();
return true;
});
imageDescriptionButton = toolbar.getMenu()
.add(R.string.image_description)
.setIcon(R.drawable.ic_fluent_image_alt_text_24_regular)
.setVisible(attachments.get(pager.getCurrentItem()).description != null
&& !attachments.get(pager.getCurrentItem()).description.isEmpty())
.setOnMenuItemClickListener(item -> {
new ImageDescriptionSheet(activity,attachments.get(pager.getCurrentItem())).show();
return true;
});
imageDescriptionButton.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
toolbar.getMenu()
.add(R.string.download)
.setIcon(R.drawable.ic_fluent_arrow_download_24_regular)
.setOnMenuItemClickListener(item -> {
saveCurrentFile();
return true;
})
.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
uiOverlay.setAlpha(0f);
videoControls=uiOverlay.findViewById(R.id.video_player_controls);
videoSeekBar=uiOverlay.findViewById(R.id.seekbar);
@@ -374,6 +390,8 @@ public class PhotoViewer implements ZoomPanView.Listener{
private void onPageChanged(int index){
currentIndex=index;
Attachment att=attachments.get(index);
imageDescriptionButton.setVisible(att.description != null && !att.description.isEmpty());
toolbar.invalidate();
V.setVisibilityAnimated(videoControls, att.type==Attachment.Type.VIDEO ? View.VISIBLE : View.GONE);
if(att.type==Attachment.Type.VIDEO){
videoSeekBar.setSecondaryProgress(0);

View File

@@ -36,6 +36,7 @@ public class DiscoverInfoBannerHelper{
case TRENDING_HASHTAGS -> R.string.trending_hashtags_info_banner;
case TRENDING_LINKS -> R.string.trending_links_info_banner;
case LOCAL_TIMELINE -> R.string.local_timeline_info_banner;
case FEDERATED_TIMELINE -> R.string.federated_timeline_info_banner;
});
}
}
@@ -59,6 +60,7 @@ public class DiscoverInfoBannerHelper{
TRENDING_HASHTAGS,
TRENDING_LINKS,
LOCAL_TIMELINE,
FEDERATED_TIMELINE,
// ACCOUNTS
}
}

View File

@@ -11,7 +11,6 @@ import android.content.res.TypedArray;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
@@ -20,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;
@@ -42,8 +42,13 @@ import org.joinmastodon.android.api.requests.accounts.SetAccountMuted;
import org.joinmastodon.android.api.requests.accounts.SetDomainBlocked;
import org.joinmastodon.android.api.requests.statuses.DeleteStatus;
import org.joinmastodon.android.api.requests.statuses.GetStatusByID;
import org.joinmastodon.android.api.requests.statuses.SetStatusPinned;
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;
@@ -51,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;
@@ -62,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;
@@ -74,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;
@@ -352,6 +365,88 @@ public class UiUtils{
});
}
public static void confirmPinPost(Activity activity, String accountID, Status status, boolean pinned, Consumer<Status> resultCallback){
showConfirmationAlert(activity,
pinned ? R.string.confirm_pin_post_title : R.string.confirm_unpin_post_title,
pinned ? R.string.confirm_pin_post : R.string.confirm_unpin_post,
pinned ? R.string.pin_post : R.string.unpin_post,
()->{
new SetStatusPinned(status.id, pinned)
.setCallback(new Callback<>() {
@Override
public void onSuccess(Status result) {
resultCallback.accept(result);
E.post(new StatusCountersUpdatedEvent(result));
if (!result.pinned)
E.post(new StatusUnpinnedEvent(status.id, accountID));
}
@Override
public void onError(ErrorResponse error) {
error.showToast(activity);
}
})
.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){
boolean secondaryStyle;
if(relationship.blocking){

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/bookmark_selected" android:state_selected="true"/>
<item android:color="?android:textColorSecondary"/>
</selector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M6.19 21.855c-0.495 0.357-1.187 0.002-1.187-0.61V6.25C5.003 4.455 6.458 3 8.253 3h7.498c1.795 0 3.25 1.455 3.25 3.25v14.996c0 0.611-0.692 0.966-1.188 0.609l-5.81-4.181-5.812 4.18z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M6.19 21.855c-0.495 0.357-1.187 0.002-1.187-0.61V6.25C5.003 4.455 6.458 3 8.253 3h7.498c1.795 0 3.25 1.455 3.25 3.25v14.996c0 0.611-0.692 0.966-1.188 0.609l-5.81-4.181-5.812 4.18zM17.502 6.25c0-0.966-0.783-1.75-1.75-1.75H8.253c-0.967 0-1.75 0.784-1.75 1.75v13.532l5.061-3.641c0.262-0.188 0.614-0.188 0.876 0l5.061 3.641V6.25z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<!--~ Copyright (c) 2022. ~ Microsoft Corporation. All rights reserved.-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_fluent_bookmark_24_filled" android:state_activated="true"/>
<item android:drawable="@drawable/ic_fluent_bookmark_24_filled" android:state_checked="true"/>
<item android:drawable="@drawable/ic_fluent_bookmark_24_filled" android:state_selected="true"/>
<item android:drawable="@drawable/ic_fluent_bookmark_24_regular"/>
</selector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M4 6.748c0-1.243 1.007-2.25 2.25-2.25h9c1.243 0 2.25 1.007 2.25 2.25V21.25c0 0.268-0.143 0.517-0.376 0.65-0.233 0.134-0.52 0.133-0.751-0.002l-5.623-3.28-5.622 3.28c-0.232 0.135-0.519 0.136-0.752 0.002C4.144 21.767 4 21.52 4 21.25V6.748zM15.25 2C17.873 2 20 4.127 20 6.75v11.873c0 0.414-0.336 0.75-0.75 0.75s-0.75-0.336-0.75-0.75V6.751c0-1.796-1.455-3.25-3.25-3.25H6.637S6.75 2.942 7.434 2.42C8 2 8.602 2 8.602 2h6.648z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:pathData="M1 3c0-1.105 0.895-2 2-2h7c1.105 0 2 0.895 2 2v6c0 1.105-0.895 2-2 2H3c-1.105 0-2-0.895-2-2V3zm2.5 1C3.224 4 3 4.224 3 4.5S3.224 5 3.5 5h6C9.776 5 10 4.776 10 4.5S9.776 4 9.5 4h-6zm0 3C3.224 7 3 7.224 3 7.5S3.224 8 3.5 8h6C9.776 8 10 7.776 10 7.5S9.776 7 9.5 7h-6zM4 12h1.5v6.75c0 0.208 0.036 0.408 0.103 0.594l5.823-5.701c0.833-0.816 2.142-0.854 3.02-0.116l0.128 0.116 5.822 5.702c0.067-0.186 0.104-0.386 0.104-0.595V7.25c0-0.966-0.784-1.75-1.75-1.75H13V4h5.75C20.545 4 22 5.455 22 7.25v11.5c0 1.795-1.455 3.25-3.25 3.25H7.25C5.455 22 4 20.545 4 18.75V12zm15.33 8.401l-5.805-5.686c-0.265-0.26-0.675-0.283-0.966-0.071l-0.084 0.07-5.807 5.687C6.85 20.465 7.046 20.5 7.25 20.5h11.5c0.203 0 0.399-0.035 0.58-0.099zM16.253 7.5c1.244 0 2.252 1.008 2.252 2.252 0 1.244-1.008 2.252-2.252 2.252-1.244 0-2.252-1.008-2.252-2.252C14 8.508 15.008 7.5 16.252 7.5zm0 1.5C15.837 9 15.5 9.337 15.5 9.752s0.337 0.752 0.752 0.752c0.416 0 0.752-0.336 0.752-0.752C17.004 9.337 16.667 9 16.252 9z" android:fillColor="@color/fluent_default_icon_tint"/>
</vector>

View File

@@ -73,6 +73,28 @@
tools:text="123"/>
</FrameLayout>
<Space
android:layout_width="0px"
android:layout_height="1px"
android:layout_weight="1"/>
<FrameLayout
android:id="@+id/bookmark_btn"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:minWidth="56dp">
<TextView
android:id="@+id/bookmark"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:layout_gravity="center"
android:drawableStart="@drawable/ic_fluent_bookmark_24_selector"
android:drawablePadding="8dp"
android:drawableTint="@color/bookmark_icon"
android:gravity="center_vertical"
android:textAppearance="@style/m3_label_large" />
</FrameLayout>
<Space
android:layout_width="0px"
android:layout_height="1px"

View File

@@ -9,20 +9,21 @@
android:layout_height="wrap_content"
android:orientation="vertical">
<org.joinmastodon.android.ui.views.ComposeMediaLayout
android:layout_width="wrap_content"
<org.joinmastodon.android.ui.views.MaxWidthFrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal">
android:layout_gravity="center"
android:maxWidth="400dp">
<ImageView
android:id="@+id/photo"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:importantForAccessibility="no"
tools:src="#0f0"/>
</org.joinmastodon.android.ui.views.ComposeMediaLayout>
</org.joinmastodon.android.ui.views.MaxWidthFrameLayout>
<TextView
android:id="@+id/title"

View File

@@ -3,6 +3,9 @@
<item android:id="@+id/vis_public"
android:icon="@drawable/ic_fluent_earth_24_filled"
android:title="@string/visibility_public"/>
<item android:id="@+id/vis_unlisted"
android:icon="@drawable/ic_fluent_people_community_24_regular"
android:title="@string/visibility_unlisted"/>
<item android:id="@+id/vis_followers"
android:icon="@drawable/ic_fluent_people_checkmark_24_regular"
android:title="@string/visibility_followers_only"/>

View File

@@ -1,6 +1,9 @@
<?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"/>
<item android:id="@+id/block" android:title="@string/block_user"/>
<item android:id="@+id/block_domain" android:title="@string/block_domain"/>

View File

@@ -7,4 +7,10 @@
<item android:id="@+id/block_domain" android:title="@string/block_domain"/>
<item android:id="@+id/hide_boosts" android:title="@string/hide_boosts_from_user"/>
<item android:id="@+id/open_in_browser" android:title="@string/open_in_browser"/>
<item
android:id="@+id/bookmarks"
android:showAsAction="always"
android:visible="false"
android:icon="@drawable/ic_fluent_bookmark_multiple_24_filled"
android:title="@string/bookmarks"/>
</menu>

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

@@ -42,6 +42,7 @@
</plurals>
<string name="posts">Beiträge</string>
<string name="posts_and_replies">Beiträge und Antworten</string>
<string name="pinned_posts">Angeheftet</string>
<string name="media">Medien</string>
<string name="profile_about">Über</string>
<string name="button_follow">Folgen</string>
@@ -121,9 +122,20 @@
<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>
<string name="confirm_pin_post">Möchtest du den Beitrag an dein Profil anheften?</string>
<string name="pinning">Wird angeheftet…</string>
<string name="unpin_post">Von Profil lösen</string>
<string name="confirm_unpin_post_title">Angehefteten Beitrag von Profil lösen</string>
<string name="confirm_unpin_post">Bist du dir sicher, dass du den angehefteten Beitrag von deinem Profil lösen möchtest?</string>
<string name="unpinning">Wird vom Profil gelöst…</string>
<string name="notification_channel_audio_player">Audiowiedergabe</string>
<string name="play">Abspielen</string>
<string name="pause">Pausieren</string>
@@ -203,6 +215,7 @@
<string name="resent_email">Bestätigungs-E-Mail gesendet</string>
<string name="compose_hint">Was gibt\'s Neues?</string>
<string name="content_warning">Inhaltswarnung</string>
<string name="image_description">Bildbeschreibung</string>
<string name="add_image_description">Füge eine Bildbeschreibung hinzu…</string>
<string name="retry_upload">Upload erneut versuchen</string>
<string name="image_upload_failed">Fehler beim Hochladen des Bildes</string>
@@ -213,6 +226,7 @@
<string name="alt_text_subtitle">Alternativtext erscheint für blinde Menschen. Versuche, nur so viele Details einzubeziehen, um den Kontext zu verstehen.</string>
<string name="alt_text_hint">z.B. Eine Giraffe auf einem Dreirad während sie eine Banane isst</string>
<string name="visibility_public">Öffentlich</string>
<string name="visibility_unlisted">Nicht gelistet</string>
<string name="visibility_followers_only">Nur Folgende</string>
<string name="visibility_private">Nur Leute, die ich erwähne</string>
<string name="search_all">Alle</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>
@@ -92,6 +92,7 @@
<color name="highlight_over_light">#18000000</color>
<color name="favorite_selected">@color/warning_500</color>
<color name="bookmark_selected">@color/success_500</color>
<color name="boost_selected">@color/primary_500</color>
<color name="shortcut_icon_background">@color/gray_100</color>

View File

@@ -4,6 +4,7 @@
<item name="profile_posts" type="id"/>
<item name="profile_posts_with_replies" type="id"/>
<item name="profile_pinned_posts" type="id"/>
<item name="profile_media" type="id"/>
<item name="profile_about" type="id"/>
@@ -12,6 +13,7 @@
<item name="discover_news" type="id"/>
<item name="discover_users" type="id"/>
<item name="discover_local_timeline" type="id"/>
<item name="discover_federated_timeline" type="id"/>
<item name="notifications_all" type="id"/>
<item name="notifications_mentions" type="id"/>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">Mastodon</string>
<string name="app_name" translatable="false">Mastadon</string>
<string name="get_started">Get started</string>
<string name="log_in">Log in</string>
@@ -47,6 +47,7 @@
</plurals>
<string name="posts">Posts</string>
<string name="posts_and_replies">Posts and Replies</string>
<string name="pinned_posts">Pinned</string>
<string name="media">Media</string>
<string name="profile_about">About</string>
<string name="button_follow">Follow</string>
@@ -126,9 +127,20 @@
<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>
<string name="confirm_pin_post">Do you want to pin this post to your profile?</string>
<string name="pinning">Pinning post…</string>
<string name="unpin_post">Unpin from profile</string>
<string name="confirm_unpin_post_title">Unpin post from profile</string>
<string name="confirm_unpin_post">Are you sure you want to unpin this post?</string>
<string name="unpinning">Unpinning post…</string>
<string name="notification_channel_audio_player">Audio playback</string>
<string name="play">Play</string>
<string name="pause">Pause</string>
@@ -209,6 +221,7 @@
<string name="compose_hint">Type or paste what\'s on your mind</string>
<string name="content_warning">Content warning</string>
<string name="add_image_description">Add image description…</string>
<string name="image_description">Image description</string>
<string name="retry_upload">Retry upload</string>
<string name="image_upload_failed">Image failed to upload</string>
<string name="video_upload_failed">Video failed to upload</string>
@@ -218,6 +231,7 @@
<string name="alt_text_subtitle">Alt text describes your photos for people with low or no vision. Try to only include enough detail to understand the context.</string>
<string name="alt_text_hint">e.g. A dog looking around suspiciously with narrowed eyes at the camera.</string>
<string name="visibility_public">Public</string>
<string name="visibility_unlisted">Unlisted</string>
<string name="visibility_followers_only">Followers only</string>
<string name="visibility_private">Only people I mention</string>
<string name="search_all">All</string>
@@ -278,6 +292,8 @@
<string name="button_reblog">Reblog</string>
<string name="button_favorite">Favorite</string>
<string name="button_share">Share</string>
<string name="button_bookmark">Bookmark</string>
<string name="bookmarks">Bookmarks</string>
<string name="media_no_description">Media without description</string>
<string name="add_media">Add media</string>
<string name="add_poll">Add a poll</string>
@@ -307,10 +323,12 @@
<string name="downloading">Downloading…</string>
<string name="no_app_to_handle_action">There\'s no app to handle this action</string>
<string name="local_timeline">Community</string>
<string name="federated_timeline">Federation</string>
<string name="trending_posts_info_banner">These are the posts gaining traction in your corner of Mastodon.</string>
<string name="trending_hashtags_info_banner">These are the hashtags gaining traction in your corner of Mastodon.</string>
<string name="trending_links_info_banner">These are the news stories being shared the most in your corner of Mastodon.</string>
<string name="local_timeline_info_banner">These are the most recent posts by the people who use the same Mastodon server as you.</string>
<string name="federated_timeline_info_banner">These are the most recent posts by the people in your federation.</string>
<string name="dismiss">Dismiss</string>
<string name="see_new_posts">See new posts</string>
<string name="load_missing_posts">Load missing posts</string>