diff --git a/.gitignore b/.gitignore index 6de4017f..18d1198a 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,5 @@ local.properties /cropper/build/ /build/ +/app/fdroid/release/ +/app/playstore/release/ diff --git a/app/build.gradle b/app/build.gradle index 66f6b632..dc9ea879 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,8 +9,8 @@ android { defaultConfig { minSdk 21 targetSdk 31 - versionCode 395 - versionName "3.0.5" + versionCode 396 + versionName "3.0.6" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } flavorDimensions "default" diff --git a/app/src/fdroid/res/xml/file_paths.xml b/app/src/fdroid/res/xml/file_paths.xml index 1d2a9d9f..758bbce9 100644 --- a/app/src/fdroid/res/xml/file_paths.xml +++ b/app/src/fdroid/res/xml/file_paths.xml @@ -2,7 +2,7 @@ + path="Android/data/fr.gouv.etalab.mastodon/files/Pictures" /> + { - Fragment fragment = getSupportFragmentManager().findFragmentByTag("f" + binding.viewPager.getCurrentItem()); - if (fragment instanceof FragmentMastodonTimeline && fragment.isVisible()) { - FragmentMastodonTimeline fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment); - fragmentMastodonTimeline.refreshAllAdapters(); + if (binding.viewPager.getAdapter() != null) { + Fragment fragment = (Fragment) binding.viewPager.getAdapter().instantiateItem(binding.viewPager, binding.tabLayout.getSelectedTabPosition()); + if (fragment instanceof FragmentMastodonTimeline && fragment.isVisible()) { + FragmentMastodonTimeline fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment); + fragmentMastodonTimeline.refreshAllAdapters(); + } } }); String finalShow_filtered = show_filtered; @@ -990,13 +996,15 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt } public void refreshFragment() { - Fragment fragment = getSupportFragmentManager().findFragmentByTag("f" + binding.viewPager.getCurrentItem()); - if (fragment instanceof FragmentNotificationContainer) { - FragmentTransaction fragTransaction = getSupportFragmentManager().beginTransaction(); - fragTransaction.detach(fragment).commit(); - FragmentTransaction fragTransaction2 = getSupportFragmentManager().beginTransaction(); - fragTransaction2.attach(fragment); - fragTransaction2.commit(); + if (binding.viewPager.getAdapter() != null) { + Fragment fragment = (Fragment) binding.viewPager.getAdapter().instantiateItem(binding.viewPager, binding.tabLayout.getSelectedTabPosition()); + if (fragment instanceof FragmentNotificationContainer) { + FragmentTransaction fragTransaction = getSupportFragmentManager().beginTransaction(); + fragTransaction.detach(fragment).commit(); + FragmentTransaction fragTransaction2 = getSupportFragmentManager().beginTransaction(); + fragTransaction2.attach(fragment); + fragTransaction2.commit(); + } } } @@ -1035,16 +1043,18 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt */ private void scrollToTop() { - Fragment fragment = getSupportFragmentManager().findFragmentByTag("f" + binding.viewPager.getCurrentItem()); - if (fragment instanceof FragmentMastodonTimeline) { - FragmentMastodonTimeline fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment); - fragmentMastodonTimeline.scrollToTop(); - } else if (fragment instanceof FragmentMastodonConversation) { - FragmentMastodonConversation fragmentMastodonConversation = ((FragmentMastodonConversation) fragment); - fragmentMastodonConversation.scrollToTop(); - } else if (fragment instanceof FragmentNotificationContainer) { - FragmentNotificationContainer fragmentNotificationContainer = ((FragmentNotificationContainer) fragment); - fragmentNotificationContainer.scrollToTop(); + if (binding.viewPager.getAdapter() != null) { + Fragment fragment = (Fragment) binding.viewPager.getAdapter().instantiateItem(binding.viewPager, binding.tabLayout.getSelectedTabPosition()); + if (fragment instanceof FragmentMastodonTimeline) { + FragmentMastodonTimeline fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment); + fragmentMastodonTimeline.scrollToTop(); + } else if (fragment instanceof FragmentMastodonConversation) { + FragmentMastodonConversation fragmentMastodonConversation = ((FragmentMastodonConversation) fragment); + fragmentMastodonConversation.scrollToTop(); + } else if (fragment instanceof FragmentNotificationContainer) { + FragmentNotificationContainer fragmentNotificationContainer = ((FragmentNotificationContainer) fragment); + fragmentNotificationContainer.scrollToTop(); + } } } diff --git a/app/src/main/java/app/fedilab/android/MainApplication.java b/app/src/main/java/app/fedilab/android/MainApplication.java index f880a27e..a2d40e7d 100644 --- a/app/src/main/java/app/fedilab/android/MainApplication.java +++ b/app/src/main/java/app/fedilab/android/MainApplication.java @@ -75,7 +75,7 @@ public class MainApplication extends MultiDexApplication { super.attachBaseContext(base); MultiDex.install(MainApplication.this); SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(MainApplication.this); - boolean send_crash_reports = sharedpreferences.getBoolean(getString(R.string.SET_SEND_CRASH_REPORTS), true); + boolean send_crash_reports = sharedpreferences.getBoolean(getString(R.string.SET_SEND_CRASH_REPORTS), false); if (send_crash_reports) { ACRA.init(this, new CoreConfigurationBuilder() //core configuration: diff --git a/app/src/main/java/app/fedilab/android/activities/ComposeActivity.java b/app/src/main/java/app/fedilab/android/activities/ComposeActivity.java index 0ddda757..58c28c85 100644 --- a/app/src/main/java/app/fedilab/android/activities/ComposeActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/ComposeActivity.java @@ -109,12 +109,17 @@ public class ComposeActivity extends BaseActivity implements ComposeAdapter.Mana @Override public void onReceive(android.content.Context context, Intent intent) { String imgpath = intent.getStringExtra("imgpath"); + float focusX = intent.getFloatExtra("focusX", -2); + float focusY = intent.getFloatExtra("focusY", -2); if (imgpath != null) { int position = 0; for (Status status : statusList) { if (status.media_attachments != null && status.media_attachments.size() > 0) { for (Attachment attachment : status.media_attachments) { if (attachment.local_path.equalsIgnoreCase(imgpath)) { + if (focusX != -2) { + attachment.focus = focusX + "," + focusY; + } composeAdapter.notifyItemChanged(position); break; } diff --git a/app/src/main/java/app/fedilab/android/activities/ContextActivity.java b/app/src/main/java/app/fedilab/android/activities/ContextActivity.java index 51fd8f32..b763e87f 100644 --- a/app/src/main/java/app/fedilab/android/activities/ContextActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/ContextActivity.java @@ -82,7 +82,7 @@ public class ContextActivity extends BaseActivity { focusedStatus = null; // or other values if (b != null) focusedStatus = (Status) b.getSerializable(Helper.ARG_STATUS); - if (focusedStatus == null) { + if (focusedStatus == null && currentAccount == null || currentAccount.mastodon_account == null) { finish(); return; } diff --git a/app/src/main/java/app/fedilab/android/activities/MediaActivity.java b/app/src/main/java/app/fedilab/android/activities/MediaActivity.java index baeb5a82..ca275b43 100644 --- a/app/src/main/java/app/fedilab/android/activities/MediaActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/MediaActivity.java @@ -33,15 +33,18 @@ import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.view.Window; import androidx.annotation.NonNull; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.viewpager2.adapter.FragmentStateAdapter; -import androidx.viewpager2.widget.ViewPager2; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; +import androidx.viewpager.widget.ViewPager; + +import org.jetbrains.annotations.NotNull; import java.util.ArrayList; @@ -94,6 +97,7 @@ public class MediaActivity extends BaseActivity implements OnDownloadInterface { private float startX; private float startY; private ActivityMediaPagerBinding binding; + private FragmentMedia mCurrentFragment; @Override protected void onCreate(Bundle savedInstanceState) { @@ -123,7 +127,7 @@ public class MediaActivity extends BaseActivity implements OnDownloadInterface { setTitle(""); - ScreenSlidePagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(MediaActivity.this); + ScreenSlidePagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager()); binding.mediaViewpager.setAdapter(mPagerAdapter); binding.mediaViewpager.setSaveEnabled(false); binding.mediaViewpager.setCurrentItem(mediaPosition - 1); @@ -135,15 +139,14 @@ public class MediaActivity extends BaseActivity implements OnDownloadInterface { binding.mediaDescription.setText(description); } - binding.mediaViewpager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { - @Override + binding.mediaViewpager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + public void onPageScrollStateChanged(int state) { + } + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - super.onPageScrolled(position, positionOffset, positionOffsetPixels); } - @Override public void onPageSelected(int position) { - super.onPageSelected(position); String description = attachments.get(position).description; if (handler != null) { handler.removeCallbacksAndMessages(null); @@ -153,13 +156,9 @@ public class MediaActivity extends BaseActivity implements OnDownloadInterface { binding.mediaDescription.setText(description); } } - - @Override - public void onPageScrollStateChanged(int state) { - super.onPageScrollStateChanged(state); - } }); + setFullscreen(true); Display display = getWindowManager().getDefaultDisplay(); Point size = new Point(); @@ -358,18 +357,22 @@ public class MediaActivity extends BaseActivity implements OnDownloadInterface { | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); } + public FragmentMedia getCurrentFragment() { + return mCurrentFragment; + } + /** * Media Pager */ - private class ScreenSlidePagerAdapter extends FragmentStateAdapter { - - ScreenSlidePagerAdapter(FragmentActivity fa) { - super(fa); + @SuppressWarnings("deprecation") + private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter { + ScreenSlidePagerAdapter(FragmentManager fm) { + super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); } - @NonNull + @NotNull @Override - public Fragment createFragment(int position) { + public Fragment getItem(int position) { Bundle bundle = new Bundle(); FragmentMedia mediaSliderFragment = new FragmentMedia(); bundle.putInt(Helper.ARG_MEDIA_POSITION, position); @@ -379,7 +382,15 @@ public class MediaActivity extends BaseActivity implements OnDownloadInterface { } @Override - public int getItemCount() { + public void setPrimaryItem(@NotNull ViewGroup container, int position, @NotNull Object object) { + if (getCurrentFragment() != object) { + mCurrentFragment = ((FragmentMedia) object); + } + super.setPrimaryItem(container, position, object); + } + + @Override + public int getCount() { return attachments.size(); } } diff --git a/app/src/main/java/app/fedilab/android/activities/ProfileActivity.java b/app/src/main/java/app/fedilab/android/activities/ProfileActivity.java index e8d8fc52..1226c83d 100644 --- a/app/src/main/java/app/fedilab/android/activities/ProfileActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/ProfileActivity.java @@ -32,7 +32,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.text.Html; -import android.text.Spannable; import android.text.SpannableString; import android.text.Spanned; import android.text.method.LinkMovementMethod; @@ -63,7 +62,7 @@ import androidx.recyclerview.widget.RecyclerView; import com.bumptech.glide.Glide; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; -import com.google.android.material.tabs.TabLayoutMediator; +import com.google.android.material.tabs.TabLayout; import java.util.ArrayList; import java.util.Date; @@ -94,6 +93,7 @@ import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; import app.fedilab.android.helper.SpannableHelper; import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.drawer.FieldAdapter; import app.fedilab.android.ui.drawer.IdentityProofsAdapter; import app.fedilab.android.ui.pageadapter.FedilabProfileTLPageAdapter; import app.fedilab.android.viewmodel.mastodon.AccountsVM; @@ -247,29 +247,27 @@ public class ProfileActivity extends BaseActivity { binding.accountTabLayout.clearOnTabSelectedListeners(); binding.accountTabLayout.removeAllTabs(); //Tablayout for timelines/following/followers - FedilabProfileTLPageAdapter fedilabProfileTLPageAdapter = new FedilabProfileTLPageAdapter(ProfileActivity.this, account); - binding.accountTabLayout.addTab(binding.accountTabLayout.newTab()); - binding.accountTabLayout.addTab(binding.accountTabLayout.newTab()); - binding.accountTabLayout.addTab(binding.accountTabLayout.newTab()); + FedilabProfileTLPageAdapter fedilabProfileTLPageAdapter = new FedilabProfileTLPageAdapter(getSupportFragmentManager(), account); + binding.accountTabLayout.addTab(binding.accountTabLayout.newTab().setText(getString(R.string.status_cnt, Helper.withSuffix(account.statuses_count)))); + binding.accountTabLayout.addTab(binding.accountTabLayout.newTab().setText(getString(R.string.following_cnt, Helper.withSuffix(account.following_count)))); + binding.accountTabLayout.addTab(binding.accountTabLayout.newTab().setText(getString(R.string.followers_cnt, Helper.withSuffix(account.followers_count)))); binding.accountViewpager.setAdapter(fedilabProfileTLPageAdapter); binding.accountViewpager.setOffscreenPageLimit(3); + binding.accountViewpager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(binding.accountTabLayout)); + binding.accountTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + binding.accountViewpager.setCurrentItem(tab.getPosition()); + } - new TabLayoutMediator(binding.accountTabLayout, binding.accountViewpager, - (tab, position) -> { - binding.accountViewpager.setCurrentItem(tab.getPosition(), true); - switch (position) { - case 0: - tab.setText(getString(R.string.status_cnt, Helper.withSuffix(account.statuses_count))); - break; - case 1: - tab.setText(getString(R.string.following_cnt, Helper.withSuffix(account.following_count))); - break; - case 2: - tab.setText(getString(R.string.followers_cnt, Helper.withSuffix(account.followers_count))); - break; - } - } - ).attach(); + @Override + public void onTabUnselected(TabLayout.Tab tab) { + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + } + }); binding.accountTabLayout.setTabTextColors(ThemeHelper.getAttColor(ProfileActivity.this, R.attr.mTextColor), ContextCompat.getColor(ProfileActivity.this, R.color.cyanea_accent_dark_reference)); binding.accountTabLayout.setTabIconTint(ThemeHelper.getColorStateList(ProfileActivity.this)); boolean disableGif = sharedpreferences.getBoolean(getString(R.string.SET_DISABLE_GIF), false); @@ -357,43 +355,9 @@ public class ProfileActivity extends BaseActivity { //Fields for profile List fields = account.fields; if (fields != null && fields.size() > 0) { - for (int i = 0; i < fields.size(); i++) { - LinearLayout field; - TextView labelView; - TextView valueView; - switch (i) { - case 1: - field = binding.field1; - labelView = binding.label1; - valueView = binding.value1; - break; - case 2: - field = binding.field2; - labelView = binding.label2; - valueView = binding.value2; - break; - case 3: - field = binding.field3; - labelView = binding.label3; - valueView = binding.value3; - break; - default: - field = binding.field4; - labelView = binding.label4; - valueView = binding.value4; - break; - } - - field.setVisibility(View.VISIBLE); - if (fields.get(i).verified_at != null) { - valueView.setCompoundDrawablesWithIntrinsicBounds(null, null, ContextCompat.getDrawable(ProfileActivity.this, R.drawable.ic_baseline_verified_24), null); - fields.get(i).value_span.setSpan(new ForegroundColorSpan(ContextCompat.getColor(ProfileActivity.this, R.color.verified_text)), 0, fields.get(i).value_span.toString().length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - valueView.setText(fields.get(i).value_span != null ? fields.get(i).value_span : fields.get(i).value, TextView.BufferType.SPANNABLE); - valueView.setMovementMethod(LinkMovementMethod.getInstance()); - labelView.setText(fields.get(i).name); - } - binding.fieldsContainer.setVisibility(View.VISIBLE); + FieldAdapter fieldAdapter = new FieldAdapter(fields); + binding.fieldsContainer.setAdapter(fieldAdapter); + binding.fieldsContainer.setLayoutManager(new LinearLayoutManager(ProfileActivity.this)); } if (account.span_display_name == null && account.display_name == null) { binding.accountDn.setText(account.username); diff --git a/app/src/main/java/app/fedilab/android/activities/ScheduledActivity.java b/app/src/main/java/app/fedilab/android/activities/ScheduledActivity.java index da261e98..f01a9e1a 100644 --- a/app/src/main/java/app/fedilab/android/activities/ScheduledActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/ScheduledActivity.java @@ -24,7 +24,7 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.ActionBar; import androidx.core.content.ContextCompat; -import com.google.android.material.tabs.TabLayoutMediator; +import com.google.android.material.tabs.TabLayout; import app.fedilab.android.R; import app.fedilab.android.databinding.ActivityScheduledBinding; @@ -56,31 +56,30 @@ public class ScheduledActivity extends BaseActivity { MastodonHelper.loadPPMastodon(binding.profilePicture, currentAccount.mastodon_account); binding.title.setText(R.string.scheduled); - binding.scheduleTablayout.addTab(binding.scheduleTablayout.newTab()); - binding.scheduleTablayout.addTab(binding.scheduleTablayout.newTab()); - binding.scheduleTablayout.addTab(binding.scheduleTablayout.newTab()); + binding.scheduleTablayout.addTab(binding.scheduleTablayout.newTab().setText(getString(R.string.toots_server))); + binding.scheduleTablayout.addTab(binding.scheduleTablayout.newTab().setText(getString(R.string.toots_client))); + binding.scheduleTablayout.addTab(binding.scheduleTablayout.newTab().setText(getString(R.string.reblog))); - binding.scheduleViewpager.setAdapter(new FedilabScheduledPageAdapter(ScheduledActivity.this)); + binding.scheduleViewpager.setAdapter(new FedilabScheduledPageAdapter(getSupportFragmentManager())); binding.scheduleViewpager.setOffscreenPageLimit(3); + binding.scheduleViewpager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(binding.scheduleTablayout)); binding.scheduleTablayout.setTabTextColors(ThemeHelper.getAttColor(ScheduledActivity.this, R.attr.mTextColor), ContextCompat.getColor(ScheduledActivity.this, R.color.cyanea_accent_dark_reference)); binding.scheduleTablayout.setTabIconTint(ThemeHelper.getColorStateList(ScheduledActivity.this)); - new TabLayoutMediator(binding.scheduleTablayout, binding.scheduleViewpager, - (tab, position) -> { - binding.scheduleViewpager.setCurrentItem(tab.getPosition(), true); - switch (position) { - case 0: - tab.setText(getString(R.string.toots_server)); - break; - case 1: - tab.setText(getString(R.string.toots_client)); - break; - case 2: - tab.setText(getString(R.string.reblog)); - break; - } - } - ).attach(); + binding.scheduleTablayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + binding.scheduleViewpager.setCurrentItem(tab.getPosition()); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + } + }); } @Override diff --git a/app/src/main/java/app/fedilab/android/activities/SearchResultTabActivity.java b/app/src/main/java/app/fedilab/android/activities/SearchResultTabActivity.java index 04195b35..7fee806a 100644 --- a/app/src/main/java/app/fedilab/android/activities/SearchResultTabActivity.java +++ b/app/src/main/java/app/fedilab/android/activities/SearchResultTabActivity.java @@ -21,6 +21,7 @@ import android.os.Bundle; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; +import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.Toast; @@ -28,11 +29,12 @@ import androidx.annotation.NonNull; import androidx.appcompat.widget.SearchView; import androidx.core.content.ContextCompat; import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; import com.google.android.material.tabs.TabLayout; -import com.google.android.material.tabs.TabLayoutMediator; import org.jetbrains.annotations.NotNull; @@ -75,35 +77,12 @@ public class SearchResultTabActivity extends BaseActivity { getSupportActionBar().setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.cyanea_primary))); } setTitle(search); - binding.searchTabLayout.addTab(binding.searchTabLayout.newTab()); - binding.searchTabLayout.addTab(binding.searchTabLayout.newTab()); - binding.searchTabLayout.addTab(binding.searchTabLayout.newTab()); - binding.searchTabLayout.addTab(binding.searchTabLayout.newTab()); + binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.tags))); + binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.accounts))); + binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.toots))); + binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.action_cache))); binding.searchTabLayout.setTabTextColors(ThemeHelper.getAttColor(SearchResultTabActivity.this, R.attr.mTextColor), ContextCompat.getColor(SearchResultTabActivity.this, R.color.cyanea_accent_dark_reference)); binding.searchTabLayout.setTabIconTint(ThemeHelper.getColorStateList(SearchResultTabActivity.this)); - ScreenSlidePagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(SearchResultTabActivity.this); - binding.searchViewpager.setAdapter(mPagerAdapter); - binding.searchViewpager.setSaveEnabled(false); - binding.searchViewpager.setOffscreenPageLimit(3); - new TabLayoutMediator(binding.searchTabLayout, binding.searchViewpager, - (tab, position) -> { - binding.searchViewpager.setCurrentItem(tab.getPosition(), true); - switch (position) { - case 0: - tab.setText(getString(R.string.tags)); - break; - case 1: - tab.setText(getString(R.string.accounts)); - break; - case 2: - tab.setText(getString(R.string.toots)); - break; - case 3: - tab.setText(getString(R.string.action_cache)); - break; - } - } - ).attach(); binding.searchTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { @Override public void onTabSelected(TabLayout.Tab tab) { @@ -112,21 +91,22 @@ public class SearchResultTabActivity extends BaseActivity { @Override public void onTabUnselected(TabLayout.Tab tab) { - } - @Override public void onTabReselected(TabLayout.Tab tab) { - Fragment fragment = getSupportFragmentManager().findFragmentByTag("f" + binding.searchViewpager.getCurrentItem()); - if (fragment instanceof FragmentMastodonAccount) { - FragmentMastodonAccount fragmentMastodonAccount = ((FragmentMastodonAccount) fragment); - fragmentMastodonAccount.scrollToTop(); - } else if (fragment instanceof FragmentMastodonTimeline) { - FragmentMastodonTimeline fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment); - fragmentMastodonTimeline.scrollToTop(); - } else if (fragment instanceof FragmentMastodonTag) { - FragmentMastodonTag fragmentMastodonTag = ((FragmentMastodonTag) fragment); - fragmentMastodonTag.scrollToTop(); + Fragment fragment; + if (binding.searchViewpager.getAdapter() != null) { + fragment = (Fragment) binding.searchViewpager.getAdapter().instantiateItem(binding.searchViewpager, tab.getPosition()); + if (fragment instanceof FragmentMastodonAccount) { + FragmentMastodonAccount fragmentMastodonAccount = ((FragmentMastodonAccount) fragment); + fragmentMastodonAccount.scrollToTop(); + } else if (fragment instanceof FragmentMastodonTimeline) { + FragmentMastodonTimeline fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment); + fragmentMastodonTimeline.scrollToTop(); + } else if (fragment instanceof FragmentMastodonTag) { + FragmentMastodonTag fragmentMastodonTag = ((FragmentMastodonTag) fragment); + fragmentMastodonTag.scrollToTop(); + } } } }); @@ -149,7 +129,7 @@ public class SearchResultTabActivity extends BaseActivity { imm.hideSoftInputFromWindow(binding.searchTabLayout.getWindowToken(), 0); query = query.replaceAll("^#+", ""); search = query.trim(); - ScreenSlidePagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(SearchResultTabActivity.this); + ScreenSlidePagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager()); binding.searchViewpager.setAdapter(mPagerAdapter); searchView.clearFocus(); setTitle(search); @@ -172,7 +152,24 @@ public class SearchResultTabActivity extends BaseActivity { searchView.setIconified(false); }); + PagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager()); + binding.searchViewpager.setAdapter(mPagerAdapter); + binding.searchViewpager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + } + + @Override + public void onPageSelected(int position) { + TabLayout.Tab tab = binding.searchTabLayout.getTabAt(position); + if (tab != null) + tab.select(); + } + @Override + public void onPageScrollStateChanged(int state) { + } + }); return true; } @@ -193,16 +190,15 @@ public class SearchResultTabActivity extends BaseActivity { /** * Pager adapter for the 4 fragments */ - private class ScreenSlidePagerAdapter extends FragmentStateAdapter { - - ScreenSlidePagerAdapter(FragmentActivity fa) { - super(fa); + @SuppressWarnings("deprecation") + private class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter { + ScreenSlidePagerAdapter(FragmentManager fm) { + super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); } - - @NonNull + @NotNull @Override - public Fragment createFragment(int position) { + public Fragment getItem(int position) { Bundle bundle = new Bundle(); switch (position) { case 0: @@ -229,7 +225,11 @@ public class SearchResultTabActivity extends BaseActivity { } @Override - public int getItemCount() { + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + } + + @Override + public int getCount() { return 4; } } diff --git a/app/src/main/java/app/fedilab/android/activities/TrendsActivity.java b/app/src/main/java/app/fedilab/android/activities/TrendsActivity.java new file mode 100644 index 00000000..ec520ce0 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/activities/TrendsActivity.java @@ -0,0 +1,156 @@ +package app.fedilab.android.activities; +/* 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 . */ + +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.view.MenuItem; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; + +import com.google.android.material.tabs.TabLayout; + +import org.jetbrains.annotations.NotNull; + +import app.fedilab.android.R; +import app.fedilab.android.client.entities.app.Timeline; +import app.fedilab.android.databinding.ActivityTrendsBinding; +import app.fedilab.android.helper.Helper; +import app.fedilab.android.helper.ThemeHelper; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTag; +import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; + + +public class TrendsActivity extends BaseActivity { + + + private ActivityTrendsBinding binding; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ThemeHelper.applyThemeBar(this); + binding = ActivityTrendsBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); + + + if (getSupportActionBar() != null) { + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + getSupportActionBar().setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.cyanea_primary))); + } + + binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.tags))); + binding.searchTabLayout.addTab(binding.searchTabLayout.newTab().setText(getString(R.string.toots))); + binding.searchTabLayout.setTabTextColors(ThemeHelper.getAttColor(TrendsActivity.this, R.attr.mTextColor), ContextCompat.getColor(TrendsActivity.this, R.color.cyanea_accent_dark_reference)); + binding.searchTabLayout.setTabIconTint(ThemeHelper.getColorStateList(TrendsActivity.this)); + binding.searchTabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + binding.trendsViewpager.setCurrentItem(tab.getPosition()); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + } + + @Override + public void onTabReselected(TabLayout.Tab tab) { + Fragment fragment; + if (binding.trendsViewpager.getAdapter() != null) { + fragment = (Fragment) binding.trendsViewpager.getAdapter().instantiateItem(binding.trendsViewpager, tab.getPosition()); + if (fragment instanceof FragmentMastodonTimeline) { + FragmentMastodonTimeline fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment); + fragmentMastodonTimeline.scrollToTop(); + } else if (fragment instanceof FragmentMastodonTag) { + FragmentMastodonTag fragmentMastodonTag = ((FragmentMastodonTag) fragment); + fragmentMastodonTag.scrollToTop(); + } + } + } + }); + + PagerAdapter mPagerAdapter = new ScreenSlidePagerAdapter(getSupportFragmentManager()); + binding.trendsViewpager.setAdapter(mPagerAdapter); + binding.trendsViewpager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + } + + @Override + public void onPageSelected(int position) { + TabLayout.Tab tab = binding.searchTabLayout.getTabAt(position); + if (tab != null) + tab.select(); + } + + @Override + public void onPageScrollStateChanged(int state) { + } + }); + } + + + @Override + public boolean onOptionsItemSelected(@NotNull MenuItem item) { + if (item.getItemId() == android.R.id.home) { + finish(); + return true; + } + return super.onOptionsItemSelected(item); + } + + + /** + * Pager adapter for the 4 fragments + */ + @SuppressWarnings("deprecation") + private static class ScreenSlidePagerAdapter extends FragmentStatePagerAdapter { + ScreenSlidePagerAdapter(FragmentManager fm) { + super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + } + + @NotNull + @Override + public Fragment getItem(int position) { + Bundle bundle = new Bundle(); + if (position == 0) { + FragmentMastodonTag fragmentMastodonTag = new FragmentMastodonTag(); + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.TREND_TAG); + fragmentMastodonTag.setArguments(bundle); + return fragmentMastodonTag; + } + FragmentMastodonTimeline fragmentMastodonTimeline = new FragmentMastodonTimeline(); + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.TREND_MESSAGE); + fragmentMastodonTimeline.setArguments(bundle); + return fragmentMastodonTimeline; + } + + @Override + public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + } + + @Override + public int getCount() { + return 2; + } + } +} diff --git a/app/src/main/java/app/fedilab/android/client/endpoints/MastodonTimelinesService.java b/app/src/main/java/app/fedilab/android/client/endpoints/MastodonTimelinesService.java index 36d8c9f6..09d6da1c 100644 --- a/app/src/main/java/app/fedilab/android/client/endpoints/MastodonTimelinesService.java +++ b/app/src/main/java/app/fedilab/android/client/endpoints/MastodonTimelinesService.java @@ -21,6 +21,7 @@ import app.fedilab.android.client.entities.api.Conversation; import app.fedilab.android.client.entities.api.Marker; import app.fedilab.android.client.entities.api.MastodonList; import app.fedilab.android.client.entities.api.Status; +import app.fedilab.android.client.entities.api.Tag; import app.fedilab.android.client.entities.misskey.MisskeyNote; import app.fedilab.android.client.entities.nitter.Nitter; import app.fedilab.android.client.entities.peertube.PeertubeVideo; @@ -52,6 +53,14 @@ public interface MastodonTimelinesService { @Query("limit") Integer limit ); + + @GET("trends/statuses") + Call> getStatusTrends(@Header("Authorization") String token); + + + @GET("trends/tags") + Call> getTagTrends(@Header("Authorization") String token); + //Public Tags timelines @GET("timelines/tag/{hashtag}") Call> getHashTag( diff --git a/app/src/main/java/app/fedilab/android/client/entities/api/Attachment.java b/app/src/main/java/app/fedilab/android/client/entities/api/Attachment.java index 2d06c314..3cd5e3c0 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/api/Attachment.java +++ b/app/src/main/java/app/fedilab/android/client/entities/api/Attachment.java @@ -43,7 +43,37 @@ public class Attachment implements Serializable { public long size; @SerializedName("local_path") public String local_path; + @SerializedName("meta") + public Meta meta; public String peertubeHost = null; public String peertubeId = null; + public String focus = null; + + public static class Meta implements Serializable { + @SerializedName("focus") + public Focus focus; + @SerializedName("original") + public MediaData original; + @SerializedName("small") + public MediaData small; + } + + public static class Focus implements Serializable { + @SerializedName("x") + public float x; + @SerializedName("y") + public float y; + } + + public static class MediaData implements Serializable { + @SerializedName("width") + public int width; + @SerializedName("height") + public int height; + @SerializedName("size") + public String size; + @SerializedName("aspect") + public float aspect; + } } diff --git a/app/src/main/java/app/fedilab/android/client/entities/app/QuickLoad.java b/app/src/main/java/app/fedilab/android/client/entities/app/QuickLoad.java index fc1d0b19..d3f56ce4 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/app/QuickLoad.java +++ b/app/src/main/java/app/fedilab/android/client/entities/app/QuickLoad.java @@ -309,7 +309,7 @@ public class QuickLoad { QuickLoad localQuickLoad = getSavedValue(account, Timeline.TimeLineEnum.LOCAL, null); QuickLoad publicQuickLoad = getSavedValue(account, Timeline.TimeLineEnum.PUBLIC, null); - if (homeQuickLoad != null && homeQuickLoad.statuses != null) { + if (homeQuickLoad != null && homeQuickLoad.statuses != null && newStatus != null) { for (int i = 0; i < homeQuickLoad.statuses.size(); i++) { if (homeQuickLoad.statuses.get(i).id.equals(newStatus.id)) { homeQuickLoad.statuses.set(i, newStatus); @@ -327,7 +327,7 @@ public class QuickLoad { e.printStackTrace(); } } - if (localQuickLoad != null && localQuickLoad.statuses != null) { + if (localQuickLoad != null && localQuickLoad.statuses != null && newStatus != null) { for (int i = 0; i < localQuickLoad.statuses.size(); i++) { if (localQuickLoad.statuses.get(i).id.equals(newStatus.id)) { localQuickLoad.statuses.set(i, newStatus); @@ -345,7 +345,7 @@ public class QuickLoad { e.printStackTrace(); } } - if (publicQuickLoad != null && publicQuickLoad.statuses != null) { + if (publicQuickLoad != null && publicQuickLoad.statuses != null && newStatus != null) { for (int i = 0; i < publicQuickLoad.statuses.size(); i++) { if (publicQuickLoad.statuses.get(i).id.equals(newStatus.id)) { publicQuickLoad.statuses.set(i, newStatus); diff --git a/app/src/main/java/app/fedilab/android/client/entities/app/Timeline.java b/app/src/main/java/app/fedilab/android/client/entities/app/Timeline.java index b8a4846c..d26badc9 100644 --- a/app/src/main/java/app/fedilab/android/client/entities/app/Timeline.java +++ b/app/src/main/java/app/fedilab/android/client/entities/app/Timeline.java @@ -370,6 +370,10 @@ public class Timeline { LIST("LIST"), @SerializedName("REMOTE") REMOTE("REMOTE"), + @SerializedName("TREND_TAG") + TREND_TAG("TREND_TAG"), + @SerializedName("TREND_MESSAGE") + TREND_MESSAGE("TREND_MESSAGE"), @SerializedName("ACCOUNT_TIMELINE") ACCOUNT_TIMELINE("ACCOUNT_TIMELINE"), @SerializedName("MUTED_TIMELINE") diff --git a/app/src/main/java/app/fedilab/android/helper/CirclesDrawingView.java b/app/src/main/java/app/fedilab/android/helper/CirclesDrawingView.java new file mode 100644 index 00000000..33672f60 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/CirclesDrawingView.java @@ -0,0 +1,249 @@ +package app.fedilab.android.helper; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.Rect; +import android.util.AttributeSet; +import android.util.SparseArray; +import android.view.MotionEvent; +import android.view.View; + +import androidx.core.content.res.ResourcesCompat; + +import java.util.HashSet; +import java.util.Random; + +import app.fedilab.android.R; + +//Original work at https://stackoverflow.com/a/17830245 +public class CirclesDrawingView extends View { + + + // Radius limit in pixels + private final static int RADIUS_LIMIT = 100; + private static final int CIRCLES_LIMIT = 1; + private final Random mRadiusGenerator = new Random(); + /** + * All available circles + */ + private final HashSet mCircles = new HashSet<>(CIRCLES_LIMIT); + private final SparseArray mCirclePointer = new SparseArray<>(CIRCLES_LIMIT); + /** + * Paint to draw circles + */ + private Paint mCirclePaint; + private CircleArea touchedCircle; + + /** + * Default constructor + * + * @param ct {@link android.content.Context} + */ + public CirclesDrawingView(final Context ct) { + super(ct); + init(ct); + } + + public CirclesDrawingView(final Context ct, final AttributeSet attrs) { + super(ct, attrs); + init(ct); + } + + public CirclesDrawingView(final Context ct, final AttributeSet attrs, final int defStyle) { + super(ct, attrs, defStyle); + init(ct); + } + + public CircleArea getTouchedCircle() { + return this.touchedCircle; + } + + private void init(final Context ct) { + // Generate bitmap used for background + mCirclePaint = new Paint(); + + mCirclePaint.setColor(ResourcesCompat.getColor(getContext().getResources(), R.color.cyanea_accent, getContext().getTheme())); + mCirclePaint.setStrokeWidth(10); + mCirclePaint.setStyle(Paint.Style.STROKE); + } + + @Override + public void onDraw(final Canvas canv) { + // background bitmap to cover all area + for (CircleArea circle : mCircles) { + canv.drawCircle(circle.centerX, circle.centerY, circle.radius, mCirclePaint); + } + } + + @Override + public boolean onTouchEvent(final MotionEvent event) { + boolean handled = false; + + + int xTouch; + int yTouch; + int pointerId; + int actionIndex = event.getActionIndex(); + + // get touch event coordinates and make transparent circle from it + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + // it's the first pointer, so clear all existing pointers data + clearCirclePointer(); + + xTouch = (int) event.getX(0); + yTouch = (int) event.getY(0); + + // check if we've touched inside some circle + touchedCircle = obtainTouchedCircle(xTouch, yTouch); + touchedCircle.centerX = xTouch; + touchedCircle.centerY = yTouch; + mCirclePointer.put(event.getPointerId(0), touchedCircle); + invalidate(); + handled = true; + break; + + case MotionEvent.ACTION_POINTER_DOWN: + // It secondary pointers, so obtain their ids and check circles + pointerId = event.getPointerId(actionIndex); + + xTouch = (int) event.getX(actionIndex); + yTouch = (int) event.getY(actionIndex); + + // check if we've touched inside some circle + touchedCircle = obtainTouchedCircle(xTouch, yTouch); + + mCirclePointer.put(pointerId, touchedCircle); + touchedCircle.centerX = xTouch; + touchedCircle.centerY = yTouch; + invalidate(); + handled = true; + break; + + case MotionEvent.ACTION_MOVE: + final int pointerCount = event.getPointerCount(); + + for (actionIndex = 0; actionIndex < pointerCount; actionIndex++) { + // Some pointer has moved, search it by pointer id + pointerId = event.getPointerId(actionIndex); + + xTouch = (int) event.getX(actionIndex); + yTouch = (int) event.getY(actionIndex); + + touchedCircle = mCirclePointer.get(pointerId); + + if (null != touchedCircle) { + touchedCircle.centerX = xTouch; + touchedCircle.centerY = yTouch; + } + } + invalidate(); + handled = true; + break; + + case MotionEvent.ACTION_UP: + clearCirclePointer(); + invalidate(); + handled = true; + break; + + case MotionEvent.ACTION_POINTER_UP: + // not general pointer was up + pointerId = event.getPointerId(actionIndex); + + mCirclePointer.remove(pointerId); + invalidate(); + handled = true; + break; + + case MotionEvent.ACTION_CANCEL: + handled = true; + break; + + default: + // do nothing + break; + } + + return super.onTouchEvent(event) || handled; + } + + /** + * Clears all CircleArea - pointer id relations + */ + private void clearCirclePointer() { + + mCirclePointer.clear(); + } + + /** + * Search and creates new (if needed) circle based on touch area + * + * @param xTouch int x of touch + * @param yTouch int y of touch + * @return obtained {@link CircleArea} + */ + private CircleArea obtainTouchedCircle(final int xTouch, final int yTouch) { + CircleArea touchedCircle = getTouchedCircle(xTouch, yTouch); + + if (null == touchedCircle) { + touchedCircle = new CircleArea(xTouch, yTouch, mRadiusGenerator.nextInt(RADIUS_LIMIT) + RADIUS_LIMIT); + + if (mCircles.size() == CIRCLES_LIMIT) { + // remove first circle + mCircles.clear(); + } + + mCircles.add(touchedCircle); + } + + return touchedCircle; + } + + /** + * Determines touched circle + * + * @param xTouch int x touch coordinate + * @param yTouch int y touch coordinate + * @return {@link CircleArea} touched circle or null if no circle has been touched + */ + private CircleArea getTouchedCircle(final int xTouch, final int yTouch) { + CircleArea touched = null; + + for (CircleArea circle : mCircles) { + if ((circle.centerX - xTouch) * (circle.centerX - xTouch) + (circle.centerY - yTouch) * (circle.centerY - yTouch) <= circle.radius * circle.radius) { + touched = circle; + break; + } + } + + return touched; + } + + @Override + protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + new Rect(0, 0, getMeasuredWidth(), getMeasuredHeight()); + } + + /** + * Stores data about single circle + */ + public static class CircleArea { + public int centerX; + public int centerY; + int radius; + + CircleArea(int centerX, int centerY, int radius) { + this.radius = radius; + this.centerX = centerX; + this.centerY = centerY; + } + + @Override + public String toString() { + return "Circle[" + centerX + ", " + centerY + ", " + radius + "]"; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/helper/GlideFocus.java b/app/src/main/java/app/fedilab/android/helper/GlideFocus.java new file mode 100644 index 00000000..688cc897 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/helper/GlideFocus.java @@ -0,0 +1,209 @@ +package app.fedilab.android.helper; +/* Copyright 2021 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 . */ + +import static com.bumptech.glide.load.resource.bitmap.TransformationUtils.PAINT_FLAGS; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.os.Build; + +import androidx.annotation.NonNull; + +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool; +import com.bumptech.glide.load.resource.bitmap.TransformationUtils; +import com.bumptech.glide.util.Synthetic; + +import java.security.MessageDigest; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import jp.wasabeef.glide.transformations.BitmapTransformation; + +public class GlideFocus extends BitmapTransformation { + + + private static final int VERSION = 1; + private static final String ID = "app.fedilab.android.GlideFocus." + VERSION; + private static final Set MODELS_REQUIRING_BITMAP_LOCK = + new HashSet<>( + Arrays.asList( + // Moto X gen 2 + "XT1085", + "XT1092", + "XT1093", + "XT1094", + "XT1095", + "XT1096", + "XT1097", + "XT1098", + // Moto G gen 1 + "XT1031", + "XT1028", + "XT937C", + "XT1032", + "XT1008", + "XT1033", + "XT1035", + "XT1034", + "XT939G", + "XT1039", + "XT1040", + "XT1042", + "XT1045", + // Moto G gen 2 + "XT1063", + "XT1064", + "XT1068", + "XT1069", + "XT1072", + "XT1077", + "XT1078", + "XT1079")); + private static final Lock BITMAP_DRAWABLE_LOCK = + MODELS_REQUIRING_BITMAP_LOCK.contains(Build.MODEL) ? new ReentrantLock() : new NoLock(); + private static final Paint DEFAULT_PAINT = new Paint(PAINT_FLAGS); + private final float focalX; + private final float focalY; + + public GlideFocus(float focalX, float focalY) { + this.focalX = focalX; + this.focalY = focalY; + } + + private static void applyMatrix( + @NonNull Bitmap inBitmap, @NonNull Bitmap targetBitmap, Matrix matrix) { + BITMAP_DRAWABLE_LOCK.lock(); + try { + Canvas canvas = new Canvas(targetBitmap); + canvas.drawBitmap(inBitmap, matrix, DEFAULT_PAINT); + clear(canvas); + } finally { + BITMAP_DRAWABLE_LOCK.unlock(); + } + } + + @NonNull + private static Bitmap.Config getNonNullConfig(@NonNull Bitmap bitmap) { + return bitmap.getConfig() != null ? bitmap.getConfig() : Bitmap.Config.ARGB_8888; + } + + // Avoids warnings in M+. + private static void clear(Canvas canvas) { + canvas.setBitmap(null); + } + + @Override + protected Bitmap transform(@NonNull Context context, @NonNull BitmapPool pool, + @NonNull Bitmap inBitmap, int width, int height) { + + if (inBitmap.getWidth() == width && inBitmap.getHeight() == height) { + return inBitmap; + } + // From ImageView/Bitmap.createScaledBitmap. + final float scale; + final float dx; + final float dy; + Matrix m = new Matrix(); + if (inBitmap.getWidth() * height > width * inBitmap.getHeight()) { + scale = (float) height / (float) inBitmap.getHeight(); + dx = (width - inBitmap.getWidth() * scale) * 0.5f * (1 + focalX); + dy = 0; + } else { + scale = (float) width / (float) inBitmap.getWidth(); + dx = 0; + dy = (height - inBitmap.getHeight() * scale) * 0.5f * (1 + focalY); + } + + m.setScale(scale, scale); + m.postTranslate((int) (dx + 0.5f), (int) (dy + 0.5f)); + + Bitmap result = pool.get(width, height, getNonNullConfig(inBitmap)); + // We don't add or remove alpha, so keep the alpha setting of the Bitmap we were given. + TransformationUtils.setAlpha(inBitmap, result); + + applyMatrix(inBitmap, result, m); + return result; + } + + @Override + public void updateDiskCacheKey(@NonNull MessageDigest messageDigest) { + messageDigest.update((ID + focalX + focalY).getBytes(CHARSET)); + } + + @Override + public boolean equals(Object o) { + return o instanceof GlideFocus && + ((GlideFocus) o).focalX == focalX && + ((GlideFocus) o).focalY == focalY; + } + + @Override + public int hashCode() { + return (int) (ID.hashCode() + focalX * 100000 + focalY * 1000); + } + + + @Override + public String toString() { + return "CropTransformation(width=" + focalX + ", height=" + focalY + ")"; + } + + private static final class NoLock implements Lock { + + @Synthetic + NoLock() { + } + + @Override + public void lock() { + // do nothing + } + + @Override + public void lockInterruptibly() throws InterruptedException { + // do nothing + } + + @Override + public boolean tryLock() { + return true; + } + + @Override + public boolean tryLock(long time, @NonNull TimeUnit unit) throws InterruptedException { + return true; + } + + @Override + public void unlock() { + // do nothing + } + + @NonNull + @Override + public Condition newCondition() { + throw new UnsupportedOperationException("Should not be called"); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/helper/Helper.java b/app/src/main/java/app/fedilab/android/helper/Helper.java index d57aa149..8029b84a 100644 --- a/app/src/main/java/app/fedilab/android/helper/Helper.java +++ b/app/src/main/java/app/fedilab/android/helper/Helper.java @@ -1379,6 +1379,9 @@ public class Helper { imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); } + + private static int notificationId = 1; + /** * Sends notification with intent * @@ -1389,7 +1392,7 @@ public class Helper { * @param message String message for the notification */ @SuppressLint("UnspecifiedImmutableFlag") - public static void notify_user(Context context, int notificationId, BaseAccount account, Intent intent, Bitmap icon, NotifType notifType, String title, String message) { + public static void notify_user(Context context, BaseAccount account, Intent intent, Bitmap icon, NotifType notifType, String title, String message) { final SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(context); // prepare intent which is triggered if the user click on the notification NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); @@ -1440,14 +1443,11 @@ public class Helper { channelTitle = context.getString(R.string.channel_notif_boost); } NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(context, channelId) - .setSmallIcon(R.drawable.ic_notification).setTicker(message) - .setWhen(System.currentTimeMillis()) - .setAutoCancel(true); + .setSmallIcon(R.drawable.ic_notification).setTicker(message); if (notifType == NotifType.MENTION) { if (message.length() > 500) { message = message.substring(0, 499) + "…"; } - notificationBuilder.setStyle(new NotificationCompat.BigTextStyle().bigText(message)); } notificationBuilder.setGroup(account.mastodon_account.acct + "@" + account.instance) .setContentIntent(pIntent) @@ -1515,19 +1515,21 @@ public class Helper { } notificationBuilder.setContentTitle(title); notificationBuilder.setLargeIcon(icon); - notificationManager.notify(notificationId, notificationBuilder.build()); - - Notification summaryNotification = - new NotificationCompat.Builder(context, channelId) - .setContentTitle(title) - .setContentText(channelTitle) - .setContentIntent(pIntent) - .setLargeIcon(icon) - .setSmallIcon(R.drawable.ic_notification) - .setGroup(account.mastodon_account.acct + "@" + account.instance) - .setGroupSummary(true) - .build(); - notificationManager.notify(notificationId, summaryNotification); + + + Notification summaryNotification = summaryNotification = new NotificationCompat.Builder(context, channelId) + .setContentTitle(title) + .setContentText(channelTitle) + .setContentIntent(pIntent) + .setLargeIcon(icon) + .setSmallIcon(R.drawable.ic_notification) + .setStyle(new NotificationCompat.BigTextStyle().bigText(message)) + .setGroup(account.mastodon_account.acct + "@" + account.instance) + .setGroupSummary(true) + .build(); + + notificationManager.notify(notificationId++, notificationBuilder.build()); + notificationManager.notify(0, summaryNotification); } public static void transfertIfExist(Context context) { diff --git a/app/src/main/java/app/fedilab/android/helper/MediaHelper.java b/app/src/main/java/app/fedilab/android/helper/MediaHelper.java index 8d3c29a9..b21703cf 100644 --- a/app/src/main/java/app/fedilab/android/helper/MediaHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/MediaHelper.java @@ -163,7 +163,7 @@ public class MediaHelper { Uri uri = Uri.fromFile(backupFile); intent.setDataAndType(uri, mime); if (!share) { - notify_user(context, Helper.NOTIFICATION_MEDIA, currentAccount, intent, BitmapFactory.decodeResource(context.getResources(), + notify_user(context, currentAccount, intent, BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher), Helper.NotifType.STORE, context.getString(R.string.save_over), context.getString(R.string.download_from, fileName)); Toasty.success(context, context.getString(R.string.save_over), Toasty.LENGTH_LONG).show(); } else { diff --git a/app/src/main/java/app/fedilab/android/helper/NestedScrollableHost.kt b/app/src/main/java/app/fedilab/android/helper/NestedScrollableHost.kt deleted file mode 100644 index 2f23c941..00000000 --- a/app/src/main/java/app/fedilab/android/helper/NestedScrollableHost.kt +++ /dev/null @@ -1,113 +0,0 @@ -package app.fedilab.android.helper - -/* - * Copyright 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - - -import android.content.Context -import android.util.AttributeSet -import android.view.MotionEvent -import android.view.View -import android.view.ViewConfiguration -import android.widget.FrameLayout -import androidx.viewpager2.widget.ViewPager2 -import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL -import kotlin.math.absoluteValue -import kotlin.math.sign - -/** - * Layout to wrap a scrollable component inside a ViewPager2. Provided as a solution to the problem - * where pages of ViewPager2 have nested scrollable elements that scroll in the same direction as - * ViewPager2. The scrollable element needs to be the immediate and only child of this host layout. - * - * This solution has limitations when using multiple levels of nested scrollable elements - * (e.g. a horizontal RecyclerView in a vertical RecyclerView in a horizontal ViewPager2). - */ -class NestedScrollableHost : FrameLayout { - constructor(context: Context) : super(context) - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - - private var touchSlop = 0 - private var initialX = 0f - private var initialY = 0f - private val parentViewPager: ViewPager2? - get() { - var v: View? = parent as? View - while (v != null && v !is ViewPager2) { - v = v.parent as? View - } - return v as? ViewPager2 - } - - private val child: View? get() = if (childCount > 0) getChildAt(0) else null - - init { - touchSlop = ViewConfiguration.get(context).scaledTouchSlop - } - - private fun canChildScroll(orientation: Int, delta: Float): Boolean { - val direction = -delta.sign.toInt() - return when (orientation) { - 0 -> child?.canScrollHorizontally(direction) ?: false - 1 -> child?.canScrollVertically(direction) ?: false - else -> throw IllegalArgumentException() - } - } - - override fun onInterceptTouchEvent(e: MotionEvent): Boolean { - handleInterceptTouchEvent(e) - return super.onInterceptTouchEvent(e) - } - - private fun handleInterceptTouchEvent(e: MotionEvent) { - val orientation = parentViewPager?.orientation ?: return - - // Early return if child can't scroll in same direction as parent - if (!canChildScroll(orientation, -1f) && !canChildScroll(orientation, 1f)) { - return - } - - if (e.action == MotionEvent.ACTION_DOWN) { - initialX = e.x - initialY = e.y - parent.requestDisallowInterceptTouchEvent(true) - } else if (e.action == MotionEvent.ACTION_MOVE) { - val dx = e.x - initialX - val dy = e.y - initialY - val isVpHorizontal = orientation == ORIENTATION_HORIZONTAL - - // assuming ViewPager2 touch-slop is 2x touch-slop of child - val scaledDx = dx.absoluteValue * if (isVpHorizontal) .5f else 1f - val scaledDy = dy.absoluteValue * if (isVpHorizontal) 1f else .5f - - if (scaledDx > touchSlop || scaledDy > touchSlop) { - if (isVpHorizontal == (scaledDy > scaledDx)) { - // Gesture is perpendicular, allow all parents to intercept - parent.requestDisallowInterceptTouchEvent(false) - } else { - // Gesture is parallel, query child if movement in that direction is possible - if (canChildScroll(orientation, if (isVpHorizontal) dx else dy)) { - // Child can scroll, disallow all parents to intercept - parent.requestDisallowInterceptTouchEvent(true) - } else { - // Child cannot scroll, allow all parents to intercept - parent.requestDisallowInterceptTouchEvent(false) - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/helper/NotificationsHelper.java b/app/src/main/java/app/fedilab/android/helper/NotificationsHelper.java index 09d2834b..60b3b7c1 100644 --- a/app/src/main/java/app/fedilab/android/helper/NotificationsHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/NotificationsHelper.java @@ -317,7 +317,7 @@ public class NotificationsHelper { since_ids.put(account.user_id + "@" + account.instance, lastNotif); editor.putString(context.getString(R.string.LAST_NOTIFICATION_ID) + account.user_id + "@" + account.instance, notifications.get(0).id); editor.apply(); - notify_user(context, Helper.NOTIFICATION_USER_NOTIF, account, intent, BitmapFactory.decodeResource(context.getResources(), + notify_user(context, account, intent, BitmapFactory.decodeResource(context.getResources(), R.mipmap.ic_launcher), finalNotifType, finalTitle, finalMessage); } return false; @@ -332,7 +332,7 @@ public class NotificationsHelper { editor.putString(context.getString(R.string.LAST_NOTIFICATION_ID) + account.user_id + "@" + account.instance, notifications.get(0).id); editor.apply(); since_ids.put(account.user_id + "@" + account.instance, lastNotif); - notify_user(context, Helper.NOTIFICATION_USER_NOTIF, account, intent, resource, finalNotifType, finalTitle, finalMessage); + notify_user(context, account, intent, resource, finalNotifType, finalTitle, finalMessage); } } diff --git a/app/src/main/java/app/fedilab/android/helper/PinnedTimelineHelper.java b/app/src/main/java/app/fedilab/android/helper/PinnedTimelineHelper.java index 28375c1e..d0b4cd04 100644 --- a/app/src/main/java/app/fedilab/android/helper/PinnedTimelineHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/PinnedTimelineHelper.java @@ -26,16 +26,14 @@ import android.view.View; import android.view.ViewGroup; import android.widget.EditText; import android.widget.LinearLayout; -import android.widget.TextView; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.PopupMenu; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentTransaction; -import androidx.viewpager2.widget.ViewPager2; +import androidx.viewpager.widget.ViewPager; import com.google.android.material.tabs.TabLayout; -import com.google.android.material.tabs.TabLayoutMediator; import java.util.ArrayList; import java.util.Arrays; @@ -149,18 +147,11 @@ public class PinnedTimelineHelper { activityMainBinding.tabLayout.removeAllTabs(); //Small hack to hide first tabs (they represent the item of the bottom menu) int toRemove = itemToRemoveInBottomMenu(activity); - List tabTitle = new ArrayList<>(); - List tabTypeRemote = new ArrayList<>(); - List tabType = new ArrayList<>(); for (int i = 0; i < (BOTTOM_TIMELINE_COUNT - toRemove); i++) { activityMainBinding.tabLayout.addTab(activityMainBinding.tabLayout.newTab()); - tabTitle.add(""); - tabType.add(Timeline.TimeLineEnum.HOME); - tabTypeRemote.add(RemoteInstance.InstanceType.MASTODON); ((ViewGroup) activityMainBinding.tabLayout.getChildAt(0)).getChildAt(i).setVisibility(View.GONE); } List pinnedTimelineVisibleList = new ArrayList<>(); - for (PinnedTimeline pinnedTimeline : pinned.pinnedTimelines) { if (pinnedTimeline.displayed) { TabLayout.Tab tab = activityMainBinding.tabLayout.newTab(); @@ -170,22 +161,46 @@ public class PinnedTimelineHelper { name = pinnedTimeline.mastodonList.title; break; case TAG: - name = pinnedTimeline.tagTimeline.name; + name = pinnedTimeline.tagTimeline.name.replaceAll("#", ""); break; case REMOTE: name = pinnedTimeline.remoteInstance.host; break; } - TextView tv = (TextView) LayoutInflater.from(activity).inflate(R.layout.custom_tab_instance, new LinearLayout(activity), false); - tv.setText(name); - tabTitle.add(name); - tabType.add(pinnedTimeline.type); - if (pinnedTimeline.type == Timeline.TimeLineEnum.REMOTE) { - tabTypeRemote.add(pinnedTimeline.remoteInstance.type); - } else { - tabTypeRemote.add(null); + TabCustomViewBinding tabCustomViewBinding = TabCustomViewBinding.inflate(activity.getLayoutInflater()); + tabCustomViewBinding.title.setText(name); + switch (pinnedTimeline.type) { + case LIST: + tabCustomViewBinding.icon.setImageResource(R.drawable.ic_tl_list); + break; + case TAG: + tabCustomViewBinding.icon.setImageResource(R.drawable.ic_tl_tag); + break; + case REMOTE: + switch (pinnedTimeline.remoteInstance.type) { + case PIXELFED: + tabCustomViewBinding.icon.setImageResource(R.drawable.pixelfed); + break; + case MASTODON: + tabCustomViewBinding.icon.setImageResource(R.drawable.mastodon_icon_item); + break; + + case MISSKEY: + tabCustomViewBinding.icon.setImageResource(R.drawable.misskey); + break; + case NITTER: + tabCustomViewBinding.icon.setImageResource(R.drawable.nitter); + break; + case GNU: + tabCustomViewBinding.icon.setImageResource(R.drawable.ic_gnu_social); + break; + case PEERTUBE: + tabCustomViewBinding.icon.setImageResource(R.drawable.peertube_icon); + break; + } + break; } - tab.setCustomView(tv); + tab.setCustomView(tabCustomViewBinding.getRoot()); activityMainBinding.tabLayout.addTab(tab); pinnedTimelineVisibleList.add(pinnedTimeline); } @@ -211,13 +226,18 @@ public class PinnedTimelineHelper { return true; }); } + activityMainBinding.viewPager.setAdapter(null); + activityMainBinding.viewPager.clearOnPageChangeListeners(); activityMainBinding.tabLayout.clearOnTabSelectedListeners(); - - FedilabPageAdapter fedilabPageAdapter = new FedilabPageAdapter(activity, activity, pinned, bottomMenu); + FedilabPageAdapter fedilabPageAdapter = new FedilabPageAdapter(activity, activity.getSupportFragmentManager(), pinned, bottomMenu); activityMainBinding.viewPager.setAdapter(fedilabPageAdapter); + activityMainBinding.viewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(activityMainBinding.tabLayout)); + activityMainBinding.viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + } - activityMainBinding.viewPager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { @Override public void onPageSelected(int position) { if (position < BOTTOM_TIMELINE_COUNT - toRemove) { @@ -230,49 +250,17 @@ public class PinnedTimelineHelper { activityMainBinding.bottomNavView.getMenu().setGroupCheckable(0, true, true); } } + + @Override + public void onPageScrollStateChanged(int state) { + } }); - new TabLayoutMediator(activityMainBinding.tabLayout, activityMainBinding.viewPager, - (tab, position) -> { - TabCustomViewBinding tabCustomViewBinding = TabCustomViewBinding.inflate(activity.getLayoutInflater()); - tabCustomViewBinding.title.setText(tabTitle.get(position)); - switch (tabType.get(position)) { - case LIST: - tabCustomViewBinding.icon.setImageResource(R.drawable.ic_tl_list); - break; - case TAG: - tabCustomViewBinding.icon.setImageResource(R.drawable.ic_tl_tag); - break; - case REMOTE: - switch (tabTypeRemote.get(position)) { - case PIXELFED: - tabCustomViewBinding.icon.setImageResource(R.drawable.pixelfed); - break; - case MASTODON: - tabCustomViewBinding.icon.setImageResource(R.drawable.mastodon_icon_item); - break; - - case MISSKEY: - tabCustomViewBinding.icon.setImageResource(R.drawable.misskey); - break; - case NITTER: - tabCustomViewBinding.icon.setImageResource(R.drawable.nitter); - break; - case GNU: - tabCustomViewBinding.icon.setImageResource(R.drawable.ic_gnu_social); - break; - case PEERTUBE: - tabCustomViewBinding.icon.setImageResource(R.drawable.peertube_icon); - break; - } - break; - } - tab.setCustomView(tabCustomViewBinding.getRoot()); - } - ).attach(); + activityMainBinding.tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { @Override public void onTabSelected(TabLayout.Tab tab) { + activityMainBinding.viewPager.setCurrentItem(tab.getPosition()); } @Override @@ -281,7 +269,7 @@ public class PinnedTimelineHelper { @Override public void onTabReselected(TabLayout.Tab tab) { - Fragment fragment = activity.getSupportFragmentManager().findFragmentByTag("f" + activityMainBinding.viewPager.getCurrentItem()); + Fragment fragment = fedilabPageAdapter.getCurrentFragment(); if (fragment instanceof FragmentMastodonTimeline) { ((FragmentMastodonTimeline) fragment).scrollToTop(); } else if (fragment instanceof FragmentMastodonConversation) { @@ -353,19 +341,19 @@ public class PinnedTimelineHelper { itemShowNSFW.setChecked(showNSFW[0]); popup.setOnDismissListener(menu1 -> { if (changes[0]) { - FragmentMastodonTimeline fragmentMastodonTimeline; - Fragment fragment = activity.getSupportFragmentManager().findFragmentByTag("f" + activityMainBinding.viewPager.getCurrentItem()); - if (fragment instanceof FragmentMastodonTimeline && fragment.isVisible()) { - fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment); - FragmentTransaction fragTransaction = activity.getSupportFragmentManager().beginTransaction(); - fragTransaction.detach(fragmentMastodonTimeline).commit(); - Bundle bundle = new Bundle(); - bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.TAG); - bundle.putSerializable(Helper.ARG_TAG_TIMELINE, tagTimeline); - fragmentMastodonTimeline.setArguments(bundle); - FragmentTransaction fragTransaction2 = activity.getSupportFragmentManager().beginTransaction(); - fragTransaction2.attach(fragmentMastodonTimeline); - fragTransaction2.commit(); + if (activityMainBinding.viewPager.getAdapter() != null) { + Fragment fragmentMastodonTimeline = (Fragment) activityMainBinding.viewPager.getAdapter().instantiateItem(activityMainBinding.viewPager, activityMainBinding.tabLayout.getSelectedTabPosition()); + if (fragmentMastodonTimeline instanceof FragmentMastodonTimeline && fragmentMastodonTimeline.isVisible()) { + FragmentTransaction fragTransaction = activity.getSupportFragmentManager().beginTransaction(); + fragTransaction.detach(fragmentMastodonTimeline).commit(); + Bundle bundle = new Bundle(); + bundle.putSerializable(Helper.ARG_TIMELINE_TYPE, Timeline.TimeLineEnum.TAG); + bundle.putSerializable(Helper.ARG_TAG_TIMELINE, tagTimeline); + fragmentMastodonTimeline.setArguments(bundle); + FragmentTransaction fragTransaction2 = activity.getSupportFragmentManager().beginTransaction(); + fragTransaction2.attach(fragmentMastodonTimeline); + fragTransaction2.commit(); + } } } }); @@ -568,9 +556,11 @@ public class PinnedTimelineHelper { }); changes[0] = true; FragmentMastodonTimeline fragmentMastodonTimeline = null; - Fragment fragment = activity.getSupportFragmentManager().findFragmentByTag("f" + activityMainBinding.viewPager.getCurrentItem()); - if (fragment instanceof FragmentMastodonTimeline && fragment.isVisible()) { - fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment); + 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); + } } if (fragmentMastodonTimeline == null) return false; @@ -613,10 +603,12 @@ public class PinnedTimelineHelper { MenuItem item = popup.getMenu().add(0, 0, Menu.NONE, title); item.setOnMenuItemClickListener(item1 -> { FragmentMastodonTimeline fragmentMastodonTimeline = null; - Fragment fragment = activity.getSupportFragmentManager().findFragmentByTag("f" + activityMainBinding.viewPager.getCurrentItem()); - if (fragment instanceof FragmentMastodonTimeline && fragment.isVisible()) { - fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment); - fragmentMastodonTimeline.refreshAllAdapters(); + 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) @@ -694,10 +686,12 @@ public class PinnedTimelineHelper { popup.setOnDismissListener(menu -> { if (changes[0]) { FragmentMastodonTimeline fragmentMastodonTimeline = null; - Fragment fragment = activity.getSupportFragmentManager().findFragmentByTag("f" + activityMainBinding.viewPager.getCurrentItem()); - if (fragment instanceof FragmentMastodonTimeline && fragment.isVisible()) { - fragmentMastodonTimeline = ((FragmentMastodonTimeline) fragment); - fragmentMastodonTimeline.refreshAllAdapters(); + 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) diff --git a/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java b/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java index d0e9afda..b9d1df8c 100644 --- a/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java +++ b/app/src/main/java/app/fedilab/android/helper/SpannableHelper.java @@ -256,14 +256,15 @@ public class SpannableHelper { content.setSpan(new LongClickableSpan() { @Override public void onLongClick(View view) { - AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context, Helper.dialogStyle()); + Context mContext = view.getContext(); + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext, Helper.dialogStyle()); PopupLinksBinding popupLinksBinding = PopupLinksBinding.inflate(LayoutInflater.from(context)); dialogBuilder.setView(popupLinksBinding.getRoot()); AlertDialog alertDialog = dialogBuilder.create(); alertDialog.show(); popupLinksBinding.displayFullLink.setOnClickListener(v -> { - AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle()); + AlertDialog.Builder builder = new AlertDialog.Builder(mContext, Helper.dialogStyle()); builder.setMessage(url); builder.setTitle(context.getString(R.string.display_full_link)); builder.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss()) @@ -720,14 +721,15 @@ public class SpannableHelper { content.setSpan(new LongClickableSpan() { @Override public void onLongClick(View view) { - AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(view.getContext(), Helper.dialogStyle()); + Context mContext = view.getContext(); + AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(mContext, Helper.dialogStyle()); PopupLinksBinding popupLinksBinding = PopupLinksBinding.inflate(LayoutInflater.from(context)); dialogBuilder.setView(popupLinksBinding.getRoot()); AlertDialog alertDialog = dialogBuilder.create(); alertDialog.show(); popupLinksBinding.displayFullLink.setOnClickListener(v -> { - AlertDialog.Builder builder = new AlertDialog.Builder(context, Helper.dialogStyle()); + AlertDialog.Builder builder = new AlertDialog.Builder(mContext, Helper.dialogStyle()); builder.setMessage(url); builder.setTitle(context.getString(R.string.display_full_link)); builder.setPositiveButton(R.string.close, (dialog, which) -> dialog.dismiss()) diff --git a/app/src/main/java/app/fedilab/android/imageeditor/EditImageActivity.java b/app/src/main/java/app/fedilab/android/imageeditor/EditImageActivity.java index a9c68c7a..81e65086 100644 --- a/app/src/main/java/app/fedilab/android/imageeditor/EditImageActivity.java +++ b/app/src/main/java/app/fedilab/android/imageeditor/EditImageActivity.java @@ -5,6 +5,7 @@ import android.Manifest; import android.content.Intent; import android.content.pm.PackageManager; import android.graphics.Bitmap; +import android.graphics.BitmapFactory; import android.graphics.Typeface; import android.net.Uri; import android.os.Bundle; @@ -33,6 +34,7 @@ import java.io.InputStream; import app.fedilab.android.R; import app.fedilab.android.databinding.ActivityEditImageBinding; +import app.fedilab.android.helper.CirclesDrawingView; import app.fedilab.android.helper.Helper; import app.fedilab.android.imageeditor.base.BaseActivity; import app.fedilab.android.imageeditor.filters.FilterListener; @@ -58,7 +60,6 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList private static final int CAMERA_REQUEST = 52; private static final int PICK_REQUEST = 53; private final int STORE_REQUEST = 54; - private final EditingToolsAdapter mEditingToolsAdapter = new EditingToolsAdapter(this); private final FilterViewAdapter mFilterViewAdapter = new FilterViewAdapter(this); private final ConstraintSet mConstraintSet = new ConstraintSet(); @@ -117,8 +118,6 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList binding.rvFilterView.setLayoutManager(llmFilters); binding.rvFilterView.setAdapter(mFilterViewAdapter); - //Typeface mTextRobotoTf = ResourcesCompat.getFont(this, R.font.roboto_medium); - //Typeface mEmojiTypeFace = Typeface.createFromAsset(getAssets(), "emojione-android.ttf"); Typeface mEmojiTypeFace = Typeface.createFromAsset(getAssets(), "emojione-android.ttf"); mPhotoEditor = new PhotoEditor.Builder(this, binding.photoEditorView) @@ -246,6 +245,49 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList if (exit) { Intent intentImage = new Intent(Helper.INTENT_SEND_MODIFIED_IMAGE); intentImage.putExtra("imgpath", imagePath); + CirclesDrawingView.CircleArea circleArea = binding.focusCircle.getTouchedCircle(); + if (circleArea != null) { + //Dimension of the editor containing the image + int pHeight = binding.photoEditorView.getHeight(); + int pWidth = binding.photoEditorView.getWidth(); + //Load the original image in a bitmap + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFile(new File(imagePath).getAbsolutePath(), options); + //Get height and width of the original image + int imageHeight = options.outHeight; + int imageWidth = options.outWidth; + + //Evaluate the dimension of the image in the editor + int imgHeightInEditor; + int imgWidthInEditor; + //If the original image has its height greater than width => heights are equals + float focusX = -2, focusY = -2; + if (imageHeight > imageWidth) { + imgHeightInEditor = pHeight; + float ratio = (float) pHeight / (float) imageHeight; + imgWidthInEditor = (int) (pWidth * ratio); + } else { //Otherwise widths are equals + imgWidthInEditor = pWidth; + float ratio = (float) pWidth / (float) imageWidth; + imgHeightInEditor = (int) (pHeight * ratio); + } + focusY = (float) (circleArea.centerY * 2 - imgHeightInEditor / 2) / (float) imgHeightInEditor - 0.5f; + focusX = (float) (circleArea.centerX * 2 - imgWidthInEditor / 2) / (float) imgWidthInEditor - 0.5f; + if (focusX > 1) { + focusX = 1; + } else if (focusX < -1) { + focusX = -1; + } + if (focusY > 1) { + focusY = 1; + } else if (focusY < -1) { + focusY = -1; + } + intentImage.putExtra("focusX", focusX); + intentImage.putExtra("focusY", focusY); + } + LocalBroadcastManager.getInstance(EditImageActivity.this).sendBroadcast(intentImage); finish(); } @@ -376,6 +418,7 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList @Override public void onToolSelected(ToolType toolType) { + binding.focusCircle.setVisibility(View.GONE); switch (toolType) { case SHAPE: mPhotoEditor.setBrushDrawingMode(true); @@ -414,6 +457,9 @@ public class EditImageActivity extends BaseActivity implements OnPhotoEditorList CropImage.activity(uri) .start(this); break; + case FOCUS: + binding.focusCircle.setVisibility(View.VISIBLE); + break; } } diff --git a/app/src/main/java/app/fedilab/android/imageeditor/tools/EditingToolsAdapter.java b/app/src/main/java/app/fedilab/android/imageeditor/tools/EditingToolsAdapter.java index 07c3ceb7..5415c24c 100644 --- a/app/src/main/java/app/fedilab/android/imageeditor/tools/EditingToolsAdapter.java +++ b/app/src/main/java/app/fedilab/android/imageeditor/tools/EditingToolsAdapter.java @@ -32,6 +32,7 @@ public class EditingToolsAdapter extends RecyclerView.Adapter attachmentCall = mastodonStatusesService.postMedia(dataPost.token, fileMultipartBody, null, attachment.description, null); + + Call attachmentCall = mastodonStatusesService.postMedia(dataPost.token, fileMultipartBody, null, attachment.description, attachment.focus); if (attachmentCall != null) { try { diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java index 35ac4ad8..4851b42a 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/ComposeAdapter.java @@ -285,25 +285,25 @@ public class ComposeAdapter extends RecyclerView.Adapter 0) { + if (instanceInfo != null && instanceInfo.getMimeTypeImage() != null && instanceInfo.getMimeTypeImage().size() > 0) { mimetypes = instanceInfo.getMimeTypeImage().toArray(new String[0]); } else { mimetypes = new String[]{"image/*"}; } } else if (type == ComposeActivity.mediaType.VIDEO) { - if (instanceInfo.getMimeTypeVideo() != null && instanceInfo.getMimeTypeVideo().size() > 0) { + if (instanceInfo != null && instanceInfo.getMimeTypeVideo() != null && instanceInfo.getMimeTypeVideo().size() > 0) { mimetypes = instanceInfo.getMimeTypeVideo().toArray(new String[0]); } else { mimetypes = new String[]{"video/*"}; } } else if (type == ComposeActivity.mediaType.AUDIO) { - if (instanceInfo.getMimeTypeAudio() != null && instanceInfo.getMimeTypeAudio().size() > 0) { + if (instanceInfo != null && instanceInfo.getMimeTypeAudio() != null && instanceInfo.getMimeTypeAudio().size() > 0) { mimetypes = instanceInfo.getMimeTypeAudio().toArray(new String[0]); } else { mimetypes = new String[]{"audio/mpeg", "audio/opus", "audio/flac", "audio/wav", "audio/ogg"}; } } else if (type == ComposeActivity.mediaType.ALL) { - if (instanceInfo.getMimeTypeOther() != null && instanceInfo.getMimeTypeOther().size() > 0) { + if (instanceInfo != null && instanceInfo.getMimeTypeOther() != null && instanceInfo.getMimeTypeOther().size() > 0) { mimetypes = instanceInfo.getMimeTypeOther().toArray(new String[0]); } else { mimetypes = new String[]{"*/*"}; @@ -706,7 +706,12 @@ public class ComposeAdapter extends RecyclerView.Adapter max_car + 1) { int from = max_car - holder.binding.contentSpoiler.getText().length(); int to = (currentLength - holder.binding.contentSpoiler.getText().length()); @@ -1073,7 +1078,8 @@ public class ComposeAdapter extends RecyclerView.Adapter= instanceInfo.configuration.statusesConf.max_media_attachments) { + if (statusDraft.media_attachments != null && + ((instanceInfo != null && statusDraft.media_attachments.size() >= instanceInfo.configuration.statusesConf.max_media_attachments) || (instanceInfo == null && statusDraft.media_attachments.size() >= 4))) { holder.binding.buttonAttachImage.setEnabled(false); holder.binding.buttonAttachVideo.setEnabled(false); holder.binding.buttonAttachAudio.setEnabled(false); diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/FieldAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/FieldAdapter.java new file mode 100644 index 00000000..df8bbe27 --- /dev/null +++ b/app/src/main/java/app/fedilab/android/ui/drawer/FieldAdapter.java @@ -0,0 +1,85 @@ +package app.fedilab.android.ui.drawer; +/* 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 . */ + + +import android.content.Context; +import android.text.Spannable; +import android.text.method.LinkMovementMethod; +import android.text.style.ForegroundColorSpan; +import android.view.LayoutInflater; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; + +import app.fedilab.android.R; +import app.fedilab.android.client.entities.api.Field; +import app.fedilab.android.databinding.DrawerFieldBinding; + + +public class FieldAdapter extends RecyclerView.Adapter { + + private final List fields; + private Context context; + + public FieldAdapter(List fields) { + this.fields = fields; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public int getItemCount() { + return fields.size(); + } + + @NonNull + @Override + public FieldViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + context = parent.getContext(); + DrawerFieldBinding itemBinding = DrawerFieldBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false); + return new FieldViewHolder(itemBinding); + } + + @Override + public void onBindViewHolder(@NonNull FieldViewHolder holder, int position) { + Field field = fields.get(position); + if (field.verified_at != null) { + holder.binding.value.setCompoundDrawablesWithIntrinsicBounds(null, null, ContextCompat.getDrawable(context, R.drawable.ic_baseline_verified_24), null); + field.value_span.setSpan(new ForegroundColorSpan(ContextCompat.getColor(context, R.color.verified_text)), 0, field.value_span.toString().length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + holder.binding.value.setText(field.value_span != null ? field.value_span : field.value, TextView.BufferType.SPANNABLE); + holder.binding.value.setMovementMethod(LinkMovementMethod.getInstance()); + holder.binding.label.setText(field.name); + } + + + public static class FieldViewHolder extends RecyclerView.ViewHolder { + DrawerFieldBinding binding; + + FieldViewHolder(DrawerFieldBinding itemView) { + super(itemView.getRoot()); + binding = itemView; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/drawer/NotificationAdapter.java b/app/src/main/java/app/fedilab/android/ui/drawer/NotificationAdapter.java index 556f88b2..7a121691 100644 --- a/app/src/main/java/app/fedilab/android/ui/drawer/NotificationAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/drawer/NotificationAdapter.java @@ -143,13 +143,21 @@ public class NotificationAdapter extends RecyclerView.Adapter { + holder.bindingFetchMore.fetchMoreContainer.setEnabled(!notification.isFetchMoreHidden); + holder.bindingFetchMore.fetchMoreMin.setOnClickListener(v -> { if (position + 1 < notificationList.size()) { //We hide the button notification.isFetchMoreHidden = true; notifyItemChanged(position); - fetchMoreCallBack.onClick(notificationList.get(position + 1).id, notification.id); + fetchMoreCallBack.onClickMin(notificationList.get(position + 1).id, notification.id); + } + }); + holder.bindingFetchMore.fetchMoreMax.setOnClickListener(v -> { + if (position - 1 >= 0) { + //We hide the button + notification.isFetchMoreHidden = true; + notifyItemChanged(position); + fetchMoreCallBack.onClickMax(notificationList.get(position - 1).id, notification.id); } }); } else { @@ -264,7 +272,9 @@ public class NotificationAdapter extends RecyclerView.Adapter } else { layoutMediaBinding.playMusic.setVisibility(View.GONE); } + float focusX = 0.f; + float focusY = 0.f; + if (statusToDeal.media_attachments.get(0).meta != null && statusToDeal.media_attachments.get(0).meta.focus != null) { + focusX = statusToDeal.media_attachments.get(0).meta.focus.x; + focusY = statusToDeal.media_attachments.get(0).meta.focus.y; + } + if (!mediaObfuscated(statusToDeal) || expand_media) { layoutMediaBinding.viewHide.setImageResource(R.drawable.ic_baseline_visibility_24); Glide.with(layoutMediaBinding.media.getContext()) .load(statusToDeal.media_attachments.get(0).preview_url) + .apply(new RequestOptions().transform(new GlideFocus(focusX, focusY))) .into(layoutMediaBinding.media); } else { layoutMediaBinding.viewHide.setImageResource(R.drawable.ic_baseline_visibility_off_24); @@ -995,6 +1004,7 @@ public class StatusAdapter extends RecyclerView.Adapter .load(statusToDeal.media_attachments.get(0).preview_url) .apply(new RequestOptions().transform(new BlurTransformation(50, 3))) // .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners((int) Helper.convertDpToPixel(3, context)))) + .apply(new RequestOptions().transform(new GlideFocus(focusX, focusY))) .into(layoutMediaBinding.media); } layoutMediaBinding.viewHide.setOnClickListener(v -> { @@ -1009,6 +1019,13 @@ public class StatusAdapter extends RecyclerView.Adapter for (Attachment attachment : statusToDeal.media_attachments) { LayoutMediaBinding layoutMediaBinding = LayoutMediaBinding.inflate(LayoutInflater.from(context), holder.binding.attachmentsList, false); RelativeLayout.LayoutParams lp; + float focusX = 0.f; + float focusY = 0.f; + if (statusToDeal.media_attachments.get(0).meta != null && statusToDeal.media_attachments.get(0).meta.focus != null) { + focusX = statusToDeal.media_attachments.get(0).meta.focus.x; + focusY = statusToDeal.media_attachments.get(0).meta.focus.y; + } + if (fullAttachement) { lp = new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.WRAP_CONTENT, RelativeLayout.LayoutParams.WRAP_CONTENT); layoutMediaBinding.media.setScaleType(ImageView.ScaleType.FIT_CENTER); @@ -1032,12 +1049,14 @@ public class StatusAdapter extends RecyclerView.Adapter Glide.with(layoutMediaBinding.media.getContext()) .load(attachment.preview_url) .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners((int) Helper.convertDpToPixel(3, context)))) + .apply(new RequestOptions().transform(new GlideFocus(focusX, focusY))) .into(layoutMediaBinding.media); } else { layoutMediaBinding.viewHide.setImageResource(R.drawable.ic_baseline_visibility_off_24); Glide.with(layoutMediaBinding.media.getContext()) .load(attachment.preview_url) .apply(new RequestOptions().transform(new BlurTransformation(50, 3))) + .apply(new RequestOptions().transform(new GlideFocus(focusX, focusY))) // .apply(new RequestOptions().transform(new CenterCrop(), new RoundedCorners((int) Helper.convertDpToPixel(3, context)))) .into(layoutMediaBinding.media); } @@ -1172,6 +1191,9 @@ public class StatusAdapter extends RecyclerView.Adapter j++; } } else { + if (statusToDeal.poll.voters_count == 0 && statusToDeal.poll.votes_count > 0) { + statusToDeal.poll.voters_count = statusToDeal.poll.votes_count; + } holder.binding.poll.rated.setVisibility(View.GONE); holder.binding.poll.submitVote.setVisibility(View.VISIBLE); if (statusToDeal.poll.multiple) { @@ -1828,13 +1850,21 @@ public class StatusAdapter extends RecyclerView.Adapter }); } else if (viewHolder.getItemViewType() == STATUS_FETCH_MORE) { StatusViewHolder holder = (StatusViewHolder) viewHolder; - holder.bindingFetchMore.fetchMore.setEnabled(!status.isFetchMoreHidden); - holder.bindingFetchMore.fetchMore.setOnClickListener(v -> { + holder.bindingFetchMore.fetchMoreContainer.setEnabled(!status.isFetchMoreHidden); + holder.bindingFetchMore.fetchMoreMin.setOnClickListener(v -> { if (position + 1 < statusList.size()) { //We hide the button status.isFetchMoreHidden = true; notifyItemChanged(position); - fetchMoreCallBack.onClick(statusList.get(position + 1).id, status.id); + fetchMoreCallBack.onClickMinId(statusList.get(position + 1).id, status.id); + } + }); + holder.bindingFetchMore.fetchMoreMax.setOnClickListener(v -> { + if (position - 1 >= 0) { + //We hide the button + status.isFetchMoreHidden = true; + notifyItemChanged(position); + fetchMoreCallBack.onClickMaxId(statusList.get(position - 1).id, status.id); } }); } @@ -1857,7 +1887,9 @@ public class StatusAdapter extends RecyclerView.Adapter } public interface FetchMoreCallBack { - void onClick(String min_id, String fetchmoreId); + void onClickMinId(String min_id, String fetchmoreId); + + void onClickMaxId(String max_id, String fetchmoreId); } public static class StatusViewHolder extends RecyclerView.ViewHolder { diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentNotificationsSettings.java b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentNotificationsSettings.java index 2e8e13e3..58fb82ba 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentNotificationsSettings.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentNotificationsSettings.java @@ -179,7 +179,13 @@ public class FragmentNotificationsSettings extends PreferenceFragmentCompat impl PushHelper.startStreaming(requireActivity()); } if (key.compareToIgnoreCase(getString(R.string.SET_LED_COLOUR_VAL)) == 0) { - sharedPreferences.edit().putInt(getString(R.string.SET_LED_COLOUR_VAL), Integer.parseInt(key)).apply(); + try { + int value = Integer.parseInt(key); + sharedPreferences.edit().putInt(getString(R.string.SET_LED_COLOUR_VAL), value).apply(); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } } } diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentThemingSettings.java b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentThemingSettings.java index 25205867..b589b36e 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentThemingSettings.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/settings/FragmentThemingSettings.java @@ -571,7 +571,7 @@ public class FragmentThemingSettings extends PreferenceFragmentCompat implements Uri uri = Uri.parse("file://" + fullPath); intentOpen.setDataAndType(uri, "text/csv"); String title = getString(R.string.data_export_theme); - Helper.notify_user(getActivity(), Helper.NOTIFICATION_THEMING, currentAccount, intentOpen, BitmapFactory.decodeResource(requireActivity().getResources(), + Helper.notify_user(getActivity(), currentAccount, intentOpen, BitmapFactory.decodeResource(requireActivity().getResources(), R.mipmap.ic_launcher), Helper.NotifType.BACKUP, title, message); } catch (Exception e) { e.printStackTrace(); diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonAccount.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonAccount.java index 8b8c3444..81101684 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonAccount.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonAccount.java @@ -45,6 +45,7 @@ import app.fedilab.android.ui.drawer.AccountAdapter; import app.fedilab.android.ui.pageadapter.FedilabProfileTLPageAdapter; import app.fedilab.android.viewmodel.mastodon.AccountsVM; import app.fedilab.android.viewmodel.mastodon.SearchVM; +import es.dmoral.toasty.Toasty; public class FragmentMastodonAccount extends Fragment { @@ -116,11 +117,15 @@ public class FragmentMastodonAccount extends Fragment { 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 -> { - Accounts accounts = new Accounts(); - Pagination pagination = new Pagination(); - accounts.accounts = results.accounts; - accounts.pagination = pagination; - initializeAccountCommonView(accounts); + 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 if (timelineType == Timeline.TimeLineEnum.MUTED_TIMELINE) { if (firstLoad) { diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonAnnouncement.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonAnnouncement.java index b8e7fe76..9c7ee82e 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonAnnouncement.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonAnnouncement.java @@ -14,8 +14,6 @@ package app.fedilab.android.ui.fragment.timeline; * You should have received a copy of the GNU General Public License along with Fedilab; if not, * see . */ -import android.app.NotificationManager; -import android.content.Context; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -32,7 +30,6 @@ import app.fedilab.android.BaseMainActivity; import app.fedilab.android.R; import app.fedilab.android.client.entities.api.Announcement; import app.fedilab.android.databinding.FragmentPaginationBinding; -import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.ThemeHelper; import app.fedilab.android.ui.drawer.AnnouncementAdapter; import app.fedilab.android.viewmodel.mastodon.AnnouncementsVM; @@ -43,7 +40,6 @@ public class FragmentMastodonAnnouncement extends Fragment { private FragmentPaginationBinding binding; private AnnouncementsVM announcementsVM; - private AnnouncementAdapter announcementAdapter; public View onCreateView(@NonNull LayoutInflater inflater, @@ -65,13 +61,6 @@ public class FragmentMastodonAnnouncement extends Fragment { return root; } - @Override - public void onResume() { - super.onResume(); - NotificationManager notificationManager = (NotificationManager) requireActivity().getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(Helper.NOTIFICATION_USER_NOTIF); - } - /** * Intialize the view for announcements * @@ -92,7 +81,7 @@ public class FragmentMastodonAnnouncement extends Fragment { } - announcementAdapter = new AnnouncementAdapter(announcements); + AnnouncementAdapter announcementAdapter = new AnnouncementAdapter(announcements); LinearLayoutManager mLayoutManager = new LinearLayoutManager(requireActivity()); binding.recyclerView.setLayoutManager(mLayoutManager); binding.recyclerView.setAdapter(announcementAdapter); diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonNotification.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonNotification.java index 9e39d61c..70005b28 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonNotification.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonNotification.java @@ -20,9 +20,11 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.service.notification.StatusBarNotification; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -86,7 +88,7 @@ public class FragmentMastodonNotification extends Fragment implements Notificati } } }; - private String max_id, min_id, min_id_fetch_more; + private String max_id, min_id, min_id_fetch_more, max_id_fetch_more; private LinearLayoutManager mLayoutManager; private String instance, user_id; private ArrayList idOfAddedNotifications; @@ -179,8 +181,16 @@ public class FragmentMastodonNotification extends Fragment implements Notificati @Override public void onResume() { super.onResume(); - NotificationManager notificationManager = (NotificationManager) requireActivity().getApplicationContext().getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.cancel(Helper.NOTIFICATION_USER_NOTIF); + NotificationManager mNotificationManager = (NotificationManager) requireActivity().getSystemService(Context.NOTIFICATION_SERVICE); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && BaseMainActivity.currentAccount != null && BaseMainActivity.currentAccount.mastodon_account != null) { + for (StatusBarNotification statusBarNotification : mNotificationManager.getActiveNotifications()) { + if ((BaseMainActivity.currentAccount.mastodon_account.acct + "@" + BaseMainActivity.currentAccount.instance).equals(statusBarNotification.getGroupKey())) { + mNotificationManager.cancel(statusBarNotification.getId()); + } + } + } else { + mNotificationManager.cancelAll(); + } } @@ -306,8 +316,8 @@ public class FragmentMastodonNotification extends Fragment implements Notificati notificationsVM.getNotifications(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null, null, null, MastodonHelper.statusesPerCall(requireActivity()), excludeType, null) .observe(getViewLifecycleOwner(), this::initializeNotificationView); } else if (direction == FragmentMastodonTimeline.DIRECTION.BOTTOM) { - notificationsVM.getNotifications(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()), excludeType, null) - .observe(getViewLifecycleOwner(), notificationsBottom -> dealWithPagination(notificationsBottom, FragmentMastodonTimeline.DIRECTION.BOTTOM, false)); + notificationsVM.getNotifications(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, fetchingMissing ? max_id_fetch_more : max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()), excludeType, null) + .observe(getViewLifecycleOwner(), notificationsBottom -> dealWithPagination(notificationsBottom, FragmentMastodonTimeline.DIRECTION.BOTTOM, fetchingMissing)); } else if (direction == FragmentMastodonTimeline.DIRECTION.TOP) { notificationsVM.getNotifications(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, null, null, fetchingMissing ? min_id_fetch_more : min_id, MastodonHelper.statusesPerCall(requireActivity()), excludeType, null) .observe(getViewLifecycleOwner(), notificationsTop -> dealWithPagination(notificationsTop, FragmentMastodonTimeline.DIRECTION.TOP, fetchingMissing)); @@ -500,7 +510,7 @@ public class FragmentMastodonNotification extends Fragment implements Notificati } @Override - public void onClick(String min_id, String id) { + public void onClickMin(String min_id, String id) { //Fetch more has been pressed min_id_fetch_more = min_id; Notification notification = null; @@ -519,6 +529,25 @@ public class FragmentMastodonNotification extends Fragment implements Notificati route(FragmentMastodonTimeline.DIRECTION.TOP, true); } + @Override + public void onClickMax(String max_id, String id) { + //Fetch more has been pressed + max_id_fetch_more = max_id; + Notification notification = null; + int position = 0; + for (Notification currentNotification : this.notificationList) { + if (currentNotification.id.compareTo(id) == 0) { + notification = currentNotification; + break; + } + position++; + } + if (notification != null) { + this.notificationList.remove(position); + notificationAdapter.notifyItemRemoved(position); + } + route(FragmentMastodonTimeline.DIRECTION.BOTTOM, true); + } public enum NotificationTypeEnum { @SerializedName("ALL") diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTag.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTag.java index eca47184..e287dd40 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTag.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTag.java @@ -31,12 +31,14 @@ import java.util.List; import app.fedilab.android.BaseMainActivity; import app.fedilab.android.R; import app.fedilab.android.client.entities.api.Tag; +import app.fedilab.android.client.entities.app.Timeline; import app.fedilab.android.databinding.FragmentPaginationBinding; import app.fedilab.android.helper.Helper; import app.fedilab.android.helper.MastodonHelper; import app.fedilab.android.helper.ThemeHelper; import app.fedilab.android.ui.drawer.TagAdapter; import app.fedilab.android.viewmodel.mastodon.SearchVM; +import app.fedilab.android.viewmodel.mastodon.TimelinesVM; public class FragmentMastodonTag extends Fragment { @@ -45,11 +47,13 @@ public class FragmentMastodonTag extends Fragment { private FragmentPaginationBinding binding; private TagAdapter tagAdapter; private String search; + private Timeline.TimeLineEnum timelineType; public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { if (getArguments() != null) { search = getArguments().getString(Helper.ARG_SEARCH_KEYWORD, null); + timelineType = (Timeline.TimeLineEnum) getArguments().get(Helper.ARG_TIMELINE_TYPE); } binding = FragmentPaginationBinding.inflate(inflater, container, false); @@ -74,7 +78,7 @@ public class FragmentMastodonTag extends Fragment { * Router for timelines */ private void router() { - if (search != null) { + 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) .observe(getViewLifecycleOwner(), results -> { @@ -82,6 +86,12 @@ public class FragmentMastodonTag extends Fragment { initializeTagCommonView(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(), tags -> { + initializeTagCommonView(tags); + }); } } diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java index 974dbba5..5d308979 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentMastodonTimeline.java @@ -77,7 +77,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. private List statuses; private String search, searchCache; private Status statusReport; - private String max_id, min_id, min_id_fetch_more; + private String max_id, min_id, min_id_fetch_more, max_id_fetch_more; private StatusAdapter statusAdapter; private Timeline.TimeLineEnum timelineType; //Handle actions that can be done in other fragments @@ -286,11 +286,13 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. binding.loader.setVisibility(View.GONE); binding.noAction.setVisibility(View.GONE); binding.swipeContainer.setRefreshing(false); - binding.swipeContainer.setOnRefreshListener(() -> { - binding.swipeContainer.setRefreshing(true); - flagLoading = false; - route(DIRECTION.REFRESH, true); - }); + if (searchCache == null && timelineType != Timeline.TimeLineEnum.TREND_MESSAGE) { + binding.swipeContainer.setOnRefreshListener(() -> { + binding.swipeContainer.setRefreshing(true); + flagLoading = false; + route(DIRECTION.REFRESH, true); + }); + } if (statuses == null || statuses.statuses == null || statuses.statuses.size() == 0) { binding.noAction.setVisibility(View.VISIBLE); @@ -356,7 +358,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. binding.recyclerView.scrollToPosition(position); } - if (searchCache == null) { + if (searchCache == null && timelineType != Timeline.TimeLineEnum.TREND_MESSAGE) { binding.recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { @@ -453,9 +455,11 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. insertedPosition = insertStatus(statusReceived); if (insertedPosition != STATUS_PRESENT && insertedPosition != STATUS_AT_THE_BOTTOM) { numberInserted++; + //Find the first position of insertion, the initial id is set to STATUS_PRESENT if (initialInsertedPosition == STATUS_PRESENT) { initialInsertedPosition = insertedPosition; } + //If next statuses have a lower id, there are inserted before (normally, that should not happen) if (insertedPosition < initialInsertedPosition) { initialInsertedPosition = lastInsertedPosition; } @@ -464,12 +468,12 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. lastInsertedPosition = initialInsertedPosition + numberInserted; //lastInsertedPosition contains the position of the last inserted status //If there were no overlap for top status - if (fetchingMissing && insertedPosition != STATUS_PRESENT && insertedPosition != STATUS_AT_THE_BOTTOM && this.statuses.size() > insertedPosition) { + if (fetchingMissing && insertedPosition != STATUS_PRESENT && insertedPosition != STATUS_AT_THE_BOTTOM && this.statuses.size() > insertedPosition && numberInserted == MastodonHelper.statusesPerCall(requireActivity())) { Status statusFetchMore = new Status(); statusFetchMore.isFetchMore = true; statusFetchMore.id = Helper.generateString(); int insertAt; - if (direction == DIRECTION.REFRESH) { + if (direction == DIRECTION.REFRESH || direction == DIRECTION.BOTTOM) { insertAt = lastInsertedPosition; } else { insertAt = initialInsertedPosition; @@ -485,7 +489,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. } /** - * Insert a status if not yet in the timeline + * Insert a status if not yet in the timeline and returns its position of insertion * * @param statusReceived - Status coming from the api/db * @return int >= 0 | STATUS_PRESENT = -1 | STATUS_AT_THE_BOTTOM = -2 @@ -618,8 +622,8 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, true, false, false, null, null, null, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), this::initializeStatusesCommonView); } else if (direction == DIRECTION.BOTTOM) { - timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, true, false, false, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity())) - .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false)); + timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, true, false, false, fetchingMissing ? max_id_fetch_more : max_id, null, null, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, fetchingMissing)); } else if (direction == DIRECTION.TOP) { timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, true, false, false, null, null, fetchingMissing ? min_id_fetch_more : min_id, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP, fetchingMissing)); @@ -638,8 +642,8 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, false, true, false, null, null, null, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), this::initializeStatusesCommonView); } else if (direction == DIRECTION.BOTTOM) { - timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, false, true, false, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity())) - .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false)); + timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, false, true, false, fetchingMissing ? max_id_fetch_more : max_id, null, null, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, fetchingMissing)); } else if (direction == DIRECTION.TOP) { timelinesVM.getPublic(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, false, true, false, null, null, fetchingMissing ? min_id_fetch_more : min_id, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP, fetchingMissing)); @@ -724,8 +728,8 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. timelinesVM.getPublic(null, remoteInstance, true, false, false, null, null, null, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), this::initializeStatusesCommonView); } else if (direction == DIRECTION.BOTTOM) { - timelinesVM.getPublic(null, remoteInstance, true, false, false, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity())) - .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false)); + timelinesVM.getPublic(null, remoteInstance, true, false, false, fetchingMissing ? max_id_fetch_more : max_id, null, null, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, fetchingMissing)); } else if (direction == DIRECTION.TOP) { timelinesVM.getPublic(null, remoteInstance, true, false, false, null, null, fetchingMissing ? min_id_fetch_more : min_id, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP, fetchingMissing)); @@ -745,8 +749,8 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. timelinesVM.getList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, list_id, null, null, null, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), this::initializeStatusesCommonView); } else if (direction == DIRECTION.BOTTOM) { - timelinesVM.getList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, list_id, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity())) - .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false)); + timelinesVM.getList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, list_id, fetchingMissing ? max_id_fetch_more : max_id, null, null, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, fetchingMissing)); } else if (direction == DIRECTION.TOP) { timelinesVM.getList(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, list_id, null, null, fetchingMissing ? min_id_fetch_more : min_id, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP, fetchingMissing)); @@ -769,8 +773,8 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. timelinesVM.getHashTag(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, tagTimeline.name, false, tagTimeline.isART, tagTimeline.all, tagTimeline.any, tagTimeline.none, null, null, null, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), this::initializeStatusesCommonView); } else if (direction == DIRECTION.BOTTOM) { - timelinesVM.getHashTag(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, tagTimeline.name, false, tagTimeline.isART, tagTimeline.all, tagTimeline.any, tagTimeline.none, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity())) - .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false)); + timelinesVM.getHashTag(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, tagTimeline.name, false, tagTimeline.isART, tagTimeline.all, tagTimeline.any, tagTimeline.none, fetchingMissing ? max_id_fetch_more : max_id, null, null, MastodonHelper.statusesPerCall(requireActivity())) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, fetchingMissing)); } else if (direction == DIRECTION.TOP) { timelinesVM.getHashTag(BaseMainActivity.currentToken, BaseMainActivity.currentInstance, tagTimeline.name, false, tagTimeline.isART, tagTimeline.all, tagTimeline.any, tagTimeline.none, null, null, fetchingMissing ? min_id_fetch_more : min_id, MastodonHelper.statusesPerCall(requireActivity())) .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.TOP, fetchingMissing)); @@ -847,6 +851,19 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. } else { flagLoading = false; } + } else if (timelineType == Timeline.TimeLineEnum.TREND_MESSAGE) { + if (direction == null) { + timelinesVM.getStatusTrends(BaseMainActivity.currentToken, BaseMainActivity.currentInstance) + .observe(getViewLifecycleOwner(), statusesTrends -> { + Statuses statuses = new Statuses(); + statuses.statuses = new ArrayList<>(); + if (statusesTrends != null) { + statuses.statuses.addAll(statusesTrends); + } + statuses.pagination = new Pagination(); + initializeStatusesCommonView(statuses); + }); + } } }; mainHandler.post(myRunnable); @@ -905,21 +922,26 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. } } else if (direction == DIRECTION.BOTTOM) { - if (networkAvailable == BaseMainActivity.status.CONNECTED) { - //We first if we get results from cache - timelinesVM.getHomeCache(BaseMainActivity.currentInstance, BaseMainActivity.currentUserID, max_id, null, null) - .observe(getViewLifecycleOwner(), statusesBottomCache -> { - if (statusesBottomCache != null && statusesBottomCache.statuses != null && statusesBottomCache.statuses.size() > 0) { - dealWithPagination(statusesBottomCache, DIRECTION.BOTTOM, false); - } else { // If not, we fetch remotely - timelinesVM.getHome(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, false, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()), false) - .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false)); - } - }); + if (!fetchingMissing) { + if (networkAvailable == BaseMainActivity.status.CONNECTED) { + //We first if we get results from cache + timelinesVM.getHomeCache(BaseMainActivity.currentInstance, BaseMainActivity.currentUserID, max_id, null, null) + .observe(getViewLifecycleOwner(), statusesBottomCache -> { + if (statusesBottomCache != null && statusesBottomCache.statuses != null && statusesBottomCache.statuses.size() > 0) { + dealWithPagination(statusesBottomCache, DIRECTION.BOTTOM, false); + } else { // If not, we fetch remotely + timelinesVM.getHome(BaseMainActivity.currentInstance, BaseMainActivity.currentToken, false, max_id, null, null, MastodonHelper.statusesPerCall(requireActivity()), false) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false)); + } + }); + } else { + timelinesVM.getHomeCache(BaseMainActivity.currentInstance, BaseMainActivity.currentUserID, max_id, null, null) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false)); + } } else { - timelinesVM.getHomeCache(BaseMainActivity.currentInstance, BaseMainActivity.currentUserID, max_id, null, null) - .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, false)); + timelinesVM.getHomeCache(BaseMainActivity.currentInstance, BaseMainActivity.currentUserID, max_id_fetch_more, null, null) + .observe(getViewLifecycleOwner(), statusesBottom -> dealWithPagination(statusesBottom, DIRECTION.BOTTOM, true)); } } else if (direction == DIRECTION.TOP) { if (!fetchingMissing) { @@ -966,7 +988,7 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. } @Override - public void onClick(String min_id, String id) { + public void onClickMinId(String min_id, String id) { //Fetch more has been pressed min_id_fetch_more = min_id; Status status = null; @@ -985,6 +1007,25 @@ public class FragmentMastodonTimeline extends Fragment implements StatusAdapter. route(DIRECTION.TOP, true); } + @Override + public void onClickMaxId(String max_id, String id) { + max_id_fetch_more = max_id; + Status status = null; + int position = 0; + for (Status currentStatus : this.statuses) { + if (currentStatus.id.compareTo(id) == 0) { + status = currentStatus; + break; + } + position++; + } + if (status != null) { + this.statuses.remove(position); + statusAdapter.notifyItemRemoved(position); + } + route(DIRECTION.BOTTOM, true); + } + public enum DIRECTION { TOP, BOTTOM, diff --git a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentProfileTimeline.java b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentProfileTimeline.java index fafdc9f0..3ab5978a 100644 --- a/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentProfileTimeline.java +++ b/app/src/main/java/app/fedilab/android/ui/fragment/timeline/FragmentProfileTimeline.java @@ -57,8 +57,9 @@ public class FragmentProfileTimeline extends Fragment { binding.tabLayout.addTab(binding.tabLayout.newTab().setText(getString(R.string.media))); binding.tabLayout.setTabTextColors(ThemeHelper.getAttColor(requireActivity(), R.attr.mTextColor), ContextCompat.getColor(requireActivity(), R.color.cyanea_accent_dark_reference)); binding.tabLayout.setTabIconTint(ThemeHelper.getColorStateList(requireActivity())); - binding.viewpager.setAdapter(new FedilabProfilePageAdapter(requireActivity(), account)); + binding.viewpager.setAdapter(new FedilabProfilePageAdapter(getChildFragmentManager(), account)); binding.viewpager.setOffscreenPageLimit(3); + binding.viewpager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(binding.tabLayout)); binding.tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { @Override public void onTabSelected(TabLayout.Tab tab) { diff --git a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabPageAdapter.java b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabPageAdapter.java index fade2a00..cda3578d 100644 --- a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabPageAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabPageAdapter.java @@ -16,11 +16,12 @@ package app.fedilab.android.ui.pageadapter; import android.os.Bundle; +import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; import app.fedilab.android.BaseMainActivity; import app.fedilab.android.client.entities.app.BottomMenu; @@ -33,24 +34,46 @@ import app.fedilab.android.ui.fragment.timeline.FragmentMastodonConversation; import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; import app.fedilab.android.ui.fragment.timeline.FragmentNotificationContainer; -public class FedilabPageAdapter extends FragmentStateAdapter { +@SuppressWarnings("deprecation") +public class FedilabPageAdapter extends FragmentStatePagerAdapter { public static final int BOTTOM_TIMELINE_COUNT = 5; //home, local, public, notification, DM private final Pinned pinned; private final BottomMenu bottomMenu; private final int toRemove; + private Fragment mCurrentFragment; - public FedilabPageAdapter(BaseMainActivity activity, FragmentActivity fa, Pinned pinned, BottomMenu bottomMenu) { - super(fa); + public FedilabPageAdapter(BaseMainActivity activity, FragmentManager fm, Pinned pinned, BottomMenu bottomMenu) { + super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); this.pinned = pinned; this.bottomMenu = bottomMenu; toRemove = PinnedTimelineHelper.itemToRemoveInBottomMenu(activity); } + public Fragment getCurrentFragment() { + return mCurrentFragment; + } + + @Override + public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + if (getCurrentFragment() != object) { + mCurrentFragment = ((Fragment) object); + } + super.setPrimaryItem(container, position, object); + } + + @Override + public int getCount() { + if (pinned != null && pinned.pinnedTimelines != null) { + return pinned.pinnedTimelines.size() + BOTTOM_TIMELINE_COUNT - toRemove; + } else { + return BOTTOM_TIMELINE_COUNT - toRemove; + } + } @NonNull @Override - public Fragment createFragment(int position) { + public Fragment getItem(int position) { FragmentMastodonTimeline fragment = new FragmentMastodonTimeline(); Bundle bundle = new Bundle(); //Position 3 is for notifications @@ -95,12 +118,9 @@ public class FedilabPageAdapter extends FragmentStateAdapter { } @Override - public int getItemCount() { - if (pinned != null && pinned.pinnedTimelines != null) { - return pinned.pinnedTimelines.size() + BOTTOM_TIMELINE_COUNT - toRemove; - } else { - return BOTTOM_TIMELINE_COUNT - toRemove; - } - } + public int getItemPosition(@NonNull Object object) { + // POSITION_NONE makes it possible to reload the PagerAdapter + return POSITION_NONE; + } } diff --git a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfilePageAdapter.java b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfilePageAdapter.java index 361f6e89..5e32fbd6 100644 --- a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfilePageAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfilePageAdapter.java @@ -15,35 +15,42 @@ package app.fedilab.android.ui.pageadapter; * see . */ import android.os.Bundle; +import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; import app.fedilab.android.client.entities.api.Account; import app.fedilab.android.client.entities.app.Timeline; import app.fedilab.android.helper.Helper; import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; -public class FedilabProfilePageAdapter extends FragmentStateAdapter { - +public class FedilabProfilePageAdapter extends FragmentStatePagerAdapter { private final Account account; + private Fragment mCurrentFragment; - public FedilabProfilePageAdapter(FragmentActivity fa, Account account) { - super(fa); + public FedilabProfilePageAdapter(FragmentManager fm, Account account) { + super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); this.account = account; } + public Fragment getCurrentFragment() { + return mCurrentFragment; + } @Override - public int getItemCount() { - return 3; + public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + if (getCurrentFragment() != object) { + mCurrentFragment = ((Fragment) object); + } + super.setPrimaryItem(container, position, object); } @NonNull @Override - public Fragment createFragment(int position) { + public Fragment getItem(int position) { Bundle bundle = new Bundle(); bundle.putString(Helper.ARG_VIEW_MODEL_KEY, "FEDILAB_" + position); switch (position) { @@ -77,5 +84,8 @@ public class FedilabProfilePageAdapter extends FragmentStateAdapter { } } - -} + @Override + public int getCount() { + return 3; + } +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfileTLPageAdapter.java b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfileTLPageAdapter.java index 7b8acd81..30542d21 100644 --- a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfileTLPageAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabProfileTLPageAdapter.java @@ -15,11 +15,12 @@ package app.fedilab.android.ui.pageadapter; * see . */ import android.os.Bundle; +import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; import app.fedilab.android.client.entities.api.Account; import app.fedilab.android.helper.Helper; @@ -27,21 +28,31 @@ import app.fedilab.android.ui.fragment.timeline.FragmentMastodonAccount; import app.fedilab.android.ui.fragment.timeline.FragmentMastodonTimeline; import app.fedilab.android.ui.fragment.timeline.FragmentProfileTimeline; -public class FedilabProfileTLPageAdapter extends FragmentStateAdapter { - +public class FedilabProfileTLPageAdapter extends FragmentStatePagerAdapter { private final Account account; + private Fragment mCurrentFragment; - public FedilabProfileTLPageAdapter(FragmentActivity fa, Account account) { - super(fa); + public FedilabProfileTLPageAdapter(FragmentManager fm, Account account) { + super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); this.account = account; } + public Fragment getCurrentFragment() { + return mCurrentFragment; + } + + @Override + public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + if (getCurrentFragment() != object) { + mCurrentFragment = ((Fragment) object); + } + super.setPrimaryItem(container, position, object); + } @NonNull @Override - public Fragment createFragment(int position) { + public Fragment getItem(int position) { switch (position) { - case 0: FragmentProfileTimeline fragmentProfileTimeline = new FragmentProfileTimeline(); Bundle bundle = new Bundle(); @@ -63,10 +74,9 @@ public class FedilabProfileTLPageAdapter extends FragmentStateAdapter { } @Override - public int getItemCount() { + public int getCount() { return 3; } - public enum follow_type { FOLLOWING, FOLLOWERS diff --git a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabScheduledPageAdapter.java b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabScheduledPageAdapter.java index 6b758bed..916860bd 100644 --- a/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabScheduledPageAdapter.java +++ b/app/src/main/java/app/fedilab/android/ui/pageadapter/FedilabScheduledPageAdapter.java @@ -15,26 +15,39 @@ package app.fedilab.android.ui.pageadapter; * see . */ import android.os.Bundle; +import android.view.ViewGroup; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; -import androidx.fragment.app.FragmentActivity; -import androidx.viewpager2.adapter.FragmentStateAdapter; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentStatePagerAdapter; import app.fedilab.android.client.entities.app.Timeline; import app.fedilab.android.helper.Helper; import app.fedilab.android.ui.fragment.timeline.FragmentScheduled; -public class FedilabScheduledPageAdapter extends FragmentStateAdapter { +public class FedilabScheduledPageAdapter extends FragmentStatePagerAdapter { + private Fragment mCurrentFragment; + public FedilabScheduledPageAdapter(FragmentManager fm) { + super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + } + + public Fragment getCurrentFragment() { + return mCurrentFragment; + } - public FedilabScheduledPageAdapter(FragmentActivity fa) { - super(fa); + @Override + public void setPrimaryItem(@NonNull ViewGroup container, int position, @NonNull Object object) { + if (getCurrentFragment() != object) { + mCurrentFragment = ((Fragment) object); + } + super.setPrimaryItem(container, position, object); } @NonNull @Override - public Fragment createFragment(int position) { + public Fragment getItem(int position) { Bundle bundle = new Bundle(); bundle.putString(Helper.ARG_VIEW_MODEL_KEY, "FEDILAB_" + position); FragmentScheduled fragmentScheduled = new FragmentScheduled(); @@ -53,7 +66,7 @@ public class FedilabScheduledPageAdapter extends FragmentStateAdapter { } @Override - public int getItemCount() { + public int getCount() { return 3; } -} +} \ No newline at end of file diff --git a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java index 4ed5ad2c..a04c2a0d 100644 --- a/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java +++ b/app/src/main/java/app/fedilab/android/viewmodel/mastodon/TimelinesVM.java @@ -38,6 +38,7 @@ import app.fedilab.android.client.entities.api.MastodonList; import app.fedilab.android.client.entities.api.Pagination; import app.fedilab.android.client.entities.api.Status; import app.fedilab.android.client.entities.api.Statuses; +import app.fedilab.android.client.entities.api.Tag; import app.fedilab.android.client.entities.app.BaseAccount; import app.fedilab.android.client.entities.app.StatusCache; import app.fedilab.android.client.entities.app.StatusDraft; @@ -75,6 +76,8 @@ public class TimelinesVM extends AndroidViewModel { private MutableLiveData mastodonListMutableLiveData; private MutableLiveData> mastodonListListMutableLiveData; private MutableLiveData markerMutableLiveData; + private MutableLiveData> statusListMutableLiveData; + private MutableLiveData> tagListMutableLiveData; public TimelinesVM(@NonNull Application application) { super(application); @@ -107,6 +110,56 @@ public class TimelinesVM extends AndroidViewModel { return retrofit.create(MastodonTimelinesService.class); } + public LiveData> getStatusTrends(String token, @NonNull String instance) { + MastodonTimelinesService mastodonTimelinesService = init(instance); + statusListMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call> publicTlCall = mastodonTimelinesService.getStatusTrends(token); + List statusList = null; + if (publicTlCall != null) { + try { + Response> publicTlResponse = publicTlCall.execute(); + if (publicTlResponse.isSuccessful()) { + statusList = publicTlResponse.body(); + statusList = SpannableHelper.convertStatus(getApplication().getApplicationContext(), statusList); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalStatusList = statusList; + Runnable myRunnable = () -> statusListMutableLiveData.setValue(finalStatusList); + mainHandler.post(myRunnable); + }).start(); + return statusListMutableLiveData; + } + + + public LiveData> getTagsTrends(String token, @NonNull String instance) { + MastodonTimelinesService mastodonTimelinesService = init(instance); + tagListMutableLiveData = new MutableLiveData<>(); + new Thread(() -> { + Call> publicTlCall = mastodonTimelinesService.getTagTrends(token); + List tagList = null; + if (publicTlCall != null) { + try { + Response> publicTlResponse = publicTlCall.execute(); + if (publicTlResponse.isSuccessful()) { + tagList = publicTlResponse.body(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + Handler mainHandler = new Handler(Looper.getMainLooper()); + List finalTagList = tagList; + Runnable myRunnable = () -> tagListMutableLiveData.setValue(finalTagList); + mainHandler.post(myRunnable); + }).start(); + return tagListMutableLiveData; + } + /** * Public timeline * diff --git a/app/src/main/res/drawable/ic_baseline_filter_center_focus_24.xml b/app/src/main/res/drawable/ic_baseline_filter_center_focus_24.xml new file mode 100644 index 00000000..757e1433 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_filter_center_focus_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_trending_up_24.xml b/app/src/main/res/drawable/ic_baseline_trending_up_24.xml new file mode 100644 index 00000000..04a5a863 --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_trending_up_24.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/drawable/ic_fetch_more_arrow_downward.xml b/app/src/main/res/drawable/ic_fetch_more_arrow_downward.xml new file mode 100644 index 00000000..2a93c310 --- /dev/null +++ b/app/src/main/res/drawable/ic_fetch_more_arrow_downward.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_fetch_more_arrow_upward.xml b/app/src/main/res/drawable/ic_fetch_more_arrow_upward.xml new file mode 100644 index 00000000..46c34a0d --- /dev/null +++ b/app/src/main/res/drawable/ic_fetch_more_arrow_upward.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_edit_image.xml b/app/src/main/res/layout/activity_edit_image.xml index b671da59..8e04a766 100644 --- a/app/src/main/res/layout/activity_edit_image.xml +++ b/app/src/main/res/layout/activity_edit_image.xml @@ -16,14 +16,27 @@ android:orientation="horizontal" app:layout_constraintGuide_end="?attr/actionBarSize" /> - + android:id="@+id/container_image" + app:layout_constraintTop_toTopOf="parent"> + + + + + + - - - - - + android:layout_marginBottom="?attr/actionBarSize" + app:defaultNavHost="true" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> - + android:paddingTop="?attr/actionBarSize"> - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/account_un"> - - - - - - - - - - - - - - - - - - - - + + + + + android:textColor="@color/red" + android:visibility="gone" /> - + android:textSize="16sp" + android:visibility="gone" /> - + + + + + + + + + + + + android:gravity="center_horizontal" + android:orientation="horizontal"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - - - - - - - - - - - + + - - - - - - - - - - + + - - - - - - - - - - - - - + android:layout_marginStart="3dp" + android:layout_marginEnd="3dp" + android:background="@drawable/blue_border" + android:singleLine="true" + android:text="@string/request_sent" + android:textColor="@color/mastodonC4" + android:visibility="gone" /> + + + + + + + @@ -510,6 +345,7 @@ android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:backgroundTint="?colorPrimaryDark" + app:layout_scrollFlags="scroll|enterAlways" app:layout_collapseMode="pin"> - - - - + - - + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_scheduled.xml b/app/src/main/res/layout/activity_scheduled.xml index 8626ae39..e3baabf6 100644 --- a/app/src/main/res/layout/activity_scheduled.xml +++ b/app/src/main/res/layout/activity_scheduled.xml @@ -63,7 +63,7 @@ - - - - - - > + android:layout_height="wrap_content" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_trends.xml b/app/src/main/res/layout/activity_trends.xml new file mode 100644 index 00000000..75ea988a --- /dev/null +++ b/app/src/main/res/layout/activity_trends.xml @@ -0,0 +1,38 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_fetch_more.xml b/app/src/main/res/layout/drawer_fetch_more.xml index 70736b25..2f20a5d2 100644 --- a/app/src/main/res/layout/drawer_fetch_more.xml +++ b/app/src/main/res/layout/drawer_fetch_more.xml @@ -1,9 +1,44 @@ - \ No newline at end of file + android:orientation="horizontal" + android:paddingHorizontal="6dp"> + + + + + + + + diff --git a/app/src/main/res/layout/drawer_field.xml b/app/src/main/res/layout/drawer_field.xml new file mode 100644 index 00000000..ddfa1bbc --- /dev/null +++ b/app/src/main/res/layout/drawer_field.xml @@ -0,0 +1,37 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_profile_timelines.xml b/app/src/main/res/layout/fragment_profile_timelines.xml index c878262d..225f4072 100644 --- a/app/src/main/res/layout/fragment_profile_timelines.xml +++ b/app/src/main/res/layout/fragment_profile_timelines.xml @@ -27,7 +27,7 @@ app:tabGravity="fill" app:tabMaxWidth="0dp" /> - diff --git a/app/src/main/res/menu/activity_main_drawer.xml b/app/src/main/res/menu/activity_main_drawer.xml index cd0a799f..4ab3fc40 100644 --- a/app/src/main/res/menu/activity_main_drawer.xml +++ b/app/src/main/res/menu/activity_main_drawer.xml @@ -35,6 +35,12 @@ android:title="@string/action_announcements" android:visible="true" /> + + Пароль Ел. пошта Облікові записи - Передмухнуте + Повідомлення Мітки Зберегти Екземпляр Екземпляр: mastodon.social Тепер працює з обліковим записом %1$s Додати обліковий запис - Вміст дмуху скопійований в буфер обміну - The URL of the toot has been copied to the clipboard + Вміст повідомлення скопійований в буфер обміну + URL-адресу повідомлення скопійовано в буфер обміну Камера Видалити все Запланувати @@ -57,17 +57,17 @@ Запити на підписку Налаштування Надіслати ел. лист - Заплановані дмухи + Заплановані повідомлення Наведена нижче інформація може відображати профіль користувача не повністю. Вставити смайли Застосунок не збирає emojis на даний момент. Are you sure you want to logout @%1$s@%2$s? Немає повідомлень для відображення - Додати цей дмух до обраного? - Видалити цей дмух з обраного? - Передмухнути? - Зняти передмух? + Додати це повідомлення до обраного\? + Видалити це повідомлення з обраного\? + Підштовхнути це повідомлення\? + Не підштовхувати це повідомлення\? Заглушити Заблокувати Повідомити про порушення @@ -130,8 +130,8 @@ Сталася помилка під час вибору медіа-файлу! Видалити цей медіа-файл? - Ваш дмух пустий! - Повідомлення було відправлено! + Ваше повідомлення порожнє! + Повідомлення відправлено! Чутливий вміст? Чернетки відсутні! Оберіть обліковий запис @@ -151,14 +151,15 @@ Немає облікових записів для відображення Запит на відмову від підписки - Дмухи \n %1$s + Повідомлення +\n %1$s Підписані на \n %1$s Стежать за вами \n %1$s Відхилити - Немає запланованих дмухів для показу! - Видалити запланований дмух? - Дмух було заплановано! + Немає запланованих повідомлень для показу! + Видалити заплановане повідомлення\? + Повідомлення заплановано! Запланована дата має бути більшою за поточну годину! Час для приглушення має бути більшим за одну хвилину. @@ -184,10 +185,10 @@ Обліковий запис більше не приглушено! На обліковий запис підписалися! Ви більше не підписані на обліковий запис! - Передмухнуто! - Більше не передмухнуто! - Дмух додано до обраного! - Дмух видалено з обраного! + Повідомлення підштовхнуте! + Повідомлення більше не підштовхнуте! + Повідомлення додано до обраного! + Повідомлення видалено з обраного! Вибачте, виникла помилка! Сталася помилка! Екземпляр не повернув код авторизації! Домен екземпляру не валідний! @@ -196,7 +197,7 @@ Ніяка дія не може відбутися Виникла помилка під час перекладу! - Число дмухів за одиницю завантаження + Кількість повідомлень за одиницю завантаження Вимкнути GIF аватари Сповіщати, якщо хтось підписується Сповіщати, якщо хтось передмухнув ваш статус @@ -275,7 +276,7 @@ Порт Логін Пароль - Додати деталі гудку при поділитися + Додати деталі повідомлення при поширюванні Підтримати додаток на Liberapay Помилка у регулярному виразі! No timelines was found on this instance! @@ -311,7 +312,7 @@ New Favourite New Mention Poll Ended - Toots Backup + Запасне копіювання повідомлень New posts Медіа завантаження Оберіть сигнал @@ -323,11 +324,11 @@ Peertube екземпляр Use Emoji One Information - Display previews in all toots + Відображати дрібнообрази в усіх повідомленнях The account id has been copied in the clipboard! Change the language - Truncate long toots - Truncate toots over \'x\' lines. Zero means disabled. + Обтинати довгі повідомлення + Обтинати повідомлення більші, ніж \'x\' рядків. Нуль означає вимикання. Display more Display less The tag already exists! @@ -373,8 +374,8 @@ Category Description Share - Toots (Server) - Toots (Device) + Повідомлення (сервер) + Повідомлення (пристрій) Timelines Interface Contacts @@ -396,7 +397,7 @@ end at %s Vote A poll you have voted in has ended - A poll you tooted has ended + Опубліковане вами опитування закінчилося Categories Move timeline Hide timeline @@ -531,7 +532,7 @@ Background color of posts in timelines Reset colors Tap here to reset all your custom colors - Reset + Скинути Icons Color of bottom icons in timelines Logo of the instance diff --git a/app/src/main/res/xml/pref_interface.xml b/app/src/main/res/xml/pref_interface.xml index 9b78b0bb..db475113 100644 --- a/app/src/main/res/xml/pref_interface.xml +++ b/app/src/main/res/xml/pref_interface.xml @@ -27,7 +27,7 @@ app:title="@string/embedded_browser" /> + path="Android/data/app.fedilab.android/files/Pictures" />