Implement media tab (#430)
This commit is contained in:
parent
a934582025
commit
dc1a60cc12
18 changed files with 597 additions and 189 deletions
|
@ -1,4 +1,5 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
compileSdkVersion 26
|
||||
|
@ -58,6 +59,7 @@ dependencies {
|
|||
compile "com.mikepenz:google-material-typeface:3.0.1.0.original@aar"
|
||||
compile "com.theartofdev.edmodo:android-image-cropper:2.5.1"
|
||||
compile 'com.evernote:android-job:1.2.0'
|
||||
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
|
||||
|
||||
//room
|
||||
compile "android.arch.persistence.room:runtime:1.0.0-rc1"
|
||||
|
@ -67,5 +69,9 @@ dependencies {
|
|||
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
|
||||
exclude group: 'com.android.support', module: 'support-annotations'
|
||||
})
|
||||
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
|
||||
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
|
@ -41,6 +41,7 @@ import android.util.Log;
|
|||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
@ -51,8 +52,8 @@ import com.keylesspalace.tusky.interfaces.ActionButtonActivity;
|
|||
import com.keylesspalace.tusky.interfaces.LinkListener;
|
||||
import com.keylesspalace.tusky.pager.AccountPagerAdapter;
|
||||
import com.keylesspalace.tusky.receiver.TimelineReceiver;
|
||||
import com.keylesspalace.tusky.util.LinkHelper;
|
||||
import com.keylesspalace.tusky.util.Assert;
|
||||
import com.keylesspalace.tusky.util.LinkHelper;
|
||||
import com.keylesspalace.tusky.util.ThemeUtils;
|
||||
import com.pkmmte.view.CircularImageView;
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
@ -66,7 +67,7 @@ import retrofit2.Call;
|
|||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class AccountActivity extends BaseActivity implements ActionButtonActivity {
|
||||
public final class AccountActivity extends BaseActivity implements ActionButtonActivity {
|
||||
private static final String TAG = "AccountActivity"; // logging tag
|
||||
|
||||
private enum FollowState {
|
||||
|
@ -81,6 +82,7 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit
|
|||
private boolean muting;
|
||||
private boolean isSelf;
|
||||
private Account loadedAccount;
|
||||
|
||||
private CircularImageView avatar;
|
||||
private ImageView header;
|
||||
private FloatingActionButton floatingBtn;
|
||||
|
@ -89,6 +91,10 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit
|
|||
private TabLayout tabLayout;
|
||||
private ImageView accountLockedView;
|
||||
private View container;
|
||||
private TextView followersTextView;
|
||||
private TextView followingTextView;
|
||||
private TextView statusesTextView;
|
||||
|
||||
private boolean hideFab;
|
||||
private int oldOffset;
|
||||
|
||||
|
@ -105,6 +111,9 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit
|
|||
tabLayout = findViewById(R.id.tab_layout);
|
||||
accountLockedView = findViewById(R.id.account_locked);
|
||||
container = findViewById(R.id.activity_account);
|
||||
followersTextView = findViewById(R.id.followers_tv);
|
||||
followingTextView = findViewById(R.id.following_tv);
|
||||
statusesTextView = findViewById(R.id.statuses_btn);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
accountId = savedInstanceState.getString("accountId");
|
||||
|
@ -139,7 +148,8 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit
|
|||
AppBarLayout appBarLayout = findViewById(R.id.account_app_bar_layout);
|
||||
final CollapsingToolbarLayout collapsingToolbar = findViewById(R.id.collapsing_toolbar);
|
||||
appBarLayout.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
|
||||
@AttrRes int priorAttribute = R.attr.account_toolbar_icon_tint_uncollapsed;
|
||||
@AttrRes
|
||||
int priorAttribute = R.attr.account_toolbar_icon_tint_uncollapsed;
|
||||
|
||||
@Override
|
||||
public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
|
||||
|
@ -166,7 +176,7 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit
|
|||
ThemeUtils.setDrawableTint(context, toolbar.getOverflowIcon(), attribute);
|
||||
}
|
||||
|
||||
if(floatingBtn != null && hideFab && !isSelf && !blocking) {
|
||||
if (floatingBtn != null && hideFab && !isSelf && !blocking) {
|
||||
if (verticalOffset > oldOffset) {
|
||||
floatingBtn.show();
|
||||
}
|
||||
|
@ -196,28 +206,62 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit
|
|||
}
|
||||
|
||||
// Setup the tabs and timeline pager.
|
||||
AccountPagerAdapter adapter = new AccountPagerAdapter(getSupportFragmentManager(), this,
|
||||
AccountPagerAdapter adapter = new AccountPagerAdapter(getSupportFragmentManager(),
|
||||
accountId);
|
||||
String[] pageTitles = {
|
||||
getString(R.string.title_statuses),
|
||||
getString(R.string.title_follows),
|
||||
getString(R.string.title_followers)
|
||||
getString(R.string.title_media)
|
||||
};
|
||||
adapter.setPageTitles(pageTitles);
|
||||
ViewPager viewPager = findViewById(R.id.pager);
|
||||
final ViewPager viewPager = findViewById(R.id.pager);
|
||||
int pageMargin = getResources().getDimensionPixelSize(R.dimen.tab_page_margin);
|
||||
viewPager.setPageMargin(pageMargin);
|
||||
Drawable pageMarginDrawable = ThemeUtils.getDrawable(this, R.attr.tab_page_margin_drawable,
|
||||
R.drawable.tab_page_margin_dark);
|
||||
viewPager.setPageMarginDrawable(pageMarginDrawable);
|
||||
viewPager.setAdapter(adapter);
|
||||
viewPager.setOffscreenPageLimit(0);
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
for (int i = 0; i < tabLayout.getTabCount(); i++) {
|
||||
TabLayout.Tab tab = tabLayout.getTabAt(i);
|
||||
if (tab != null) {
|
||||
tab.setCustomView(adapter.getTabView(i, tabLayout));
|
||||
|
||||
View.OnClickListener accountListClickListener = new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
AccountListActivity.Type type;
|
||||
switch (v.getId()) {
|
||||
case R.id.followers_tv:
|
||||
type = AccountListActivity.Type.FOLLOWERS;
|
||||
break;
|
||||
case R.id.following_tv:
|
||||
type = AccountListActivity.Type.FOLLOWING;
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
Intent intent = AccountListActivity.newIntent(AccountActivity.this, type,
|
||||
accountId);
|
||||
startActivity(intent);
|
||||
}
|
||||
};
|
||||
followersTextView.setOnClickListener(accountListClickListener);
|
||||
followingTextView.setOnClickListener(accountListClickListener);
|
||||
|
||||
statusesTextView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
// Make nice ripple effect on tab
|
||||
|
||||
//noinspection ConstantConditions
|
||||
tabLayout.getTabAt(0).select();
|
||||
final View poorTabView = ((ViewGroup) tabLayout.getChildAt(0)).getChildAt(0);
|
||||
poorTabView.setPressed(true);
|
||||
tabLayout.postDelayed(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
poorTabView.setPressed(false);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -307,10 +351,16 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit
|
|||
// Add counts to the tabs in the TabLayout.
|
||||
String[] counts = {
|
||||
nf.format(Integer.parseInt(account.statusesCount)),
|
||||
nf.format(Integer.parseInt(account.followingCount)),
|
||||
nf.format(Integer.parseInt(account.followersCount)),
|
||||
""
|
||||
};
|
||||
|
||||
long followersCount = Long.parseLong(account.followersCount);
|
||||
long followingCount = Long.parseLong(account.followingCount);
|
||||
long statusesCount = Long.parseLong(account.statusesCount);
|
||||
followersTextView.setText(getString(R.string.title_x_followers, followersCount));
|
||||
followingTextView.setText(getString(R.string.title_x_following, followingCount));
|
||||
statusesTextView.setText(getString(R.string.title_x_statuses, statusesCount));
|
||||
|
||||
for (int i = 0; i < tabLayout.getTabCount(); i++) {
|
||||
TabLayout.Tab tab = tabLayout.getTabAt(i);
|
||||
if (tab != null) {
|
||||
|
@ -397,7 +447,7 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit
|
|||
private void updateButtons() {
|
||||
invalidateOptionsMenu();
|
||||
|
||||
if(!isSelf && !blocking) {
|
||||
if (!isSelf && !blocking) {
|
||||
floatingBtn.show();
|
||||
followBtn.setVisibility(View.VISIBLE);
|
||||
|
||||
|
@ -449,9 +499,11 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit
|
|||
private String getFollowAction() {
|
||||
switch (followState) {
|
||||
default:
|
||||
case NOT_FOLLOWING: return getString(R.string.action_follow);
|
||||
case NOT_FOLLOWING:
|
||||
return getString(R.string.action_follow);
|
||||
case REQUESTED:
|
||||
case FOLLOWING: return getString(R.string.action_unfollow);
|
||||
case FOLLOWING:
|
||||
return getString(R.string.action_unfollow);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -517,8 +569,14 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit
|
|||
|
||||
Assert.expect(followState != FollowState.REQUESTED);
|
||||
switch (followState) {
|
||||
case NOT_FOLLOWING: { mastodonApi.followAccount(id).enqueue(cb); break; }
|
||||
case FOLLOWING: { mastodonApi.unfollowAccount(id).enqueue(cb); break; }
|
||||
case NOT_FOLLOWING: {
|
||||
mastodonApi.followAccount(id).enqueue(cb);
|
||||
break;
|
||||
}
|
||||
case FOLLOWING: {
|
||||
mastodonApi.unfollowAccount(id).enqueue(cb);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -655,7 +713,6 @@ public class AccountActivity extends BaseActivity implements ActionButtonActivit
|
|||
.mentionedUsernames(Collections.singleton(loadedAccount.username))
|
||||
.build(this);
|
||||
startActivity(intent);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -15,8 +15,10 @@
|
|||
|
||||
package com.keylesspalace.tusky;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentTransaction;
|
||||
|
@ -26,11 +28,27 @@ import android.view.MenuItem;
|
|||
|
||||
import com.keylesspalace.tusky.fragment.AccountListFragment;
|
||||
|
||||
public class AccountListActivity extends BaseActivity {
|
||||
public final class AccountListActivity extends BaseActivity {
|
||||
|
||||
private static final String TYPE_EXTRA = "type";
|
||||
private static final String ARG_EXTRA = "arg";
|
||||
|
||||
public static Intent newIntent(@NonNull Context context, @NonNull Type type,
|
||||
@Nullable String argument) {
|
||||
Intent intent = new Intent(context, AccountListActivity.class);
|
||||
intent.putExtra(TYPE_EXTRA, type);
|
||||
if (argument != null) {
|
||||
intent.putExtra(ARG_EXTRA, argument);
|
||||
}
|
||||
return intent;
|
||||
}
|
||||
|
||||
enum Type {
|
||||
BLOCKS,
|
||||
MUTES,
|
||||
FOLLOW_REQUESTS,
|
||||
FOLLOWERS,
|
||||
FOLLOWING,
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -51,29 +69,55 @@ public class AccountListActivity extends BaseActivity {
|
|||
ActionBar bar = getSupportActionBar();
|
||||
if (bar != null) {
|
||||
switch (type) {
|
||||
case BLOCKS: { bar.setTitle(getString(R.string.title_blocks)); break; }
|
||||
case MUTES: { bar.setTitle(getString(R.string.title_mutes)); break; }
|
||||
case BLOCKS: {
|
||||
bar.setTitle(getString(R.string.title_blocks));
|
||||
break;
|
||||
}
|
||||
case MUTES: {
|
||||
bar.setTitle(getString(R.string.title_mutes));
|
||||
break;
|
||||
}
|
||||
case FOLLOW_REQUESTS: {
|
||||
bar.setTitle(getString(R.string.title_follow_requests));
|
||||
break;
|
||||
}
|
||||
case FOLLOWERS:
|
||||
bar.setTitle(getString(R.string.title_followers));
|
||||
break;
|
||||
case FOLLOWING:
|
||||
bar.setTitle(getString(R.string.title_follows));
|
||||
}
|
||||
bar.setDisplayHomeAsUpEnabled(true);
|
||||
bar.setDisplayShowHomeEnabled(true);
|
||||
}
|
||||
|
||||
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
|
||||
AccountListFragment.Type fragmentType;
|
||||
AccountListFragment fragment;
|
||||
switch (type) {
|
||||
default:
|
||||
case BLOCKS: { fragmentType = AccountListFragment.Type.BLOCKS; break; }
|
||||
case MUTES: { fragmentType = AccountListFragment.Type.MUTES; break; }
|
||||
case BLOCKS: {
|
||||
fragment = AccountListFragment.newInstance(AccountListFragment.Type.BLOCKS);
|
||||
break;
|
||||
}
|
||||
case MUTES: {
|
||||
fragment = AccountListFragment.newInstance(AccountListFragment.Type.MUTES);
|
||||
break;
|
||||
}
|
||||
case FOLLOWERS: {
|
||||
String argument = intent.getStringExtra(ARG_EXTRA);
|
||||
fragment = AccountListFragment.newInstance(AccountListFragment.Type.FOLLOWERS, argument);
|
||||
break;
|
||||
}
|
||||
case FOLLOWING: {
|
||||
String argument = intent.getStringExtra(ARG_EXTRA);
|
||||
fragment = AccountListFragment.newInstance(AccountListFragment.Type.FOLLOWS, argument);
|
||||
break;
|
||||
}
|
||||
case FOLLOW_REQUESTS: {
|
||||
fragmentType = AccountListFragment.Type.FOLLOW_REQUESTS;
|
||||
fragment = AccountListFragment.newInstance(AccountListFragment.Type.FOLLOW_REQUESTS);
|
||||
break;
|
||||
}
|
||||
}
|
||||
Fragment fragment = AccountListFragment.newInstance(fragmentType);
|
||||
fragmentTransaction.replace(R.id.fragment_container, fragment);
|
||||
fragmentTransaction.commit();
|
||||
}
|
||||
|
|
|
@ -182,7 +182,7 @@ public class ReportActivity extends BaseActivity {
|
|||
onFetchStatusesFailure((Exception) t);
|
||||
}
|
||||
};
|
||||
mastodonApi.accountStatuses(accountId, null, null, null)
|
||||
mastodonApi.accountStatuses(accountId, null, null, null, null)
|
||||
.enqueue(callback);
|
||||
}
|
||||
|
||||
|
|
|
@ -61,6 +61,9 @@ import retrofit2.Response;
|
|||
public class AccountListFragment extends BaseFragment implements AccountActionListener {
|
||||
private static final String TAG = "AccountList"; // logging tag
|
||||
|
||||
public AccountListFragment() {
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
FOLLOWS,
|
||||
FOLLOWERS,
|
||||
|
@ -75,13 +78,11 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
|||
private RecyclerView recyclerView;
|
||||
private EndlessOnScrollListener scrollListener;
|
||||
private AccountAdapter adapter;
|
||||
private TabLayout.OnTabSelectedListener onTabSelectedListener;
|
||||
private MastodonApi api;
|
||||
private boolean bottomLoading;
|
||||
private int bottomFetches;
|
||||
private boolean topLoading;
|
||||
private int topFetches;
|
||||
private boolean hideFab;
|
||||
|
||||
public static AccountListFragment newInstance(Type type) {
|
||||
Bundle arguments = new Bundle();
|
||||
|
@ -152,61 +153,10 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
|||
super.onActivityCreated(savedInstanceState);
|
||||
BaseActivity activity = (BaseActivity) getActivity();
|
||||
|
||||
if (jumpToTopAllowed()) {
|
||||
TabLayout layout = activity.findViewById(R.id.tab_layout);
|
||||
onTabSelectedListener = new TabLayout.OnTabSelectedListener() {
|
||||
@Override
|
||||
public void onTabSelected(TabLayout.Tab tab) {}
|
||||
|
||||
@Override
|
||||
public void onTabUnselected(TabLayout.Tab tab) {}
|
||||
|
||||
@Override
|
||||
public void onTabReselected(TabLayout.Tab tab) {
|
||||
jumpToTop();
|
||||
}
|
||||
};
|
||||
layout.addOnTabSelectedListener(onTabSelectedListener);
|
||||
}
|
||||
|
||||
/* MastodonApi on the base activity is only guaranteed to be initialised after the parent
|
||||
* activity is created, so everything needing to access the api object has to be delayed
|
||||
* until here. */
|
||||
api = activity.mastodonApi;
|
||||
|
||||
|
||||
if (actionButtonPresent()) {
|
||||
/* Use a modified scroll listener that both loads more statuses as it goes, and hides
|
||||
* the follow button on down-scroll. */
|
||||
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(getContext());
|
||||
hideFab = preferences.getBoolean("fabHide", false);
|
||||
scrollListener = new EndlessOnScrollListener(layoutManager) {
|
||||
@Override
|
||||
public void onScrolled(RecyclerView view, int dx, int dy) {
|
||||
super.onScrolled(view, dx, dy);
|
||||
|
||||
ActionButtonActivity actionButtonActivity = (ActionButtonActivity) getActivity();
|
||||
FloatingActionButton composeButton = actionButtonActivity.getActionButton();
|
||||
|
||||
if (composeButton != null) {
|
||||
if (hideFab) {
|
||||
if (dy > 0 && composeButton.isShown()) {
|
||||
composeButton.hide(); // hides the button if we're scrolling down
|
||||
} else if (dy < 0 && !composeButton.isShown()) {
|
||||
composeButton.show(); // shows it if we are scrolling up
|
||||
}
|
||||
} else if (!composeButton.isShown()) {
|
||||
composeButton.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadMore(int page, int totalItemsCount, RecyclerView view) {
|
||||
AccountListFragment.this.onLoadMore(view);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// Just use the basic scroll listener to load more accounts.
|
||||
scrollListener = new EndlessOnScrollListener(layoutManager) {
|
||||
@Override
|
||||
|
@ -214,24 +164,10 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
|||
AccountListFragment.this.onLoadMore(view);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
recyclerView.addOnScrollListener(scrollListener);
|
||||
}
|
||||
|
||||
private boolean actionButtonPresent() {
|
||||
return type == Type.FOLLOWS || type == Type.FOLLOWERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
if (jumpToTopAllowed()) {
|
||||
TabLayout tabLayout = getActivity().findViewById(R.id.tab_layout);
|
||||
tabLayout.removeOnTabSelectedListener(onTabSelectedListener);
|
||||
}
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAccount(String id) {
|
||||
Intent intent = new Intent(getContext(), AccountActivity.class);
|
||||
|
@ -420,10 +356,6 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
|||
Log.e(TAG, message);
|
||||
}
|
||||
|
||||
private boolean jumpToTopAllowed() {
|
||||
return type == Type.FOLLOWS || type == Type.FOLLOWERS;
|
||||
}
|
||||
|
||||
private void jumpToTop() {
|
||||
layoutManager.scrollToPositionWithOffset(0, 0);
|
||||
scrollListener.reset();
|
||||
|
@ -437,11 +369,16 @@ public class AccountListFragment extends BaseFragment implements AccountActionLi
|
|||
private Call<List<Account>> getFetchCallByListType(Type type, String fromId, String uptoId) {
|
||||
switch (type) {
|
||||
default:
|
||||
case FOLLOWS: return api.accountFollowing(accountId, fromId, uptoId, null);
|
||||
case FOLLOWERS: return api.accountFollowers(accountId, fromId, uptoId, null);
|
||||
case BLOCKS: return api.blocks(fromId, uptoId, null);
|
||||
case MUTES: return api.mutes(fromId, uptoId, null);
|
||||
case FOLLOW_REQUESTS: return api.followRequests(fromId, uptoId, null);
|
||||
case FOLLOWS:
|
||||
return api.accountFollowing(accountId, fromId, uptoId, null);
|
||||
case FOLLOWERS:
|
||||
return api.accountFollowers(accountId, fromId, uptoId, null);
|
||||
case BLOCKS:
|
||||
return api.blocks(fromId, uptoId, null);
|
||||
case MUTES:
|
||||
return api.mutes(fromId, uptoId, null);
|
||||
case FOLLOW_REQUESTS:
|
||||
return api.followRequests(fromId, uptoId, null);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,288 @@
|
|||
/* Copyright 2017 Andrew Dawson
|
||||
*
|
||||
* This file is a part of Tusky.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* Tusky 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 Tusky; if not,
|
||||
* see <http://www.gnu.org/licenses>. */
|
||||
|
||||
package com.keylesspalace.tusky.fragment
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.preference.PreferenceManager
|
||||
import android.support.v4.app.ActivityOptionsCompat
|
||||
import android.support.v4.content.ContextCompat
|
||||
import android.support.v4.view.ViewCompat
|
||||
import android.support.v4.widget.SwipeRefreshLayout
|
||||
import android.support.v7.widget.GridLayoutManager
|
||||
import android.support.v7.widget.RecyclerView
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import com.keylesspalace.tusky.BaseActivity
|
||||
import com.keylesspalace.tusky.R
|
||||
import com.keylesspalace.tusky.ViewMediaActivity
|
||||
import com.keylesspalace.tusky.ViewVideoActivity
|
||||
import com.keylesspalace.tusky.entity.Status
|
||||
import com.keylesspalace.tusky.network.MastodonApi
|
||||
import com.keylesspalace.tusky.view.SquareImageView
|
||||
import com.squareup.picasso.Picasso
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Created by charlag on 26/10/2017.
|
||||
*
|
||||
* Fragment with multiple columns of media previews for the specified account.
|
||||
*/
|
||||
|
||||
class AccountMediaFragment : BaseFragment() {
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun newInstance(accountId: String): AccountMediaFragment {
|
||||
val fragment = AccountMediaFragment()
|
||||
fragment.arguments = Bundle()
|
||||
fragment.arguments.putString(ACCOUNT_ID_ARG, accountId)
|
||||
return fragment
|
||||
}
|
||||
|
||||
private const val ACCOUNT_ID_ARG = "account_id"
|
||||
private const val TAG = "AccountMediaFragment"
|
||||
}
|
||||
|
||||
private val adapter = MediaGridAdapter()
|
||||
private var currentCall: Call<List<Status>>? = null
|
||||
private lateinit var api: MastodonApi
|
||||
private val statuses = mutableListOf<Status>()
|
||||
private var fetchingStatus = FetchingStatus.NOT_FETCHING
|
||||
lateinit private var swipeLayout: SwipeRefreshLayout
|
||||
|
||||
private val callback = object : Callback<List<Status>> {
|
||||
override fun onFailure(call: Call<List<Status>>?, t: Throwable?) {
|
||||
fetchingStatus = FetchingStatus.NOT_FETCHING
|
||||
swipeLayout.isRefreshing = false
|
||||
Log.d(TAG, "Failed to fetch account media", t)
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<List<Status>>, response: Response<List<Status>>) {
|
||||
fetchingStatus = FetchingStatus.NOT_FETCHING
|
||||
swipeLayout.isRefreshing = false
|
||||
val body = response.body()
|
||||
body?.let { fetched ->
|
||||
statuses.addAll(0, fetched)
|
||||
// flatMap requires iterable but I don't want to box each array into list
|
||||
val result = mutableListOf<Status.MediaAttachment>()
|
||||
for (status in fetched) {
|
||||
result.addAll(status.attachments)
|
||||
}
|
||||
adapter.addTop(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val bottomCallback = object : Callback<List<Status>> {
|
||||
override fun onFailure(call: Call<List<Status>>?, t: Throwable?) {
|
||||
fetchingStatus = FetchingStatus.NOT_FETCHING
|
||||
Log.d(TAG, "Failed to fetch account media", t)
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call<List<Status>>, response: Response<List<Status>>) {
|
||||
fetchingStatus = FetchingStatus.NOT_FETCHING
|
||||
val body = response.body()
|
||||
body?.let { fetched ->
|
||||
Log.d(TAG, "fetched ${fetched.size} statuses")
|
||||
if (fetched.isNotEmpty()) Log.d(TAG, "first: ${fetched.first().id}, last: ${fetched.last().id}")
|
||||
statuses.addAll(fetched)
|
||||
Log.d(TAG, "now there are ${statuses.size} statuses")
|
||||
// flatMap requires iterable but I don't want to box each array into list
|
||||
val result = mutableListOf<Status.MediaAttachment>()
|
||||
for (status in fetched) {
|
||||
result.addAll(status.attachments)
|
||||
}
|
||||
adapter.addBottom(result)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onAttach(context: Context) {
|
||||
super.onAttach(context)
|
||||
// we should get rid of this
|
||||
api = (context as BaseActivity).mastodonApi
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_timeline, container, false)
|
||||
val recyclerView = view.findViewById<RecyclerView>(R.id.recycler_view)
|
||||
val columnCount = context.resources.getInteger(R.integer.profile_media_column_count)
|
||||
val layoutManager = GridLayoutManager(context, columnCount)
|
||||
|
||||
val lightThemeEnabled = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.getBoolean("lightTheme", false)
|
||||
val bgRes = if (lightThemeEnabled) R.color.window_background_light
|
||||
else R.color.window_background_dark
|
||||
adapter.baseItemColor = ContextCompat.getColor(recyclerView.context, bgRes)
|
||||
|
||||
recyclerView.layoutManager = layoutManager
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
val accountId = arguments.getString(ACCOUNT_ID_ARG)
|
||||
|
||||
swipeLayout = view.findViewById<SwipeRefreshLayout>(R.id.swipe_refresh_layout)
|
||||
swipeLayout.setOnRefreshListener {
|
||||
if (fetchingStatus != FetchingStatus.NOT_FETCHING) return@setOnRefreshListener
|
||||
currentCall = if (statuses.isEmpty()) {
|
||||
fetchingStatus = FetchingStatus.INITIAL_FETCHING
|
||||
api.accountStatuses(accountId, null, null, null, true)
|
||||
} else {
|
||||
fetchingStatus = FetchingStatus.REFRESHING
|
||||
api.accountStatuses(accountId, null, statuses[0].id, null, true)
|
||||
}
|
||||
currentCall?.enqueue(callback)
|
||||
|
||||
}
|
||||
|
||||
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
||||
|
||||
override fun onScrolled(recyclerView: RecyclerView?, dx: Int, dy: Int) {
|
||||
if (dy > 0) {
|
||||
val itemCount = layoutManager.itemCount
|
||||
val lastItem = layoutManager.findLastCompletelyVisibleItemPosition()
|
||||
if (itemCount <= lastItem + 3 && fetchingStatus == FetchingStatus.NOT_FETCHING) {
|
||||
statuses.lastOrNull()?.let { last ->
|
||||
Log.d(TAG, "Requesting statuses with max_id: ${last.id}, (bottom)")
|
||||
fetchingStatus = FetchingStatus.FETCHING_BOTTOM
|
||||
currentCall = api.accountStatuses(accountId, last.id, null, null, true)
|
||||
currentCall?.enqueue(bottomCallback)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
// That's sort of an optimization to only load media once user has opened the tab
|
||||
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
|
||||
super.setUserVisibleHint(isVisibleToUser)
|
||||
if (!isVisibleToUser) return
|
||||
val accountId = arguments.getString(ACCOUNT_ID_ARG)
|
||||
if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) {
|
||||
fetchingStatus = FetchingStatus.INITIAL_FETCHING
|
||||
currentCall = api.accountStatuses(accountId, null, null, null, true)
|
||||
currentCall?.enqueue(callback)
|
||||
}
|
||||
}
|
||||
|
||||
private fun viewMedia(items: List<Status.MediaAttachment>, currentIndex: Int, view: View?) {
|
||||
val urls = items.map { it.url }.toTypedArray()
|
||||
val type = items[currentIndex].type
|
||||
|
||||
when (type) {
|
||||
Status.MediaAttachment.Type.IMAGE -> {
|
||||
val intent = Intent(context, ViewMediaActivity::class.java)
|
||||
intent.putExtra("urls", urls)
|
||||
intent.putExtra("urlIndex", currentIndex)
|
||||
if (view != null) {
|
||||
val url = urls[currentIndex]
|
||||
ViewCompat.setTransitionName(view, url)
|
||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity,
|
||||
view, url)
|
||||
startActivity(intent, options.toBundle())
|
||||
} else {
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
Status.MediaAttachment.Type.GIFV, Status.MediaAttachment.Type.VIDEO -> {
|
||||
val intent = Intent(context, ViewVideoActivity::class.java)
|
||||
intent.putExtra("url", urls[currentIndex])
|
||||
startActivity(intent)
|
||||
}
|
||||
Status.MediaAttachment.Type.UNKNOWN, null -> {
|
||||
}/* Intentionally do nothing. This case is here is to handle when new attachment
|
||||
* types are added to the API before code is added here to handle them. So, the
|
||||
* best fallback is to just show the preview and ignore requests to view them. */
|
||||
}
|
||||
}
|
||||
|
||||
private enum class FetchingStatus {
|
||||
NOT_FETCHING, INITIAL_FETCHING, FETCHING_BOTTOM, REFRESHING
|
||||
}
|
||||
|
||||
inner class MediaGridAdapter
|
||||
: RecyclerView.Adapter<MediaGridAdapter.MediaViewHolder>() {
|
||||
|
||||
var baseItemColor = Color.BLACK
|
||||
|
||||
private val items = mutableListOf<Status.MediaAttachment>()
|
||||
private val itemBgBaseHSV = FloatArray(3)
|
||||
private val random = Random()
|
||||
|
||||
fun addTop(newItems: List<Status.MediaAttachment>) {
|
||||
items.addAll(0, newItems)
|
||||
notifyItemRangeInserted(0, newItems.size)
|
||||
}
|
||||
|
||||
fun addBottom(newItems: List<Status.MediaAttachment>) {
|
||||
if (newItems.isEmpty()) return
|
||||
|
||||
val oldLen = items.size
|
||||
items.addAll(newItems)
|
||||
notifyItemRangeInserted(oldLen, newItems.size)
|
||||
}
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
val hsv = FloatArray(3)
|
||||
Color.colorToHSV(baseItemColor, hsv)
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {
|
||||
val view = SquareImageView(parent.context)
|
||||
view.scaleType = ImageView.ScaleType.CENTER_CROP
|
||||
return MediaViewHolder(view)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = items.size
|
||||
|
||||
override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {
|
||||
itemBgBaseHSV[2] = random.nextFloat() * (1f - 0.3f) + 0.3f
|
||||
holder.imageView.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV))
|
||||
val item = items[position]
|
||||
Picasso.with(holder.imageView.context)
|
||||
.load(item.previewUrl)
|
||||
.into(holder.imageView)
|
||||
}
|
||||
|
||||
|
||||
inner class MediaViewHolder(val imageView: ImageView)
|
||||
: RecyclerView.ViewHolder(imageView),
|
||||
View.OnClickListener {
|
||||
init {
|
||||
itemView.setOnClickListener(this)
|
||||
}
|
||||
|
||||
// saving some allocations
|
||||
override fun onClick(v: View?) {
|
||||
viewMedia(items, adapterPosition, imageView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ import android.content.SharedPreferences;
|
|||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.design.widget.TabLayout;
|
||||
|
@ -263,7 +264,7 @@ public class TimelineFragment extends SFragment implements
|
|||
final Status status = statuses.get(position);
|
||||
super.reblogWithCallback(status, reblog, new Callback<Status>() {
|
||||
@Override
|
||||
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) {
|
||||
public void onResponse(@NonNull Call<Status> call, @NonNull retrofit2.Response<Status> response) {
|
||||
if (response.isSuccessful()) {
|
||||
status.reblogged = reblog;
|
||||
|
||||
|
@ -280,7 +281,7 @@ public class TimelineFragment extends SFragment implements
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Status> call, Throwable t) {
|
||||
public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) {
|
||||
Log.d(TAG, "Failed to reblog status " + status.id);
|
||||
t.printStackTrace();
|
||||
}
|
||||
|
@ -293,7 +294,7 @@ public class TimelineFragment extends SFragment implements
|
|||
|
||||
super.favouriteWithCallback(status, favourite, new Callback<Status>() {
|
||||
@Override
|
||||
public void onResponse(Call<Status> call, retrofit2.Response<Status> response) {
|
||||
public void onResponse(@NonNull Call<Status> call, @NonNull retrofit2.Response<Status> response) {
|
||||
if (response.isSuccessful()) {
|
||||
status.favourited = favourite;
|
||||
|
||||
|
@ -310,7 +311,7 @@ public class TimelineFragment extends SFragment implements
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Status> call, Throwable t) {
|
||||
public void onFailure(@NonNull Call<Status> call, @NonNull Throwable t) {
|
||||
Log.d(TAG, "Failed to favourite status " + status.id);
|
||||
t.printStackTrace();
|
||||
}
|
||||
|
@ -462,7 +463,7 @@ public class TimelineFragment extends SFragment implements
|
|||
case TAG:
|
||||
return api.hashtagTimeline(tagOrId, null, fromId, uptoId, null);
|
||||
case USER:
|
||||
return api.accountStatuses(tagOrId, fromId, uptoId, null);
|
||||
return api.accountStatuses(tagOrId, fromId, uptoId, null, null);
|
||||
case FAVOURITES:
|
||||
return api.favourites(fromId, uptoId, null);
|
||||
}
|
||||
|
@ -495,7 +496,7 @@ public class TimelineFragment extends SFragment implements
|
|||
|
||||
Callback<List<Status>> callback = new Callback<List<Status>>() {
|
||||
@Override
|
||||
public void onResponse(Call<List<Status>> call, Response<List<Status>> response) {
|
||||
public void onResponse(@NonNull Call<List<Status>> call, @NonNull Response<List<Status>> response) {
|
||||
if (response.isSuccessful()) {
|
||||
String linkHeader = response.headers().get("Link");
|
||||
onFetchTimelineSuccess(response.body(), linkHeader, fetchEnd);
|
||||
|
@ -505,7 +506,7 @@ public class TimelineFragment extends SFragment implements
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<List<Status>> call, Throwable t) {
|
||||
public void onFailure(@NonNull Call<List<Status>> call, @NonNull Throwable t) {
|
||||
onFetchTimelineFailure((Exception) t, fetchEnd);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
|
||||
package com.keylesspalace.tusky.network;
|
||||
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import com.keylesspalace.tusky.entity.AccessToken;
|
||||
import com.keylesspalace.tusky.entity.Account;
|
||||
import com.keylesspalace.tusky.entity.AppCredentials;
|
||||
|
@ -127,12 +129,25 @@ public interface MastodonApi {
|
|||
@Query("limit") Integer limit);
|
||||
@GET("api/v1/accounts/{id}")
|
||||
Call<Account> account(@Path("id") String accountId);
|
||||
|
||||
/**
|
||||
* Method to fetch statuses for the specified account.
|
||||
* @param accountId ID for account for which statuses will be requested
|
||||
* @param maxId Only statuses with ID less than maxID will be returned
|
||||
* @param sinceId Only statuses with ID bigger than sinceID will be returned
|
||||
* @param limit Limit returned statuses (current API limits: default - 20, max - 40)
|
||||
* @param onlyMedia Should server return only statuses which contain media. Caution! The server
|
||||
* works in a weird way so if any value if present at this field it will be
|
||||
* interpreted as "true". Pass null to return all statuses.
|
||||
* @return
|
||||
*/
|
||||
@GET("api/v1/accounts/{id}/statuses")
|
||||
Call<List<Status>> accountStatuses(
|
||||
@Path("id") String accountId,
|
||||
@Query("max_id") String maxId,
|
||||
@Query("since_id") String sinceId,
|
||||
@Query("limit") Integer limit);
|
||||
@Query("limit") Integer limit,
|
||||
@Nullable @Query("only_media") Boolean onlyMedia);
|
||||
@GET("api/v1/accounts/{id}/followers")
|
||||
Call<List<Account>> accountFollowers(
|
||||
@Path("id") String accountId,
|
||||
|
|
|
@ -15,27 +15,19 @@
|
|||
|
||||
package com.keylesspalace.tusky.pager;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentPagerAdapter;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.keylesspalace.tusky.R;
|
||||
import com.keylesspalace.tusky.fragment.AccountListFragment;
|
||||
import com.keylesspalace.tusky.fragment.AccountMediaFragment;
|
||||
import com.keylesspalace.tusky.fragment.TimelineFragment;
|
||||
|
||||
public class AccountPagerAdapter extends FragmentPagerAdapter {
|
||||
private Context context;
|
||||
private String accountId;
|
||||
private String[] pageTitles;
|
||||
|
||||
public AccountPagerAdapter(FragmentManager manager, Context context, String accountId) {
|
||||
public AccountPagerAdapter(FragmentManager manager, String accountId) {
|
||||
super(manager);
|
||||
this.context = context;
|
||||
this.accountId = accountId;
|
||||
}
|
||||
|
||||
|
@ -50,31 +42,21 @@ public class AccountPagerAdapter extends FragmentPagerAdapter {
|
|||
return TimelineFragment.newInstance(TimelineFragment.Kind.USER, accountId);
|
||||
}
|
||||
case 1: {
|
||||
return AccountListFragment.newInstance(AccountListFragment.Type.FOLLOWS, accountId);
|
||||
}
|
||||
case 2: {
|
||||
return AccountListFragment.newInstance(AccountListFragment.Type.FOLLOWERS, accountId);
|
||||
return AccountMediaFragment.newInstance(accountId);
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
throw new AssertionError("Page " + position + " is out of AccountPagerAdapter bounds");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return 3;
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
return pageTitles[position];
|
||||
}
|
||||
|
||||
public View getTabView(int position, ViewGroup root) {
|
||||
View view = LayoutInflater.from(context).inflate(R.layout.tab_account, root, false);
|
||||
TextView title = view.findViewById(R.id.title);
|
||||
title.setText(pageTitles[position]);
|
||||
return view;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
package com.keylesspalace.tusky.view
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.ImageView
|
||||
|
||||
/**
|
||||
* Created by charlag on 26/10/2017.
|
||||
*/
|
||||
|
||||
class SquareImageView : ImageView {
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attributes: AttributeSet) : super(context, attributes)
|
||||
|
||||
constructor(context: Context, attributes: AttributeSet, defStyleAttr: Int)
|
||||
: super(context, attributes, defStyleAttr)
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
val width = measuredWidth
|
||||
setMeasuredDimension(width, width)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_selected="false" android:color="?android:attr/textColorTertiary"/>
|
||||
<item android:state_selected="true" android:color="?attr/colorAccent"/>
|
||||
<item android:state_pressed="false" android:color="?android:attr/textColorPrimary"/>
|
||||
<item android:state_pressed="true" android:color="?android:attr/textColorTertiary"/>
|
||||
</selector>
|
|
@ -40,7 +40,7 @@
|
|||
android:scaleType="centerCrop"
|
||||
app:layout_collapseMode="pin" />
|
||||
|
||||
<RelativeLayout
|
||||
<android.support.constraint.ConstraintLayout
|
||||
android:id="@+id/account_header_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
|
@ -54,71 +54,67 @@
|
|||
android:id="@+id/account_avatar"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:layout_marginRight="10dp"
|
||||
android:layout_toLeftOf="@+id/follow_btn"
|
||||
android:layout_toStartOf="@+id/follow_btn"
|
||||
android:src="@drawable/avatar_default"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shadow="true" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/follow_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_marginTop="6dp" />
|
||||
android:layout_marginTop="6dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/account_follows_you"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_below="@id/follow_btn"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:layout_marginRight="10dp"
|
||||
android:text="@string/follows_you"
|
||||
android:textColor="?android:textColorPrimary" />
|
||||
android:textColor="?android:textColorPrimary"
|
||||
app:layout_constraintEnd_toEndOf="@id/follow_btn"
|
||||
app:layout_constraintTop_toBottomOf="@id/follow_btn" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/account_display_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/account_avatar"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="?android:textColorPrimary"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="normal|bold"
|
||||
app:layout_constraintTop_toBottomOf="@id/account_avatar"
|
||||
tools:text="Tusky Mastodon Client" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/account_username"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/account_display_name"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="1"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
app:layout_constraintTop_toBottomOf="@id/account_display_name"
|
||||
tools:text="\@Tusky" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/account_locked"
|
||||
android:layout_width="16sp"
|
||||
android:layout_height="16sp"
|
||||
android:layout_alignBottom="@id/account_username"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginLeft="4dp"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_toEndOf="@id/account_username"
|
||||
android:layout_toRightOf="@id/account_username"
|
||||
android:contentDescription="@string/description_account_locked"
|
||||
android:tint="?android:textColorSecondary"
|
||||
android:visibility="gone"
|
||||
app:srcCompat="@drawable/reblog_disabled_light" />
|
||||
app:layout_constraintBottom_toBottomOf="@id/account_username"
|
||||
app:layout_constraintLeft_toRightOf="@id/account_username"
|
||||
app:srcCompat="@drawable/reblog_disabled_light"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/account_note"
|
||||
|
@ -128,9 +124,46 @@
|
|||
android:paddingBottom="16dp"
|
||||
android:paddingTop="10dp"
|
||||
android:textColor="?android:textColorTertiary"
|
||||
app:layout_constraintTop_toBottomOf="@id/account_username"
|
||||
tools:text="This is a test description" />
|
||||
|
||||
</RelativeLayout>
|
||||
<TextView
|
||||
android:id="@+id/followers_tv"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/account_note"
|
||||
tools:text="3000 Followers"
|
||||
android:background="@android:color/transparent"
|
||||
android:textColor="@color/account_tab_font_color"
|
||||
app:layout_constraintHorizontal_bias="0"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/following_tv"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="@id/followers_tv"
|
||||
app:layout_constraintEnd_toStartOf="@id/statuses_btn"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
app:layout_constraintStart_toEndOf="@id/followers_tv"
|
||||
app:layout_constraintTop_toTopOf="@id/followers_tv"
|
||||
tools:text="500 Following"
|
||||
android:textColor="@color/account_tab_font_color" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/statuses_btn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintStart_toEndOf="@id/following_tv"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/followers_tv"
|
||||
tools:text="3000 Posts"
|
||||
app:layout_constraintHorizontal_bias="0"
|
||||
android:textColor="@color/account_tab_font_color"/>
|
||||
|
||||
</android.support.constraint.ConstraintLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
|
@ -158,12 +191,8 @@
|
|||
android:id="@+id/tab_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:tabBackground="?android:colorBackground"
|
||||
app:tabGravity="fill">
|
||||
|
||||
<android.support.design.widget.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
android:background="?android:colorBackground"
|
||||
app:tabSelectedTextColor="?attr/colorAccent" >
|
||||
|
||||
<android.support.design.widget.TabItem
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
@ -1,26 +1,36 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
<android.support.v7.widget.AppCompatTextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="6dp"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/account_tab_font_color"
|
||||
android:textStyle="normal|bold" />
|
||||
android:textStyle="normal|bold"
|
||||
tools:text="Followers"
|
||||
android:singleLine="true"
|
||||
app:autoSizeTextType="uniform"
|
||||
app:autoSizeMinTextSize="12sp"
|
||||
app:autoSizeMaxTextSize="14sp"
|
||||
app:autoSizeStepGranularity="2sp"
|
||||
android:textAlignment="center"
|
||||
android:ellipsize="middle"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/total"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/title"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:textColor="@color/account_tab_font_color"
|
||||
android:textSize="12sp" />
|
||||
android:textSize="12sp"
|
||||
tools:text="2,412"/>
|
||||
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
|
@ -218,6 +218,7 @@
|
|||
|
||||
<string name="follows_you">Подписан(а) на вас</string>
|
||||
<string name="pref_title_alway_show_sensitive_media">Всегда показывать NSFW-контент</string>
|
||||
<string name="title_media">Медиа</string>
|
||||
<string name="replying_to">Ответ @%s</string>
|
||||
|
||||
|
||||
|
|
4
app/src/main/res/values-small/integer.xml
Normal file
4
app/src/main/res/values-small/integer.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="profile_media_column_count">2</integer>
|
||||
</resources>
|
4
app/src/main/res/values/integers.xml
Normal file
4
app/src/main/res/values/integers.xml
Normal file
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="profile_media_column_count">3</integer>
|
||||
</resources>
|
|
@ -33,6 +33,9 @@
|
|||
<string name="title_follow_requests">Follow Requests</string>
|
||||
<string name="title_edit_profile">Edit your profile</string>
|
||||
<string name="title_saved_toot">Drafts</string>
|
||||
<string name="title_x_followers"><b>%d</b> Followers</string>
|
||||
<string name="title_x_following"><b>%d</b> Following</string>
|
||||
<string name="title_x_statuses"><b>%d</b> Posts</string>
|
||||
|
||||
<string name="status_username_format">\@%s</string>
|
||||
<string name="status_boosted_format">%s boosted</string>
|
||||
|
@ -232,6 +235,7 @@
|
|||
|
||||
<string name="follows_you">Follows you</string>
|
||||
<string name="pref_title_alway_show_sensitive_media">Always show all nsfw content</string>
|
||||
<string name="title_media">Media</string>
|
||||
<string name="replying_to">Replying to @%s</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.1.51'
|
||||
repositories {
|
||||
jcenter()
|
||||
google()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.0.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue