Compare commits
52 Commits
v2.1.6+for
...
v2.1.6+for
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36699e0ab1 | ||
|
|
1a382606d7 | ||
|
|
2522c9c428 | ||
|
|
d1ee12f248 | ||
|
|
d5f9c19fd3 | ||
|
|
dc700d9e37 | ||
|
|
6eb87149e2 | ||
|
|
79a0f8a891 | ||
|
|
26d41b006f | ||
|
|
60d9dc22d2 | ||
|
|
c0bf78fa7c | ||
|
|
f0c2bd0fe6 | ||
|
|
235cfd1e80 | ||
|
|
c96931ba72 | ||
|
|
2c654a28d6 | ||
|
|
72d3fc84f5 | ||
|
|
3b77604ae6 | ||
|
|
4e5f49b37d | ||
|
|
a89c075082 | ||
|
|
99e881bb95 | ||
|
|
4c585fc5d0 | ||
|
|
ae934b5167 | ||
|
|
6b234209c6 | ||
|
|
786ce78b08 | ||
|
|
cde332684e | ||
|
|
d51e06b61f | ||
|
|
0c376d57e7 | ||
|
|
ee82772fee | ||
|
|
d5b6750abe | ||
|
|
70328217c6 | ||
|
|
733fb3f53a | ||
|
|
f82eb96a35 | ||
|
|
148952b96c | ||
|
|
52928e1577 | ||
|
|
cd13777c06 | ||
|
|
b21e2acdb6 | ||
|
|
2b65aeb8b4 | ||
|
|
a8dcb11094 | ||
|
|
aa42873274 | ||
|
|
8a5b36db96 | ||
|
|
c85af5502d | ||
|
|
06698d3c52 | ||
|
|
2818672cda | ||
|
|
24794f28aa | ||
|
|
50d1523210 | ||
|
|
613cd2a1ea | ||
|
|
295cb74287 | ||
|
|
95dd3ff068 | ||
|
|
d6aeb753fc | ||
|
|
a085744038 | ||
|
|
26391a6f14 | ||
|
|
4cf734ce9a |
@@ -15,8 +15,8 @@ android {
|
||||
applicationId "org.joinmastodon.android.sk"
|
||||
minSdk 23
|
||||
targetSdk 33
|
||||
versionCode 108
|
||||
versionName "2.1.6+fork.108"
|
||||
versionCode 109
|
||||
versionName "2.1.6+fork.109"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
resourceConfigurations += ['ar-rSA', 'ar-rDZ', 'be-rBY', 'bn-rBD', 'bs-rBA', 'ca-rES', 'cs-rCZ', 'da-rDK', 'de-rDE', 'el-rGR', 'es-rES', 'eu-rES', 'fa-rIR', 'fi-rFI', 'fil-rPH', 'fr-rFR', 'ga-rIE', 'gd-rGB', 'gl-rES', 'hi-rIN', 'hr-rHR', 'hu-rHU', 'hy-rAM', 'ig-rNG', 'in-rID', 'is-rIS', 'it-rIT', 'iw-rIL', 'ja-rJP', 'kab', 'ko-rKR', 'my-rMM', 'nl-rNL', 'no-rNO', 'oc-rFR', 'pl-rPL', 'pt-rBR', 'pt-rPT', 'ro-rRO', 'ru-rRU', 'si-rLK', 'sl-rSI', 'sv-rSE', 'th-rTH', 'tr-rTR', 'uk-rUA', 'ur-rIN', 'vi-rVN', 'zh-rCN', 'zh-rTW']
|
||||
}
|
||||
@@ -33,7 +33,6 @@ android {
|
||||
applicationIdSuffix '.debug'
|
||||
}
|
||||
githubRelease { initWith release }
|
||||
playRelease { initWith release }
|
||||
fdroidRelease { initWith release }
|
||||
}
|
||||
compileOptions {
|
||||
|
||||
@@ -257,5 +257,9 @@ public class UiUtilsTest {
|
||||
assertEquals("* (asterisk)", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
|
||||
makeField("pronouns", "-- * (asterisk) --")
|
||||
)).orElseThrow());
|
||||
|
||||
assertEquals("they/(she?)", UiUtils.extractPronouns(MastodonApp.context, fakeAccount(
|
||||
makeField("pronouns", "they/(she?)...")
|
||||
)).orElseThrow());
|
||||
}
|
||||
}
|
||||
202
mastodon/src/main/java/name/fraser/neil/plaintext/LICENSE
Normal file
202
mastodon/src/main/java/name/fraser/neil/plaintext/LICENSE
Normal file
@@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -31,18 +31,44 @@ import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
import org.parceler.Parcels;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.time.Instant;
|
||||
|
||||
import me.grishka.appkit.FragmentStackActivity;
|
||||
import me.grishka.appkit.Nav;
|
||||
import me.grishka.appkit.api.Callback;
|
||||
import me.grishka.appkit.api.ErrorResponse;
|
||||
|
||||
public class MainActivity extends FragmentStackActivity implements ProvidesAssistContent {
|
||||
private static final String TAG="MainActivity";
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState){
|
||||
AccountSession session=getCurrentSession();
|
||||
UiUtils.setUserPreferredTheme(this, session);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
Thread.UncaughtExceptionHandler defaultHandler=Thread.getDefaultUncaughtExceptionHandler();
|
||||
Thread.setDefaultUncaughtExceptionHandler((t, e)->{
|
||||
File file=new File(MastodonApp.context.getFilesDir(), "crash.log");
|
||||
try(FileOutputStream out=new FileOutputStream(file)){
|
||||
PrintWriter writer=new PrintWriter(out);
|
||||
writer.println(BuildConfig.VERSION_NAME+" ("+BuildConfig.VERSION_CODE+")");
|
||||
writer.println(Instant.now().toString());
|
||||
writer.println();
|
||||
e.printStackTrace(writer);
|
||||
writer.flush();
|
||||
}catch(IOException x){
|
||||
Log.e(TAG, "Error writing crash.log", x);
|
||||
}finally{
|
||||
defaultHandler.uncaughtException(t, e);
|
||||
}
|
||||
});
|
||||
|
||||
if(savedInstanceState==null){
|
||||
restartHomeFragment();
|
||||
}
|
||||
|
||||
@@ -53,7 +53,9 @@ public class MastodonAPIController{
|
||||
.registerTypeAdapter(Status.class, new Status.StatusDeserializer())
|
||||
.create();
|
||||
private static WorkerThread thread=new WorkerThread("MastodonAPIController");
|
||||
private static OkHttpClient httpClient=new OkHttpClient.Builder().build();
|
||||
private static OkHttpClient httpClient=new OkHttpClient.Builder()
|
||||
.readTimeout(30, TimeUnit.SECONDS)
|
||||
.build();
|
||||
|
||||
private AccountSession session;
|
||||
private static List<String> badDomains = new ArrayList<>();
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.joinmastodon.android.api.requests.accounts;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
|
||||
public class SetPrivateNote extends MastodonAPIRequest<Relationship>{
|
||||
public SetPrivateNote(String id, String comment){
|
||||
super(MastodonAPIRequest.HttpMethod.POST, "/accounts/"+id+"/note", Relationship.class);
|
||||
Request req = new Request(comment);
|
||||
setRequestBody(req);
|
||||
}
|
||||
|
||||
private static class Request{
|
||||
public String comment;
|
||||
public Request(String comment){
|
||||
this.comment=comment;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package org.joinmastodon.android.api.requests.statuses;
|
||||
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.model.AkkomaTranslation;
|
||||
|
||||
public class AkkomaTranslateStatus extends MastodonAPIRequest<AkkomaTranslation>{
|
||||
public AkkomaTranslateStatus(String id, String lang){
|
||||
super(HttpMethod.GET, "/statuses/"+id+"/translations/"+lang.toUpperCase(), AkkomaTranslation.class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ public class CreateStatus extends MastodonAPIRequest<Status>{
|
||||
public String quoteId;
|
||||
public ContentType contentType;
|
||||
|
||||
public boolean preview;
|
||||
|
||||
public static class Poll{
|
||||
public ArrayList<String> options=new ArrayList<>();
|
||||
public int expiresIn;
|
||||
|
||||
@@ -26,6 +26,8 @@ public class GetStatusEditHistory extends MastodonAPIRequest<List<Status>>{
|
||||
s.visibility=StatusPrivacy.PUBLIC;
|
||||
s.mentions=Collections.emptyList();
|
||||
s.tags=Collections.emptyList();
|
||||
if (s.poll != null)
|
||||
s.poll.id="fakeID"+i;
|
||||
i++;
|
||||
}
|
||||
super.validateAndPostprocessResponse(respObj, httpResponse);
|
||||
|
||||
@@ -260,11 +260,13 @@ public class AccountSession{
|
||||
}
|
||||
|
||||
private boolean isFilteredType(Status s){
|
||||
AccountLocalPreferences localPreferences = getLocalPreferences();
|
||||
return (!localPreferences.showReplies && s.inReplyToId != null)
|
||||
|| (!localPreferences.showBoosts && s.reblog != null);
|
||||
}
|
||||
|
||||
public <T> void filterStatusContainingObjects(List<T> objects, Function<T, Status> extractor, FilterContext context, Account profile){
|
||||
AccountLocalPreferences localPreferences = getLocalPreferences();
|
||||
if(!localPreferences.serverSideFiltersSupported) for(T obj:objects){
|
||||
Status s=extractor.apply(obj);
|
||||
if(s!=null && s.filtered!=null){
|
||||
@@ -307,7 +309,7 @@ public class AccountSession{
|
||||
if(isFilteredType(s) && (context == FilterContext.HOME || context == FilterContext.PUBLIC))
|
||||
return true;
|
||||
// Even with server-side filters, clients are expected to remove statuses that match a filter that hides them
|
||||
if(localPreferences.serverSideFiltersSupported){
|
||||
if(getLocalPreferences().serverSideFiltersSupported){
|
||||
for(FilterResult filter : s.filtered){
|
||||
if(filter.filter.isActive() && filter.filter.filterAction==FilterAction.HIDE)
|
||||
return true;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.joinmastodon.android.api.session;
|
||||
|
||||
import static org.unifiedpush.android.connector.UnifiedPush.getDistributor;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.ComponentName;
|
||||
@@ -34,6 +36,7 @@ import org.joinmastodon.android.model.EmojiCategory;
|
||||
import org.joinmastodon.android.model.LegacyFilter;
|
||||
import org.joinmastodon.android.model.Instance;
|
||||
import org.joinmastodon.android.model.Token;
|
||||
import org.unifiedpush.android.connector.UnifiedPush;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
@@ -101,6 +104,7 @@ public class AccountSessionManager{
|
||||
}
|
||||
|
||||
public void addAccount(Instance instance, Token token, Account self, Application app, AccountActivationInfo activationInfo){
|
||||
Context context = MastodonApp.context;
|
||||
instances.put(instance.uri, instance);
|
||||
AccountSession session=new AccountSession(token, self, app, instance.uri, activationInfo==null, activationInfo);
|
||||
sessions.put(session.getID(), session);
|
||||
@@ -113,7 +117,14 @@ public class AccountSessionManager{
|
||||
MastodonAPIController.runInBackground(()->writeInstanceInfoFile(wrapper, instance.uri));
|
||||
|
||||
updateMoreInstanceInfo(instance, instance.uri);
|
||||
if(PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
if (!UnifiedPush.getDistributor(context).isEmpty()) {
|
||||
UnifiedPush.registerApp(
|
||||
context,
|
||||
session.getID(),
|
||||
new ArrayList<>(),
|
||||
context.getPackageName()
|
||||
);
|
||||
} else if(PushSubscriptionManager.arePushNotificationsAvailable()){
|
||||
session.getPushSubscriptionManager().registerAccountForPush(null);
|
||||
}
|
||||
maybeUpdateShortcuts();
|
||||
|
||||
@@ -19,12 +19,15 @@ import android.widget.Toolbar;
|
||||
import org.joinmastodon.android.E;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIRequest;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.polls.SubmitPollVote;
|
||||
import org.joinmastodon.android.api.requests.statuses.AkkomaTranslateStatus;
|
||||
import org.joinmastodon.android.api.requests.statuses.TranslateStatus;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.events.PollUpdatedEvent;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.AkkomaTranslation;
|
||||
import org.joinmastodon.android.model.DisplayItemsParent;
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.model.Relationship;
|
||||
@@ -47,6 +50,7 @@ import org.joinmastodon.android.ui.displayitems.TextStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.WarningFilteredStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewer;
|
||||
import org.joinmastodon.android.ui.photoviewer.PhotoViewerHost;
|
||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||
import org.joinmastodon.android.ui.utils.MediaAttachmentViewController;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.utils.ProvidesAssistContent;
|
||||
@@ -59,6 +63,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -358,12 +363,14 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}
|
||||
});
|
||||
list.addItemDecoration(new StatusListItemDecoration());
|
||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||
((UsableRecyclerView)list).setSelectorBoundsProvider(new UsableRecyclerView.SelectorBoundsProvider(){
|
||||
private Rect tmpRect=new Rect();
|
||||
@Override
|
||||
public void getSelectorBounds(View view, Rect outRect){
|
||||
boolean hasDescendant = false, hasAncestor = false, isWarning = false;
|
||||
int lastIndex = -1, firstIndex = -1;
|
||||
if(list!=view.getParent()) return;
|
||||
boolean hasDescendant=false, hasAncestor=false, isWarning=false;
|
||||
int lastIndex=-1, firstIndex=-1;
|
||||
if(((UsableRecyclerView) list).isIncludeMarginsInItemHitbox()){
|
||||
list.getDecoratedBoundsWithMargins(view, outRect);
|
||||
}else{
|
||||
@@ -493,7 +500,7 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
spoilerFooterIndex=spoilerItem.contentItems.indexOf(pollItems.get(pollItems.size()-1));
|
||||
}
|
||||
pollItems.clear();
|
||||
StatusDisplayItem.buildPollItems(itemID, this, poll, pollItems);
|
||||
StatusDisplayItem.buildPollItems(itemID, this, poll, status, pollItems);
|
||||
if(spoilerItem!=null){
|
||||
spoilerItem.contentItems.subList(spoilerFirstOptionIndex, spoilerFooterIndex+1).clear();
|
||||
spoilerItem.contentItems.addAll(spoilerFirstOptionIndex, pollItems);
|
||||
@@ -564,7 +571,8 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
|
||||
public void onRevealSpoilerClick(SpoilerStatusDisplayItem.Holder holder){
|
||||
Status status=holder.getItem().status;
|
||||
toggleSpoiler(status, holder.getItemID());
|
||||
boolean isForQuote=holder.getItem().isForQuote;
|
||||
toggleSpoiler(status, isForQuote, holder.getItemID());
|
||||
}
|
||||
|
||||
public void onVisibilityIconClick(HeaderStatusDisplayItem.Holder holder) {
|
||||
@@ -586,15 +594,16 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
else notifyItemChangedBefore(holder.getItem(), HeaderStatusDisplayItem.class);
|
||||
}
|
||||
|
||||
protected void toggleSpoiler(Status status, String itemID){
|
||||
protected void toggleSpoiler(Status status, boolean isForQuote, String itemID){
|
||||
status.spoilerRevealed=!status.spoilerRevealed;
|
||||
if (!status.spoilerRevealed && !AccountSessionManager.get(accountID).getLocalPreferences().revealCWs)
|
||||
status.sensitiveRevealed = false;
|
||||
|
||||
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
|
||||
List<SpoilerStatusDisplayItem.Holder> spoilers=findAllHoldersOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
|
||||
SpoilerStatusDisplayItem.Holder spoiler=spoilers.size() > 1 && isForQuote ? spoilers.get(1) : spoilers.get(0);
|
||||
if(spoiler!=null) spoiler.rebind();
|
||||
else notifyItemChanged(itemID, SpoilerStatusDisplayItem.class);
|
||||
SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(findItemOfType(itemID, SpoilerStatusDisplayItem.class));
|
||||
SpoilerStatusDisplayItem spoilerItem=Objects.requireNonNull(spoiler.getItem());
|
||||
|
||||
int index=displayItems.indexOf(spoilerItem);
|
||||
if(status.spoilerRevealed){
|
||||
@@ -850,45 +859,53 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
status.translationState=Status.TranslationState.SHOWN;
|
||||
}else{
|
||||
status.translationState=Status.TranslationState.LOADING;
|
||||
new TranslateStatus(status.getContentStatus().id, Locale.getDefault().getLanguage())
|
||||
.setCallback(new Callback<>(){
|
||||
Consumer<Translation> successCallback=(result)->{
|
||||
status.translation=result;
|
||||
status.translationState=Status.TranslationState.SHOWN;
|
||||
updateTranslation(itemID);
|
||||
};
|
||||
MastodonAPIRequest<?> req=isInstanceAkkoma()
|
||||
? new AkkomaTranslateStatus(status.getContentStatus().id, Locale.getDefault().getLanguage()).setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(AkkomaTranslation result){
|
||||
if(getActivity()!=null) successCallback.accept(result.toTranslation());
|
||||
}
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(getActivity()!=null) translationCallbackError(status, itemID);
|
||||
}
|
||||
})
|
||||
: new TranslateStatus(status.getContentStatus().id, Locale.getDefault().getLanguage()).setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Translation result){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
status.translation=result;
|
||||
status.translationState=Status.TranslationState.SHOWN;
|
||||
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
|
||||
if(text!=null){
|
||||
text.updateTranslation(true);
|
||||
imgLoader.bindViewHolder((ImageLoaderRecyclerAdapter) list.getAdapter(), text, text.getAbsoluteAdapterPosition());
|
||||
}else{
|
||||
notifyItemChanged(itemID, TextStatusDisplayItem.class);
|
||||
}
|
||||
if(getActivity()!=null) successCallback.accept(result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
if(getActivity()==null)
|
||||
return;
|
||||
status.translationState=Status.TranslationState.HIDDEN;
|
||||
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
|
||||
if(text!=null){
|
||||
text.updateTranslation(true);
|
||||
}else{
|
||||
notifyItemChanged(itemID, TextStatusDisplayItem.class);
|
||||
}
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.translation_failed)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
if(getActivity()!=null) translationCallbackError(status, itemID);
|
||||
}
|
||||
})
|
||||
.exec(accountID);
|
||||
});
|
||||
|
||||
// 1 minute
|
||||
req.setTimeout(60000).exec(accountID);
|
||||
}
|
||||
}
|
||||
}
|
||||
updateTranslation(itemID);
|
||||
}
|
||||
|
||||
private void translationCallbackError(Status status, String itemID) {
|
||||
status.translationState=Status.TranslationState.HIDDEN;
|
||||
updateTranslation(itemID);
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setTitle(R.string.error)
|
||||
.setMessage(R.string.translation_failed)
|
||||
.setPositiveButton(R.string.ok, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void updateTranslation(String itemID) {
|
||||
TextStatusDisplayItem.Holder text=findHolderOfType(itemID, TextStatusDisplayItem.Holder.class);
|
||||
if(text!=null){
|
||||
text.updateTranslation(true);
|
||||
@@ -896,6 +913,25 @@ public abstract class BaseStatusListFragment<T extends DisplayItemsParent> exten
|
||||
}else{
|
||||
notifyItemChanged(itemID, TextStatusDisplayItem.class);
|
||||
}
|
||||
|
||||
if(isInstanceAkkoma())
|
||||
return;
|
||||
|
||||
SpoilerStatusDisplayItem.Holder spoiler=findHolderOfType(itemID, SpoilerStatusDisplayItem.Holder.class);
|
||||
if(spoiler!=null){
|
||||
spoiler.rebind();
|
||||
}
|
||||
|
||||
MediaGridStatusDisplayItem.Holder media=findHolderOfType(itemID, MediaGridStatusDisplayItem.Holder.class);
|
||||
if (media!=null) {
|
||||
media.rebind();
|
||||
}
|
||||
|
||||
for(int i=0;i<list.getChildCount();i++){
|
||||
if(list.getChildViewHolder(list.getChildAt(i)) instanceof PollOptionStatusDisplayItem.Holder item){
|
||||
item.rebind();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void rebuildAllDisplayItems(){
|
||||
|
||||
@@ -736,7 +736,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
String prefix = (GlobalUserPreferences.prefixReplies == ALWAYS
|
||||
|| (GlobalUserPreferences.prefixReplies == TO_OTHERS && !ownID.equals(status.account.id)))
|
||||
&& !status.spoilerText.startsWith("re: ") ? "re: " : "";
|
||||
spoilerEdit.setText(prefix + replyTo.spoilerText);
|
||||
spoilerEdit.setText(prefix + status.spoilerText);
|
||||
spoilerBtn.setSelected(true);
|
||||
}
|
||||
if (status.language != null && !status.language.isEmpty()) setPostLanguage(status.language);
|
||||
@@ -807,25 +807,28 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
actionItem.setActionView(wrap);
|
||||
actionItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
|
||||
draftsBtn = wrap.findViewById(R.id.drafts_btn);
|
||||
draftOptionsPopup = new PopupMenu(getContext(), draftsBtn);
|
||||
draftsBtn=wrap.findViewById(R.id.drafts_btn);
|
||||
draftOptionsPopup=new PopupMenu(getContext(), draftsBtn);
|
||||
draftOptionsPopup.inflate(R.menu.compose_more);
|
||||
draftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.draft);
|
||||
undraftMenuItem = draftOptionsPopup.getMenu().findItem(R.id.undraft);
|
||||
scheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.schedule);
|
||||
unscheduleMenuItem = draftOptionsPopup.getMenu().findItem(R.id.unschedule);
|
||||
Menu draftOptionsMenu=draftOptionsPopup.getMenu();
|
||||
draftMenuItem=draftOptionsMenu.findItem(R.id.draft);
|
||||
undraftMenuItem=draftOptionsMenu.findItem(R.id.undraft);
|
||||
scheduleMenuItem=draftOptionsMenu.findItem(R.id.schedule);
|
||||
unscheduleMenuItem=draftOptionsMenu.findItem(R.id.unschedule);
|
||||
draftOptionsMenu.findItem(R.id.preview).setVisible(isInstanceAkkoma());
|
||||
draftOptionsPopup.setOnMenuItemClickListener(i->{
|
||||
int id = i.getItemId();
|
||||
if (id == R.id.draft) updateScheduledAt(getDraftInstant());
|
||||
else if (id == R.id.schedule) pickScheduledDateTime();
|
||||
else if (id == R.id.unschedule || id == R.id.undraft) updateScheduledAt(null);
|
||||
else navigateToUnsentPosts();
|
||||
int id=i.getItemId();
|
||||
if(id==R.id.draft) updateScheduledAt(getDraftInstant());
|
||||
else if(id==R.id.schedule) pickScheduledDateTime();
|
||||
else if(id==R.id.unschedule || id==R.id.undraft) updateScheduledAt(null);
|
||||
else if(id==R.id.drafts) navigateToUnsentPosts();
|
||||
else if(id==R.id.preview) publish(true);
|
||||
return true;
|
||||
});
|
||||
UiUtils.enablePopupMenuIcons(getContext(), draftOptionsPopup);
|
||||
|
||||
publishButton = wrap.findViewById(R.id.publish_btn);
|
||||
languageButton = wrap.findViewById(R.id.language_btn);
|
||||
publishButton=wrap.findViewById(R.id.publish_btn);
|
||||
languageButton=wrap.findViewById(R.id.language_btn);
|
||||
languageButton.setOnClickListener(v->showLanguageAlert());
|
||||
languageButton.setOnLongClickListener(v->{
|
||||
if(!getLocalPrefs().bottomEncoding){
|
||||
@@ -1051,6 +1054,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
|
||||
private void publish(){
|
||||
publish(false);
|
||||
}
|
||||
|
||||
private void publish(boolean preview){
|
||||
sendingOverlay=new View(getActivity());
|
||||
WindowManager.LayoutParams overlayParams=new WindowManager.LayoutParams();
|
||||
overlayParams.type=WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
|
||||
@@ -1064,10 +1071,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
publishButton.setEnabled(false);
|
||||
V.setVisibilityAnimated(sendProgress, View.VISIBLE);
|
||||
|
||||
mediaViewController.saveAltTextsBeforePublishing(this::actuallyPublish, this::handlePublishError);
|
||||
mediaViewController.saveAltTextsBeforePublishing(
|
||||
()->actuallyPublish(preview),
|
||||
this::handlePublishError);
|
||||
}
|
||||
|
||||
private void actuallyPublish(){
|
||||
private void actuallyPublish(boolean preview){
|
||||
String text=mainEditText.getText().toString();
|
||||
CreateStatus.Request req=new CreateStatus.Request();
|
||||
if("bottom".equals(postLang.encoding)){
|
||||
@@ -1085,6 +1094,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
req.sensitive=sensitive;
|
||||
req.contentType=contentType==ContentType.UNSPECIFIED ? null : contentType;
|
||||
req.scheduledAt=scheduledAt;
|
||||
req.preview=preview;
|
||||
if(!mediaViewController.isEmpty()){
|
||||
req.mediaIds=mediaViewController.getAttachmentIDs();
|
||||
if(editingStatus != null){
|
||||
@@ -1112,7 +1122,12 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
Callback<Status> resCallback=new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(Status result){
|
||||
maybeDeleteScheduledPost(() -> {
|
||||
if(preview){
|
||||
openPreview(result);
|
||||
return;
|
||||
}
|
||||
|
||||
maybeDeleteScheduledPost(()->{
|
||||
wm.removeView(sendingOverlay);
|
||||
sendingOverlay=null;
|
||||
if(editingStatus==null || redraftStatus){
|
||||
@@ -1134,10 +1149,10 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
E.post(new StatusUpdatedEvent(editedStatus));
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()) {
|
||||
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.O || !isStateSaved()){
|
||||
Nav.finish(ComposeFragment.this);
|
||||
}
|
||||
if (getArguments().getBoolean("navigateToStatus", false)) {
|
||||
if(getArguments().getBoolean("navigateToStatus", false)){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("status", Parcels.wrap(result));
|
||||
@@ -1153,11 +1168,11 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
};
|
||||
|
||||
if(editingStatus!=null && !redraftStatus){
|
||||
if(editingStatus!=null && !redraftStatus && !preview){
|
||||
new EditStatus(req, editingStatus.id)
|
||||
.setCallback(resCallback)
|
||||
.exec(accountID);
|
||||
}else if(req.scheduledAt == null){
|
||||
}else if(req.scheduledAt == null || preview){
|
||||
new CreateStatus(req, uuid)
|
||||
.setCallback(resCallback)
|
||||
.exec(accountID);
|
||||
@@ -1210,6 +1225,25 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
}
|
||||
|
||||
private void openPreview(Status result){
|
||||
result.preview=true;
|
||||
wm.removeView(sendingOverlay);
|
||||
sendingOverlay=null;
|
||||
publishButton.setEnabled(true);
|
||||
V.setVisibilityAnimated(sendProgress, View.GONE);
|
||||
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
|
||||
imm.hideSoftInputFromWindow(contentView.getWindowToken(), 0);
|
||||
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
args.putParcelable("status", Parcels.wrap(result));
|
||||
if(replyTo!=null){
|
||||
args.putParcelable("inReplyTo", Parcels.wrap(replyTo));
|
||||
args.putParcelable("inReplyToAccount", Parcels.wrap(replyTo.account));
|
||||
}
|
||||
Nav.go(getActivity(), ThreadFragment.class, args);
|
||||
}
|
||||
|
||||
private void updateRecentLanguages() {
|
||||
if (postLang == null || postLang.language == null) return;
|
||||
String language = postLang.language.getLanguage();
|
||||
@@ -1528,6 +1562,7 @@ public class ComposeFragment extends MastodonToolbarFragment implements OnBackPr
|
||||
}
|
||||
|
||||
contentTypePopup.setOnMenuItemClickListener(i->{
|
||||
uuid=null;
|
||||
int index=i.getItemId();
|
||||
contentType=ContentType.values()[index];
|
||||
btn.setSelected(index!=ContentType.UNSPECIFIED.ordinal() && index!=ContentType.PLAIN.ordinal());
|
||||
|
||||
@@ -60,191 +60,191 @@ import me.grishka.appkit.api.ErrorResponse;
|
||||
import me.grishka.appkit.utils.BindableViewHolder;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefinition> implements ScrollableToTop {
|
||||
private String accountID;
|
||||
private TimelinesAdapter adapter;
|
||||
private final ItemTouchHelper itemTouchHelper;
|
||||
private Menu optionsMenu;
|
||||
private boolean updated;
|
||||
private final Map<MenuItem, TimelineDefinition> timelineByMenuItem = new HashMap<>();
|
||||
private final List<ListTimeline> listTimelines = new ArrayList<>();
|
||||
private final List<Hashtag> hashtags = new ArrayList<>();
|
||||
private MenuItem addHashtagItem;
|
||||
public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefinition> implements ScrollableToTop{
|
||||
private String accountID;
|
||||
private TimelinesAdapter adapter;
|
||||
private final ItemTouchHelper itemTouchHelper;
|
||||
private Menu optionsMenu;
|
||||
private boolean updated;
|
||||
private final Map<MenuItem, TimelineDefinition> timelineByMenuItem=new HashMap<>();
|
||||
private final List<ListTimeline> listTimelines=new ArrayList<>();
|
||||
private final List<Hashtag> hashtags=new ArrayList<>();
|
||||
private MenuItem addHashtagItem;
|
||||
|
||||
public EditTimelinesFragment() {
|
||||
super(10);
|
||||
ItemTouchHelper.SimpleCallback itemTouchCallback = new ItemTouchHelperCallback() ;
|
||||
itemTouchHelper = new ItemTouchHelper(itemTouchCallback);
|
||||
}
|
||||
public EditTimelinesFragment(){
|
||||
super(10);
|
||||
ItemTouchHelper.SimpleCallback itemTouchCallback=new ItemTouchHelperCallback();
|
||||
itemTouchHelper=new ItemTouchHelper(itemTouchCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
setTitle(R.string.sk_timelines);
|
||||
accountID = getArguments().getString("account");
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
setTitle(R.string.sk_timelines);
|
||||
accountID=getArguments().getString("account");
|
||||
|
||||
new GetLists().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> result) {
|
||||
listTimelines.addAll(result);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
new GetLists().setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(List<ListTimeline> result){
|
||||
listTimelines.addAll(result);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
|
||||
new GetFollowedHashtags().setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Hashtag> result) {
|
||||
hashtags.addAll(result);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
new GetFollowedHashtags().setCallback(new Callback<>(){
|
||||
@Override
|
||||
public void onSuccess(HeaderPaginationList<Hashtag> result){
|
||||
hashtags.addAll(result);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
@Override
|
||||
public void onError(ErrorResponse error){
|
||||
error.showToast(getContext());
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading) loadData();
|
||||
}
|
||||
@Override
|
||||
protected void onShown(){
|
||||
super.onShown();
|
||||
if(!getArguments().getBoolean("noAutoLoad") && !loaded && !dataLoading) loadData();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
itemTouchHelper.attachToRecyclerView(list);
|
||||
refreshLayout.setEnabled(false);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16));
|
||||
}
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
itemTouchHelper.attachToRecyclerView(list);
|
||||
refreshLayout.setEnabled(false);
|
||||
list.addItemDecoration(new DividerItemDecoration(getActivity(), R.attr.colorM3OutlineVariant, 0.5f, 56, 16));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
this.optionsMenu = menu;
|
||||
updateOptionsMenu();
|
||||
}
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater){
|
||||
this.optionsMenu=menu;
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.menu_back) {
|
||||
updateOptionsMenu();
|
||||
optionsMenu.performIdentifierAction(R.id.menu_add_timeline, 0);
|
||||
return true;
|
||||
}
|
||||
TimelineDefinition tl = timelineByMenuItem.get(item);
|
||||
if (tl != null) {
|
||||
addTimeline(tl);
|
||||
} else if (item == addHashtagItem) {
|
||||
makeTimelineEditor(null, (hashtag) -> {
|
||||
if (hashtag != null) addTimeline(hashtag);
|
||||
}, null);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item){
|
||||
if(item.getItemId()==R.id.menu_back){
|
||||
updateOptionsMenu();
|
||||
optionsMenu.performIdentifierAction(R.id.menu_add_timeline, 0);
|
||||
return true;
|
||||
}
|
||||
TimelineDefinition tl=timelineByMenuItem.get(item);
|
||||
if(tl!=null){
|
||||
addTimeline(tl);
|
||||
}else if(item==addHashtagItem){
|
||||
makeTimelineEditor(null, (hashtag)->{
|
||||
if(hashtag!=null) addTimeline(hashtag);
|
||||
}, null);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private void addTimeline(TimelineDefinition tl) {
|
||||
data.add(tl.copy());
|
||||
adapter.notifyItemInserted(data.size());
|
||||
saveTimelines();
|
||||
updateOptionsMenu();
|
||||
}
|
||||
private void addTimeline(TimelineDefinition tl){
|
||||
data.add(tl.copy());
|
||||
adapter.notifyItemInserted(data.size());
|
||||
saveTimelines();
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
private void addTimelineToOptions(TimelineDefinition tl, Menu menu) {
|
||||
if (data.contains(tl)) return;
|
||||
MenuItem item = addOptionsItem(menu, tl.getTitle(getContext()), tl.getIcon().iconRes);
|
||||
timelineByMenuItem.put(item, tl);
|
||||
}
|
||||
private void addTimelineToOptions(TimelineDefinition tl, Menu menu){
|
||||
if(data.contains(tl)) return;
|
||||
MenuItem item=addOptionsItem(menu, tl.getTitle(getContext()), tl.getIcon().iconRes);
|
||||
timelineByMenuItem.put(item, tl);
|
||||
}
|
||||
|
||||
private MenuItem addOptionsItem(Menu menu, String name, @DrawableRes int icon) {
|
||||
MenuItem item = menu.add(0, View.generateViewId(), Menu.NONE, name);
|
||||
item.setIcon(icon);
|
||||
return item;
|
||||
}
|
||||
private MenuItem addOptionsItem(Menu menu, String name, @DrawableRes int icon){
|
||||
MenuItem item=menu.add(0, View.generateViewId(), Menu.NONE, name);
|
||||
item.setIcon(icon);
|
||||
return item;
|
||||
}
|
||||
|
||||
private void updateOptionsMenu() {
|
||||
if(getActivity()==null) return;
|
||||
optionsMenu.clear();
|
||||
timelineByMenuItem.clear();
|
||||
private void updateOptionsMenu(){
|
||||
if(getActivity()==null) return;
|
||||
optionsMenu.clear();
|
||||
timelineByMenuItem.clear();
|
||||
|
||||
SubMenu menu = optionsMenu.addSubMenu(0, R.id.menu_add_timeline, NONE, R.string.sk_timelines_add);
|
||||
menu.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
menu.getItem().setIcon(R.drawable.ic_fluent_add_24_regular);
|
||||
SubMenu menu=optionsMenu.addSubMenu(0, R.id.menu_add_timeline, NONE, R.string.sk_timelines_add);
|
||||
menu.getItem().setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
|
||||
menu.getItem().setIcon(R.drawable.ic_fluent_add_24_regular);
|
||||
|
||||
SubMenu timelinesMenu = menu.addSubMenu(R.string.sk_timeline);
|
||||
timelinesMenu.getItem().setIcon(R.drawable.ic_fluent_timeline_24_regular);
|
||||
SubMenu listsMenu = menu.addSubMenu(R.string.sk_list);
|
||||
listsMenu.getItem().setIcon(R.drawable.ic_fluent_people_24_regular);
|
||||
SubMenu hashtagsMenu = menu.addSubMenu(R.string.sk_hashtag);
|
||||
hashtagsMenu.getItem().setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
|
||||
SubMenu timelinesMenu=menu.addSubMenu(R.string.sk_timeline);
|
||||
timelinesMenu.getItem().setIcon(R.drawable.ic_fluent_timeline_24_regular);
|
||||
SubMenu listsMenu=menu.addSubMenu(R.string.sk_list);
|
||||
listsMenu.getItem().setIcon(R.drawable.ic_fluent_people_24_regular);
|
||||
SubMenu hashtagsMenu=menu.addSubMenu(R.string.sk_hashtag);
|
||||
hashtagsMenu.getItem().setIcon(R.drawable.ic_fluent_number_symbol_24_regular);
|
||||
|
||||
makeBackItem(timelinesMenu);
|
||||
makeBackItem(listsMenu);
|
||||
makeBackItem(hashtagsMenu);
|
||||
makeBackItem(timelinesMenu);
|
||||
makeBackItem(listsMenu);
|
||||
makeBackItem(hashtagsMenu);
|
||||
|
||||
TimelineDefinition.getAllTimelines(accountID).stream().forEach(tl -> addTimelineToOptions(tl, timelinesMenu));
|
||||
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl -> addTimelineToOptions(tl, listsMenu));
|
||||
addHashtagItem = addOptionsItem(hashtagsMenu, getContext().getString(R.string.sk_timelines_add), R.drawable.ic_fluent_add_24_regular);
|
||||
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl -> addTimelineToOptions(tl, hashtagsMenu));
|
||||
TimelineDefinition.getAllTimelines(accountID).stream().forEach(tl->addTimelineToOptions(tl, timelinesMenu));
|
||||
listTimelines.stream().map(TimelineDefinition::ofList).forEach(tl->addTimelineToOptions(tl, listsMenu));
|
||||
addHashtagItem=addOptionsItem(hashtagsMenu, getContext().getString(R.string.sk_timelines_add), R.drawable.ic_fluent_add_24_regular);
|
||||
hashtags.stream().map(TimelineDefinition::ofHashtag).forEach(tl->addTimelineToOptions(tl, hashtagsMenu));
|
||||
|
||||
timelinesMenu.getItem().setVisible(timelinesMenu.size() > 0);
|
||||
listsMenu.getItem().setVisible(listsMenu.size() > 0);
|
||||
hashtagsMenu.getItem().setVisible(hashtagsMenu.size() > 0);
|
||||
timelinesMenu.getItem().setVisible(timelinesMenu.size()>0);
|
||||
listsMenu.getItem().setVisible(listsMenu.size()>0);
|
||||
hashtagsMenu.getItem().setVisible(hashtagsMenu.size()>0);
|
||||
|
||||
UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu, R.id.menu_add_timeline);
|
||||
}
|
||||
UiUtils.enableOptionsMenuIcons(getContext(), optionsMenu, R.id.menu_add_timeline);
|
||||
}
|
||||
|
||||
private void saveTimelines() {
|
||||
updated=true;
|
||||
private void saveTimelines(){
|
||||
updated=true;
|
||||
AccountLocalPreferences prefs=AccountSessionManager.get(accountID).getLocalPreferences();
|
||||
if(data.isEmpty()) data.add(TimelineDefinition.HOME_TIMELINE);
|
||||
prefs.timelines=data;
|
||||
prefs.save();
|
||||
}
|
||||
|
||||
private void removeTimeline(int position) {
|
||||
data.remove(position);
|
||||
adapter.notifyItemRemoved(position);
|
||||
saveTimelines();
|
||||
updateOptionsMenu();
|
||||
}
|
||||
private void removeTimeline(int position){
|
||||
data.remove(position);
|
||||
adapter.notifyItemRemoved(position);
|
||||
saveTimelines();
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
onDataLoaded(AccountSessionManager.get(accountID).getLocalPreferences().timelines);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
onDataLoaded(AccountSessionManager.get(accountID).getLocalPreferences().timelines);
|
||||
updateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RecyclerView.Adapter<TimelineViewHolder> getAdapter() {
|
||||
return adapter = new TimelinesAdapter();
|
||||
}
|
||||
@Override
|
||||
protected RecyclerView.Adapter<TimelineViewHolder> getAdapter(){
|
||||
return adapter=new TimelinesAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void scrollToTop() {
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
@Override
|
||||
public void scrollToTop(){
|
||||
smoothScrollRecyclerViewToTop(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (updated) UiUtils.restartApp();
|
||||
}
|
||||
@Override
|
||||
public void onDestroy(){
|
||||
super.onDestroy();
|
||||
if(updated) UiUtils.restartApp();
|
||||
}
|
||||
|
||||
private boolean setTagListContent(NachoTextView editText, @Nullable List<String> tags) {
|
||||
if (tags == null || tags.isEmpty()) return false;
|
||||
private boolean setTagListContent(NachoTextView editText, @Nullable List<String> tags){
|
||||
if(tags==null || tags.isEmpty()) return false;
|
||||
editText.setText(tags);
|
||||
editText.chipifyAllUnterminatedTokens();
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private NachoTextView prepareChipTextView(NachoTextView nacho) {
|
||||
private NachoTextView prepareChipTextView(NachoTextView nacho){
|
||||
//I’ll Be Back
|
||||
nacho.setChipTerminators(
|
||||
Map.of(
|
||||
@@ -254,223 +254,228 @@ public class EditTimelinesFragment extends MastodonRecyclerFragment<TimelineDefi
|
||||
';', BEHAVIOR_CHIPIFY_ALL
|
||||
)
|
||||
);
|
||||
nacho.enableEditChipOnTouch(true, true);
|
||||
nacho.setOnFocusChangeListener((v, hasFocus) -> nacho.chipifyAllUnterminatedTokens());
|
||||
return nacho;
|
||||
}
|
||||
nacho.enableEditChipOnTouch(true, true);
|
||||
nacho.setOnFocusChangeListener((v, hasFocus)->nacho.chipifyAllUnterminatedTokens());
|
||||
return nacho;
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
protected void makeTimelineEditor(@Nullable TimelineDefinition item, Consumer<TimelineDefinition> onSave, Runnable onRemove) {
|
||||
Context ctx = getContext();
|
||||
View view = getActivity().getLayoutInflater().inflate(R.layout.edit_timeline, list, false);
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
protected void makeTimelineEditor(@Nullable TimelineDefinition item, Consumer<TimelineDefinition> onSave, Runnable onRemove){
|
||||
Context ctx=getContext();
|
||||
View view=getActivity().getLayoutInflater().inflate(R.layout.edit_timeline, list, false);
|
||||
|
||||
View divider = view.findViewById(R.id.divider);
|
||||
Button advancedBtn = view.findViewById(R.id.advanced);
|
||||
EditText editText = view.findViewById(R.id.input);
|
||||
if (item != null) editText.setText(item.getCustomTitle());
|
||||
editText.setHint(item != null ? item.getDefaultTitle(ctx) : ctx.getString(R.string.sk_hashtag));
|
||||
View divider=view.findViewById(R.id.divider);
|
||||
Button advancedBtn=view.findViewById(R.id.advanced);
|
||||
EditText editText=view.findViewById(R.id.input);
|
||||
if(item!=null) editText.setText(item.getCustomTitle());
|
||||
editText.setHint(item!=null ? item.getDefaultTitle(ctx) : ctx.getString(R.string.sk_hashtag));
|
||||
|
||||
LinearLayout tagWrap = view.findViewById(R.id.tag_wrap);
|
||||
boolean advancedOptionsAvailable = item == null || item.getType() == TimelineDefinition.TimelineType.HASHTAG;
|
||||
advancedBtn.setVisibility(advancedOptionsAvailable ? View.VISIBLE : View.GONE);
|
||||
advancedBtn.setOnClickListener(l -> {
|
||||
advancedBtn.setSelected(!advancedBtn.isSelected());
|
||||
LinearLayout tagWrap=view.findViewById(R.id.tag_wrap);
|
||||
boolean hashtagOptionsAvailable=item==null || item.getType()==TimelineDefinition.TimelineType.HASHTAG;
|
||||
advancedBtn.setVisibility(hashtagOptionsAvailable ? View.VISIBLE : View.GONE);
|
||||
advancedBtn.setOnClickListener(l->{
|
||||
advancedBtn.setSelected(!advancedBtn.isSelected());
|
||||
advancedBtn.setText(advancedBtn.isSelected() ? R.string.sk_advanced_options_hide : R.string.sk_advanced_options_show);
|
||||
divider.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
|
||||
tagWrap.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
|
||||
tagWrap.setVisibility(advancedBtn.isSelected() ? View.VISIBLE : View.GONE);
|
||||
UiUtils.beginLayoutTransition((ViewGroup) view);
|
||||
});
|
||||
});
|
||||
|
||||
Switch localOnlySwitch = view.findViewById(R.id.local_only_switch);
|
||||
view.findViewById(R.id.local_only)
|
||||
.setOnClickListener(l -> localOnlySwitch.setChecked(!localOnlySwitch.isChecked()));
|
||||
Switch localOnlySwitch=view.findViewById(R.id.local_only_switch);
|
||||
view.findViewById(R.id.local_only).setOnClickListener(l->localOnlySwitch.setChecked(!localOnlySwitch.isChecked()));
|
||||
|
||||
EditText tagMain = view.findViewById(R.id.tag_main);
|
||||
NachoTextView tagsAny = prepareChipTextView(view.findViewById(R.id.tags_any));
|
||||
NachoTextView tagsAll = prepareChipTextView(view.findViewById(R.id.tags_all));
|
||||
NachoTextView tagsNone = prepareChipTextView(view.findViewById(R.id.tags_none));
|
||||
if (item != null) {
|
||||
tagMain.setText(item.getHashtagName());
|
||||
boolean hasAdvanced = !TextUtils.isEmpty(item.getCustomTitle()) && !Objects.equals(item.getHashtagName(), item.getCustomTitle());
|
||||
hasAdvanced = setTagListContent(tagsAny, item.getHashtagAny()) || hasAdvanced;
|
||||
hasAdvanced = setTagListContent(tagsAll, item.getHashtagAll()) || hasAdvanced;
|
||||
hasAdvanced = setTagListContent(tagsNone, item.getHashtagNone()) || hasAdvanced;
|
||||
if (item.isHashtagLocalOnly()) {
|
||||
localOnlySwitch.setChecked(true);
|
||||
hasAdvanced = true;
|
||||
}
|
||||
if (hasAdvanced) {
|
||||
advancedBtn.setSelected(true);
|
||||
advancedBtn.setText(R.string.sk_advanced_options_hide);
|
||||
EditText tagMain=view.findViewById(R.id.tag_main);
|
||||
NachoTextView tagsAny=prepareChipTextView(view.findViewById(R.id.tags_any));
|
||||
NachoTextView tagsAll=prepareChipTextView(view.findViewById(R.id.tags_all));
|
||||
NachoTextView tagsNone=prepareChipTextView(view.findViewById(R.id.tags_none));
|
||||
|
||||
if(item!=null && hashtagOptionsAvailable){
|
||||
tagMain.setText(item.getHashtagName());
|
||||
boolean hasAdvanced=!TextUtils.isEmpty(item.getCustomTitle()) && !Objects.equals(item.getHashtagName(), item.getCustomTitle());
|
||||
hasAdvanced=setTagListContent(tagsAny, item.getHashtagAny()) || hasAdvanced;
|
||||
hasAdvanced=setTagListContent(tagsAll, item.getHashtagAll()) || hasAdvanced;
|
||||
hasAdvanced=setTagListContent(tagsNone, item.getHashtagNone()) || hasAdvanced;
|
||||
if(item.isHashtagLocalOnly()){
|
||||
localOnlySwitch.setChecked(true);
|
||||
hasAdvanced=true;
|
||||
}
|
||||
if(hasAdvanced){
|
||||
advancedBtn.setSelected(true);
|
||||
advancedBtn.setText(R.string.sk_advanced_options_hide);
|
||||
tagWrap.setVisibility(View.VISIBLE);
|
||||
divider.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImageButton btn = view.findViewById(R.id.button);
|
||||
PopupMenu popup = new PopupMenu(ctx, btn);
|
||||
TimelineDefinition.Icon currentIcon = item != null ? item.getIcon() : TimelineDefinition.Icon.HASHTAG;
|
||||
btn.setImageResource(currentIcon.iconRes);
|
||||
btn.setTag(currentIcon.ordinal());
|
||||
btn.setContentDescription(ctx.getString(currentIcon.nameRes));
|
||||
btn.setOnTouchListener(popup.getDragToOpenListener());
|
||||
btn.setOnClickListener(l -> popup.show());
|
||||
ImageButton btn=view.findViewById(R.id.button);
|
||||
PopupMenu popup=new PopupMenu(ctx, btn);
|
||||
TimelineDefinition.Icon currentIcon=item!=null ? item.getIcon() : TimelineDefinition.Icon.HASHTAG;
|
||||
btn.setImageResource(currentIcon.iconRes);
|
||||
btn.setTag(currentIcon.ordinal());
|
||||
btn.setContentDescription(ctx.getString(currentIcon.nameRes));
|
||||
btn.setOnTouchListener(popup.getDragToOpenListener());
|
||||
btn.setOnClickListener(l->popup.show());
|
||||
|
||||
Menu menu = popup.getMenu();
|
||||
TimelineDefinition.Icon defaultIcon = item != null ? item.getDefaultIcon() : TimelineDefinition.Icon.HASHTAG;
|
||||
menu.add(0, currentIcon.ordinal(), NONE, currentIcon.nameRes).setIcon(currentIcon.iconRes);
|
||||
if (!currentIcon.equals(defaultIcon)) {
|
||||
menu.add(0, defaultIcon.ordinal(), NONE, defaultIcon.nameRes).setIcon(defaultIcon.iconRes);
|
||||
}
|
||||
for (TimelineDefinition.Icon icon : TimelineDefinition.Icon.values()) {
|
||||
if (icon.hidden || icon.ordinal() == (int) btn.getTag()) continue;
|
||||
menu.add(0, icon.ordinal(), NONE, icon.nameRes).setIcon(icon.iconRes);
|
||||
}
|
||||
UiUtils.enablePopupMenuIcons(ctx, popup);
|
||||
Menu menu=popup.getMenu();
|
||||
TimelineDefinition.Icon defaultIcon=item!=null ? item.getDefaultIcon() : TimelineDefinition.Icon.HASHTAG;
|
||||
menu.add(0, currentIcon.ordinal(), NONE, currentIcon.nameRes).setIcon(currentIcon.iconRes);
|
||||
if(!currentIcon.equals(defaultIcon)){
|
||||
menu.add(0, defaultIcon.ordinal(), NONE, defaultIcon.nameRes).setIcon(defaultIcon.iconRes);
|
||||
}
|
||||
for(TimelineDefinition.Icon icon : TimelineDefinition.Icon.values()){
|
||||
if(icon.hidden || icon.ordinal()==(int) btn.getTag()) continue;
|
||||
menu.add(0, icon.ordinal(), NONE, icon.nameRes).setIcon(icon.iconRes);
|
||||
}
|
||||
UiUtils.enablePopupMenuIcons(ctx, popup);
|
||||
|
||||
popup.setOnMenuItemClickListener(menuItem -> {
|
||||
TimelineDefinition.Icon icon = TimelineDefinition.Icon.values()[menuItem.getItemId()];
|
||||
btn.setImageResource(icon.iconRes);
|
||||
btn.setTag(menuItem.getItemId());
|
||||
btn.setContentDescription(ctx.getString(icon.nameRes));
|
||||
return true;
|
||||
});
|
||||
popup.setOnMenuItemClickListener(menuItem->{
|
||||
TimelineDefinition.Icon icon=TimelineDefinition.Icon.values()[menuItem.getItemId()];
|
||||
btn.setImageResource(icon.iconRes);
|
||||
btn.setTag(menuItem.getItemId());
|
||||
btn.setContentDescription(ctx.getString(icon.nameRes));
|
||||
return true;
|
||||
});
|
||||
|
||||
AlertDialog.Builder builder = new M3AlertDialogBuilder(ctx)
|
||||
.setTitle(item == null ? R.string.sk_add_timeline : R.string.sk_edit_timeline)
|
||||
.setView(view)
|
||||
.setPositiveButton(R.string.save, (d, which) -> {
|
||||
tagsAny.chipifyAllUnterminatedTokens();
|
||||
tagsAll.chipifyAllUnterminatedTokens();
|
||||
tagsNone.chipifyAllUnterminatedTokens();
|
||||
String name = editText.getText().toString().trim();
|
||||
String mainHashtag = tagMain.getText().toString().trim();
|
||||
if (TextUtils.isEmpty(mainHashtag)) {
|
||||
mainHashtag = name;
|
||||
name = null;
|
||||
}
|
||||
if (TextUtils.isEmpty(mainHashtag) && (item != null && item.getType() == TimelineDefinition.TimelineType.HASHTAG)) {
|
||||
Toast.makeText(ctx, R.string.sk_add_timeline_tag_error_empty, Toast.LENGTH_SHORT).show();
|
||||
onSave.accept(null);
|
||||
return;
|
||||
}
|
||||
AlertDialog.Builder builder=new M3AlertDialogBuilder(ctx)
|
||||
.setTitle(item==null ? R.string.sk_add_timeline : R.string.sk_edit_timeline)
|
||||
.setView(view)
|
||||
.setPositiveButton(R.string.save, (d, which)->{
|
||||
String name=editText.getText().toString().trim();
|
||||
|
||||
TimelineDefinition tl = item != null ? item : TimelineDefinition.ofHashtag(name);
|
||||
TimelineDefinition.Icon icon = TimelineDefinition.Icon.values()[(int) btn.getTag()];
|
||||
tl.setIcon(icon);
|
||||
tl.setTitle(name);
|
||||
tl.setTagOptions(
|
||||
mainHashtag,
|
||||
tagsAny.getChipValues(),
|
||||
tagsAll.getChipValues(),
|
||||
tagsNone.getChipValues(),
|
||||
localOnlySwitch.isChecked()
|
||||
);
|
||||
onSave.accept(tl);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, (d, which) -> {});
|
||||
String mainHashtag=tagMain.getText().toString().trim();
|
||||
if(item.getType()==TimelineDefinition.TimelineType.HASHTAG){
|
||||
tagsAny.chipifyAllUnterminatedTokens();
|
||||
tagsAll.chipifyAllUnterminatedTokens();
|
||||
tagsNone.chipifyAllUnterminatedTokens();
|
||||
if(TextUtils.isEmpty(mainHashtag)){
|
||||
mainHashtag=name;
|
||||
name=null;
|
||||
}
|
||||
if(TextUtils.isEmpty(mainHashtag) && (item!=null && item.getType()==TimelineDefinition.TimelineType.HASHTAG)){
|
||||
Toast.makeText(ctx, R.string.sk_add_timeline_tag_error_empty, Toast.LENGTH_SHORT).show();
|
||||
onSave.accept(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (onRemove != null) builder.setNeutralButton(R.string.sk_remove, (d, which) -> onRemove.run());
|
||||
TimelineDefinition tl=item!=null ? item : TimelineDefinition.ofHashtag(name);
|
||||
TimelineDefinition.Icon icon=TimelineDefinition.Icon.values()[(int) btn.getTag()];
|
||||
tl.setIcon(icon);
|
||||
tl.setTitle(name);
|
||||
if(item.getType()==TimelineDefinition.TimelineType.HASHTAG){
|
||||
tl.setTagOptions(
|
||||
mainHashtag,
|
||||
tagsAny.getChipValues(),
|
||||
tagsAll.getChipValues(),
|
||||
tagsNone.getChipValues(),
|
||||
localOnlySwitch.isChecked()
|
||||
);
|
||||
}
|
||||
onSave.accept(tl);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, (d, which)->{});
|
||||
|
||||
builder.show();
|
||||
btn.requestFocus();
|
||||
}
|
||||
if(onRemove!=null) builder.setNeutralButton(R.string.sk_remove, (d, which)->onRemove.run());
|
||||
|
||||
private class TimelinesAdapter extends RecyclerView.Adapter<TimelineViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
public TimelineViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new TimelineViewHolder();
|
||||
}
|
||||
builder.show();
|
||||
btn.requestFocus();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull TimelineViewHolder holder, int position) {
|
||||
holder.bind(data.get(position));
|
||||
}
|
||||
private class TimelinesAdapter extends RecyclerView.Adapter<TimelineViewHolder>{
|
||||
@NonNull
|
||||
@Override
|
||||
public TimelineViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
|
||||
return new TimelineViewHolder();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data.size();
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull TimelineViewHolder holder, int position){
|
||||
holder.bind(data.get(position));
|
||||
}
|
||||
|
||||
private class TimelineViewHolder extends BindableViewHolder<TimelineDefinition> implements UsableRecyclerView.Clickable{
|
||||
private final TextView title;
|
||||
private final ImageView dragger;
|
||||
@Override
|
||||
public int getItemCount(){
|
||||
return data.size();
|
||||
}
|
||||
}
|
||||
|
||||
public TimelineViewHolder(){
|
||||
super(getActivity(), R.layout.item_text, list);
|
||||
title=findViewById(R.id.title);
|
||||
dragger=findViewById(R.id.dragger_thingy);
|
||||
}
|
||||
private class TimelineViewHolder extends BindableViewHolder<TimelineDefinition> implements UsableRecyclerView.Clickable{
|
||||
private final TextView title;
|
||||
private final ImageView dragger;
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onBind(TimelineDefinition item) {
|
||||
title.setText(item.getTitle(getContext()));
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(item.getIcon().iconRes), null, null, null);
|
||||
dragger.setVisibility(View.VISIBLE);
|
||||
dragger.setOnTouchListener((View v, MotionEvent event) -> {
|
||||
if (event.getAction() == MotionEvent.ACTION_DOWN) {
|
||||
itemTouchHelper.startDrag(this);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
public TimelineViewHolder(){
|
||||
super(getActivity(), R.layout.item_text, list);
|
||||
title=findViewById(R.id.title);
|
||||
dragger=findViewById(R.id.dragger_thingy);
|
||||
}
|
||||
|
||||
private void onSave(TimelineDefinition tl) {
|
||||
saveTimelines();
|
||||
rebind();
|
||||
}
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onBind(TimelineDefinition item){
|
||||
title.setText(item.getTitle(getContext()));
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(itemView.getContext().getDrawable(item.getIcon().iconRes), null, null, null);
|
||||
dragger.setVisibility(View.VISIBLE);
|
||||
dragger.setOnTouchListener((View v, MotionEvent event)->{
|
||||
if(event.getAction()==MotionEvent.ACTION_DOWN){
|
||||
itemTouchHelper.startDrag(this);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
private void onRemove() {
|
||||
removeTimeline(getAbsoluteAdapterPosition());
|
||||
}
|
||||
private void onSave(TimelineDefinition tl){
|
||||
saveTimelines();
|
||||
rebind();
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onClick() {
|
||||
makeTimelineEditor(item, this::onSave, this::onRemove);
|
||||
}
|
||||
}
|
||||
private void onRemove(){
|
||||
removeTimeline(getAbsoluteAdapterPosition());
|
||||
}
|
||||
|
||||
private class ItemTouchHelperCallback extends ItemTouchHelper.SimpleCallback {
|
||||
public ItemTouchHelperCallback() {
|
||||
super(ItemTouchHelper.UP | ItemTouchHelper.DOWN, ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT);
|
||||
}
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@Override
|
||||
public void onClick(){
|
||||
makeTimelineEditor(item, this::onSave, this::onRemove);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
|
||||
int fromPosition = viewHolder.getAbsoluteAdapterPosition();
|
||||
int toPosition = target.getAbsoluteAdapterPosition();
|
||||
if (Math.max(fromPosition, toPosition) >= data.size() || Math.min(fromPosition, toPosition) < 0) {
|
||||
return false;
|
||||
} else {
|
||||
Collections.swap(data, fromPosition, toPosition);
|
||||
adapter.notifyItemMoved(fromPosition, toPosition);
|
||||
saveTimelines();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
private class ItemTouchHelperCallback extends ItemTouchHelper.SimpleCallback{
|
||||
public ItemTouchHelperCallback(){
|
||||
super(ItemTouchHelper.UP|ItemTouchHelper.DOWN, ItemTouchHelper.LEFT|ItemTouchHelper.RIGHT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState) {
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG && viewHolder != null) {
|
||||
viewHolder.itemView.animate().alpha(0.65f);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target){
|
||||
int fromPosition=viewHolder.getAbsoluteAdapterPosition();
|
||||
int toPosition=target.getAbsoluteAdapterPosition();
|
||||
if(Math.max(fromPosition, toPosition)>=data.size() || Math.min(fromPosition, toPosition)<0){
|
||||
return false;
|
||||
}else{
|
||||
Collections.swap(data, fromPosition, toPosition);
|
||||
adapter.notifyItemMoved(fromPosition, toPosition);
|
||||
saveTimelines();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
viewHolder.itemView.animate().alpha(1f);
|
||||
}
|
||||
@Override
|
||||
public void onSelectedChanged(@Nullable RecyclerView.ViewHolder viewHolder, int actionState){
|
||||
if(actionState==ItemTouchHelper.ACTION_STATE_DRAG && viewHolder!=null){
|
||||
viewHolder.itemView.animate().alpha(0.65f);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
|
||||
int position = viewHolder.getAbsoluteAdapterPosition();
|
||||
removeTimeline(position);
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder){
|
||||
super.clearView(recyclerView, viewHolder);
|
||||
viewHolder.itemView.animate().alpha(1f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction){
|
||||
int position=viewHolder.getAbsoluteAdapterPosition();
|
||||
removeTimeline(position);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,7 +174,6 @@ public class NotificationsListFragment extends BaseStatusListFragment<Notificati
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||
list.addItemDecoration(new RecyclerView.ItemDecoration(){
|
||||
private Paint paint=new Paint();
|
||||
private Rect tmpRect=new Rect();
|
||||
|
||||
@@ -21,8 +21,11 @@ import android.graphics.drawable.LayerDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.InputType;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.transition.ChangeBounds;
|
||||
import android.transition.Fade;
|
||||
import android.transition.TransitionManager;
|
||||
@@ -56,6 +59,7 @@ import org.joinmastodon.android.api.requests.accounts.GetAccountRelationships;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetAccountStatuses;
|
||||
import org.joinmastodon.android.api.requests.accounts.GetOwnAccount;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetAccountFollowed;
|
||||
import org.joinmastodon.android.api.requests.accounts.SetPrivateNote;
|
||||
import org.joinmastodon.android.api.requests.accounts.UpdateAccountCredentials;
|
||||
import org.joinmastodon.android.api.requests.instance.GetInstance;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
@@ -145,7 +149,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private SwipeRefreshLayout refreshLayout;
|
||||
private View followersBtn, followingBtn;
|
||||
private EditText nameEdit, bioEdit;
|
||||
private ProgressBar actionProgress, notifyProgress;
|
||||
private ProgressBar actionProgress, notifyProgress, noteSaveProgress;
|
||||
private FrameLayout[] tabViews;
|
||||
private TabLayoutMediator tabLayoutMediator;
|
||||
private TextView followsYouView;
|
||||
@@ -186,6 +190,11 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
private ItemTouchHelper dragHelper=new ItemTouchHelper(new ReorderCallback());
|
||||
private ListImageLoaderWrapper imgLoader;
|
||||
|
||||
// profile note
|
||||
private FrameLayout noteWrap;
|
||||
private ImageButton noteSaveBtn;
|
||||
private EditText noteEdit;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -257,6 +266,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
bioEditWrap=content.findViewById(R.id.bio_edit_wrap);
|
||||
actionProgress=content.findViewById(R.id.action_progress);
|
||||
notifyProgress=content.findViewById(R.id.notify_progress);
|
||||
noteSaveProgress=content.findViewById(R.id.note_save_progress);
|
||||
fab=content.findViewById(R.id.fab);
|
||||
followsYouView=content.findViewById(R.id.follows_you);
|
||||
countersLayout=content.findViewById(R.id.profile_counters);
|
||||
@@ -271,6 +281,51 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
avatar.setOutlineProvider(OutlineProviders.roundedRect(24));
|
||||
avatar.setClipToOutline(true);
|
||||
|
||||
noteEdit=content.findViewById(R.id.note_edit);
|
||||
noteWrap=content.findViewById(R.id.note_edit_wrap);
|
||||
noteSaveBtn=content.findViewById(R.id.note_save_btn);
|
||||
|
||||
noteSaveBtn.setOnClickListener((v->{
|
||||
savePrivateNote(noteEdit.getText().toString());
|
||||
InputMethodManager imm=(InputMethodManager) getContext().getSystemService(Activity.INPUT_METHOD_SERVICE);
|
||||
imm.hideSoftInputFromWindow(this.getView().getRootView().getWindowToken(), 0);
|
||||
noteEdit.clearFocus();
|
||||
noteSaveBtn.clearFocus();
|
||||
}));
|
||||
|
||||
|
||||
noteEdit.setOnFocusChangeListener((v, hasFocus)->{
|
||||
if(hasFocus){
|
||||
hideFab();
|
||||
V.setVisibilityAnimated(noteSaveBtn, View.VISIBLE);
|
||||
noteEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
|
||||
}else if(!noteSaveBtn.hasFocus()){
|
||||
showFab();
|
||||
hideNoteSaveBtnIfNotDirty();
|
||||
}
|
||||
});
|
||||
|
||||
noteEdit.addTextChangedListener(new TextWatcher(){
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after){}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count){
|
||||
if(relationship!=null && noteSaveBtn.getVisibility()!=View.VISIBLE && !s.toString().equals(relationship.note))
|
||||
V.setVisibilityAnimated(noteSaveBtn, View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s){}
|
||||
});
|
||||
|
||||
noteSaveBtn.setOnFocusChangeListener((v, hasFocus)->{
|
||||
if(!hasFocus && !noteEdit.hasFocus()){
|
||||
showFab();
|
||||
hideNoteSaveBtnIfNotDirty();
|
||||
}
|
||||
});
|
||||
|
||||
FrameLayout sizeWrapper=new FrameLayout(getActivity()){
|
||||
@Override
|
||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
|
||||
@@ -435,6 +490,46 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
return sizeWrapper;
|
||||
}
|
||||
|
||||
private void hideNoteSaveBtnIfNotDirty(){
|
||||
if(noteEdit.getText().toString().equals(relationship.note)){
|
||||
V.setVisibilityAnimated(noteSaveBtn, View.INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
private void showPrivateNote(){
|
||||
noteWrap.setVisibility(View.VISIBLE);
|
||||
noteEdit.setText(relationship.note);
|
||||
}
|
||||
|
||||
private void hidePrivateNote(){
|
||||
noteWrap.setVisibility(View.GONE);
|
||||
noteEdit.setText(null);
|
||||
}
|
||||
|
||||
private void savePrivateNote(String note){
|
||||
if(note!=null && note.equals(relationship.note)){
|
||||
updateRelationship();
|
||||
invalidateOptionsMenu();
|
||||
return;
|
||||
}
|
||||
V.setVisibilityAnimated(noteSaveProgress, View.VISIBLE);
|
||||
V.setVisibilityAnimated(noteSaveBtn, View.INVISIBLE);
|
||||
new SetPrivateNote(profileAccountID, note).setCallback(new Callback<>() {
|
||||
@Override
|
||||
public void onSuccess(Relationship result) {
|
||||
updateRelationship(result);
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(ErrorResponse error) {
|
||||
error.showToast(getContext());
|
||||
V.setVisibilityAnimated(noteSaveProgress, View.GONE);
|
||||
V.setVisibilityAnimated(noteSaveBtn, View.VISIBLE);
|
||||
}
|
||||
}).exec(accountID);
|
||||
}
|
||||
|
||||
private void onAccountLoaded(Account result) {
|
||||
account=result;
|
||||
isOwnProfile=AccountSessionManager.getInstance().isSelf(accountID, account);
|
||||
@@ -793,6 +888,8 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}else{
|
||||
blockDomain.setVisible(false);
|
||||
}
|
||||
menu.findItem(R.id.edit_note).setTitle(noteWrap.getVisibility()==View.GONE && (relationship.note==null || relationship.note.isEmpty())
|
||||
? R.string.sk_add_note : R.string.sk_delete_note);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -874,6 +971,26 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
}else if(id==R.id.save){
|
||||
if(isInEditMode)
|
||||
saveAndExitEditMode();
|
||||
}else if(id==R.id.edit_note){
|
||||
if(noteWrap.getVisibility()==View.GONE){
|
||||
showPrivateNote();
|
||||
UiUtils.beginLayoutTransition(scrollableContent);
|
||||
noteEdit.requestFocus();
|
||||
noteEdit.postDelayed(()->{
|
||||
InputMethodManager imm=getActivity().getSystemService(InputMethodManager.class);
|
||||
imm.showSoftInput(noteEdit, 0);
|
||||
}, 100);
|
||||
}else if(relationship.note.isEmpty()){
|
||||
hidePrivateNote();
|
||||
UiUtils.beginLayoutTransition(scrollableContent);
|
||||
}else{
|
||||
new M3AlertDialogBuilder(getActivity())
|
||||
.setMessage(getContext().getString(R.string.sk_private_note_confirm_delete, account.getDisplayUsername()))
|
||||
.setPositiveButton(R.string.delete, (dlg, btn)->savePrivateNote(null))
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
invalidateOptionsMenu();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -899,15 +1016,22 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
|
||||
private void updateRelationship(){
|
||||
if(getActivity()==null) return;
|
||||
if(relationship.note!=null && !relationship.note.isEmpty()) showPrivateNote();
|
||||
else hidePrivateNote();
|
||||
invalidateOptionsMenu();
|
||||
actionButton.setVisibility(View.VISIBLE);
|
||||
notifyButton.setVisibility(relationship.following ? View.VISIBLE : View.GONE);
|
||||
UiUtils.setRelationshipToActionButtonM3(relationship, actionButton);
|
||||
actionProgress.setIndeterminateTintList(actionButton.getTextColors());
|
||||
notifyProgress.setIndeterminateTintList(notifyButton.getTextColors());
|
||||
noteSaveProgress.setIndeterminateTintList(noteEdit.getTextColors());
|
||||
followsYouView.setVisibility(relationship.followedBy ? View.VISIBLE : View.GONE);
|
||||
notifyButton.setSelected(relationship.notifying);
|
||||
notifyButton.setContentDescription(getString(relationship.notifying ? R.string.sk_user_post_notifications_on : R.string.sk_user_post_notifications_off, '@'+account.username));
|
||||
noteEdit.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_CAP_SENTENCES | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
|
||||
V.setVisibilityAnimated(noteSaveProgress, View.GONE);
|
||||
V.setVisibilityAnimated(noteSaveBtn, View.INVISIBLE);
|
||||
UiUtils.beginLayoutTransition(scrollableContent);
|
||||
}
|
||||
|
||||
public ImageButton getFab() {
|
||||
@@ -1477,7 +1601,7 @@ public class ProfileFragment extends LoaderFragment implements OnBackPressedList
|
||||
title.setText(item.parsedName);
|
||||
value.setText(item.parsedValue);
|
||||
if(item.verifiedAt!=null){
|
||||
int textColor=UiUtils.isDarkTheme() ? 0xFF89bb9c : 0xFF5b8e63;
|
||||
int textColor=UiUtils.getThemeColor(getContext(), R.attr.colorM3Success);
|
||||
value.setTextColor(textColor);
|
||||
value.setLinkTextColor(textColor);
|
||||
Drawable check=getResources().getDrawable(R.drawable.ic_fluent_checkmark_starburst_20_regular, getActivity().getTheme()).mutate();
|
||||
|
||||
@@ -12,18 +12,22 @@ import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.displayitems.DummyStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.ReblogOrReplyLineStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.InsetStatusItemDecoration;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
|
||||
import java.time.ZoneId;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.EnumSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import me.grishka.appkit.api.SimpleCallback;
|
||||
import name.fraser.neil.plaintext.diff_match_patch;
|
||||
|
||||
public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
private String id, url;
|
||||
@@ -58,7 +62,7 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, null, StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS);
|
||||
List<StatusDisplayItem> items=new ArrayList<>();
|
||||
int idx=data.indexOf(s);
|
||||
if(idx>=0){
|
||||
String date=UiUtils.DATE_TIME_FORMATTER.format(s.createdAt.atZone(ZoneId.systemDefault()));
|
||||
@@ -83,8 +87,11 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
EnumSet<StatusEditChangeType> changes=EnumSet.noneOf(StatusEditChangeType.class);
|
||||
Status prev=data.get(idx+1);
|
||||
|
||||
if(!Objects.equals(s.content, prev.content)){
|
||||
// if only formatting was changed, don't even try to create a diff text
|
||||
if(!Objects.equals(HtmlParser.text(s.content), HtmlParser.text(prev.content))){
|
||||
changes.add(StatusEditChangeType.TEXT_CHANGED);
|
||||
//update status content to display a diffs
|
||||
s.content=createDiffText(prev.content, s.content);
|
||||
}
|
||||
if(!Objects.equals(s.spoilerText, prev.spoilerText)){
|
||||
if(s.spoilerText==null){
|
||||
@@ -147,15 +154,10 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
items.add(0, new ReblogOrReplyLineStatusDisplayItem(s.id, this, action+" "+sep+" "+date, Collections.emptyList(), 0, null, null, s));
|
||||
items.add(1, new DummyStatusDisplayItem(s.id, this));
|
||||
}
|
||||
items.addAll(StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, null, StatusDisplayItem.FLAG_NO_FOOTER|StatusDisplayItem.FLAG_INSET|StatusDisplayItem.FLAG_NO_EMOJI_REACTIONS));
|
||||
return items;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(View view, Bundle savedInstanceState){
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
list.addItemDecoration(new InsetStatusItemDecoration(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isItemEnabled(String id){
|
||||
return false;
|
||||
@@ -170,4 +172,28 @@ public class StatusEditHistoryFragment extends StatusListFragment{
|
||||
public Uri getWebUri(Uri.Builder base) {
|
||||
return Uri.parse(url);
|
||||
}
|
||||
|
||||
private String createDiffText(String original, String modified) {
|
||||
diff_match_patch dmp=new diff_match_patch();
|
||||
LinkedList<diff_match_patch.Diff> diffs=dmp.diff_main(original, modified);
|
||||
dmp.diff_cleanupSemantic(diffs);
|
||||
|
||||
StringBuilder stringBuilder=new StringBuilder();
|
||||
for(diff_match_patch.Diff diff : diffs){
|
||||
switch(diff.operation){
|
||||
case DELETE->{
|
||||
stringBuilder.append("<edit-diff-delete>");
|
||||
stringBuilder.append(diff.text);
|
||||
stringBuilder.append("</edit-diff-delete>");
|
||||
}
|
||||
case INSERT->{
|
||||
stringBuilder.append("<edit-diff-insert>");
|
||||
stringBuilder.append(diff.text);
|
||||
stringBuilder.append("</edit-diff-insert>");
|
||||
}
|
||||
default->stringBuilder.append(diff.text);
|
||||
}
|
||||
}
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,8 +84,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
|
||||
@Override
|
||||
public void onItemClick(String id){
|
||||
Status status=getContentStatusByID(id);
|
||||
if(status==null)
|
||||
return;
|
||||
if(status==null || status.preview) return;
|
||||
status.filterRevealed=true;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
@@ -353,6 +352,7 @@ public abstract class StatusListFragment extends BaseStatusListFragment<Status>
|
||||
Status contentStatus=status.getContentStatus();
|
||||
if(contentStatus.poll!=null && contentStatus.poll.id.equals(ev.poll.id)){
|
||||
updatePoll(status.id, contentStatus, ev.poll);
|
||||
AccountSessionManager.get(accountID).getCacheController().updateStatus(contentStatus);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,21 +50,25 @@ import me.grishka.appkit.api.SimpleCallback;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class ThreadFragment extends StatusListFragment implements ProvidesAssistContent {
|
||||
protected Status mainStatus, updatedStatus;
|
||||
protected Status mainStatus, updatedStatus, replyTo;
|
||||
private final HashMap<String, NeighborAncestryInfo> ancestryMap = new HashMap<>();
|
||||
private StatusContext result;
|
||||
protected boolean contextInitiallyRendered, transitionFinished;
|
||||
protected boolean contextInitiallyRendered, transitionFinished, preview;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
mainStatus=Parcels.unwrap(getArguments().getParcelable("status"));
|
||||
replyTo=Parcels.unwrap(getArguments().getParcelable("inReplyTo"));
|
||||
Account inReplyToAccount=Parcels.unwrap(getArguments().getParcelable("inReplyToAccount"));
|
||||
refreshing=contextInitiallyRendered=getArguments().getBoolean("refresh", false);
|
||||
if(inReplyToAccount!=null)
|
||||
knownAccounts.put(inReplyToAccount.id, inReplyToAccount);
|
||||
data.add(mainStatus);
|
||||
onAppendItems(Collections.singletonList(mainStatus));
|
||||
setTitle(HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.getDisplayName()), mainStatus.account.emojis));
|
||||
preview=mainStatus.preview;
|
||||
if(preview) setRefreshEnabled(false);
|
||||
setTitle(preview ? getString(R.string.sk_post_preview) : HtmlParser.parseCustomEmoji(getString(R.string.post_from_user, mainStatus.account.getDisplayName()), mainStatus.account.emojis));
|
||||
transitionFinished = getArguments().getBoolean("noTransition", false);
|
||||
}
|
||||
|
||||
@@ -129,11 +133,21 @@ public class ThreadFragment extends StatusListFragment implements ProvidesAssist
|
||||
|
||||
@Override
|
||||
protected void doLoadData(int offset, int count){
|
||||
if (refreshing) loadMainStatus();
|
||||
currentRequest=new GetStatusContext(mainStatus.id)
|
||||
if(preview && replyTo==null){
|
||||
result=new StatusContext();
|
||||
result.descendants=Collections.emptyList();
|
||||
result.ancestors=Collections.emptyList();
|
||||
return;
|
||||
}
|
||||
if(refreshing && !preview) loadMainStatus();
|
||||
currentRequest=new GetStatusContext(preview ? replyTo.id : mainStatus.id)
|
||||
.setCallback(new SimpleCallback<>(this){
|
||||
@Override
|
||||
public void onSuccess(StatusContext result){
|
||||
if(preview){
|
||||
result.descendants=Collections.emptyList();
|
||||
result.ancestors.add(replyTo);
|
||||
}
|
||||
ThreadFragment.this.result = result;
|
||||
maybeApplyContext();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package org.joinmastodon.android.fragments.report;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
@@ -25,6 +23,7 @@ import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.displayitems.AudioStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.CheckableHeaderStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.DummyStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.LinkCardStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.MediaGridStatusDisplayItem;
|
||||
import org.joinmastodon.android.ui.displayitems.StatusDisplayItem;
|
||||
@@ -97,8 +96,7 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
.exec(accountID);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemClick(String id){
|
||||
public void onToggleItem(String id){
|
||||
if(selectedIDs.contains(id))
|
||||
selectedIDs.remove(id);
|
||||
else
|
||||
@@ -121,13 +119,20 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
|
||||
if(holder.getAbsoluteAdapterPosition()==0 || holder instanceof CheckableHeaderStatusDisplayItem.Holder)
|
||||
return;
|
||||
outRect.left=V.dp(40);
|
||||
boolean isRTL=parent.getLayoutDirection()==View.LAYOUT_DIRECTION_RTL;
|
||||
if(isRTL) outRect.right=V.dp(40);
|
||||
else outRect.left=V.dp(40);
|
||||
if(holder instanceof AudioStatusDisplayItem.Holder){
|
||||
outRect.bottom=V.dp(16);
|
||||
}else if(holder instanceof LinkCardStatusDisplayItem.Holder || holder instanceof MediaGridStatusDisplayItem.Holder){
|
||||
outRect.bottom=V.dp(16);
|
||||
outRect.left+=V.dp(16);
|
||||
outRect.right=V.dp(16);
|
||||
outRect.bottom=V.dp(8);
|
||||
if(isRTL){
|
||||
outRect.right+=V.dp(16);
|
||||
outRect.left=V.dp(16);
|
||||
}else{
|
||||
outRect.left+=V.dp(16);
|
||||
outRect.right=V.dp(16);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -155,9 +160,6 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
return adapter;
|
||||
}
|
||||
|
||||
protected void drawDivider(View child, View bottomSibling, RecyclerView.ViewHolder holder, RecyclerView.ViewHolder siblingHolder, RecyclerView parent, Canvas c, Paint paint){
|
||||
}
|
||||
|
||||
private void onButtonClick(View v){
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", accountID);
|
||||
@@ -201,7 +203,9 @@ public class ReportAddPostsChoiceFragment extends StatusListFragment{
|
||||
|
||||
@Override
|
||||
protected List<StatusDisplayItem> buildDisplayItems(Status s){
|
||||
return StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), StatusDisplayItem.FLAG_INSET | StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_CHECKABLE | StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN);
|
||||
List<StatusDisplayItem> items=StatusDisplayItem.buildItems(this, s, accountID, s, knownAccounts, getFilterContext(), StatusDisplayItem.FLAG_NO_FOOTER | StatusDisplayItem.FLAG_CHECKABLE | StatusDisplayItem.FLAG_MEDIA_FORCE_HIDDEN);
|
||||
items.add(new DummyStatusDisplayItem(s.getID(), this));
|
||||
return items;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -204,14 +204,6 @@ public class ReportReasonChoiceFragment extends StatusListFragment{
|
||||
float off=paint.getStrokeWidth()/2f;
|
||||
c.drawRoundRect(V.dp(16)-off, top-off, parent.getWidth()-V.dp(16)+off, bottom+off, V.dp(12), V.dp(12), paint);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getItemOffsets(@NonNull Rect outRect, @NonNull View view, @NonNull RecyclerView parent, @NonNull RecyclerView.State state){
|
||||
RecyclerView.ViewHolder holder=parent.getChildViewHolder(view);
|
||||
if(holder instanceof StatusDisplayItem.Holder<?>){
|
||||
outRect.left=outRect.right=V.dp(16);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,38 @@ package org.joinmastodon.android.fragments.settings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.Gravity;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.MastodonAPIController;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.StringReader;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -24,30 +43,46 @@ import me.grishka.appkit.utils.SingleViewRecyclerAdapter;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{
|
||||
private ListItem<Void> mediaCacheItem;
|
||||
private static final String TAG="SettingsAboutAppFragment";
|
||||
private ListItem<Void> mediaCacheItem, copyCrashLogItem;
|
||||
private CheckableListItem<Void> enablePreReleasesItem;
|
||||
private AccountSession session;
|
||||
private boolean timelineCacheCleared=false;
|
||||
private File crashLogFile=new File(MastodonApp.context.getFilesDir(), "crash.log");
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
setTitle(getString(R.string.about_app, getString(R.string.sk_app_name)));
|
||||
session=AccountSessionManager.get(accountID);
|
||||
onDataLoaded(List.of(
|
||||
|
||||
String lastModified=crashLogFile.exists()
|
||||
? DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT).withZone(ZoneId.systemDefault()).format(Instant.ofEpochMilli(crashLogFile.lastModified()))
|
||||
: getString(R.string.sk_settings_crash_log_unavailable);
|
||||
List<ListItem<Void>> items=new ArrayList<>(List.of(
|
||||
new ListItem<>(R.string.sk_settings_donate, 0, R.drawable.ic_fluent_heart_24_regular, i->UiUtils.openHashtagTimeline(getActivity(), accountID, getString(R.string.donate_hashtag))),
|
||||
new ListItem<>(R.string.sk_settings_contribute, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), getString(R.string.repo_url))),
|
||||
new ListItem<>(R.string.settings_tos, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")),
|
||||
new ListItem<>(R.string.settings_privacy_policy, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), getString(R.string.privacy_policy_url)), 0, true),
|
||||
mediaCacheItem=new ListItem<>(R.string.settings_clear_cache, 0, this::onClearMediaCacheClick),
|
||||
new ListItem<>(getString(R.string.sk_settings_clear_timeline_cache), session.domain, this::onClearTimelineCacheClick)
|
||||
new ListItem<>(getString(R.string.sk_settings_clear_timeline_cache), session.domain, this::onClearTimelineCacheClick),
|
||||
copyCrashLogItem=new ListItem<>(getString(R.string.sk_settings_copy_crash_log), lastModified, 0, this::onCopyCrashLog)
|
||||
));
|
||||
|
||||
if(GithubSelfUpdater.needSelfUpdating()){
|
||||
items.add(enablePreReleasesItem=new CheckableListItem<>(R.string.sk_updater_enable_pre_releases, 0, CheckableListItem.Style.SWITCH, GlobalUserPreferences.enablePreReleases, i->toggleCheckableItem(enablePreReleasesItem)));
|
||||
}
|
||||
|
||||
copyCrashLogItem.isEnabled=crashLogFile.exists();
|
||||
onDataLoaded(items);
|
||||
updateMediaCacheItem();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onHidden(){
|
||||
super.onHidden();
|
||||
GlobalUserPreferences.enablePreReleases=enablePreReleasesItem.checked;
|
||||
GlobalUserPreferences.save();
|
||||
if(timelineCacheCleared) getActivity().recreate();
|
||||
}
|
||||
|
||||
@@ -94,4 +129,17 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void>{
|
||||
mediaCacheItem.isEnabled=size>0;
|
||||
rebindItem(mediaCacheItem);
|
||||
}
|
||||
|
||||
private void onCopyCrashLog(ListItem<?> item){
|
||||
if(!crashLogFile.exists()) return;
|
||||
try(InputStream is=new FileInputStream(crashLogFile)){
|
||||
BufferedReader reader=new BufferedReader(new InputStreamReader(is));
|
||||
StringBuilder sb=new StringBuilder();
|
||||
String line;
|
||||
while ((line=reader.readLine())!=null) sb.append(line).append("\n");
|
||||
UiUtils.copyText(list, sb.toString());
|
||||
} catch(IOException e){
|
||||
Log.e(TAG, "Error reading crash log", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,13 +334,15 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
|
||||
return;
|
||||
}
|
||||
|
||||
UnifiedPush.unregisterApp(
|
||||
getContext(),
|
||||
accountID
|
||||
);
|
||||
for (AccountSession accountSession : AccountSessionManager.getInstance().getLoggedInAccounts()) {
|
||||
UnifiedPush.unregisterApp(
|
||||
getContext(),
|
||||
accountSession.getID()
|
||||
);
|
||||
|
||||
//re-register to fcm
|
||||
AccountSessionManager.getInstance().getAccount(accountID).getPushSubscriptionManager().registerAccountForPush(getPushSubscription());
|
||||
//re-register to fcm
|
||||
accountSession.getPushSubscriptionManager().registerAccountForPush(getPushSubscription());
|
||||
}
|
||||
unifiedPushItem.toggle();
|
||||
rebindItem(unifiedPushItem);
|
||||
}
|
||||
@@ -350,12 +352,14 @@ public class SettingsNotificationsFragment extends BaseSettingsFragment<Void>{
|
||||
(dialog, which)->{
|
||||
String userDistrib = distributors.get(which);
|
||||
UnifiedPush.saveDistributor(getContext(), userDistrib);
|
||||
UnifiedPush.registerApp(
|
||||
getContext(),
|
||||
accountID,
|
||||
new ArrayList<>(),
|
||||
getContext().getPackageName()
|
||||
);
|
||||
for (AccountSession accountSession : AccountSessionManager.getInstance().getLoggedInAccounts()){
|
||||
UnifiedPush.registerApp(
|
||||
getContext(),
|
||||
accountSession.getID(),
|
||||
new ArrayList<>(),
|
||||
getContext().getPackageName()
|
||||
);
|
||||
}
|
||||
unifiedPushItem.toggle();
|
||||
rebindItem(unifiedPushItem);
|
||||
}).setOnCancelListener(d->rebindItem(unifiedPushItem)).show();
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
public class AkkomaTranslation extends BaseModel{
|
||||
public String text;
|
||||
public String detectedLanguage;
|
||||
|
||||
public Translation toTranslation() {
|
||||
Translation translation=new Translation();
|
||||
translation.content=text;
|
||||
translation.detectedSourceLanguage=detectedLanguage;
|
||||
translation.provider="Akkoma";
|
||||
return translation;
|
||||
}
|
||||
}
|
||||
@@ -161,11 +161,15 @@ public class Instance extends BaseModel{
|
||||
case BUBBLE_TIMELINE -> pleromaFeatures
|
||||
.map(f -> f.contains("bubble_timeline"))
|
||||
.orElse(false);
|
||||
case MACHINE_TRANSLATION -> pleromaFeatures
|
||||
.map(f -> f.contains("akkoma:machine_translation"))
|
||||
.orElse(false);
|
||||
};
|
||||
}
|
||||
|
||||
public enum Feature {
|
||||
BUBBLE_TIMELINE
|
||||
BUBBLE_TIMELINE,
|
||||
MACHINE_TRANSLATION
|
||||
}
|
||||
|
||||
@Parcel
|
||||
|
||||
@@ -4,6 +4,7 @@ import static org.joinmastodon.android.api.MastodonAPIController.gson;
|
||||
import static org.joinmastodon.android.api.MastodonAPIController.gsonWithoutDeserializer;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.joinmastodon.android.api.ObjectValidationException;
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
@@ -96,6 +97,7 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
||||
public transient TranslationState translationState=TranslationState.HIDDEN;
|
||||
public transient Translation translation;
|
||||
public transient boolean fromStatusCreated;
|
||||
public transient boolean preview;
|
||||
|
||||
public Status(){}
|
||||
|
||||
@@ -123,6 +125,8 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
||||
if(filtered!=null)
|
||||
for(FilterResult fr:filtered)
|
||||
fr.postprocess();
|
||||
if(quote!=null)
|
||||
quote.postprocess();
|
||||
|
||||
spoilerRevealed=!hasSpoiler();
|
||||
if(!spoilerRevealed) sensitive=true;
|
||||
@@ -217,16 +221,17 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
||||
|
||||
public static final Pattern BOTTOM_TEXT_PATTERN = Pattern.compile("(?:[\uD83E\uDEC2\uD83D\uDC96✨\uD83E\uDD7A,]+|❤️)(?:\uD83D\uDC49\uD83D\uDC48(?:[\uD83E\uDEC2\uD83D\uDC96✨\uD83E\uDD7A,]+|❤️))*\uD83D\uDC49\uD83D\uDC48");
|
||||
public boolean isEligibleForTranslation(AccountSession session){
|
||||
Instance instanceInfo = AccountSessionManager.getInstance().getInstanceInfo(session.domain);
|
||||
boolean translateEnabled = instanceInfo != null &&
|
||||
instanceInfo.v2 != null && instanceInfo.v2.configuration.translation != null &&
|
||||
instanceInfo.v2.configuration.translation.enabled;
|
||||
Instance instanceInfo=AccountSessionManager.getInstance().getInstanceInfo(session.domain);
|
||||
boolean translateEnabled=instanceInfo!=null && (
|
||||
(instanceInfo.v2!=null && instanceInfo.v2.configuration.translation!=null && instanceInfo.v2.configuration.translation.enabled) ||
|
||||
(instanceInfo.isAkkoma() && instanceInfo.hasFeature(Instance.Feature.MACHINE_TRANSLATION))
|
||||
);
|
||||
|
||||
try {
|
||||
String bottomText = BOTTOM_TEXT_PATTERN.matcher(getStrippedText()).find()
|
||||
Pair<String, List<String>> decoded=BOTTOM_TEXT_PATTERN.matcher(getStrippedText()).find()
|
||||
? new StatusTextEncoder(Bottom::decode).decode(getStrippedText(), BOTTOM_TEXT_PATTERN)
|
||||
: null;
|
||||
if(bottomText==null || bottomText.length()==0 || bottomText.equals("\u0005")) bottomText=null;
|
||||
String bottomText=decoded==null || decoded.second.stream().allMatch(s->s.trim().isEmpty()) ? null : decoded.first;
|
||||
if(bottomText!=null){
|
||||
translation=new Translation();
|
||||
translation.content=bottomText;
|
||||
@@ -263,7 +268,7 @@ public class Status extends BaseModel implements DisplayItemsParent, Searchable{
|
||||
s.visibility=StatusPrivacy.PUBLIC;
|
||||
s.reactions=List.of();
|
||||
s.mentions=List.of();
|
||||
s.tags =List.of();
|
||||
s.tags=List.of();
|
||||
s.emojis=List.of();
|
||||
s.filtered=List.of();
|
||||
return s;
|
||||
|
||||
@@ -12,6 +12,8 @@ import androidx.annotation.StringRes;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.api.session.AccountSession;
|
||||
import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.BookmarkedStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.FavoritedStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.HashtagTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.HomeTimelineFragment;
|
||||
import org.joinmastodon.android.fragments.ListTimelineFragment;
|
||||
@@ -138,6 +140,8 @@ public class TimelineDefinition {
|
||||
case LIST -> listTitle;
|
||||
case HASHTAG -> hashtagName;
|
||||
case BUBBLE -> ctx.getString(R.string.sk_timeline_bubble);
|
||||
case BOOKMARKS -> ctx.getString(R.string.bookmarks);
|
||||
case FAVORITES -> ctx.getString(R.string.your_favorites);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -150,6 +154,8 @@ public class TimelineDefinition {
|
||||
case LIST -> listIsExclusive ? Icon.EXCLUSIVE_LIST : Icon.LIST;
|
||||
case HASHTAG -> Icon.HASHTAG;
|
||||
case BUBBLE -> Icon.BUBBLE;
|
||||
case BOOKMARKS -> Icon.BOOKMARKS;
|
||||
case FAVORITES -> Icon.FAVORITES;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -162,6 +168,8 @@ public class TimelineDefinition {
|
||||
case HASHTAG -> new HashtagTimelineFragment();
|
||||
case POST_NOTIFICATIONS -> new NotificationsListFragment();
|
||||
case BUBBLE -> new BubbleTimelineFragment();
|
||||
case BOOKMARKS -> new BookmarkedStatusListFragment();
|
||||
case FAVORITES -> new FavoritedStatusListFragment();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -228,7 +236,19 @@ public class TimelineDefinition {
|
||||
return args;
|
||||
}
|
||||
|
||||
public enum TimelineType { HOME, LOCAL, FEDERATED, POST_NOTIFICATIONS, LIST, HASHTAG, BUBBLE }
|
||||
public enum TimelineType {
|
||||
HOME,
|
||||
LOCAL,
|
||||
FEDERATED,
|
||||
POST_NOTIFICATIONS,
|
||||
LIST,
|
||||
HASHTAG,
|
||||
BUBBLE,
|
||||
|
||||
// not really timelines, but some people want it, so,,
|
||||
BOOKMARKS,
|
||||
FAVORITES
|
||||
}
|
||||
|
||||
public enum Icon {
|
||||
HEART(R.drawable.ic_fluent_heart_24_regular, R.string.sk_icon_heart),
|
||||
@@ -293,6 +313,13 @@ public class TimelineDefinition {
|
||||
DOCTOR(R.drawable.ic_fluent_doctor_24_regular, R.string.sk_icon_doctor),
|
||||
DIAMOND(R.drawable.ic_fluent_premium_24_regular, R.string.sk_icon_diamond),
|
||||
UMBRELLA(R.drawable.ic_fluent_umbrella_24_regular, R.string.sk_icon_umbrella),
|
||||
WATER(R.drawable.ic_fluent_water_24_regular, R.string.sk_icon_water),
|
||||
SUN(R.drawable.ic_fluent_weather_sunny_24_regular, R.string.sk_icon_sun),
|
||||
SUNSET(R.drawable.ic_fluent_weather_sunny_low_24_regular, R.string.sk_icon_sunset),
|
||||
CLOUD(R.drawable.ic_fluent_cloud_24_regular, R.string.sk_icon_cloud),
|
||||
THUNDERSTORM(R.drawable.ic_fluent_weather_thunderstorm_24_regular, R.string.sk_icon_thunderstorm),
|
||||
RAIN(R.drawable.ic_fluent_weather_rain_24_regular, R.string.sk_icon_rain),
|
||||
SNOWFLAKE(R.drawable.ic_fluent_weather_snowflake_24_regular, R.string.sk_icon_snowflake),
|
||||
|
||||
HOME(R.drawable.ic_fluent_home_24_regular, R.string.sk_timeline_home, true),
|
||||
LOCAL(R.drawable.ic_fluent_people_community_24_regular, R.string.sk_timeline_local, true),
|
||||
@@ -301,7 +328,9 @@ public class TimelineDefinition {
|
||||
LIST(R.drawable.ic_fluent_people_24_regular, R.string.sk_list, true),
|
||||
EXCLUSIVE_LIST(R.drawable.ic_fluent_rss_24_regular, R.string.sk_exclusive_list, true),
|
||||
HASHTAG(R.drawable.ic_fluent_number_symbol_24_regular, R.string.sk_hashtag, true),
|
||||
BUBBLE(R.drawable.ic_fluent_circle_24_regular, R.string.sk_timeline_bubble, true);
|
||||
BUBBLE(R.drawable.ic_fluent_circle_24_regular, R.string.sk_timeline_bubble, true),
|
||||
BOOKMARKS(R.drawable.ic_fluent_bookmark_multiple_24_regular, R.string.bookmarks, true),
|
||||
FAVORITES(R.drawable.ic_fluent_star_24_regular, R.string.your_favorites, true);
|
||||
|
||||
public final int iconRes, nameRes;
|
||||
public final boolean hidden;
|
||||
@@ -321,6 +350,8 @@ public class TimelineDefinition {
|
||||
public static final TimelineDefinition LOCAL_TIMELINE = new TimelineDefinition(TimelineType.LOCAL);
|
||||
public static final TimelineDefinition FEDERATED_TIMELINE = new TimelineDefinition(TimelineType.FEDERATED);
|
||||
public static final TimelineDefinition POSTS_TIMELINE = new TimelineDefinition(TimelineType.POST_NOTIFICATIONS);
|
||||
public static final TimelineDefinition BOOKMARKS_TIMELINE = new TimelineDefinition(TimelineType.BOOKMARKS);
|
||||
public static final TimelineDefinition FAVORITES_TIMELINE = new TimelineDefinition(TimelineType.FAVORITES);
|
||||
public static final TimelineDefinition BUBBLE_TIMELINE = new TimelineDefinition(TimelineType.BUBBLE) {
|
||||
@Override
|
||||
public boolean isCompatible(AccountSession session) {
|
||||
@@ -365,6 +396,8 @@ public class TimelineDefinition {
|
||||
LOCAL_TIMELINE,
|
||||
FEDERATED_TIMELINE,
|
||||
POSTS_TIMELINE,
|
||||
BUBBLE_TIMELINE
|
||||
BUBBLE_TIMELINE,
|
||||
BOOKMARKS_TIMELINE,
|
||||
FAVORITES_TIMELINE
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,30 @@
|
||||
package org.joinmastodon.android.model;
|
||||
|
||||
import org.joinmastodon.android.api.AllFieldsAreRequired;
|
||||
|
||||
@AllFieldsAreRequired
|
||||
import org.joinmastodon.android.api.RequiredField;
|
||||
|
||||
public class Translation extends BaseModel{
|
||||
@RequiredField
|
||||
public String content;
|
||||
@RequiredField
|
||||
public String detectedSourceLanguage;
|
||||
@RequiredField
|
||||
public String provider;
|
||||
public String spoilerText;
|
||||
public MediaAttachment[] mediaAttachments;
|
||||
public PollTranslation poll;
|
||||
|
||||
public static class MediaAttachment {
|
||||
public String id;
|
||||
public String description;
|
||||
}
|
||||
|
||||
public static class PollTranslation {
|
||||
public String id;
|
||||
public PollOption[] options;
|
||||
}
|
||||
|
||||
public static class PollOption {
|
||||
public String title;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,6 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class AudioStatusDisplayItem extends StatusDisplayItem{
|
||||
public final Status status;
|
||||
public final Attachment attachment;
|
||||
private final ImageLoaderRequest imageRequest;
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@ package org.joinmastodon.android.ui.displayitems;
|
||||
import android.app.Activity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import android.widget.CheckBox;
|
||||
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.fragments.report.ReportAddPostsChoiceFragment;
|
||||
import org.joinmastodon.android.model.Account;
|
||||
import org.joinmastodon.android.model.Notification;
|
||||
import org.joinmastodon.android.model.ScheduledStatus;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.views.CheckableRelativeLayout;
|
||||
|
||||
@@ -34,8 +34,16 @@ public class CheckableHeaderStatusDisplayItem extends HeaderStatusDisplayItem{
|
||||
public Holder(Activity activity, ViewGroup parent){
|
||||
super(activity, R.layout.display_item_header_checkable, parent);
|
||||
checkbox=findViewById(R.id.checkbox);
|
||||
view=(CheckableRelativeLayout) itemView;
|
||||
view=findViewById(R.id.checkbox_wrap);
|
||||
checkbox.setBackground(new CheckBox(activity).getButtonDrawable());
|
||||
view.setOnClickListener(this::onToggle);
|
||||
view.setAccessibilityDelegate(new View.AccessibilityDelegate(){
|
||||
@Override
|
||||
public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info){
|
||||
super.onInitializeAccessibilityNodeInfo(host, info);
|
||||
info.setClassName(CheckBox.class.getName());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -46,6 +54,12 @@ public class CheckableHeaderStatusDisplayItem extends HeaderStatusDisplayItem{
|
||||
}
|
||||
}
|
||||
|
||||
private void onToggle(View v){
|
||||
if(item.parentFragment instanceof ReportAddPostsChoiceFragment reportFragment){
|
||||
reportFragment.onToggleItem(item.parentID);
|
||||
}
|
||||
}
|
||||
|
||||
public void setIsChecked(Predicate<Holder> isChecked){
|
||||
this.isChecked=isChecked;
|
||||
}
|
||||
|
||||
@@ -58,7 +58,6 @@ import me.grishka.appkit.utils.V;
|
||||
import me.grishka.appkit.views.UsableRecyclerView;
|
||||
|
||||
public class EmojiReactionsStatusDisplayItem extends StatusDisplayItem {
|
||||
public final Status status;
|
||||
private final Drawable placeholder;
|
||||
private final boolean hideEmpty, forAnnouncement, playGifs;
|
||||
private final String accountID;
|
||||
|
||||
@@ -36,7 +36,6 @@ import androidx.annotation.PluralsRes;
|
||||
import me.grishka.appkit.Nav;
|
||||
|
||||
public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
public final Status status;
|
||||
public final String accountID;
|
||||
|
||||
private static final DateTimeFormatter TIME_FORMATTER=DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG, FormatStyle.SHORT);
|
||||
@@ -130,6 +129,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void startAccountListFragment(Class<? extends StatusRelatedAccountListFragment> cls){
|
||||
if(item.status.preview) return;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putParcelable("status", Parcels.wrap(item.status));
|
||||
@@ -137,6 +137,7 @@ public class ExtendedFooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void startEditHistoryFragment(){
|
||||
if(item.status.preview) return;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putString("id", item.status.id);
|
||||
|
||||
@@ -38,7 +38,6 @@ import me.grishka.appkit.utils.CubicBezierInterpolator;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
public final Status status;
|
||||
private final String accountID;
|
||||
public boolean hideCounts;
|
||||
|
||||
@@ -159,6 +158,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private boolean onButtonTouch(View v, MotionEvent event){
|
||||
if(item.status.preview) return false;
|
||||
boolean disabled = !v.isEnabled() || (v instanceof FrameLayout parentFrame &&
|
||||
parentFrame.getChildCount() > 0 && !parentFrame.getChildAt(0).isEnabled());
|
||||
int action = event.getAction();
|
||||
@@ -181,6 +181,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void onReplyClick(View v){
|
||||
if(item.status.preview) return;
|
||||
UiUtils.opacityIn(v);
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.accountID);
|
||||
@@ -189,6 +190,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private boolean onReplyLongClick(View v) {
|
||||
if(item.status.preview) return false;
|
||||
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
|
||||
UiUtils.pickAccount(v.getContext(), item.accountID, R.string.sk_reply_as, R.drawable.ic_fluent_arrow_reply_28_regular, session -> {
|
||||
Bundle args=new Bundle();
|
||||
@@ -204,6 +206,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void onBoostClick(View v){
|
||||
if(item.status.preview) return;
|
||||
if (GlobalUserPreferences.confirmBoost) {
|
||||
UiUtils.opacityIn(v);
|
||||
onBoostLongClick(v);
|
||||
@@ -219,6 +222,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private boolean onBoostLongClick(View v){
|
||||
if(item.status.preview) return false;
|
||||
Context ctx = itemView.getContext();
|
||||
View menu = LayoutInflater.from(ctx).inflate(R.layout.item_boost_menu, null);
|
||||
Dialog dialog = new M3AlertDialogBuilder(ctx).setView(menu).create();
|
||||
@@ -301,6 +305,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void onFavoriteClick(View v){
|
||||
if(item.status.preview) return;
|
||||
favorite.setSelected(!item.status.favourited);
|
||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setFavorited(item.status, !item.status.favourited, r->{
|
||||
UiUtils.opacityIn(v);
|
||||
@@ -309,6 +314,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private boolean onFavoriteLongClick(View v) {
|
||||
if(item.status.preview) return false;
|
||||
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
|
||||
UiUtils.pickInteractAs(v.getContext(),
|
||||
item.accountID, item.status,
|
||||
@@ -323,6 +329,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void onBookmarkClick(View v){
|
||||
if(item.status.preview) return;
|
||||
bookmark.setSelected(!item.status.bookmarked);
|
||||
AccountSessionManager.getInstance().getAccount(item.accountID).getStatusInteractionController().setBookmarked(item.status, !item.status.bookmarked, r->{
|
||||
UiUtils.opacityIn(v);
|
||||
@@ -330,6 +337,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private boolean onBookmarkLongClick(View v) {
|
||||
if(item.status.preview) return false;
|
||||
if (AccountSessionManager.getInstance().getLoggedInAccounts().size() < 2) return false;
|
||||
UiUtils.pickInteractAs(v.getContext(),
|
||||
item.accountID, item.status,
|
||||
@@ -344,6 +352,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void onShareClick(View v){
|
||||
if(item.status.preview) return;
|
||||
UiUtils.opacityIn(v);
|
||||
Intent intent=new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("text/plain");
|
||||
@@ -352,6 +361,7 @@ public class FooterStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private boolean onShareLongClick(View v){
|
||||
if(item.status.preview) return false;
|
||||
UiUtils.copyText(v, item.status.url);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import me.grishka.appkit.utils.V;
|
||||
|
||||
public class GapStatusDisplayItem extends StatusDisplayItem{
|
||||
public boolean loading;
|
||||
private final Status status;
|
||||
|
||||
public GapStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, Status status){
|
||||
super(parentID, parentFragment);
|
||||
|
||||
@@ -76,7 +76,6 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
private String accountID;
|
||||
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
||||
private SpannableStringBuilder parsedName;
|
||||
public final Status status;
|
||||
public boolean hasVisibilityToggle;
|
||||
boolean needBottomPadding;
|
||||
private CharSequence extraText;
|
||||
@@ -446,6 +445,7 @@ public class HeaderStatusDisplayItem extends StatusDisplayItem{
|
||||
}
|
||||
|
||||
private void onMoreClick(View v){
|
||||
if(item.status.preview) return;
|
||||
updateOptionsMenu();
|
||||
optionsMenu.show();
|
||||
if(relationship==null && currentRelationshipRequest==null){
|
||||
|
||||
@@ -24,7 +24,6 @@ import me.grishka.appkit.imageloader.requests.UrlImageLoaderRequest;
|
||||
import me.grishka.appkit.utils.V;
|
||||
|
||||
public class LinkCardStatusDisplayItem extends StatusDisplayItem{
|
||||
private final Status status;
|
||||
private final UrlImageLoaderRequest imgRequest;
|
||||
|
||||
public LinkCardStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, Status status){
|
||||
|
||||
@@ -12,6 +12,7 @@ import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -26,6 +27,7 @@ import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Attachment;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.model.Translation;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.PhotoLayoutHelper;
|
||||
import org.joinmastodon.android.ui.drawables.SpoilerStripesDrawable;
|
||||
@@ -38,7 +40,12 @@ import org.joinmastodon.android.ui.views.MediaGridLayout;
|
||||
import org.joinmastodon.android.utils.TypedObjectPool;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
|
||||
import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
@@ -52,8 +59,8 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
private PhotoLayoutHelper.TiledLayoutResult tiledLayout;
|
||||
private final TypedObjectPool<GridItemType, MediaAttachmentViewController> viewPool;
|
||||
private final List<Attachment> attachments;
|
||||
private final Map<String, Pair<String, String>> translatedAttachments = new HashMap<>();
|
||||
private final ArrayList<ImageLoaderRequest> requests=new ArrayList<>();
|
||||
public final Status status;
|
||||
public String sensitiveTitle;
|
||||
|
||||
public MediaGridStatusDisplayItem(String parentID, BaseStatusListFragment<?> parentFragment, PhotoLayoutHelper.TiledLayoutResult tiledLayout, List<Attachment> attachments, Status status){
|
||||
@@ -189,6 +196,25 @@ public class MediaGridStatusDisplayItem extends StatusDisplayItem{
|
||||
c.btnsWrap.setAlpha(1f);
|
||||
}
|
||||
controllers.add(c);
|
||||
|
||||
if (item.status.translation != null){
|
||||
if(item.status.translationState==Status.TranslationState.SHOWN){
|
||||
if(!item.translatedAttachments.containsKey(att.id)){
|
||||
Optional<Translation.MediaAttachment> translatedAttachment=Arrays.stream(item.status.translation.mediaAttachments).filter(mediaAttachment->mediaAttachment.id.equals(att.id)).findFirst();
|
||||
translatedAttachment.ifPresent(mediaAttachment->{
|
||||
item.translatedAttachments.put(mediaAttachment.id, new Pair<>(att.description, mediaAttachment.description));
|
||||
att.description=mediaAttachment.description;
|
||||
});
|
||||
}else{
|
||||
//SAFETY: must be non-null, as we check if the map contains the attachment before
|
||||
att.description=Objects.requireNonNull(item.translatedAttachments.get(att.id)).second;
|
||||
}
|
||||
}else{
|
||||
if (item.translatedAttachments.containsKey(att.id)) {
|
||||
att.description=Objects.requireNonNull(item.translatedAttachments.get(att.id)).first;
|
||||
}
|
||||
}
|
||||
}
|
||||
c.bind(att, item.status);
|
||||
i++;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import android.widget.TextView;
|
||||
import org.joinmastodon.android.R;
|
||||
import org.joinmastodon.android.fragments.BaseStatusListFragment;
|
||||
import org.joinmastodon.android.model.Poll;
|
||||
import org.joinmastodon.android.model.Status;
|
||||
import org.joinmastodon.android.ui.OutlineProviders;
|
||||
import org.joinmastodon.android.ui.text.HtmlParser;
|
||||
import org.joinmastodon.android.ui.utils.CustomEmojiHelper;
|
||||
@@ -23,6 +24,7 @@ import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
|
||||
public class PollOptionStatusDisplayItem extends StatusDisplayItem{
|
||||
private CharSequence text;
|
||||
private CharSequence translatedText;
|
||||
public final Poll.Option option;
|
||||
private CustomEmojiHelper emojiHelper=new CustomEmojiHelper();
|
||||
private boolean showResults;
|
||||
@@ -30,12 +32,15 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
|
||||
private boolean isMostVoted;
|
||||
private final int optionIndex;
|
||||
public final Poll poll;
|
||||
public final Status status;
|
||||
|
||||
public PollOptionStatusDisplayItem(String parentID, Poll poll, int optionIndex, BaseStatusListFragment parentFragment){
|
||||
|
||||
public PollOptionStatusDisplayItem(String parentID, Poll poll, int optionIndex, BaseStatusListFragment parentFragment, Status status){
|
||||
super(parentID, parentFragment);
|
||||
this.optionIndex=optionIndex;
|
||||
option=poll.options.get(optionIndex);
|
||||
this.poll=poll;
|
||||
this.status=status;
|
||||
text=HtmlParser.parseCustomEmoji(option.title, poll.emojis);
|
||||
emojiHelper.setText(text);
|
||||
showResults=poll.isExpired() || poll.voted;
|
||||
@@ -84,7 +89,14 @@ public class PollOptionStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
@Override
|
||||
public void onBind(PollOptionStatusDisplayItem item){
|
||||
text.setText(item.text);
|
||||
if (item.status.translation != null && item.status.translationState == Status.TranslationState.SHOWN) {
|
||||
if(item.translatedText==null){
|
||||
item.translatedText=item.status.translation.poll.options[item.optionIndex].title;
|
||||
}
|
||||
text.setText(item.translatedText);
|
||||
} else {
|
||||
text.setText(item.text);
|
||||
}
|
||||
percent.setVisibility(item.showResults ? View.VISIBLE : View.GONE);
|
||||
itemView.setClickable(!item.showResults);
|
||||
icon.setImageDrawable(itemView.getContext().getDrawable(item.poll.multiple ?
|
||||
|
||||
@@ -43,7 +43,6 @@ public class ReblogOrReplyLineStatusDisplayItem extends StatusDisplayItem{
|
||||
public boolean needBottomPadding;
|
||||
ReblogOrReplyLineStatusDisplayItem extra;
|
||||
CharSequence fullText;
|
||||
Status status;
|
||||
|
||||
public ReblogOrReplyLineStatusDisplayItem(String parentID, BaseStatusListFragment parentFragment, CharSequence text, List<Emoji> emojis, @DrawableRes int icon, StatusPrivacy visibility, @Nullable View.OnClickListener handleClick, Status status) {
|
||||
this(parentID, parentFragment, text, emojis, icon, visibility, handleClick, text, status);
|
||||
|
||||
@@ -24,9 +24,9 @@ import me.grishka.appkit.imageloader.ImageLoaderViewHolder;
|
||||
import me.grishka.appkit.imageloader.requests.ImageLoaderRequest;
|
||||
|
||||
public class SpoilerStatusDisplayItem extends StatusDisplayItem{
|
||||
public final Status status;
|
||||
public final ArrayList<StatusDisplayItem> contentItems=new ArrayList<>();
|
||||
private final CharSequence parsedTitle;
|
||||
private CharSequence translatedTitle;
|
||||
private final CustomEmojiHelper emojiHelper;
|
||||
private final Type type;
|
||||
private final int attachmentCount;
|
||||
@@ -90,7 +90,14 @@ public class SpoilerStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
@Override
|
||||
public void onBind(SpoilerStatusDisplayItem item){
|
||||
title.setText(item.parsedTitle);
|
||||
if(item.status.translationState==Status.TranslationState.SHOWN){
|
||||
if(item.translatedTitle==null){
|
||||
item.translatedTitle=item.status.translation.spoilerText;
|
||||
}
|
||||
title.setText(item.translatedTitle);
|
||||
}else{
|
||||
title.setText(item.parsedTitle);
|
||||
}
|
||||
action.setText(item.status.spoilerRevealed ? R.string.spoiler_hide : R.string.sk_spoiler_show);
|
||||
itemView.setPadding(
|
||||
itemView.getPaddingLeft(),
|
||||
|
||||
@@ -58,13 +58,15 @@ import me.grishka.appkit.views.UsableRecyclerView;
|
||||
public abstract class StatusDisplayItem{
|
||||
public final String parentID;
|
||||
public final BaseStatusListFragment<?> parentFragment;
|
||||
public Status status;
|
||||
public boolean inset;
|
||||
public int index;
|
||||
public boolean
|
||||
hasDescendantNeighbor=false,
|
||||
hasAncestoringNeighbor=false,
|
||||
isMainStatus=true,
|
||||
isDirectDescendant=false;
|
||||
isDirectDescendant=false,
|
||||
isForQuote=false;
|
||||
|
||||
public static final int FLAG_INSET=1;
|
||||
public static final int FLAG_NO_FOOTER=1 << 1;
|
||||
@@ -73,6 +75,7 @@ public abstract class StatusDisplayItem{
|
||||
public static final int FLAG_NO_HEADER=1 << 4;
|
||||
public static final int FLAG_NO_TRANSLATE=1 << 5;
|
||||
public static final int FLAG_NO_EMOJI_REACTIONS=1 << 6;
|
||||
public static final int FLAG_IS_FOR_QUOTE=1 << 7;
|
||||
|
||||
public void setAncestryInfo(
|
||||
boolean hasDescendantNeighbor,
|
||||
@@ -232,27 +235,28 @@ public abstract class StatusDisplayItem{
|
||||
if(statusForContent.hasSpoiler()){
|
||||
if (AccountSessionManager.get(accountID).getLocalPreferences().revealCWs) statusForContent.spoilerRevealed = true;
|
||||
SpoilerStatusDisplayItem spoilerItem=new SpoilerStatusDisplayItem(parentID, fragment, null, statusForContent, Type.SPOILER);
|
||||
if((flags & FLAG_IS_FOR_QUOTE)!=0){
|
||||
for(StatusDisplayItem item:spoilerItem.contentItems){
|
||||
item.isForQuote=true;
|
||||
}
|
||||
}
|
||||
items.add(spoilerItem);
|
||||
contentItems=spoilerItem.contentItems;
|
||||
}else{
|
||||
contentItems=items;
|
||||
}
|
||||
|
||||
if (statusForContent.quote != null) {
|
||||
boolean hasQuoteInlineTag = statusForContent.content.contains("<span class=\"quote-inline\">");
|
||||
if (!hasQuoteInlineTag) {
|
||||
String quoteUrl = statusForContent.quote.url;
|
||||
String quoteInline = String.format("<span class=\"quote-inline\">%sRE: <a href=\"%s\">%s</a></span>",
|
||||
statusForContent.content.endsWith("</p>") ? "" : "<br/><br/>", quoteUrl, quoteUrl);
|
||||
statusForContent.content += quoteInline;
|
||||
}
|
||||
if(statusForContent.quote!=null) {
|
||||
int quoteInlineIndex=statusForContent.content.lastIndexOf("<span class=\"quote-inline\"><br/><br/>RE:");
|
||||
if (quoteInlineIndex!=-1)
|
||||
statusForContent.content=statusForContent.content.substring(0, quoteInlineIndex);
|
||||
}
|
||||
|
||||
boolean hasSpoiler=!TextUtils.isEmpty(statusForContent.spoilerText);
|
||||
if(!TextUtils.isEmpty(statusForContent.content)){
|
||||
SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID);
|
||||
SpannableStringBuilder parsedText=HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, fragment.getContext());
|
||||
HtmlParser.applyFilterHighlights(fragment.getActivity(), parsedText, status.filtered);
|
||||
TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID), fragment, statusForContent, (flags & FLAG_NO_TRANSLATE) != 0);
|
||||
TextStatusDisplayItem text=new TextStatusDisplayItem(parentID, HtmlParser.parse(statusForContent.content, statusForContent.emojis, statusForContent.mentions, statusForContent.tags, accountID, fragment.getContext()), fragment, statusForContent, (flags & FLAG_NO_TRANSLATE) != 0);
|
||||
contentItems.add(text);
|
||||
}else if(!hasSpoiler && header!=null){
|
||||
header.needBottomPadding=true;
|
||||
@@ -270,9 +274,11 @@ public abstract class StatusDisplayItem{
|
||||
}
|
||||
PhotoLayoutHelper.TiledLayoutResult layout=PhotoLayoutHelper.processThumbs(imageAttachments);
|
||||
MediaGridStatusDisplayItem mediaGrid=new MediaGridStatusDisplayItem(parentID, fragment, layout, imageAttachments, statusForContent);
|
||||
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0)
|
||||
if((flags & FLAG_MEDIA_FORCE_HIDDEN)!=0){
|
||||
mediaGrid.sensitiveTitle=fragment.getString(R.string.media_hidden);
|
||||
else if(statusForContent.sensitive && AccountSessionManager.get(accountID).getLocalPreferences().revealCWs && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia)
|
||||
statusForContent.sensitiveRevealed=false;
|
||||
statusForContent.sensitive=true;
|
||||
} else if(statusForContent.sensitive && AccountSessionManager.get(accountID).getLocalPreferences().revealCWs && !AccountSessionManager.get(accountID).getLocalPreferences().hideSensitiveMedia)
|
||||
statusForContent.sensitiveRevealed=true;
|
||||
contentItems.add(mediaGrid);
|
||||
}
|
||||
@@ -285,17 +291,23 @@ public abstract class StatusDisplayItem{
|
||||
}
|
||||
}
|
||||
if(statusForContent.poll!=null){
|
||||
buildPollItems(parentID, fragment, statusForContent.poll, contentItems);
|
||||
buildPollItems(parentID, fragment, statusForContent.poll, status, contentItems);
|
||||
}
|
||||
if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty()){
|
||||
if(statusForContent.card!=null && statusForContent.mediaAttachments.isEmpty() && statusForContent.quote==null){
|
||||
contentItems.add(new LinkCardStatusDisplayItem(parentID, fragment, statusForContent));
|
||||
}
|
||||
if(statusForContent.quote!=null && !(parentObject instanceof Notification)){
|
||||
if(!statusForContent.mediaAttachments.isEmpty() && statusForContent.poll==null) // add spacing if immediately preceded by attachment
|
||||
contentItems.add(new DummyStatusDisplayItem(parentID, fragment));
|
||||
contentItems.addAll(buildItems(fragment, statusForContent.quote, accountID, parentObject, knownAccounts, filterContext, FLAG_NO_FOOTER | FLAG_INSET | FLAG_NO_EMOJI_REACTIONS | FLAG_IS_FOR_QUOTE));
|
||||
}
|
||||
if(contentItems!=items && statusForContent.spoilerRevealed){
|
||||
items.addAll(contentItems);
|
||||
}
|
||||
AccountLocalPreferences lp=fragment.getLocalPrefs();
|
||||
if((flags & FLAG_NO_EMOJI_REACTIONS)==0 && lp.emojiReactionsEnabled &&
|
||||
(lp.showEmojiReactions!=ONLY_OPENED || fragment instanceof ThreadFragment)){
|
||||
if((flags & FLAG_NO_EMOJI_REACTIONS)==0 && !status.preview && lp.emojiReactionsEnabled &&
|
||||
(lp.showEmojiReactions!=ONLY_OPENED || fragment instanceof ThreadFragment) &&
|
||||
statusForContent.reactions!=null){
|
||||
boolean isMainStatus=fragment instanceof ThreadFragment t && t.getMainStatus().id.equals(statusForContent.id);
|
||||
boolean showAddButton=lp.showEmojiReactions==ALWAYS || isMainStatus;
|
||||
items.add(new EmojiReactionsStatusDisplayItem(parentID, fragment, statusForContent, accountID, !showAddButton, false));
|
||||
@@ -307,8 +319,9 @@ public abstract class StatusDisplayItem{
|
||||
items.add(footer);
|
||||
}
|
||||
boolean inset=(flags & FLAG_INSET)!=0;
|
||||
boolean isForQuote=(flags & FLAG_IS_FOR_QUOTE)!=0;
|
||||
// add inset dummy so last content item doesn't clip out of inset bounds
|
||||
if((inset || footer==null) && (flags & FLAG_CHECKABLE)==0){
|
||||
if((inset || footer==null) && (flags & FLAG_CHECKABLE)==0 && !isForQuote){
|
||||
items.add(new DummyStatusDisplayItem(parentID, fragment));
|
||||
// in case we ever need the dummy to display a margin for the media grid again:
|
||||
// (i forgot why we apparently don't need this anymore)
|
||||
@@ -320,12 +333,22 @@ public abstract class StatusDisplayItem{
|
||||
items.add(gap=new GapStatusDisplayItem(parentID, fragment, status));
|
||||
int i=1;
|
||||
for(StatusDisplayItem item:items){
|
||||
item.inset=inset;
|
||||
if(inset)
|
||||
item.inset=true;
|
||||
if(isForQuote){
|
||||
item.status=statusForContent;
|
||||
item.isForQuote=true;
|
||||
}
|
||||
item.index=i++;
|
||||
}
|
||||
if(items!=contentItems && !statusForContent.spoilerRevealed){
|
||||
for(StatusDisplayItem item:contentItems){
|
||||
item.inset=inset;
|
||||
if(inset)
|
||||
item.inset=true;
|
||||
if(isForQuote){
|
||||
item.status=statusForContent;
|
||||
item.isForQuote=true;
|
||||
}
|
||||
item.index=i++;
|
||||
}
|
||||
}
|
||||
@@ -339,10 +362,10 @@ public abstract class StatusDisplayItem{
|
||||
);
|
||||
}
|
||||
|
||||
public static void buildPollItems(String parentID, BaseStatusListFragment fragment, Poll poll, List<StatusDisplayItem> items){
|
||||
public static void buildPollItems(String parentID, BaseStatusListFragment fragment, Poll poll, Status status, List<StatusDisplayItem> items){
|
||||
int i=0;
|
||||
for(Poll.Option opt:poll.options){
|
||||
items.add(new PollOptionStatusDisplayItem(parentID, poll, i, fragment));
|
||||
items.add(new PollOptionStatusDisplayItem(parentID, poll, i, fragment, status));
|
||||
i++;
|
||||
}
|
||||
items.add(new PollFooterStatusDisplayItem(parentID, fragment, poll));
|
||||
@@ -375,12 +398,15 @@ public abstract class StatusDisplayItem{
|
||||
}
|
||||
|
||||
public static abstract class Holder<T extends StatusDisplayItem> extends BindableViewHolder<T> implements UsableRecyclerView.DisableableClickable{
|
||||
private Context context;
|
||||
|
||||
public Holder(View itemView){
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
public Holder(Context context, int layout, ViewGroup parent){
|
||||
super(context, layout, parent);
|
||||
this.context=context;
|
||||
}
|
||||
|
||||
public String getItemID(){
|
||||
@@ -389,6 +415,16 @@ public abstract class StatusDisplayItem{
|
||||
|
||||
@Override
|
||||
public void onClick(){
|
||||
if(item.isForQuote){
|
||||
item.status.filterRevealed=true;
|
||||
Bundle args=new Bundle();
|
||||
args.putString("account", item.parentFragment.getAccountID());
|
||||
args.putParcelable("status", Parcels.wrap(item.status.clone()));
|
||||
args.putBoolean("refresh", true);
|
||||
Nav.go((Activity) context, ThreadFragment.class, args);
|
||||
return;
|
||||
}
|
||||
|
||||
item.parentFragment.onItemClick(item.parentID);
|
||||
}
|
||||
|
||||
@@ -420,13 +456,13 @@ public abstract class StatusDisplayItem{
|
||||
|
||||
public boolean isLastDisplayItemForStatus(){
|
||||
return getNextVisibleDisplayItem()
|
||||
.map(n->!n.parentID.equals(item.parentID))
|
||||
.map(next->!next.parentID.equals(item.parentID) || item.inset && !next.inset)
|
||||
.orElse(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled(){
|
||||
return item.parentFragment.isItemEnabled(item.parentID);
|
||||
return item.parentFragment.isItemEnabled(item.parentID) || item.isForQuote;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,6 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
public boolean textSelectable;
|
||||
public boolean reduceTopPadding;
|
||||
public boolean disableTranslate;
|
||||
public final Status status;
|
||||
|
||||
public TextStatusDisplayItem(String parentID, CharSequence text, BaseStatusListFragment parentFragment, Status status, boolean disableTranslate){
|
||||
super(parentID, parentFragment);
|
||||
@@ -113,7 +112,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
text.setText(item.text);
|
||||
}
|
||||
text.setTextIsSelectable(false);
|
||||
if(item.textSelectable) itemView.post(() -> text.setTextIsSelectable(true));
|
||||
if(item.textSelectable && !item.isForQuote) itemView.post(() -> text.setTextIsSelectable(true));
|
||||
text.setInvalidateOnEveryFrame(false);
|
||||
itemView.setClickable(false);
|
||||
itemView.setPadding(itemView.getPaddingLeft(), item.reduceTopPadding ? V.dp(6) : V.dp(12), itemView.getPaddingRight(), itemView.getPaddingBottom());
|
||||
@@ -124,8 +123,8 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
|
||||
StatusDisplayItem next=getNextVisibleDisplayItem().orElse(null);
|
||||
if(next!=null && !next.parentID.equals(item.parentID)) next=null;
|
||||
int bottomPadding=next instanceof FooterStatusDisplayItem ? V.dp(6)
|
||||
: item.inset ? V.dp(12)
|
||||
int bottomPadding=item.inset ? V.dp(12)
|
||||
: next instanceof FooterStatusDisplayItem ? V.dp(6)
|
||||
: (next instanceof EmojiReactionsStatusDisplayItem || next==null) ? 0
|
||||
: V.dp(12);
|
||||
itemView.setPadding(itemView.getPaddingLeft(), itemView.getPaddingTop(), itemView.getPaddingRight(), bottomPadding);
|
||||
@@ -192,7 +191,7 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
public void updateTranslation(boolean updateText){
|
||||
if(item.status==null)
|
||||
return;
|
||||
boolean translateEnabled=!item.disableTranslate && item.status.isEligibleForTranslation(item.parentFragment.getSession());
|
||||
boolean translateEnabled=!item.disableTranslate && item.status.isEligibleForTranslation(item.parentFragment.getSession()) && !item.isForQuote;
|
||||
if(translationFooter==null && translateEnabled){
|
||||
translationFooter=translationFooterStub.inflate();
|
||||
translationInfo=findViewById(R.id.translation_info_text);
|
||||
@@ -211,8 +210,9 @@ public class TextStatusDisplayItem extends StatusDisplayItem{
|
||||
String existingTransLang=existingTrans!=null ? existingTrans.detectedSourceLanguage : null;
|
||||
String lang=existingTransLang!=null ? existingTransLang : item.status.getContentStatus().language;
|
||||
Locale locale=lang!=null ? Locale.forLanguageTag(lang) : null;
|
||||
translationButton.setText(locale!=null
|
||||
? item.parentFragment.getString(R.string.translate_post, locale.getDisplayLanguage())
|
||||
String displayLang=locale==null || locale.getDisplayLanguage().isBlank() ? lang : locale.getDisplayLanguage();
|
||||
translationButton.setText(displayLang!=null
|
||||
? item.parentFragment.getString(R.string.translate_post, displayLang)
|
||||
: item.parentFragment.getString(R.string.sk_translate_post));
|
||||
translationButton.setClickable(true);
|
||||
translationButton.animate().alpha(1).setDuration(100).start();
|
||||
|
||||
@@ -14,7 +14,6 @@ import java.util.List;
|
||||
|
||||
public class WarningFilteredStatusDisplayItem extends StatusDisplayItem{
|
||||
public boolean loading;
|
||||
public final Status status;
|
||||
public List<StatusDisplayItem> filteredItems;
|
||||
public LegacyFilter applyingFilter;
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package org.joinmastodon.android.ui.text;
|
||||
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.CharacterStyle;
|
||||
|
||||
public class DiffRemovedSpan extends CharacterStyle {
|
||||
|
||||
private final String text;
|
||||
private final int color;
|
||||
|
||||
public DiffRemovedSpan(String text, int color){
|
||||
this.text=text;
|
||||
this.color=color;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void updateDrawState(TextPaint tp) {
|
||||
tp.setStrikeThruText(true);
|
||||
tp.setColor(color);
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.joinmastodon.android.ui.text;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Typeface;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
@@ -69,6 +70,10 @@ public class HtmlParser{
|
||||
|
||||
private HtmlParser(){}
|
||||
|
||||
public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID){
|
||||
return parse(source, emojis, mentions, tags, accountID, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse HTML and custom emoji into a spanned string for display.
|
||||
* Supported tags: <ul>
|
||||
@@ -81,7 +86,7 @@ public class HtmlParser{
|
||||
* @param emojis Custom emojis that are present in source as <code>:code:</code>
|
||||
* @return a spanned string
|
||||
*/
|
||||
public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID){
|
||||
public static SpannableStringBuilder parse(String source, List<Emoji> emojis, List<Mention> mentions, List<Hashtag> tags, String accountID, Context context){
|
||||
class SpanInfo{
|
||||
public Object span;
|
||||
public int start;
|
||||
@@ -106,6 +111,9 @@ public class HtmlParser{
|
||||
Map<String, Hashtag> tagsByTag=tags.stream().distinct().collect(Collectors.toMap(t->t.name.toLowerCase(), Function.identity()));
|
||||
|
||||
final SpannableStringBuilder ssb=new SpannableStringBuilder();
|
||||
int colorInsert=UiUtils.getThemeColor(context, R.attr.colorM3Success);
|
||||
int colorDelete=UiUtils.getThemeColor(context, R.attr.colorM3Error);
|
||||
|
||||
Jsoup.parseBodyFragment(source).body().traverse(new NodeVisitor(){
|
||||
private final ArrayList<SpanInfo> openSpans=new ArrayList<>();
|
||||
|
||||
@@ -171,6 +179,9 @@ public class HtmlParser{
|
||||
}
|
||||
case "code", "pre" -> openSpans.add(new SpanInfo(new TypefaceSpan("monospace"), ssb.length(), el));
|
||||
case "blockquote" -> openSpans.add(new SpanInfo(new LeadingMarginSpan.Standard(V.dp(10)), ssb.length(), el));
|
||||
// fake elements for the edit history diff view
|
||||
case "edit-diff-insert" -> openSpans.add(new SpanInfo(new ForegroundColorSpan(colorInsert), ssb.length(), el));
|
||||
case "edit-diff-delete" -> openSpans.add(new SpanInfo(new DiffRemovedSpan(el.text(), colorDelete), ssb.length(), el));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1683,12 +1683,14 @@ public class UiUtils {
|
||||
"pronouns.page/"
|
||||
};
|
||||
|
||||
private static final Pattern trimPronouns=Pattern.compile("[^\\w*]*([\\w*].*[\\w*]|[\\w*])\\W*");
|
||||
private static final String PRONOUN_CHARS="\\w*¿¡!?";
|
||||
private static final Pattern trimPronouns=
|
||||
Pattern.compile("[^"+PRONOUN_CHARS+"]*(["+PRONOUN_CHARS+"].*["+PRONOUN_CHARS+"]|["+PRONOUN_CHARS+"])\\W*");
|
||||
private static String extractPronounsFromField(String localizedPronouns, AccountField field) {
|
||||
if(!field.name.toLowerCase().contains(localizedPronouns) &&
|
||||
!field.name.toLowerCase().contains("pronouns")) return null;
|
||||
String text=HtmlParser.text(field.value);
|
||||
if(field.value.toLowerCase().contains("https://")){
|
||||
if(text.toLowerCase().contains("https://")){
|
||||
for(String pronounUrl : pronounsUrls){
|
||||
int index=text.indexOf(pronounUrl);
|
||||
int beginPronouns=index+pronounUrl.length();
|
||||
@@ -1707,13 +1709,20 @@ public class UiUtils {
|
||||
Matcher matcher=trimPronouns.matcher(text);
|
||||
if(!matcher.find()) return null;
|
||||
String pronouns=matcher.group(1);
|
||||
// crude fix to allow for pronouns like "it(/she)"
|
||||
int missingClosingParens=0;
|
||||
|
||||
// crude fix to allow for pronouns like "it(/she)" or "(de) sie/ihr"
|
||||
int missingParens=0, missingBrackets=0;
|
||||
for(char c : pronouns.toCharArray()){
|
||||
if(c=='(') missingClosingParens++;
|
||||
if(c==')') missingClosingParens--;
|
||||
if(c=='(') missingParens++;
|
||||
else if(c=='[') missingBrackets++;
|
||||
else if(c==')') missingParens--;
|
||||
else if(c==']') missingBrackets--;
|
||||
}
|
||||
pronouns+=")".repeat(Math.max(0, missingClosingParens));
|
||||
if(missingParens > 0) pronouns+=")".repeat(missingParens);
|
||||
else if(missingParens < 0) pronouns="(".repeat(missingParens*-1)+pronouns;
|
||||
if(missingBrackets > 0) pronouns+="]".repeat(missingBrackets);
|
||||
else if(missingBrackets < 0) pronouns="[".repeat(missingBrackets*-1)+pronouns;
|
||||
|
||||
// if ends with an un-closed custom emoji
|
||||
if(pronouns.matches("^.*\\s+:[a-zA-Z_]+$")) pronouns+=':';
|
||||
return pronouns;
|
||||
|
||||
@@ -42,6 +42,7 @@ public class ComposePollViewController{
|
||||
30*60,
|
||||
3600,
|
||||
6*3600,
|
||||
12*3600,
|
||||
24*3600,
|
||||
3*24*3600,
|
||||
7*24*3600,
|
||||
|
||||
@@ -217,6 +217,7 @@ public class AccountViewHolder extends BindableViewHolder<AccountViewModel> impl
|
||||
Menu menu=contextMenu.getMenu();
|
||||
Account account=item.account;
|
||||
|
||||
menu.findItem(R.id.edit_note).setVisible(false);
|
||||
menu.findItem(R.id.manage_user_lists).setTitle(fragment.getString(R.string.sk_lists_with_user, account.getShortUsername()));
|
||||
MenuItem mute=menu.findItem(R.id.mute);
|
||||
mute.setTitle(fragment.getString(relationship.muting ? R.string.unmute_user : R.string.mute_user, account.getShortUsername()));
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package org.joinmastodon.android.utils;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.joinmastodon.android.fragments.ComposeFragment;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.MatchResult;
|
||||
import java.util.regex.Matcher;
|
||||
@@ -40,19 +40,22 @@ public class StatusTextEncoder {
|
||||
}
|
||||
|
||||
// prettiest almost-exact replica of a pretty function
|
||||
public String decode(String content, Pattern regex) {
|
||||
Matcher m = regex.matcher(content);
|
||||
StringBuilder decodedString = new StringBuilder();
|
||||
int previousEnd = 0;
|
||||
public Pair<String, List<String>> decode(String content, Pattern regex) {
|
||||
Matcher m=regex.matcher(content);
|
||||
StringBuilder decodedString=new StringBuilder();
|
||||
List<String> decodedParts=new ArrayList<>();
|
||||
int previousEnd=0;
|
||||
while (m.find()) {
|
||||
MatchResult res = m.toMatchResult();
|
||||
MatchResult res=m.toMatchResult();
|
||||
// everything before the match - do not decode
|
||||
decodedString.append(content.substring(previousEnd, res.start()));
|
||||
previousEnd = res.end();
|
||||
previousEnd=res.end();
|
||||
// the match - do decode
|
||||
decodedString.append(fn.apply(res.group()));
|
||||
String decoded=fn.apply(res.group());
|
||||
decodedParts.add(decoded);
|
||||
decodedString.append(decoded);
|
||||
}
|
||||
decodedString.append(content.substring(previousEnd));
|
||||
return decodedString.toString();
|
||||
return Pair.create(decodedString.toString(), decodedParts);
|
||||
}
|
||||
}
|
||||
|
||||
7
mastodon/src/main/res/drawable/bg_note_edit.xml
Normal file
7
mastodon/src/main/res/drawable/bg_note_edit.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:tint="@color/m3_primary_alpha11"
|
||||
android:tintMode="src_over">
|
||||
<solid android:color="?colorM3Surface" />
|
||||
<corners android:radius="26dp" />
|
||||
</shape>
|
||||
@@ -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="M12 5.5c-2.413 0-4.383 1.9-4.495 4.285-0.019 0.4-0.349 0.715-0.75 0.715H6.5c-1.657 0-3 1.343-3 3s1.343 3 3 3h11c1.657 0 3-1.343 3-3s-1.343-3-3-3h-0.256c-0.4 0-0.73-0.315-0.749-0.715C16.383 7.4 14.413 5.5 12 5.5zM6.08 9.02C6.548 6.171 9.02 4 12 4s5.452 2.172 5.92 5.02C20.208 9.23 22 11.155 22 13.5c0 2.485-2.015 4.5-4.5 4.5h-11C4.015 18 2 15.985 2 13.5c0-2.344 1.792-4.269 4.08-4.48z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -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="M11 15c0-0.35 0.06-0.687 0.171-1H4.253c-1.242 0-2.25 1.007-2.25 2.25v0.577c0 0.892 0.32 1.756 0.9 2.435 1.565 1.834 3.951 2.74 7.097 2.74 0.398 0 0.783-0.015 1.157-0.044C11.055 21.658 11 21.335 11 21v-0.534c-0.322 0.023-0.655 0.035-1 0.035-2.739 0-4.705-0.745-5.958-2.213-0.348-0.407-0.54-0.926-0.54-1.461v-0.578c0-0.413 0.336-0.749 0.75-0.749H11V15zM10 2.005c2.762 0 5 2.239 5 5s-2.238 5-5 5c-2.761 0-5-2.239-5-5s2.239-5 5-5zm0 1.5c-1.933 0-3.5 1.567-3.5 3.5s1.567 3.5 3.5 3.5 3.5-1.567 3.5-3.5-1.567-3.5-3.5-3.5zM12 15c0-1.104 0.896-2 2-2h7c1.105 0 2 0.896 2 2v6c0 1.105-0.895 2-2 2h-7c-1.104 0-2-0.895-2-2v-6zm2.5 1c-0.276 0-0.5 0.224-0.5 0.5s0.224 0.5 0.5 0.5h6c0.277 0 0.5-0.224 0.5-0.5S20.777 16 20.5 16h-6zm0 3c-0.276 0-0.5 0.224-0.5 0.5s0.224 0.5 0.5 0.5h6c0.277 0 0.5-0.224 0.5-0.5S20.777 19 20.5 19h-6z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -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="M16.088 6.412c-0.072-0.093-0.15-0.182-0.234-0.266-0.312-0.313-0.693-0.55-1.113-0.69L13.363 5.01c-0.106-0.037-0.198-0.107-0.263-0.198C13.035 4.719 13 4.609 13 4.497c0-0.113 0.035-0.223 0.1-0.315 0.065-0.091 0.157-0.16 0.263-0.198l1.378-0.448c0.414-0.143 0.789-0.379 1.096-0.69 0.299-0.304 0.525-0.67 0.663-1.072l0.011-0.034 0.448-1.377c0.038-0.106 0.107-0.198 0.2-0.263 0.091-0.065 0.2-0.1 0.313-0.1 0.113 0 0.223 0.035 0.315 0.1s0.161 0.157 0.199 0.263l0.447 1.377c0.14 0.418 0.375 0.798 0.687 1.11 0.312 0.312 0.693 0.547 1.111 0.686l1.378 0.448 0.028 0.007c0.106 0.037 0.198 0.107 0.263 0.198 0.065 0.092 0.1 0.202 0.1 0.314 0 0.113-0.035 0.223-0.1 0.315-0.065 0.091-0.157 0.16-0.263 0.198l-1.378 0.448c-0.419 0.139-0.8 0.374-1.112 0.686-0.312 0.311-0.547 0.692-0.686 1.11l-0.448 1.377L18 8.671c-0.04 0.092-0.104 0.171-0.186 0.23C17.722 8.964 17.613 9 17.5 9c-0.113 0-0.222-0.035-0.314-0.1s-0.162-0.157-0.2-0.263L16.54 7.26c-0.101-0.307-0.254-0.593-0.45-0.848zm7.695 3.801l-0.766-0.248c-0.232-0.078-0.444-0.208-0.617-0.381-0.173-0.174-0.304-0.385-0.381-0.617l-0.25-0.765c-0.02-0.06-0.059-0.11-0.11-0.146C21.61 8.018 21.547 8 21.485 8c-0.063 0-0.124 0.02-0.175 0.056-0.051 0.036-0.09 0.087-0.11 0.146l-0.25 0.764c-0.075 0.231-0.203 0.442-0.374 0.615-0.17 0.173-0.379 0.304-0.609 0.384l-0.765 0.248c-0.06 0.021-0.11 0.06-0.147 0.11C19.02 10.376 19 10.437 19 10.499c0 0.063 0.02 0.124 0.055 0.175 0.037 0.05 0.088 0.09 0.147 0.11l0.765 0.249c0.233 0.077 0.445 0.208 0.619 0.382 0.173 0.174 0.303 0.386 0.38 0.62l0.249 0.764c0.02 0.06 0.06 0.11 0.11 0.146C21.376 12.982 21.437 13 21.5 13c0.063 0 0.124-0.02 0.175-0.056 0.05-0.036 0.09-0.087 0.11-0.146l0.249-0.764c0.077-0.233 0.208-0.444 0.381-0.618 0.174-0.173 0.385-0.303 0.618-0.38l0.765-0.25c0.06-0.02 0.11-0.059 0.147-0.11C23.98 10.626 24 10.564 24 10.502c0-0.063-0.02-0.124-0.055-0.175-0.037-0.05-0.088-0.09-0.147-0.11l-0.015-0.004zM5.25 3h7.797c-0.306 0.11-0.573 0.308-0.767 0.569C12.098 3.834 12 4.148 12 4.469V4.5H5.25C4.836 4.5 4.5 4.836 4.5 5.25v12.5c0 0.966 0.784 1.75 1.75 1.75h9.25V7.327c0.034 0.072 0.065 0.146 0.09 0.222L16 8.999c0.106 0.31 0.305 0.579 0.57 0.77 0.133 0.092 0.278 0.162 0.43 0.21V14h4v3.75c0 1.795-1.455 3.25-3.25 3.25H6.25C4.455 21 3 19.545 3 17.75V5.25C3 4.007 4.007 3 5.25 3zM17 19.5h0.75c0.966 0 1.75-0.784 1.75-1.75V15.5H17v4zM7.25 7C6.836 7 6.5 7.336 6.5 7.75S6.836 8.5 7.25 8.5h5.5c0.414 0 0.75-0.336 0.75-0.75S13.164 7 12.75 7h-5.5zM6.5 11.75C6.5 11.336 6.836 11 7.25 11h5.5c0.414 0 0.75 0.336 0.75 0.75s-0.336 0.75-0.75 0.75h-5.5c-0.414 0-0.75-0.336-0.75-0.75zM7.25 15c-0.414 0-0.75 0.336-0.75 0.75s0.336 0.75 0.75 0.75h3c0.414 0 0.75-0.336 0.75-0.75S10.664 15 10.25 15h-3z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -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="M18.38 4c0.31 0 0.588 0.191 0.7 0.482 0.56 1.462 1.609 2.015 2.17 2.015 0.414 0 0.75 0.335 0.75 0.75 0 0.414-0.336 0.75-0.75 0.75-1.002 0-2.087-0.611-2.873-1.687-0.814 1.093-1.971 1.687-3.187 1.687-1.217 0-2.376-0.595-3.19-1.69-0.814 1.095-1.973 1.69-3.19 1.69-1.215 0-2.373-0.593-3.187-1.686C4.838 7.388 3.753 8 2.75 8 2.336 8 2 7.664 2 7.25S2.336 6.5 2.75 6.5c0.56 0 1.61-0.553 2.17-2.018C5.031 4.192 5.31 4.001 5.62 4c0.311 0 0.59 0.192 0.701 0.482 0.544 1.419 1.57 2.015 2.49 2.015 0.92 0 1.945-0.596 2.489-2.015C11.41 4.192 11.69 4 12 4c0.31 0 0.59 0.192 0.7 0.482 0.544 1.419 1.57 2.015 2.49 2.015 0.92 0 1.945-0.597 2.489-2.015C17.79 4.192 18.069 4 18.379 4zm0 6c0.31 0 0.588 0.191 0.7 0.482 0.56 1.462 1.609 2.015 2.17 2.015 0.414 0 0.75 0.335 0.75 0.75 0 0.414-0.336 0.75-0.75 0.75-1.002 0-2.087-0.611-2.873-1.687-0.814 1.093-1.971 1.687-3.187 1.687-1.217 0-2.376-0.595-3.19-1.69-0.814 1.095-1.973 1.69-3.19 1.69-1.215 0-2.373-0.593-3.187-1.685C4.838 13.388 3.753 14 2.75 14 2.336 14 2 13.664 2 13.25s0.336-0.75 0.75-0.75c0.56 0 1.61-0.553 2.17-2.018 0.111-0.29 0.39-0.481 0.7-0.481 0.311 0 0.59 0.191 0.701 0.481 0.544 1.419 1.57 2.015 2.49 2.015 0.92 0 1.945-0.596 2.489-2.015 0.11-0.29 0.39-0.481 0.7-0.481 0.31 0 0.59 0.191 0.7 0.481 0.544 1.419 1.57 2.015 2.49 2.015 0.92 0 1.945-0.597 2.489-2.015 0.111-0.29 0.39-0.482 0.7-0.482zm0.7 6.482C18.969 16.192 18.69 16 18.38 16c-0.311 0-0.59 0.192-0.701 0.482-0.544 1.418-1.57 2.015-2.49 2.015-0.92 0-1.945-0.596-2.489-2.015C12.59 16.192 12.31 16 12 16c-0.31 0-0.59 0.192-0.7 0.482-0.544 1.419-1.57 2.015-2.49 2.015-0.92 0-1.945-0.596-2.489-2.015C6.21 16.192 5.931 16 5.621 16c-0.311 0-0.59 0.192-0.7 0.482C4.358 17.947 3.31 18.5 2.75 18.5 2.336 18.5 2 18.836 2 19.25S2.336 20 2.75 20c1.003 0 2.088-0.612 2.873-1.689 0.814 1.093 1.972 1.686 3.187 1.686 1.217 0 2.376-0.595 3.19-1.69 0.814 1.096 1.973 1.69 3.19 1.69 1.216 0 2.373-0.594 3.187-1.687 0.786 1.076 1.87 1.687 2.873 1.687 0.414 0 0.75-0.336 0.75-0.75 0-0.415-0.336-0.75-0.75-0.75-0.561 0-1.61-0.553-2.17-2.015z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -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="M12 4.001c3.168 0 4.966 2.097 5.227 4.63h0.08c2.04 0 3.692 1.649 3.692 3.683 0 2.033-1.653 3.682-3.692 3.682h-0.582l-1.582 2.635c-0.207 0.358-0.666 0.481-1.025 0.274-0.329-0.19-0.46-0.591-0.32-0.933l0.046-0.091 1.149-1.885h-2.136l-1.582 2.635c-0.207 0.358-0.666 0.481-1.025 0.274-0.329-0.19-0.46-0.591-0.32-0.933l0.046-0.091 1.148-1.885H8.988l-1.582 2.635c-0.207 0.358-0.665 0.481-1.024 0.274-0.329-0.19-0.46-0.591-0.32-0.933l0.045-0.091 1.148-1.885H6.693C4.653 15.996 3 14.347 3 12.314 3 10.28 4.654 8.63 6.693 8.63h0.08C7.035 6.081 8.83 4.001 12 4.001zm0 1.498c-2.071 0-3.877 1.633-3.877 3.889 0 0.357-0.319 0.638-0.684 0.638H6.75c-1.261 0-2.284 1.001-2.284 2.236 0 1.235 1.022 2.237 2.284 2.237h10.5c1.261 0 2.284-1.002 2.284-2.237s-1.023-2.236-2.284-2.236h-0.69c-0.365 0-0.684-0.28-0.684-0.638 0-2.285-1.806-3.89-3.877-3.89z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -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="M11.75 2c0.414 0 0.75 0.336 0.75 0.75v3.155l2.133-2.134c0.293-0.292 0.768-0.292 1.061 0 0.293 0.293 0.293 0.768 0 1.061L12.5 8.026V11h2.974l3.194-3.194c0.293-0.293 0.768-0.293 1.06 0 0.293 0.293 0.293 0.768 0 1.06L17.596 11h3.155c0.414 0 0.75 0.336 0.75 0.75s-0.336 0.75-0.75 0.75h-3.155l2.134 2.133c0.293 0.293 0.293 0.768 0 1.061-0.293 0.293-0.768 0.293-1.061 0L15.474 12.5H12.5v2.974l3.194 3.194c0.293 0.293 0.293 0.768 0 1.06-0.293 0.293-0.768 0.293-1.06 0L12.5 17.596v3.155c0 0.414-0.336 0.75-0.75 0.75S11 21.164 11 20.75v-3.155L8.867 19.73c-0.293 0.293-0.768 0.293-1.061 0-0.293-0.293-0.293-0.768 0-1.061L11 15.474V12.5H8.026l-3.194 3.194c-0.293 0.293-0.768 0.293-1.06 0-0.293-0.293-0.293-0.768 0-1.06L5.904 12.5H2.75C2.336 12.5 2 12.164 2 11.75S2.336 11 2.75 11h3.155L3.77 8.867c-0.292-0.293-0.292-0.768 0-1.061 0.293-0.293 0.768-0.293 1.061 0L8.026 11H11V8.026L7.806 4.832c-0.293-0.293-0.293-0.768 0-1.06 0.293-0.293 0.768-0.293 1.06 0L11 5.904V2.75C11 2.336 11.336 2 11.75 2z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -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="M12 2c0.414 0 0.75 0.336 0.75 0.75v1.5C12.75 4.664 12.414 5 12 5s-0.75-0.336-0.75-0.75v-1.5C11.25 2.336 11.586 2 12 2zm0 15c2.761 0 5-2.239 5-5s-2.239-5-5-5-5 2.239-5 5 2.239 5 5 5zm0-1.5c-1.933 0-3.5-1.567-3.5-3.5s1.567-3.5 3.5-3.5 3.5 1.567 3.5 3.5-1.567 3.5-3.5 3.5zm9.25-2.75c0.414 0 0.75-0.336 0.75-0.75s-0.336-0.75-0.75-0.75h-1.5C19.336 11.25 19 11.586 19 12s0.336 0.75 0.75 0.75h1.5zM12 19c0.414 0 0.75 0.336 0.75 0.75v1.5c0 0.414-0.336 0.75-0.75 0.75s-0.75-0.336-0.75-0.75v-1.5c0-0.414 0.336-0.75 0.75-0.75zm-7.75-6.25C4.664 12.75 5 12.414 5 12s-0.336-0.75-0.75-0.75h-1.5C2.336 11.25 2 11.586 2 12s0.336 0.75 0.75 0.75h1.5zM4.22 4.22c0.293-0.293 0.767-0.293 1.06 0l1.5 1.5c0.293 0.293 0.293 0.768 0 1.06-0.293 0.294-0.767 0.294-1.06 0l-1.5-1.5c-0.293-0.292-0.293-0.767 0-1.06zm1.06 15.56c-0.293 0.294-0.767 0.294-1.06 0-0.293-0.292-0.293-0.767 0-1.06l1.5-1.5c0.293-0.293 0.767-0.293 1.06 0 0.293 0.293 0.293 0.768 0 1.06l-1.5 1.5zm14.5-15.56c-0.293-0.293-0.767-0.293-1.06 0l-1.5 1.5c-0.293 0.293-0.293 0.768 0 1.06 0.293 0.294 0.767 0.294 1.06 0l1.5-1.5c0.293-0.292 0.293-0.767 0-1.06zm-1.06 15.56c0.293 0.294 0.767 0.294 1.06 0 0.293-0.292 0.293-0.767 0-1.06l-1.5-1.5c-0.293-0.293-0.767-0.293-1.06 0-0.293 0.293-0.293 0.768 0 1.06l1.5 1.5z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -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="M12.75 2.75C12.75 2.336 12.414 2 12 2s-0.75 0.336-0.75 0.75v1.5C11.25 4.664 11.586 5 12 5s0.75-0.336 0.75-0.75v-1.5zm6.28 2.22c0.293 0.293 0.293 0.767 0 1.06l-1.06 1.061c-0.293 0.293-0.768 0.293-1.061 0-0.293-0.293-0.293-0.768 0-1.06l1.06-1.061c0.294-0.293 0.768-0.293 1.061 0zM17.41 13c0.059-0.324 0.09-0.659 0.09-1 0-3.038-2.462-5.5-5.5-5.5S6.5 8.962 6.5 12c0 0.341 0.031 0.676 0.09 1H2.75C2.336 13 2 13.336 2 13.75s0.336 0.75 0.75 0.75h18.5c0.414 0 0.75-0.336 0.75-0.75S21.664 13 21.25 13h-3.84zM12 8c2.21 0 4 1.79 4 4 0 0.345-0.044 0.68-0.126 1H8.126C8.044 12.68 8 12.345 8 12c0-2.21 1.79-4 4-4zm-6 8.75C6 16.336 6.336 16 6.75 16h10.5c0.414 0 0.75 0.336 0.75 0.75s-0.336 0.75-0.75 0.75H6.75C6.336 17.5 6 17.164 6 16.75zm4 3c0-0.414 0.336-0.75 0.75-0.75h2.5c0.414 0 0.75 0.336 0.75 0.75s-0.336 0.75-0.75 0.75h-2.5c-0.414 0-0.75-0.336-0.75-0.75zM4.97 4.97c0.293-0.293 0.768-0.293 1.06 0l1.061 1.06c0.293 0.293 0.293 0.768 0 1.061-0.293 0.293-0.768 0.293-1.06 0L4.97 6.031c-0.293-0.294-0.293-0.768 0-1.061z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -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="M10.464 15.748L12.7 13.26c0.277-0.308 0.751-0.333 1.06-0.056 0.28 0.252 0.326 0.666 0.124 0.971l-0.068 0.088-1.111 1.236h2.276c0.594 0 0.938 0.648 0.645 1.134l-0.058 0.084-3.212 4.031c-0.258 0.324-0.73 0.378-1.054 0.12-0.294-0.235-0.365-0.646-0.182-0.963l0.063-0.091 2.242-2.815h-2.403c-0.613 0-0.953-0.685-0.623-1.168l0.065-0.083L12.7 13.26l-2.236 2.488zm2.538-10.74c3.168 0 4.966 2.098 5.227 4.631h0.08c2.04 0 3.692 1.649 3.692 3.683 0 2.033-1.653 3.682-3.692 3.682l-1.788 0.001c0.144-0.213 0.228-0.471 0.228-0.748 0-0.279-0.085-0.537-0.23-0.751h1.734c1.261 0 2.283-1 2.283-2.236 0-1.235-1.022-2.236-2.283-2.236h-0.69c-0.366 0-0.685-0.28-0.685-0.638 0-2.285-1.805-3.89-3.876-3.89-2.072 0-3.877 1.634-3.877 3.89 0 0.357-0.319 0.638-0.684 0.638H7.75c-1.262 0-2.284 1-2.284 2.236 0 1.235 1.022 2.237 2.283 2.237l1.762-0.001c-0.146 0.214-0.23 0.472-0.23 0.75s0.084 0.535 0.228 0.75l-1.816-0.002c-2.039 0-3.692-1.649-3.692-3.682 0-2.034 1.653-3.683 3.692-3.683h0.08c0.263-2.55 2.06-4.63 5.228-4.63zM10 2c1.617 0 3.05 0.815 3.9 2.062-0.29-0.035-0.59-0.053-0.898-0.053-0.395 0-0.775 0.029-1.139 0.085C11.335 3.72 10.69 3.5 10 3.5c-1.567 0-2.902 1.13-3.17 2.656L6.759 6.57c-0.084 0.478-0.5 0.827-0.985 0.827h-0.49C4.297 7.397 3.5 8.195 3.5 9.18c0 0.49 0.198 0.934 0.52 1.257-0.316 0.4-0.566 0.855-0.736 1.347C2.504 11.184 2 10.24 2 9.18 2 7.43 3.37 6 5.096 5.903l0.257-0.006C5.743 3.677 7.682 2 10 2z" android:fillColor="@color/fluent_default_icon_tint"/>
|
||||
</vector>
|
||||
@@ -1,40 +1,39 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.joinmastodon.android.ui.views.CheckableRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingRight="16dp"
|
||||
android:paddingLeft="16dp"
|
||||
android:clipToPadding="false">
|
||||
|
||||
<FrameLayout
|
||||
<org.joinmastodon.android.ui.views.CheckableRelativeLayout
|
||||
android:id="@+id/checkbox_wrap"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="46sp"
|
||||
android:duplicateParentState="true">
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingTop="16dp">
|
||||
|
||||
<View
|
||||
android:id="@+id/checkbox"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginStart="-4dp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:duplicateParentState="true"/>
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="46sp"
|
||||
android:duplicateParentState="true">
|
||||
|
||||
</FrameLayout>
|
||||
<View
|
||||
android:id="@+id/checkbox"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_gravity="center"
|
||||
android:duplicateParentState="true"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</org.joinmastodon.android.ui.views.CheckableRelativeLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-16dp"
|
||||
android:layout_marginStart="-16dp"
|
||||
android:layout_marginHorizontal="-16dp"
|
||||
android:layout_toEndOf="@id/checkbox_wrap">
|
||||
|
||||
<include layout="@layout/display_item_header" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</org.joinmastodon.android.ui.views.CheckableRelativeLayout>
|
||||
</RelativeLayout>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<org.joinmastodon.android.ui.views.NestedRecyclerScrollView
|
||||
android:id="@+id/scroller"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:nestedScrollingEnabled="true">
|
||||
|
||||
<org.joinmastodon.android.ui.views.CustomDrawingOrderLinearLayout
|
||||
@@ -50,7 +50,7 @@
|
||||
android:text="@string/follows_you"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="#fff"
|
||||
android:textSize="14dp"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
@@ -80,11 +80,13 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/cover"
|
||||
android:layout_alignParentEnd="true">
|
||||
android:layout_alignParentEnd="true"
|
||||
android:clipChildren="false">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="4dp">
|
||||
|
||||
@@ -95,7 +97,8 @@
|
||||
style="@style/Widget.Mastodon.M3.Button.Tonal"
|
||||
android:background="@drawable/bg_button_m3_tonal_circle_selector"
|
||||
android:paddingStart="12dp"
|
||||
android:drawableStart="@drawable/ic_fluent_alert_24_selector" />
|
||||
android:drawableStart="@drawable/ic_fluent_alert_24_selector"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/notify_progress"
|
||||
@@ -112,6 +115,7 @@
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="48dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_marginEnd="16dp">
|
||||
@@ -213,6 +217,60 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/username"
|
||||
android:id="@+id/note_edit_wrap"
|
||||
android:layout_marginTop="4dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/note_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="52dp"
|
||||
android:paddingVertical="15dp"
|
||||
android:textColor="?colorM3OnSurface"
|
||||
android:inputType="text|textMultiLine|textCapSentences"
|
||||
android:singleLine="false"
|
||||
android:background="@drawable/bg_note_edit"
|
||||
android:paddingEnd="52dp"
|
||||
android:paddingStart="20dp"
|
||||
android:elevation="0dp"
|
||||
android:hint="@string/sk_private_note_hint"
|
||||
tools:ignore="RtlSymmetry" />
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
android:layout_gravity="end">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/note_save_btn"
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
android:visibility="invisible"
|
||||
android:background="@drawable/bg_button_m3_text_circle"
|
||||
android:tooltipText="@string/sk_confirm_changes"
|
||||
android:contentDescription="@string/sk_confirm_changes"
|
||||
android:src="@drawable/ic_fluent_checkmark_24_regular"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/note_save_progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
style="?android:progressBarStyleSmall"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<org.joinmastodon.android.ui.views.LinkedTextView
|
||||
android:id="@+id/bio"
|
||||
android:layout_width="match_parent"
|
||||
@@ -284,7 +342,8 @@
|
||||
<me.grishka.appkit.views.UsableRecyclerView
|
||||
android:id="@+id/metadata"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:paddingTop="4dp" />
|
||||
|
||||
<LinearLayout
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
android:id="@+id/forward_report"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:paddingHorizontal="16dp"
|
||||
android:paddingVertical="12dp"
|
||||
android:background="?android:selectableItemBackground">
|
||||
|
||||
@@ -5,4 +5,5 @@
|
||||
<item android:id="@+id/draft" android:title="@string/sk_mark_as_draft" android:icon="@drawable/ic_fluent_drafts_24_regular" />
|
||||
<item android:id="@+id/undraft" android:title="@string/sk_mark_as_draft" android:icon="@drawable/ic_fluent_drafts_24_filled" android:contentDescription="@string/sk_compose_no_draft" />
|
||||
<item android:id="@+id/drafts" android:title="@string/sk_unsent_posts" android:icon="@drawable/ic_fluent_folder_open_24_regular" />
|
||||
<item android:id="@+id/preview" android:title="@string/sk_open_post_preview" android:icon="@drawable/ic_fluent_receipt_sparkles_24_regular" android:visible="false" />
|
||||
</menu>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<group android:id="@+id/menu_group1">
|
||||
<item android:id="@+id/edit_note" android:title="@string/sk_add_note" android:icon="@drawable/ic_fluent_person_note_24_regular" />
|
||||
</group>
|
||||
<group android:id="@+id/menu_group2">
|
||||
<item android:id="@+id/manage_user_lists" android:title="@string/sk_lists_with_user" android:icon="@drawable/ic_fluent_people_24_regular"/>
|
||||
<item android:id="@+id/mute" android:title="@string/mute_user" android:icon="@drawable/ic_fluent_speaker_off_24_regular"/>
|
||||
<item android:id="@+id/hide_boosts" android:title="@string/hide_boosts_from_user" android:icon="@drawable/ic_fluent_arrow_repeat_all_off_24_regular"/>
|
||||
@@ -9,7 +12,7 @@
|
||||
<item android:id="@+id/report" android:title="@string/report_user" android:icon="@drawable/ic_fluent_warning_24_regular"/>
|
||||
<item android:id="@+id/block_domain" android:title="@string/block_domain" android:icon="@drawable/ic_fluent_shield_prohibited_24_regular"/>
|
||||
</group>
|
||||
<group android:id="@+id/menu_group2">
|
||||
<group android:id="@+id/menu_group3">
|
||||
<item android:id="@+id/open_in_browser" android:title="@string/open_in_browser" android:icon="@drawable/ic_fluent_globe_24_regular"/>
|
||||
<item android:id="@+id/share" android:title="@string/share_user" android:icon="@drawable/ic_fluent_share_24_regular"/>
|
||||
<item android:id="@+id/open_with_account" android:title="@string/sk_open_with_account" android:visible="false" android:icon="@drawable/ic_fluent_person_swap_24_regular">
|
||||
|
||||
@@ -415,5 +415,23 @@
|
||||
<string name="sk_settings_default_visibility">Standard-Sichtbarkeit für Posts</string>
|
||||
<string name="sk_settings_lock_account">Neue Follower_innen manuell genehmigen</string>
|
||||
<string name="sk_timeline_cache_cleared">Start-Timeline geleert</string>
|
||||
<string name="sk_button_mutuals">Befreundet</string>
|
||||
<string name="sk_button_mutuals">Befreundet</string>
|
||||
<string name="sk_icon_snowflake">Schneeflocke</string>
|
||||
<string name="sk_icon_cloud">Wolke</string>
|
||||
<string name="sk_icon_sunset">Sonnenuntergang</string>
|
||||
<string name="sk_private_note_hint">Private Notiz über dieses Profil hinzufügen</string>
|
||||
<string name="sk_icon_water">Wasser</string>
|
||||
<string name="sk_icon_sun">Sonne</string>
|
||||
<string name="sk_icon_rain">Regen</string>
|
||||
<string name="sk_icon_thunderstorm">Gewitter</string>
|
||||
<string name="sk_private_note_update_failed">Notiz speichern fehlgeschlagen</string>
|
||||
<string name="sk_delete_note">Private Notiz löschen</string>
|
||||
<string name="sk_confirm_changes">Änderungen bestätigen</string>
|
||||
<string name="sk_settings_crash_log_unavailable">Keines verfügbar… noch</string>
|
||||
<string name="sk_crash_log_copied">Absturzprotokoll kopiert</string>
|
||||
<string name="sk_add_note">Private Notiz hinzufügen</string>
|
||||
<string name="sk_settings_copy_crash_log">Neuestes Absturzprotokoll kopieren</string>
|
||||
<string name="sk_open_post_preview">Vorschau öffnen</string>
|
||||
<string name="sk_post_preview">Vorschau</string>
|
||||
<string name="sk_private_note_confirm_delete">Private Notiz über %s löschen\?</string>
|
||||
</resources>
|
||||
2
mastodon/src/main/res/values-eo/strings_sk.xml
Normal file
2
mastodon/src/main/res/values-eo/strings_sk.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
@@ -402,15 +402,28 @@
|
||||
<string name="sk_trending_links_info_banner">Estas noticias están dando que hablar en todo el Fediverso.</string>
|
||||
<string name="sk_blocked_accounts">Cuentas bloqueadas</string>
|
||||
<string name="sk_muted_accounts">Cuentas silenciadas</string>
|
||||
<string name="sk_settings_like_icon">Utilizar el corazón como icono favorito</string>
|
||||
<string name="sk_settings_like_icon">Utilizar un corazón como icono de favorito</string>
|
||||
<string name="sk_recently_used">Utilizado recientemente</string>
|
||||
<string name="sk_set_as_default">Establecer por defecto</string>
|
||||
<string name="sk_settings_color_palette_default">Por defecto (%s)</string>
|
||||
<string name="sk_settings_underlined_links">Enlaces subrayados</string>
|
||||
<string name="sk_settings_underlined_links">Subrayar enlaces</string>
|
||||
<string name="sk_edit_alt_text">Editar el texto alternativo</string>
|
||||
<string name="sk_settings_default_visibility">Visibilidad de la publicación predeterminada</string>
|
||||
<string name="sk_settings_default_visibility">Visibilidad de publicación predeterminada</string>
|
||||
<string name="sk_settings_lock_account">Aprobar nuevos seguidores manualmente</string>
|
||||
<string name="sk_timeline_cache_cleared">Caché de la línea de tiempo de inicio borrada</string>
|
||||
<string name="sk_settings_clear_timeline_cache">Borrar la caché de la línea de tiempo de inicio</string>
|
||||
<string name="sk_timeline_cache_cleared">Caché de la cronología de inicio borrada</string>
|
||||
<string name="sk_settings_clear_timeline_cache">Borrar la caché de la cronología de inicio</string>
|
||||
<string name="sk_button_mutuals">Amigos</string>
|
||||
<string name="sk_icon_snowflake">Copos de nieve</string>
|
||||
<string name="sk_private_note_confirm">Confirmar los cambios en la nota</string>
|
||||
<string name="sk_private_note_update_failed">No se pudo guardar la nota</string>
|
||||
<string name="sk_icon_cloud">Nube</string>
|
||||
<string name="sk_delete_note">Borrar la nota personal</string>
|
||||
<string name="sk_icon_sunset">Puesta de sol</string>
|
||||
<string name="sk_private_note_hint">Añadir una nota personal sobre este perfil</string>
|
||||
<string name="sk_icon_water">Agua</string>
|
||||
<string name="sk_icon_sun">Sol</string>
|
||||
<string name="sk_add_note">Añadir una nota personal</string>
|
||||
<string name="sk_icon_rain">Lluvia</string>
|
||||
<string name="sk_icon_thunderstorm">Tormenta eléctrica</string>
|
||||
<string name="sk_private_note_confirm_delete">¿Borrar la nota personal sobre %s\?</string>
|
||||
</resources>
|
||||
@@ -403,4 +403,9 @@
|
||||
<string name="sk_muted_accounts">حسابهای خموش شده</string>
|
||||
<string name="sk_recently_used">اخیرا مورد استفاده قرار گرفته</string>
|
||||
<string name="sk_edit_alt_text">ویرایش متن جایگزین</string>
|
||||
<string name="sk_settings_default_visibility">نمایانی فرسته پیشگزیده</string>
|
||||
<string name="sk_settings_lock_account">تایید دستی پیگیران جدید</string>
|
||||
<string name="sk_timeline_cache_cleared">کش خط زمانی خانه پاک شد</string>
|
||||
<string name="sk_button_mutuals">متقابل</string>
|
||||
<string name="sk_settings_clear_timeline_cache">پاک کردن کش خط زمانی خانه</string>
|
||||
</resources>
|
||||
@@ -297,7 +297,7 @@
|
||||
<string name="sk_settings_allow_remote_loading">Charger des informations à partir d\'instances distantes</string>
|
||||
<string name="sk_no_remote_info_hint">informations distantes indisponibles</string>
|
||||
<string name="sk_error_loading_profile">Échec du chargement du profil via %s</string>
|
||||
<string name="sk_settings_allow_remote_loading_explanation">Essayez de récupérer des listes plus précises pour les abonnés, les likes et les boosts en chargeant les informations à partir de l\'instance d\'origine.</string>
|
||||
<string name="sk_settings_allow_remote_loading_explanation">Essaye de récupérer des listes plus précises pour les abonné·e·s, les favoris et les boosts en chargeant les informations à partir de l\'instance d\'origine.</string>
|
||||
<string name="sk_settings_auto_reveal_equal_spoilers">Révéler les CW identiques dans les réponses</string>
|
||||
<string name="sk_settings_auto_reveal_nobody">Jamais</string>
|
||||
<string name="sk_settings_auto_reveal_author">Réponses du même auteur</string>
|
||||
@@ -412,7 +412,7 @@
|
||||
<string name="sk_message_cache_cleared">Cache des messages vidé</string>
|
||||
<string name="sk_settings_clear_timeline_cache">Effacer le cache du fil d\'accueil</string>
|
||||
<string name="sk_settings_default_visibility">Visibilité de publication par défaut</string>
|
||||
<string name="sk_settings_lock_account">Approuver manuellement les nouveaux abonnés</string>
|
||||
<string name="sk_settings_lock_account">Approuver manuellement les nouveaux·elles abonné·e·s</string>
|
||||
<string name="sk_timeline_cache_cleared">Cache du fil d\'accueil vidé</string>
|
||||
<string name="sk_button_mutuals">Amis</string>
|
||||
<string name="sk_button_mutuals">Suivi mutuel</string>
|
||||
</resources>
|
||||
@@ -157,7 +157,7 @@
|
||||
<string name="sk_visibility_unlisted">Nenavedeno</string>
|
||||
<string name="sk_lists_with_user">Liste s %s</string>
|
||||
<string name="sk_settings_show_federated_timeline">Prikaži objedinjenu vremensku traku</string>
|
||||
<string name="sk_translated_using">"Prevedeno uporabom %s"</string>
|
||||
<string name="sk_translated_using">Prevedeno uporabom %s</string>
|
||||
<string name="sk_settings_translate_only_opened">Prevodi samo otvorene objave</string>
|
||||
<string name="sk_loading_fediverse_resource_title">Pretraga na Fediversu</string>
|
||||
<string name="sk_loading_resource_on_instance_title">Pretraga na %s</string>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<string name="sk_delete_and_redraft">削除して再編集</string>
|
||||
<string name="sk_confirm_delete_and_redraft_title">投稿を削除して再編集</string>
|
||||
<string name="sk_confirm_delete_and_redraft">本当にこの投稿を削除して再編集しますか?</string>
|
||||
<string name="sk_pin_post">プロファイルに固定</string>
|
||||
<string name="sk_pin_post">プロフィールに固定</string>
|
||||
<string name="sk_confirm_pin_post_title">投稿をプロフィールに固定</string>
|
||||
<string name="sk_confirm_pin_post">この投稿をあなたのプロフィールに固定しますか?</string>
|
||||
<string name="sk_pinning">投稿を固定しています…</string>
|
||||
@@ -154,7 +154,7 @@
|
||||
<string name="sk_scheduled_too_soon">投稿は10分以上後の予約である必要があります。</string>
|
||||
<string name="sk_settings_unifiedpush">UnifiedPush を使用</string>
|
||||
<string name="sk_settings_unifiedpush_choose">ディストリビューターを選択</string>
|
||||
<string name="sk_list_replies_policy_followed">フォローされたユーザー</string>
|
||||
<string name="sk_list_replies_policy_followed">フォロー中のユーザー</string>
|
||||
<string name="sk_delete_list">リストを削除</string>
|
||||
<string name="sk_delete_list_confirm">本当にリスト “%s” を削除しますか?</string>
|
||||
<string name="sk_edit_list_title">リストを編集</string>
|
||||
@@ -288,7 +288,7 @@
|
||||
<string name="sk_settings_display_pronouns_in_user_listings">ユーザーリストに代名詞を表示</string>
|
||||
<string name="sk_settings_unifiedpush_no_distributor_body">UnifiedPush による通知を動作させるにはディストリビューターをインストールする必要があります。詳しくは https://unifiedpush.org/ をご覧ください</string>
|
||||
<string name="sk_list_replies_policy_list">リストのメンバー</string>
|
||||
<string name="sk_settings_support_local_only">サーバーはローカルのみの投稿をサポートしています</string>
|
||||
<string name="sk_settings_support_local_only">ローカルのみの投稿をサポートするサーバー</string>
|
||||
<string name="sk_spoiler_show">コンテンツを表示</string>
|
||||
<string name="sk_advanced_options_hide">高度な設定を非表示にする</string>
|
||||
<string name="sk_signed_up">登録済み</string>
|
||||
@@ -302,7 +302,7 @@
|
||||
</plurals>
|
||||
<string name="sk_reported">報告済み</string>
|
||||
<string name="sk_set_as_default">デフォルトに設定</string>
|
||||
<string name="sk_settings_like_icon">お気に入りアイコンにハートを使用する</string>
|
||||
<string name="sk_settings_like_icon">お気に入りアイコンにハートを使用</string>
|
||||
<string name="sk_content_type_html">HTML</string>
|
||||
<string name="sk_settings_color_palette_default">デフォルト (%s)</string>
|
||||
<string name="sk_open_in_app">アプリで開く</string>
|
||||
@@ -354,4 +354,34 @@
|
||||
<string name="sk_show_thread">スレッドを表示</string>
|
||||
<string name="sk_settings_clear_timeline_cache">ホームタイムラインキャッシュをクリア</string>
|
||||
<string name="sk_add_timeline_tag_error_empty">ハッシュタグは空欄にできません</string>
|
||||
<string name="sk_settings_allow_remote_loading_explanation">発信元のインスタンスから情報を読み込んで、フォロワー、いいね、ブーストのより正確なリストを取得しましょう。</string>
|
||||
<string name="sk_disable_pill_shaped_active_indicator">錠剤型のアクティブタブインジケーターを無効にする</string>
|
||||
<string name="sk_settings_show_emoji_reactions_only_opened">投稿を開いた時のみ</string>
|
||||
<string name="sk_settings_glitch_instance">Glitchのローカル限定モード</string>
|
||||
<string name="sk_settings_allow_remote_loading">リモートインスタンスから情報を読み込む</string>
|
||||
<string name="sk_notify_posts_info_banner">投稿通知を有効にすると、その人の新しい投稿がここに表示されます。</string>
|
||||
<string name="sk_settings_auto_reveal_anyone">全員の返信</string>
|
||||
<string name="sk_settings_show_emoji_reactions_hide_empty">空の絵文字リアクションを隠す</string>
|
||||
<string name="sk_settings_show_emoji_reactions">絵文字リアクションをタイムラインに表示</string>
|
||||
<string name="sk_settings_default_visibility">投稿のデフォルト公開範囲</string>
|
||||
<string name="sk_settings_underlined_links">下線付きリンク</string>
|
||||
<string name="sk_list_replies_policy_none">なし</string>
|
||||
<string name="sk_settings_show_alt_indicator">代替テキストがあることを示す表示</string>
|
||||
<string name="sk_settings_auto_reveal_nobody">なし</string>
|
||||
<string name="sk_settings_lock_account">新しいフォロワーを手動で承認</string>
|
||||
<string name="sk_settings_glitch_mode_explanation">ホームインスタンスがGlitchで動作している場合、これを有効にしてください。HometownやAkkomaでは不要です。</string>
|
||||
<string name="sk_settings_auto_reveal_equal_spoilers">返信内にある同一のCWを自動で展開</string>
|
||||
<string name="sk_settings_show_no_alt_indicator">代替テキストがないことを示す表示</string>
|
||||
<string name="sk_no_remote_info_hint">リモートの情報が利用できません</string>
|
||||
<string name="sk_settings_auto_reveal_author">同じ作成者の返信</string>
|
||||
<string name="sk_error_loading_profile">%s からのプロフィールの読み込みに失敗しました</string>
|
||||
<string name="sk_trending_posts_info_banner">これらの投稿は現在Fediverseで人気を集めています。</string>
|
||||
<string name="sk_in_reply">返信</string>
|
||||
<string name="sk_settings_confirm_before_reblog">ブーストする前に確認</string>
|
||||
<string name="sk_settings_emoji_reactions_explanation">投稿に対する絵文字リアクションを表示、追加することができます。様々なFediverseサーバーがこれに対応していますが、Mastodonは対応していません。</string>
|
||||
<string name="sk_recently_used">最近使用</string>
|
||||
<string name="sk_button_mutuals">相互フォロー中</string>
|
||||
<string name="sk_notify_poll_results">投票結果</string>
|
||||
<string name="sk_post_contains_media">メディアを含む投稿</string>
|
||||
<string name="sk_edit_alt_text">代替テキストを編集</string>
|
||||
</resources>
|
||||
@@ -409,4 +409,5 @@
|
||||
<string name="sk_settings_lock_account">Aprobați manual urmăritori noi</string>
|
||||
<string name="sk_timeline_cache_cleared">Memoria cache a cronologiei acasă a fost ștearsă</string>
|
||||
<string name="sk_settings_clear_timeline_cache">Ștergeți memoria cache a cronologiei acasă</string>
|
||||
<string name="sk_button_mutuals">Prieteni</string>
|
||||
</resources>
|
||||
@@ -415,4 +415,18 @@
|
||||
<string name="sk_settings_default_visibility">Усталена видимість дописів</string>
|
||||
<string name="sk_settings_lock_account">Затверджувати нових підписників уручну</string>
|
||||
<string name="sk_timeline_cache_cleared">Кеш домашньої стрічки очищено</string>
|
||||
<string name="sk_button_mutuals">Друзі</string>
|
||||
<string name="sk_icon_snowflake">Сніжинка</string>
|
||||
<string name="sk_private_note_confirm">Підтвердити зміни</string>
|
||||
<string name="sk_private_note_update_failed">Не вдалося зберегти нотатку</string>
|
||||
<string name="sk_icon_cloud">Хмара</string>
|
||||
<string name="sk_delete_note">Видалити нотатку</string>
|
||||
<string name="sk_icon_sunset">Захід сонця</string>
|
||||
<string name="sk_private_note_hint">Додати нотатку для себе про цей профіль</string>
|
||||
<string name="sk_icon_water">Вода</string>
|
||||
<string name="sk_icon_sun">Сонце</string>
|
||||
<string name="sk_add_note">Додати нотатку</string>
|
||||
<string name="sk_icon_rain">Дощ</string>
|
||||
<string name="sk_icon_thunderstorm">Гроза</string>
|
||||
<string name="sk_private_note_confirm_delete">Видалити нотатку про %s\?</string>
|
||||
</resources>
|
||||
@@ -2,29 +2,29 @@
|
||||
<resources>
|
||||
<string name="sk_pinned_posts">置顶</string>
|
||||
<string name="sk_delete_and_redraft">删除并重新编辑</string>
|
||||
<string name="sk_confirm_delete_and_redraft_title">删除并重新编辑帖文</string>
|
||||
<string name="sk_confirm_delete_and_redraft">确定要删除并重新编辑此帖文吗?</string>
|
||||
<string name="sk_confirm_delete_and_redraft_title">删除并重新编辑嘟文</string>
|
||||
<string name="sk_confirm_delete_and_redraft">确定要删除并重新编辑此嘟文吗?</string>
|
||||
<string name="sk_pin_post">置顶</string>
|
||||
<string name="sk_confirm_pin_post_title">置顶帖文</string>
|
||||
<string name="sk_confirm_pin_post">你确定要在资料页置顶此帖文吗?</string>
|
||||
<string name="sk_pinning">正在置顶帖文…</string>
|
||||
<string name="sk_confirm_pin_post_title">置顶嘟文</string>
|
||||
<string name="sk_confirm_pin_post">你确定要在资料页置顶此嘟文吗?</string>
|
||||
<string name="sk_pinning">正在置顶嘟文…</string>
|
||||
<string name="sk_unpin_post">取消置顶</string>
|
||||
<string name="sk_confirm_unpin_post_title">取消帖文置顶</string>
|
||||
<string name="sk_confirm_unpin_post">你确定不再置顶此帖文吗?</string>
|
||||
<string name="sk_confirm_unpin_post_title">取消嘟文置顶</string>
|
||||
<string name="sk_confirm_unpin_post">你确定不再置顶此嘟文吗?</string>
|
||||
<string name="sk_unpinning">正在取消置顶…</string>
|
||||
<string name="sk_image_description">图片描述</string>
|
||||
<string name="sk_visibility_unlisted">不公开</string>
|
||||
<string name="sk_federated_timeline">联邦时间轴</string>
|
||||
<string name="sk_federated_timeline_info_banner">这些是互联实例中最新发布的帖文。</string>
|
||||
<string name="sk_federated_timeline_info_banner">这些是互联实例中最新发布的嘟文。</string>
|
||||
<string name="sk_app_name">Megalodon</string>
|
||||
<string name="sk_settings_show_replies">显示回复</string>
|
||||
<string name="sk_settings_show_boosts">显示转嘟</string>
|
||||
<string name="sk_settings_load_new_posts">自动加载新帖文</string>
|
||||
<string name="sk_settings_load_new_posts">自动加载新嘟文</string>
|
||||
<string name="sk_settings_show_interaction_counts">显示互动次数</string>
|
||||
<string name="sk_settings_app_version">Megalodon v%1$s (%2$d)</string>
|
||||
<string name="sk_mark_media_as_sensitive">标记为敏感媒体</string>
|
||||
<string name="sk_user_post_notifications_on">启用 %s 的帖文通知</string>
|
||||
<string name="sk_user_post_notifications_off">关闭 %s 的帖文通知</string>
|
||||
<string name="sk_user_post_notifications_on">启用 %s 的嘟文通知</string>
|
||||
<string name="sk_user_post_notifications_off">关闭 %s 的嘟文通知</string>
|
||||
<string name="sk_update_available">Megalodon %s 已经可以下载了。</string>
|
||||
<string name="sk_update_ready">Megalodon %s 已下载,准备安装。</string>
|
||||
<string name="sk_check_for_update">检查更新</string>
|
||||
@@ -45,8 +45,8 @@
|
||||
<string name="sk_color_palette_blue">蓝</string>
|
||||
<string name="sk_color_palette_brown">棕</string>
|
||||
<string name="sk_color_palette_yellow">黄</string>
|
||||
<string name="sk_notification_type_status">帖文</string>
|
||||
<string name="sk_notification_type_posts">帖文通知</string>
|
||||
<string name="sk_notification_type_status">嘟文</string>
|
||||
<string name="sk_notification_type_posts">嘟文通知</string>
|
||||
<string name="sk_translate_post">翻译</string>
|
||||
<string name="sk_translate_show_original">显示原文</string>
|
||||
<string name="sk_poll_allow_multiple">允许多选</string>
|
||||
@@ -84,8 +84,8 @@
|
||||
<string name="sk_loading_fediverse_resource_title">在联邦宇宙上查找</string>
|
||||
<string name="sk_undo_reblog">撤销转嘟</string>
|
||||
<string name="sk_reblog_with_visibility">转嘟可见性</string>
|
||||
<string name="sk_quote_post">引用此帖文</string>
|
||||
<string name="sk_copy_link_to_post">复制帖文链接</string>
|
||||
<string name="sk_quote_post">引用此嘟文</string>
|
||||
<string name="sk_copy_link_to_post">复制嘟文链接</string>
|
||||
<string name="sk_hashtags_you_follow">你关注的标签</string>
|
||||
<string name="sk_loading_resource_on_instance_title">在 %s 上查找</string>
|
||||
<string name="sk_resource_not_found">找不到资源</string>
|
||||
@@ -101,27 +101,27 @@
|
||||
<string name="sk_already_reblogged">已转嘟过</string>
|
||||
<string name="sk_reply_as">用其他账号回复</string>
|
||||
<string name="sk_settings_uniform_icon_for_notifications">所有通知的统一图标</string>
|
||||
<string name="sk_unsent_posts">未发送的帖文</string>
|
||||
<string name="sk_unsent_posts">未发送的嘟文</string>
|
||||
<string name="sk_confirm_delete_draft_title">删除草稿</string>
|
||||
<string name="sk_draft">草稿</string>
|
||||
<string name="sk_schedule">定时</string>
|
||||
<string name="sk_confirm_delete_scheduled_post_title">删除定时帖文</string>
|
||||
<string name="sk_confirm_delete_scheduled_post">你确定要删除此定时帖文吗?</string>
|
||||
<string name="sk_confirm_delete_scheduled_post_title">删除定时嘟文</string>
|
||||
<string name="sk_confirm_delete_scheduled_post">你确定要删除此定时嘟文吗?</string>
|
||||
<string name="sk_draft_or_schedule">草稿或定时</string>
|
||||
<string name="sk_compose_draft">帖文将保存为草稿。</string>
|
||||
<string name="sk_compose_draft">嘟文将保存为草稿。</string>
|
||||
<string name="sk_compose_scheduled">定时于</string>
|
||||
<string name="sk_draft_saved">草稿已保存</string>
|
||||
<string name="sk_post_scheduled">帖文已定时</string>
|
||||
<string name="sk_post_scheduled">嘟文已定时</string>
|
||||
<string name="sk_forward_report_to">转嘟给 %s</string>
|
||||
<string name="sk_confirm_delete_draft">你确定要删除此帖文草稿吗?</string>
|
||||
<string name="sk_confirm_delete_draft">你确定要删除此嘟文草稿吗?</string>
|
||||
<string name="sk_scheduled_too_soon_title">定时时间过早</string>
|
||||
<string name="sk_scheduled_too_soon">帖文只能设置为 10 分钟或更晚发送。</string>
|
||||
<string name="sk_scheduled_too_soon">嘟文只能设置为 10 分钟或更晚发送。</string>
|
||||
<string name="sk_save_draft">保存为草稿?</string>
|
||||
<string name="sk_save_changes">保存更改?</string>
|
||||
<string name="sk_confirm_save_draft">保存草稿?</string>
|
||||
<string name="sk_confirm_save_changes">保存更改?</string>
|
||||
<string name="sk_mark_as_draft">标记为草稿</string>
|
||||
<string name="sk_schedule_post">定时帖文</string>
|
||||
<string name="sk_schedule_post">定时嘟文</string>
|
||||
<string name="sk_compose_no_schedule">不要定时</string>
|
||||
<string name="sk_compose_no_draft">不要标记为草稿</string>
|
||||
<string name="sk_settings_reduce_motion">减少动画效果</string>
|
||||
@@ -151,14 +151,14 @@
|
||||
<string name="sk_timeline_local">本站</string>
|
||||
<string name="sk_alt_text_missing">至少有一个附件不包含描述。</string>
|
||||
<string name="sk_publish_anyway">仍然发布</string>
|
||||
<string name="sk_notify_posts_info_banner">如果你为某些人启用了帖文通知,其新帖文将显示在此处。</string>
|
||||
<string name="sk_notify_posts_info_banner">如果你为某些人启用了嘟文通知,其新嘟文将显示在此处。</string>
|
||||
<string name="sk_timelines">时间线</string>
|
||||
<string name="sk_edit_timelines">编辑时间线</string>
|
||||
<string name="sk_alt_button">ALT</string>
|
||||
<string name="sk_post_edited">编辑</string>
|
||||
<string name="sk_notification_type_update">编辑帖文</string>
|
||||
<string name="sk_notification_type_update">编辑嘟文</string>
|
||||
<string name="sk_alt_text_missing_title">缺少 ALT 文本</string>
|
||||
<string name="sk_timeline_posts">帖文</string>
|
||||
<string name="sk_timeline_posts">嘟文</string>
|
||||
<string name="sk_timelines_add">添加</string>
|
||||
<string name="sk_timeline">时间线</string>
|
||||
<string name="sk_list">列表</string>
|
||||
@@ -223,7 +223,7 @@
|
||||
<string name="sk_icon_headphones">耳机</string>
|
||||
<string name="sk_icon_human">人类</string>
|
||||
<string name="sk_icon_globe">地球</string>
|
||||
<string name="sk_notify_update">编辑已转嘟帖文</string>
|
||||
<string name="sk_notify_update">编辑已转嘟嘟文</string>
|
||||
<string name="sk_icon_pin">钉子</string>
|
||||
<string name="sk_remove_follower_confirm">通过屏蔽并立即解除屏蔽以移除%s的关注者身份?</string>
|
||||
<string name="sk_icon_clapper_board">拍板</string>
|
||||
@@ -248,7 +248,7 @@
|
||||
<string name="sk_settings_glitch_mode_explanation">如果你的主实例运行 Glitch,请启用此功能。Hometown 或 Akkoma 不需要启用。</string>
|
||||
<string name="sk_sign_ups">用户注册</string>
|
||||
<string name="sk_new_reports">新举报</string>
|
||||
<string name="sk_settings_see_new_posts_button">“查看新帖文” 按钮</string>
|
||||
<string name="sk_settings_see_new_posts_button">“查看新嘟文” 按钮</string>
|
||||
<string name="sk_settings_server_version">服务器版本: %s</string>
|
||||
<string name="sk_notify_poll_results">投票结果</string>
|
||||
<string name="sk_expand">展开</string>
|
||||
@@ -256,8 +256,8 @@
|
||||
<string name="sk_unfinished_attachments">正在上传附件</string>
|
||||
<string name="sk_unfinished_attachments_message">部分附件尚未上传完毕。</string>
|
||||
<string name="sk_filtered">已过滤:%s</string>
|
||||
<string name="sk_settings_collapse_long_posts">折叠很长的帖文</string>
|
||||
<string name="sk_settings_prefix_reply_cw_with_re">回复时在 CW 前加上 “re:”</string>
|
||||
<string name="sk_settings_collapse_long_posts">折叠很长的嘟文</string>
|
||||
<string name="sk_settings_prefix_reply_cw_with_re">回复时在内容警告信息前加上 “re:”</string>
|
||||
<string name="sk_spectator_mode">旁观模式</string>
|
||||
<string name="sk_settings_hide_interaction">隐藏互动按钮</string>
|
||||
<string name="sk_settings_reply_visibility_self">对我的回复</string>
|
||||
@@ -287,7 +287,7 @@
|
||||
<string name="sk_content_type_markdown">Markdown</string>
|
||||
<string name="sk_content_type_bbcode">BBCode</string>
|
||||
<string name="sk_content_type_mfm">MFM</string>
|
||||
<string name="sk_settings_content_types">启用帖文格式</string>
|
||||
<string name="sk_settings_content_types">启用嘟文格式</string>
|
||||
<string name="sk_settings_content_types_explanation">允许在创建文章时设置类似Markdown的内容类型。注意,不是所有的实例都支持这个。</string>
|
||||
<string name="sk_settings_default_content_type">默认的内容类型</string>
|
||||
<string name="sk_timeline_bubble">Bubble</string>
|
||||
@@ -307,7 +307,7 @@
|
||||
<string name="sk_edit_timeline_tag_all">…并包含其中全部</string>
|
||||
<string name="sk_edit_timeline_tags_explanation">请注意,服务器会处理这些操作。可能不支持合并这些操作。</string>
|
||||
<string name="sk_settings_allow_remote_loading_explanation">尝试从原实例加载信息,以获取更准确的关注者、点赞和转发列表。</string>
|
||||
<string name="sk_settings_auto_reveal_equal_spoilers">在回复中自动显示相同的 CWs</string>
|
||||
<string name="sk_settings_auto_reveal_equal_spoilers">在回复中自动显示相同的内容警告</string>
|
||||
<string name="sk_settings_forward_report_default">\"转发报告 \"开关默认值</string>
|
||||
<string name="sk_list_exclusive_switch_explanation">排除列表的成员不会显示在你的主页时间线上--如果你的实例支持的话。</string>
|
||||
<string name="sk_advanced_options_show">显示高级选项</string>
|
||||
@@ -319,7 +319,7 @@
|
||||
<string name="sk_disable_pill_shaped_active_indicator">禁用药丸状的活跃选项卡指示器</string>
|
||||
<string name="sk_settings_true_black">全黑模式</string>
|
||||
<string name="sk_settings_display_pronouns_in_timelines">在时间线上显示性别代词</string>
|
||||
<string name="sk_settings_emoji_reactions_explanation">显示对帖文的表情回应,并允许你添加自己的表情回应。许多 Fediverse 服务器支持此功能,但 Mastodon 不支持。</string>
|
||||
<string name="sk_settings_emoji_reactions_explanation">显示对嘟文的表情回应,并允许你添加自己的表情回应。许多 Fediverse 服务器支持此功能,但 Mastodon 不支持。</string>
|
||||
<string name="sk_settings_emoji_reactions_in_lists">在时间线中显示表情回应</string>
|
||||
<string name="sk_settings_emoji_reactions_in_lists_explanation">是否在时间线中显示表情回应。如果此选项为关闭,则只有在查看对话时才会显示表情回应。</string>
|
||||
<string name="sk_button_react">用表情回应</string>
|
||||
@@ -349,12 +349,12 @@
|
||||
<string name="sk_icon_doctor">医生</string>
|
||||
<string name="sk_icon_diamond">钻石</string>
|
||||
<string name="sk_icon_umbrella">雨伞</string>
|
||||
<string name="sk_edit_timeline_tag_main">包含标签的帖文…</string>
|
||||
<string name="sk_edit_timeline_tag_main">包含标签的嘟文…</string>
|
||||
<string name="sk_edit_timeline_tag_none">…但都不包含</string>
|
||||
<string name="sk_edit_timeline_tag_any">…或包含其中任何一个</string>
|
||||
<string name="sk_edit_timeline_tag_hint">输入标签…</string>
|
||||
<string name="sk_edit_timeline_tags_hint">输入标签…</string>
|
||||
<string name="sk_hashtag_timeline_local_only_switch">仅显示本地帖文?</string>
|
||||
<string name="sk_hashtag_timeline_local_only_switch">仅显示本地嘟文?</string>
|
||||
<string name="sk_add_timeline_tag_error_empty">标签不可为空</string>
|
||||
<string name="sk_gif_badge">GIF</string>
|
||||
<string name="sk_settings_prefix_replies_always">回复至任何人</string>
|
||||
@@ -370,29 +370,29 @@
|
||||
<string name="sk_tab_profile">个人资料</string>
|
||||
<string name="sk_settings_show_labels_in_navigation_bar">在导航栏中显示选项卡标签</string>
|
||||
<string name="sk_reacted_with">%1$s 回应了 %2$s</string>
|
||||
<string name="sk_bubble_timeline_info_banner">这些都是实例管理员从网络中最新精选出来的帖文。</string>
|
||||
<string name="sk_bubble_timeline_info_banner">这些都是实例管理员从网络中最新精选出来的嘟文。</string>
|
||||
<string name="sk_search_fediverse">搜索联邦宇宙</string>
|
||||
<string name="sk_time_seconds">%d 秒</string>
|
||||
<string name="sk_settings_show_emoji_reactions_only_opened">仅当帖文被打开时</string>
|
||||
<string name="sk_settings_show_emoji_reactions_only_opened">仅当嘟文被打开时</string>
|
||||
<string name="sk_time_hours">%d 时</string>
|
||||
<string name="sk_settings_show_emoji_reactions_hide_empty">隐藏空的表情回应</string>
|
||||
<string name="sk_settings_show_emoji_reactions">在时间线中显示表情回应</string>
|
||||
<plurals name="sk_posts_count_label">
|
||||
<item quantity="other">帖文</item>
|
||||
<item quantity="other">嘟文</item>
|
||||
</plurals>
|
||||
<string name="sk_suicide_search_terms">自杀</string>
|
||||
<string name="sk_load_missing_posts_above">加载较新的帖文</string>
|
||||
<string name="sk_load_missing_posts_above">加载较新的嘟文</string>
|
||||
<string name="sk_time_days">%d 天</string>
|
||||
<string name="sk_settings_show_emoji_reactions_always">始终显示添加按钮</string>
|
||||
<string name="sk_search_suicide_hotlines">查找求助热线</string>
|
||||
<string name="sk_do_not_show_again">下次不再显示</string>
|
||||
<string name="sk_trending_posts_info_banner">这些帖文正在联邦宇宙上引起关注。</string>
|
||||
<string name="sk_load_missing_posts_below">加载较旧的帖文</string>
|
||||
<string name="sk_trending_posts_info_banner">这些嘟文正在联邦宇宙上引起关注。</string>
|
||||
<string name="sk_load_missing_posts_below">加载较旧的嘟文</string>
|
||||
<string name="sk_search_suicide_title">以防你遇到困难…</string>
|
||||
<string name="sk_time_minutes">%d 分</string>
|
||||
<string name="sk_search_suicide_message">如果你正在寻找一个不自杀的迹象,这就是。如果你遇到困难,请考虑拨打当地的自杀热线。</string>
|
||||
<string name="sk_trending_links_info_banner">这些新闻故事正在联邦宇宙上被讨论。</string>
|
||||
<string name="sk_post_contains_media">帖文包含媒体</string>
|
||||
<string name="sk_post_contains_media">嘟文包含媒体</string>
|
||||
<string name="sk_blocked_accounts">已屏蔽账号</string>
|
||||
<string name="sk_muted_accounts">已静音账号</string>
|
||||
<string name="sk_settings_like_icon">使用心形作为收藏图标</string>
|
||||
|
||||
@@ -60,4 +60,41 @@
|
||||
<string name="sk_color_palette_yellow">黃色</string>
|
||||
<string name="sk_settings_translation_availability_note_unavailable">%s 似乎不支援翻譯.</string>
|
||||
<string name="sk_lists_with_user">%s 所在的列表</string>
|
||||
<string name="sk_settings_color_palette_default">預設 (%s)</string>
|
||||
<plurals name="sk_posts_count_label">
|
||||
<item quantity="other">嘟文</item>
|
||||
</plurals>
|
||||
<string name="sk_quoting_user">引用自 %s</string>
|
||||
<string name="sk_example_domain">example.social</string>
|
||||
<string name="sk_settings_reply_visibility_all">所有回覆</string>
|
||||
<string name="sk_settings_reply_visibility">回覆能見度</string>
|
||||
<string name="sk_list">列表</string>
|
||||
<string name="sk_post_contains_media">包含媒體的嘟文</string>
|
||||
<string name="sk_clear_all_notifications_confirm_action">全部刪除</string>
|
||||
<string name="sk_copy_link_to_post">複製嘟文連結</string>
|
||||
<string name="sk_mark_as_draft">標記為草稿</string>
|
||||
<string name="sk_announcements">公告</string>
|
||||
<string name="sk_settings_enable_delete_notifications">允許刪除通知</string>
|
||||
<string name="sk_clear_all_notifications">清除所有通知</string>
|
||||
<string name="sk_settings_see_new_posts_button">「顯示新嘟文」按鈕</string>
|
||||
<string name="sk_edit_timelines">編輯時間軸</string>
|
||||
<string name="sk_quote_post">在新嘟文中引述</string>
|
||||
<string name="sk_blocked_accounts">已封鎖的帳號</string>
|
||||
<string name="sk_your_lists">您的清單</string>
|
||||
<string name="sk_settings_donate">捐款</string>
|
||||
<string name="sk_timeline_home">首頁</string>
|
||||
<string name="sk_settings_lock_account">手動審查新的跟隨者</string>
|
||||
<string name="sk_schedule_post">排定時間發佈</string>
|
||||
<string name="sk_timeline_federated">聯邦時間軸</string>
|
||||
<string name="sk_settings_instance">站臺</string>
|
||||
<string name="sk_timeline_local">此伺服器</string>
|
||||
<string name="sk_hashtag_timeline_local_only_switch">只顯示此伺服器的嘟文?</string>
|
||||
<string name="sk_settings_uniform_icon_for_notifications">所有通知使用一致的圖示</string>
|
||||
<string name="sk_muted_accounts">已靜音的帳號</string>
|
||||
<string name="sk_settings_single_notification">只顯示一則通知</string>
|
||||
<string name="sk_unsent_posts">未送出的嘟文</string>
|
||||
<string name="sk_attach_file">附加檔案</string>
|
||||
<string name="sk_search_fediverse">在聯邦宇宙中搜尋</string>
|
||||
<string name="sk_tab_notifications">通知</string>
|
||||
<string name="sk_settings_unifiedpush">使用 UnifiedPush</string>
|
||||
</resources>
|
||||
@@ -37,6 +37,7 @@
|
||||
<attr name="colorM3DarkOnSurface" format="color" />
|
||||
<attr name="colorTabBarAlpha" format="color" />
|
||||
<attr name="colorFilledCardAlpha" format="color" />
|
||||
<attr name="colorM3Success" format="color" />
|
||||
<attr name="toolbarActionButtonStyle" format="reference" />
|
||||
|
||||
<attr name="colorPrimary25" format="color" />
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
<item name="colorPoll">@color/bookmark_selected</item>
|
||||
<item name="colorTabBarAlpha">#14000000</item>
|
||||
<item name="colorFilledCardAlpha">#22000000</item>
|
||||
<item name="colorM3Success">#FF5b8e63</item>
|
||||
|
||||
<item name="colorM3DisabledBackground">#1F1F1F1F</item>
|
||||
<item name="colorM3Error">#B3261E</item>
|
||||
@@ -143,6 +144,7 @@
|
||||
<item name="colorM3OnErrorContainer">#F9DEDC</item>
|
||||
<item name="colorWhite">#000</item>
|
||||
<item name="colorSensitiveOverlay">#80000000</item>
|
||||
<item name="colorM3Success">#FF89bb9c</item>
|
||||
</style>
|
||||
|
||||
<style name="ColorPalette.Dark.TrueBlack">
|
||||
|
||||
@@ -244,6 +244,13 @@
|
||||
<string name="sk_icon_doctor">Doctor</string>
|
||||
<string name="sk_icon_diamond">Diamond</string>
|
||||
<string name="sk_icon_umbrella">Umbrella</string>
|
||||
<string name="sk_icon_water">Water</string>
|
||||
<string name="sk_icon_sun">Sun</string>
|
||||
<string name="sk_icon_sunset">Sunset</string>
|
||||
<string name="sk_icon_cloud">Cloud</string>
|
||||
<string name="sk_icon_thunderstorm">Thunderstorm</string>
|
||||
<string name="sk_icon_rain">Rain</string>
|
||||
<string name="sk_icon_snowflake">Snowflake</string>
|
||||
<string name="sk_edit_timeline">Edit timeline</string>
|
||||
<string name="sk_add_timeline">Add timeline</string>
|
||||
<string name="sk_edit_timelines">Edit timelines</string>
|
||||
@@ -417,4 +424,15 @@
|
||||
<string name="sk_settings_lock_account">Manually approve new followers</string>
|
||||
<string name="sk_settings_default_visibility">Default posting visibility</string>
|
||||
<string name="sk_button_mutuals">Mutuals</string>
|
||||
<string name="sk_private_note_hint">Add a personal note about this profile</string>
|
||||
<string name="sk_confirm_changes">Confirm changes</string>
|
||||
<string name="sk_private_note_update_failed">Failed to save note</string>
|
||||
<string name="sk_private_note_confirm_delete">Delete personal note about %s?</string>
|
||||
<string name="sk_delete_note">Delete personal note</string>
|
||||
<string name="sk_add_note">Add personal note</string>
|
||||
<string name="sk_settings_copy_crash_log">Copy latest crash log</string>
|
||||
<string name="sk_settings_crash_log_unavailable">None available… yet</string>
|
||||
<string name="sk_crash_log_copied">Crash log copied</string>
|
||||
<string name="sk_open_post_preview">Preview post</string>
|
||||
<string name="sk_post_preview">Preview</string>
|
||||
</resources>
|
||||
6
metadata/en-US/changelogs/109.txt
Normal file
6
metadata/en-US/changelogs/109.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
- The latest crash log is now kept and ready to be copied from the "About Megalodon" settings page
|
||||
- Add and display personal notes on profiles
|
||||
- Text modifications are now highlighted in the edit history
|
||||
- Akkoma: Quoted posts are displayed in the timeline; translation; preview posts before publishing
|
||||
- Added Bookmarks and Your favorites as pinnable timelines
|
||||
- Various bugfixes
|
||||
6
metadata/uk/changelogs/108.txt
Normal file
6
metadata/uk/changelogs/108.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
- Нарешті: довгоочікуване оновлення інтерфейсу Material 3!
|
||||
- Прогалини в домашній стрічці тепер дозволяють вибирати, завантажувати нові чи старі дані
|
||||
- Для серверних оголошень і користувачів Akkoma: реакції емоджі!
|
||||
- Нещодавно використані емоджі в інструменті вибору емоджі
|
||||
- Займенники тепер відображаються поруч із іменами
|
||||
- Багато виправлень та нових помилок, які ви можете виявити ✨
|
||||
@@ -1,4 +1,4 @@
|
||||
Megalodon — це модифікована версія <a href="https://github.com/mastodon/mastodon-android">офіційного застосунку Mastodon для Android</a>, який додає важливі функції, яких немає в офіційному застосунку, наприклад федеративна стрічка, приховані дописи, а також переглядач опису зображення.
|
||||
Megalodon — це модифікована версія <a href="https://github.com/mastodon/mastodon-android">офіційного застосунку Mastodon для Android</a>, яка додає важливі функції, яких немає в офіційному застосунку, наприклад федеративна стрічка, приховані дописи, а також переглядач опису зображення.
|
||||
|
||||
<b>Ключові функції</b>
|
||||
|
||||
@@ -6,11 +6,8 @@ Megalodon — це модифікована версія <a href="https://github
|
||||
- <b>Федеративна стрічка</b>: Переглядайте всі загальнодоступні дописи від людей зі всього Федівсесвіту!
|
||||
- <b>Власні стрічки</b>: Закріпіть будь-який список або хештег на домашній вкладці Megalodon, щоб просто переходити між улюбленими темами та людьми!
|
||||
- <b>Чернетки й заплановані дописи</b>: Дозволяє підготувати допис і запланувати його автонадсилання у вказаний час.
|
||||
- <b>Закріплення дописів</b>: Закріплюйте ваші найважливіші дописи у своєму профілі та переглядайте, що закріпили люди у вкладці «Закріплене».
|
||||
- <b>Слідкуйте за хештегами</b>: Переглядайте нові дописи з певними хештегами безпосередньо у своїй домашній стрічці, підписавшись на них.
|
||||
- <b>Відповідайте на запити на стеження</b>: Погоджуйте або відхиляйте запити на стеження зі сповіщення чи виділеного списку «Запити на стеження».
|
||||
- <b>Видалити та переробити</b>: Улюблена функція, яка уможливила редагування дописів без фактичної функції редагування.
|
||||
- <b>Вибір мови selector</b>: Зручний вибір мови для кожного допису для коректної роботи фільтрів і перекладу.
|
||||
- <b>Переклад</b>: Легко перекладайте дописи прямо в Megalodon! Працює лише за умови, що ця функція також доступна у вашому Mastodon для браузера.
|
||||
- <b>Покажчик видимості допису</b>: Під час відкриття або відповіді на допис показуватиметься зручна піктограма, яка вказує на видимість допису.
|
||||
- <b>Колірні теми</b>: Якщо вам не подобається усталений рожевий колір (акулка мовчазно засуджує вас), кольорові теми Moshidon пропонують вам інші варіанти.
|
||||
- <b>Краща сумісність</b>: Для тих, хто працює на серверах Glitch або Akkoma, Megalodon надає різні додаткові функції та покращення.
|
||||
|
||||
6
metadata/zh-Hans/changelogs/76.txt
Normal file
6
metadata/zh-Hans/changelogs/76.txt
Normal file
@@ -0,0 +1,6 @@
|
||||
- 改进的过滤器,包括“隐藏警告”兼容性
|
||||
- 重新设计的个人资料页面,元数据直接位于个人简介下方
|
||||
- 对于很长的帖子可以折叠或展开
|
||||
- 自动在回复的内容警告信息前加上“re:”的选项
|
||||
- 隐藏时间线中交互按钮的选项
|
||||
- 各种错误修正、调整和改进
|
||||
4
metadata/zh-Hans/changelogs/77.txt
Normal file
4
metadata/zh-Hans/changelogs/77.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
- 长按 "关注 "按钮可关注其他账户中的配置文件
|
||||
- 在其他账户中打开配置文件的选项
|
||||
- 向下滚动时间轴时自动隐藏撰写按钮
|
||||
- 修复打开服务器管理员配置文件时的崩溃问题
|
||||
5
metadata/zh-Hans/changelogs/83.txt
Normal file
5
metadata/zh-Hans/changelogs/83.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
- 直接从通知栏点赞、收藏和回复
|
||||
- 时间线中转发和回复的标题更美观、更一致
|
||||
- 通知点(尚未实际加载通知)
|
||||
- 针对 Akkoma 用户: 回复可见性、主题回复排序、引用...
|
||||
- 崩溃修复和细微调整
|
||||
8
metadata/zh-Hans/changelogs/94.txt
Normal file
8
metadata/zh-Hans/changelogs/94.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
- 改进后更清晰的主题视图
|
||||
- 通过共享网站到 Megalodon 打开账户/帖子
|
||||
- 从源实例加载追随者/收藏夹/......列表
|
||||
- 改进非Mastodon服务器的兼容性
|
||||
- 设置帖子内容类型的选项
|
||||
- 支持从 Pixel 设备上的 "Recent" 应用程序复制 URL
|
||||
- Auto-reveal equal CWs in threads
|
||||
- 错误修复和用户界面改进
|
||||
Reference in New Issue
Block a user