Merge branch 'develop'
|
@ -8,13 +8,13 @@ plugins {
|
|||
}
|
||||
def flavor
|
||||
android {
|
||||
compileSdk 32
|
||||
compileSdk 33
|
||||
|
||||
defaultConfig {
|
||||
minSdk 21
|
||||
targetSdk 32
|
||||
versionCode 450
|
||||
versionName "3.12.0"
|
||||
targetSdk 33
|
||||
versionCode 454
|
||||
versionName "3.13.0"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
flavorDimensions "default"
|
||||
|
@ -106,7 +106,7 @@ dependencies {
|
|||
|
||||
|
||||
implementation 'com.burhanrashid52:photoeditor:1.5.1'
|
||||
implementation project(path: ':cropper')
|
||||
implementation("com.vanniktech:android-image-cropper:4.3.3")
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:4.12.0"
|
||||
implementation 'jp.wasabeef:glide-transformations:4.3.0'
|
||||
implementation 'com.github.penfeizhou.android.animation:glide-plugin:2.24.0'
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
@ -213,7 +214,7 @@
|
|||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/scheduled" />
|
||||
<activity
|
||||
android:name="com.theartofdev.edmodo.cropper.CropImageActivity"
|
||||
android:name="com.canhub.cropper.CropImageActivity"
|
||||
android:theme="@style/Base.Theme.AppCompat" />
|
||||
|
||||
|
||||
|
|
8206
app/src/main/assets/quotes/famous.json
Normal file
|
@ -1,4 +1,24 @@
|
|||
[
|
||||
{
|
||||
"version": "3.13.0",
|
||||
"code": "454",
|
||||
"note": "Added:\n- Post random quotes\n- Group reblogs in home timeline\n- Rename Nitter timelines\n- Android 13 support\n- Pagination with search / trending\n- Allow to remove left margin in messages (default: disabled)\n\nChanged:\n- Display translate button only when language is different\n- Respect blank spaces between words in messages\n- Focus button more accessible when editing media\n- Visual feedback for block on account list\n- Visual changes with compose / top bar\n- Use custom Nitter timeline name in manage timelines\n\nFixed:\n- Behavior with cw toggle\n- Truncated gimini links\n- Nav buttons not visible with media (Light theme)\n- Status bar with Android 5\n- Fix links not clickable\n- Fix deep links\n- Fix remote threads not fetched for some instances\n- Adding description to shared media\n- Open with another accounts\n- Chars size not respected for Android 5-6\n- Wrong instance fetched for instances.social\n- Bouncing Timeline on refresh\n- Links to mentions, tags, urls, not visible.\n- Custom channel sounds not applied\n- users with short username are not linked\n- Fix crashes"
|
||||
},
|
||||
{
|
||||
"version": "3.12.3",
|
||||
"code": "453",
|
||||
"note": "Added:\n- Pagination with search / trending\n\nFixed:\n- Long press on Nitter tabs\n- Open with another accounts\n- Chars size not respected for Android 5-6\n- Wrong instance fetched for instances.social"
|
||||
},
|
||||
{
|
||||
"version": "3.12.2",
|
||||
"code": "452",
|
||||
"note": "Added:\n- Rename Nitter timelines\n- Android 13 support\n\nChanged:\n- Visual feedback for block on account list\n- Visual changes with compose / top bar\n\nFixed:\n- Nav buttons not visible with media (Light theme)\n- Status bar with Android 5\n- Fix links not clickable\n- Fix deep links\n- Fix remote threads not fetched for some instances\n- Adding description to shared media\n- Fix crashes"
|
||||
},
|
||||
{
|
||||
"version": "3.12.1",
|
||||
"code": "451",
|
||||
"note": "Added:\n- Post random quotes\n- Group reblogs in home timeline\n\nChanged:\n- Display translate button only when language is different\n- Respect blank spaces between words in messages\n- Focus button more accessible when editing media\n\nFixed:\n- Behavior with cw toggle\n- Truncated gimini links"
|
||||
},
|
||||
{
|
||||
"version": "3.12.0",
|
||||
"code": "450",
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
base_theme,1
|
||||
author,acn128
|
||||
name,ACN
|
||||
theme_boost_header_color,-727060
|
||||
theme_text_header_1_line,-10865760
|
||||
theme_text_header_2_line,-11719543
|
||||
theme_statuses_color,-197380
|
||||
theme_link_color,-13681272
|
||||
theme_icons_color,-15395562
|
||||
pref_color_background,-1
|
||||
pref_color_navigation_bar,true
|
||||
pref_color_status_bar,true
|
||||
theme_accent,-14059009
|
||||
theme_text_color,-13027015
|
||||
theme_primary,-1
|
|
|
@ -1,13 +0,0 @@
|
|||
base_theme,2
|
||||
author,Fedilab
|
||||
name,Breeze Dark - Yellow
|
||||
theme_boost_header_color,-14012878
|
||||
theme_statuses_color,-14473687
|
||||
theme_link_color,-12734743
|
||||
theme_icons_color,-4340793
|
||||
pref_color_background,-15658735
|
||||
pref_color_navigation_bar,true
|
||||
pref_color_status_bar,true
|
||||
theme_accent,-148405
|
||||
theme_text_color,-1052431
|
||||
theme_primary,-13552069
|
|
|
@ -1,15 +0,0 @@
|
|||
base_theme,2
|
||||
author,Roboron
|
||||
name,Cyberpunk Neon
|
||||
theme_boost_header_color,-16776697,
|
||||
theme_text_header_1_line,-1441575,
|
||||
theme_text_header_2_line,-5242717,
|
||||
theme_statuses_color,-16181197,
|
||||
theme_link_color,-1441575,
|
||||
theme_icons_color,-16138810,
|
||||
pref_color_background,-16774370,
|
||||
pref_color_navigation_bar,true,
|
||||
pref_color_status_bar,true,
|
||||
theme_accent,-1441575,
|
||||
theme_text_color,-16138810,
|
||||
theme_primary,-16774370,
|
|
|
@ -1,15 +0,0 @@
|
|||
base_theme,0
|
||||
author,S1m
|
||||
name,Dark Elephant
|
||||
theme_boost_header_color,-13552317
|
||||
theme_text_header_1_line,-3479297
|
||||
theme_text_header_2_line,-7287815
|
||||
theme_statuses_color,-13552317
|
||||
theme_link_color,-11098143
|
||||
theme_icons_color,-789517
|
||||
pref_color_background,-14144456
|
||||
pref_color_navigation_bar,true
|
||||
pref_color_status_bar,true
|
||||
theme_accent,-13922086
|
||||
theme_text_color,-789517
|
||||
theme_primary,-14144456
|
|
|
@ -1,15 +0,0 @@
|
|||
base_theme,2
|
||||
author,Jøta Seth
|
||||
name,Grey Orange
|
||||
theme_boost_header_color,-14869219
|
||||
theme_text_header_1_line,-1
|
||||
theme_text_header_2_line,-1
|
||||
theme_statuses_color,-14145496
|
||||
theme_link_color,-26624
|
||||
theme_icons_color,-26624
|
||||
pref_color_background,-13092808
|
||||
pref_color_navigation_bar,true
|
||||
pref_color_status_bar,true
|
||||
theme_accent,-26624
|
||||
theme_text_color,-1
|
||||
theme_primary,-14408668
|
|
|
@ -1,15 +0,0 @@
|
|||
base_theme,2
|
||||
author,@AntoineD@h.kher.nl
|
||||
name,Gruvbox OLED
|
||||
theme_boost_header_color,-16777216
|
||||
theme_text_header_1_line,-265785
|
||||
theme_text_header_2_line,-6777062
|
||||
theme_statuses_color,-16777216
|
||||
theme_link_color,-2647775
|
||||
theme_icons_color,-7175308
|
||||
pref_color_background,-16777216
|
||||
pref_color_navigation_bar,true
|
||||
pref_color_status_bar,true
|
||||
theme_accent,-9921174
|
||||
theme_text_color,-265785
|
||||
theme_primary,-16777216
|
|
|
@ -1,15 +0,0 @@
|
|||
base_theme,2
|
||||
author,AngryTux
|
||||
name,Less Angry Orange
|
||||
theme_boost_header_color,-15855063
|
||||
theme_text_header_1_line,-2128640
|
||||
theme_text_header_2_line,-5329234
|
||||
theme_statuses_color,-1
|
||||
theme_link_color,-12146699
|
||||
theme_icons_color,-2128640
|
||||
pref_color_background,-15987700
|
||||
pref_color_navigation_bar,true
|
||||
pref_color_status_bar,true
|
||||
theme_accent,-3968000
|
||||
theme_text_color,-197380
|
||||
theme_primary,-14408668
|
|
|
@ -1,15 +0,0 @@
|
|||
base_theme,2
|
||||
author,Mondstern
|
||||
name,Mondstern Fedilab
|
||||
theme_boost_header_color,-1,
|
||||
theme_text_header_1_line,-13855804,
|
||||
theme_text_header_2_line,-16227945,
|
||||
theme_statuses_color,-14935012,
|
||||
theme_link_color,-15542685,
|
||||
theme_icons_color,-10723999,
|
||||
pref_color_background,-15921907,
|
||||
pref_color_navigation_bar,false,
|
||||
pref_color_status_bar,false,
|
||||
theme_accent,-15542685,
|
||||
theme_text_color,-1,
|
||||
theme_primary,-14474461,
|
|
|
@ -1,13 +0,0 @@
|
|||
base_theme,2
|
||||
author,Fedilab
|
||||
name,Nocturnal
|
||||
theme_boost_header_color,-12895429
|
||||
theme_statuses_color,-13553359
|
||||
theme_link_color,-16747570
|
||||
theme_icons_color,-10158118
|
||||
pref_color_background,-14606047
|
||||
pref_color_navigation_bar,true
|
||||
pref_color_status_bar,true
|
||||
theme_accent,-13136013
|
||||
theme_text_color,-2236963
|
||||
theme_primary,-14013910
|
|
|
@ -1,15 +0,0 @@
|
|||
base_theme,2
|
||||
author,Jøta Seth
|
||||
name,Photon Dark
|
||||
theme_boost_header_color,-14145496
|
||||
theme_text_header_1_line,-1
|
||||
theme_text_header_2_line,-1
|
||||
theme_statuses_color,-14935012
|
||||
theme_link_color,-14059009
|
||||
theme_icons_color,-9474193
|
||||
pref_color_background,-15921907
|
||||
pref_color_navigation_bar,true
|
||||
pref_color_status_bar,true
|
||||
theme_accent,-14059009
|
||||
theme_text_color,-1
|
||||
theme_primary,-14474461
|
|
|
@ -1,15 +0,0 @@
|
|||
base_theme,1
|
||||
author,Animaly
|
||||
name,Solarized - Cyan Accent
|
||||
theme_boost_header_color,-69167
|
||||
theme_text_header_1_line,-4880128
|
||||
theme_text_header_2_line,-7102047
|
||||
theme_statuses_color,-133405
|
||||
theme_link_color,-14251054
|
||||
theme_icons_color,-10106727
|
||||
pref_color_background,-1120043
|
||||
pref_color_navigation_bar,true
|
||||
pref_color_status_bar,true
|
||||
theme_accent,-8021760
|
||||
theme_text_color,-16766154
|
||||
theme_primary,-1120043
|
|
|
@ -1,15 +0,0 @@
|
|||
base_theme,2
|
||||
author,Fedilab
|
||||
name,Solarized Dark - Purple
|
||||
theme_boost_header_color,-16506327
|
||||
theme_text_header_1_line,-1120043
|
||||
theme_text_header_2_line,-1120043
|
||||
theme_statuses_color,-16304574
|
||||
theme_link_color,-14251054
|
||||
theme_icons_color,-7102047
|
||||
pref_color_background,-16766154
|
||||
pref_color_navigation_bar,true
|
||||
pref_color_status_bar,true
|
||||
theme_accent,-9670204
|
||||
theme_text_color,-133405
|
||||
theme_primary,-16304574
|
|
|
@ -1,47 +0,0 @@
|
|||
[
|
||||
{
|
||||
"theme_name": "Dark",
|
||||
"base_theme": "DARK",
|
||||
"primary": "#FF272727",
|
||||
"primary_dark": "#FF272727",
|
||||
"primary_light": "#FFd9e1e8",
|
||||
"accent": "#FF2b90d9",
|
||||
"accent_dark": "#FF1b80c9",
|
||||
"accent_light": "#FF772b90d9",
|
||||
"background": "#FF272727",
|
||||
"background_dark": "#FF282c37",
|
||||
"background_light": "#FF282c37",
|
||||
"should_tint_statusbar": true,
|
||||
"should_tint_navbar": true
|
||||
},
|
||||
{
|
||||
"theme_name": "Light",
|
||||
"base_theme": "LIGHT",
|
||||
"primary": "#FFFFFF",
|
||||
"primary_dark": "#FFFFFFFF",
|
||||
"primary_light": "#FFd9e1e8",
|
||||
"accent": "#FF2b90d9",
|
||||
"accent_dark": "#FF1b80c9",
|
||||
"accent_light": "#FF772b90d9",
|
||||
"background": "#FFFFFFFF",
|
||||
"background_dark": "#FFFFFFFF",
|
||||
"background_light": "#FFFFFFFF",
|
||||
"should_tint_statusbar": true,
|
||||
"should_tint_navbar": true
|
||||
},
|
||||
{
|
||||
"theme_name": "Black",
|
||||
"base_theme": "DARK",
|
||||
"primary": "#FF000000",
|
||||
"primary_dark": "#FF000000",
|
||||
"primary_light": "#FF000000",
|
||||
"accent": "#FF606984",
|
||||
"accent_dark": "#FF606984",
|
||||
"accent_light": "#FF606984",
|
||||
"background": "#FF000000",
|
||||
"background_dark": "#FF000000",
|
||||
"background_light": "#FF000000",
|
||||
"should_tint_statusbar": true,
|
||||
"should_tint_navbar": true
|
||||
}
|
||||
]
|
|
@ -1076,8 +1076,12 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
|
|||
Uri imageUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||
if (imageUri != null) {
|
||||
Bundle b = new Bundle();
|
||||
b.putParcelable(Helper.ARG_SHARE_URI, imageUri);
|
||||
CrossActionHelper.doCrossShare(BaseMainActivity.this, b);
|
||||
List<Uri> uris = new ArrayList<>();
|
||||
uris.add(imageUri);
|
||||
Helper.createAttachmentFromUri(BaseMainActivity.this, uris, attachments -> {
|
||||
b.putSerializable(Helper.ARG_MEDIA_ATTACHMENTS, new ArrayList<>(attachments));
|
||||
CrossActionHelper.doCrossShare(BaseMainActivity.this, b);
|
||||
});
|
||||
} else {
|
||||
Toasty.warning(BaseMainActivity.this, getString(R.string.toast_error), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
|
@ -1087,7 +1091,10 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
|
|||
ArrayList<Uri> imageList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
||||
if (imageList != null) {
|
||||
Bundle b = new Bundle();
|
||||
b.putParcelableArrayList(Helper.ARG_SHARE_URI_LIST, imageList);
|
||||
Helper.createAttachmentFromUri(BaseMainActivity.this, imageList, attachments -> {
|
||||
b.putSerializable(Helper.ARG_MEDIA_ATTACHMENTS, new ArrayList<>(attachments));
|
||||
CrossActionHelper.doCrossShare(BaseMainActivity.this, b);
|
||||
});
|
||||
CrossActionHelper.doCrossShare(BaseMainActivity.this, b);
|
||||
} else {
|
||||
Toasty.warning(BaseMainActivity.this, getString(R.string.toast_error), Toast.LENGTH_LONG).show();
|
||||
|
@ -1118,8 +1125,8 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
|
|||
}
|
||||
//Here we know that the intent contains a valid URL
|
||||
if (!url.contains("medium.com")) {
|
||||
Pattern link = Pattern.compile("https?://([\\da-z.-]+\\.[a-z.]{2,10})/(@[\\w._-]*[0-9]*)(/[0-9]+)?$");
|
||||
Matcher matcherLink = null;
|
||||
Pattern link = Pattern.compile("https?://([\\da-z.-]+[à-ü]?\\.[a-z.]{2,10})/(@[\\w._-]*[0-9]*)(/[0-9]+)?$");
|
||||
Matcher matcherLink;
|
||||
matcherLink = link.matcher(url);
|
||||
if (matcherLink.find()) {
|
||||
if (currentAccount == null) {
|
||||
|
@ -1137,10 +1144,14 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
|
|||
CrossActionHelper.fetchRemoteStatus(BaseMainActivity.this, currentAccount, url, new CrossActionHelper.Callback() {
|
||||
@Override
|
||||
public void federatedStatus(Status status) {
|
||||
Intent intent = new Intent(BaseMainActivity.this, ContextActivity.class);
|
||||
intent.putExtra(Helper.ARG_STATUS, status);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
if (status != null) {
|
||||
Intent intent = new Intent(BaseMainActivity.this, ContextActivity.class);
|
||||
intent.putExtra(Helper.ARG_STATUS, status);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
Toasty.error(BaseMainActivity.this, getString(R.string.toast_error), Toasty.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1155,12 +1166,16 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt
|
|||
|
||||
@Override
|
||||
public void federatedAccount(app.fedilab.android.client.entities.api.Account account) {
|
||||
Intent intent = new Intent(BaseMainActivity.this, ProfileActivity.class);
|
||||
Bundle b = new Bundle();
|
||||
b.putSerializable(Helper.ARG_ACCOUNT, account);
|
||||
intent.putExtras(b);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
if (account != null) {
|
||||
Intent intent = new Intent(BaseMainActivity.this, ProfileActivity.class);
|
||||
Bundle b = new Bundle();
|
||||
b.putSerializable(Helper.ARG_ACCOUNT, account);
|
||||
intent.putExtras(b);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intent);
|
||||
} else {
|
||||
Toasty.error(BaseMainActivity.this, getString(R.string.toast_error), Toasty.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -19,8 +19,11 @@ import android.annotation.SuppressLint;
|
|||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
@ -125,9 +128,15 @@ public class BaseActivity extends AppCompatActivity {
|
|||
}
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
if (android.os.Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
|
||||
ThemeHelper.adjustFontScale(this, getResources().getConfiguration());
|
||||
}
|
||||
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
Window window = getWindow();
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
||||
window.setStatusBarColor(Color.BLACK);
|
||||
window.setNavigationBarColor(Color.BLACK);
|
||||
}
|
||||
Helper.setLocale(this);
|
||||
}
|
||||
|
||||
|
|
|
@ -19,8 +19,11 @@ import android.annotation.SuppressLint;
|
|||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
@ -111,6 +114,12 @@ public class BaseBarActivity extends AppCompatActivity {
|
|||
}
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
Window window = getWindow();
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
||||
window.setStatusBarColor(Color.BLACK);
|
||||
window.setNavigationBarColor(Color.BLACK);
|
||||
}
|
||||
if (android.os.Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
|
||||
ThemeHelper.adjustFontScale(this, getResources().getConfiguration());
|
||||
}
|
||||
|
|
|
@ -19,8 +19,11 @@ import android.annotation.SuppressLint;
|
|||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
@ -111,6 +114,12 @@ public class BaseTransparentActivity extends AppCompatActivity {
|
|||
}
|
||||
}
|
||||
super.onCreate(savedInstanceState);
|
||||
if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
|
||||
Window window = getWindow();
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
||||
window.setStatusBarColor(Color.BLACK);
|
||||
window.setNavigationBarColor(Color.BLACK);
|
||||
}
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) {
|
||||
ThemeHelper.adjustFontScale(this, getResources().getConfiguration());
|
||||
}
|
||||
|
|
|
@ -109,6 +109,7 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana
|
|||
private ComposeAdapter composeAdapter;
|
||||
private boolean promptSaveDraft;
|
||||
private boolean restoredDraft;
|
||||
private List<Attachment> sharedAttachments;
|
||||
|
||||
|
||||
private final BroadcastReceiver imageReceiver = new BroadcastReceiver() {
|
||||
|
@ -126,6 +127,7 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana
|
|||
if (focusX != -2) {
|
||||
attachment.focus = focusX + "," + focusY;
|
||||
}
|
||||
|
||||
composeAdapter.notifyItemChanged(position);
|
||||
break;
|
||||
}
|
||||
|
@ -146,8 +148,6 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana
|
|||
private app.fedilab.android.client.entities.api.Account accountMention;
|
||||
private String statusReplyId;
|
||||
private app.fedilab.android.client.entities.api.Account mentionBooster;
|
||||
private ArrayList<Uri> sharedUriList = new ArrayList<>();
|
||||
private Uri sharedUri;
|
||||
private String sharedSubject, sharedContent, sharedTitle, sharedDescription, shareURL, sharedUrlMedia;
|
||||
private String editMessageId;
|
||||
|
||||
|
@ -482,8 +482,7 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana
|
|||
mentionBooster = (app.fedilab.android.client.entities.api.Account) b.getSerializable(Helper.ARG_MENTION_BOOSTER);
|
||||
accountMention = (app.fedilab.android.client.entities.api.Account) b.getSerializable(Helper.ARG_ACCOUNT_MENTION);
|
||||
//Shared elements
|
||||
sharedUriList = b.getParcelableArrayList(Helper.ARG_SHARE_URI_LIST);
|
||||
sharedUri = b.getParcelable(Helper.ARG_SHARE_URI);
|
||||
sharedAttachments = (ArrayList<Attachment>) b.getSerializable(Helper.ARG_MEDIA_ATTACHMENTS);
|
||||
sharedUrlMedia = b.getString(Helper.ARG_SHARE_URL_MEDIA);
|
||||
sharedSubject = b.getString(Helper.ARG_SHARE_SUBJECT, null);
|
||||
sharedContent = b.getString(Helper.ARG_SHARE_CONTENT, null);
|
||||
|
@ -677,18 +676,19 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana
|
|||
}, 0, 10000);
|
||||
}
|
||||
|
||||
if (sharedUriList != null && sharedUriList.size() > 0) {
|
||||
List<Uri> uris = new ArrayList<>(sharedUriList);
|
||||
Helper.createAttachmentFromUri(ComposeActivity.this, uris, attachment -> {
|
||||
if (sharedAttachments != null && sharedAttachments.size() > 0) {
|
||||
for (Attachment attachment : sharedAttachments) {
|
||||
composeAdapter.addAttachment(-1, attachment);
|
||||
});
|
||||
} else if (sharedUri != null && !sharedUri.toString().startsWith("http")) {
|
||||
}
|
||||
} /*else if (sharedUri != null && !sharedUri.toString().startsWith("http")) {
|
||||
List<Uri> uris = new ArrayList<>();
|
||||
uris.add(sharedUri);
|
||||
Helper.createAttachmentFromUri(ComposeActivity.this, uris, attachment -> {
|
||||
composeAdapter.addAttachment(-1, attachment);
|
||||
Helper.createAttachmentFromUri(ComposeActivity.this, uris, attachments -> {
|
||||
for(Attachment attachment: attachments) {
|
||||
composeAdapter.addAttachment(-1, attachment);
|
||||
}
|
||||
});
|
||||
} else if (shareURL != null) {
|
||||
} */ else if (shareURL != null) {
|
||||
|
||||
Helper.download(ComposeActivity.this, sharedUrlMedia, new OnDownloadInterface() {
|
||||
@Override
|
||||
|
|
|
@ -34,6 +34,8 @@ import androidx.fragment.app.Fragment;
|
|||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
@ -184,11 +186,18 @@ public class ContextActivity extends BaseActivity implements FragmentMastodonCon
|
|||
return true;
|
||||
}
|
||||
if (firstMessage.account.acct != null) {
|
||||
String[] splitAcct = firstMessage.account.acct.split("@");
|
||||
String instance;
|
||||
if (splitAcct.length > 1) {
|
||||
instance = splitAcct[1];
|
||||
} else {
|
||||
String instance = null;
|
||||
try {
|
||||
URL url = new URL(firstMessage.uri);
|
||||
instance = url.getHost();
|
||||
} catch (MalformedURLException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (instance == null) {
|
||||
Toasty.info(ContextActivity.this, getString(R.string.toast_error_fetch_message), Toasty.LENGTH_SHORT).show();
|
||||
return true;
|
||||
}
|
||||
if (instance.equalsIgnoreCase(MainActivity.currentInstance)) {
|
||||
Toasty.info(ContextActivity.this, getString(R.string.toast_on_your_instance), Toasty.LENGTH_SHORT).show();
|
||||
return true;
|
||||
}
|
||||
|
@ -200,11 +209,12 @@ public class ContextActivity extends BaseActivity implements FragmentMastodonCon
|
|||
}
|
||||
if (remoteId != null) {
|
||||
StatusesVM statusesVM = new ViewModelProvider(ContextActivity.this).get(StatusesVM.class);
|
||||
String finalInstance = instance;
|
||||
statusesVM.getStatus(instance, null, remoteId).observe(ContextActivity.this, status -> {
|
||||
if (status != null) {
|
||||
Intent intentContext = new Intent(ContextActivity.this, ContextActivity.class);
|
||||
intentContext.putExtra(Helper.ARG_STATUS, status);
|
||||
intentContext.putExtra(Helper.ARG_REMOTE_INSTANCE, instance);
|
||||
intentContext.putExtra(Helper.ARG_REMOTE_INSTANCE, finalInstance);
|
||||
intentContext.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
startActivity(intentContext);
|
||||
} else {
|
||||
|
|
|
@ -81,6 +81,12 @@ public class InstanceHealthActivity extends BaseAlertDialogActivity {
|
|||
instanceSocialVM.getInstances(BaseMainActivity.currentInstance.trim()).observe(InstanceHealthActivity.this, instanceSocialList -> {
|
||||
if (instanceSocialList != null && instanceSocialList.instances.size() > 0) {
|
||||
InstanceSocial.Instance instanceSocial = instanceSocialList.instances.get(0);
|
||||
for (InstanceSocial.Instance instance : instanceSocialList.instances) {
|
||||
if (instance.name.equalsIgnoreCase(BaseMainActivity.currentInstance.trim())) {
|
||||
instanceSocial = instance;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (instanceSocial.thumbnail != null && !instanceSocial.thumbnail.equals("null"))
|
||||
Glide.with(InstanceHealthActivity.this)
|
||||
.asBitmap()
|
||||
|
|
|
@ -495,10 +495,10 @@ public class MediaActivity extends BaseTransparentActivity implements OnDownload
|
|||
// except for the ones that make the content appear under the system bars.
|
||||
private void showSystemUI() {
|
||||
View decorView = getWindow().getDecorView();
|
||||
decorView.setSystemUiVisibility(
|
||||
decorView.setSystemUiVisibility(flags |
|
||||
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
|
||||
}
|
||||
|
||||
public FragmentMedia getCurrentFragment() {
|
||||
|
|
|
@ -251,7 +251,7 @@ public interface MastodonStatusesService {
|
|||
@Part MultipartBody.Part file,
|
||||
@Part MultipartBody.Part thumbnail,
|
||||
@Part("description") RequestBody description,
|
||||
@Part("focus") String focus
|
||||
@Part("focus") RequestBody focus
|
||||
);
|
||||
|
||||
//Edit a Media
|
||||
|
|
|
@ -62,7 +62,9 @@ public interface MastodonTimelinesService {
|
|||
|
||||
|
||||
@GET("trends/tags")
|
||||
Call<List<Tag>> getTagTrends(@Header("Authorization") String token);
|
||||
Call<List<Tag>> getTagTrends(@Header("Authorization") String token,
|
||||
@Query("offset") Integer offset,
|
||||
@Query("limit") Integer limit);
|
||||
|
||||
//Public Tags timelines
|
||||
@GET("timelines/tag/{hashtag}")
|
||||
|
|
|
@ -19,4 +19,5 @@ public class Pagination {
|
|||
public String max_id;
|
||||
public String min_id;
|
||||
public String since_id;
|
||||
public Integer offset;
|
||||
}
|
||||
|
|
|
@ -24,5 +24,6 @@ public class Results {
|
|||
public java.util.List<Status> statuses;
|
||||
@SerializedName("hashtags")
|
||||
public java.util.List<Tag> hashtags;
|
||||
public Pagination pagination;
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
package app.fedilab.android.client.entities.app;
|
||||
/* Copyright 2022 Thomas Schneider
|
||||
*
|
||||
* This file is a part of Fedilab
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or modify it under the terms of the
|
||||
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Fedilab is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Fedilab; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
public class Quotes implements Serializable {
|
||||
|
||||
@SerializedName("quotes")
|
||||
public List<Quote> quotes;
|
||||
|
||||
public static class Quote implements Serializable {
|
||||
@SerializedName("author")
|
||||
public String author;
|
||||
@SerializedName("content")
|
||||
public String content;
|
||||
}
|
||||
}
|
|
@ -27,6 +27,8 @@ public class RemoteInstance implements Serializable {
|
|||
public String id;
|
||||
@SerializedName("host")
|
||||
public String host;
|
||||
@SerializedName("displayName")
|
||||
public String displayName;
|
||||
@SerializedName("type")
|
||||
public InstanceType type;
|
||||
@SerializedName("tags")
|
||||
|
|
|
@ -96,7 +96,7 @@ public class CacheHelper {
|
|||
String[] children = dir.list();
|
||||
assert children != null;
|
||||
for (String aChildren : children) {
|
||||
if (!aChildren.equals("databases") && !aChildren.equals("shared_prefs")) {
|
||||
if (!aChildren.equals("databases") && !aChildren.equals("shared_prefs") && !aChildren.equals(Helper.TEMP_MEDIA_DIRECTORY)) {
|
||||
boolean success = deleteDir(new File(dir, aChildren));
|
||||
if (!success) {
|
||||
return false;
|
||||
|
|
|
@ -36,7 +36,6 @@ import android.content.pm.ResolveInfo;
|
|||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Color;
|
||||
|
@ -44,7 +43,6 @@ import android.graphics.Paint;
|
|||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.RingtoneManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
|
@ -157,7 +155,6 @@ import app.fedilab.android.client.entities.app.Timeline;
|
|||
import app.fedilab.android.databinding.PopupReleaseNotesBinding;
|
||||
import app.fedilab.android.exception.DBException;
|
||||
import app.fedilab.android.interfaces.OnDownloadInterface;
|
||||
import app.fedilab.android.sqlite.Sqlite;
|
||||
import app.fedilab.android.ui.drawer.ReleaseNoteAdapter;
|
||||
import app.fedilab.android.viewmodel.mastodon.AccountsVM;
|
||||
import app.fedilab.android.viewmodel.mastodon.OauthVM;
|
||||
|
@ -171,10 +168,6 @@ import okhttp3.RequestBody;
|
|||
public class Helper {
|
||||
|
||||
public static final String TAG = "fedilab_app";
|
||||
public static final String APP_CLIENT_ID = "APP_CLIENT_ID";
|
||||
public static final String APP_CLIENT_SECRET = "APP_CLIENT_SECRET";
|
||||
public static final String APP_INSTANCE = "APP_INSTANCE";
|
||||
public static final String APP_API = "APP_API";
|
||||
public static final String CLIP_BOARD = "CLIP_BOARD";
|
||||
|
||||
public static final String INSTANCE_SOCIAL_KEY = "jGj9gW3z9ptyIpB8CMGhAlTlslcemMV6AgoiImfw3vPP98birAJTHOWiu5ZWfCkLvcaLsFZw9e3Pb7TIwkbIyrj3z6S7r2oE6uy6EFHvls3YtapP8QKNZ980p9RfzTb4";
|
||||
|
@ -241,10 +234,8 @@ public class Helper {
|
|||
public static final String ARG_MINIFIED = "ARG_MINIFIED";
|
||||
public static final String ARG_STATUS_REPORT = "ARG_STATUS_REPORT";
|
||||
public static final String ARG_STATUS_MENTION = "ARG_STATUS_MENTION";
|
||||
public static final String ARG_SHARE_URI = "ARG_SHARE_URI";
|
||||
public static final String ARG_SHARE_URL_MEDIA = "ARG_SHARE_URL_MEDIA";
|
||||
public static final String ARG_SHARE_URL = "ARG_SHARE_URL";
|
||||
public static final String ARG_SHARE_URI_LIST = "ARG_SHARE_URI_LIST";
|
||||
public static final String ARG_SHARE_TITLE = "ARG_SHARE_TITLE";
|
||||
public static final String ARG_SHARE_SUBJECT = "ARG_SHARE_SUBJECT";
|
||||
public static final String ARG_SHARE_DESCRIPTION = "ARG_SHARE_DESCRIPTION";
|
||||
|
@ -264,6 +255,7 @@ public class Helper {
|
|||
public static final String ARG_TAG_TIMELINE = "ARG_TAG_TIMELINE";
|
||||
public static final String ARG_MEDIA_POSITION = "ARG_MEDIA_POSITION";
|
||||
public static final String ARG_MEDIA_ATTACHMENT = "ARG_MEDIA_ATTACHMENT";
|
||||
public static final String ARG_MEDIA_ATTACHMENTS = "ARG_MEDIA_ATTACHMENTS";
|
||||
public static final String ARG_SHOW_REPLIES = "ARG_SHOW_REPLIES";
|
||||
public static final String ARG_SHOW_REBLOGS = "ARG_SHOW_REBLOGS";
|
||||
public static final String ARG_INITIALIZE_VIEW = "ARG_INITIALIZE_VIEW";
|
||||
|
@ -323,9 +315,9 @@ public class Helper {
|
|||
|
||||
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL);*/
|
||||
|
||||
public static final Pattern hashtagPattern = Pattern.compile("(^|\\s+|[.,;?!-]+)(#[\\w_A-zÀ-ÿ]+)");
|
||||
public static final Pattern hashtagPattern = Pattern.compile("(#[\\w_A-zÀ-ÿ]+)");
|
||||
public static final Pattern groupPattern = Pattern.compile("(![\\w_]+)");
|
||||
public static final Pattern mentionPattern = Pattern.compile("(@[\\w_.-]+[\\w])");
|
||||
public static final Pattern mentionPattern = Pattern.compile("(@[\\w_.-]?[\\w]+)");
|
||||
public static final Pattern mentionLongPattern = Pattern.compile("(@[\\w_.-]+@[a-zA-Z0-9][a-zA-Z0-9.-]{1,61}[a-zA-Z0-9](?:\\.[a-zA-Z]{2,})+)");
|
||||
|
||||
public static final Pattern twitterPattern = Pattern.compile("((@[\\w]+)@twitter\\.com)");
|
||||
|
@ -1226,6 +1218,7 @@ public class Helper {
|
|||
|
||||
public static void createAttachmentFromUri(Context context, List<Uri> uris, OnAttachmentCopied callBack) {
|
||||
new Thread(() -> {
|
||||
List<Attachment> attachments = new ArrayList<>();
|
||||
for (Uri uri : uris) {
|
||||
Attachment attachment = new Attachment();
|
||||
attachment.filename = Helper.getFileName(context, uri);
|
||||
|
@ -1243,36 +1236,48 @@ public class Helper {
|
|||
counter++;
|
||||
Date now = new Date();
|
||||
attachment.filename = formatter.format(now) + "." + extension;
|
||||
InputStream selectedFileInputStream;
|
||||
try {
|
||||
selectedFileInputStream = context.getContentResolver().openInputStream(uri);
|
||||
if (selectedFileInputStream != null) {
|
||||
final File certCacheDir = new File(context.getCacheDir(), TEMP_MEDIA_DIRECTORY);
|
||||
boolean isCertCacheDirExists = certCacheDir.exists();
|
||||
if (!isCertCacheDirExists) {
|
||||
isCertCacheDirExists = certCacheDir.mkdirs();
|
||||
}
|
||||
if (isCertCacheDirExists) {
|
||||
String filePath = certCacheDir.getAbsolutePath() + "/" + attachment.filename;
|
||||
attachment.local_path = filePath;
|
||||
OutputStream selectedFileOutPutStream = new FileOutputStream(filePath);
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = selectedFileInputStream.read(buffer)) > 0) {
|
||||
selectedFileOutPutStream.write(buffer, 0, length);
|
||||
}
|
||||
selectedFileOutPutStream.flush();
|
||||
selectedFileOutPutStream.close();
|
||||
}
|
||||
selectedFileInputStream.close();
|
||||
if (attachment.mimeType.startsWith("image")) {
|
||||
final File certCacheDir = new File(context.getCacheDir(), TEMP_MEDIA_DIRECTORY);
|
||||
boolean isCertCacheDirExists = certCacheDir.exists();
|
||||
if (!isCertCacheDirExists) {
|
||||
certCacheDir.mkdirs();
|
||||
}
|
||||
String filePath = certCacheDir.getAbsolutePath() + "/" + attachment.filename;
|
||||
MediaHelper.ResizedImageRequestBody(context, uri, new File(filePath));
|
||||
attachment.local_path = filePath;
|
||||
} else {
|
||||
InputStream selectedFileInputStream;
|
||||
try {
|
||||
selectedFileInputStream = context.getContentResolver().openInputStream(uri);
|
||||
if (selectedFileInputStream != null) {
|
||||
final File certCacheDir = new File(context.getCacheDir(), TEMP_MEDIA_DIRECTORY);
|
||||
boolean isCertCacheDirExists = certCacheDir.exists();
|
||||
if (!isCertCacheDirExists) {
|
||||
isCertCacheDirExists = certCacheDir.mkdirs();
|
||||
}
|
||||
if (isCertCacheDirExists) {
|
||||
String filePath = certCacheDir.getAbsolutePath() + "/" + attachment.filename;
|
||||
attachment.local_path = filePath;
|
||||
OutputStream selectedFileOutPutStream = new FileOutputStream(filePath);
|
||||
byte[] buffer = new byte[1024];
|
||||
int length;
|
||||
while ((length = selectedFileInputStream.read(buffer)) > 0) {
|
||||
selectedFileOutPutStream.write(buffer, 0, length);
|
||||
}
|
||||
selectedFileOutPutStream.flush();
|
||||
selectedFileOutPutStream.close();
|
||||
}
|
||||
selectedFileInputStream.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
Runnable myRunnable = () -> callBack.onAttachmentCopied(attachment);
|
||||
mainHandler.post(myRunnable);
|
||||
attachments.add(attachment);
|
||||
}
|
||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
Runnable myRunnable = () -> callBack.onAttachmentCopied(attachments);
|
||||
mainHandler.post(myRunnable);
|
||||
}).start();
|
||||
}
|
||||
|
||||
|
@ -1322,6 +1327,7 @@ public class Helper {
|
|||
|
||||
public static void createAttachmentFromPAth(String path, OnAttachmentCopied callBack) {
|
||||
new Thread(() -> {
|
||||
List<Attachment> attachmentList = new ArrayList<>();
|
||||
Attachment attachment = new Attachment();
|
||||
attachment.mimeType = "image/*";
|
||||
String extension = "jpg";
|
||||
|
@ -1330,7 +1336,8 @@ public class Helper {
|
|||
Date now = new Date();
|
||||
attachment.filename = formatter.format(now) + "." + extension;
|
||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
Runnable myRunnable = () -> callBack.onAttachmentCopied(attachment);
|
||||
attachmentList.add(attachment);
|
||||
Runnable myRunnable = () -> callBack.onAttachmentCopied(attachmentList);
|
||||
mainHandler.post(myRunnable);
|
||||
}).start();
|
||||
}
|
||||
|
@ -1492,7 +1499,7 @@ public class Helper {
|
|||
channelTitle = context.getString(R.string.channel_notif_backup);
|
||||
break;
|
||||
case STORE:
|
||||
channelId = "channel_store";
|
||||
channelId = "channel_media";
|
||||
channelTitle = context.getString(R.string.channel_notif_media);
|
||||
break;
|
||||
case TOOT:
|
||||
|
@ -1561,12 +1568,12 @@ public class Helper {
|
|||
channel.setLightColor(ledColour);
|
||||
} else {
|
||||
channel = new NotificationChannel(channelId, channelTitle, NotificationManager.IMPORTANCE_DEFAULT);
|
||||
String soundUri = sharedpreferences.getString(context.getString(R.string.SET_NOTIF_SOUND), ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.getPackageName() + "/" + R.raw.boop);
|
||||
/*String soundUri = sharedpreferences.getString(context.getString(R.string.SET_NOTIF_SOUND), ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.getPackageName() + "/" + R.raw.boop);
|
||||
AudioAttributes audioAttributes = new AudioAttributes.Builder()
|
||||
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
||||
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
|
||||
.build();
|
||||
channel.setSound(Uri.parse(soundUri), audioAttributes);
|
||||
channel.setSound(Uri.parse(soundUri), audioAttributes);*/
|
||||
}
|
||||
assert mNotificationManager != null;
|
||||
mNotificationManager.createNotificationChannel(channel);
|
||||
|
@ -1600,28 +1607,6 @@ public class Helper {
|
|||
}
|
||||
}
|
||||
|
||||
public static void transfertIfExist(Context context) {
|
||||
File dbFile = context.getDatabasePath(OLD_DB_NAME);
|
||||
if (!dbFile.exists()) {
|
||||
return;
|
||||
}
|
||||
int version = -1;
|
||||
try {
|
||||
SQLiteDatabase sqlDb = SQLiteDatabase.openDatabase
|
||||
(context.getDatabasePath(OLD_DB_NAME).getAbsolutePath(), null, SQLiteDatabase.OPEN_READONLY);
|
||||
version = sqlDb.getVersion();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
try {
|
||||
if (version == -1) {
|
||||
version = 38;
|
||||
}
|
||||
SQLiteDatabase oldDb = Sqlite.getInstance(context.getApplicationContext(), OLD_DB_NAME, null, version).open();
|
||||
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
context.deleteDatabase(OLD_DB_NAME);
|
||||
}
|
||||
|
||||
public static String dateDiffFull(Date dateToot) {
|
||||
SimpleDateFormat df = (SimpleDateFormat) DateFormat.getDateTimeInstance(DateFormat.LONG, DateFormat.MEDIUM, Locale.getDefault());
|
||||
|
@ -1749,7 +1734,11 @@ public class Helper {
|
|||
fileName = FileNameCleaner.cleanFileName(fileName);
|
||||
// opens input stream from the HTTP connection
|
||||
InputStream inputStream = httpURLConnection.getInputStream();
|
||||
File saveDir = context.getCacheDir();
|
||||
final File saveDir = new File(context.getCacheDir(), TEMP_MEDIA_DIRECTORY);
|
||||
boolean isCertCacheDirExists = saveDir.exists();
|
||||
if (!isCertCacheDirExists) {
|
||||
saveDir.mkdirs();
|
||||
}
|
||||
final String saveFilePath = saveDir + File.separator + fileName;
|
||||
// opens an output stream to save into file
|
||||
FileOutputStream outputStream = new FileOutputStream(saveFilePath);
|
||||
|
@ -1995,7 +1984,7 @@ public class Helper {
|
|||
}
|
||||
|
||||
public interface OnAttachmentCopied {
|
||||
void onAttachmentCopied(Attachment attachment);
|
||||
void onAttachmentCopied(List<Attachment> attachments);
|
||||
}
|
||||
|
||||
public interface OnFileCopied {
|
||||
|
|
|
@ -80,6 +80,7 @@ public class MastodonHelper {
|
|||
|
||||
public static final int ACCOUNTS_PER_CALL = 40;
|
||||
public static final int STATUSES_PER_CALL = 40;
|
||||
public static final int SEARCH_PER_CALL = 20;
|
||||
public static final int NOTIFICATIONS_PER_CALL = 30;
|
||||
|
||||
|
||||
|
|
|
@ -21,10 +21,12 @@ import static app.fedilab.android.helper.LogoHelper.getMainLogo;
|
|||
|
||||
import android.app.Activity;
|
||||
import android.app.DownloadManager;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.media.MediaRecorder;
|
||||
import android.media.MediaScannerConnection;
|
||||
|
@ -42,7 +44,7 @@ import android.widget.Toast;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.FileProvider;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
|
@ -53,8 +55,10 @@ import org.jetbrains.annotations.NotNull;
|
|||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
|
@ -69,6 +73,7 @@ import java.util.concurrent.atomic.AtomicInteger;
|
|||
import app.fedilab.android.BuildConfig;
|
||||
import app.fedilab.android.R;
|
||||
import app.fedilab.android.activities.ComposeActivity;
|
||||
import app.fedilab.android.activities.MainActivity;
|
||||
import app.fedilab.android.client.entities.api.Attachment;
|
||||
import app.fedilab.android.databinding.DatetimePickerBinding;
|
||||
import app.fedilab.android.databinding.PopupRecordBinding;
|
||||
|
@ -84,7 +89,6 @@ public class MediaHelper {
|
|||
* @param url String download url
|
||||
*/
|
||||
public static long manageDownloadsNoPopup(final Context context, final String url) {
|
||||
final SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
final DownloadManager.Request request;
|
||||
try {
|
||||
|
@ -108,7 +112,11 @@ public class MediaHelper {
|
|||
}
|
||||
|
||||
if (!new File(myDir).exists()) {
|
||||
new File(myDir).mkdir();
|
||||
boolean created = new File(myDir).mkdir();
|
||||
if (!created) {
|
||||
Toasty.error(context, context.getString(R.string.toast_error), Toasty.LENGTH_SHORT).show();
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if (mime.toLowerCase().startsWith("video")) {
|
||||
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_MOVIES, context.getString(R.string.app_name) + "/" + fileName);
|
||||
|
@ -148,7 +156,11 @@ public class MediaHelper {
|
|||
File path = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
|
||||
File targeted_folder = new File(path, context.getString(R.string.app_name));
|
||||
if (!targeted_folder.exists()) {
|
||||
targeted_folder.mkdir();
|
||||
boolean created = targeted_folder.mkdir();
|
||||
if (!created) {
|
||||
Toasty.error(context, context.getString(R.string.toast_error), Toasty.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
}
|
||||
FileInputStream fis = null;
|
||||
FileOutputStream fos = null;
|
||||
|
@ -217,20 +229,6 @@ public class MediaHelper {
|
|||
});
|
||||
}
|
||||
|
||||
public static String formatSeconds(int seconds) {
|
||||
return getTwoDecimalsValue(seconds / 3600) + ":"
|
||||
+ getTwoDecimalsValue(seconds / 60) + ":"
|
||||
+ getTwoDecimalsValue(seconds % 60);
|
||||
}
|
||||
|
||||
private static String getTwoDecimalsValue(int value) {
|
||||
if (value >= 0 && value <= 9) {
|
||||
return "0" + value;
|
||||
} else {
|
||||
return value + "";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static String getMimeType(String url) {
|
||||
String type = null;
|
||||
|
@ -272,14 +270,12 @@ public class MediaHelper {
|
|||
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.ENGLISH).format(new Date());
|
||||
String imageFileName = "JPEG_" + timeStamp + "_";
|
||||
File storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
|
||||
File image = File.createTempFile(
|
||||
// Save a file: path for use with ACTION_VIEW intents
|
||||
return File.createTempFile(
|
||||
imageFileName, /* prefix */
|
||||
".jpg", /* suffix */
|
||||
storageDir /* directory */
|
||||
);
|
||||
// Save a file: path for use with ACTION_VIEW intents
|
||||
String mCurrentPhotoPath = image.getAbsolutePath();
|
||||
return image;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -400,6 +396,7 @@ public class MediaHelper {
|
|||
* @param attachmentList - List<Attachment>
|
||||
* @return int - The max height
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static int returnMaxHeightForPreviews(Context context, List<Attachment> attachmentList) {
|
||||
int maxHeight = RelativeLayout.LayoutParams.WRAP_CONTENT;
|
||||
if (attachmentList != null && attachmentList.size() > 0) {
|
||||
|
@ -420,4 +417,168 @@ public class MediaHelper {
|
|||
public interface OnSchedule {
|
||||
void scheduledAt(String scheduledDate);
|
||||
}
|
||||
|
||||
public static void ResizedImageRequestBody(Context context, Uri uri, File targetedFile) {
|
||||
InputStream decodeBitmapInputStream = null;
|
||||
try {
|
||||
InputStream inputStream = context.getContentResolver().openInputStream(uri);
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeStream(inputStream, null, options);
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
int orientation = getImageOrientation(uri, context.getContentResolver());
|
||||
int scaledImageSize = 1024;
|
||||
do {
|
||||
FileOutputStream outputStream = new FileOutputStream(targetedFile);
|
||||
decodeBitmapInputStream = context.getContentResolver().openInputStream(uri);
|
||||
options.inSampleSize = calculateInSampleSize(options, scaledImageSize, scaledImageSize);
|
||||
options.inJustDecodeBounds = false;
|
||||
Bitmap scaledBitmap = BitmapFactory.decodeStream(decodeBitmapInputStream, null, options);
|
||||
Bitmap reorientedBitmap = reorientBitmap(scaledBitmap, orientation);
|
||||
if (reorientedBitmap == null) {
|
||||
scaledBitmap.recycle();
|
||||
return;
|
||||
}
|
||||
Bitmap.CompressFormat format;
|
||||
if (!reorientedBitmap.hasAlpha()) {
|
||||
format = Bitmap.CompressFormat.JPEG;
|
||||
} else {
|
||||
format = Bitmap.CompressFormat.PNG;
|
||||
}
|
||||
reorientedBitmap.compress(format, 100, outputStream);
|
||||
reorientedBitmap.recycle();
|
||||
scaledImageSize /= 2;
|
||||
} while (targetedFile.length() > getMaxSize(targetedFile.length()));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
if (decodeBitmapInputStream != null) {
|
||||
try {
|
||||
decodeBitmapInputStream.close();
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static int calculateInSampleSize(BitmapFactory.Options options, int rqWidth, int rqHeight) {
|
||||
int height = options.outHeight;
|
||||
int width = options.outWidth;
|
||||
int inSampleSize = 1;
|
||||
if (height > rqHeight || width > rqWidth) {
|
||||
|
||||
int halfHeight = height / 2;
|
||||
int halfWidth = width / 2;
|
||||
while ((halfHeight / inSampleSize) > rqHeight && (halfWidth / inSampleSize) > rqWidth) {
|
||||
inSampleSize *= 2;
|
||||
}
|
||||
}
|
||||
return inSampleSize;
|
||||
}
|
||||
|
||||
private static int getImageOrientation(Uri uri, ContentResolver contentResolver) {
|
||||
InputStream inputStream;
|
||||
try {
|
||||
inputStream = contentResolver.openInputStream(uri);
|
||||
} catch (FileNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
return ExifInterface.ORIENTATION_UNDEFINED;
|
||||
}
|
||||
if (inputStream == null) {
|
||||
return ExifInterface.ORIENTATION_UNDEFINED;
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
try {
|
||||
ExifInterface exifInterface = new ExifInterface(inputStream);
|
||||
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||
inputStream.close();
|
||||
return orientation;
|
||||
} catch (IOException e) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
return ExifInterface.ORIENTATION_UNDEFINED;
|
||||
}
|
||||
e.printStackTrace();
|
||||
return ExifInterface.ORIENTATION_UNDEFINED;
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
ExifInterface exifInterface = new ExifInterface(uri.getPath());
|
||||
int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||
inputStream.close();
|
||||
return orientation;
|
||||
} catch (IOException e) {
|
||||
try {
|
||||
inputStream.close();
|
||||
} catch (IOException ex) {
|
||||
ex.printStackTrace();
|
||||
return ExifInterface.ORIENTATION_UNDEFINED;
|
||||
}
|
||||
e.printStackTrace();
|
||||
return ExifInterface.ORIENTATION_UNDEFINED;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static long getMaxSize(long maxSize) {
|
||||
if (MainActivity.instanceInfo != null && MainActivity.instanceInfo.configuration != null && MainActivity.instanceInfo.configuration.media_attachments != null) {
|
||||
maxSize = MainActivity.instanceInfo.configuration.media_attachments.image_size_limit;
|
||||
}
|
||||
return maxSize;
|
||||
}
|
||||
|
||||
public static Bitmap reorientBitmap(Bitmap bitmap, int orientation) {
|
||||
Matrix matrix = new Matrix();
|
||||
switch (orientation) {
|
||||
case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
|
||||
matrix.setScale(-1.0f, 1.0f);
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_ROTATE_180:
|
||||
matrix.setRotate(180.0f);
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_FLIP_VERTICAL:
|
||||
matrix.setRotate(180.0f);
|
||||
matrix.postScale(-1.0f, 1.0f);
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_TRANSPOSE:
|
||||
matrix.setRotate(90.0f);
|
||||
matrix.postScale(-1.0f, 1.0f);
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_ROTATE_90:
|
||||
matrix.setRotate(90.0f);
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_TRANSVERSE:
|
||||
matrix.setRotate(-90.0f);
|
||||
matrix.postScale(-1.0f, 1.0f);
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_ROTATE_270:
|
||||
matrix.setRotate(-90.0f);
|
||||
break;
|
||||
default:
|
||||
return bitmap;
|
||||
}
|
||||
if (bitmap == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
Bitmap result = Bitmap.createBitmap(
|
||||
bitmap, 0, 0, bitmap.getWidth(),
|
||||
bitmap.getHeight(), matrix, true);
|
||||
if (!bitmap.sameAs(result)) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ import android.widget.TextView;
|
|||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.AppCompatTextView;
|
||||
import androidx.appcompat.widget.PopupMenu;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
@ -356,6 +357,10 @@ public class PinnedTimelineHelper {
|
|||
name = pinnedTimeline.remoteInstance.host;
|
||||
if (pinnedTimeline.remoteInstance.type == RemoteInstance.InstanceType.NITTER) {
|
||||
String remoteInstance = sharedpreferences.getString(activity.getString(R.string.SET_NITTER_HOST), activity.getString(R.string.DEFAULT_NITTER_HOST)).toLowerCase();
|
||||
//Custom name for Nitter instances
|
||||
if (pinnedTimeline.remoteInstance.displayName != null && pinnedTimeline.remoteInstance.displayName.trim().length() > 0) {
|
||||
name = pinnedTimeline.remoteInstance.displayName;
|
||||
}
|
||||
ident = "@R@" + remoteInstance;
|
||||
} else {
|
||||
ident = "@R@" + pinnedTimeline.remoteInstance.host;
|
||||
|
@ -521,7 +526,7 @@ public class PinnedTimelineHelper {
|
|||
if (pinnedTimelineVisibleList.get(position).remoteInstance.type != RemoteInstance.InstanceType.NITTER) {
|
||||
instanceClick(activity, finalPinned, v, activityMainBinding, finalI, activityMainBinding.tabLayout.getTabAt(finalI).getTag().toString());
|
||||
} else {
|
||||
nitterClick(activity, finalPinned, activityMainBinding, finalI, activityMainBinding.tabLayout.getTabAt(finalI).getTag().toString());
|
||||
nitterClick(activity, finalPinned, v, activityMainBinding, finalI, activityMainBinding.tabLayout.getTabAt(finalI).getTag().toString());
|
||||
}
|
||||
break;
|
||||
case HOME:
|
||||
|
@ -979,6 +984,10 @@ public class PinnedTimelineHelper {
|
|||
if (values.trim().length() == 0)
|
||||
values = tag;
|
||||
tagTimeline.displayName = values;
|
||||
View titleView = view.findViewById(R.id.title);
|
||||
if (titleView instanceof AppCompatTextView) {
|
||||
((AppCompatTextView) titleView).setText(tagTimeline.displayName);
|
||||
}
|
||||
pinned.pinnedTimelines.get(finalOffSetPosition).tagTimeline = tagTimeline;
|
||||
try {
|
||||
new Pinned(activity).updatePinned(pinned);
|
||||
|
@ -1224,7 +1233,7 @@ public class PinnedTimelineHelper {
|
|||
* @param pinned - {@link Pinned}
|
||||
* @param position - int position of the tab
|
||||
*/
|
||||
public static void nitterClick(BaseMainActivity activity, Pinned pinned, ActivityMainBinding activityMainBinding, int position, String slug) {
|
||||
public static void nitterClick(BaseMainActivity activity, Pinned pinned, View view, ActivityMainBinding activityMainBinding, int position, String slug) {
|
||||
|
||||
int toRemove = itemToRemoveInBottomMenu(activity);
|
||||
int offSetPosition = position - (BOTTOM_TIMELINE_COUNT - toRemove);
|
||||
|
@ -1236,55 +1245,98 @@ public class PinnedTimelineHelper {
|
|||
RemoteInstance remoteInstance = pinned.pinnedTimelines.get(offSetPosition).remoteInstance;
|
||||
if (remoteInstance == null)
|
||||
return;
|
||||
String accounts = remoteInstance.host;
|
||||
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(activity, Helper.dialogStyle());
|
||||
LayoutInflater inflater = activity.getLayoutInflater();
|
||||
View dialogView = inflater.inflate(R.layout.tags_any, new LinearLayout(activity), false);
|
||||
dialogBuilder.setView(dialogView);
|
||||
final EditText editText = dialogView.findViewById(R.id.filter_any);
|
||||
editText.setHint(R.string.list_of_twitter_accounts);
|
||||
if (accounts != null) {
|
||||
editText.setText(accounts);
|
||||
editText.setSelection(editText.getText().toString().length());
|
||||
}
|
||||
PopupMenu popup = new PopupMenu(activity, view);
|
||||
popup.getMenuInflater()
|
||||
.inflate(R.menu.option_nitter_timeline, popup.getMenu());
|
||||
int finalOffSetPosition = offSetPosition;
|
||||
dialogBuilder.setPositiveButton(R.string.validate, (dialog, id) -> {
|
||||
pinned.pinnedTimelines.get(finalOffSetPosition).remoteInstance.host = editText.getText().toString().trim();
|
||||
try {
|
||||
new Pinned(activity).updatePinned(pinned);
|
||||
} catch (DBException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
FragmentMastodonTimeline fragmentMastodonTimeline = null;
|
||||
try {
|
||||
new StatusCache(activity).deleteForSlug(slug);
|
||||
} catch (DBException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (activityMainBinding.viewPager.getAdapter() != null) {
|
||||
Fragment fragment = (Fragment) activityMainBinding.viewPager.getAdapter().instantiateItem(activityMainBinding.viewPager, activityMainBinding.tabLayout.getSelectedTabPosition());
|
||||
if (fragment instanceof FragmentMastodonTimeline && fragment.isVisible()) {
|
||||
fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment);
|
||||
fragmentMastodonTimeline.refreshAllAdapters();
|
||||
popup.setOnMenuItemClickListener(item -> {
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == R.id.action_displayname) {
|
||||
AlertDialog.Builder dialogBuilder;
|
||||
LayoutInflater inflater;
|
||||
View dialogView;
|
||||
AlertDialog alertDialog;
|
||||
dialogBuilder = new AlertDialog.Builder(activity, Helper.dialogStyle());
|
||||
inflater = activity.getLayoutInflater();
|
||||
dialogView = inflater.inflate(R.layout.tags_name, new LinearLayout(activity), false);
|
||||
dialogBuilder.setView(dialogView);
|
||||
final EditText editTextName = dialogView.findViewById(R.id.column_name);
|
||||
if (remoteInstance.displayName != null) {
|
||||
editTextName.setText(remoteInstance.displayName);
|
||||
editTextName.setSelection(editTextName.getText().toString().length());
|
||||
}
|
||||
dialogBuilder.setPositiveButton(R.string.validate, (dialog, id) -> {
|
||||
String values = editTextName.getText().toString();
|
||||
if (values.trim().length() == 0) {
|
||||
values = remoteInstance.displayName;
|
||||
}
|
||||
remoteInstance.displayName = values;
|
||||
View titleView = view.findViewById(R.id.title);
|
||||
if (titleView instanceof AppCompatTextView) {
|
||||
((AppCompatTextView) titleView).setText(remoteInstance.displayName);
|
||||
}
|
||||
pinned.pinnedTimelines.get(finalOffSetPosition).remoteInstance = remoteInstance;
|
||||
try {
|
||||
new Pinned(activity).updatePinned(pinned);
|
||||
} catch (DBException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
alertDialog = dialogBuilder.create();
|
||||
alertDialog.show();
|
||||
} else if (itemId == R.id.action_nitter_manage_accounts) {
|
||||
String accounts = remoteInstance.host;
|
||||
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(activity, Helper.dialogStyle());
|
||||
LayoutInflater inflater = activity.getLayoutInflater();
|
||||
View dialogView = inflater.inflate(R.layout.tags_any, new LinearLayout(activity), false);
|
||||
dialogBuilder.setView(dialogView);
|
||||
final EditText editText = dialogView.findViewById(R.id.filter_any);
|
||||
editText.setHint(R.string.list_of_twitter_accounts);
|
||||
if (accounts != null) {
|
||||
editText.setText(accounts);
|
||||
editText.setSelection(editText.getText().toString().length());
|
||||
}
|
||||
dialogBuilder.setPositiveButton(R.string.validate, (dialog, id) -> {
|
||||
pinned.pinnedTimelines.get(finalOffSetPosition).remoteInstance.host = editText.getText().toString().trim();
|
||||
try {
|
||||
new Pinned(activity).updatePinned(pinned);
|
||||
} catch (DBException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
FragmentMastodonTimeline fragmentMastodonTimeline = null;
|
||||
try {
|
||||
new StatusCache(activity).deleteForSlug(slug);
|
||||
} catch (DBException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
if (activityMainBinding.viewPager.getAdapter() != null) {
|
||||
Fragment fragment = (Fragment) activityMainBinding.viewPager.getAdapter().instantiateItem(activityMainBinding.viewPager, activityMainBinding.tabLayout.getSelectedTabPosition());
|
||||
if (fragment instanceof FragmentMastodonTimeline && fragment.isVisible()) {
|
||||
fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment);
|
||||
fragmentMastodonTimeline.refreshAllAdapters();
|
||||
}
|
||||
}
|
||||
FragmentTransaction fragTransaction1 = activity.getSupportFragmentManager().beginTransaction();
|
||||
if (fragmentMastodonTimeline == null)
|
||||
return;
|
||||
fragTransaction1.detach(fragmentMastodonTimeline).commit();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putSerializable(Helper.ARG_REMOTE_INSTANCE, pinned.pinnedTimelines.get(finalOffSetPosition));
|
||||
bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.REMOTE);
|
||||
bundle.putSerializable(Helper.ARG_INITIALIZE_VIEW, false);
|
||||
fragmentMastodonTimeline.setArguments(bundle);
|
||||
FragmentTransaction fragTransaction2 = activity.getSupportFragmentManager().beginTransaction();
|
||||
fragTransaction2.attach(fragmentMastodonTimeline);
|
||||
fragTransaction2.commit();
|
||||
fragmentMastodonTimeline.recreate();
|
||||
});
|
||||
AlertDialog alertDialog = dialogBuilder.create();
|
||||
alertDialog.show();
|
||||
}
|
||||
FragmentTransaction fragTransaction1 = activity.getSupportFragmentManager().beginTransaction();
|
||||
if (fragmentMastodonTimeline == null)
|
||||
return;
|
||||
fragTransaction1.detach(fragmentMastodonTimeline).commit();
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putSerializable(Helper.ARG_REMOTE_INSTANCE, pinned.pinnedTimelines.get(finalOffSetPosition));
|
||||
bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.REMOTE);
|
||||
bundle.putSerializable(Helper.ARG_INITIALIZE_VIEW, false);
|
||||
fragmentMastodonTimeline.setArguments(bundle);
|
||||
FragmentTransaction fragTransaction2 = activity.getSupportFragmentManager().beginTransaction();
|
||||
fragTransaction2.attach(fragmentMastodonTimeline);
|
||||
fragTransaction2.commit();
|
||||
fragmentMastodonTimeline.recreate();
|
||||
return true;
|
||||
});
|
||||
AlertDialog alertDialog = dialogBuilder.create();
|
||||
alertDialog.show();
|
||||
popup.show();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -127,7 +127,9 @@ public class SpannableHelper {
|
|||
} else {
|
||||
linkColor = -1;
|
||||
}
|
||||
|
||||
if (linkColor == 0) {
|
||||
linkColor = -1;
|
||||
}
|
||||
SpannableString initialContent;
|
||||
if (text == null) {
|
||||
return null;
|
||||
|
@ -185,6 +187,8 @@ public class SpannableHelper {
|
|||
urlDetails.put(url, urlText);
|
||||
}
|
||||
}
|
||||
text = text.trim().replaceAll("\\s{3}", " ");
|
||||
text = text.trim().replaceAll("\\s{2}", " ");
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
initialContent = new SpannableString(Html.fromHtml(text, Html.FROM_HTML_MODE_LEGACY));
|
||||
else
|
||||
|
@ -252,6 +256,14 @@ public class SpannableHelper {
|
|||
if (urlDetails.containsKey(url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ClickableSpan[] clickableSpans = content.getSpans(matchStart, matchEnd, ClickableSpan.class);
|
||||
if (clickableSpans != null) {
|
||||
for (ClickableSpan clickableSpan : clickableSpans) {
|
||||
content.removeSpan(clickableSpan);
|
||||
}
|
||||
}
|
||||
content.removeSpan(clickableSpans);
|
||||
String newURL = Helper.transformURL(context, url);
|
||||
//If URL has been transformed
|
||||
if (newURL.compareTo(url) != 0) {
|
||||
|
@ -263,7 +275,7 @@ public class SpannableHelper {
|
|||
//Truncate URL if needed
|
||||
//TODO: add an option to disable truncated URLs
|
||||
String urlText = newURL;
|
||||
if (newURL.length() > 30 && !urlDetails.containsKey(urlText)) {
|
||||
if (newURL.length() > 30 && !urlDetails.containsKey(urlText) && !urlText.startsWith("gemini")) {
|
||||
urlText = urlText.substring(0, 30);
|
||||
urlText += "…";
|
||||
content.replace(matchStart, matchEnd, urlText);
|
||||
|
@ -512,6 +524,15 @@ public class SpannableHelper {
|
|||
if (content.toString().length() < matchEnd || matchStart < 0 || matchStart > matchEnd) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ClickableSpan[] clickableSpans = content.getSpans(matchStart, matchEnd, ClickableSpan.class);
|
||||
if (clickableSpans != null) {
|
||||
for (ClickableSpan clickableSpan : clickableSpans) {
|
||||
content.removeSpan(clickableSpan);
|
||||
}
|
||||
}
|
||||
content.removeSpan(clickableSpans);
|
||||
|
||||
if (matchEnd <= content.length()) {
|
||||
content.setSpan(new LongClickableSpan() {
|
||||
@Override
|
||||
|
|
|
@ -17,6 +17,7 @@ package app.fedilab.android.helper;
|
|||
import static app.fedilab.android.BaseMainActivity.filteredAccounts;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.os.Build;
|
||||
|
@ -25,6 +26,7 @@ import android.text.Html;
|
|||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.lifecycle.ViewModelStoreOwner;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
@ -35,6 +37,7 @@ import java.util.regex.Matcher;
|
|||
import java.util.regex.Pattern;
|
||||
|
||||
import app.fedilab.android.BaseMainActivity;
|
||||
import app.fedilab.android.R;
|
||||
import app.fedilab.android.activities.MainActivity;
|
||||
import app.fedilab.android.client.endpoints.MastodonFiltersService;
|
||||
import app.fedilab.android.client.entities.api.Account;
|
||||
|
@ -153,21 +156,40 @@ public class TimelineHelper {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
boolean groupReblogs = sharedpreferences.getBoolean(context.getString(R.string.SET_GROUP_REBLOGS), true);
|
||||
if (filterTimeLineType == Timeline.TimeLineEnum.HOME) {
|
||||
if (filteredAccounts != null && filteredAccounts.size() > 0) {
|
||||
for (Status status : statuses) {
|
||||
|
||||
for (int i = 0; i < statuses.size(); i++) {
|
||||
if (filteredAccounts != null && filteredAccounts.size() > 0) {
|
||||
for (Account account : filteredAccounts) {
|
||||
if (account.acct.equals(status.account.acct) || (status.reblog != null && account.acct.equals(status.reblog.account.acct))) {
|
||||
if (account.acct.equals(statuses.get(i).account.acct) || (statuses.get(i).reblog != null && account.acct.equals(statuses.get(i).reblog.account.acct))) {
|
||||
Filter filterCustom = new Filter();
|
||||
filterCustom.filter_action = "hide";
|
||||
ArrayList<String> contextCustom = new ArrayList<>();
|
||||
contextCustom.add("home");
|
||||
filterCustom.title = "Fedilab";
|
||||
filterCustom.context = contextCustom;
|
||||
status.filteredByApp = filterCustom;
|
||||
statuses.get(i).filteredByApp = filterCustom;
|
||||
}
|
||||
}
|
||||
}
|
||||
//Group boosts
|
||||
if (groupReblogs && statuses.get(i).filteredByApp == null && statuses.get(i).reblog != null) {
|
||||
for (int j = 0; j < i; j++) {
|
||||
if (statuses.get(j).reblog != null && statuses.get(j).reblog.id.equals(statuses.get(i).reblog.id)) {
|
||||
Filter filterCustom = new Filter();
|
||||
filterCustom.filter_action = "hide";
|
||||
ArrayList<String> contextCustom = new ArrayList<>();
|
||||
contextCustom.add("home");
|
||||
filterCustom.title = "Fedilab reblog";
|
||||
filterCustom.context = contextCustom;
|
||||
statuses.get(i).filteredByApp = filterCustom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ import android.view.MotionEvent;
|
|||
import android.view.View;
|
||||
import android.view.animation.AnticipateOvershootInterpolator;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.constraintlayout.widget.ConstraintSet;
|
||||
|
@ -25,8 +26,12 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
|||
import androidx.transition.ChangeBounds;
|
||||
import androidx.transition.TransitionManager;
|
||||
|
||||
import com.canhub.cropper.CropImage;
|
||||
import com.canhub.cropper.CropImageContract;
|
||||
import com.canhub.cropper.CropImageContractOptions;
|
||||
import com.canhub.cropper.CropImageOptions;
|
||||
import com.canhub.cropper.CropImageView;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
import com.theartofdev.edmodo.cropper.CropImage;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
@ -73,6 +78,8 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList
|
|||
private Uri uri;
|
||||
private boolean exit;
|
||||
private ActivityEditImageBinding binding;
|
||||
CropImageContractOptions cropImageContractOptions;
|
||||
ActivityResultLauncher<CropImageContractOptions> cropImageContractOptionsActivityResultLauncher;
|
||||
|
||||
private static int exifToDegrees(int exifOrientation) {
|
||||
if (exifOrientation == ExifInterface.ORIENTATION_ROTATE_90) {
|
||||
|
@ -146,6 +153,33 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList
|
|||
}
|
||||
}
|
||||
|
||||
cropImageContractOptions = new CropImageContractOptions(uri, new CropImageOptions())
|
||||
.setGuidelines(CropImageView.Guidelines.ON)
|
||||
.setCropShape(CropImageView.CropShape.RECTANGLE)
|
||||
.setAllowRotation(true)
|
||||
.setAllowFlipping(true)
|
||||
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
||||
.setAllowCounterRotation(true)
|
||||
.setImageSource(true, false)
|
||||
.setScaleType(CropImageView.ScaleType.CENTER);
|
||||
cropImageContractOptionsActivityResultLauncher = registerForActivityResult(
|
||||
new CropImageContract(),
|
||||
result -> {
|
||||
if (result.isSuccessful()) {
|
||||
Uri resultUri = result.getUriContent();
|
||||
if (resultUri != null) {
|
||||
binding.photoEditorView.getSource().setImageURI(resultUri);
|
||||
if (uri != null && uri.getPath() != null) {
|
||||
File fdelete = new File(uri.getPath());
|
||||
if (fdelete.exists()) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
fdelete.delete();
|
||||
}
|
||||
}
|
||||
uri = resultUri;
|
||||
}
|
||||
}
|
||||
});
|
||||
mPhotoEditor.setFilterEffect(PhotoFilter.NONE);
|
||||
binding.send.setOnClickListener(v -> {
|
||||
exit = true;
|
||||
|
@ -262,7 +296,7 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList
|
|||
int imgHeightInEditor;
|
||||
int imgWidthInEditor;
|
||||
//If the original image has its height greater than width => heights are equals
|
||||
float focusX = -2, focusY = -2;
|
||||
float focusX, focusY;
|
||||
if (imageHeight > imageWidth) {
|
||||
imgHeightInEditor = pHeight;
|
||||
float ratio = (float) pHeight / (float) imageHeight;
|
||||
|
@ -286,8 +320,8 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList
|
|||
}
|
||||
intentImage.putExtra("focusX", focusX);
|
||||
intentImage.putExtra("focusY", focusY);
|
||||
}
|
||||
|
||||
}
|
||||
LocalBroadcastManager.getInstance(EditImageActivity.this).sendBroadcast(intentImage);
|
||||
finish();
|
||||
}
|
||||
|
@ -351,22 +385,35 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList
|
|||
}
|
||||
break;
|
||||
case CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE:
|
||||
|
||||
CropImage.ActivityResult result = CropImage.getActivityResult(data);
|
||||
if (result != null) {
|
||||
Uri resultUri = result.getUri();
|
||||
if (resultUri != null) {
|
||||
binding.photoEditorView.getSource().setImageURI(resultUri);
|
||||
binding.photoEditorView.getSource().setRotation(rotationInDegrees);
|
||||
if (uri != null && uri.getPath() != null) {
|
||||
File fdelete = new File(uri.getPath());
|
||||
if (fdelete.exists()) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
fdelete.delete();
|
||||
}
|
||||
}
|
||||
uri = resultUri;
|
||||
}
|
||||
if (data != null && data.getData() != null) {
|
||||
CropImageContractOptions cropImageContractOptions = new CropImageContractOptions(data.getData(), new CropImageOptions())
|
||||
.setGuidelines(CropImageView.Guidelines.ON)
|
||||
.setCropShape(CropImageView.CropShape.RECTANGLE)
|
||||
.setAllowRotation(true)
|
||||
.setAllowFlipping(true)
|
||||
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
||||
.setAllowCounterRotation(true)
|
||||
.setImageSource(true, false)
|
||||
.setScaleType(CropImageView.ScaleType.CENTER);
|
||||
ActivityResultLauncher<CropImageContractOptions> cropImageContractOptionsActivityResultLauncher = registerForActivityResult(
|
||||
new CropImageContract(),
|
||||
result -> {
|
||||
if (result.isSuccessful()) {
|
||||
Uri resultUri = result.getUriContent();
|
||||
if (resultUri != null) {
|
||||
binding.photoEditorView.getSource().setImageURI(resultUri);
|
||||
if (uri != null && uri.getPath() != null) {
|
||||
File fdelete = new File(uri.getPath());
|
||||
if (fdelete.exists()) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
fdelete.delete();
|
||||
}
|
||||
}
|
||||
uri = resultUri;
|
||||
}
|
||||
}
|
||||
});
|
||||
cropImageContractOptionsActivityResultLauncher.launch(cropImageContractOptions);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -454,8 +501,8 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList
|
|||
mPropertiesBSFragment.show(getSupportFragmentManager(), mPropertiesBSFragment.getTag());
|
||||
break;
|
||||
case CROP:
|
||||
CropImage.activity(uri)
|
||||
.start(this);
|
||||
|
||||
cropImageContractOptionsActivityResultLauncher.launch(cropImageContractOptions);
|
||||
break;
|
||||
case FOCUS:
|
||||
binding.focusCircle.setVisibility(View.VISIBLE);
|
||||
|
|
|
@ -27,12 +27,12 @@ public class EditingToolsAdapter extends RecyclerView.Adapter<EditingToolsAdapte
|
|||
public EditingToolsAdapter(OnItemSelected onItemSelected) {
|
||||
mOnItemSelected = onItemSelected;
|
||||
mToolList.add(new ToolModel("Crop", R.drawable.ic_crop, ToolType.CROP));
|
||||
mToolList.add(new ToolModel("Focus", R.drawable.ic_baseline_filter_center_focus_24, ToolType.FOCUS));
|
||||
mToolList.add(new ToolModel("Shape", R.drawable.ic_oval, ToolType.SHAPE));
|
||||
mToolList.add(new ToolModel("Text", R.drawable.ic_text, ToolType.TEXT));
|
||||
mToolList.add(new ToolModel("Eraser", R.drawable.ic_eraser, ToolType.ERASER));
|
||||
mToolList.add(new ToolModel("Filter", R.drawable.ic_photo_filter, ToolType.FILTER));
|
||||
mToolList.add(new ToolModel("Emoji", R.drawable.ic_insert_emoticon, ToolType.EMOJI));
|
||||
mToolList.add(new ToolModel("Focus", R.drawable.ic_baseline_filter_center_focus_24, ToolType.FOCUS));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
package app.fedilab.android.interfaces;
|
||||
|
||||
public interface ProgressListener {
|
||||
void onProgress(long transferred, long total);
|
||||
}
|
|
@ -221,10 +221,10 @@ public class ComposeWorker extends Worker {
|
|||
if (dataPost.scheduledDate == null) {
|
||||
if (dataPost.statusEditId == null) {
|
||||
statusCall = mastodonStatusesService.createStatus(null, dataPost.token, statuses.get(i).text, attachmentIds, poll_options, poll_expire_in,
|
||||
poll_multiple, poll_hide_totals, in_reply_to_status, statuses.get(i).sensitive, statuses.get(i).spoiler_text, statuses.get(i).visibility.toLowerCase(), language);
|
||||
poll_multiple, poll_hide_totals, in_reply_to_status, statuses.get(i).sensitive, statuses.get(i).spoilerChecked ? statuses.get(i).spoiler_text : null, statuses.get(i).visibility.toLowerCase(), language);
|
||||
} else { //Status is edited
|
||||
statusCall = mastodonStatusesService.updateStatus(null, dataPost.token, dataPost.statusEditId, statuses.get(i).text, attachmentIds, poll_options, poll_expire_in,
|
||||
poll_multiple, poll_hide_totals, in_reply_to_status, statuses.get(i).sensitive, statuses.get(i).spoiler_text, statuses.get(i).visibility.toLowerCase(), language);
|
||||
poll_multiple, poll_hide_totals, in_reply_to_status, statuses.get(i).sensitive, statuses.get(i).spoilerChecked ? statuses.get(i).spoiler_text : null, statuses.get(i).visibility.toLowerCase(), language);
|
||||
}
|
||||
try {
|
||||
Response<Status> statusResponse = statusCall.execute();
|
||||
|
@ -285,7 +285,7 @@ public class ComposeWorker extends Worker {
|
|||
}
|
||||
} else {
|
||||
Call<ScheduledStatus> scheduledStatusCall = mastodonStatusesService.createScheduledStatus(null, dataPost.token, statuses.get(i).text, attachmentIds, poll_options, poll_expire_in,
|
||||
poll_multiple, poll_hide_totals, in_reply_to_status, statuses.get(i).sensitive, statuses.get(i).spoiler_text, statuses.get(i).visibility.toLowerCase(), dataPost.scheduledDate, statuses.get(i).language);
|
||||
poll_multiple, poll_hide_totals, in_reply_to_status, statuses.get(i).sensitive, statuses.get(i).spoilerChecked ? statuses.get(i).spoiler_text : null, statuses.get(i).visibility.toLowerCase(), dataPost.scheduledDate, statuses.get(i).language);
|
||||
try {
|
||||
Response<ScheduledStatus> statusResponse = scheduledStatusCall.execute();
|
||||
|
||||
|
@ -341,10 +341,14 @@ public class ComposeWorker extends Worker {
|
|||
private static String postAttachment(MastodonStatusesService mastodonStatusesService, DataPost dataPost, MultipartBody.Part fileMultipartBody, Attachment attachment) {
|
||||
|
||||
RequestBody descriptionBody = null;
|
||||
RequestBody focusBody = null;
|
||||
if (attachment.description != null && attachment.description.trim().length() > 0) {
|
||||
descriptionBody = RequestBody.create(MediaType.parse("text/plain"), attachment.description);
|
||||
}
|
||||
Call<Attachment> attachmentCall = mastodonStatusesService.postMedia(dataPost.token, fileMultipartBody, null, descriptionBody, attachment.focus);
|
||||
if (attachment.focus != null && attachment.focus.trim().length() > 0) {
|
||||
focusBody = RequestBody.create(MediaType.parse("text/plain"), attachment.focus);
|
||||
}
|
||||
Call<Attachment> attachmentCall = mastodonStatusesService.postMedia(dataPost.token, fileMultipartBody, null, descriptionBody, focusBody);
|
||||
|
||||
if (attachmentCall != null) {
|
||||
try {
|
||||
|
|
|
@ -142,14 +142,18 @@ public class AccountAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
|||
|
||||
|
||||
if (account.relationShip.blocking) {
|
||||
accountViewHolder.binding.block.setChecked(true);
|
||||
accountViewHolder.binding.block.setBackgroundTintList(ColorStateList.valueOf(ThemeHelper.getAttColor(context, R.attr.colorError)));
|
||||
accountViewHolder.binding.block.setIconResource(R.drawable.ic_baseline_lock_open_24);
|
||||
accountViewHolder.binding.block.setContentDescription(context.getString(R.string.action_unblock));
|
||||
accountViewHolder.binding.block.setOnClickListener(v -> accountsVM.unblock(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id)
|
||||
.observe((LifecycleOwner) context, relationShip -> {
|
||||
account.relationShip = relationShip;
|
||||
adapter.notifyItemChanged(position);
|
||||
}));
|
||||
} else {
|
||||
accountViewHolder.binding.block.setChecked(false);
|
||||
accountViewHolder.binding.block.setBackgroundTintList(ColorStateList.valueOf(ThemeHelper.getAttColor(context, R.attr.colorPrimary)));
|
||||
accountViewHolder.binding.block.setIconResource(R.drawable.ic_baseline_block_24);
|
||||
accountViewHolder.binding.block.setContentDescription(context.getString(R.string.more_action_2));
|
||||
accountViewHolder.binding.block.setOnClickListener(v -> accountsVM.block(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, account.id)
|
||||
.observe((LifecycleOwner) context, relationShip -> {
|
||||
account.relationShip = relationShip;
|
||||
|
|
|
@ -71,12 +71,17 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
|||
import com.bumptech.glide.request.RequestOptions;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.vanniktech.emoji.EmojiManager;
|
||||
import com.vanniktech.emoji.EmojiPopup;
|
||||
import com.vanniktech.emoji.one.EmojiOneProvider;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.Normalizer;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
|
@ -86,6 +91,7 @@ import java.util.LinkedHashMap;
|
|||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
@ -102,6 +108,7 @@ import app.fedilab.android.client.entities.api.Status;
|
|||
import app.fedilab.android.client.entities.api.Tag;
|
||||
import app.fedilab.android.client.entities.app.BaseAccount;
|
||||
import app.fedilab.android.client.entities.app.Languages;
|
||||
import app.fedilab.android.client.entities.app.Quotes;
|
||||
import app.fedilab.android.client.entities.app.StatusDraft;
|
||||
import app.fedilab.android.databinding.ComposeAttachmentItemBinding;
|
||||
import app.fedilab.android.databinding.ComposePollBinding;
|
||||
|
@ -214,8 +221,10 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
|||
promptDraftListener.promptDraft();
|
||||
}
|
||||
int finalPosition = position;
|
||||
Helper.createAttachmentFromUri(context, uris, attachment -> {
|
||||
statusList.get(finalPosition).media_attachments.add(attachment);
|
||||
Helper.createAttachmentFromUri(context, uris, attachments -> {
|
||||
for (Attachment attachment : attachments) {
|
||||
statusList.get(finalPosition).media_attachments.add(attachment);
|
||||
}
|
||||
notifyItemChanged(finalPosition);
|
||||
});
|
||||
}
|
||||
|
@ -545,6 +554,7 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
|||
public void run() {
|
||||
String fedilabHugsTrigger = ":fedilab_hugs:";
|
||||
String fedilabMorseTrigger = ":fedilab_morse:";
|
||||
String fedilabQuoteTrigger = ":fedilab_quote:";
|
||||
if (s.toString().contains(fedilabHugsTrigger)) {
|
||||
newContent[0] = s.toString().replaceAll(Pattern.quote(fedilabHugsTrigger), "").trim();
|
||||
int toFill = 500 - newContent[0].length();
|
||||
|
@ -604,6 +614,57 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
|||
newContent[0] = newContent[0].replaceAll("null", "");
|
||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
Runnable myRunnable = () -> {
|
||||
holder.binding.content.setText(newContent[0]);
|
||||
statusList.get(holder.getBindingAdapterPosition()).text = newContent[0];
|
||||
holder.binding.content.setSelection(holder.binding.content.getText().length());
|
||||
autocomplete = false;
|
||||
updateCharacterCount(holder);
|
||||
};
|
||||
mainHandler.post(myRunnable);
|
||||
} else if (s.toString().contains(fedilabQuoteTrigger)) {
|
||||
newContent[0] = s.toString().replaceAll(fedilabQuoteTrigger, "").trim();
|
||||
List<String> mentions = new ArrayList<>();
|
||||
String mentionPattern = "@[a-z0-9_]+(@[a-z0-9.\\-]+[a-z0-9]+)?";
|
||||
final Pattern mPattern = Pattern.compile(mentionPattern, Pattern.CASE_INSENSITIVE);
|
||||
Matcher matcherMentions = mPattern.matcher(newContent[0]);
|
||||
while (matcherMentions.find()) {
|
||||
mentions.add(matcherMentions.group());
|
||||
}
|
||||
for (String mention : mentions) {
|
||||
newContent[0] = newContent[0].replace(mention, "");
|
||||
}
|
||||
|
||||
InputStream is;
|
||||
newContent[0] = "";
|
||||
if (mentions.size() > 0) {
|
||||
for (String mention : mentions) {
|
||||
newContent[0] += mention + " ";
|
||||
}
|
||||
}
|
||||
try {
|
||||
is = context.getAssets().open("quotes/famous.json");
|
||||
int size;
|
||||
size = is.available();
|
||||
byte[] buffer = new byte[size];
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
is.read(buffer);
|
||||
is.close();
|
||||
String json = new String(buffer, StandardCharsets.UTF_8);
|
||||
Gson gson = new Gson();
|
||||
List<Quotes.Quote> quotes = gson.fromJson(json, new TypeToken<List<Quotes.Quote>>() {
|
||||
}.getType());
|
||||
if (quotes != null && quotes.size() > 0) {
|
||||
final int random = new Random().nextInt(quotes.size());
|
||||
Quotes.Quote quote = quotes.get(random);
|
||||
newContent[0] += quote.content + "\n- " + quote.author;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
Runnable myRunnable = () -> {
|
||||
holder.binding.content.setText(newContent[0]);
|
||||
statusList.get(holder.getBindingAdapterPosition()).text = newContent[0];
|
||||
|
@ -649,6 +710,15 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
|||
autocomplete = true;
|
||||
return;
|
||||
}
|
||||
|
||||
String patternQ = "^(.|\\s)*(:fedilab_quote:)";
|
||||
final Pattern qPattern = Pattern.compile(patternQ);
|
||||
Matcher mq = qPattern.matcher((s.toString().substring(currentCursorPosition[0] - searchLength[0], currentCursorPosition[0])));
|
||||
if (mq.matches()) {
|
||||
autocomplete = true;
|
||||
return;
|
||||
}
|
||||
|
||||
String[] searchInArray = (s.toString().substring(currentCursorPosition[0] - searchLength[0], currentCursorPosition[0])).split("\\s");
|
||||
if (searchInArray.length < 1) {
|
||||
updateCharacterCount(holder);
|
||||
|
@ -1030,6 +1100,11 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
|||
public void onLoadCleared(@Nullable Drawable placeholder) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFailed(@Nullable Drawable errorDrawable) {
|
||||
super.onLoadFailed(errorDrawable);
|
||||
}
|
||||
});
|
||||
builderInner.setNegativeButton(R.string.cancel, (dialog, which) -> dialog.dismiss());
|
||||
if (attachment.description != null) {
|
||||
|
@ -1088,15 +1163,15 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
|||
});
|
||||
composeAttachmentItemBinding.preview.setOnClickListener(v -> displayAttachments(holder, position, finalMediaPosition));
|
||||
if (attachment.description == null || attachment.description.trim().isEmpty()) {
|
||||
composeAttachmentItemBinding.buttonDescription.setIconResource(R.drawable.ic_baseline_warning_24);
|
||||
composeAttachmentItemBinding.buttonDescription.setChipIconResource(R.drawable.ic_baseline_warning_24);
|
||||
composeAttachmentItemBinding.buttonDescription.setTextColor(ContextCompat.getColor(context, R.color.black));
|
||||
composeAttachmentItemBinding.buttonDescription.setIconTintResource(R.color.black);
|
||||
composeAttachmentItemBinding.buttonDescription.setBackgroundTintList(ThemeHelper.getNoDescriptionColorStateList(context));
|
||||
composeAttachmentItemBinding.buttonDescription.setChipIconTintResource(R.color.black);
|
||||
composeAttachmentItemBinding.buttonDescription.setChipBackgroundColor(ThemeHelper.getNoDescriptionColorStateList(context));
|
||||
} else {
|
||||
composeAttachmentItemBinding.buttonDescription.setIconResource(R.drawable.ic_baseline_check_circle_24);
|
||||
composeAttachmentItemBinding.buttonDescription.setChipIconResource(R.drawable.ic_baseline_check_circle_24);
|
||||
composeAttachmentItemBinding.buttonDescription.setTextColor(ContextCompat.getColor(context, R.color.white));
|
||||
composeAttachmentItemBinding.buttonDescription.setIconTintResource(R.color.white);
|
||||
composeAttachmentItemBinding.buttonDescription.setBackgroundTintList(ThemeHelper.getHavingDescriptionColorStateList(context));
|
||||
composeAttachmentItemBinding.buttonDescription.setChipIconTintResource(R.color.white);
|
||||
composeAttachmentItemBinding.buttonDescription.setChipBackgroundColor(ThemeHelper.getHavingDescriptionColorStateList(context));
|
||||
}
|
||||
holder.binding.attachmentsList.addView(composeAttachmentItemBinding.getRoot());
|
||||
mediaPosition++;
|
||||
|
@ -1296,6 +1371,7 @@ public class ComposeAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
|||
break;
|
||||
}
|
||||
|
||||
holder.binding.visibilityPanel.setOnTouchListener((view, motionEvent) -> true);
|
||||
holder.binding.buttonCloseAttachmentPanel.setOnClickListener(v -> holder.binding.attachmentChoicesPanel.setVisibility(View.GONE));
|
||||
holder.binding.buttonVisibility.setOnClickListener(v -> {
|
||||
holder.binding.visibilityPanel.setVisibility(View.VISIBLE);
|
||||
|
|
|
@ -36,6 +36,7 @@ import app.fedilab.android.R;
|
|||
import app.fedilab.android.activities.ReorderTimelinesActivity;
|
||||
import app.fedilab.android.client.entities.app.Pinned;
|
||||
import app.fedilab.android.client.entities.app.PinnedTimeline;
|
||||
import app.fedilab.android.client.entities.app.RemoteInstance;
|
||||
import app.fedilab.android.client.entities.app.Timeline;
|
||||
import app.fedilab.android.databinding.DrawerReorderBinding;
|
||||
import app.fedilab.android.exception.DBException;
|
||||
|
@ -99,7 +100,15 @@ public class ReorderTabAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
|
|||
holder.binding.icon.setImageResource(R.drawable.nitter);
|
||||
break;
|
||||
}
|
||||
holder.binding.text.setText(pinned.pinnedTimelines.get(position).remoteInstance.host);
|
||||
if (pinned.pinnedTimelines.get(position).remoteInstance.type != RemoteInstance.InstanceType.NITTER) {
|
||||
holder.binding.text.setText(pinned.pinnedTimelines.get(position).remoteInstance.host);
|
||||
} else {
|
||||
if (pinned.pinnedTimelines.get(position).remoteInstance.displayName != null && pinned.pinnedTimelines.get(position).remoteInstance.displayName.trim().length() > 0) {
|
||||
holder.binding.text.setText(pinned.pinnedTimelines.get(position).remoteInstance.displayName);
|
||||
} else {
|
||||
holder.binding.text.setText(pinned.pinnedTimelines.get(position).remoteInstance.host);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case TAG:
|
||||
holder.binding.icon.setImageResource(R.drawable.ic_baseline_label_24);
|
||||
|
|
|
@ -68,6 +68,7 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.LinearLayoutCompat;
|
||||
import androidx.appcompat.widget.PopupMenu;
|
||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||
import androidx.core.app.ActivityOptionsCompat;
|
||||
|
@ -397,6 +398,47 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
boolean displayBookmark = sharedpreferences.getBoolean(context.getString(R.string.SET_DISPLAY_BOOKMARK), true);
|
||||
boolean displayTranslate = sharedpreferences.getBoolean(context.getString(R.string.SET_DISPLAY_TRANSLATE), false);
|
||||
boolean displayCounters = sharedpreferences.getBoolean(context.getString(R.string.SET_DISPLAY_COUNTER_FAV_BOOST), false);
|
||||
boolean removeLeftMargin = sharedpreferences.getBoolean(context.getString(R.string.SET_REMOVE_LEFT_MARGIN), false);
|
||||
|
||||
if (removeLeftMargin) {
|
||||
LinearLayoutCompat.MarginLayoutParams p = (LinearLayoutCompat.MarginLayoutParams) holder.binding.spoiler.getLayoutParams();
|
||||
p.setMarginStart((int) Helper.convertDpToPixel(6, context));
|
||||
holder.binding.spoiler.setLayoutParams(p);
|
||||
LinearLayoutCompat.MarginLayoutParams pe = (LinearLayoutCompat.MarginLayoutParams) holder.binding.spoilerExpand.getLayoutParams();
|
||||
pe.setMarginStart((int) Helper.convertDpToPixel(6, context));
|
||||
holder.binding.spoilerExpand.setLayoutParams(pe);
|
||||
LinearLayoutCompat.MarginLayoutParams psc = (LinearLayoutCompat.MarginLayoutParams) holder.binding.statusContent.getLayoutParams();
|
||||
psc.setMarginStart((int) Helper.convertDpToPixel(6, context));
|
||||
holder.binding.statusContent.setLayoutParams(psc);
|
||||
LinearLayoutCompat.MarginLayoutParams pct = (LinearLayoutCompat.MarginLayoutParams) holder.binding.containerTrans.getLayoutParams();
|
||||
psc.setMarginStart((int) Helper.convertDpToPixel(6, context));
|
||||
holder.binding.containerTrans.setLayoutParams(psc);
|
||||
LinearLayoutCompat.MarginLayoutParams pcv = (LinearLayoutCompat.MarginLayoutParams) holder.binding.card.getLayoutParams();
|
||||
pcv.setMarginStart((int) Helper.convertDpToPixel(6, context));
|
||||
holder.binding.card.setLayoutParams(pcv);
|
||||
LinearLayoutCompat.MarginLayoutParams pmc = (LinearLayoutCompat.MarginLayoutParams) holder.binding.mediaContainer.getLayoutParams();
|
||||
pmc.setMarginStart((int) Helper.convertDpToPixel(6, context));
|
||||
holder.binding.mediaContainer.setLayoutParams(pmc);
|
||||
LinearLayoutCompat.MarginLayoutParams pal = (LinearLayoutCompat.MarginLayoutParams) holder.binding.attachmentsListContainer.getLayoutParams();
|
||||
pal.setMarginStart((int) Helper.convertDpToPixel(6, context));
|
||||
holder.binding.attachmentsListContainer.setLayoutParams(pal);
|
||||
LinearLayoutCompat.MarginLayoutParams pp = (LinearLayoutCompat.MarginLayoutParams) holder.binding.poll.pollContainer.getLayoutParams();
|
||||
pp.setMarginStart((int) Helper.convertDpToPixel(6, context));
|
||||
holder.binding.poll.pollContainer.setLayoutParams(pp);
|
||||
LinearLayoutCompat.MarginLayoutParams pet = (LinearLayoutCompat.MarginLayoutParams) holder.binding.editTime.getLayoutParams();
|
||||
pet.setMarginStart((int) Helper.convertDpToPixel(6, context));
|
||||
holder.binding.editTime.setLayoutParams(pet);
|
||||
LinearLayoutCompat.MarginLayoutParams psi = (LinearLayoutCompat.MarginLayoutParams) holder.binding.statusInfo.getLayoutParams();
|
||||
psi.setMarginStart((int) Helper.convertDpToPixel(6, context));
|
||||
holder.binding.statusInfo.setLayoutParams(psi);
|
||||
LinearLayoutCompat.MarginLayoutParams pas = (LinearLayoutCompat.MarginLayoutParams) holder.binding.actionShareContainer.getLayoutParams();
|
||||
pas.setMarginStart((int) Helper.convertDpToPixel(6, context));
|
||||
holder.binding.actionShareContainer.setLayoutParams(pas);
|
||||
LinearLayoutCompat.MarginLayoutParams pab = (LinearLayoutCompat.MarginLayoutParams) holder.binding.actionButtons.getLayoutParams();
|
||||
pab.setMarginStart((int) Helper.convertDpToPixel(6, context));
|
||||
holder.binding.actionButtons.setLayoutParams(pab);
|
||||
}
|
||||
|
||||
String loadMediaType = sharedpreferences.getString(context.getString(R.string.SET_LOAD_MEDIA_TYPE), "ALWAYS");
|
||||
|
||||
if (currentAccount != null && currentAccount.api == Account.API.PLEROMA) {
|
||||
|
@ -630,7 +672,11 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
holder.binding.actionButtonBookmark.setVisibility(View.GONE);
|
||||
}
|
||||
if (displayTranslate) {
|
||||
holder.binding.actionButtonTranslate.setVisibility(View.VISIBLE);
|
||||
if (statusToDeal.language != null && statusToDeal.language.trim().length() > 0 && statusToDeal.language.equalsIgnoreCase(MyTransL.getLocale())) {
|
||||
holder.binding.actionButtonTranslate.setVisibility(View.GONE);
|
||||
} else {
|
||||
holder.binding.actionButtonTranslate.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
holder.binding.actionButtonTranslate.setVisibility(View.GONE);
|
||||
}
|
||||
|
@ -1163,15 +1209,15 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
float mediaH = -1.0f;
|
||||
|
||||
if (attachment.measuredWidth > 0) {
|
||||
float viewWidth = attachment.measuredWidth;
|
||||
if (attachment.meta != null && attachment.meta.small != null) {
|
||||
float viewWidth = attachment.measuredWidth;
|
||||
mediaH = attachment.meta.small.height;
|
||||
float mediaW = attachment.meta.small.width;
|
||||
if (mediaW != 0) {
|
||||
ratio = viewWidth / mediaW;
|
||||
}
|
||||
}
|
||||
loadAndAddAttachment(context, layoutMediaBinding, holder, adapter, mediaPosition, mediaH, ratio, statusToDeal, attachment, singleMedia);
|
||||
loadAndAddAttachment(context, layoutMediaBinding, holder, adapter, mediaPosition, viewWidth, mediaH, ratio, statusToDeal, attachment, singleMedia);
|
||||
} else {
|
||||
int finalMediaPosition = mediaPosition;
|
||||
layoutMediaBinding.media.getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
|
||||
|
@ -1181,20 +1227,20 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
attachment.measuredWidth = layoutMediaBinding.media.getWidth();
|
||||
float ratio = 1.0f;
|
||||
float mediaH = -1.0f;
|
||||
float viewWidth = attachment.measuredWidth;
|
||||
if (attachment.meta != null && attachment.meta.small != null) {
|
||||
float viewWidth = attachment.measuredWidth;
|
||||
mediaH = attachment.meta.small.height;
|
||||
float mediaW = attachment.meta.small.width;
|
||||
if (mediaW != 0) {
|
||||
ratio = viewWidth / mediaW;
|
||||
}
|
||||
}
|
||||
loadAndAddAttachment(context, layoutMediaBinding, holder, adapter, finalMediaPosition, mediaH, ratio, statusToDeal, attachment, singleMedia);
|
||||
loadAndAddAttachment(context, layoutMediaBinding, holder, adapter, finalMediaPosition, viewWidth, mediaH, ratio, statusToDeal, attachment, singleMedia);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
loadAndAddAttachment(context, layoutMediaBinding, holder, adapter, mediaPosition, -1.f, -1.f, statusToDeal, attachment, singleMedia);
|
||||
loadAndAddAttachment(context, layoutMediaBinding, holder, adapter, mediaPosition, -1.f, -1.f, -1.f, statusToDeal, attachment, singleMedia);
|
||||
}
|
||||
mediaPosition++;
|
||||
if (fullAttachement || singleMedia) {
|
||||
|
@ -1429,18 +1475,20 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
}
|
||||
holder.binding.poll.refreshPoll.setOnClickListener(v -> statusesVM.getPoll(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, statusToDeal.poll.id)
|
||||
.observe((LifecycleOwner) context, poll -> {
|
||||
//Store span elements
|
||||
int i = 0;
|
||||
for (Poll.PollItem item : statusToDeal.poll.options) {
|
||||
if (item.span_title != null) {
|
||||
poll.options.get(i).span_title = item.span_title;
|
||||
} else {
|
||||
poll.options.get(i).span_title = new SpannableString(item.title);
|
||||
if (poll != null) {
|
||||
//Store span elements
|
||||
int i = 0;
|
||||
for (Poll.PollItem item : statusToDeal.poll.options) {
|
||||
if (item.span_title != null) {
|
||||
poll.options.get(i).span_title = item.span_title;
|
||||
} else {
|
||||
poll.options.get(i).span_title = new SpannableString(item.title);
|
||||
}
|
||||
i++;
|
||||
}
|
||||
i++;
|
||||
statusToDeal.poll = poll;
|
||||
adapter.notifyItemChanged(holder.getBindingAdapterPosition());
|
||||
}
|
||||
statusToDeal.poll = poll;
|
||||
adapter.notifyItemChanged(holder.getBindingAdapterPosition());
|
||||
}));
|
||||
holder.binding.poll.pollContainer.setVisibility(View.VISIBLE);
|
||||
String pollInfo = context.getResources().getQuantityString(R.plurals.number_of_voters, statusToDeal.poll.voters_count, statusToDeal.poll.voters_count);
|
||||
|
@ -1799,7 +1847,7 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
BaseMainActivity.currentToken = account.token;
|
||||
BaseMainActivity.currentUserID = account.user_id;
|
||||
BaseMainActivity.currentInstance = account.instance;
|
||||
MainActivity.currentAccount = account;
|
||||
currentAccount = account;
|
||||
SharedPreferences.Editor editor = sharedpreferences.edit();
|
||||
editor.putString(PREF_USER_TOKEN, account.token);
|
||||
editor.putString(PREF_USER_INSTANCE, account.instance);
|
||||
|
@ -1816,6 +1864,28 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
builderSingle.show();
|
||||
};
|
||||
mainHandler.post(myRunnable);
|
||||
} else if (accounts.size() == 1) {
|
||||
Handler mainHandler = new Handler(Looper.getMainLooper());
|
||||
Runnable myRunnable = () -> {
|
||||
BaseAccount account = accounts.get(0);
|
||||
Toasty.info(context, context.getString(R.string.toast_account_changed, "@" + account.mastodon_account.acct + "@" + account.instance), Toasty.LENGTH_LONG).show();
|
||||
BaseMainActivity.currentToken = account.token;
|
||||
BaseMainActivity.currentUserID = account.user_id;
|
||||
BaseMainActivity.currentInstance = account.instance;
|
||||
currentAccount = account;
|
||||
SharedPreferences.Editor editor = sharedpreferences.edit();
|
||||
editor.putString(PREF_USER_TOKEN, account.token);
|
||||
editor.putString(PREF_USER_INSTANCE, account.instance);
|
||||
editor.putString(PREF_USER_ID, account.user_id);
|
||||
editor.commit();
|
||||
Intent mainActivity = new Intent(context, MainActivity.class);
|
||||
mainActivity.putExtra(Helper.INTENT_ACTION, Helper.OPEN_WITH_ANOTHER_ACCOUNT);
|
||||
mainActivity.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
mainActivity.putExtra(Helper.PREF_MESSAGE_URL, statusToDeal.url);
|
||||
context.startActivity(mainActivity);
|
||||
((Activity) context).finish();
|
||||
};
|
||||
mainHandler.post(myRunnable);
|
||||
}
|
||||
|
||||
} catch (DBException e) {
|
||||
|
@ -1966,7 +2036,7 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
private static void loadAndAddAttachment(Context context, LayoutMediaBinding layoutMediaBinding,
|
||||
StatusViewHolder holder,
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder> adapter,
|
||||
int mediaPosition, float mediaH, float ratio,
|
||||
int mediaPosition, float viewWidth, float mediaH, float ratio,
|
||||
Status statusToDeal, Attachment attachment, boolean singleImage) {
|
||||
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
final int timeout = sharedpreferences.getInt(context.getString(R.string.SET_NSFW_TIMEOUT), 5);
|
||||
|
@ -2037,6 +2107,7 @@ public class StatusAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder>
|
|||
requestBuilder = requestBuilder.apply(new RequestOptions().transform(new GlideFocus(focusX, focusY)));
|
||||
} else {
|
||||
requestBuilder = requestBuilder.placeholder(R.color.transparent_grey);
|
||||
requestBuilder = requestBuilder.apply(new RequestOptions().override((int) viewWidth, (int) mediaH));
|
||||
requestBuilder = requestBuilder.fitCenter();
|
||||
}
|
||||
requestBuilder.into(layoutMediaBinding.media);
|
||||
|
|
|
@ -56,6 +56,7 @@ public class FragmentMastodonAccount extends Fragment {
|
|||
private boolean flagLoading;
|
||||
private List<Account> accounts;
|
||||
private String max_id;
|
||||
private Integer offset;
|
||||
private AccountAdapter accountAdapter;
|
||||
private String search;
|
||||
private Account accountTimeline;
|
||||
|
@ -84,6 +85,11 @@ public class FragmentMastodonAccount extends Fragment {
|
|||
binding.recyclerView.setVisibility(View.GONE);
|
||||
accountsVM = new ViewModelProvider(FragmentMastodonAccount.this).get(viewModelKey, AccountsVM.class);
|
||||
max_id = null;
|
||||
offset = 0;
|
||||
if (search != null) {
|
||||
binding.swipeContainer.setRefreshing(false);
|
||||
binding.swipeContainer.setEnabled(false);
|
||||
}
|
||||
router(true);
|
||||
}
|
||||
|
||||
|
@ -109,18 +115,31 @@ public class FragmentMastodonAccount extends Fragment {
|
|||
}
|
||||
} else if (search != null) {
|
||||
SearchVM searchVM = new ViewModelProvider(FragmentMastodonAccount.this).get(viewModelKey, SearchVM.class);
|
||||
searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, "accounts", false, true, false, 0, null, null, MastodonHelper.STATUSES_PER_CALL)
|
||||
.observe(getViewLifecycleOwner(), results -> {
|
||||
if (results != null) {
|
||||
Accounts accounts = new Accounts();
|
||||
Pagination pagination = new Pagination();
|
||||
accounts.accounts = results.accounts;
|
||||
accounts.pagination = pagination;
|
||||
initializeAccountCommonView(accounts);
|
||||
} else {
|
||||
Toasty.error(requireActivity(), getString(R.string.toast_error), Toasty.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
if (firstLoad) {
|
||||
searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, "accounts", false, true, false, 0, null, null, MastodonHelper.SEARCH_PER_CALL)
|
||||
.observe(getViewLifecycleOwner(), results -> {
|
||||
if (results != null) {
|
||||
Accounts accounts = new Accounts();
|
||||
Pagination pagination = new Pagination();
|
||||
accounts.accounts = results.accounts;
|
||||
accounts.pagination = pagination;
|
||||
initializeAccountCommonView(accounts);
|
||||
} else {
|
||||
Toasty.error(requireActivity(), getString(R.string.toast_error), Toasty.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, "accounts", false, true, false, offset, null, null, MastodonHelper.SEARCH_PER_CALL)
|
||||
.observe(getViewLifecycleOwner(), results -> {
|
||||
if (results != null) {
|
||||
Accounts accounts = new Accounts();
|
||||
Pagination pagination = new Pagination();
|
||||
accounts.accounts = results.accounts;
|
||||
accounts.pagination = pagination;
|
||||
dealWithPagination(accounts);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (timelineType == Timeline.TimeLineEnum.MUTED_TIMELINE) {
|
||||
if (firstLoad) {
|
||||
accountsVM.getMutes(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, String.valueOf(MastodonHelper.accountsPerCall(requireActivity())), null, null)
|
||||
|
@ -204,7 +223,11 @@ public class FragmentMastodonAccount extends Fragment {
|
|||
|
||||
this.accounts = accounts.accounts;
|
||||
accountAdapter = new AccountAdapter(this.accounts, timelineType == Timeline.TimeLineEnum.MUTED_TIMELINE_HOME);
|
||||
flagLoading = accounts.pagination.max_id == null;
|
||||
if (search == null) {
|
||||
flagLoading = accounts.pagination.max_id == null;
|
||||
} else {
|
||||
offset += MastodonHelper.SEARCH_PER_CALL;
|
||||
}
|
||||
LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity());
|
||||
binding.recyclerView.setLayoutManager(mLayoutManager);
|
||||
binding.recyclerView.setAdapter(accountAdapter);
|
||||
|
@ -263,6 +286,9 @@ public class FragmentMastodonAccount extends Fragment {
|
|||
//Fetch the relationship
|
||||
fetchRelationShip(fetched_accounts.accounts, position);
|
||||
max_id = fetched_accounts.pagination.max_id;
|
||||
if (search != null) {
|
||||
offset += MastodonHelper.SEARCH_PER_CALL;
|
||||
}
|
||||
accountAdapter.notifyItemRangeInserted(startId, fetched_accounts.accounts.size());
|
||||
} else {
|
||||
flagLoading = true;
|
||||
|
|
|
@ -25,6 +25,7 @@ import androidx.annotation.Nullable;
|
|||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
@ -49,6 +50,9 @@ public class FragmentMastodonTag extends Fragment {
|
|||
private TagAdapter tagAdapter;
|
||||
private String search;
|
||||
private Timeline.TimeLineEnum timelineType;
|
||||
private Integer offset;
|
||||
private boolean flagLoading;
|
||||
private List<Tag> tagList;
|
||||
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
ViewGroup container, Bundle savedInstanceState) {
|
||||
|
@ -66,6 +70,10 @@ public class FragmentMastodonTag extends Fragment {
|
|||
super.onViewCreated(view, savedInstanceState);
|
||||
binding.loader.setVisibility(View.VISIBLE);
|
||||
binding.recyclerView.setVisibility(View.GONE);
|
||||
offset = 0;
|
||||
flagLoading = false;
|
||||
binding.swipeContainer.setRefreshing(false);
|
||||
binding.swipeContainer.setEnabled(false);
|
||||
router();
|
||||
}
|
||||
|
||||
|
@ -75,16 +83,24 @@ public class FragmentMastodonTag extends Fragment {
|
|||
private void router() {
|
||||
if (search != null && timelineType == null) {
|
||||
SearchVM searchVM = new ViewModelProvider(FragmentMastodonTag.this).get(SearchVM.class);
|
||||
searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, "hashtags", false, true, false, 0, null, null, MastodonHelper.STATUSES_PER_CALL)
|
||||
searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, "hashtags", false, true, false, offset, null, null, MastodonHelper.SEARCH_PER_CALL)
|
||||
.observe(getViewLifecycleOwner(), results -> {
|
||||
if (results != null && results.hashtags != null) {
|
||||
if (results != null && results.hashtags != null && offset == 0) {
|
||||
initializeTagCommonView(results.hashtags);
|
||||
} else if (results != null && results.hashtags != null) {
|
||||
dealWithPaginationTag(results.hashtags);
|
||||
}
|
||||
});
|
||||
} else if (timelineType == Timeline.TimeLineEnum.TREND_TAG) {
|
||||
TimelinesVM timelinesVM = new ViewModelProvider(FragmentMastodonTag.this).get(TimelinesVM.class);
|
||||
timelinesVM.getTagsTrends(BaseMainActivity.currentToken, BaseMainActivity.currentInstance)
|
||||
.observe(getViewLifecycleOwner(), this::initializeTagCommonView);
|
||||
timelinesVM.getTagsTrends(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, offset, MastodonHelper.SEARCH_PER_CALL)
|
||||
.observe(getViewLifecycleOwner(), tags -> {
|
||||
if (tags != null && offset == 0) {
|
||||
initializeTagCommonView(tags);
|
||||
} else if (tags != null) {
|
||||
dealWithPaginationTag(tags);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,6 +108,24 @@ public class FragmentMastodonTag extends Fragment {
|
|||
binding.recyclerView.setAdapter(tagAdapter);
|
||||
}
|
||||
|
||||
private void dealWithPaginationTag(final List<Tag> tags) {
|
||||
if (binding == null || !isAdded() || getActivity() == null) {
|
||||
return;
|
||||
}
|
||||
if (tags == null || tags.size() == 0) {
|
||||
flagLoading = true;
|
||||
binding.loadingNextElements.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
offset += MastodonHelper.SEARCH_PER_CALL;
|
||||
binding.swipeContainer.setRefreshing(false);
|
||||
binding.loadingNextElements.setVisibility(View.GONE);
|
||||
flagLoading = false;
|
||||
int start = tagList.size();
|
||||
tagList.addAll(tags);
|
||||
tagAdapter.notifyItemRangeInserted(start, tags.size());
|
||||
}
|
||||
|
||||
/**
|
||||
* Intialize the view for tags
|
||||
*
|
||||
|
@ -101,6 +135,7 @@ public class FragmentMastodonTag extends Fragment {
|
|||
if (binding == null || !isAdded() || getActivity() == null) {
|
||||
return;
|
||||
}
|
||||
tagList = new ArrayList<>();
|
||||
binding.loader.setVisibility(View.GONE);
|
||||
binding.noAction.setVisibility(View.GONE);
|
||||
binding.swipeContainer.setRefreshing(false);
|
||||
|
@ -130,12 +165,35 @@ public class FragmentMastodonTag extends Fragment {
|
|||
tags.add(0, tag);
|
||||
}
|
||||
}
|
||||
offset += MastodonHelper.SEARCH_PER_CALL;
|
||||
binding.recyclerView.setVisibility(View.VISIBLE);
|
||||
binding.noAction.setVisibility(View.GONE);
|
||||
tagAdapter = new TagAdapter(tags);
|
||||
tagList.addAll(tags);
|
||||
tagAdapter = new TagAdapter(tagList);
|
||||
LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity());
|
||||
binding.recyclerView.setLayoutManager(mLayoutManager);
|
||||
binding.recyclerView.setAdapter(tagAdapter);
|
||||
binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
|
||||
int firstVisibleItem = mLayoutManager.findFirstVisibleItemPosition();
|
||||
if (dy > 0) {
|
||||
int visibleItemCount = mLayoutManager.getChildCount();
|
||||
int totalItemCount = mLayoutManager.getItemCount();
|
||||
|
||||
if (firstVisibleItem + visibleItemCount == totalItemCount) {
|
||||
if (!flagLoading) {
|
||||
flagLoading = true;
|
||||
binding.loadingNextElements.setVisibility(View.VISIBLE);
|
||||
router();
|
||||
}
|
||||
} else {
|
||||
binding.loadingNextElements.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -80,6 +80,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
|
|||
private String search, searchCache;
|
||||
private Status statusReport;
|
||||
private String max_id, min_id, min_id_fetch_more, max_id_fetch_more;
|
||||
private Integer offset;
|
||||
private StatusAdapter statusAdapter;
|
||||
private Timeline.TimeLineEnum timelineType;
|
||||
private List<Status> timelineStatuses;
|
||||
|
@ -188,6 +189,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
|
|||
if (statusAdapter != null) {
|
||||
statusAdapter.notifyItemRangeRemoved(0, count);
|
||||
max_id = statusReport != null ? statusReport.id : null;
|
||||
offset = 0;
|
||||
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity());
|
||||
rememberPosition = sharedpreferences.getBoolean(getString(R.string.SET_REMEMBER_POSITION), true);
|
||||
//Inner marker are only for pinned timelines and main timelines, they have isViewInitialized set to false
|
||||
|
@ -264,7 +266,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
|
|||
}
|
||||
|
||||
public void scrollToTop() {
|
||||
if (binding != null) {
|
||||
if (binding != null && search == null) {
|
||||
binding.swipeContainer.setRefreshing(true);
|
||||
flagLoading = false;
|
||||
route(DIRECTION.SCROLL_TOP, true);
|
||||
|
@ -281,12 +283,17 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
|
|||
binding.loader.setVisibility(View.VISIBLE);
|
||||
binding.recyclerView.setVisibility(View.GONE);
|
||||
max_id = statusReport != null ? statusReport.id : null;
|
||||
offset = 0;
|
||||
SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity());
|
||||
rememberPosition = sharedpreferences.getBoolean(getString(R.string.SET_REMEMBER_POSITION), true);
|
||||
//Inner marker are only for pinned timelines and main timelines, they have isViewInitialized set to false
|
||||
if (max_id == null && !isViewInitialized && rememberPosition) {
|
||||
max_id = sharedpreferences.getString(getString(R.string.SET_INNER_MARKER) + BaseMainActivity.currentUserID + BaseMainActivity.currentInstance + slug, null);
|
||||
}
|
||||
if (search != null) {
|
||||
binding.swipeContainer.setRefreshing(false);
|
||||
binding.swipeContainer.setEnabled(false);
|
||||
}
|
||||
//Only fragment in main view pager should not have the view initialized
|
||||
//AND Only the first fragment will initialize its view
|
||||
flagLoading = false;
|
||||
|
@ -428,7 +435,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
|
|||
}
|
||||
//Update the timeline with new statuses
|
||||
int insertedStatus;
|
||||
if (timelineType != Timeline.TimeLineEnum.TREND_MESSAGE_PUBLIC && timelineType != Timeline.TimeLineEnum.TREND_MESSAGE) {
|
||||
if (timelineType != Timeline.TimeLineEnum.TREND_MESSAGE_PUBLIC && timelineType != Timeline.TimeLineEnum.TREND_MESSAGE && search == null) {
|
||||
insertedStatus = updateStatusListWith(fetched_statuses.statuses);
|
||||
} else { //Trends cannot be ordered by id
|
||||
insertedStatus = fetched_statuses.statuses.size();
|
||||
|
@ -455,6 +462,9 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
|
|||
min_id = fetched_statuses.pagination.min_id;
|
||||
}
|
||||
}
|
||||
if (search != null) {
|
||||
offset += MastodonHelper.SEARCH_PER_CALL;
|
||||
}
|
||||
int sizeBeforeFilter = 0;
|
||||
int filteredMessage = 0;
|
||||
int requestedMessages = MastodonHelper.statusesPerCall(requireActivity());
|
||||
|
@ -479,10 +489,9 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
|
|||
flagLoading = true;
|
||||
}
|
||||
if (direction == DIRECTION.SCROLL_TOP) {
|
||||
binding.recyclerView.scrollToPosition(0);
|
||||
new Handler().postDelayed(() -> binding.recyclerView.scrollToPosition(0), 200);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -501,6 +510,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
|
|||
}
|
||||
return;
|
||||
}
|
||||
|
||||
binding.loader.setVisibility(View.GONE);
|
||||
binding.noAction.setVisibility(View.GONE);
|
||||
binding.swipeContainer.setRefreshing(false);
|
||||
|
@ -557,6 +567,9 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
|
|||
if (min_id == null || (statuses.pagination.min_id != null && Helper.compareTo(statuses.pagination.min_id, min_id) > 0)) {
|
||||
min_id = statuses.pagination.min_id;
|
||||
}
|
||||
if (search != null) {
|
||||
offset += MastodonHelper.SEARCH_PER_CALL;
|
||||
}
|
||||
statusAdapter = new StatusAdapter(timelineStatuses, timelineType, minified, canBeFederated, checkRemotely);
|
||||
statusAdapter.fetchMoreCallBack = this;
|
||||
if (statusReport != null) {
|
||||
|
@ -570,8 +583,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
|
|||
mLayoutManager.setOrientation(LinearLayoutManager.VERTICAL);
|
||||
binding.recyclerView.setLayoutManager(mLayoutManager);
|
||||
binding.recyclerView.setAdapter(statusAdapter);
|
||||
|
||||
if (searchCache == null && timelineType != Timeline.TimeLineEnum.TREND_MESSAGE) {
|
||||
if (timelineType != Timeline.TimeLineEnum.TREND_MESSAGE) {
|
||||
binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
|
@ -1033,17 +1045,31 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter.
|
|||
}
|
||||
} else if (search != null) {
|
||||
SearchVM searchVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, SearchVM.class);
|
||||
searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, null, false, true, false, 0, null, null, MastodonHelper.STATUSES_PER_CALL)
|
||||
.observe(getViewLifecycleOwner(), results -> {
|
||||
if (results != null) {
|
||||
Statuses statuses = new Statuses();
|
||||
statuses.statuses = results.statuses;
|
||||
statuses.pagination = new Pagination();
|
||||
initializeStatusesCommonView(statuses);
|
||||
} else {
|
||||
Toasty.error(requireActivity(), getString(R.string.toast_error), Toasty.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
if (direction == null) {
|
||||
searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, null, false, true, false, 0, null, null, MastodonHelper.SEARCH_PER_CALL)
|
||||
.observe(getViewLifecycleOwner(), results -> {
|
||||
if (results != null) {
|
||||
Statuses statuses = new Statuses();
|
||||
statuses.statuses = results.statuses;
|
||||
statuses.pagination = new Pagination();
|
||||
initializeStatusesCommonView(statuses);
|
||||
} else {
|
||||
Toasty.error(requireActivity(), getString(R.string.toast_error), Toasty.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
} else if (direction == DIRECTION.BOTTOM) {
|
||||
searchVM.search(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, search.trim(), null, null, false, true, false, offset, null, null, MastodonHelper.SEARCH_PER_CALL)
|
||||
.observe(getViewLifecycleOwner(), results -> {
|
||||
if (results != null) {
|
||||
Statuses statuses = new Statuses();
|
||||
statuses.statuses = results.statuses;
|
||||
statuses.pagination = new Pagination();
|
||||
dealWithPagination(statuses, direction, false);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
flagLoading = false;
|
||||
}
|
||||
} else if (searchCache != null) {
|
||||
SearchVM searchVM = new ViewModelProvider(FragmentMastodonTimeline.this).get(viewModelKey, SearchVM.class);
|
||||
searchVM.searchCache(BaseMainActivity.currentInstance, BaseMainActivity.currentUserID, searchCache.trim())
|
||||
|
|
|
@ -1347,7 +1347,7 @@ public class AccountsVM extends AndroidViewModel {
|
|||
if (acceptFollowResponse.isSuccessful()) {
|
||||
relationShip = acceptFollowResponse.body();
|
||||
}
|
||||
new StatusCache(getApplication().getApplicationContext()).deleteStatus(instance, id);
|
||||
new StatusCache(getApplication().getApplicationContext()).deleteNotifications(MainActivity.currentUserID, instance);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
@ -1378,7 +1378,7 @@ public class AccountsVM extends AndroidViewModel {
|
|||
if (rejectFollowResponse.isSuccessful()) {
|
||||
relationShip = rejectFollowResponse.body();
|
||||
}
|
||||
new StatusCache(getApplication().getApplicationContext()).deleteStatus(instance, id);
|
||||
new StatusCache(getApplication().getApplicationContext()).deleteNotifications(MainActivity.currentUserID, instance);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
|
|
@ -99,10 +99,15 @@ public class SearchVM extends AndroidViewModel {
|
|||
MastodonSearchService mastodonSearchService = init(instance);
|
||||
resultsMutableLiveData = new MutableLiveData<>();
|
||||
new Thread(() -> {
|
||||
int finalLimit = 40;
|
||||
if (limit != null && limit < 40) {
|
||||
finalLimit = limit;
|
||||
}
|
||||
Call<Results> resultsCall = mastodonSearchService.search(
|
||||
token, q, account_id, type, exclude_unreviewed,
|
||||
resolve, following, offset, max_id, min_id, limit);
|
||||
resolve, following, offset, max_id, min_id, finalLimit);
|
||||
Results results = null;
|
||||
|
||||
if (resultsCall != null) {
|
||||
try {
|
||||
Response<Results> resultsResponse = resultsCall.execute();
|
||||
|
|
|
@ -127,7 +127,11 @@ public class StatusesVM extends AndroidViewModel {
|
|||
if (description != null && description.trim().length() > 0) {
|
||||
descriptionBody = RequestBody.create(MediaType.parse("text/plain"), description);
|
||||
}
|
||||
Call<Attachment> attachmentCall = mastodonStatusesService.postMedia(token, fileMultipartBody, thumbnailMultipartBody, descriptionBody, focus);
|
||||
RequestBody focusBody = null;
|
||||
if (focus != null && focus.trim().length() > 0) {
|
||||
focusBody = RequestBody.create(MediaType.parse("text/plain"), focus);
|
||||
}
|
||||
Call<Attachment> attachmentCall = mastodonStatusesService.postMedia(token, fileMultipartBody, thumbnailMultipartBody, descriptionBody, focusBody);
|
||||
Attachment attachment = null;
|
||||
if (attachmentCall != null) {
|
||||
try {
|
||||
|
|
|
@ -198,11 +198,11 @@ public class TimelinesVM extends AndroidViewModel {
|
|||
return statusesMutableLiveData;
|
||||
}
|
||||
|
||||
public LiveData<List<Tag>> getTagsTrends(String token, @NonNull String instance) {
|
||||
public LiveData<List<Tag>> getTagsTrends(String token, @NonNull String instance, Integer offset, Integer limit) {
|
||||
MastodonTimelinesService mastodonTimelinesService = init(instance);
|
||||
tagListMutableLiveData = new MutableLiveData<>();
|
||||
new Thread(() -> {
|
||||
Call<List<Tag>> publicTlCall = mastodonTimelinesService.getTagTrends(token);
|
||||
Call<List<Tag>> publicTlCall = mastodonTimelinesService.getTagTrends(token, offset, limit);
|
||||
List<Tag> tagList = null;
|
||||
if (publicTlCall != null) {
|
||||
try {
|
||||
|
|
5
app/src/main/res/drawable/bg_compose_panels.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<solid android:color="?colorSurfaceVariant" />
|
||||
<corners android:radius="12dp" />
|
||||
</shape>
|
|
@ -4,9 +4,9 @@
|
|||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorControlNormal"
|
||||
android:fillColor="?attr/colorTertiary"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2z" />
|
||||
<path
|
||||
android:fillColor="?attr/colorPrimary"
|
||||
android:pathData="M10,16.5v-9l6,4.5 -6,4.5z" />
|
||||
android:fillColor="?attr/colorOnTertiary"
|
||||
android:pathData="M10,15.5v-7c0,-0.41 0.47,-0.65 0.8,-0.4l4.67,3.5c0.27,0.2 0.27,0.6 0,0.8l-4.67,3.5c-0.33,0.25 -0.8,0.01 -0.8,-0.4z" />
|
||||
</vector>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="511.999"
|
||||
android:viewportHeight="511.999">
|
||||
<path
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?attr/colorControlNormal"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
|
||||
<app.fedilab.android.helper.CirclesDrawingView
|
||||
android:id="@+id/focus_circle"
|
||||
android:elevation="5dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone" />
|
||||
|
|
|
@ -32,7 +32,6 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true"
|
||||
app:contentScrim="?colorPrimary"
|
||||
app:expandedTitleGravity="top"
|
||||
app:expandedTitleMarginEnd="64dp"
|
||||
app:expandedTitleMarginStart="48dp"
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/preview"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="100dp"
|
||||
android:layout_height="108dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
|
@ -23,83 +23,66 @@
|
|||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:src="@tools:sample/backgrounds/scenic" />
|
||||
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_play"
|
||||
style="@style/Widget.AppCompat.Button.Borderless"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:padding="0dp"
|
||||
app:icon="@drawable/ic_compose_attachment_play"
|
||||
app:iconGravity="textStart"
|
||||
android:scaleType="fitCenter"
|
||||
app:iconPadding="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/preview"
|
||||
app:layout_constraintEnd_toEndOf="@id/preview"
|
||||
app:layout_constraintStart_toStartOf="@id/preview"
|
||||
app:layout_constraintTop_toTopOf="@id/preview" />
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/button_play"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:layout_constraintBottom_toBottomOf="@id/preview"
|
||||
app:layout_constraintEnd_toEndOf="@id/preview"
|
||||
app:layout_constraintStart_toStartOf="@id/preview"
|
||||
app:layout_constraintTop_toTopOf="@id/preview"
|
||||
app:srcCompat="@drawable/ic_compose_attachment_play" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/edit_preview"
|
||||
style="@style/Widget.Material3.Button.Icon"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
style="@style/Widget.Material3.Button.IconButton.Filled"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="true"
|
||||
android:padding="0dp"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
android:backgroundTint="@color/editColor"
|
||||
app:icon="@drawable/ic_baseline_mode_edit_24"
|
||||
app:iconGravity="textStart"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_remove"
|
||||
android:padding="0dp"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
style="@style/Widget.Material3.Button.Icon"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:adjustViewBounds="true"
|
||||
style="@style/Widget.Material3.Button.IconButton.Filled"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:backgroundTint="@color/errorColor"
|
||||
app:icon="@drawable/ic_compose_attachment_remove"
|
||||
app:iconGravity="textStart"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_order_up"
|
||||
style="@style/Widget.Material3.Button.Icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="true"
|
||||
app:icon="@drawable/ic_compose_attachment_order_up"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/preview" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
<com.google.android.material.chip.Chip
|
||||
android:id="@+id/button_description"
|
||||
style="@style/Widget.Material3.Button"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
style="@style/Widget.Material3.Chip.Assist.Elevated"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/description"
|
||||
android:textColor="@color/black"
|
||||
android:textAlignment="textStart"
|
||||
app:backgroundTint="@color/no_description"
|
||||
app:icon="@drawable/ic_baseline_warning_24"
|
||||
app:iconGravity="end"
|
||||
app:iconTint="@color/black"
|
||||
app:chipBackgroundColor="@color/no_description"
|
||||
app:chipIcon="@drawable/ic_baseline_warning_24"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_order_down"
|
||||
app:layout_constraintStart_toEndOf="@id/button_order_up"
|
||||
app:layout_constraintTop_toBottomOf="@id/preview" />
|
||||
|
||||
app:layout_constraintTop_toBottomOf="@id/preview"
|
||||
app:textEndPadding="0dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_order_down"
|
||||
style="@style/Widget.Material3.Button.Icon"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:adjustViewBounds="true"
|
||||
|
|
|
@ -143,15 +143,15 @@
|
|||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/block"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
style="@style/Widget.Material3.Button"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="24dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginTop="4dp"
|
||||
android:padding="0dp"
|
||||
app:icon="@drawable/ic_baseline_block_24"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
app:icon="@drawable/ic_baseline_block_24"
|
||||
app:layout_constraintStart_toEndOf="@id/mute_group"
|
||||
app:layout_constraintTop_toBottomOf="@id/bio" />
|
||||
|
||||
|
|
|
@ -561,11 +561,15 @@
|
|||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:gravity="end"
|
||||
android:visibility="gone">
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/action_share"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton.Icon"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
app:strokeColor="?colorPrimary"
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:adjustViewBounds="true"
|
||||
|
|
|
@ -18,96 +18,78 @@
|
|||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/cardview_container"
|
||||
style="@style/Widget.Material3.CardView.Elevated"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="6dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:clipChildren="false"
|
||||
android:layout_marginTop="10dp"
|
||||
android:clipToPadding="false"
|
||||
app:cardElevation="5dp"
|
||||
app:strokeWidth="0dp">
|
||||
android:clipToPadding="false">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="6dp">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/add_remove_status"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton.Icon"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:contentDescription="@string/add_status"
|
||||
android:padding="0dp"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
app:icon="@drawable/ic_compose_thread_add_status"
|
||||
app:strokeColor="?colorPrimary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
android:id="@+id/content_spoiler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="6dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:hint="@string/eg_sensitive_content"
|
||||
android:inputType="textMultiLine|textCapSentences"
|
||||
android:singleLine="true"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/add_remove_status" />
|
||||
app:layout_constraintEnd_toStartOf="@id/button_emoji"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/add_remove_status"
|
||||
style="@style/Fedilab.SmallIconButton"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:contentDescription="@string/add_status"
|
||||
app:icon="@drawable/ic_compose_thread_add_status"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<app.fedilab.android.helper.FedilabAutoCompleteTextView
|
||||
android:id="@+id/content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="6dp"
|
||||
android:layout_marginStart="6dp"
|
||||
android:focusable="true"
|
||||
android:gravity="top|start"
|
||||
android:inputType="textMultiLine|textCapSentences"
|
||||
android:minLines="6"
|
||||
app:layout_constraintEnd_toStartOf="@+id/emoji_container"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_emoji"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/content_spoiler" />
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/emoji_container"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="0dp"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_emoji"
|
||||
style="@style/Fedilab.SmallIconButton"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="@id/content"
|
||||
android:contentDescription="@string/emoji_picker"
|
||||
app:icon="@drawable/ic_compose_emoji"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/content">
|
||||
app:layout_constraintTop_toBottomOf="@id/add_remove_status" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_emoji"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton.Icon"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:padding="0dp"
|
||||
android:layout_marginTop="6dp"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
android:contentDescription="@string/emoji_picker"
|
||||
app:icon="@drawable/ic_compose_emoji" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_emoji_one"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton.Icon"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:padding="0dp"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
android:contentDescription="@string/emoji_picker"
|
||||
app:icon="@drawable/ic_baseline_insert_emoticon_24"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_emoji_one"
|
||||
style="@style/Fedilab.SmallIconButton"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:contentDescription="@string/emoji_picker"
|
||||
android:visibility="gone"
|
||||
app:icon="@drawable/ic_baseline_insert_emoticon_24"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/button_emoji"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.google.android.material.checkbox.MaterialCheckBox
|
||||
android:id="@+id/sensitive_media"
|
||||
|
@ -148,56 +130,56 @@
|
|||
android:layout_marginHorizontal="6dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/attachments_list_container" />
|
||||
|
||||
<com.google.android.material.button.MaterialButtonToggleGroup
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_attach"
|
||||
style="@style/Widget.Material3.Button.IconButton.Outlined"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/action_container"
|
||||
android:layout_marginVertical="6dp"
|
||||
android:layout_marginStart="6dp"
|
||||
app:icon="@drawable/ic_compose_attach"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_area_divider">
|
||||
app:layout_constraintTop_toBottomOf="@id/text_area_divider" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_attach"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton.Icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
app:icon="@drawable/ic_compose_attach"
|
||||
android:padding="0dp"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp" />
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_sensitive"
|
||||
style="@style/Widget.Material3.Button.IconButton.Outlined"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="6dp"
|
||||
android:checkable="true"
|
||||
app:icon="@drawable/ic_compose_sensitive"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/button_attach"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_area_divider"
|
||||
app:toggleCheckedStateOnClick="true" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_sensitive"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton.Icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginTop="6dp"
|
||||
app:icon="@drawable/ic_compose_sensitive"
|
||||
android:padding="0dp"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_visibility"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton.Icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
app:icon="@drawable/ic_compose_visibility_public"
|
||||
android:padding="0dp"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_language"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton.Icon"
|
||||
android:layout_width="70dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="?colorPrimary"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
tools:text="EN" />
|
||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_visibility"
|
||||
style="@style/Widget.Material3.Button.IconButton.Outlined"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="6dp"
|
||||
app:icon="@drawable/ic_compose_visibility_public"
|
||||
app:iconGravity="textStart"
|
||||
app:iconPadding="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/button_sensitive"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_area_divider" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_language"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="6dp"
|
||||
android:fontFamily="monospace"
|
||||
android:minWidth="72dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/button_visibility"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_area_divider"
|
||||
tools:text="EN" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/character_count"
|
||||
|
@ -207,7 +189,7 @@
|
|||
android:text="0"
|
||||
app:layout_constraintBottom_toTopOf="@id/character_progress"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_post"
|
||||
app:layout_constraintStart_toEndOf="@id/action_container"
|
||||
app:layout_constraintStart_toEndOf="@id/button_language"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_area_divider"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:ignore="HardcodedText" />
|
||||
|
@ -219,16 +201,18 @@
|
|||
android:layout_height="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_post"
|
||||
app:layout_constraintStart_toEndOf="@id/action_container"
|
||||
app:layout_constraintStart_toEndOf="@id/button_language"
|
||||
app:layout_constraintTop_toBottomOf="@id/character_count" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_post"
|
||||
style="@style/Widget.Material3.Button.Icon"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
style="@style/Widget.Material3.Button.IconButton.Filled"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginVertical="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
app:icon="@drawable/ic_compose_post"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_area_divider" />
|
||||
|
||||
|
@ -236,9 +220,8 @@
|
|||
android:id="@+id/attachment_choices_panel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:elevation="2dp"
|
||||
android:background="?colorSurface"
|
||||
android:background="@drawable/bg_compose_panels"
|
||||
android:padding="6dp"
|
||||
android:visibility="gone"
|
||||
app:constraint_referenced_ids="button_attach_image,button_attach_audio,button_attach_video,button_close_attachment_panel,button_poll,button_attach_manual"
|
||||
app:flow_maxElementsWrap="3"
|
||||
|
@ -246,177 +229,125 @@
|
|||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_attach_image"
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:src="@drawable/ic_compose_attach_image" />
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_compose_attach_image" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_attach_audio"
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:src="@drawable/ic_compose_attach_audio" />
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_compose_attach_audio" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_attach_video"
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:src="@drawable/ic_compose_attach_video" />
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_compose_attach_video" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/button_attach_manual"
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:src="@drawable/ic_compose_attach_more" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_close_attachment_panel"
|
||||
style="@style/Widget.AppCompat.Button.Borderless"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:src="@drawable/ic_baseline_close_24" />
|
||||
style="@style/Widget.Material3.Button.IconButton.Outlined"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_baseline_close_24" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_poll"
|
||||
style="@style/Widget.AppCompat.Button.Borderless"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:src="@drawable/ic_compose_poll" />
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_compose_poll" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_attach_manual"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_compose_attach_more" />
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/visibility_panel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="6dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:elevation="2dp"
|
||||
android:background="@drawable/bg_compose_panels"
|
||||
android:gravity="bottom"
|
||||
android:orientation="vertical"
|
||||
android:padding="6dp"
|
||||
android:visibility="gone"
|
||||
android:background="?colorSurface"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="1">
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
<com.google.android.material.button.MaterialButtonToggleGroup
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical">
|
||||
android:orientation="vertical"
|
||||
android:padding="6dp"
|
||||
app:singleSelection="true">
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_visibility_direct"
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||
android:layout_width="0dp"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="36dp">
|
||||
android:insetTop="0dp"
|
||||
android:insetBottom="0dp"
|
||||
android:minHeight="36dp"
|
||||
android:text="@string/v_direct"
|
||||
android:textAlignment="textStart"
|
||||
app:icon="@drawable/ic_compose_visibility_direct" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_compose_visibility_direct"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_area_divider" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/v_direct"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:id="@+id/button_close_visibility_panel"
|
||||
style="@style/Widget.AppCompat.Button.Borderless"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:src="@drawable/ic_baseline_close_24" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/button_visibility_private"
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="36dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_compose_visibility_private"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_area_divider" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:layout_width="0dp"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_visibility_private"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_weight="1"
|
||||
android:insetTop="0dp"
|
||||
android:insetBottom="0dp"
|
||||
android:minHeight="36dp"
|
||||
android:text="@string/followers_only"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
|
||||
android:textAlignment="textStart"
|
||||
app:icon="@drawable/ic_compose_visibility_private" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/button_visibility_unlisted"
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="36dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_compose_visibility_unlisted"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_area_divider" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:layout_width="0dp"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_visibility_unlisted"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_weight="1"
|
||||
android:insetTop="0dp"
|
||||
android:insetBottom="0dp"
|
||||
android:minHeight="36dp"
|
||||
android:text="@string/v_unlisted"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
|
||||
android:textAlignment="textStart"
|
||||
app:icon="@drawable/ic_compose_visibility_unlisted" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
<androidx.appcompat.widget.LinearLayoutCompat
|
||||
android:id="@+id/button_visibility_public"
|
||||
style="@style/Widget.AppCompat.Button.Borderless.Colored"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="36dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:src="@drawable/ic_compose_visibility_public"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/text_area_divider" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:layout_width="0dp"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_visibility_public"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_weight="1"
|
||||
android:insetTop="0dp"
|
||||
android:insetBottom="0dp"
|
||||
android:minHeight="36dp"
|
||||
android:text="@string/v_public"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyMedium" />
|
||||
android:textAlignment="textStart"
|
||||
app:icon="@drawable/ic_compose_visibility_public" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
</com.google.android.material.button.MaterialButtonToggleGroup>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/button_close_visibility_panel"
|
||||
style="@style/Widget.Material3.Button.IconButton.Outlined"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="96dp"
|
||||
app:icon="@drawable/ic_baseline_close_24" />
|
||||
|
||||
</androidx.appcompat.widget.LinearLayoutCompat>
|
||||
|
||||
|
|
12
app/src/main/res/menu/option_nitter_timeline.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/action_displayname"
|
||||
android:title="@string/change_tag_column"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/action_nitter_manage_accounts"
|
||||
android:title="@string/manage_accounts"
|
||||
app:showAsAction="always" />
|
||||
</menu>
|
|
@ -550,7 +550,7 @@
|
|||
<string name="make_an_action">القيام بإجراء</string>
|
||||
<string name="translation">الترجمة</string>
|
||||
<string name="text_color_title">لون النص</string>
|
||||
<string name="text_color">تغيير لون النص في البقع</string>
|
||||
<string name="text_color">تغيير لون النص في الرسائل</string>
|
||||
<string name="pref_custom_theme">استخدم قالبا مخصصا</string>
|
||||
<string name="theming">المظهر</string>
|
||||
<string name="data_export_theme">تم تصدير القالب</string>
|
||||
|
@ -750,4 +750,65 @@
|
|||
<string name="admin_reject_obfuscate">تشويش اسم النطاق</string>
|
||||
<string name="type_default_theme_dark">المظهر الداكن الافتراضي</string>
|
||||
<string name="public_comment">تعليق للعامة</string>
|
||||
<string name="action_announcement_from_to">إعلان · %1$s - %2$s</string>
|
||||
<string name="label_shape">الشكل</string>
|
||||
<string name="pref_theme_base_summary">اختر ما إذا كان يجب أن تكون قاعدة الحُلة داكنة أو فاتحة</string>
|
||||
<string name="pref_contributor_summary">اختر حُلة أنشأها المساهمون</string>
|
||||
<string name="message_language">لغة الرسائل</string>
|
||||
<string name="post_message_text">إرسال الرسالة %d / %d</string>
|
||||
<string name="remove_status">إزالة المنشور</string>
|
||||
<string name="report_1_title">ألا تريد رؤية هذا؟</string>
|
||||
<string name="report_1_unfollow">أنت تتابع هذا الحساب. لكي لا ترى منشوراته في خيطك الرئيسي بعد الآن ، قم بإلغاء متابعته.</string>
|
||||
<string name="report_more">هل هناك أي شيء آخر تعتقد أننا يجب أن نعرفه؟</string>
|
||||
<string name="notif_display_poll_results">نتائج الاستطلاع</string>
|
||||
<string name="clear_all_notif">محو كافة الإشعارات</string>
|
||||
<string name="display_all_categories">عرض جميع الفئات</string>
|
||||
<string name="label_line">خط</string>
|
||||
<string name="fetch_more_messages">جلب المزيد من الرسائل…</string>
|
||||
<string name="also_boosted_by">شاركه أيضا:</string>
|
||||
<string name="set_display_translate_indication">عرض زر الترجمة دائما</string>
|
||||
<string name="confirmed">مؤكّد</string>
|
||||
<string name="display">عرض</string>
|
||||
<string name="label_oval">بيضاوي</string>
|
||||
<string name="open_draft">افتح المسودة</string>
|
||||
<string name="joined">انضم</string>
|
||||
<string name="recent_ip">أحدث عنوان إيبي</string>
|
||||
<string name="unconfirmed">غير مؤكد</string>
|
||||
<string name="report_val_more2">روابط ضارة أو مشاركة مزيفة أو ردود متكررة</string>
|
||||
<string name="notif_display_mentions">الإشارات</string>
|
||||
<string name="notif_display_favourites">المفضلة</string>
|
||||
<string name="clear_cache">مسح ذاكرة التخزين المؤقت</string>
|
||||
<string name="default_system_language">استخدم لغة النظام الافتراضية</string>
|
||||
<string name="toast_token">فشل التطبيق في الحصول على رمز مميز</string>
|
||||
<string name="refresh_every">إحضار الإشعارات كل:</string>
|
||||
<string name="export_settings">تصدير الإعدادات</string>
|
||||
<string name="set_single_topbar_title">شريط إجراءات واحد</string>
|
||||
<string name="report_3_title">أي قواعد تم انتهاكها؟</string>
|
||||
<string name="invite_join_the_fediverse">أهلاً! ندعوك للانضمام إلى الـ Fediverse.</string>
|
||||
<string name="set_display_bookmark_indication">عرض زر الفاصلة المرجعية دائما</string>
|
||||
<string name="mark_all_as_read">وضع علامة مقروءة على جميع الإشعارات</string>
|
||||
<string name="also_favourite_by">"يفضله أيضًا: "</string>
|
||||
<string name="label_rectangle">مستطيل</string>
|
||||
<string name="label_eraser_mode">وضع الممحاة</string>
|
||||
<string name="msg_save_image">هل تريد الخروج دون حفظ الصورة؟</string>
|
||||
<string name="tap_here_to_refresh_poll">اضغط هنا لتحديث استطلاع الرأي</string>
|
||||
<string name="set_timelines_in_a_list_title">الخيوط الزمنية في قائمة</string>
|
||||
<string name="action_unfollow_tag_confirm">هل أنت متأكد من أنك تريد إلغاء متابعة هذا الوسم؟</string>
|
||||
<string name="action_unfollow_tag">إلغاء متابعة الوسم</string>
|
||||
<string name="report_val_more3">أنت مدرك أنه يخالف قواعد محددة</string>
|
||||
<string name="report_more_additional">تعليقات إضافية</string>
|
||||
<string name="report_more_remote">الحساب من خادم آخر. هل تريد إرسال نسخة مجهولة من التقرير هناك أيضًا؟</string>
|
||||
<string name="report_more_forward">إعادة التوجيه إلى %1$s</string>
|
||||
<string name="report_sent">تم إرسال التقرير!</string>
|
||||
<string name="dont_have_an_account">لا تملك حسابا؟</string>
|
||||
<string name="join_the_fediverse">انضم إلى الفديفرس</string>
|
||||
<string name="notif_display_reblogs">إعادة التدوين</string>
|
||||
<string name="filter">عامل التصفية</string>
|
||||
<string name="delete_cache">حذف ذاكرة التخزين المؤقت</string>
|
||||
<string name="release_notes">ملاحظات الإصدار</string>
|
||||
<string name="load_media_type_title">تحميل الصور المصغرة للوسائط</string>
|
||||
<string name="display_media">عرض الوسائط</string>
|
||||
<string name="import_settings">استيراد الإعدادت</string>
|
||||
<string name="media_cannot_be_uploaded">لا يمكن تحميل الوسائط!</string>
|
||||
<string name="origin_report">أصل الحساب المبلغ عنه</string>
|
||||
</resources>
|
|
@ -49,7 +49,7 @@
|
|||
<string name="action_open_in_web">Otevřít v prohlížeči</string>
|
||||
<string name="translate">Přeložit</string>
|
||||
<!--- Menu -->
|
||||
<string name="home_menu">Domů</string>
|
||||
<string name="home_menu">Domov</string>
|
||||
<string name="local_menu">Místní časová osa</string>
|
||||
<string name="muted_menu">Ztlumení uživatelé</string>
|
||||
<string name="blocked_menu">Blokovaní uživatelé</string>
|
||||
|
@ -59,8 +59,8 @@
|
|||
<string name="send_email">Poslat e-mail</string>
|
||||
<string name="scheduled_toots">Naplánované zprávy</string>
|
||||
<string name="disclaimer_full">Níže uvedené informace mohou popisovat uživatelský profil neúplně.</string>
|
||||
<string name="insert_emoji">Vložit smajlík</string>
|
||||
<string name="no_emoji">Aplikace prozatím nenačetla uživatelské smajlíky.</string>
|
||||
<string name="insert_emoji">Vložit emoji</string>
|
||||
<string name="no_emoji">Aplikace prozatím nenasbírala uživatelské emoji.</string>
|
||||
<string name="logout_account_confirmation">Opravdu se chcete odhlásit od @%1$s@%2$s\?</string>
|
||||
<!-- Status -->
|
||||
<string name="no_status">Žádné zprávy k zobrazení</string>
|
||||
|
@ -341,7 +341,7 @@
|
|||
<string name="profile_banner">Hlavička profilu</string>
|
||||
<string name="contact_instance_admin">Kontaktovat administrátora instance</string>
|
||||
<string name="mastohost_logo">Logo MastoHost</string>
|
||||
<string name="emoji_picker">Výběr emotikonů</string>
|
||||
<string name="emoji_picker">Výběr emoji</string>
|
||||
<string name="expand_conversation">Ukázat celou konverzaci</string>
|
||||
<string name="custom_emoji_picker">Uživatelský výběr emoji</string>
|
||||
<string name="favicon">Favicon</string>
|
||||
|
@ -367,7 +367,7 @@
|
|||
<string name="none_tags">Žádný</string>
|
||||
<string name="some_words_any">Kterékoliv slovo (odělené mezerami)</string>
|
||||
<string name="some_words_all">Všechna slova (oddělená mezerami)</string>
|
||||
<string name="some_tags">Add some words to filter (space-separated)</string>
|
||||
<string name="some_tags">Přidejte do filtru nějaká slova (oddělená mezerami)</string>
|
||||
<string name="change_tag_column">Změnit název sloupce</string>
|
||||
<string name="misskey_instance">Instance Misskey</string>
|
||||
<string name="trending">Populární</string>
|
||||
|
@ -478,7 +478,7 @@
|
|||
<string name="unsuspend">Zrušit pozastavení</string>
|
||||
<string name="audio">Zvuk</string>
|
||||
<string name="voice_message">Hlasová zpráva</string>
|
||||
<string name="set_enable_time_slot_indication">Během časového úseku budu aplikace posílat upozornění. Můžete to pro tento úsek změnit (tzn. ztišit) pomocí ovládacího prvku vpravo.</string>
|
||||
<string name="set_enable_time_slot_indication">Během časového úseku bude aplikace posílat upozornění. Můžete to pro tento úsek změnit (tzn. ztišit) pomocí ovládacího prvku vpravo.</string>
|
||||
<string name="set_fit_preview_indication">Náhledy nebudou v časových osách oříznuty</string>
|
||||
<string name="set_capitalize_indication">Automaticky vkládat zalomení řádku za zmínku, aby bylo první písmeno velké</string>
|
||||
<string name="settings_title_custom_sharing_indication">Umožňuje tvůrcům obsahu sdílet statusy do jejich kanálů RSS</string>
|
||||
|
@ -492,7 +492,7 @@
|
|||
\n
|
||||
\nMůžete přidat dodatečný obsah. Děkuji!</string>
|
||||
<string name="visibility">Viditelnost</string>
|
||||
<string name="set_disable_animated_emoji">Disable custom animated emojis</string>
|
||||
<string name="set_disable_animated_emoji">Vypnout vlastní animované emoji</string>
|
||||
<string name="report_account">Nahlásit účet</string>
|
||||
<plurals name="number_of_voters">
|
||||
<item quantity="one">%d hlasující</item>
|
||||
|
@ -794,7 +794,7 @@
|
|||
<string name="Suggestions">Návrhy</string>
|
||||
<string name="remember_position">Zapamatovat pozici v časových osách</string>
|
||||
<string name="toast_pin">Zpráva byla připnuta</string>
|
||||
<string name="set_live_translate_title">Přeložit zprávy</string>
|
||||
<string name="set_live_translate_title">Překládat zprávy</string>
|
||||
<string name="moderator">Moderátor</string>
|
||||
<string name="administrator">Administrátor</string>
|
||||
<string name="confirmed">Potvrzeno</string>
|
||||
|
@ -910,7 +910,7 @@
|
|||
<string name="set_language_picker">Umožňuje omezit seznam jazyků ve výběru při vytváření zprávy.</string>
|
||||
<string name="change_logo_description">Změnit logo aplikace na vašem zařízení</string>
|
||||
<string name="set_dynamic_color_indication">Přizpůsobuje tonalitu barevného schématu podle vaší osobní tapety.</string>
|
||||
<string name="messages_in_cache_for_home">Zprávy v cache pro domovskou časovou osu</string>
|
||||
<string name="messages_in_cache_for_home">Zprávy v cache pro domov</string>
|
||||
<string name="push_distributors">Distributor pro push</string>
|
||||
<string name="notif_update">Upravil(a) zprávu</string>
|
||||
<string name="filter_action_explanations">Vyberte, kterou akci provést, pokud bude příspěvek vyhovovat filtru</string>
|
||||
|
@ -935,7 +935,11 @@
|
|||
<string name="mute_them_all">Ztlumit všechny</string>
|
||||
<string name="muted_menu_home">Domácí ztlumení uživatelé</string>
|
||||
<string name="account_silenced">Účet ztišen</string>
|
||||
<string name="mute_home">Ztlumit pro domovskou časovou osu</string>
|
||||
<string name="unmute_home">Zrušit ztlumení pro domovskou časovou osu</string>
|
||||
<string name="mute_home">Ztlumit pro domov</string>
|
||||
<string name="unmute_home">Zrušit ztlumení pro domov</string>
|
||||
<string name="put_all_accounts_in_home_muted">Všechny účtu budou na domovské časové ose ztlumeny.</string>
|
||||
<string name="import_data">Importovat data</string>
|
||||
<string name="add_all_users_home_muted">Přidat všechny uživatele pro ztlumený domov</string>
|
||||
<string name="display">Zobrazit</string>
|
||||
<string name="group_reblogs">Seskupovat reblogy v domovské časové ose</string>
|
||||
</resources>
|
|
@ -532,7 +532,7 @@
|
|||
<string name="make_an_action">Aktion ausführen</string>
|
||||
<string name="translation">Übersetzung</string>
|
||||
<string name="text_color_title">Textfarbe</string>
|
||||
<string name="text_color">Ändern Du die Textfarbe in posts</string>
|
||||
<string name="text_color">Ändere die Textfarbe in Nachrichten</string>
|
||||
<string name="pref_custom_theme">Verwenden Du ein benutzerdefiniertes Design</string>
|
||||
<string name="theming">Farbschema</string>
|
||||
<string name="data_export_theme">Das Theme wurde exportiert</string>
|
||||
|
@ -904,4 +904,34 @@
|
|||
<string name="type_default_theme_dark">Standard dunkles Design</string>
|
||||
<string name="type_of_theme">Wähle den Modus für das Design</string>
|
||||
<string name="pref_customize">Farben anpassen</string>
|
||||
<string name="set_cardview_indication">Wenn aktiviert, sind die Beiträge in der Timeline schattiert und erhöht.</string>
|
||||
<string name="set_customize_light">Helles Thema anpassen</string>
|
||||
<string name="set_customize_light_indication">Gestattet, einige Elemente des hellen Themas zu personalisieren.</string>
|
||||
<string name="set_customize_dark">Dunkles Thema anpassen</string>
|
||||
<string name="set_customize_dark_indication">Gestattet, einige Elemente des dunklen Themas zu personalisieren.</string>
|
||||
<string name="set_custom_colors">Eigene Farben auswählen</string>
|
||||
<string name="light_custom_colors">Hell - Eigene Farben</string>
|
||||
<string name="cark_custom_colors">Dunkel - Eigene Farben</string>
|
||||
<string name="display_remote_conversation">Entfernte Konversation anzeigen</string>
|
||||
<string name="toast_on_your_instance">Die Konversation begann auf Deiner Instanz!</string>
|
||||
<string name="toast_error_fetch_message">Die Anwendung hat keine entfernten Nachrichten gefunden.</string>
|
||||
<string name="mute_tag_action">Tag stummschalten</string>
|
||||
<string name="unpin_tag">Anpinnen des Tags aufheben</string>
|
||||
<string name="unmute_tag_action">Stummschaltung des Tags aufheben</string>
|
||||
<string name="toast_try_later">Bitte später nochmal versuchen.</string>
|
||||
<string name="pin_tag">Tag anpinnen</string>
|
||||
<string name="put_all_accounts_in_home_muted">Alle Konten werden in der Timeline der Startseite stumm geschaltet.</string>
|
||||
<string name="add_all_users_home_muted">Alle Benutzer auf der Startseite stummschalten</string>
|
||||
<string name="import_data">Daten importieren</string>
|
||||
<string name="group_reblogs">Gruppiere geteilte Nachrichten in der Timeline der Startseite</string>
|
||||
<string name="mute_them_all">Alle stummschalten</string>
|
||||
<string name="manage_accounts">Konten verwalten</string>
|
||||
<string name="muted_menu_home">Auf Startseite stummgeschaltete Nutzer</string>
|
||||
<string name="unfollow_tag">Tag entfolgen</string>
|
||||
<string name="mute_home">Für Startseite stummschalten</string>
|
||||
<string name="unmute_home">Stummschaltung auf der Startseite aufheben</string>
|
||||
<string name="set_remove_left_margin">Entfernt den linken Rand in den Timelines, um die Nachricht kompakter zu machen</string>
|
||||
<string name="set_display_translate_indication">Übersetzungs-Knopf immer anzeigen</string>
|
||||
<string name="set_cardview">Kartenansicht</string>
|
||||
<string name="set_remove_left_margin_title">Entferne linken Rand</string>
|
||||
</resources>
|
|
@ -35,8 +35,8 @@
|
|||
<string name="open_with">Malfermi per</string>
|
||||
<string name="validate">Validigu</string>
|
||||
<string name="media">Aŭdovidaĵoj</string>
|
||||
<string name="share_with">Konigi kun</string>
|
||||
<string name="shared_via">Komuna tra Fedilab</string>
|
||||
<string name="share_with">Konigi per</string>
|
||||
<string name="shared_via">Konigita per Fedilab</string>
|
||||
<string name="replies">Respondoj</string>
|
||||
<string name="username">Uzantnomo</string>
|
||||
<string name="drafts">Malnetoj</string>
|
||||
|
@ -45,7 +45,7 @@
|
|||
<string name="mention">Mencioj</string>
|
||||
<string name="reblog">Diskonigoj</string>
|
||||
<string name="show_boosts">Montri diskonigojn</string>
|
||||
<string name="show_replies">Montri la respondojn</string>
|
||||
<string name="show_replies">Montri respondojn</string>
|
||||
<string name="action_open_in_web">Malfermu en retumilo</string>
|
||||
<string name="translate">Traduku</string>
|
||||
<!--- Menu -->
|
||||
|
@ -56,12 +56,12 @@
|
|||
<string name="notifications">Sciigoj</string>
|
||||
<string name="follow_request">Petoj de sekvado</string>
|
||||
<string name="settings">Agordoj</string>
|
||||
<string name="send_email">Sendi per retmesaĝo</string>
|
||||
<string name="send_email">Sendi retpoŝton</string>
|
||||
<string name="scheduled_toots">Planitaj hupoj</string>
|
||||
<string name="disclaimer_full">Subaj informoj povas nekomplete prezenti la profilon de la uzanto.</string>
|
||||
<string name="insert_emoji">Enmeti emoĝion</string>
|
||||
<string name="no_emoji">The app did not collect custom emojis for the moment.</string>
|
||||
<string name="logout_account_confirmation">Are you sure you want to logout @%1$s@%2$s?</string>
|
||||
<string name="no_emoji">La apo ne havas adaptitajn emoĝiojn ĉi-okaze.</string>
|
||||
<string name="logout_account_confirmation">Ĉu vi certas ke vi volas elsaluti\? @%1$s@%2$s\?</string>
|
||||
<!-- Status -->
|
||||
<string name="no_status">Neniu hupo por montri</string>
|
||||
<string name="favourite_add">Aldoni hupon al viajn stelumajn\?</string>
|
||||
|
@ -75,7 +75,7 @@
|
|||
<string name="more_action_5">Kopii</string>
|
||||
<string name="more_action_6">Konigi</string>
|
||||
<string name="more_action_7">Mencii</string>
|
||||
<string name="more_action_8">Timed mute</string>
|
||||
<string name="more_action_8">Tempumita silentigo</string>
|
||||
<string name="more_action_9">Forigi kaj reskribi</string>
|
||||
<string-array name="more_action_confirm">
|
||||
<item>Silentigi ĉi-tiun konton?</item>
|
||||
|
@ -125,95 +125,99 @@
|
|||
<string name="toot_error_no_content">Via hupo estas malplena!</string>
|
||||
<string name="toot_sent">La hupo estis sendita!</string>
|
||||
<string name="toot_sensitive">Tikla enhavo?</string>
|
||||
<string name="no_draft">Neniu malnetoj!</string>
|
||||
<string name="choose_accounts">Elekti kontojn</string>
|
||||
<string name="select_accounts">Elekti kelkaj kontoj</string>
|
||||
<string name="remove_draft">Forigi malneto?</string>
|
||||
<string name="upload_form_description">Describe for the visually impaired</string>
|
||||
<string name="no_draft">Neniu malneto!</string>
|
||||
<string name="choose_accounts">Elekti konton</string>
|
||||
<string name="select_accounts">Elektu kelkajn kontojn</string>
|
||||
<string name="remove_draft">Forigi malneton\?</string>
|
||||
<string name="upload_form_description">Priskribu por vidhandikapitoj</string>
|
||||
<!-- Instance -->
|
||||
<string name="instance_no_description">Neniu priskribo havebla!</string>
|
||||
<!-- About -->
|
||||
<string name="about_vesrion">Release %1$s</string>
|
||||
<string name="about_vesrion">Eldono %1$s</string>
|
||||
<string name="about_developer">Programistoj:</string>
|
||||
<string name="about_license">Permesilo: </string>
|
||||
<string name="about_license_action">GNU GPL V3</string>
|
||||
<string name="about_code">Kodo: </string>
|
||||
<string name="about_thekinrar">Serĉi instencoj:</string>
|
||||
<string name="about_thekinrar">Serĉi instancojn:</string>
|
||||
<!-- Conversation -->
|
||||
<!-- Accounts -->
|
||||
<string name="no_accounts">Neniu konto por montri</string>
|
||||
<string name="no_follow_request">Ne sekva peton</string>
|
||||
<string name="no_follow_request">Neniu peto pri sekvado</string>
|
||||
<string name="status_cnt">Hupoj
|
||||
\n %1$s</string>
|
||||
<string name="following_cnt">Sekvante \n %1$s</string>
|
||||
<string name="followers_cnt">Sekvante \n %1$s</string>
|
||||
<string name="following_cnt">Sekvas
|
||||
\n %1$s</string>
|
||||
<string name="followers_cnt">Sekvantoj
|
||||
\n %1$s</string>
|
||||
<string name="reject">Malakcepti</string>
|
||||
<!-- Scheduled toots -->
|
||||
<string name="no_scheduled_toots">Neniu planitaj hupoj por montri!</string>
|
||||
<string name="remove_scheduled">Forigi planita mesaĝon?</string>
|
||||
<string name="toot_scheduled">La mesaĝo estis planita!</string>
|
||||
<string name="toot_scheduled_date">La planita dato devas pli granda ol la nunhoro!</string>
|
||||
<string name="toot_scheduled_date">La planita dato devas pli malfrua ol la nuna horo!</string>
|
||||
<!-- timed mute -->
|
||||
<string name="timed_mute_date_error">The time for muting should be greater than one minute.</string>
|
||||
<string name="timed_mute_date">%1$s has been muted until %2$s.\n You can unmute this account from their profile page.</string>
|
||||
<string name="timed_mute_profile">%1$s is muted until %2$s.\n Tap here to unmute the account.</string>
|
||||
<string name="timed_mute_date_error">La tempo por silentigo devas esti pli longa ol unu minuto.</string>
|
||||
<string name="timed_mute_date">%1$s silentigita ĝis %2$s.
|
||||
\nVi povas malsilentigi ĉi tiun konton en ĝia profilpaĝo.</string>
|
||||
<string name="timed_mute_profile">%1$s silentigita ĝis %2$s.
|
||||
\nKlaku ĉi tie por malsilentigi la konton.</string>
|
||||
<!-- Notifications -->
|
||||
<string name="no_notifications">Neniun sciigon por montri</string>
|
||||
<string name="no_notifications">Neniu sciigo por montri</string>
|
||||
<string name="notif_mention">menciis vin</string>
|
||||
<string name="notif_status">wrote a new message</string>
|
||||
<string name="notif_reblog">{name} diskonigis vian mesaĝon</string>
|
||||
<string name="notif_favourite">{name} stelumis vian mesaĝon</string>
|
||||
<string name="notif_status">verkis novan mesaĝon</string>
|
||||
<string name="notif_reblog">diskonigis vian mesaĝon</string>
|
||||
<string name="notif_favourite">stelumis vian mesaĝon</string>
|
||||
<string name="notif_follow">eksekvis vin</string>
|
||||
<string name="notif_follow_request">asked to follow you</string>
|
||||
<string name="delete_notification_ask_all">Forigi sciigoj?</string>
|
||||
<string name="delete_notification_all">All notifications have been deleted!</string>
|
||||
<string name="notif_follow_request">petis sekvi vin</string>
|
||||
<string name="delete_notification_ask_all">Forigi ĉiujn sciigojn\?</string>
|
||||
<string name="delete_notification_all">Ĉiuj sciigoj forigitaj!</string>
|
||||
<!-- HEADER -->
|
||||
<string name="followers">Sekvitaj</string>
|
||||
<string name="followers">Sekvantoj</string>
|
||||
<!-- TOAST -->
|
||||
<string name="client_error">Unable to get client id!</string>
|
||||
<string name="client_error">Ne eblas ricevi la klientan identigilon!</string>
|
||||
<string name="toast_block">La konto estis blokita!</string>
|
||||
<string name="toast_unblock">La konto ne plu estas blokita!</string>
|
||||
<string name="toast_mute">La konto estis silentigita!</string>
|
||||
<string name="toast_unmute">La konto ne plu silentigita!</string>
|
||||
<string name="toast_follow">La konto estis sekvita!</string>
|
||||
<string name="toast_unfollow">La konto estas ne plu sekvas!</string>
|
||||
<string name="toast_follow">La konto estas eksekvita!</string>
|
||||
<string name="toast_unfollow">La konto estas ne plu estas sekvata!</string>
|
||||
<string name="toast_reblog">The toot was boosted!</string>
|
||||
<string name="toast_unreblog">The toot is no longer boosted!</string>
|
||||
<string name="toast_favourite">La hup estis aldonita al stelumoj!</string>
|
||||
<string name="toast_unfavourite">The toot was removed from your favourites!</string>
|
||||
<string name="toast_error">Ups ! Eraro okazis!</string>
|
||||
<string name="toast_code_error">An error occurred! The instance did not return an authorisation code!</string>
|
||||
<string name="toast_error_instance">The instance domain does not seem to be valid!</string>
|
||||
<string name="toast_error_loading_account">An error occurred while switching between accounts!</string>
|
||||
<string name="toast_error_search">An error occurred while searching!</string>
|
||||
<string name="nothing_to_do">No action can be taken</string>
|
||||
<string name="toast_error_translate">Eraro okazis dum traduktas!</string>
|
||||
<string name="toast_code_error">Eraro okazis! La instanco ne donis rajtigan kodon!</string>
|
||||
<string name="toast_error_instance">La adreso de la instanco ne ŝajnas valida!</string>
|
||||
<string name="toast_error_loading_account">Okazis eraro dum ŝanĝado de kontoj!</string>
|
||||
<string name="toast_error_search">Okazis eraro dum serĉado!</string>
|
||||
<string name="nothing_to_do">Neniu ago eblas</string>
|
||||
<string name="toast_error_translate">Eraro okazis dum tradukado!</string>
|
||||
<!-- Settings -->
|
||||
<string name="set_toots_page">Number of toots per load</string>
|
||||
<string name="set_disable_gif">Disable GIF avatars</string>
|
||||
<string name="set_notif_follow">Notify when someone follows you</string>
|
||||
<string name="set_notif_follow_share">Notify when someone boosts your status</string>
|
||||
<string name="set_notif_follow_add">Notify when someone favourites your status</string>
|
||||
<string name="set_notif_follow_mention">Notify when someone mentions you</string>
|
||||
<string name="set_notif_follow_poll">Notify when a poll ended</string>
|
||||
<string name="set_notif_status">Notify for new posts</string>
|
||||
<string name="set_share_validation">Show confirmation dialog before boosting</string>
|
||||
<string name="set_share_validation_fav">Show confirmation dialog before adding to favourites</string>
|
||||
<string name="set_disable_gif">Malŝalti GIF-profilbildojn</string>
|
||||
<string name="set_notif_follow">Sciigu kiam iu eksekvas vin</string>
|
||||
<string name="set_notif_follow_share">Sciigu kiam iu diskonigas vian afiŝon</string>
|
||||
<string name="set_notif_follow_add">Sciigu kiam iu stelumas vian statuson</string>
|
||||
<string name="set_notif_follow_mention">Sciigu kiam iu mencias vin</string>
|
||||
<string name="set_notif_follow_poll">Sciigu kiam enketo finiĝas</string>
|
||||
<string name="set_notif_status">Sciigu pri novaj afiŝoj</string>
|
||||
<string name="set_share_validation">Montru konfirman dialogon antaŭ diskonigo</string>
|
||||
<string name="set_share_validation_fav">Montru konfirman dialogon antaŭ aldono al legosignoj</string>
|
||||
<string name="set_notify">Sciigi?</string>
|
||||
<string name="set_notif_silent">Silent Notifications</string>
|
||||
<string name="set_nsfw_timeout">NSFW view timeout (seconds, 0 means off)</string>
|
||||
<string name="set_med_desc_timeout">Media Description timeout (seconds, 0 means off)</string>
|
||||
<string name="settings_title_custom_sharing">Custom sharing</string>
|
||||
<string name="settings_custom_sharing_url">Your custom sharing URL…</string>
|
||||
<string name="set_notif_silent">Silentaj sciigoj</string>
|
||||
<string name="set_nsfw_timeout">Tempolimo de NSFW-vido (sekundoj, 0 estas malŝalto)</string>
|
||||
<string name="set_med_desc_timeout">Tempolimo de aŭdvida priskribo (sekundoj, 0 estas malŝalto)</string>
|
||||
<string name="settings_title_custom_sharing">Adaptita kundivido</string>
|
||||
<string name="settings_custom_sharing_url">URL por via adaptita kundivido…</string>
|
||||
<string name="set_lock_account">Ŝlosi konton</string>
|
||||
<string name="set_save_changes">Konservi ŝanĝojn</string>
|
||||
<string name="set_fit_preview">Fit preview images</string>
|
||||
<string name="set_fit_preview">Adapti antaŭvidajn bildojn</string>
|
||||
<string name="settings_time_from">Inter</string>
|
||||
<string name="settings_time_to">kaj</string>
|
||||
<string name="embedded_browser">Use the built-in browser</string>
|
||||
<string name="embedded_browser">Uzu la enkonstruitan retumilon</string>
|
||||
<string name="custom_tabs">Propra langetoj</string>
|
||||
<string name="expand_cw">Automatically expand cw</string>
|
||||
<string name="set_led_colour">Set LED colour:</string>
|
||||
<string name="expand_cw">Aŭtomate montri enhavon sub averto</string>
|
||||
<string name="set_led_colour">Agordi koloron de LED:</string>
|
||||
<string-array name="led_colours">
|
||||
<item>Blua</item>
|
||||
<item>Cejana</item>
|
||||
|
@ -229,8 +233,8 @@
|
|||
<string name="action_unmute">Malsilentigu</string>
|
||||
<string name="request_sent">Peto sendita</string>
|
||||
<string name="followed_by">Sekvas vin</string>
|
||||
<string name="set_capitalize">First letter in capital for replies</string>
|
||||
<string name="set_resize_picture">Resize pictures</string>
|
||||
<string name="set_capitalize">Unua litero majuskle por respondoj</string>
|
||||
<string name="set_resize_picture">Adaptu fotojn</string>
|
||||
<string name="set_resize_video">Regrandigi videoj</string>
|
||||
<!-- Quick settings for notifications -->
|
||||
<!-- CACHE -->
|
||||
|
|
|
@ -440,7 +440,7 @@
|
|||
<string name="agreement_check">J\'accepte les %1$s et les %2$s</string>
|
||||
<string name="server_rules">règles du serveur</string>
|
||||
<string name="tos">conditions de service</string>
|
||||
<string name="sign_up">S\'inscrire</string>
|
||||
<string name="sign_up">S’inscrire</string>
|
||||
<string name="validation_needed">Cette instance fonctionne avec des invitations. Votre compte devra être approuvé manuellement par un·e administrateur·rice pour qu\'il devienne utilisable.</string>
|
||||
<string name="password_error">Les mots de passe ne sont pas identiques !</string>
|
||||
<string name="email_error">L\'adresse ne semble pas être valide !</string>
|
||||
|
@ -864,7 +864,7 @@
|
|||
<string name="channel_notif_report">Nouveau signalement</string>
|
||||
<string name="notif_update_push">Un message que vous avez partagé a été édité</string>
|
||||
<string name="notif_sign_up">Un⋅e utilisateur⋅ice s\'est abonné⋅e</string>
|
||||
<string name="action_privacy_policy">Politique de confidentialité</string>
|
||||
<string name="action_privacy_policy">Politique de vie privée</string>
|
||||
<string name="no_blocked_domains">Vous n’avez pas bloqué de domaines</string>
|
||||
<string name="unpin_timeline">Supprimer le fil épinglé \?</string>
|
||||
<string name="action_pinned_delete">Supprimer les fils épinglés \?</string>
|
||||
|
@ -877,7 +877,7 @@
|
|||
<string name="public_comment">Commentaire public</string>
|
||||
<string name="saved_changes">Les modifications ont été enregistrées !</string>
|
||||
<string name="create_domain_block">Créer un bloquage de domaine</string>
|
||||
<string name="unblock_domain_confirm">Êtes-vous sûr de vouloir débloquer %1$s \?</string>
|
||||
<string name="unblock_domain_confirm">Êtes-vous certain de débloquer %1$s \?</string>
|
||||
<string name="notif_reported">a envoyé un rapport</string>
|
||||
<string name="blocked_domains">Domaines bloqués</string>
|
||||
<string name="reject_reports">Rejeter les rapports</string>
|
||||
|
@ -887,17 +887,17 @@
|
|||
<string name="unpin_timeline_description">Voulez-vous vraiment désépingler ce fil \?</string>
|
||||
<string name="set_notif_user_sign_up">Nouvelle inscription (modérateurs)</string>
|
||||
<string name="admin_domainblock_reject_media">Ignorer tous les rapports provenant de ce domaine. Non pertinent pour les suspensions</string>
|
||||
<string name="reject_media">Rejeter les médias</string>
|
||||
<string name="reject_media">Rejeter le média</string>
|
||||
<string name="admin_reject_reports">Rejeter les rapports</string>
|
||||
<string name="mute_tag">Voulez-vous vraiment masquer le tag %1$s \?</string>
|
||||
<string name="Suggestions">Suggestions</string>
|
||||
<string name="not_interested">Pas intéressé</string>
|
||||
<string name="set_notif_update">Avertir des mises à jour</string>
|
||||
<string name="set_notif_update">Notifier les mises à jour</string>
|
||||
<string name="delete_timeline">Supprimer le fil chronologique</string>
|
||||
<string name="notif_signed_up">Inscrit·e</string>
|
||||
<string name="type_default_theme_dark">Thème sombre par défaut</string>
|
||||
<string name="toast_error_fetch_message">L\'app n\'a pas trouvé le message distant.</string>
|
||||
<string name="order_lists">Ordonner les listes</string>
|
||||
<string name="order_lists">Ré-arranger les listes</string>
|
||||
<string name="severity">Sévérité</string>
|
||||
<string name="admin_domainblock_domain">Le blocage de domaine n\'empêchera pas la création d\'entrées de compte dans la base de données, mais appliquera rétroactivement et automatiquement des méthodes de modération spécifiques à ces comptes.</string>
|
||||
<string name="admin_reject_media">Rejeter les fichiers médias</string>
|
||||
|
@ -933,4 +933,9 @@
|
|||
<string name="muted_menu_home">utilisateur·ices silencé·es sur l\'accueil</string>
|
||||
<string name="notif_submitted_report">Soumettre un rapport</string>
|
||||
<string name="admin_domainblock_reject_obfuscate">Masquer partiellement le nom de domaine dans la liste si l\'option de publication restreinte de la liste de domaine est activée</string>
|
||||
<string name="import_data">Importer des données</string>
|
||||
<string name="manage_accounts">Gérer les comptes</string>
|
||||
<string name="admin_domainblock_public_comment">Commentaire à propos de la limitation de ce domaine au public, si l\'annonce de la liste de limitation de domaines est activée.</string>
|
||||
<string name="group_reblogs">Re-blog de groupe dans la timeline d’accueil</string>
|
||||
<string name="add_all_users_home_muted">Ajouter tout les utilisateurs masqués à l’accueil</string>
|
||||
</resources>
|
|
@ -916,4 +916,19 @@
|
|||
<string name="toast_on_your_instance">A conversa comezou na túa instancia!</string>
|
||||
<string name="cark_custom_colors">Escuro - Cores personais</string>
|
||||
<string name="set_cardview">Tarxetas elevadas</string>
|
||||
<string name="import_data">Importar datos</string>
|
||||
<string name="set_display_translate_indication">Mostrar sempre botón de tradución</string>
|
||||
<string name="mute_tag_action">Acalar cancelo</string>
|
||||
<string name="unmute_tag_action">Restablecer cancelo</string>
|
||||
<string name="unfollow_tag">Non seguir cancelo</string>
|
||||
<string name="pin_tag">Fixar cancelo</string>
|
||||
<string name="unpin_tag">Desafixar cancelo</string>
|
||||
<string name="mute_home">Acalar para Inicio</string>
|
||||
<string name="unmute_home">Non acalar para Inicio</string>
|
||||
<string name="add_all_users_home_muted">Engadir tódolas usuarias ao inicio acalado</string>
|
||||
<string name="put_all_accounts_in_home_muted">Tódalas contas serán acaladas para a cronoloxía de Inicio.</string>
|
||||
<string name="mute_them_all">Acalalas todas</string>
|
||||
<string name="muted_menu_home">Usuarias acalas en Inicio</string>
|
||||
<string name="group_reblogs">Comparticións para o grupo no Inicio</string>
|
||||
<string name="manage_accounts">Xestionar contas</string>
|
||||
</resources>
|
|
@ -915,4 +915,15 @@
|
|||
<string name="unfollow_tag">Non seguire etichetta</string>
|
||||
<string name="set_language_picker_title">Selettore lingua</string>
|
||||
<string name="display_remote_conversation">Mostra conversazione remota</string>
|
||||
<string name="report_val_more2">Link malevoli, siti ingannevoli, o risposte ripetitive</string>
|
||||
<string name="import_data">Importa dati</string>
|
||||
<string name="instance_health_uptime">Tempo online: %,.2f %%</string>
|
||||
<string name="report_all_more">Seleziona tutto</string>
|
||||
<string name="report_2_title">Ci sono dei post a supporto di questa segnalazione\?</string>
|
||||
<string name="replace_youtube_host">Dominio frontend YouTube</string>
|
||||
<string name="replace_twitter_host">Dominio frontend Twitter</string>
|
||||
<string name="replace_instagram_host">Dominio frontend Instagram</string>
|
||||
<string name="replace_reddit_host">Dominio frontend Reddit</string>
|
||||
<string name="boosted_by">Ricondiviso da</string>
|
||||
<string name="admin_domainblock_reject_obfuscate">Offusca parzialmente il nome di dominio nella lista se mostra le limitazioni di dominio é abilitato</string>
|
||||
</resources>
|
|
@ -13,19 +13,19 @@
|
|||
<string name="download">Baixar</string>
|
||||
<string name="download_file">Baixar %1$s</string>
|
||||
<string name="save_over">Mídia salva</string>
|
||||
<string name="download_from" formatted="false">Ficheiro: %1$s</string>
|
||||
<string name="download_from" formatted="false">Arquivo: %1$s</string>
|
||||
<string name="password">Senha</string>
|
||||
<string name="email">E-mail</string>
|
||||
<string name="accounts">Contas</string>
|
||||
<string name="toots">Mensagens</string>
|
||||
<string name="tags">Tags</string>
|
||||
<string name="tags">Etiquetas</string>
|
||||
<string name="save">Salvar</string>
|
||||
<string name="instance">Instância</string>
|
||||
<string name="instance_example">Instância: mastodon.social</string>
|
||||
<string name="toast_account_changed" formatted="false">Usando a conta %1$s agora</string>
|
||||
<string name="add_account">Adicionar conta</string>
|
||||
<string name="add_account">Adicionar uma conta</string>
|
||||
<string name="clipboard">O conteúdo da mensagem foi copiado para a área de transferência</string>
|
||||
<string name="clipboard_url">O link da mensagem foi copiado para a área de transferência</string>
|
||||
<string name="clipboard_url">A URL da mensagem foi copiada para a área de transferência</string>
|
||||
<string name="camera">Câmera</string>
|
||||
<string name="delete_all">Excluir tudo</string>
|
||||
<string name="schedule">Agendar</string>
|
||||
|
@ -33,7 +33,7 @@
|
|||
<string name="next">Próximo</string>
|
||||
<string name="previous">Anterior</string>
|
||||
<string name="open_with">Abrir com</string>
|
||||
<string name="validate">Ok</string>
|
||||
<string name="validate">Validar</string>
|
||||
<string name="media">Mídia</string>
|
||||
<string name="share_with">Compartilhar com</string>
|
||||
<string name="shared_via">Compartilhado via Fedilab</string>
|
||||
|
@ -58,14 +58,14 @@
|
|||
<string name="settings">Configurações</string>
|
||||
<string name="send_email">Mandar um e-mail</string>
|
||||
<string name="scheduled_toots">Mensagens agendadas</string>
|
||||
<string name="disclaimer_full">As informações abaixo podem refletir incompletamente o perfil do usuário.</string>
|
||||
<string name="disclaimer_full">As informações abaixo podem refletir o perfil do usuário de forma incompleta.</string>
|
||||
<string name="insert_emoji">Inserir emoji</string>
|
||||
<string name="no_emoji">O aplicativo não achou emojis personalizados no momento.</string>
|
||||
<string name="logout_account_confirmation">Tem a certeza que quer sair @%1$s@%2$s?</string>
|
||||
<string name="logout_account_confirmation">Você tem certeza que deseja sair de @%1$s@%2$s\?</string>
|
||||
<!-- Status -->
|
||||
<string name="no_status">Sem mensagens para mostrar</string>
|
||||
<string name="favourite_add">Favoritar essa mensagem\?</string>
|
||||
<string name="favourite_remove">Desfavoritar essa mensagem\?</string>
|
||||
<string name="no_status">Nenhuma mensagem para exibir</string>
|
||||
<string name="favourite_add">Adicionar esta mensagem aos seus favoritos\?</string>
|
||||
<string name="favourite_remove">Remover esta mensagem dos seus favoritos\?</string>
|
||||
<string name="reblog_add">Dar boost\?</string>
|
||||
<string name="reblog_remove">Desfazer boost\?</string>
|
||||
<string name="more_action_1">Silenciar</string>
|
||||
|
@ -75,7 +75,7 @@
|
|||
<string name="more_action_5">Copiar</string>
|
||||
<string name="more_action_6">Compartilhar</string>
|
||||
<string name="more_action_7">Mencionar</string>
|
||||
<string name="more_action_8">Silenciar até...</string>
|
||||
<string name="more_action_8">Silenciar temporariamente</string>
|
||||
<string name="more_action_9">Excluir & rascunhar</string>
|
||||
<string-array name="more_action_confirm">
|
||||
<item>Silenciar esta conta?</item>
|
||||
|
@ -93,9 +93,9 @@
|
|||
<item>Excluir este toot?</item>
|
||||
<item>Excluir & rascunhar este toot?</item>
|
||||
</string-array>
|
||||
<string name="bookmarks">Salvos</string>
|
||||
<string name="bookmark_add">Salvar</string>
|
||||
<string name="bookmark_remove">Remover dos Salvos</string>
|
||||
<string name="bookmarks">Favoritos</string>
|
||||
<string name="bookmark_add">Adicionar aos favoritos</string>
|
||||
<string name="bookmark_remove">Remover dos favoritos</string>
|
||||
<string name="status_bookmarked">Toot foi salvo!</string>
|
||||
<string name="status_unbookmarked">Toot foi removido dos Salvos!</string>
|
||||
<!-- Date -->
|
||||
|
@ -125,9 +125,9 @@
|
|||
</plurals>
|
||||
<!-- TOOT -->
|
||||
<string name="toot_select_image_error">Ocorreu um erro ao selecionar a mídia!</string>
|
||||
<string name="toot_delete_media">Remover mídia?</string>
|
||||
<string name="toot_delete_media">Remover esta mídia\?</string>
|
||||
<string name="toot_error_no_content">Sua mensagem está vazia!</string>
|
||||
<string name="toot_sent">Mensagem enviada!</string>
|
||||
<string name="toot_sent">A mensagem foi enviada!</string>
|
||||
<string name="toot_sensitive">Conteúdo sensível?</string>
|
||||
<string name="no_draft">Sem rascunhos!</string>
|
||||
<string name="choose_accounts">Escolha uma conta</string>
|
||||
|
@ -135,7 +135,7 @@
|
|||
<string name="remove_draft">Excluir rascunho?</string>
|
||||
<string name="upload_form_description">Descreva para os deficientes visuais</string>
|
||||
<!-- Instance -->
|
||||
<string name="instance_no_description">Sem descrição!</string>
|
||||
<string name="instance_no_description">Nenhuma descrição disponível!</string>
|
||||
<!-- About -->
|
||||
<string name="about_vesrion">Versão %1$s</string>
|
||||
<string name="about_developer">Desenvolvedor:</string>
|
||||
|
@ -145,7 +145,7 @@
|
|||
<string name="about_thekinrar">Procure instâncias:</string>
|
||||
<!-- Conversation -->
|
||||
<!-- Accounts -->
|
||||
<string name="no_accounts">Sem conta</string>
|
||||
<string name="no_accounts">Nenhuma conta para exibir</string>
|
||||
<string name="no_follow_request">Sem seguidores pendentes</string>
|
||||
<string name="status_cnt">Mensagens
|
||||
\n %1$s</string>
|
||||
|
@ -153,13 +153,14 @@
|
|||
<string name="followers_cnt">Seguidores \n %1$s</string>
|
||||
<string name="reject">Recusar</string>
|
||||
<!-- Scheduled toots -->
|
||||
<string name="no_scheduled_toots">Sem mensagens agendadas!</string>
|
||||
<string name="no_scheduled_toots">Nenhuma mensagem agendada para exibir!</string>
|
||||
<string name="remove_scheduled">Excluir mensagem agendada\?</string>
|
||||
<string name="toot_scheduled">Mensagem agendada!</string>
|
||||
<string name="toot_scheduled">A mensagem foi agendada!</string>
|
||||
<string name="toot_scheduled_date">A data de agendamento deve ser após o horário atual!</string>
|
||||
<!-- timed mute -->
|
||||
<string name="timed_mute_date_error">O tempo de silêncio deve ser maior do que um minuto.</string>
|
||||
<string name="timed_mute_date">%1$s foi silenciado até %2$s. \n Você pode desativar no perfil do usuário.</string>
|
||||
<string name="timed_mute_date_error">O tempo de silenciamento deve ser maior que um minuto.</string>
|
||||
<string name="timed_mute_date">%1$s foi silenciado até %2$s.
|
||||
\n Você pode anular o silenciar desta conta na página de perfil dela.</string>
|
||||
<string name="timed_mute_profile">%1$s está silenciado até %2$s. \n Toque aqui para desativar o silêncio.</string>
|
||||
<!-- Notifications -->
|
||||
<string name="no_notifications">Sem notificações</string>
|
||||
|
@ -418,12 +419,12 @@
|
|||
<string name="discard">Cancelar</string>
|
||||
<string name="saving">Salvando…</string>
|
||||
<string name="image_saved">Imagem salva!</string>
|
||||
<string name="save_image_failed">Falha ao salvar imagem.</string>
|
||||
<string name="save_image_failed">Falha ao salvar imagem</string>
|
||||
<string name="add_poll_item">Adicionar um item</string>
|
||||
<string name="mute_conversation">Silenciar conversa</string>
|
||||
<string name="unmute_conversation">Desativar silêncio</string>
|
||||
<string name="toast_unmute_conversation">Silêncio desativado!</string>
|
||||
<string name="toast_mute_conversation">Silêncio ativado!</string>
|
||||
<string name="toast_mute_conversation">A conversa está silenciada</string>
|
||||
<string name="category_general">Geral</string>
|
||||
<string name="category_regional">Regional</string>
|
||||
<string name="category_art">Arte</string>
|
||||
|
@ -489,6 +490,7 @@
|
|||
<string name="report_account">Denunciar conta</string>
|
||||
<plurals name="number_of_voters">
|
||||
<item quantity="one">%d votante</item>
|
||||
<item quantity="many">%d votantes</item>
|
||||
<item quantity="other">%d votantes</item>
|
||||
</plurals>
|
||||
<string-array name="poll_choice_type">
|
||||
|
@ -506,11 +508,11 @@
|
|||
</string-array>
|
||||
<string name="poll_duplicated_entry">A sua votação não pode ter opções duplicadas!</string>
|
||||
<string name="set_clear_cache_exit">Limpar cache ao sair</string>
|
||||
<string name="set_clear_cache_exit_indication">The cache (media, cached messages, data from the built-in browser) will be automatically cleared when leaving the application.</string>
|
||||
<string name="set_clear_cache_exit_indication">O cache (mídia, mensagens em cache, dados do navegador integrado) será limpo automaticamente ao sair do aplicativo.</string>
|
||||
<string name="unfollow_confirm">Quer deixar de seguir esta conta?</string>
|
||||
<string name="set_unfollow_validation">Mostrar diálogo de confirmação antes de deixar de seguir uma conta</string>
|
||||
<string name="replace_medium">Replace Medium links</string>
|
||||
<string name="replace_medium_description">Replace medium.com links with an open source alternative front-end focused on privacy.</string>
|
||||
<string name="replace_medium_description">Use um frontend alternativo para o Medium</string>
|
||||
<string name="replace_medium_host">Default: scribe.rip</string>
|
||||
<string name="set_push_notifications">Use um sistema notificações instantâneas para ter notificações em tempo real.</string>
|
||||
<string name="action_add_notes">Adicionar notas</string>
|
||||
|
@ -530,53 +532,57 @@
|
|||
<string name="clik_reset">Toque aqui para repor todas as suas cores personalizadas</string>
|
||||
<string name="reset">Reset</string>
|
||||
<string name="icons_color_title">Icons</string>
|
||||
<string name="icons_color">Color of bottom icons in timelines</string>
|
||||
<string name="logo_of_the_instance">Logo of the instance</string>
|
||||
<string name="edit_profile">Edit profile</string>
|
||||
<string name="make_an_action">Make an action</string>
|
||||
<string name="translation">Translation</string>
|
||||
<string name="icons_color">Cor dos ícones inferiores nas linhas do tempo</string>
|
||||
<string name="logo_of_the_instance">Logo da instância</string>
|
||||
<string name="edit_profile">Editar Perfil</string>
|
||||
<string name="make_an_action">Faça uma ação</string>
|
||||
<string name="translation">Tradução</string>
|
||||
<string name="text_color_title">Text color</string>
|
||||
<string name="text_color">Change the text color in messages</string>
|
||||
<string name="pref_custom_theme">Use a custom theme</string>
|
||||
<string name="theming">Theming</string>
|
||||
<string name="data_export_theme">The theme was exported</string>
|
||||
<string name="data_export_theme_success">The theme has been successfully exported in CSV</string>
|
||||
<string name="text_color">Alterar a cor do texto nas mensagens</string>
|
||||
<string name="pref_custom_theme">Use um tema personalizado</string>
|
||||
<string name="theming">Personalização</string>
|
||||
<string name="data_export_theme">O tema foi exportado</string>
|
||||
<string name="data_export_theme_success">O tema foi exportado com sucesso em CSV</string>
|
||||
<string name="import_theme">Import a theme</string>
|
||||
<string name="import_theme_title">Tap here to import a theme from a previous export</string>
|
||||
<string name="import_theme_title">Toque aqui para importar um tema exportado previamente</string>
|
||||
<string name="export_theme">Export the theme</string>
|
||||
<string name="export_theme_title">Tap here to export the current theme</string>
|
||||
<string name="theme_file_error">An error occurred when selecting the theme file</string>
|
||||
<string name="user_count">User count</string>
|
||||
<string name="status_count">Status count</string>
|
||||
<string name="instance_count">Instance count</string>
|
||||
<string name="export_theme_title">Toque aqui para exportar o tema atual</string>
|
||||
<string name="theme_file_error">Ocorreu um erro ao selecionar o arquivo do tema</string>
|
||||
<string name="user_count">Número de usuários</string>
|
||||
<string name="status_count">Número de status</string>
|
||||
<string name="instance_count">Número de instâncias</string>
|
||||
<string name="poll_finish_in">End in %s</string>
|
||||
<string name="no_instance_reccord">This instance is not available on https://instances.social</string>
|
||||
<string name="no_instance_reccord">Esta instância não está disponível em https://instances.social</string>
|
||||
<string name="display_full_link">Exibir link completo</string>
|
||||
<string name="share_link">Partilhar link</string>
|
||||
<string name="open_other_app">Abrir noutra aplicação</string>
|
||||
<string name="check_redirect">Check redirect</string>
|
||||
<string name="no_redirect">This URL does not redirect</string>
|
||||
<string name="redirect_detected">%1$s \n\nredirects to\n\n %2$s</string>
|
||||
<string name="set_utm_parameters">Remove UTM parameters</string>
|
||||
<string name="set_utm_parameters_indication">The app will automatically remove UTM parameters from URLs before visiting a link.</string>
|
||||
<string name="talking_about">%d people talking</string>
|
||||
<string name="twitter_accounts">Twitter accounts (via Nitter)</string>
|
||||
<string name="list_of_twitter_accounts">Twitter usernames space separated</string>
|
||||
<string name="identity_proofs">Identity proofs</string>
|
||||
<string name="verified_user">Verified identity</string>
|
||||
<string name="verified_by">Verified by %1$s (%2$s)</string>
|
||||
<string name="action_disabled">Action disabled</string>
|
||||
<string name="action_unfollow">Unfollow</string>
|
||||
<string name="error_destination_path">Something went wrong, please check your download directory in settings.</string>
|
||||
<string name="action_announcements">Announcements</string>
|
||||
<string name="no_announcements">No announcements!</string>
|
||||
<string name="add_reaction">Add a reaction</string>
|
||||
<string name="set_video_cache">Video cache in MB, zero means no cache.</string>
|
||||
<string name="set_watermark">Watermarks</string>
|
||||
<string name="set_watermark_indication">Automatically add a watermark at the bottom of pictures. The text can be customized for each account.</string>
|
||||
<string name="no_distributors_found">No distributors found!</string>
|
||||
<string name="check_redirect">Verifique o redirecionamento</string>
|
||||
<string name="no_redirect">Este URL não redireciona</string>
|
||||
<string name="redirect_detected">%1$s
|
||||
\n
|
||||
\nredireciona para
|
||||
\n
|
||||
\n %2$s</string>
|
||||
<string name="set_utm_parameters">Remover parâmetros UTM</string>
|
||||
<string name="set_utm_parameters_indication">O aplicativo removerá automaticamente os parâmetros UTM das URLs antes de visitar um link.</string>
|
||||
<string name="talking_about">%d pessoas conversando</string>
|
||||
<string name="twitter_accounts">Contas do Twitter (via Nitter)</string>
|
||||
<string name="list_of_twitter_accounts">Nomes de usuários do Twitter separados por espaço</string>
|
||||
<string name="identity_proofs">Provas de identidade</string>
|
||||
<string name="verified_user">Identidade verificada</string>
|
||||
<string name="verified_by">Verificado por %1$s (%2$s)</string>
|
||||
<string name="action_disabled">Ação desativada</string>
|
||||
<string name="action_unfollow">Deixar de seguir</string>
|
||||
<string name="error_destination_path">Algo deu errado, verifique seu diretório de download nas configurações.</string>
|
||||
<string name="action_announcements">Anúncios</string>
|
||||
<string name="no_announcements">Sem anúncios!</string>
|
||||
<string name="add_reaction">Adicionar uma reação</string>
|
||||
<string name="set_video_cache">Cache de vídeo em MB, zero significa sem cache.</string>
|
||||
<string name="set_watermark">Marcas d\'água</string>
|
||||
<string name="set_watermark_indication">Adicione automaticamente uma marca d\'água na parte inferior das fotos. O texto pode ser personalizado para cada conta.</string>
|
||||
<string name="no_distributors_found">Nenhum distribuidor encontrado!</string>
|
||||
<string name="no_distributors_explanation">Necessita de um distribuidor para receber notificações instantâneas. \nVai encontrar mais detalhes em %1$s.\n\nPode desactivar as mensagens instantâneas nas configurações para ignorar esta mensagem.</string>
|
||||
<string name="select_distributors">Select a distributor</string>
|
||||
<string name="select_distributors">Selecione um distribuidor</string>
|
||||
<string name="notification_sounds">Sons de notificação</string>
|
||||
<string name="replace_twitter_description">Usar um frontend alternativo para Twitter</string>
|
||||
<string name="replace_twitter">Twitter</string>
|
||||
|
@ -638,4 +644,24 @@
|
|||
<string name="add_status">Adicionar status</string>
|
||||
<string name="post_message_text">Enviando mensagem %d/%d</string>
|
||||
<string name="channel_notif_update">Nova atualização</string>
|
||||
<string name="channel_notif_signup">Nova inscrição</string>
|
||||
<string name="notif_update_push">Uma mensagem que você compartilhou foi editada</string>
|
||||
<string name="notif_report">Um usuário fez uma denúncia</string>
|
||||
<string name="notif_sign_up">Um usuário se inscreveu</string>
|
||||
<string name="sign_ups">Inscrições</string>
|
||||
<string name="set_customize_dark">Personalizar tema escuro</string>
|
||||
<string name="set_customize_light">Personalizar tema claro</string>
|
||||
<string name="cark_custom_colors">Escuro - Cores personalizadas</string>
|
||||
<string name="toast_try_later">Por favor, tente novamente mais tarde.</string>
|
||||
<string name="toast_on_your_instance">A conversa começou na sua instância!</string>
|
||||
<string name="report_val1">Eu não gosto disso</string>
|
||||
<string name="light_custom_colors">Claro - Cores personalizadas</string>
|
||||
<string name="manage_accounts">Gerenciar contas</string>
|
||||
<string name="import_data">Importar dados</string>
|
||||
<string name="set_cardview_indication">Quando ativado, os itens nas linhas do tempo terão uma sombra e uma elevação.</string>
|
||||
<string name="type_default_theme_light">Tema claro padrão</string>
|
||||
<string name="type_default_theme_dark">Tema escuro padrão</string>
|
||||
<string name="channel_notif_report">Nova denúncia</string>
|
||||
<string name="show_anyway">Mostrar mesmo assim</string>
|
||||
<string name="pref_customize">Personalizar cores</string>
|
||||
</resources>
|
|
@ -926,4 +926,7 @@
|
|||
<string name="unmute_home">Torra a ativare pro sa pàgina printzipale</string>
|
||||
<string name="put_all_accounts_in_home_muted">Totu is contos ant a abarrare a sa muda in sa lìnia de tempus printzipale.</string>
|
||||
<string name="mute_them_all">Pone·los totus a sa muda</string>
|
||||
<string name="import_data">Importa datos</string>
|
||||
<string name="group_reblogs">Agrupa is cumpartziduras in sa lìnia de tempus printzipale</string>
|
||||
<string name="manage_accounts">Amministra is contos</string>
|
||||
</resources>
|
|
@ -932,4 +932,7 @@
|
|||
<string name="unmute_home">Ana sayfa için susturmaktan vazgeç</string>
|
||||
<string name="add_all_users_home_muted">Tüm kullanıcıları ana sayfada susturmaya ekle</string>
|
||||
<string name="mute_home">Ana sayfa için sustur</string>
|
||||
<string name="import_data">Verileri içe aktar</string>
|
||||
<string name="group_reblogs">Ana sayfa zaman çizelgesinde yeniden blogları gruplandır</string>
|
||||
<string name="manage_accounts">Hesapları yönet</string>
|
||||
</resources>
|
|
@ -1402,6 +1402,8 @@
|
|||
<string name="SET_DISPLAY_TRANSLATE" translatable="false">SET_DISPLAY_TRANSLATE</string>
|
||||
<string name="SET_NOTIF_VALIDATION_FAV" translatable="false">SET_NOTIF_VALIDATION_FAV</string>
|
||||
<string name="SET_DISPLAY_COUNTER_FAV_BOOST" translatable="false">SET_DISPLAY_COUNTER_FAV_BOOST</string>
|
||||
<string name="SET_REMOVE_LEFT_MARGIN" translatable="false">SET_REMOVE_LEFT_MARGIN</string>
|
||||
|
||||
<string name="SET_INNER_MARKER" translatable="false">SET_INNER_MARKER</string>
|
||||
<string name="SET_NOTIF_SILENT" translatable="false">SET_NOTIF_SILENT</string>
|
||||
<string name="SET_REMEMBER_POSITION" translatable="false">SET_REMEMBER_POSITION</string>
|
||||
|
@ -1409,6 +1411,8 @@
|
|||
<string name="SET_DISPLAY_ALL_NOTIFICATIONS_TYPE" translatable="false">SET_DISPLAY_ALL_NOTIFICATIONS_TYPE</string>
|
||||
<string name="SET_EXCLUDED_NOTIFICATIONS_TYPE" translatable="false">SET_EXCLUDED_NOTIFICATIONS_TYPE</string>
|
||||
<string name="SET_EXPAND_MEDIA" translatable="false">SET_EXPAND_MEDIA</string>
|
||||
<string name="SET_GROUP_REBLOGS" translatable="false">SET_GROUP_REBLOGS</string>
|
||||
|
||||
<string name="SET_LIVE_TRANSLATE" translatable="false">SET_LIVE_TRANSLATE</string>
|
||||
<string name="SET_TRUNCATE_TOOTS_SIZE" translatable="false">SET_TRUNCATE_TOOTS_SIZE</string>
|
||||
<string name="SET_ART_WITH_NSFW" translatable="false">SET_ART_WITH_NSFW</string>
|
||||
|
@ -2108,4 +2112,8 @@
|
|||
<string name="put_all_accounts_in_home_muted">All accounts will be muted for the Home timeline.</string>
|
||||
<string name="mute_them_all">Mute them all</string>
|
||||
<string name="import_data">Import data</string>
|
||||
<string name="group_reblogs">Group reblogs in home timeline</string>
|
||||
<string name="manage_accounts">Manage accounts</string>
|
||||
<string name="set_remove_left_margin_title">Remove left margin</string>
|
||||
<string name="set_remove_left_margin">Remove the left margin in timelines to make messages more compact</string>
|
||||
</resources>
|
|
@ -240,4 +240,9 @@
|
|||
<item name="colorPrimaryInverse">@color/solarized_md_theme_light_inversePrimary</item>
|
||||
</style>
|
||||
|
||||
<style name="Fedilab.SmallIconButton" parent="Widget.Material3.Button.IconButton">
|
||||
<item name="android:layout_width">36dp</item>
|
||||
<item name="android:layout_height">36dp</item>
|
||||
<item name="android:padding">4dp</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
|
|
@ -20,6 +20,14 @@
|
|||
app:summary="@string/set_timelines_in_a_list"
|
||||
app:title="@string/set_timelines_in_a_list_title" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
app:defaultValue="false"
|
||||
app:iconSpaceReserved="false"
|
||||
app:key="@string/SET_REMOVE_LEFT_MARGIN"
|
||||
app:singleLineTitle="false"
|
||||
app:summary="@string/set_remove_left_margin"
|
||||
app:title="@string/set_remove_left_margin_title" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
app:defaultValue="true"
|
||||
app:iconSpaceReserved="false"
|
||||
|
|
|
@ -19,7 +19,12 @@
|
|||
app:key="@string/SET_EXPAND_MEDIA"
|
||||
app:singleLineTitle="false"
|
||||
app:title="@string/expand_image" />
|
||||
|
||||
<SwitchPreferenceCompat
|
||||
android:defaultValue="true"
|
||||
app:iconSpaceReserved="false"
|
||||
app:key="@string/SET_GROUP_REBLOGS"
|
||||
app:singleLineTitle="false"
|
||||
app:title="@string/group_reblogs" />
|
||||
<ListPreference
|
||||
android:defaultValue="default"
|
||||
app:entries="@array/SET_LIVE_TRANSLATE_VALUES"
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
|
||||
compileSdk 31
|
||||
defaultConfig {
|
||||
minSdkVersion 14
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api 'androidx.appcompat:appcompat:1.4.2'
|
||||
implementation "androidx.exifinterface:exifinterface:1.3.3"
|
||||
}
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
<manifest package="com.theartofdev.edmodo.cropper">
|
||||
|
||||
</manifest>
|
|
@ -1,354 +0,0 @@
|
|||
// "Therefore those skilled at the unorthodox
|
||||
// are infinite as heaven and earth,
|
||||
// inexhaustible as the great rivers.
|
||||
// When they come to an end,
|
||||
// they begin again,
|
||||
// like the days and months;
|
||||
// they die and are reborn,
|
||||
// like the four seasons."
|
||||
//
|
||||
// - Sun Tsu,
|
||||
// "The Art of War"
|
||||
|
||||
package com.theartofdev.edmodo.cropper;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* Task to crop bitmap asynchronously from the UI thread.
|
||||
*/
|
||||
final class BitmapCroppingWorkerTask
|
||||
extends AsyncTask<Void, Void, BitmapCroppingWorkerTask.Result> {
|
||||
|
||||
// region: Fields and Consts
|
||||
|
||||
/**
|
||||
* Use a WeakReference to ensure the ImageView can be garbage collected
|
||||
*/
|
||||
private final WeakReference<CropImageView> mCropImageViewReference;
|
||||
|
||||
/**
|
||||
* the bitmap to crop
|
||||
*/
|
||||
private final Bitmap mBitmap;
|
||||
|
||||
/**
|
||||
* The Android URI of the image to load
|
||||
*/
|
||||
private final Uri mUri;
|
||||
|
||||
/**
|
||||
* The context of the crop image view widget used for loading of bitmap by Android URI
|
||||
*/
|
||||
private final Context mContext;
|
||||
|
||||
/**
|
||||
* Required cropping 4 points (x0,y0,x1,y1,x2,y2,x3,y3)
|
||||
*/
|
||||
private final float[] mCropPoints;
|
||||
|
||||
/**
|
||||
* Degrees the image was rotated after loading
|
||||
*/
|
||||
private final int mDegreesRotated;
|
||||
|
||||
/**
|
||||
* the original width of the image to be cropped (for image loaded from URI)
|
||||
*/
|
||||
private final int mOrgWidth;
|
||||
|
||||
/**
|
||||
* the original height of the image to be cropped (for image loaded from URI)
|
||||
*/
|
||||
private final int mOrgHeight;
|
||||
|
||||
/**
|
||||
* is there is fixed aspect ratio for the crop rectangle
|
||||
*/
|
||||
private final boolean mFixAspectRatio;
|
||||
|
||||
/**
|
||||
* the X aspect ration of the crop rectangle
|
||||
*/
|
||||
private final int mAspectRatioX;
|
||||
|
||||
/**
|
||||
* the Y aspect ration of the crop rectangle
|
||||
*/
|
||||
private final int mAspectRatioY;
|
||||
|
||||
/**
|
||||
* required width of the cropping image
|
||||
*/
|
||||
private final int mReqWidth;
|
||||
|
||||
/**
|
||||
* required height of the cropping image
|
||||
*/
|
||||
private final int mReqHeight;
|
||||
|
||||
/**
|
||||
* is the image flipped horizontally
|
||||
*/
|
||||
private final boolean mFlipHorizontally;
|
||||
|
||||
/**
|
||||
* is the image flipped vertically
|
||||
*/
|
||||
private final boolean mFlipVertically;
|
||||
|
||||
/**
|
||||
* The option to handle requested width/height
|
||||
*/
|
||||
private final CropImageView.RequestSizeOptions mReqSizeOptions;
|
||||
|
||||
/**
|
||||
* the Android Uri to save the cropped image to
|
||||
*/
|
||||
private final Uri mSaveUri;
|
||||
|
||||
/**
|
||||
* the compression format to use when writing the image
|
||||
*/
|
||||
private final Bitmap.CompressFormat mSaveCompressFormat;
|
||||
|
||||
/**
|
||||
* the quality (if applicable) to use when writing the image (0 - 100)
|
||||
*/
|
||||
private final int mSaveCompressQuality;
|
||||
// endregion
|
||||
|
||||
BitmapCroppingWorkerTask(
|
||||
CropImageView cropImageView,
|
||||
Bitmap bitmap,
|
||||
float[] cropPoints,
|
||||
int degreesRotated,
|
||||
boolean fixAspectRatio,
|
||||
int aspectRatioX,
|
||||
int aspectRatioY,
|
||||
int reqWidth,
|
||||
int reqHeight,
|
||||
boolean flipHorizontally,
|
||||
boolean flipVertically,
|
||||
CropImageView.RequestSizeOptions options,
|
||||
Uri saveUri,
|
||||
Bitmap.CompressFormat saveCompressFormat,
|
||||
int saveCompressQuality) {
|
||||
|
||||
mCropImageViewReference = new WeakReference<>(cropImageView);
|
||||
mContext = cropImageView.getContext();
|
||||
mBitmap = bitmap;
|
||||
mCropPoints = cropPoints;
|
||||
mUri = null;
|
||||
mDegreesRotated = degreesRotated;
|
||||
mFixAspectRatio = fixAspectRatio;
|
||||
mAspectRatioX = aspectRatioX;
|
||||
mAspectRatioY = aspectRatioY;
|
||||
mReqWidth = reqWidth;
|
||||
mReqHeight = reqHeight;
|
||||
mFlipHorizontally = flipHorizontally;
|
||||
mFlipVertically = flipVertically;
|
||||
mReqSizeOptions = options;
|
||||
mSaveUri = saveUri;
|
||||
mSaveCompressFormat = saveCompressFormat;
|
||||
mSaveCompressQuality = saveCompressQuality;
|
||||
mOrgWidth = 0;
|
||||
mOrgHeight = 0;
|
||||
}
|
||||
|
||||
BitmapCroppingWorkerTask(
|
||||
CropImageView cropImageView,
|
||||
Uri uri,
|
||||
float[] cropPoints,
|
||||
int degreesRotated,
|
||||
int orgWidth,
|
||||
int orgHeight,
|
||||
boolean fixAspectRatio,
|
||||
int aspectRatioX,
|
||||
int aspectRatioY,
|
||||
int reqWidth,
|
||||
int reqHeight,
|
||||
boolean flipHorizontally,
|
||||
boolean flipVertically,
|
||||
CropImageView.RequestSizeOptions options,
|
||||
Uri saveUri,
|
||||
Bitmap.CompressFormat saveCompressFormat,
|
||||
int saveCompressQuality) {
|
||||
|
||||
mCropImageViewReference = new WeakReference<>(cropImageView);
|
||||
mContext = cropImageView.getContext();
|
||||
mUri = uri;
|
||||
mCropPoints = cropPoints;
|
||||
mDegreesRotated = degreesRotated;
|
||||
mFixAspectRatio = fixAspectRatio;
|
||||
mAspectRatioX = aspectRatioX;
|
||||
mAspectRatioY = aspectRatioY;
|
||||
mOrgWidth = orgWidth;
|
||||
mOrgHeight = orgHeight;
|
||||
mReqWidth = reqWidth;
|
||||
mReqHeight = reqHeight;
|
||||
mFlipHorizontally = flipHorizontally;
|
||||
mFlipVertically = flipVertically;
|
||||
mReqSizeOptions = options;
|
||||
mSaveUri = saveUri;
|
||||
mSaveCompressFormat = saveCompressFormat;
|
||||
mSaveCompressQuality = saveCompressQuality;
|
||||
mBitmap = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Android URI that this task is currently loading.
|
||||
*/
|
||||
public Uri getUri() {
|
||||
return mUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crop image in background.
|
||||
*
|
||||
* @param params ignored
|
||||
* @return the decoded bitmap data
|
||||
*/
|
||||
@Override
|
||||
protected BitmapCroppingWorkerTask.Result doInBackground(Void... params) {
|
||||
try {
|
||||
if (!isCancelled()) {
|
||||
|
||||
BitmapUtils.BitmapSampled bitmapSampled;
|
||||
if (mUri != null) {
|
||||
bitmapSampled =
|
||||
BitmapUtils.cropBitmap(
|
||||
mContext,
|
||||
mUri,
|
||||
mCropPoints,
|
||||
mDegreesRotated,
|
||||
mOrgWidth,
|
||||
mOrgHeight,
|
||||
mFixAspectRatio,
|
||||
mAspectRatioX,
|
||||
mAspectRatioY,
|
||||
mReqWidth,
|
||||
mReqHeight,
|
||||
mFlipHorizontally,
|
||||
mFlipVertically);
|
||||
} else if (mBitmap != null) {
|
||||
bitmapSampled =
|
||||
BitmapUtils.cropBitmapObjectHandleOOM(
|
||||
mBitmap,
|
||||
mCropPoints,
|
||||
mDegreesRotated,
|
||||
mFixAspectRatio,
|
||||
mAspectRatioX,
|
||||
mAspectRatioY,
|
||||
mFlipHorizontally,
|
||||
mFlipVertically);
|
||||
} else {
|
||||
return new Result((Bitmap) null, 1);
|
||||
}
|
||||
|
||||
Bitmap bitmap =
|
||||
BitmapUtils.resizeBitmap(bitmapSampled.bitmap, mReqWidth, mReqHeight, mReqSizeOptions);
|
||||
|
||||
if (mSaveUri == null) {
|
||||
return new Result(bitmap, bitmapSampled.sampleSize);
|
||||
} else {
|
||||
BitmapUtils.writeBitmapToUri(
|
||||
mContext, bitmap, mSaveUri, mSaveCompressFormat, mSaveCompressQuality);
|
||||
if (bitmap != null) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
return new Result(mSaveUri, bitmapSampled.sampleSize);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
return new Result(e, mSaveUri != null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Once complete, see if ImageView is still around and set bitmap.
|
||||
*
|
||||
* @param result the result of bitmap cropping
|
||||
*/
|
||||
@Override
|
||||
protected void onPostExecute(Result result) {
|
||||
if (result != null) {
|
||||
boolean completeCalled = false;
|
||||
if (!isCancelled()) {
|
||||
CropImageView cropImageView = mCropImageViewReference.get();
|
||||
if (cropImageView != null) {
|
||||
completeCalled = true;
|
||||
cropImageView.onImageCroppingAsyncComplete(result);
|
||||
}
|
||||
}
|
||||
if (!completeCalled && result.bitmap != null) {
|
||||
// fast release of unused bitmap
|
||||
result.bitmap.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region: Inner class: Result
|
||||
|
||||
/**
|
||||
* The result of BitmapCroppingWorkerTask async loading.
|
||||
*/
|
||||
static final class Result {
|
||||
|
||||
/**
|
||||
* The cropped bitmap
|
||||
*/
|
||||
public final Bitmap bitmap;
|
||||
|
||||
/**
|
||||
* The saved cropped bitmap uri
|
||||
*/
|
||||
public final Uri uri;
|
||||
|
||||
/**
|
||||
* The error that occurred during async bitmap cropping.
|
||||
*/
|
||||
final Exception error;
|
||||
|
||||
/**
|
||||
* is the cropping request was to get a bitmap or to save it to uri
|
||||
*/
|
||||
final boolean isSave;
|
||||
|
||||
/**
|
||||
* sample size used creating the crop bitmap to lower its size
|
||||
*/
|
||||
final int sampleSize;
|
||||
|
||||
Result(Bitmap bitmap, int sampleSize) {
|
||||
this.bitmap = bitmap;
|
||||
this.uri = null;
|
||||
this.error = null;
|
||||
this.isSave = false;
|
||||
this.sampleSize = sampleSize;
|
||||
}
|
||||
|
||||
Result(Uri uri, int sampleSize) {
|
||||
this.bitmap = null;
|
||||
this.uri = uri;
|
||||
this.error = null;
|
||||
this.isSave = true;
|
||||
this.sampleSize = sampleSize;
|
||||
}
|
||||
|
||||
Result(Exception error, boolean isSave) {
|
||||
this.bitmap = null;
|
||||
this.uri = null;
|
||||
this.error = error;
|
||||
this.isSave = isSave;
|
||||
this.sampleSize = 1;
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
|
@ -1,176 +0,0 @@
|
|||
// "Therefore those skilled at the unorthodox
|
||||
// are infinite as heaven and earth,
|
||||
// inexhaustible as the great rivers.
|
||||
// When they come to an end,
|
||||
// they begin again,
|
||||
// like the days and months;
|
||||
// they die and are reborn,
|
||||
// like the four seasons."
|
||||
//
|
||||
// - Sun Tsu,
|
||||
// "The Art of War"
|
||||
|
||||
package com.theartofdev.edmodo.cropper;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
/**
|
||||
* Task to load bitmap asynchronously from the UI thread.
|
||||
*/
|
||||
final class BitmapLoadingWorkerTask extends AsyncTask<Void, Void, BitmapLoadingWorkerTask.Result> {
|
||||
|
||||
// region: Fields and Consts
|
||||
|
||||
/**
|
||||
* Use a WeakReference to ensure the ImageView can be garbage collected
|
||||
*/
|
||||
private final WeakReference<CropImageView> mCropImageViewReference;
|
||||
|
||||
/**
|
||||
* The Android URI of the image to load
|
||||
*/
|
||||
private final Uri mUri;
|
||||
|
||||
/**
|
||||
* The context of the crop image view widget used for loading of bitmap by Android URI
|
||||
*/
|
||||
private final Context mContext;
|
||||
|
||||
/**
|
||||
* required width of the cropping image after density adjustment
|
||||
*/
|
||||
private final int mWidth;
|
||||
|
||||
/**
|
||||
* required height of the cropping image after density adjustment
|
||||
*/
|
||||
private final int mHeight;
|
||||
// endregion
|
||||
|
||||
public BitmapLoadingWorkerTask(CropImageView cropImageView, Uri uri) {
|
||||
mUri = uri;
|
||||
mCropImageViewReference = new WeakReference<>(cropImageView);
|
||||
|
||||
mContext = cropImageView.getContext();
|
||||
|
||||
DisplayMetrics metrics = cropImageView.getResources().getDisplayMetrics();
|
||||
double densityAdj = metrics.density > 1 ? 1 / metrics.density : 1;
|
||||
mWidth = (int) (metrics.widthPixels * densityAdj);
|
||||
mHeight = (int) (metrics.heightPixels * densityAdj);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Android URI that this task is currently loading.
|
||||
*/
|
||||
public Uri getUri() {
|
||||
return mUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode image in background.
|
||||
*
|
||||
* @param params ignored
|
||||
* @return the decoded bitmap data
|
||||
*/
|
||||
@Override
|
||||
protected Result doInBackground(Void... params) {
|
||||
try {
|
||||
if (!isCancelled()) {
|
||||
|
||||
BitmapUtils.BitmapSampled decodeResult =
|
||||
BitmapUtils.decodeSampledBitmap(mContext, mUri, mWidth, mHeight);
|
||||
|
||||
if (!isCancelled()) {
|
||||
|
||||
BitmapUtils.RotateBitmapResult rotateResult =
|
||||
BitmapUtils.rotateBitmapByExif(decodeResult.bitmap, mContext, mUri);
|
||||
|
||||
return new Result(
|
||||
mUri, rotateResult.bitmap, decodeResult.sampleSize, rotateResult.degrees);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
} catch (Exception e) {
|
||||
return new Result(mUri, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Once complete, see if ImageView is still around and set bitmap.
|
||||
*
|
||||
* @param result the result of bitmap loading
|
||||
*/
|
||||
@Override
|
||||
protected void onPostExecute(Result result) {
|
||||
if (result != null) {
|
||||
boolean completeCalled = false;
|
||||
if (!isCancelled()) {
|
||||
CropImageView cropImageView = mCropImageViewReference.get();
|
||||
if (cropImageView != null) {
|
||||
completeCalled = true;
|
||||
cropImageView.onSetImageUriAsyncComplete(result);
|
||||
}
|
||||
}
|
||||
if (!completeCalled && result.bitmap != null) {
|
||||
// fast release of unused bitmap
|
||||
result.bitmap.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// region: Inner class: Result
|
||||
|
||||
/**
|
||||
* The result of BitmapLoadingWorkerTask async loading.
|
||||
*/
|
||||
public static final class Result {
|
||||
|
||||
/**
|
||||
* The Android URI of the image to load
|
||||
*/
|
||||
public final Uri uri;
|
||||
|
||||
/**
|
||||
* The loaded bitmap
|
||||
*/
|
||||
public final Bitmap bitmap;
|
||||
|
||||
/**
|
||||
* The sample size used to load the given bitmap
|
||||
*/
|
||||
public final int loadSampleSize;
|
||||
|
||||
/**
|
||||
* The degrees the image was rotated
|
||||
*/
|
||||
public final int degreesRotated;
|
||||
|
||||
/**
|
||||
* The error that occurred during async bitmap loading.
|
||||
*/
|
||||
public final Exception error;
|
||||
|
||||
Result(Uri uri, Bitmap bitmap, int loadSampleSize, int degreesRotated) {
|
||||
this.uri = uri;
|
||||
this.bitmap = bitmap;
|
||||
this.loadSampleSize = loadSampleSize;
|
||||
this.degreesRotated = degreesRotated;
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
Result(Uri uri, Exception error) {
|
||||
this.uri = uri;
|
||||
this.bitmap = null;
|
||||
this.loadSampleSize = 0;
|
||||
this.degreesRotated = 0;
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
|
@ -1,923 +0,0 @@
|
|||
// "Therefore those skilled at the unorthodox
|
||||
// are infinite as heaven and earth,
|
||||
// inexhaustible as the great rivers.
|
||||
// When they come to an end,
|
||||
// they begin again,
|
||||
// like the days and months;
|
||||
// they die and are reborn,
|
||||
// like the four seasons."
|
||||
//
|
||||
// - Sun Tsu,
|
||||
// "The Art of War"
|
||||
|
||||
package com.theartofdev.edmodo.cropper;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.BitmapRegionDecoder;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.net.Uri;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
|
||||
import androidx.exifinterface.media.ExifInterface;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
import javax.microedition.khronos.egl.EGL10;
|
||||
import javax.microedition.khronos.egl.EGLConfig;
|
||||
import javax.microedition.khronos.egl.EGLContext;
|
||||
import javax.microedition.khronos.egl.EGLDisplay;
|
||||
|
||||
/**
|
||||
* Utility class that deals with operations with an ImageView.
|
||||
*/
|
||||
final class BitmapUtils {
|
||||
|
||||
static final Rect EMPTY_RECT = new Rect();
|
||||
|
||||
static final RectF EMPTY_RECT_F = new RectF();
|
||||
|
||||
/**
|
||||
* Reusable rectangle for general internal usage
|
||||
*/
|
||||
static final RectF RECT = new RectF();
|
||||
|
||||
/**
|
||||
* Reusable point for general internal usage
|
||||
*/
|
||||
static final float[] POINTS = new float[6];
|
||||
|
||||
/**
|
||||
* Reusable point for general internal usage
|
||||
*/
|
||||
static final float[] POINTS2 = new float[6];
|
||||
/**
|
||||
* used to save bitmaps during state save and restore so not to reload them.
|
||||
*/
|
||||
static Pair<String, WeakReference<Bitmap>> mStateBitmap;
|
||||
/**
|
||||
* Used to know the max texture size allowed to be rendered
|
||||
*/
|
||||
private static int mMaxTextureSize;
|
||||
|
||||
/**
|
||||
* Rotate the given image by reading the Exif value of the image (uri).<br>
|
||||
* If no rotation is required the image will not be rotated.<br>
|
||||
* New bitmap is created and the old one is recycled.
|
||||
*/
|
||||
static RotateBitmapResult rotateBitmapByExif(Bitmap bitmap, Context context, Uri uri) {
|
||||
ExifInterface ei = null;
|
||||
try {
|
||||
InputStream is = context.getContentResolver().openInputStream(uri);
|
||||
if (is != null) {
|
||||
ei = new ExifInterface(is);
|
||||
is.close();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return ei != null ? rotateBitmapByExif(bitmap, ei) : new RotateBitmapResult(bitmap, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate the given image by given Exif value.<br>
|
||||
* If no rotation is required the image will not be rotated.<br>
|
||||
* New bitmap is created and the old one is recycled.
|
||||
*/
|
||||
static RotateBitmapResult rotateBitmapByExif(Bitmap bitmap, ExifInterface exif) {
|
||||
int degrees;
|
||||
int orientation =
|
||||
exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);
|
||||
switch (orientation) {
|
||||
case ExifInterface.ORIENTATION_ROTATE_90:
|
||||
degrees = 90;
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_ROTATE_180:
|
||||
degrees = 180;
|
||||
break;
|
||||
case ExifInterface.ORIENTATION_ROTATE_270:
|
||||
degrees = 270;
|
||||
break;
|
||||
default:
|
||||
degrees = 0;
|
||||
break;
|
||||
}
|
||||
return new RotateBitmapResult(bitmap, degrees);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode bitmap from stream using sampling to get bitmap with the requested limit.
|
||||
*/
|
||||
static BitmapSampled decodeSampledBitmap(Context context, Uri uri, int reqWidth, int reqHeight) {
|
||||
|
||||
try {
|
||||
ContentResolver resolver = context.getContentResolver();
|
||||
|
||||
// First decode with inJustDecodeBounds=true to check dimensions
|
||||
BitmapFactory.Options options = decodeImageForOption(resolver, uri);
|
||||
|
||||
if (options.outWidth == -1 && options.outHeight == -1)
|
||||
throw new RuntimeException("File is not a picture");
|
||||
|
||||
// Calculate inSampleSize
|
||||
options.inSampleSize =
|
||||
Math.max(
|
||||
calculateInSampleSizeByReqestedSize(
|
||||
options.outWidth, options.outHeight, reqWidth, reqHeight),
|
||||
calculateInSampleSizeByMaxTextureSize(options.outWidth, options.outHeight));
|
||||
|
||||
// Decode bitmap with inSampleSize set
|
||||
Bitmap bitmap = decodeImage(resolver, uri, options);
|
||||
|
||||
return new BitmapSampled(bitmap, options.inSampleSize);
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(
|
||||
"Failed to load sampled bitmap: " + uri + "\r\n" + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crop image bitmap from given bitmap using the given points in the original bitmap and the given
|
||||
* rotation.<br>
|
||||
* if the rotation is not 0,90,180 or 270 degrees then we must first crop a larger area of the
|
||||
* image that contains the requires rectangle, rotate and then crop again a sub rectangle.<br>
|
||||
* If crop fails due to OOM we scale the cropping image by 0.5 every time it fails until it is
|
||||
* small enough.
|
||||
*/
|
||||
static BitmapSampled cropBitmapObjectHandleOOM(
|
||||
Bitmap bitmap,
|
||||
float[] points,
|
||||
int degreesRotated,
|
||||
boolean fixAspectRatio,
|
||||
int aspectRatioX,
|
||||
int aspectRatioY,
|
||||
boolean flipHorizontally,
|
||||
boolean flipVertically) {
|
||||
int scale = 1;
|
||||
while (true) {
|
||||
try {
|
||||
Bitmap cropBitmap =
|
||||
cropBitmapObjectWithScale(
|
||||
bitmap,
|
||||
points,
|
||||
degreesRotated,
|
||||
fixAspectRatio,
|
||||
aspectRatioX,
|
||||
aspectRatioY,
|
||||
1 / (float) scale,
|
||||
flipHorizontally,
|
||||
flipVertically);
|
||||
return new BitmapSampled(cropBitmap, scale);
|
||||
} catch (OutOfMemoryError e) {
|
||||
scale *= 2;
|
||||
if (scale > 8) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crop image bitmap from given bitmap using the given points in the original bitmap and the given
|
||||
* rotation.<br>
|
||||
* if the rotation is not 0,90,180 or 270 degrees then we must first crop a larger area of the
|
||||
* image that contains the requires rectangle, rotate and then crop again a sub rectangle.
|
||||
*
|
||||
* @param scale how much to scale the cropped image part, use 0.5 to lower the image by half (OOM
|
||||
* handling)
|
||||
*/
|
||||
private static Bitmap cropBitmapObjectWithScale(
|
||||
Bitmap bitmap,
|
||||
float[] points,
|
||||
int degreesRotated,
|
||||
boolean fixAspectRatio,
|
||||
int aspectRatioX,
|
||||
int aspectRatioY,
|
||||
float scale,
|
||||
boolean flipHorizontally,
|
||||
boolean flipVertically) {
|
||||
|
||||
// get the rectangle in original image that contains the required cropped area (larger for non
|
||||
// rectangular crop)
|
||||
Rect rect =
|
||||
getRectFromPoints(
|
||||
points,
|
||||
bitmap.getWidth(),
|
||||
bitmap.getHeight(),
|
||||
fixAspectRatio,
|
||||
aspectRatioX,
|
||||
aspectRatioY);
|
||||
|
||||
// crop and rotate the cropped image in one operation
|
||||
Matrix matrix = new Matrix();
|
||||
matrix.setRotate(degreesRotated, bitmap.getWidth() / 2, bitmap.getHeight() / 2);
|
||||
matrix.postScale(flipHorizontally ? -scale : scale, flipVertically ? -scale : scale);
|
||||
Bitmap result =
|
||||
Bitmap.createBitmap(bitmap, rect.left, rect.top, rect.width(), rect.height(), matrix, true);
|
||||
|
||||
if (result == bitmap) {
|
||||
// corner case when all bitmap is selected, no worth optimizing for it
|
||||
result = bitmap.copy(bitmap.getConfig(), false);
|
||||
}
|
||||
|
||||
// rotating by 0, 90, 180 or 270 degrees doesn't require extra cropping
|
||||
if (degreesRotated % 90 != 0) {
|
||||
|
||||
// extra crop because non rectangular crop cannot be done directly on the image without
|
||||
// rotating first
|
||||
result =
|
||||
cropForRotatedImage(
|
||||
result, points, rect, degreesRotated, fixAspectRatio, aspectRatioX, aspectRatioY);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crop image bitmap from URI by decoding it with specific width and height to down-sample if
|
||||
* required.<br>
|
||||
* Additionally if OOM is thrown try to increase the sampling (2,4,8).
|
||||
*/
|
||||
static BitmapSampled cropBitmap(
|
||||
Context context,
|
||||
Uri loadedImageUri,
|
||||
float[] points,
|
||||
int degreesRotated,
|
||||
int orgWidth,
|
||||
int orgHeight,
|
||||
boolean fixAspectRatio,
|
||||
int aspectRatioX,
|
||||
int aspectRatioY,
|
||||
int reqWidth,
|
||||
int reqHeight,
|
||||
boolean flipHorizontally,
|
||||
boolean flipVertically) {
|
||||
int sampleMulti = 1;
|
||||
while (true) {
|
||||
try {
|
||||
// if successful, just return the resulting bitmap
|
||||
return cropBitmap(
|
||||
context,
|
||||
loadedImageUri,
|
||||
points,
|
||||
degreesRotated,
|
||||
orgWidth,
|
||||
orgHeight,
|
||||
fixAspectRatio,
|
||||
aspectRatioX,
|
||||
aspectRatioY,
|
||||
reqWidth,
|
||||
reqHeight,
|
||||
flipHorizontally,
|
||||
flipVertically,
|
||||
sampleMulti);
|
||||
} catch (OutOfMemoryError e) {
|
||||
// if OOM try to increase the sampling to lower the memory usage
|
||||
sampleMulti *= 2;
|
||||
if (sampleMulti > 16) {
|
||||
throw new RuntimeException(
|
||||
"Failed to handle OOM by sampling ("
|
||||
+ sampleMulti
|
||||
+ "): "
|
||||
+ loadedImageUri
|
||||
+ "\r\n"
|
||||
+ e.getMessage(),
|
||||
e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get left value of the bounding rectangle of the given points.
|
||||
*/
|
||||
static float getRectLeft(float[] points) {
|
||||
return Math.min(Math.min(Math.min(points[0], points[2]), points[4]), points[6]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get top value of the bounding rectangle of the given points.
|
||||
*/
|
||||
static float getRectTop(float[] points) {
|
||||
return Math.min(Math.min(Math.min(points[1], points[3]), points[5]), points[7]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get right value of the bounding rectangle of the given points.
|
||||
*/
|
||||
static float getRectRight(float[] points) {
|
||||
return Math.max(Math.max(Math.max(points[0], points[2]), points[4]), points[6]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bottom value of the bounding rectangle of the given points.
|
||||
*/
|
||||
static float getRectBottom(float[] points) {
|
||||
return Math.max(Math.max(Math.max(points[1], points[3]), points[5]), points[7]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get width of the bounding rectangle of the given points.
|
||||
*/
|
||||
static float getRectWidth(float[] points) {
|
||||
return getRectRight(points) - getRectLeft(points);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get height of the bounding rectangle of the given points.
|
||||
*/
|
||||
static float getRectHeight(float[] points) {
|
||||
return getRectBottom(points) - getRectTop(points);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get horizontal center value of the bounding rectangle of the given points.
|
||||
*/
|
||||
static float getRectCenterX(float[] points) {
|
||||
return (getRectRight(points) + getRectLeft(points)) / 2f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get vertical center value of the bounding rectangle of the given points.
|
||||
*/
|
||||
static float getRectCenterY(float[] points) {
|
||||
return (getRectBottom(points) + getRectTop(points)) / 2f;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a rectangle for the given 4 points (x0,y0,x1,y1,x2,y2,x3,y3) by finding the min/max 2
|
||||
* points that contains the given 4 points and is a straight rectangle.
|
||||
*/
|
||||
static Rect getRectFromPoints(
|
||||
float[] points,
|
||||
int imageWidth,
|
||||
int imageHeight,
|
||||
boolean fixAspectRatio,
|
||||
int aspectRatioX,
|
||||
int aspectRatioY) {
|
||||
int left = Math.round(Math.max(0, getRectLeft(points)));
|
||||
int top = Math.round(Math.max(0, getRectTop(points)));
|
||||
int right = Math.round(Math.min(imageWidth, getRectRight(points)));
|
||||
int bottom = Math.round(Math.min(imageHeight, getRectBottom(points)));
|
||||
|
||||
Rect rect = new Rect(left, top, right, bottom);
|
||||
if (fixAspectRatio) {
|
||||
fixRectForAspectRatio(rect, aspectRatioX, aspectRatioY);
|
||||
}
|
||||
|
||||
return rect;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix the given rectangle if it doesn't confirm to aspect ration rule.<br>
|
||||
* Make sure that width and height are equal if 1:1 fixed aspect ratio is requested.
|
||||
*/
|
||||
private static void fixRectForAspectRatio(Rect rect, int aspectRatioX, int aspectRatioY) {
|
||||
if (aspectRatioX == aspectRatioY && rect.width() != rect.height()) {
|
||||
if (rect.height() > rect.width()) {
|
||||
rect.bottom -= rect.height() - rect.width();
|
||||
} else {
|
||||
rect.right -= rect.width() - rect.height();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write given bitmap to a temp file. If file already exists no-op as we already saved the file in
|
||||
* this session. Uses JPEG 95% compression.
|
||||
*
|
||||
* @param uri the uri to write the bitmap to, if null
|
||||
* @return the uri where the image was saved in, either the given uri or new pointing to temp
|
||||
* file.
|
||||
*/
|
||||
static Uri writeTempStateStoreBitmap(Context context, Bitmap bitmap, Uri uri) {
|
||||
try {
|
||||
boolean needSave = true;
|
||||
if (uri == null) {
|
||||
uri =
|
||||
Uri.fromFile(
|
||||
File.createTempFile("aic_state_store_temp", ".jpg", context.getCacheDir()));
|
||||
} else if (new File(uri.getPath()).exists()) {
|
||||
needSave = false;
|
||||
}
|
||||
if (needSave) {
|
||||
writeBitmapToUri(context, bitmap, uri, Bitmap.CompressFormat.JPEG, 95);
|
||||
}
|
||||
return uri;
|
||||
} catch (Exception e) {
|
||||
Log.w("AIC", "Failed to write bitmap to temp file for image-cropper save instance state", e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the given bitmap to the given uri using the given compression.
|
||||
*/
|
||||
static void writeBitmapToUri(
|
||||
Context context,
|
||||
Bitmap bitmap,
|
||||
Uri uri,
|
||||
Bitmap.CompressFormat compressFormat,
|
||||
int compressQuality)
|
||||
throws FileNotFoundException {
|
||||
OutputStream outputStream = null;
|
||||
try {
|
||||
outputStream = context.getContentResolver().openOutputStream(uri);
|
||||
bitmap.compress(compressFormat, compressQuality, outputStream);
|
||||
} finally {
|
||||
closeSafe(outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize the given bitmap to the given width/height by the given option.<br>
|
||||
*/
|
||||
static Bitmap resizeBitmap(
|
||||
Bitmap bitmap, int reqWidth, int reqHeight, CropImageView.RequestSizeOptions options) {
|
||||
try {
|
||||
if (reqWidth > 0
|
||||
&& reqHeight > 0
|
||||
&& (options == CropImageView.RequestSizeOptions.RESIZE_FIT
|
||||
|| options == CropImageView.RequestSizeOptions.RESIZE_INSIDE
|
||||
|| options == CropImageView.RequestSizeOptions.RESIZE_EXACT)) {
|
||||
|
||||
Bitmap resized = null;
|
||||
if (options == CropImageView.RequestSizeOptions.RESIZE_EXACT) {
|
||||
resized = Bitmap.createScaledBitmap(bitmap, reqWidth, reqHeight, false);
|
||||
} else {
|
||||
int width = bitmap.getWidth();
|
||||
int height = bitmap.getHeight();
|
||||
float scale = Math.max(width / (float) reqWidth, height / (float) reqHeight);
|
||||
if (scale > 1 || options == CropImageView.RequestSizeOptions.RESIZE_FIT) {
|
||||
resized =
|
||||
Bitmap.createScaledBitmap(
|
||||
bitmap, (int) (width / scale), (int) (height / scale), false);
|
||||
}
|
||||
}
|
||||
if (resized != null) {
|
||||
if (resized != bitmap) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
return resized;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.w("AIC", "Failed to resize cropped image, return bitmap before resize", e);
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
// region: Private methods
|
||||
|
||||
/**
|
||||
* Crop image bitmap from URI by decoding it with specific width and height to down-sample if
|
||||
* required.
|
||||
*
|
||||
* @param orgWidth used to get rectangle from points (handle edge cases to limit rectangle)
|
||||
* @param orgHeight used to get rectangle from points (handle edge cases to limit rectangle)
|
||||
* @param sampleMulti used to increase the sampling of the image to handle memory issues.
|
||||
*/
|
||||
private static BitmapSampled cropBitmap(
|
||||
Context context,
|
||||
Uri loadedImageUri,
|
||||
float[] points,
|
||||
int degreesRotated,
|
||||
int orgWidth,
|
||||
int orgHeight,
|
||||
boolean fixAspectRatio,
|
||||
int aspectRatioX,
|
||||
int aspectRatioY,
|
||||
int reqWidth,
|
||||
int reqHeight,
|
||||
boolean flipHorizontally,
|
||||
boolean flipVertically,
|
||||
int sampleMulti) {
|
||||
|
||||
// get the rectangle in original image that contains the required cropped area (larger for non
|
||||
// rectangular crop)
|
||||
Rect rect =
|
||||
getRectFromPoints(points, orgWidth, orgHeight, fixAspectRatio, aspectRatioX, aspectRatioY);
|
||||
|
||||
int width = reqWidth > 0 ? reqWidth : rect.width();
|
||||
int height = reqHeight > 0 ? reqHeight : rect.height();
|
||||
|
||||
Bitmap result = null;
|
||||
int sampleSize = 1;
|
||||
try {
|
||||
// decode only the required image from URI, optionally sub-sampling if reqWidth/reqHeight is
|
||||
// given.
|
||||
BitmapSampled bitmapSampled =
|
||||
decodeSampledBitmapRegion(context, loadedImageUri, rect, width, height, sampleMulti);
|
||||
result = bitmapSampled.bitmap;
|
||||
sampleSize = bitmapSampled.sampleSize;
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
|
||||
if (result != null) {
|
||||
try {
|
||||
// rotate the decoded region by the required amount
|
||||
result = rotateAndFlipBitmapInt(result, degreesRotated, flipHorizontally, flipVertically);
|
||||
|
||||
// rotating by 0, 90, 180 or 270 degrees doesn't require extra cropping
|
||||
if (degreesRotated % 90 != 0) {
|
||||
|
||||
// extra crop because non rectangular crop cannot be done directly on the image without
|
||||
// rotating first
|
||||
result =
|
||||
cropForRotatedImage(
|
||||
result, points, rect, degreesRotated, fixAspectRatio, aspectRatioX, aspectRatioY);
|
||||
}
|
||||
} catch (OutOfMemoryError e) {
|
||||
if (result != null) {
|
||||
result.recycle();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return new BitmapSampled(result, sampleSize);
|
||||
} else {
|
||||
// failed to decode region, may be skia issue, try full decode and then crop
|
||||
return cropBitmap(
|
||||
context,
|
||||
loadedImageUri,
|
||||
points,
|
||||
degreesRotated,
|
||||
fixAspectRatio,
|
||||
aspectRatioX,
|
||||
aspectRatioY,
|
||||
sampleMulti,
|
||||
rect,
|
||||
width,
|
||||
height,
|
||||
flipHorizontally,
|
||||
flipVertically);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crop bitmap by fully loading the original and then cropping it, fallback in case cropping
|
||||
* region failed.
|
||||
*/
|
||||
private static BitmapSampled cropBitmap(
|
||||
Context context,
|
||||
Uri loadedImageUri,
|
||||
float[] points,
|
||||
int degreesRotated,
|
||||
boolean fixAspectRatio,
|
||||
int aspectRatioX,
|
||||
int aspectRatioY,
|
||||
int sampleMulti,
|
||||
Rect rect,
|
||||
int width,
|
||||
int height,
|
||||
boolean flipHorizontally,
|
||||
boolean flipVertically) {
|
||||
Bitmap result = null;
|
||||
int sampleSize;
|
||||
try {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inSampleSize =
|
||||
sampleSize =
|
||||
sampleMulti
|
||||
* calculateInSampleSizeByReqestedSize(rect.width(), rect.height(), width, height);
|
||||
|
||||
Bitmap fullBitmap = decodeImage(context.getContentResolver(), loadedImageUri, options);
|
||||
if (fullBitmap != null) {
|
||||
try {
|
||||
// adjust crop points by the sampling because the image is smaller
|
||||
float[] points2 = new float[points.length];
|
||||
System.arraycopy(points, 0, points2, 0, points.length);
|
||||
for (int i = 0; i < points2.length; i++) {
|
||||
points2[i] = points2[i] / options.inSampleSize;
|
||||
}
|
||||
|
||||
result =
|
||||
cropBitmapObjectWithScale(
|
||||
fullBitmap,
|
||||
points2,
|
||||
degreesRotated,
|
||||
fixAspectRatio,
|
||||
aspectRatioX,
|
||||
aspectRatioY,
|
||||
1,
|
||||
flipHorizontally,
|
||||
flipVertically);
|
||||
} finally {
|
||||
if (result != fullBitmap) {
|
||||
fullBitmap.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (OutOfMemoryError e) {
|
||||
if (result != null) {
|
||||
result.recycle();
|
||||
}
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(
|
||||
"Failed to load sampled bitmap: " + loadedImageUri + "\r\n" + e.getMessage(), e);
|
||||
}
|
||||
return new BitmapSampled(result, sampleSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode image from uri using "inJustDecodeBounds" to get the image dimensions.
|
||||
*/
|
||||
private static BitmapFactory.Options decodeImageForOption(ContentResolver resolver, Uri uri)
|
||||
throws FileNotFoundException {
|
||||
InputStream stream = null;
|
||||
try {
|
||||
stream = resolver.openInputStream(uri);
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeStream(stream, EMPTY_RECT, options);
|
||||
options.inJustDecodeBounds = false;
|
||||
return options;
|
||||
} finally {
|
||||
closeSafe(stream);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode image from uri using given "inSampleSize", but if failed due to out-of-memory then raise
|
||||
* the inSampleSize until success.
|
||||
*/
|
||||
private static Bitmap decodeImage(
|
||||
ContentResolver resolver, Uri uri, BitmapFactory.Options options)
|
||||
throws FileNotFoundException {
|
||||
do {
|
||||
InputStream stream = null;
|
||||
try {
|
||||
stream = resolver.openInputStream(uri);
|
||||
return BitmapFactory.decodeStream(stream, EMPTY_RECT, options);
|
||||
} catch (OutOfMemoryError e) {
|
||||
options.inSampleSize *= 2;
|
||||
} finally {
|
||||
closeSafe(stream);
|
||||
}
|
||||
} while (options.inSampleSize <= 512);
|
||||
throw new RuntimeException("Failed to decode image: " + uri);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode specific rectangle bitmap from stream using sampling to get bitmap with the requested
|
||||
* limit.
|
||||
*
|
||||
* @param sampleMulti used to increase the sampling of the image to handle memory issues.
|
||||
*/
|
||||
private static BitmapSampled decodeSampledBitmapRegion(
|
||||
Context context, Uri uri, Rect rect, int reqWidth, int reqHeight, int sampleMulti) {
|
||||
InputStream stream = null;
|
||||
BitmapRegionDecoder decoder = null;
|
||||
try {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inSampleSize =
|
||||
sampleMulti
|
||||
* calculateInSampleSizeByReqestedSize(
|
||||
rect.width(), rect.height(), reqWidth, reqHeight);
|
||||
|
||||
stream = context.getContentResolver().openInputStream(uri);
|
||||
decoder = BitmapRegionDecoder.newInstance(stream, false);
|
||||
do {
|
||||
try {
|
||||
return new BitmapSampled(decoder.decodeRegion(rect, options), options.inSampleSize);
|
||||
} catch (OutOfMemoryError e) {
|
||||
options.inSampleSize *= 2;
|
||||
}
|
||||
} while (options.inSampleSize <= 512);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(
|
||||
"Failed to load sampled bitmap: " + uri + "\r\n" + e.getMessage(), e);
|
||||
} finally {
|
||||
closeSafe(stream);
|
||||
if (decoder != null) {
|
||||
decoder.recycle();
|
||||
}
|
||||
}
|
||||
return new BitmapSampled(null, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Special crop of bitmap rotated by not stright angle, in this case the original crop bitmap
|
||||
* contains parts beyond the required crop area, this method crops the already cropped and rotated
|
||||
* bitmap to the final rectangle.<br>
|
||||
* Note: rotating by 0, 90, 180 or 270 degrees doesn't require extra cropping.
|
||||
*/
|
||||
private static Bitmap cropForRotatedImage(
|
||||
Bitmap bitmap,
|
||||
float[] points,
|
||||
Rect rect,
|
||||
int degreesRotated,
|
||||
boolean fixAspectRatio,
|
||||
int aspectRatioX,
|
||||
int aspectRatioY) {
|
||||
if (degreesRotated % 90 != 0) {
|
||||
|
||||
int adjLeft = 0, adjTop = 0, width = 0, height = 0;
|
||||
double rads = Math.toRadians(degreesRotated);
|
||||
int compareTo =
|
||||
degreesRotated < 90 || (degreesRotated > 180 && degreesRotated < 270)
|
||||
? rect.left
|
||||
: rect.right;
|
||||
for (int i = 0; i < points.length; i += 2) {
|
||||
if (points[i] >= compareTo - 1 && points[i] <= compareTo + 1) {
|
||||
adjLeft = (int) Math.abs(Math.sin(rads) * (rect.bottom - points[i + 1]));
|
||||
adjTop = (int) Math.abs(Math.cos(rads) * (points[i + 1] - rect.top));
|
||||
width = (int) Math.abs((points[i + 1] - rect.top) / Math.sin(rads));
|
||||
height = (int) Math.abs((rect.bottom - points[i + 1]) / Math.cos(rads));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
rect.set(adjLeft, adjTop, adjLeft + width, adjTop + height);
|
||||
if (fixAspectRatio) {
|
||||
fixRectForAspectRatio(rect, aspectRatioX, aspectRatioY);
|
||||
}
|
||||
|
||||
Bitmap bitmapTmp = bitmap;
|
||||
bitmap = Bitmap.createBitmap(bitmap, rect.left, rect.top, rect.width(), rect.height());
|
||||
if (bitmapTmp != bitmap) {
|
||||
bitmapTmp.recycle();
|
||||
}
|
||||
}
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the largest inSampleSize value that is a power of 2 and keeps both height and width
|
||||
* larger than the requested height and width.
|
||||
*/
|
||||
private static int calculateInSampleSizeByReqestedSize(
|
||||
int width, int height, int reqWidth, int reqHeight) {
|
||||
int inSampleSize = 1;
|
||||
if (height > reqHeight || width > reqWidth) {
|
||||
while ((height / 2 / inSampleSize) > reqHeight && (width / 2 / inSampleSize) > reqWidth) {
|
||||
inSampleSize *= 2;
|
||||
}
|
||||
}
|
||||
return inSampleSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the largest inSampleSize value that is a power of 2 and keeps both height and width
|
||||
* smaller than max texture size allowed for the device.
|
||||
*/
|
||||
private static int calculateInSampleSizeByMaxTextureSize(int width, int height) {
|
||||
int inSampleSize = 1;
|
||||
if (mMaxTextureSize == 0) {
|
||||
mMaxTextureSize = getMaxTextureSize();
|
||||
}
|
||||
if (mMaxTextureSize > 0) {
|
||||
while ((height / inSampleSize) > mMaxTextureSize
|
||||
|| (width / inSampleSize) > mMaxTextureSize) {
|
||||
inSampleSize *= 2;
|
||||
}
|
||||
}
|
||||
return inSampleSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate the given bitmap by the given degrees.<br>
|
||||
* New bitmap is created and the old one is recycled.
|
||||
*/
|
||||
private static Bitmap rotateAndFlipBitmapInt(
|
||||
Bitmap bitmap, int degrees, boolean flipHorizontally, boolean flipVertically) {
|
||||
if (degrees > 0 || flipHorizontally || flipVertically) {
|
||||
Matrix matrix = new Matrix();
|
||||
matrix.setRotate(degrees);
|
||||
matrix.postScale(flipHorizontally ? -1 : 1, flipVertically ? -1 : 1);
|
||||
Bitmap newBitmap =
|
||||
Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, false);
|
||||
if (newBitmap != bitmap) {
|
||||
bitmap.recycle();
|
||||
}
|
||||
return newBitmap;
|
||||
} else {
|
||||
return bitmap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the max size of bitmap allowed to be rendered on the device.<br>
|
||||
* http://stackoverflow.com/questions/7428996/hw-accelerated-activity-how-to-get-opengl-texture-size-limit.
|
||||
*/
|
||||
private static int getMaxTextureSize() {
|
||||
// Safe minimum default size
|
||||
final int IMAGE_MAX_BITMAP_DIMENSION = 2048;
|
||||
|
||||
try {
|
||||
// Get EGL Display
|
||||
EGL10 egl = (EGL10) EGLContext.getEGL();
|
||||
EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
|
||||
|
||||
// Initialise
|
||||
int[] version = new int[2];
|
||||
egl.eglInitialize(display, version);
|
||||
|
||||
// Query total number of configurations
|
||||
int[] totalConfigurations = new int[1];
|
||||
egl.eglGetConfigs(display, null, 0, totalConfigurations);
|
||||
|
||||
// Query actual list configurations
|
||||
EGLConfig[] configurationsList = new EGLConfig[totalConfigurations[0]];
|
||||
egl.eglGetConfigs(display, configurationsList, totalConfigurations[0], totalConfigurations);
|
||||
|
||||
int[] textureSize = new int[1];
|
||||
int maximumTextureSize = 0;
|
||||
|
||||
// Iterate through all the configurations to located the maximum texture size
|
||||
for (int i = 0; i < totalConfigurations[0]; i++) {
|
||||
// Only need to check for width since opengl textures are always squared
|
||||
egl.eglGetConfigAttrib(
|
||||
display, configurationsList[i], EGL10.EGL_MAX_PBUFFER_WIDTH, textureSize);
|
||||
|
||||
// Keep track of the maximum texture size
|
||||
if (maximumTextureSize < textureSize[0]) {
|
||||
maximumTextureSize = textureSize[0];
|
||||
}
|
||||
}
|
||||
|
||||
// Release
|
||||
egl.eglTerminate(display);
|
||||
|
||||
// Return largest texture size found, or default
|
||||
return Math.max(maximumTextureSize, IMAGE_MAX_BITMAP_DIMENSION);
|
||||
} catch (Exception e) {
|
||||
return IMAGE_MAX_BITMAP_DIMENSION;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the given closeable object (Stream) in a safe way: check if it is null and catch-log
|
||||
* exception thrown.
|
||||
*
|
||||
* @param closeable the closable object to close
|
||||
*/
|
||||
private static void closeSafe(Closeable closeable) {
|
||||
if (closeable != null) {
|
||||
try {
|
||||
closeable.close();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region: Inner class: BitmapSampled
|
||||
|
||||
/**
|
||||
* Holds bitmap instance and the sample size that the bitmap was loaded/cropped with.
|
||||
*/
|
||||
static final class BitmapSampled {
|
||||
|
||||
/**
|
||||
* The bitmap instance
|
||||
*/
|
||||
public final Bitmap bitmap;
|
||||
|
||||
/**
|
||||
* The sample size used to lower the size of the bitmap (1,2,4,8,...)
|
||||
*/
|
||||
final int sampleSize;
|
||||
|
||||
BitmapSampled(Bitmap bitmap, int sampleSize) {
|
||||
this.bitmap = bitmap;
|
||||
this.sampleSize = sampleSize;
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region: Inner class: RotateBitmapResult
|
||||
|
||||
/**
|
||||
* The result of {@link #rotateBitmapByExif(android.graphics.Bitmap, ExifInterface)}.
|
||||
*/
|
||||
static final class RotateBitmapResult {
|
||||
|
||||
/**
|
||||
* The loaded bitmap
|
||||
*/
|
||||
public final Bitmap bitmap;
|
||||
|
||||
/**
|
||||
* The degrees the image was rotated
|
||||
*/
|
||||
final int degrees;
|
||||
|
||||
RotateBitmapResult(Bitmap bitmap, int degrees) {
|
||||
this.bitmap = bitmap;
|
||||
this.degrees = degrees;
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
}
|
|
@ -1,367 +0,0 @@
|
|||
// "Therefore those skilled at the unorthodox
|
||||
// are infinite as heaven and earth,
|
||||
// inexhaustible as the great rivers.
|
||||
// When they come to an end,
|
||||
// they begin again,
|
||||
// like the days and months;
|
||||
// they die and are reborn,
|
||||
// like the four seasons."
|
||||
//
|
||||
// - Sun Tsu,
|
||||
// "The Art of War"
|
||||
|
||||
package com.theartofdev.edmodo.cropper;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* Built-in activity for image cropping.<br>
|
||||
* Use {@link CropImage#activity(Uri)} to create a builder to start this activity.
|
||||
*/
|
||||
public class CropImageActivity extends AppCompatActivity
|
||||
implements CropImageView.OnSetImageUriCompleteListener,
|
||||
CropImageView.OnCropImageCompleteListener {
|
||||
|
||||
/**
|
||||
* The crop image view library widget used in the activity
|
||||
*/
|
||||
private CropImageView mCropImageView;
|
||||
|
||||
/**
|
||||
* Persist URI image to crop URI if specific permissions are required
|
||||
*/
|
||||
private Uri mCropImageUri;
|
||||
|
||||
/**
|
||||
* the options that were set for the crop image
|
||||
*/
|
||||
private CropImageOptions mOptions;
|
||||
|
||||
@Override
|
||||
@SuppressLint("NewApi")
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.crop_image_activity);
|
||||
|
||||
mCropImageView = findViewById(R.id.cropImageView);
|
||||
|
||||
Bundle bundle = getIntent().getBundleExtra(CropImage.CROP_IMAGE_EXTRA_BUNDLE);
|
||||
mCropImageUri = bundle.getParcelable(CropImage.CROP_IMAGE_EXTRA_SOURCE);
|
||||
mOptions = bundle.getParcelable(CropImage.CROP_IMAGE_EXTRA_OPTIONS);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
if (mCropImageUri == null || mCropImageUri.equals(Uri.EMPTY)) {
|
||||
if (CropImage.isExplicitCameraPermissionRequired(this)) {
|
||||
// request permissions and handle the result in onRequestPermissionsResult()
|
||||
requestPermissions(
|
||||
new String[]{Manifest.permission.CAMERA},
|
||||
CropImage.CAMERA_CAPTURE_PERMISSIONS_REQUEST_CODE);
|
||||
} else {
|
||||
CropImage.startPickImageActivity(this);
|
||||
}
|
||||
} else if (CropImage.isReadExternalStoragePermissionsRequired(this, mCropImageUri)) {
|
||||
// request permissions and handle the result in onRequestPermissionsResult()
|
||||
requestPermissions(
|
||||
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
|
||||
CropImage.PICK_IMAGE_PERMISSIONS_REQUEST_CODE);
|
||||
} else {
|
||||
// no permissions required or already grunted, can start crop image activity
|
||||
mCropImageView.setImageUriAsync(mCropImageUri);
|
||||
}
|
||||
}
|
||||
|
||||
ActionBar actionBar = getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
CharSequence title = mOptions != null &&
|
||||
mOptions.activityTitle != null && mOptions.activityTitle.length() > 0
|
||||
? mOptions.activityTitle
|
||||
: getResources().getString(R.string.crop_image_activity_title);
|
||||
actionBar.setTitle(title);
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
mCropImageView.setOnSetImageUriCompleteListener(this);
|
||||
mCropImageView.setOnCropImageCompleteListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
mCropImageView.setOnSetImageUriCompleteListener(null);
|
||||
mCropImageView.setOnCropImageCompleteListener(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.crop_image_menu, menu);
|
||||
|
||||
if (!mOptions.allowRotation) {
|
||||
menu.removeItem(R.id.crop_image_menu_rotate_left);
|
||||
menu.removeItem(R.id.crop_image_menu_rotate_right);
|
||||
} else if (mOptions.allowCounterRotation) {
|
||||
menu.findItem(R.id.crop_image_menu_rotate_left).setVisible(true);
|
||||
}
|
||||
|
||||
if (!mOptions.allowFlipping) {
|
||||
menu.removeItem(R.id.crop_image_menu_flip);
|
||||
}
|
||||
|
||||
if (mOptions.cropMenuCropButtonTitle != null) {
|
||||
menu.findItem(R.id.crop_image_menu_crop).setTitle(mOptions.cropMenuCropButtonTitle);
|
||||
}
|
||||
|
||||
Drawable cropIcon = null;
|
||||
try {
|
||||
if (mOptions.cropMenuCropButtonIcon != 0) {
|
||||
cropIcon = ContextCompat.getDrawable(this, mOptions.cropMenuCropButtonIcon);
|
||||
menu.findItem(R.id.crop_image_menu_crop).setIcon(cropIcon);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
if (mOptions.activityMenuIconColor != 0) {
|
||||
updateMenuItemIconColor(
|
||||
menu, R.id.crop_image_menu_rotate_left, mOptions.activityMenuIconColor);
|
||||
updateMenuItemIconColor(
|
||||
menu, R.id.crop_image_menu_rotate_right, mOptions.activityMenuIconColor);
|
||||
updateMenuItemIconColor(menu, R.id.crop_image_menu_flip, mOptions.activityMenuIconColor);
|
||||
if (cropIcon != null) {
|
||||
updateMenuItemIconColor(menu, R.id.crop_image_menu_crop, mOptions.activityMenuIconColor);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.crop_image_menu_crop) {
|
||||
cropImage();
|
||||
return true;
|
||||
}
|
||||
if (item.getItemId() == R.id.crop_image_menu_rotate_left) {
|
||||
rotateImage(-mOptions.rotationDegrees);
|
||||
return true;
|
||||
}
|
||||
if (item.getItemId() == R.id.crop_image_menu_rotate_right) {
|
||||
rotateImage(mOptions.rotationDegrees);
|
||||
return true;
|
||||
}
|
||||
if (item.getItemId() == R.id.crop_image_menu_flip_horizontally) {
|
||||
mCropImageView.flipImageHorizontally();
|
||||
return true;
|
||||
}
|
||||
if (item.getItemId() == R.id.crop_image_menu_flip_vertically) {
|
||||
mCropImageView.flipImageVertically();
|
||||
return true;
|
||||
}
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
setResultCancel();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
super.onBackPressed();
|
||||
setResultCancel();
|
||||
}
|
||||
|
||||
@Override
|
||||
@SuppressLint("NewApi")
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
|
||||
// handle result of pick image chooser
|
||||
if (requestCode == CropImage.PICK_IMAGE_CHOOSER_REQUEST_CODE) {
|
||||
if (resultCode == Activity.RESULT_CANCELED) {
|
||||
// User cancelled the picker. We don't have anything to crop
|
||||
setResultCancel();
|
||||
}
|
||||
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
mCropImageUri = CropImage.getPickImageResultUri(this, data);
|
||||
|
||||
// For API >= 23 we need to check specifically that we have permissions to read external
|
||||
// storage.
|
||||
if (CropImage.isReadExternalStoragePermissionsRequired(this, mCropImageUri)) {
|
||||
// request permissions and handle the result in onRequestPermissionsResult()
|
||||
requestPermissions(
|
||||
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
|
||||
CropImage.PICK_IMAGE_PERMISSIONS_REQUEST_CODE);
|
||||
} else {
|
||||
// no permissions required or already grunted, can start crop image activity
|
||||
mCropImageView.setImageUriAsync(mCropImageUri);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(
|
||||
int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
if (requestCode == CropImage.PICK_IMAGE_PERMISSIONS_REQUEST_CODE) {
|
||||
if (mCropImageUri != null
|
||||
&& grantResults.length > 0
|
||||
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
// required permissions granted, start crop image activity
|
||||
mCropImageView.setImageUriAsync(mCropImageUri);
|
||||
} else {
|
||||
Toast.makeText(this, R.string.crop_image_activity_no_permissions, Toast.LENGTH_LONG).show();
|
||||
setResultCancel();
|
||||
}
|
||||
}
|
||||
|
||||
if (requestCode == CropImage.CAMERA_CAPTURE_PERMISSIONS_REQUEST_CODE) {
|
||||
// Irrespective of whether camera permission was given or not, we show the picker
|
||||
// The picker will not add the camera intent if permission is not available
|
||||
CropImage.startPickImageActivity(this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetImageUriComplete(CropImageView view, Uri uri, Exception error) {
|
||||
if (error == null) {
|
||||
if (mOptions.initialCropWindowRectangle != null) {
|
||||
mCropImageView.setCropRect(mOptions.initialCropWindowRectangle);
|
||||
}
|
||||
if (mOptions.initialRotation > -1) {
|
||||
mCropImageView.setRotatedDegrees(mOptions.initialRotation);
|
||||
}
|
||||
} else {
|
||||
setResult(null, error, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCropImageComplete(CropImageView view, CropImageView.CropResult result) {
|
||||
setResult(result.getUri(), result.getError(), result.getSampleSize());
|
||||
}
|
||||
|
||||
// region: Private methods
|
||||
|
||||
/**
|
||||
* Execute crop image and save the result tou output uri.
|
||||
*/
|
||||
protected void cropImage() {
|
||||
if (mOptions.noOutputImage) {
|
||||
setResult(null, null, 1);
|
||||
} else {
|
||||
Uri outputUri = getOutputUri();
|
||||
mCropImageView.saveCroppedImageAsync(
|
||||
outputUri,
|
||||
mOptions.outputCompressFormat,
|
||||
mOptions.outputCompressQuality,
|
||||
mOptions.outputRequestWidth,
|
||||
mOptions.outputRequestHeight,
|
||||
mOptions.outputRequestSizeOptions);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rotate the image in the crop image view.
|
||||
*/
|
||||
protected void rotateImage(int degrees) {
|
||||
mCropImageView.rotateImage(degrees);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Android uri to save the cropped image into.<br>
|
||||
* Use the given in options or create a temp file.
|
||||
*/
|
||||
protected Uri getOutputUri() {
|
||||
Uri outputUri = mOptions.outputUri;
|
||||
if (outputUri == null || outputUri.equals(Uri.EMPTY)) {
|
||||
try {
|
||||
String ext =
|
||||
mOptions.outputCompressFormat == Bitmap.CompressFormat.JPEG
|
||||
? ".jpg"
|
||||
: mOptions.outputCompressFormat == Bitmap.CompressFormat.PNG ? ".png" : ".webp";
|
||||
outputUri = Uri.fromFile(File.createTempFile("cropped", ext, getCacheDir()));
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to create temp file for output image", e);
|
||||
}
|
||||
}
|
||||
return outputUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Result with cropped image data or error if failed.
|
||||
*/
|
||||
protected void setResult(Uri uri, Exception error, int sampleSize) {
|
||||
int resultCode = error == null ? RESULT_OK : CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE;
|
||||
setResult(resultCode, getResultIntent(uri, error, sampleSize));
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel of cropping activity.
|
||||
*/
|
||||
protected void setResultCancel() {
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get intent instance to be used for the result of this activity.
|
||||
*/
|
||||
protected Intent getResultIntent(Uri uri, Exception error, int sampleSize) {
|
||||
CropImage.ActivityResult result =
|
||||
new CropImage.ActivityResult(
|
||||
mCropImageView.getImageUri(),
|
||||
uri,
|
||||
error,
|
||||
mCropImageView.getCropPoints(),
|
||||
mCropImageView.getCropRect(),
|
||||
mCropImageView.getRotatedDegrees(),
|
||||
mCropImageView.getWholeImageRect(),
|
||||
sampleSize);
|
||||
Intent intent = new Intent();
|
||||
intent.putExtras(getIntent());
|
||||
intent.putExtra(CropImage.CROP_IMAGE_EXTRA_RESULT, result);
|
||||
return intent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the color of a specific menu item to the given color.
|
||||
*/
|
||||
private void updateMenuItemIconColor(Menu menu, int itemId, int color) {
|
||||
MenuItem menuItem = menu.findItem(itemId);
|
||||
if (menuItem != null) {
|
||||
Drawable menuItemIcon = menuItem.getIcon();
|
||||
if (menuItemIcon != null) {
|
||||
try {
|
||||
menuItemIcon.mutate();
|
||||
menuItemIcon.setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
|
||||
menuItem.setIcon(menuItemIcon);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,123 +0,0 @@
|
|||
// "Therefore those skilled at the unorthodox
|
||||
// are infinite as heaven and earth,
|
||||
// inexhaustible as the great rivers.
|
||||
// When they come to an end,
|
||||
// they begin again,
|
||||
// like the days and months;
|
||||
// they die and are reborn,
|
||||
// like the four seasons."
|
||||
//
|
||||
// - Sun Tsu,
|
||||
// "The Art of War"
|
||||
|
||||
package com.theartofdev.edmodo.cropper;
|
||||
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.RectF;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.Transformation;
|
||||
import android.widget.ImageView;
|
||||
|
||||
/**
|
||||
* Animation to handle smooth cropping image matrix transformation change, specifically for
|
||||
* zoom-in/out.
|
||||
*/
|
||||
final class CropImageAnimation extends Animation implements Animation.AnimationListener {
|
||||
|
||||
// region: Fields and Consts
|
||||
|
||||
private final ImageView mImageView;
|
||||
|
||||
private final CropOverlayView mCropOverlayView;
|
||||
|
||||
private final float[] mStartBoundPoints = new float[8];
|
||||
|
||||
private final float[] mEndBoundPoints = new float[8];
|
||||
|
||||
private final RectF mStartCropWindowRect = new RectF();
|
||||
|
||||
private final RectF mEndCropWindowRect = new RectF();
|
||||
|
||||
private final float[] mStartImageMatrix = new float[9];
|
||||
|
||||
private final float[] mEndImageMatrix = new float[9];
|
||||
|
||||
private final RectF mAnimRect = new RectF();
|
||||
|
||||
private final float[] mAnimPoints = new float[8];
|
||||
|
||||
private final float[] mAnimMatrix = new float[9];
|
||||
// endregion
|
||||
|
||||
public CropImageAnimation(ImageView cropImageView, CropOverlayView cropOverlayView) {
|
||||
mImageView = cropImageView;
|
||||
mCropOverlayView = cropOverlayView;
|
||||
|
||||
setDuration(300);
|
||||
setFillAfter(true);
|
||||
setInterpolator(new AccelerateDecelerateInterpolator());
|
||||
setAnimationListener(this);
|
||||
}
|
||||
|
||||
public void setStartState(float[] boundPoints, Matrix imageMatrix) {
|
||||
reset();
|
||||
System.arraycopy(boundPoints, 0, mStartBoundPoints, 0, 8);
|
||||
mStartCropWindowRect.set(mCropOverlayView.getCropWindowRect());
|
||||
imageMatrix.getValues(mStartImageMatrix);
|
||||
}
|
||||
|
||||
public void setEndState(float[] boundPoints, Matrix imageMatrix) {
|
||||
System.arraycopy(boundPoints, 0, mEndBoundPoints, 0, 8);
|
||||
mEndCropWindowRect.set(mCropOverlayView.getCropWindowRect());
|
||||
imageMatrix.getValues(mEndImageMatrix);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void applyTransformation(float interpolatedTime, Transformation t) {
|
||||
|
||||
mAnimRect.left =
|
||||
mStartCropWindowRect.left
|
||||
+ (mEndCropWindowRect.left - mStartCropWindowRect.left) * interpolatedTime;
|
||||
mAnimRect.top =
|
||||
mStartCropWindowRect.top
|
||||
+ (mEndCropWindowRect.top - mStartCropWindowRect.top) * interpolatedTime;
|
||||
mAnimRect.right =
|
||||
mStartCropWindowRect.right
|
||||
+ (mEndCropWindowRect.right - mStartCropWindowRect.right) * interpolatedTime;
|
||||
mAnimRect.bottom =
|
||||
mStartCropWindowRect.bottom
|
||||
+ (mEndCropWindowRect.bottom - mStartCropWindowRect.bottom) * interpolatedTime;
|
||||
mCropOverlayView.setCropWindowRect(mAnimRect);
|
||||
|
||||
for (int i = 0; i < mAnimPoints.length; i++) {
|
||||
mAnimPoints[i] =
|
||||
mStartBoundPoints[i] + (mEndBoundPoints[i] - mStartBoundPoints[i]) * interpolatedTime;
|
||||
}
|
||||
mCropOverlayView.setBounds(mAnimPoints, mImageView.getWidth(), mImageView.getHeight());
|
||||
|
||||
for (int i = 0; i < mAnimMatrix.length; i++) {
|
||||
mAnimMatrix[i] =
|
||||
mStartImageMatrix[i] + (mEndImageMatrix[i] - mStartImageMatrix[i]) * interpolatedTime;
|
||||
}
|
||||
Matrix m = mImageView.getImageMatrix();
|
||||
m.setValues(mAnimMatrix);
|
||||
mImageView.setImageMatrix(m);
|
||||
|
||||
mImageView.invalidate();
|
||||
mCropOverlayView.invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationStart(Animation animation) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
mImageView.clearAnimation();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animation animation) {
|
||||
}
|
||||
}
|
|
@ -1,541 +0,0 @@
|
|||
// "Therefore those skilled at the unorthodox
|
||||
// are infinite as heaven and earth;
|
||||
// inexhaustible as the great rivers.
|
||||
// When they come to an end;
|
||||
// they begin again;
|
||||
// like the days and months;
|
||||
// they die and are reborn;
|
||||
// like the four seasons."
|
||||
//
|
||||
// - Sun Tsu;
|
||||
// "The Art of War"
|
||||
|
||||
package com.theartofdev.edmodo.cropper;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.TypedValue;
|
||||
|
||||
/**
|
||||
* All the possible options that can be set to customize crop image.<br>
|
||||
* Initialized with default values.
|
||||
*/
|
||||
public class CropImageOptions implements Parcelable {
|
||||
|
||||
public static final Creator<CropImageOptions> CREATOR =
|
||||
new Creator<CropImageOptions>() {
|
||||
@Override
|
||||
public CropImageOptions createFromParcel(Parcel in) {
|
||||
return new CropImageOptions(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public CropImageOptions[] newArray(int size) {
|
||||
return new CropImageOptions[size];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The shape of the cropping window.
|
||||
*/
|
||||
public CropImageView.CropShape cropShape;
|
||||
|
||||
/**
|
||||
* An edge of the crop window will snap to the corresponding edge of a specified bounding box when
|
||||
* the crop window edge is less than or equal to this distance (in pixels) away from the bounding
|
||||
* box edge. (in pixels)
|
||||
*/
|
||||
public float snapRadius;
|
||||
|
||||
/**
|
||||
* The radius of the touchable area around the handle. (in pixels)<br>
|
||||
* We are basing this value off of the recommended 48dp Rhythm.<br>
|
||||
* See: http://developer.android.com/design/style/metrics-grids.html#48dp-rhythm
|
||||
*/
|
||||
public float touchRadius;
|
||||
|
||||
/**
|
||||
* whether the guidelines should be on, off, or only showing when resizing.
|
||||
*/
|
||||
public CropImageView.Guidelines guidelines;
|
||||
|
||||
/**
|
||||
* The initial scale type of the image in the crop image view
|
||||
*/
|
||||
public CropImageView.ScaleType scaleType;
|
||||
|
||||
/**
|
||||
* if to show crop overlay UI what contains the crop window UI surrounded by background over the
|
||||
* cropping image.<br>
|
||||
* default: true, may disable for animation or frame transition.
|
||||
*/
|
||||
public boolean showCropOverlay;
|
||||
|
||||
/**
|
||||
* if to show progress bar when image async loading/cropping is in progress.<br>
|
||||
* default: true, disable to provide custom progress bar UI.
|
||||
*/
|
||||
public boolean showProgressBar;
|
||||
|
||||
/**
|
||||
* if auto-zoom functionality is enabled.<br>
|
||||
* default: true.
|
||||
*/
|
||||
public boolean autoZoomEnabled;
|
||||
|
||||
/**
|
||||
* if multi-touch should be enabled on the crop box default: false
|
||||
*/
|
||||
public boolean multiTouchEnabled;
|
||||
|
||||
/**
|
||||
* The max zoom allowed during cropping.
|
||||
*/
|
||||
public int maxZoom;
|
||||
|
||||
/**
|
||||
* The initial crop window padding from image borders in percentage of the cropping image
|
||||
* dimensions.
|
||||
*/
|
||||
public float initialCropWindowPaddingRatio;
|
||||
|
||||
/**
|
||||
* whether the width to height aspect ratio should be maintained or free to change.
|
||||
*/
|
||||
public boolean fixAspectRatio;
|
||||
|
||||
/**
|
||||
* the X value of the aspect ratio.
|
||||
*/
|
||||
public int aspectRatioX;
|
||||
|
||||
/**
|
||||
* the Y value of the aspect ratio.
|
||||
*/
|
||||
public int aspectRatioY;
|
||||
|
||||
/**
|
||||
* the thickness of the guidelines lines in pixels. (in pixels)
|
||||
*/
|
||||
public float borderLineThickness;
|
||||
|
||||
/**
|
||||
* the color of the guidelines lines
|
||||
*/
|
||||
public int borderLineColor;
|
||||
|
||||
/**
|
||||
* thickness of the corner line. (in pixels)
|
||||
*/
|
||||
public float borderCornerThickness;
|
||||
|
||||
/**
|
||||
* the offset of corner line from crop window border. (in pixels)
|
||||
*/
|
||||
public float borderCornerOffset;
|
||||
|
||||
/**
|
||||
* the length of the corner line away from the corner. (in pixels)
|
||||
*/
|
||||
public float borderCornerLength;
|
||||
|
||||
/**
|
||||
* the color of the corner line
|
||||
*/
|
||||
public int borderCornerColor;
|
||||
|
||||
/**
|
||||
* the thickness of the guidelines lines. (in pixels)
|
||||
*/
|
||||
public float guidelinesThickness;
|
||||
|
||||
/**
|
||||
* the color of the guidelines lines
|
||||
*/
|
||||
public int guidelinesColor;
|
||||
|
||||
/**
|
||||
* the color of the overlay background around the crop window cover the image parts not in the
|
||||
* crop window.
|
||||
*/
|
||||
public int backgroundColor;
|
||||
|
||||
/**
|
||||
* the min width the crop window is allowed to be. (in pixels)
|
||||
*/
|
||||
public int minCropWindowWidth;
|
||||
|
||||
/**
|
||||
* the min height the crop window is allowed to be. (in pixels)
|
||||
*/
|
||||
public int minCropWindowHeight;
|
||||
|
||||
/**
|
||||
* the min width the resulting cropping image is allowed to be, affects the cropping window
|
||||
* limits. (in pixels)
|
||||
*/
|
||||
public int minCropResultWidth;
|
||||
|
||||
/**
|
||||
* the min height the resulting cropping image is allowed to be, affects the cropping window
|
||||
* limits. (in pixels)
|
||||
*/
|
||||
public int minCropResultHeight;
|
||||
|
||||
/**
|
||||
* the max width the resulting cropping image is allowed to be, affects the cropping window
|
||||
* limits. (in pixels)
|
||||
*/
|
||||
public int maxCropResultWidth;
|
||||
|
||||
/**
|
||||
* the max height the resulting cropping image is allowed to be, affects the cropping window
|
||||
* limits. (in pixels)
|
||||
*/
|
||||
public int maxCropResultHeight;
|
||||
|
||||
/**
|
||||
* the title of the {@link CropImageActivity}
|
||||
*/
|
||||
public CharSequence activityTitle;
|
||||
|
||||
/**
|
||||
* the color to use for action bar items icons
|
||||
*/
|
||||
public int activityMenuIconColor;
|
||||
|
||||
/**
|
||||
* the Android Uri to save the cropped image to
|
||||
*/
|
||||
public Uri outputUri;
|
||||
|
||||
/**
|
||||
* the compression format to use when writing the image
|
||||
*/
|
||||
public Bitmap.CompressFormat outputCompressFormat;
|
||||
|
||||
/**
|
||||
* the quality (if applicable) to use when writing the image (0 - 100)
|
||||
*/
|
||||
public int outputCompressQuality;
|
||||
|
||||
/**
|
||||
* the width to resize the cropped image to (see options)
|
||||
*/
|
||||
public int outputRequestWidth;
|
||||
|
||||
/**
|
||||
* the height to resize the cropped image to (see options)
|
||||
*/
|
||||
public int outputRequestHeight;
|
||||
|
||||
/**
|
||||
* the resize method to use on the cropped bitmap (see options documentation)
|
||||
*/
|
||||
public CropImageView.RequestSizeOptions outputRequestSizeOptions;
|
||||
|
||||
/**
|
||||
* if the result of crop image activity should not save the cropped image bitmap
|
||||
*/
|
||||
public boolean noOutputImage;
|
||||
|
||||
/**
|
||||
* the initial rectangle to set on the cropping image after loading
|
||||
*/
|
||||
public Rect initialCropWindowRectangle;
|
||||
|
||||
/**
|
||||
* the initial rotation to set on the cropping image after loading (0-360 degrees clockwise)
|
||||
*/
|
||||
public int initialRotation;
|
||||
|
||||
/**
|
||||
* if to allow (all) rotation during cropping (activity)
|
||||
*/
|
||||
public boolean allowRotation;
|
||||
|
||||
/**
|
||||
* if to allow (all) flipping during cropping (activity)
|
||||
*/
|
||||
public boolean allowFlipping;
|
||||
|
||||
/**
|
||||
* if to allow counter-clockwise rotation during cropping (activity)
|
||||
*/
|
||||
public boolean allowCounterRotation;
|
||||
|
||||
/**
|
||||
* the amount of degrees to rotate clockwise or counter-clockwise
|
||||
*/
|
||||
public int rotationDegrees;
|
||||
|
||||
/**
|
||||
* whether the image should be flipped horizontally
|
||||
*/
|
||||
public boolean flipHorizontally;
|
||||
|
||||
/**
|
||||
* whether the image should be flipped vertically
|
||||
*/
|
||||
public boolean flipVertically;
|
||||
|
||||
/**
|
||||
* optional, the text of the crop menu crop button
|
||||
*/
|
||||
public CharSequence cropMenuCropButtonTitle;
|
||||
|
||||
/**
|
||||
* optional image resource to be used for crop menu crop icon instead of text
|
||||
*/
|
||||
public int cropMenuCropButtonIcon;
|
||||
|
||||
/**
|
||||
* Init options with defaults.
|
||||
*/
|
||||
public CropImageOptions() {
|
||||
|
||||
DisplayMetrics dm = Resources.getSystem().getDisplayMetrics();
|
||||
|
||||
cropShape = CropImageView.CropShape.RECTANGLE;
|
||||
snapRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, dm);
|
||||
touchRadius = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 24, dm);
|
||||
guidelines = CropImageView.Guidelines.ON_TOUCH;
|
||||
scaleType = CropImageView.ScaleType.FIT_CENTER;
|
||||
showCropOverlay = true;
|
||||
showProgressBar = true;
|
||||
autoZoomEnabled = true;
|
||||
multiTouchEnabled = false;
|
||||
maxZoom = 4;
|
||||
initialCropWindowPaddingRatio = 0.1f;
|
||||
|
||||
fixAspectRatio = false;
|
||||
aspectRatioX = 1;
|
||||
aspectRatioY = 1;
|
||||
|
||||
borderLineThickness = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 3, dm);
|
||||
borderLineColor = Color.argb(170, 255, 255, 255);
|
||||
borderCornerThickness = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, dm);
|
||||
borderCornerOffset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, dm);
|
||||
borderCornerLength = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 14, dm);
|
||||
borderCornerColor = Color.WHITE;
|
||||
|
||||
guidelinesThickness = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 1, dm);
|
||||
guidelinesColor = Color.argb(170, 255, 255, 255);
|
||||
backgroundColor = Color.argb(119, 0, 0, 0);
|
||||
|
||||
minCropWindowWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42, dm);
|
||||
minCropWindowHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 42, dm);
|
||||
minCropResultWidth = 40;
|
||||
minCropResultHeight = 40;
|
||||
maxCropResultWidth = 99999;
|
||||
maxCropResultHeight = 99999;
|
||||
|
||||
activityTitle = "";
|
||||
activityMenuIconColor = 0;
|
||||
|
||||
outputUri = Uri.EMPTY;
|
||||
outputCompressFormat = Bitmap.CompressFormat.JPEG;
|
||||
outputCompressQuality = 90;
|
||||
outputRequestWidth = 0;
|
||||
outputRequestHeight = 0;
|
||||
outputRequestSizeOptions = CropImageView.RequestSizeOptions.NONE;
|
||||
noOutputImage = false;
|
||||
|
||||
initialCropWindowRectangle = null;
|
||||
initialRotation = -1;
|
||||
allowRotation = true;
|
||||
allowFlipping = true;
|
||||
allowCounterRotation = false;
|
||||
rotationDegrees = 90;
|
||||
flipHorizontally = false;
|
||||
flipVertically = false;
|
||||
cropMenuCropButtonTitle = null;
|
||||
|
||||
cropMenuCropButtonIcon = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create object from parcel.
|
||||
*/
|
||||
protected CropImageOptions(Parcel in) {
|
||||
cropShape = CropImageView.CropShape.values()[in.readInt()];
|
||||
snapRadius = in.readFloat();
|
||||
touchRadius = in.readFloat();
|
||||
guidelines = CropImageView.Guidelines.values()[in.readInt()];
|
||||
scaleType = CropImageView.ScaleType.values()[in.readInt()];
|
||||
showCropOverlay = in.readByte() != 0;
|
||||
showProgressBar = in.readByte() != 0;
|
||||
autoZoomEnabled = in.readByte() != 0;
|
||||
multiTouchEnabled = in.readByte() != 0;
|
||||
maxZoom = in.readInt();
|
||||
initialCropWindowPaddingRatio = in.readFloat();
|
||||
fixAspectRatio = in.readByte() != 0;
|
||||
aspectRatioX = in.readInt();
|
||||
aspectRatioY = in.readInt();
|
||||
borderLineThickness = in.readFloat();
|
||||
borderLineColor = in.readInt();
|
||||
borderCornerThickness = in.readFloat();
|
||||
borderCornerOffset = in.readFloat();
|
||||
borderCornerLength = in.readFloat();
|
||||
borderCornerColor = in.readInt();
|
||||
guidelinesThickness = in.readFloat();
|
||||
guidelinesColor = in.readInt();
|
||||
backgroundColor = in.readInt();
|
||||
minCropWindowWidth = in.readInt();
|
||||
minCropWindowHeight = in.readInt();
|
||||
minCropResultWidth = in.readInt();
|
||||
minCropResultHeight = in.readInt();
|
||||
maxCropResultWidth = in.readInt();
|
||||
maxCropResultHeight = in.readInt();
|
||||
activityTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
|
||||
activityMenuIconColor = in.readInt();
|
||||
outputUri = in.readParcelable(Uri.class.getClassLoader());
|
||||
outputCompressFormat = Bitmap.CompressFormat.valueOf(in.readString());
|
||||
outputCompressQuality = in.readInt();
|
||||
outputRequestWidth = in.readInt();
|
||||
outputRequestHeight = in.readInt();
|
||||
outputRequestSizeOptions = CropImageView.RequestSizeOptions.values()[in.readInt()];
|
||||
noOutputImage = in.readByte() != 0;
|
||||
initialCropWindowRectangle = in.readParcelable(Rect.class.getClassLoader());
|
||||
initialRotation = in.readInt();
|
||||
allowRotation = in.readByte() != 0;
|
||||
allowFlipping = in.readByte() != 0;
|
||||
allowCounterRotation = in.readByte() != 0;
|
||||
rotationDegrees = in.readInt();
|
||||
flipHorizontally = in.readByte() != 0;
|
||||
flipVertically = in.readByte() != 0;
|
||||
cropMenuCropButtonTitle = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
|
||||
cropMenuCropButtonIcon = in.readInt();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeInt(cropShape.ordinal());
|
||||
dest.writeFloat(snapRadius);
|
||||
dest.writeFloat(touchRadius);
|
||||
dest.writeInt(guidelines.ordinal());
|
||||
dest.writeInt(scaleType.ordinal());
|
||||
dest.writeByte((byte) (showCropOverlay ? 1 : 0));
|
||||
dest.writeByte((byte) (showProgressBar ? 1 : 0));
|
||||
dest.writeByte((byte) (autoZoomEnabled ? 1 : 0));
|
||||
dest.writeByte((byte) (multiTouchEnabled ? 1 : 0));
|
||||
dest.writeInt(maxZoom);
|
||||
dest.writeFloat(initialCropWindowPaddingRatio);
|
||||
dest.writeByte((byte) (fixAspectRatio ? 1 : 0));
|
||||
dest.writeInt(aspectRatioX);
|
||||
dest.writeInt(aspectRatioY);
|
||||
dest.writeFloat(borderLineThickness);
|
||||
dest.writeInt(borderLineColor);
|
||||
dest.writeFloat(borderCornerThickness);
|
||||
dest.writeFloat(borderCornerOffset);
|
||||
dest.writeFloat(borderCornerLength);
|
||||
dest.writeInt(borderCornerColor);
|
||||
dest.writeFloat(guidelinesThickness);
|
||||
dest.writeInt(guidelinesColor);
|
||||
dest.writeInt(backgroundColor);
|
||||
dest.writeInt(minCropWindowWidth);
|
||||
dest.writeInt(minCropWindowHeight);
|
||||
dest.writeInt(minCropResultWidth);
|
||||
dest.writeInt(minCropResultHeight);
|
||||
dest.writeInt(maxCropResultWidth);
|
||||
dest.writeInt(maxCropResultHeight);
|
||||
TextUtils.writeToParcel(activityTitle, dest, flags);
|
||||
dest.writeInt(activityMenuIconColor);
|
||||
dest.writeParcelable(outputUri, flags);
|
||||
dest.writeString(outputCompressFormat.name());
|
||||
dest.writeInt(outputCompressQuality);
|
||||
dest.writeInt(outputRequestWidth);
|
||||
dest.writeInt(outputRequestHeight);
|
||||
dest.writeInt(outputRequestSizeOptions.ordinal());
|
||||
dest.writeInt(noOutputImage ? 1 : 0);
|
||||
dest.writeParcelable(initialCropWindowRectangle, flags);
|
||||
dest.writeInt(initialRotation);
|
||||
dest.writeByte((byte) (allowRotation ? 1 : 0));
|
||||
dest.writeByte((byte) (allowFlipping ? 1 : 0));
|
||||
dest.writeByte((byte) (allowCounterRotation ? 1 : 0));
|
||||
dest.writeInt(rotationDegrees);
|
||||
dest.writeByte((byte) (flipHorizontally ? 1 : 0));
|
||||
dest.writeByte((byte) (flipVertically ? 1 : 0));
|
||||
TextUtils.writeToParcel(cropMenuCropButtonTitle, dest, flags);
|
||||
dest.writeInt(cropMenuCropButtonIcon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate all the options are withing valid range.
|
||||
*
|
||||
* @throws IllegalArgumentException if any of the options is not valid
|
||||
*/
|
||||
public void validate() {
|
||||
if (maxZoom < 0) {
|
||||
throw new IllegalArgumentException("Cannot set max zoom to a number < 1");
|
||||
}
|
||||
if (touchRadius < 0) {
|
||||
throw new IllegalArgumentException("Cannot set touch radius value to a number <= 0 ");
|
||||
}
|
||||
if (initialCropWindowPaddingRatio < 0 || initialCropWindowPaddingRatio >= 0.5) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot set initial crop window padding value to a number < 0 or >= 0.5");
|
||||
}
|
||||
if (aspectRatioX <= 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot set aspect ratio value to a number less than or equal to 0.");
|
||||
}
|
||||
if (aspectRatioY <= 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot set aspect ratio value to a number less than or equal to 0.");
|
||||
}
|
||||
if (borderLineThickness < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot set line thickness value to a number less than 0.");
|
||||
}
|
||||
if (borderCornerThickness < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot set corner thickness value to a number less than 0.");
|
||||
}
|
||||
if (guidelinesThickness < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot set guidelines thickness value to a number less than 0.");
|
||||
}
|
||||
if (minCropWindowHeight < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot set min crop window height value to a number < 0 ");
|
||||
}
|
||||
if (minCropResultWidth < 0) {
|
||||
throw new IllegalArgumentException("Cannot set min crop result width value to a number < 0 ");
|
||||
}
|
||||
if (minCropResultHeight < 0) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot set min crop result height value to a number < 0 ");
|
||||
}
|
||||
if (maxCropResultWidth < minCropResultWidth) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot set max crop result width to smaller value than min crop result width");
|
||||
}
|
||||
if (maxCropResultHeight < minCropResultHeight) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot set max crop result height to smaller value than min crop result height");
|
||||
}
|
||||
if (outputRequestWidth < 0) {
|
||||
throw new IllegalArgumentException("Cannot set request width value to a number < 0 ");
|
||||
}
|
||||
if (outputRequestHeight < 0) {
|
||||
throw new IllegalArgumentException("Cannot set request height value to a number < 0 ");
|
||||
}
|
||||
if (rotationDegrees < 0 || rotationDegrees > 360) {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot set rotation degrees value to a number < 0 or > 360");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,405 +0,0 @@
|
|||
// "Therefore those skilled at the unorthodox
|
||||
// are infinite as heaven and earth,
|
||||
// inexhaustible as the great rivers.
|
||||
// When they come to an end,
|
||||
// they begin again,
|
||||
// like the days and months;
|
||||
// they die and are reborn,
|
||||
// like the four seasons."
|
||||
//
|
||||
// - Sun Tsu,
|
||||
// "The Art of War"
|
||||
|
||||
package com.theartofdev.edmodo.cropper;
|
||||
|
||||
import android.graphics.RectF;
|
||||
|
||||
/**
|
||||
* Handler from crop window stuff, moving and knowing possition.
|
||||
*/
|
||||
final class CropWindowHandler {
|
||||
|
||||
// region: Fields and Consts
|
||||
|
||||
/**
|
||||
* The 4 edges of the crop window defining its coordinates and size
|
||||
*/
|
||||
private final RectF mEdges = new RectF();
|
||||
|
||||
/**
|
||||
* Rectangle used to return the edges rectangle without ability to change it and without creating
|
||||
* new all the time.
|
||||
*/
|
||||
private final RectF mGetEdges = new RectF();
|
||||
|
||||
/**
|
||||
* Minimum width in pixels that the crop window can get.
|
||||
*/
|
||||
private float mMinCropWindowWidth;
|
||||
|
||||
/**
|
||||
* Minimum height in pixels that the crop window can get.
|
||||
*/
|
||||
private float mMinCropWindowHeight;
|
||||
|
||||
/**
|
||||
* Maximum width in pixels that the crop window can CURRENTLY get.
|
||||
*/
|
||||
private float mMaxCropWindowWidth;
|
||||
|
||||
/**
|
||||
* Maximum height in pixels that the crop window can CURRENTLY get.
|
||||
*/
|
||||
private float mMaxCropWindowHeight;
|
||||
|
||||
/**
|
||||
* Minimum width in pixels that the result of cropping an image can get, affects crop window width
|
||||
* adjusted by width scale factor.
|
||||
*/
|
||||
private float mMinCropResultWidth;
|
||||
|
||||
/**
|
||||
* Minimum height in pixels that the result of cropping an image can get, affects crop window
|
||||
* height adjusted by height scale factor.
|
||||
*/
|
||||
private float mMinCropResultHeight;
|
||||
|
||||
/**
|
||||
* Maximum width in pixels that the result of cropping an image can get, affects crop window width
|
||||
* adjusted by width scale factor.
|
||||
*/
|
||||
private float mMaxCropResultWidth;
|
||||
|
||||
/**
|
||||
* Maximum height in pixels that the result of cropping an image can get, affects crop window
|
||||
* height adjusted by height scale factor.
|
||||
*/
|
||||
private float mMaxCropResultHeight;
|
||||
|
||||
/**
|
||||
* The width scale factor of shown image and actual image
|
||||
*/
|
||||
private float mScaleFactorWidth = 1;
|
||||
|
||||
/**
|
||||
* The height scale factor of shown image and actual image
|
||||
*/
|
||||
private float mScaleFactorHeight = 1;
|
||||
// endregion
|
||||
|
||||
/**
|
||||
* Determines if the specified coordinate is in the target touch zone for a corner handle.
|
||||
*
|
||||
* @param x the x-coordinate of the touch point
|
||||
* @param y the y-coordinate of the touch point
|
||||
* @param handleX the x-coordinate of the corner handle
|
||||
* @param handleY the y-coordinate of the corner handle
|
||||
* @param targetRadius the target radius in pixels
|
||||
* @return true if the touch point is in the target touch zone; false otherwise
|
||||
*/
|
||||
private static boolean isInCornerTargetZone(
|
||||
float x, float y, float handleX, float handleY, float targetRadius) {
|
||||
return Math.abs(x - handleX) <= targetRadius && Math.abs(y - handleY) <= targetRadius;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the specified coordinate is in the target touch zone for a horizontal bar handle.
|
||||
*
|
||||
* @param x the x-coordinate of the touch point
|
||||
* @param y the y-coordinate of the touch point
|
||||
* @param handleXStart the left x-coordinate of the horizontal bar handle
|
||||
* @param handleXEnd the right x-coordinate of the horizontal bar handle
|
||||
* @param handleY the y-coordinate of the horizontal bar handle
|
||||
* @param targetRadius the target radius in pixels
|
||||
* @return true if the touch point is in the target touch zone; false otherwise
|
||||
*/
|
||||
private static boolean isInHorizontalTargetZone(
|
||||
float x, float y, float handleXStart, float handleXEnd, float handleY, float targetRadius) {
|
||||
return x > handleXStart && x < handleXEnd && Math.abs(y - handleY) <= targetRadius;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the specified coordinate is in the target touch zone for a vertical bar handle.
|
||||
*
|
||||
* @param x the x-coordinate of the touch point
|
||||
* @param y the y-coordinate of the touch point
|
||||
* @param handleX the x-coordinate of the vertical bar handle
|
||||
* @param handleYStart the top y-coordinate of the vertical bar handle
|
||||
* @param handleYEnd the bottom y-coordinate of the vertical bar handle
|
||||
* @param targetRadius the target radius in pixels
|
||||
* @return true if the touch point is in the target touch zone; false otherwise
|
||||
*/
|
||||
private static boolean isInVerticalTargetZone(
|
||||
float x, float y, float handleX, float handleYStart, float handleYEnd, float targetRadius) {
|
||||
return Math.abs(x - handleX) <= targetRadius && y > handleYStart && y < handleYEnd;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the specified coordinate falls anywhere inside the given bounds.
|
||||
*
|
||||
* @param x the x-coordinate of the touch point
|
||||
* @param y the y-coordinate of the touch point
|
||||
* @param left the x-coordinate of the left bound
|
||||
* @param top the y-coordinate of the top bound
|
||||
* @param right the x-coordinate of the right bound
|
||||
* @param bottom the y-coordinate of the bottom bound
|
||||
* @return true if the touch point is inside the bounding rectangle; false otherwise
|
||||
*/
|
||||
private static boolean isInCenterTargetZone(
|
||||
float x, float y, float left, float top, float right, float bottom) {
|
||||
return x > left && x < right && y > top && y < bottom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the left/top/right/bottom coordinates of the crop window.
|
||||
*/
|
||||
public RectF getRect() {
|
||||
mGetEdges.set(mEdges);
|
||||
return mGetEdges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the left/top/right/bottom coordinates of the crop window.
|
||||
*/
|
||||
public void setRect(RectF rect) {
|
||||
mEdges.set(rect);
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum width in pixels that the crop window can get.
|
||||
*/
|
||||
public float getMinCropWidth() {
|
||||
return Math.max(mMinCropWindowWidth, mMinCropResultWidth / mScaleFactorWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum height in pixels that the crop window can get.
|
||||
*/
|
||||
public float getMinCropHeight() {
|
||||
return Math.max(mMinCropWindowHeight, mMinCropResultHeight / mScaleFactorHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum width in pixels that the crop window can get.
|
||||
*/
|
||||
public float getMaxCropWidth() {
|
||||
return Math.min(mMaxCropWindowWidth, mMaxCropResultWidth / mScaleFactorWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maximum height in pixels that the crop window can get.
|
||||
*/
|
||||
public float getMaxCropHeight() {
|
||||
return Math.min(mMaxCropWindowHeight, mMaxCropResultHeight / mScaleFactorHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* get the scale factor (on width) of the showen image to original image.
|
||||
*/
|
||||
public float getScaleFactorWidth() {
|
||||
return mScaleFactorWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* get the scale factor (on height) of the showen image to original image.
|
||||
*/
|
||||
public float getScaleFactorHeight() {
|
||||
return mScaleFactorHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* the min size the resulting cropping image is allowed to be, affects the cropping window limits
|
||||
* (in pixels).<br>
|
||||
*/
|
||||
public void setMinCropResultSize(int minCropResultWidth, int minCropResultHeight) {
|
||||
mMinCropResultWidth = minCropResultWidth;
|
||||
mMinCropResultHeight = minCropResultHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* the max size the resulting cropping image is allowed to be, affects the cropping window limits
|
||||
* (in pixels).<br>
|
||||
*/
|
||||
public void setMaxCropResultSize(int maxCropResultWidth, int maxCropResultHeight) {
|
||||
mMaxCropResultWidth = maxCropResultWidth;
|
||||
mMaxCropResultHeight = maxCropResultHeight;
|
||||
}
|
||||
|
||||
// region: Private methods
|
||||
|
||||
/**
|
||||
* set the max width/height and scale factor of the showen image to original image to scale the
|
||||
* limits appropriately.
|
||||
*/
|
||||
public void setCropWindowLimits(
|
||||
float maxWidth, float maxHeight, float scaleFactorWidth, float scaleFactorHeight) {
|
||||
mMaxCropWindowWidth = maxWidth;
|
||||
mMaxCropWindowHeight = maxHeight;
|
||||
mScaleFactorWidth = scaleFactorWidth;
|
||||
mScaleFactorHeight = scaleFactorHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the variables to be used during crop window handling.
|
||||
*/
|
||||
public void setInitialAttributeValues(CropImageOptions options) {
|
||||
mMinCropWindowWidth = options.minCropWindowWidth;
|
||||
mMinCropWindowHeight = options.minCropWindowHeight;
|
||||
mMinCropResultWidth = options.minCropResultWidth;
|
||||
mMinCropResultHeight = options.minCropResultHeight;
|
||||
mMaxCropResultWidth = options.maxCropResultWidth;
|
||||
mMaxCropResultHeight = options.maxCropResultHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates whether the crop window is small enough that the guidelines should be shown. Public
|
||||
* because this function is also used to determine if the center handle should be focused.
|
||||
*
|
||||
* @return boolean Whether the guidelines should be shown or not
|
||||
*/
|
||||
public boolean showGuidelines() {
|
||||
return !(mEdges.width() < 100 || mEdges.height() < 100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which, if any, of the handles are pressed given the touch coordinates, the bounding
|
||||
* box, and the touch radius.
|
||||
*
|
||||
* @param x the x-coordinate of the touch point
|
||||
* @param y the y-coordinate of the touch point
|
||||
* @param targetRadius the target radius in pixels
|
||||
* @return the Handle that was pressed; null if no Handle was pressed
|
||||
*/
|
||||
public CropWindowMoveHandler getMoveHandler(
|
||||
float x, float y, float targetRadius, CropImageView.CropShape cropShape) {
|
||||
CropWindowMoveHandler.Type type =
|
||||
cropShape == CropImageView.CropShape.OVAL
|
||||
? getOvalPressedMoveType(x, y)
|
||||
: getRectanglePressedMoveType(x, y, targetRadius);
|
||||
return type != null ? new CropWindowMoveHandler(type, this, x, y) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which, if any, of the handles are pressed given the touch coordinates, the bounding
|
||||
* box, and the touch radius.
|
||||
*
|
||||
* @param x the x-coordinate of the touch point
|
||||
* @param y the y-coordinate of the touch point
|
||||
* @param targetRadius the target radius in pixels
|
||||
* @return the Handle that was pressed; null if no Handle was pressed
|
||||
*/
|
||||
private CropWindowMoveHandler.Type getRectanglePressedMoveType(
|
||||
float x, float y, float targetRadius) {
|
||||
CropWindowMoveHandler.Type moveType = null;
|
||||
|
||||
// Note: corner-handles take precedence, then side-handles, then center.
|
||||
if (CropWindowHandler.isInCornerTargetZone(x, y, mEdges.left, mEdges.top, targetRadius)) {
|
||||
moveType = CropWindowMoveHandler.Type.TOP_LEFT;
|
||||
} else if (CropWindowHandler.isInCornerTargetZone(
|
||||
x, y, mEdges.right, mEdges.top, targetRadius)) {
|
||||
moveType = CropWindowMoveHandler.Type.TOP_RIGHT;
|
||||
} else if (CropWindowHandler.isInCornerTargetZone(
|
||||
x, y, mEdges.left, mEdges.bottom, targetRadius)) {
|
||||
moveType = CropWindowMoveHandler.Type.BOTTOM_LEFT;
|
||||
} else if (CropWindowHandler.isInCornerTargetZone(
|
||||
x, y, mEdges.right, mEdges.bottom, targetRadius)) {
|
||||
moveType = CropWindowMoveHandler.Type.BOTTOM_RIGHT;
|
||||
} else if (CropWindowHandler.isInCenterTargetZone(
|
||||
x, y, mEdges.left, mEdges.top, mEdges.right, mEdges.bottom)
|
||||
&& focusCenter()) {
|
||||
moveType = CropWindowMoveHandler.Type.CENTER;
|
||||
} else if (CropWindowHandler.isInHorizontalTargetZone(
|
||||
x, y, mEdges.left, mEdges.right, mEdges.top, targetRadius)) {
|
||||
moveType = CropWindowMoveHandler.Type.TOP;
|
||||
} else if (CropWindowHandler.isInHorizontalTargetZone(
|
||||
x, y, mEdges.left, mEdges.right, mEdges.bottom, targetRadius)) {
|
||||
moveType = CropWindowMoveHandler.Type.BOTTOM;
|
||||
} else if (CropWindowHandler.isInVerticalTargetZone(
|
||||
x, y, mEdges.left, mEdges.top, mEdges.bottom, targetRadius)) {
|
||||
moveType = CropWindowMoveHandler.Type.LEFT;
|
||||
} else if (CropWindowHandler.isInVerticalTargetZone(
|
||||
x, y, mEdges.right, mEdges.top, mEdges.bottom, targetRadius)) {
|
||||
moveType = CropWindowMoveHandler.Type.RIGHT;
|
||||
} else if (CropWindowHandler.isInCenterTargetZone(
|
||||
x, y, mEdges.left, mEdges.top, mEdges.right, mEdges.bottom)
|
||||
&& !focusCenter()) {
|
||||
moveType = CropWindowMoveHandler.Type.CENTER;
|
||||
}
|
||||
|
||||
return moveType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines which, if any, of the handles are pressed given the touch coordinates, the bounding
|
||||
* box/oval, and the touch radius.
|
||||
*
|
||||
* @param x the x-coordinate of the touch point
|
||||
* @param y the y-coordinate of the touch point
|
||||
* @return the Handle that was pressed; null if no Handle was pressed
|
||||
*/
|
||||
private CropWindowMoveHandler.Type getOvalPressedMoveType(float x, float y) {
|
||||
|
||||
/*
|
||||
Use a 6x6 grid system divided into 9 "handles", with the center the biggest region. While
|
||||
this is not perfect, it's a good quick-to-ship approach.
|
||||
|
||||
TL T T T T TR
|
||||
L C C C C R
|
||||
L C C C C R
|
||||
L C C C C R
|
||||
L C C C C R
|
||||
BL B B B B BR
|
||||
*/
|
||||
|
||||
float cellLength = mEdges.width() / 6;
|
||||
float leftCenter = mEdges.left + cellLength;
|
||||
float rightCenter = mEdges.left + (5 * cellLength);
|
||||
|
||||
float cellHeight = mEdges.height() / 6;
|
||||
float topCenter = mEdges.top + cellHeight;
|
||||
float bottomCenter = mEdges.top + 5 * cellHeight;
|
||||
|
||||
CropWindowMoveHandler.Type moveType;
|
||||
if (x < leftCenter) {
|
||||
if (y < topCenter) {
|
||||
moveType = CropWindowMoveHandler.Type.TOP_LEFT;
|
||||
} else if (y < bottomCenter) {
|
||||
moveType = CropWindowMoveHandler.Type.LEFT;
|
||||
} else {
|
||||
moveType = CropWindowMoveHandler.Type.BOTTOM_LEFT;
|
||||
}
|
||||
} else if (x < rightCenter) {
|
||||
if (y < topCenter) {
|
||||
moveType = CropWindowMoveHandler.Type.TOP;
|
||||
} else if (y < bottomCenter) {
|
||||
moveType = CropWindowMoveHandler.Type.CENTER;
|
||||
} else {
|
||||
moveType = CropWindowMoveHandler.Type.BOTTOM;
|
||||
}
|
||||
} else {
|
||||
if (y < topCenter) {
|
||||
moveType = CropWindowMoveHandler.Type.TOP_RIGHT;
|
||||
} else if (y < bottomCenter) {
|
||||
moveType = CropWindowMoveHandler.Type.RIGHT;
|
||||
} else {
|
||||
moveType = CropWindowMoveHandler.Type.BOTTOM_RIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
return moveType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the cropper should focus on the center handle or the side handles. If it is a
|
||||
* small image, focus on the center handle so the user can move it. If it is a large image, focus
|
||||
* on the side handles so user can grab them. Corresponds to the appearance of the
|
||||
* RuleOfThirdsGuidelines.
|
||||
*
|
||||
* @return true if it is small enough such that it should focus on the center; less than
|
||||
* show_guidelines limit
|
||||
*/
|
||||
private boolean focusCenter() {
|
||||
return !showGuidelines();
|
||||
}
|
||||
// endregion
|
||||
}
|
|
@ -1,786 +0,0 @@
|
|||
// "Therefore those skilled at the unorthodox
|
||||
// are infinite as heaven and earth,
|
||||
// inexhaustible as the great rivers.
|
||||
// When they come to an end,
|
||||
// they begin again,
|
||||
// like the days and months;
|
||||
// they die and are reborn,
|
||||
// like the four seasons."
|
||||
//
|
||||
// - Sun Tsu,
|
||||
// "The Art of War"
|
||||
|
||||
package com.theartofdev.edmodo.cropper;
|
||||
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.RectF;
|
||||
|
||||
/**
|
||||
* Handler to update crop window edges by the move type - Horizontal, Vertical, Corner or Center.
|
||||
* <br>
|
||||
*/
|
||||
final class CropWindowMoveHandler {
|
||||
|
||||
// region: Fields and Consts
|
||||
|
||||
/**
|
||||
* Matrix used for rectangle rotation handling
|
||||
*/
|
||||
private static final Matrix MATRIX = new Matrix();
|
||||
|
||||
/**
|
||||
* Minimum width in pixels that the crop window can get.
|
||||
*/
|
||||
private final float mMinCropWidth;
|
||||
|
||||
/**
|
||||
* Minimum width in pixels that the crop window can get.
|
||||
*/
|
||||
private final float mMinCropHeight;
|
||||
|
||||
/**
|
||||
* Maximum height in pixels that the crop window can get.
|
||||
*/
|
||||
private final float mMaxCropWidth;
|
||||
|
||||
/**
|
||||
* Maximum height in pixels that the crop window can get.
|
||||
*/
|
||||
private final float mMaxCropHeight;
|
||||
|
||||
/**
|
||||
* The type of crop window move that is handled.
|
||||
*/
|
||||
private final Type mType;
|
||||
|
||||
/**
|
||||
* Holds the x and y offset between the exact touch location and the exact handle location that is
|
||||
* activated. There may be an offset because we allow for some leeway (specified by mHandleRadius)
|
||||
* in activating a handle. However, we want to maintain these offset values while the handle is
|
||||
* being dragged so that the handle doesn't jump.
|
||||
*/
|
||||
private final PointF mTouchOffset = new PointF();
|
||||
// endregion
|
||||
|
||||
/**
|
||||
* @param edgeMoveType the type of move this handler is executing
|
||||
* @param horizontalEdge the primary edge associated with this handle; may be null
|
||||
* @param verticalEdge the secondary edge associated with this handle; may be null
|
||||
* @param cropWindowHandler main crop window handle to get and update the crop window edges
|
||||
* @param touchX the location of the initial toch possition to measure move distance
|
||||
* @param touchY the location of the initial toch possition to measure move distance
|
||||
*/
|
||||
public CropWindowMoveHandler(
|
||||
Type type, CropWindowHandler cropWindowHandler, float touchX, float touchY) {
|
||||
mType = type;
|
||||
mMinCropWidth = cropWindowHandler.getMinCropWidth();
|
||||
mMinCropHeight = cropWindowHandler.getMinCropHeight();
|
||||
mMaxCropWidth = cropWindowHandler.getMaxCropWidth();
|
||||
mMaxCropHeight = cropWindowHandler.getMaxCropHeight();
|
||||
calculateTouchOffset(cropWindowHandler.getRect(), touchX, touchY);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the aspect ratio given a rectangle.
|
||||
*/
|
||||
private static float calculateAspectRatio(float left, float top, float right, float bottom) {
|
||||
return (right - left) / (bottom - top);
|
||||
}
|
||||
|
||||
// region: Private methods
|
||||
|
||||
/**
|
||||
* Updates the crop window by change in the toch location.<br>
|
||||
* Move type handled by this instance, as initialized in creation, affects how the change in toch
|
||||
* location changes the crop window position and size.<br>
|
||||
* After the crop window position/size is changed by toch move it may result in values that
|
||||
* vialate contraints: outside the bounds of the shown bitmap, smaller/larger than min/max size or
|
||||
* missmatch in aspect ratio. So a series of fixes is executed on "secondary" edges to adjust it
|
||||
* by the "primary" edge movement.<br>
|
||||
* Primary is the edge directly affected by move type, secondary is the other edge.<br>
|
||||
* The crop window is changed by directly setting the Edge coordinates.
|
||||
*
|
||||
* @param x the new x-coordinate of this handle
|
||||
* @param y the new y-coordinate of this handle
|
||||
* @param bounds the bounding rectangle of the image
|
||||
* @param viewWidth The bounding image view width used to know the crop overlay is at view edges.
|
||||
* @param viewHeight The bounding image view height used to know the crop overlay is at view
|
||||
* edges.
|
||||
* @param parentView the parent View containing the image
|
||||
* @param snapMargin the maximum distance (in pixels) at which the crop window should snap to the
|
||||
* image
|
||||
* @param fixedAspectRatio is the aspect ration fixed and 'targetAspectRatio' should be used
|
||||
* @param aspectRatio the aspect ratio to maintain
|
||||
*/
|
||||
public void move(
|
||||
RectF rect,
|
||||
float x,
|
||||
float y,
|
||||
RectF bounds,
|
||||
int viewWidth,
|
||||
int viewHeight,
|
||||
float snapMargin,
|
||||
boolean fixedAspectRatio,
|
||||
float aspectRatio) {
|
||||
|
||||
// Adjust the coordinates for the finger position's offset (i.e. the
|
||||
// distance from the initial touch to the precise handle location).
|
||||
// We want to maintain the initial touch's distance to the pressed
|
||||
// handle so that the crop window size does not "jump".
|
||||
float adjX = x + mTouchOffset.x;
|
||||
float adjY = y + mTouchOffset.y;
|
||||
|
||||
if (mType == Type.CENTER) {
|
||||
moveCenter(rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin);
|
||||
} else {
|
||||
if (fixedAspectRatio) {
|
||||
moveSizeWithFixedAspectRatio(
|
||||
rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin, aspectRatio);
|
||||
} else {
|
||||
moveSizeWithFreeAspectRatio(rect, adjX, adjY, bounds, viewWidth, viewHeight, snapMargin);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the offset of the touch point from the precise location of the specified handle.<br>
|
||||
* Save these values in a member variable since we want to maintain this offset as we drag the
|
||||
* handle.
|
||||
*/
|
||||
private void calculateTouchOffset(RectF rect, float touchX, float touchY) {
|
||||
|
||||
float touchOffsetX = 0;
|
||||
float touchOffsetY = 0;
|
||||
|
||||
// Calculate the offset from the appropriate handle.
|
||||
switch (mType) {
|
||||
case TOP_LEFT:
|
||||
touchOffsetX = rect.left - touchX;
|
||||
touchOffsetY = rect.top - touchY;
|
||||
break;
|
||||
case TOP_RIGHT:
|
||||
touchOffsetX = rect.right - touchX;
|
||||
touchOffsetY = rect.top - touchY;
|
||||
break;
|
||||
case BOTTOM_LEFT:
|
||||
touchOffsetX = rect.left - touchX;
|
||||
touchOffsetY = rect.bottom - touchY;
|
||||
break;
|
||||
case BOTTOM_RIGHT:
|
||||
touchOffsetX = rect.right - touchX;
|
||||
touchOffsetY = rect.bottom - touchY;
|
||||
break;
|
||||
case LEFT:
|
||||
touchOffsetX = rect.left - touchX;
|
||||
touchOffsetY = 0;
|
||||
break;
|
||||
case TOP:
|
||||
touchOffsetX = 0;
|
||||
touchOffsetY = rect.top - touchY;
|
||||
break;
|
||||
case RIGHT:
|
||||
touchOffsetX = rect.right - touchX;
|
||||
touchOffsetY = 0;
|
||||
break;
|
||||
case BOTTOM:
|
||||
touchOffsetX = 0;
|
||||
touchOffsetY = rect.bottom - touchY;
|
||||
break;
|
||||
case CENTER:
|
||||
touchOffsetX = rect.centerX() - touchX;
|
||||
touchOffsetY = rect.centerY() - touchY;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
mTouchOffset.x = touchOffsetX;
|
||||
mTouchOffset.y = touchOffsetY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Center move only changes the position of the crop window without changing the size.
|
||||
*/
|
||||
private void moveCenter(
|
||||
RectF rect, float x, float y, RectF bounds, int viewWidth, int viewHeight, float snapRadius) {
|
||||
float dx = x - rect.centerX();
|
||||
float dy = y - rect.centerY();
|
||||
if (rect.left + dx < 0
|
||||
|| rect.right + dx > viewWidth
|
||||
|| rect.left + dx < bounds.left
|
||||
|| rect.right + dx > bounds.right) {
|
||||
dx /= 1.05f;
|
||||
mTouchOffset.x -= dx / 2;
|
||||
}
|
||||
if (rect.top + dy < 0
|
||||
|| rect.bottom + dy > viewHeight
|
||||
|| rect.top + dy < bounds.top
|
||||
|| rect.bottom + dy > bounds.bottom) {
|
||||
dy /= 1.05f;
|
||||
mTouchOffset.y -= dy / 2;
|
||||
}
|
||||
rect.offset(dx, dy);
|
||||
snapEdgesToBounds(rect, bounds, snapRadius);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the size of the crop window on the required edge (or edges for corner size move) without
|
||||
* affecting "secondary" edges.<br>
|
||||
* Only the primary edge(s) are fixed to stay within limits.
|
||||
*/
|
||||
private void moveSizeWithFreeAspectRatio(
|
||||
RectF rect, float x, float y, RectF bounds, int viewWidth, int viewHeight, float snapMargin) {
|
||||
switch (mType) {
|
||||
case TOP_LEFT:
|
||||
adjustTop(rect, y, bounds, snapMargin, 0, false, false);
|
||||
adjustLeft(rect, x, bounds, snapMargin, 0, false, false);
|
||||
break;
|
||||
case TOP_RIGHT:
|
||||
adjustTop(rect, y, bounds, snapMargin, 0, false, false);
|
||||
adjustRight(rect, x, bounds, viewWidth, snapMargin, 0, false, false);
|
||||
break;
|
||||
case BOTTOM_LEFT:
|
||||
adjustBottom(rect, y, bounds, viewHeight, snapMargin, 0, false, false);
|
||||
adjustLeft(rect, x, bounds, snapMargin, 0, false, false);
|
||||
break;
|
||||
case BOTTOM_RIGHT:
|
||||
adjustBottom(rect, y, bounds, viewHeight, snapMargin, 0, false, false);
|
||||
adjustRight(rect, x, bounds, viewWidth, snapMargin, 0, false, false);
|
||||
break;
|
||||
case LEFT:
|
||||
adjustLeft(rect, x, bounds, snapMargin, 0, false, false);
|
||||
break;
|
||||
case TOP:
|
||||
adjustTop(rect, y, bounds, snapMargin, 0, false, false);
|
||||
break;
|
||||
case RIGHT:
|
||||
adjustRight(rect, x, bounds, viewWidth, snapMargin, 0, false, false);
|
||||
break;
|
||||
case BOTTOM:
|
||||
adjustBottom(rect, y, bounds, viewHeight, snapMargin, 0, false, false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the size of the crop window on the required "primary" edge WITH affect to relevant
|
||||
* "secondary" edge via aspect ratio.<br>
|
||||
* Example: change in the left edge (primary) will affect top and bottom edges (secondary) to
|
||||
* preserve the given aspect ratio.
|
||||
*/
|
||||
private void moveSizeWithFixedAspectRatio(
|
||||
RectF rect,
|
||||
float x,
|
||||
float y,
|
||||
RectF bounds,
|
||||
int viewWidth,
|
||||
int viewHeight,
|
||||
float snapMargin,
|
||||
float aspectRatio) {
|
||||
switch (mType) {
|
||||
case TOP_LEFT:
|
||||
if (calculateAspectRatio(x, y, rect.right, rect.bottom) < aspectRatio) {
|
||||
adjustTop(rect, y, bounds, snapMargin, aspectRatio, true, false);
|
||||
adjustLeftByAspectRatio(rect, aspectRatio);
|
||||
} else {
|
||||
adjustLeft(rect, x, bounds, snapMargin, aspectRatio, true, false);
|
||||
adjustTopByAspectRatio(rect, aspectRatio);
|
||||
}
|
||||
break;
|
||||
case TOP_RIGHT:
|
||||
if (calculateAspectRatio(rect.left, y, x, rect.bottom) < aspectRatio) {
|
||||
adjustTop(rect, y, bounds, snapMargin, aspectRatio, false, true);
|
||||
adjustRightByAspectRatio(rect, aspectRatio);
|
||||
} else {
|
||||
adjustRight(rect, x, bounds, viewWidth, snapMargin, aspectRatio, true, false);
|
||||
adjustTopByAspectRatio(rect, aspectRatio);
|
||||
}
|
||||
break;
|
||||
case BOTTOM_LEFT:
|
||||
if (calculateAspectRatio(x, rect.top, rect.right, y) < aspectRatio) {
|
||||
adjustBottom(rect, y, bounds, viewHeight, snapMargin, aspectRatio, true, false);
|
||||
adjustLeftByAspectRatio(rect, aspectRatio);
|
||||
} else {
|
||||
adjustLeft(rect, x, bounds, snapMargin, aspectRatio, false, true);
|
||||
adjustBottomByAspectRatio(rect, aspectRatio);
|
||||
}
|
||||
break;
|
||||
case BOTTOM_RIGHT:
|
||||
if (calculateAspectRatio(rect.left, rect.top, x, y) < aspectRatio) {
|
||||
adjustBottom(rect, y, bounds, viewHeight, snapMargin, aspectRatio, false, true);
|
||||
adjustRightByAspectRatio(rect, aspectRatio);
|
||||
} else {
|
||||
adjustRight(rect, x, bounds, viewWidth, snapMargin, aspectRatio, false, true);
|
||||
adjustBottomByAspectRatio(rect, aspectRatio);
|
||||
}
|
||||
break;
|
||||
case LEFT:
|
||||
adjustLeft(rect, x, bounds, snapMargin, aspectRatio, true, true);
|
||||
adjustTopBottomByAspectRatio(rect, bounds, aspectRatio);
|
||||
break;
|
||||
case TOP:
|
||||
adjustTop(rect, y, bounds, snapMargin, aspectRatio, true, true);
|
||||
adjustLeftRightByAspectRatio(rect, bounds, aspectRatio);
|
||||
break;
|
||||
case RIGHT:
|
||||
adjustRight(rect, x, bounds, viewWidth, snapMargin, aspectRatio, true, true);
|
||||
adjustTopBottomByAspectRatio(rect, bounds, aspectRatio);
|
||||
break;
|
||||
case BOTTOM:
|
||||
adjustBottom(rect, y, bounds, viewHeight, snapMargin, aspectRatio, true, true);
|
||||
adjustLeftRightByAspectRatio(rect, bounds, aspectRatio);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if edges have gone out of bounds (including snap margin), and fix if needed.
|
||||
*/
|
||||
private void snapEdgesToBounds(RectF edges, RectF bounds, float margin) {
|
||||
if (edges.left < bounds.left + margin) {
|
||||
edges.offset(bounds.left - edges.left, 0);
|
||||
}
|
||||
if (edges.top < bounds.top + margin) {
|
||||
edges.offset(0, bounds.top - edges.top);
|
||||
}
|
||||
if (edges.right > bounds.right - margin) {
|
||||
edges.offset(bounds.right - edges.right, 0);
|
||||
}
|
||||
if (edges.bottom > bounds.bottom - margin) {
|
||||
edges.offset(0, bounds.bottom - edges.bottom);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resulting x-position of the left edge of the crop window given the handle's position
|
||||
* and the image's bounding box and snap radius.
|
||||
*
|
||||
* @param left the position that the left edge is dragged to
|
||||
* @param bounds the bounding box of the image that is being cropped
|
||||
* @param snapMargin the snap distance to the image edge (in pixels)
|
||||
*/
|
||||
private void adjustLeft(
|
||||
RectF rect,
|
||||
float left,
|
||||
RectF bounds,
|
||||
float snapMargin,
|
||||
float aspectRatio,
|
||||
boolean topMoves,
|
||||
boolean bottomMoves) {
|
||||
|
||||
float newLeft = left;
|
||||
|
||||
if (newLeft < 0) {
|
||||
newLeft /= 1.05f;
|
||||
mTouchOffset.x -= newLeft / 1.1f;
|
||||
}
|
||||
|
||||
if (newLeft < bounds.left) {
|
||||
mTouchOffset.x -= (newLeft - bounds.left) / 2f;
|
||||
}
|
||||
|
||||
if (newLeft - bounds.left < snapMargin) {
|
||||
newLeft = bounds.left;
|
||||
}
|
||||
|
||||
// Checks if the window is too small horizontally
|
||||
if (rect.right - newLeft < mMinCropWidth) {
|
||||
newLeft = rect.right - mMinCropWidth;
|
||||
}
|
||||
|
||||
// Checks if the window is too large horizontally
|
||||
if (rect.right - newLeft > mMaxCropWidth) {
|
||||
newLeft = rect.right - mMaxCropWidth;
|
||||
}
|
||||
|
||||
if (newLeft - bounds.left < snapMargin) {
|
||||
newLeft = bounds.left;
|
||||
}
|
||||
|
||||
// check vertical bounds if aspect ratio is in play
|
||||
if (aspectRatio > 0) {
|
||||
float newHeight = (rect.right - newLeft) / aspectRatio;
|
||||
|
||||
// Checks if the window is too small vertically
|
||||
if (newHeight < mMinCropHeight) {
|
||||
newLeft = Math.max(bounds.left, rect.right - mMinCropHeight * aspectRatio);
|
||||
newHeight = (rect.right - newLeft) / aspectRatio;
|
||||
}
|
||||
|
||||
// Checks if the window is too large vertically
|
||||
if (newHeight > mMaxCropHeight) {
|
||||
newLeft = Math.max(bounds.left, rect.right - mMaxCropHeight * aspectRatio);
|
||||
newHeight = (rect.right - newLeft) / aspectRatio;
|
||||
}
|
||||
|
||||
// if top AND bottom edge moves by aspect ratio check that it is within full height bounds
|
||||
if (topMoves && bottomMoves) {
|
||||
newLeft =
|
||||
Math.max(newLeft, Math.max(bounds.left, rect.right - bounds.height() * aspectRatio));
|
||||
} else {
|
||||
// if top edge moves by aspect ratio check that it is within bounds
|
||||
if (topMoves && rect.bottom - newHeight < bounds.top) {
|
||||
newLeft = Math.max(bounds.left, rect.right - (rect.bottom - bounds.top) * aspectRatio);
|
||||
newHeight = (rect.right - newLeft) / aspectRatio;
|
||||
}
|
||||
|
||||
// if bottom edge moves by aspect ratio check that it is within bounds
|
||||
if (bottomMoves && rect.top + newHeight > bounds.bottom) {
|
||||
newLeft =
|
||||
Math.max(
|
||||
newLeft,
|
||||
Math.max(bounds.left, rect.right - (bounds.bottom - rect.top) * aspectRatio));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rect.left = newLeft;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resulting x-position of the right edge of the crop window given the handle's position
|
||||
* and the image's bounding box and snap radius.
|
||||
*
|
||||
* @param right the position that the right edge is dragged to
|
||||
* @param bounds the bounding box of the image that is being cropped
|
||||
* @param viewWidth
|
||||
* @param snapMargin the snap distance to the image edge (in pixels)
|
||||
*/
|
||||
private void adjustRight(
|
||||
RectF rect,
|
||||
float right,
|
||||
RectF bounds,
|
||||
int viewWidth,
|
||||
float snapMargin,
|
||||
float aspectRatio,
|
||||
boolean topMoves,
|
||||
boolean bottomMoves) {
|
||||
|
||||
float newRight = right;
|
||||
|
||||
if (newRight > viewWidth) {
|
||||
newRight = viewWidth + (newRight - viewWidth) / 1.05f;
|
||||
mTouchOffset.x -= (newRight - viewWidth) / 1.1f;
|
||||
}
|
||||
|
||||
if (newRight > bounds.right) {
|
||||
mTouchOffset.x -= (newRight - bounds.right) / 2f;
|
||||
}
|
||||
|
||||
// If close to the edge
|
||||
if (bounds.right - newRight < snapMargin) {
|
||||
newRight = bounds.right;
|
||||
}
|
||||
|
||||
// Checks if the window is too small horizontally
|
||||
if (newRight - rect.left < mMinCropWidth) {
|
||||
newRight = rect.left + mMinCropWidth;
|
||||
}
|
||||
|
||||
// Checks if the window is too large horizontally
|
||||
if (newRight - rect.left > mMaxCropWidth) {
|
||||
newRight = rect.left + mMaxCropWidth;
|
||||
}
|
||||
|
||||
// If close to the edge
|
||||
if (bounds.right - newRight < snapMargin) {
|
||||
newRight = bounds.right;
|
||||
}
|
||||
|
||||
// check vertical bounds if aspect ratio is in play
|
||||
if (aspectRatio > 0) {
|
||||
float newHeight = (newRight - rect.left) / aspectRatio;
|
||||
|
||||
// Checks if the window is too small vertically
|
||||
if (newHeight < mMinCropHeight) {
|
||||
newRight = Math.min(bounds.right, rect.left + mMinCropHeight * aspectRatio);
|
||||
newHeight = (newRight - rect.left) / aspectRatio;
|
||||
}
|
||||
|
||||
// Checks if the window is too large vertically
|
||||
if (newHeight > mMaxCropHeight) {
|
||||
newRight = Math.min(bounds.right, rect.left + mMaxCropHeight * aspectRatio);
|
||||
newHeight = (newRight - rect.left) / aspectRatio;
|
||||
}
|
||||
|
||||
// if top AND bottom edge moves by aspect ratio check that it is within full height bounds
|
||||
if (topMoves && bottomMoves) {
|
||||
newRight =
|
||||
Math.min(newRight, Math.min(bounds.right, rect.left + bounds.height() * aspectRatio));
|
||||
} else {
|
||||
// if top edge moves by aspect ratio check that it is within bounds
|
||||
if (topMoves && rect.bottom - newHeight < bounds.top) {
|
||||
newRight = Math.min(bounds.right, rect.left + (rect.bottom - bounds.top) * aspectRatio);
|
||||
newHeight = (newRight - rect.left) / aspectRatio;
|
||||
}
|
||||
|
||||
// if bottom edge moves by aspect ratio check that it is within bounds
|
||||
if (bottomMoves && rect.top + newHeight > bounds.bottom) {
|
||||
newRight =
|
||||
Math.min(
|
||||
newRight,
|
||||
Math.min(bounds.right, rect.left + (bounds.bottom - rect.top) * aspectRatio));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rect.right = newRight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resulting y-position of the top edge of the crop window given the handle's position and
|
||||
* the image's bounding box and snap radius.
|
||||
*
|
||||
* @param top the x-position that the top edge is dragged to
|
||||
* @param bounds the bounding box of the image that is being cropped
|
||||
* @param snapMargin the snap distance to the image edge (in pixels)
|
||||
*/
|
||||
private void adjustTop(
|
||||
RectF rect,
|
||||
float top,
|
||||
RectF bounds,
|
||||
float snapMargin,
|
||||
float aspectRatio,
|
||||
boolean leftMoves,
|
||||
boolean rightMoves) {
|
||||
|
||||
float newTop = top;
|
||||
|
||||
if (newTop < 0) {
|
||||
newTop /= 1.05f;
|
||||
mTouchOffset.y -= newTop / 1.1f;
|
||||
}
|
||||
|
||||
if (newTop < bounds.top) {
|
||||
mTouchOffset.y -= (newTop - bounds.top) / 2f;
|
||||
}
|
||||
|
||||
if (newTop - bounds.top < snapMargin) {
|
||||
newTop = bounds.top;
|
||||
}
|
||||
|
||||
// Checks if the window is too small vertically
|
||||
if (rect.bottom - newTop < mMinCropHeight) {
|
||||
newTop = rect.bottom - mMinCropHeight;
|
||||
}
|
||||
|
||||
// Checks if the window is too large vertically
|
||||
if (rect.bottom - newTop > mMaxCropHeight) {
|
||||
newTop = rect.bottom - mMaxCropHeight;
|
||||
}
|
||||
|
||||
if (newTop - bounds.top < snapMargin) {
|
||||
newTop = bounds.top;
|
||||
}
|
||||
|
||||
// check horizontal bounds if aspect ratio is in play
|
||||
if (aspectRatio > 0) {
|
||||
float newWidth = (rect.bottom - newTop) * aspectRatio;
|
||||
|
||||
// Checks if the crop window is too small horizontally due to aspect ratio adjustment
|
||||
if (newWidth < mMinCropWidth) {
|
||||
newTop = Math.max(bounds.top, rect.bottom - (mMinCropWidth / aspectRatio));
|
||||
newWidth = (rect.bottom - newTop) * aspectRatio;
|
||||
}
|
||||
|
||||
// Checks if the crop window is too large horizontally due to aspect ratio adjustment
|
||||
if (newWidth > mMaxCropWidth) {
|
||||
newTop = Math.max(bounds.top, rect.bottom - (mMaxCropWidth / aspectRatio));
|
||||
newWidth = (rect.bottom - newTop) * aspectRatio;
|
||||
}
|
||||
|
||||
// if left AND right edge moves by aspect ratio check that it is within full width bounds
|
||||
if (leftMoves && rightMoves) {
|
||||
newTop = Math.max(newTop, Math.max(bounds.top, rect.bottom - bounds.width() / aspectRatio));
|
||||
} else {
|
||||
// if left edge moves by aspect ratio check that it is within bounds
|
||||
if (leftMoves && rect.right - newWidth < bounds.left) {
|
||||
newTop = Math.max(bounds.top, rect.bottom - (rect.right - bounds.left) / aspectRatio);
|
||||
newWidth = (rect.bottom - newTop) * aspectRatio;
|
||||
}
|
||||
|
||||
// if right edge moves by aspect ratio check that it is within bounds
|
||||
if (rightMoves && rect.left + newWidth > bounds.right) {
|
||||
newTop =
|
||||
Math.max(
|
||||
newTop,
|
||||
Math.max(bounds.top, rect.bottom - (bounds.right - rect.left) / aspectRatio));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rect.top = newTop;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the resulting y-position of the bottom edge of the crop window given the handle's position
|
||||
* and the image's bounding box and snap radius.
|
||||
*
|
||||
* @param bottom the position that the bottom edge is dragged to
|
||||
* @param bounds the bounding box of the image that is being cropped
|
||||
* @param viewHeight
|
||||
* @param snapMargin the snap distance to the image edge (in pixels)
|
||||
*/
|
||||
private void adjustBottom(
|
||||
RectF rect,
|
||||
float bottom,
|
||||
RectF bounds,
|
||||
int viewHeight,
|
||||
float snapMargin,
|
||||
float aspectRatio,
|
||||
boolean leftMoves,
|
||||
boolean rightMoves) {
|
||||
|
||||
float newBottom = bottom;
|
||||
|
||||
if (newBottom > viewHeight) {
|
||||
newBottom = viewHeight + (newBottom - viewHeight) / 1.05f;
|
||||
mTouchOffset.y -= (newBottom - viewHeight) / 1.1f;
|
||||
}
|
||||
|
||||
if (newBottom > bounds.bottom) {
|
||||
mTouchOffset.y -= (newBottom - bounds.bottom) / 2f;
|
||||
}
|
||||
|
||||
if (bounds.bottom - newBottom < snapMargin) {
|
||||
newBottom = bounds.bottom;
|
||||
}
|
||||
|
||||
// Checks if the window is too small vertically
|
||||
if (newBottom - rect.top < mMinCropHeight) {
|
||||
newBottom = rect.top + mMinCropHeight;
|
||||
}
|
||||
|
||||
// Checks if the window is too small vertically
|
||||
if (newBottom - rect.top > mMaxCropHeight) {
|
||||
newBottom = rect.top + mMaxCropHeight;
|
||||
}
|
||||
|
||||
if (bounds.bottom - newBottom < snapMargin) {
|
||||
newBottom = bounds.bottom;
|
||||
}
|
||||
|
||||
// check horizontal bounds if aspect ratio is in play
|
||||
if (aspectRatio > 0) {
|
||||
float newWidth = (newBottom - rect.top) * aspectRatio;
|
||||
|
||||
// Checks if the window is too small horizontally
|
||||
if (newWidth < mMinCropWidth) {
|
||||
newBottom = Math.min(bounds.bottom, rect.top + mMinCropWidth / aspectRatio);
|
||||
newWidth = (newBottom - rect.top) * aspectRatio;
|
||||
}
|
||||
|
||||
// Checks if the window is too large horizontally
|
||||
if (newWidth > mMaxCropWidth) {
|
||||
newBottom = Math.min(bounds.bottom, rect.top + mMaxCropWidth / aspectRatio);
|
||||
newWidth = (newBottom - rect.top) * aspectRatio;
|
||||
}
|
||||
|
||||
// if left AND right edge moves by aspect ratio check that it is within full width bounds
|
||||
if (leftMoves && rightMoves) {
|
||||
newBottom =
|
||||
Math.min(newBottom, Math.min(bounds.bottom, rect.top + bounds.width() / aspectRatio));
|
||||
} else {
|
||||
// if left edge moves by aspect ratio check that it is within bounds
|
||||
if (leftMoves && rect.right - newWidth < bounds.left) {
|
||||
newBottom = Math.min(bounds.bottom, rect.top + (rect.right - bounds.left) / aspectRatio);
|
||||
newWidth = (newBottom - rect.top) * aspectRatio;
|
||||
}
|
||||
|
||||
// if right edge moves by aspect ratio check that it is within bounds
|
||||
if (rightMoves && rect.left + newWidth > bounds.right) {
|
||||
newBottom =
|
||||
Math.min(
|
||||
newBottom,
|
||||
Math.min(bounds.bottom, rect.top + (bounds.right - rect.left) / aspectRatio));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rect.bottom = newBottom;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust left edge by current crop window height and the given aspect ratio, the right edge
|
||||
* remains in possition while the left adjusts to keep aspect ratio to the height.
|
||||
*/
|
||||
private void adjustLeftByAspectRatio(RectF rect, float aspectRatio) {
|
||||
rect.left = rect.right - rect.height() * aspectRatio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust top edge by current crop window width and the given aspect ratio, the bottom edge
|
||||
* remains in possition while the top adjusts to keep aspect ratio to the width.
|
||||
*/
|
||||
private void adjustTopByAspectRatio(RectF rect, float aspectRatio) {
|
||||
rect.top = rect.bottom - rect.width() / aspectRatio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust right edge by current crop window height and the given aspect ratio, the left edge
|
||||
* remains in possition while the left adjusts to keep aspect ratio to the height.
|
||||
*/
|
||||
private void adjustRightByAspectRatio(RectF rect, float aspectRatio) {
|
||||
rect.right = rect.left + rect.height() * aspectRatio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust bottom edge by current crop window width and the given aspect ratio, the top edge
|
||||
* remains in possition while the top adjusts to keep aspect ratio to the width.
|
||||
*/
|
||||
private void adjustBottomByAspectRatio(RectF rect, float aspectRatio) {
|
||||
rect.bottom = rect.top + rect.width() / aspectRatio;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust left and right edges by current crop window height and the given aspect ratio, both
|
||||
* right and left edges adjusts equally relative to center to keep aspect ratio to the height.
|
||||
*/
|
||||
private void adjustLeftRightByAspectRatio(RectF rect, RectF bounds, float aspectRatio) {
|
||||
rect.inset((rect.width() - rect.height() * aspectRatio) / 2, 0);
|
||||
if (rect.left < bounds.left) {
|
||||
rect.offset(bounds.left - rect.left, 0);
|
||||
}
|
||||
if (rect.right > bounds.right) {
|
||||
rect.offset(bounds.right - rect.right, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adjust top and bottom edges by current crop window width and the given aspect ratio, both top
|
||||
* and bottom edges adjusts equally relative to center to keep aspect ratio to the width.
|
||||
*/
|
||||
private void adjustTopBottomByAspectRatio(RectF rect, RectF bounds, float aspectRatio) {
|
||||
rect.inset(0, (rect.height() - rect.width() / aspectRatio) / 2);
|
||||
if (rect.top < bounds.top) {
|
||||
rect.offset(0, bounds.top - rect.top);
|
||||
}
|
||||
if (rect.bottom > bounds.bottom) {
|
||||
rect.offset(0, bounds.bottom - rect.bottom);
|
||||
}
|
||||
}
|
||||
// endregion
|
||||
|
||||
// region: Inner class: Type
|
||||
|
||||
/**
|
||||
* The type of crop window move that is handled.
|
||||
*/
|
||||
public enum Type {
|
||||
TOP_LEFT,
|
||||
TOP_RIGHT,
|
||||
BOTTOM_LEFT,
|
||||
BOTTOM_RIGHT,
|
||||
LEFT,
|
||||
TOP,
|
||||
RIGHT,
|
||||
BOTTOM,
|
||||
CENTER
|
||||
}
|
||||
// endregion
|
||||
}
|
Before Width: | Height: | Size: 262 B |
Before Width: | Height: | Size: 634 B |
Before Width: | Height: | Size: 617 B |
Before Width: | Height: | Size: 259 B |
Before Width: | Height: | Size: 798 B |
Before Width: | Height: | Size: 787 B |
Before Width: | Height: | Size: 417 B |