Merge branch 'master' into timeline-improvement
This commit is contained in:
commit
21de60f739
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -290,6 +290,7 @@ public class TimelineFragment extends SFragment implements
|
|||
super.reblogWithCallback(status, reblog, new Callback<Status>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
status.reblogged = reblog;
|
||||
|
||||
|
@ -324,6 +325,7 @@ public class TimelineFragment extends SFragment implements
|
|||
super.favouriteWithCallback(status, favourite, new Callback<Status>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<Status> call, @NonNull Response<Status> response) {
|
||||
|
||||
if (response.isSuccessful()) {
|
||||
status.favourited = favourite;
|
||||
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string-array name="pull_notification_check_interval_names">
|
||||
<item>15分</item>
|
||||
<item>20分</item>
|
||||
<item>25分</item>
|
||||
<item>30分</item>
|
||||
<item>45分</item>
|
||||
<item>1時間</item>
|
||||
<item>2時間</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
|
@ -100,9 +100,11 @@
|
|||
<string name="action_reject">拒否</string>
|
||||
<string name="action_search">検索</string>
|
||||
<string name="action_access_saved_toot">下書き</string>
|
||||
<string name="action_copy_link">リンクをコピー</string>
|
||||
|
||||
<string name="download_image">%1$s をダウンロードしています</string>
|
||||
|
||||
<string name="action_copy_link">リンクをコピー</string>
|
||||
|
||||
<string name="send_status_link_to">トゥートのURLを共有…</string>
|
||||
<string name="send_status_content_to">トゥートを共有…</string>
|
||||
|
||||
|
@ -127,14 +129,16 @@
|
|||
|
||||
<string name="login_connection">接続中…</string>
|
||||
|
||||
<string name="dialog_whats_an_instance">mastodon.social, mstdn.jp, pawoo.net や
|
||||
<a href="https://instances.social">その他</a>
|
||||
のような、あらゆるインスタンスのアドレスやドメインを入力できます。
|
||||
\n\nまだアカウントをお持ちでない場合は、参加したいインスタンスの名前を入力することで
|
||||
そのインスタンスにアカウントを作成できます。\n\nインスタンスはあなたのアカウントが提供される単独の場所ですが、
|
||||
他のインスタンスのユーザーとあたかも同じ場所にいるように簡単にコミュニケーションをとったりフォローしたりできます。
|
||||
<string name="dialog_whats_an_instance">mastodon.social, mstdn.jp, pawoo.netや<!--
|
||||
--><a href="https://instances.social">その他</a><!--
|
||||
-->のような、あらゆるインスタンスのアドレスやドメインを入力できます。
|
||||
\n\nまだアカウントをお持ちでない場合は、参加したいインスタンスの名前を入力することで<!--
|
||||
-->そのインスタンスにアカウントを作成できます。
|
||||
\n\nインスタンスはあなたのアカウントが提供される単独の場所ですが、<!--
|
||||
-->他のインスタンスのユーザーとあたかも同じ場所にいるように簡単にコミュニケーションをとったりフォローしたりできます。
|
||||
\n\nさらに詳しい情報は<a href="https://joinmastodon.org">joinmastodon.org</a>でご覧いただけます。
|
||||
</string>
|
||||
<!-- Commented out line breaks to avoid appearing spaces in the middle of a sentence. -->
|
||||
<string name="dialog_title_finishing_media_upload">メディアをアップロードしています</string>
|
||||
<string name="dialog_message_uploading_media">アップロード中…</string>
|
||||
<string name="dialog_download_image">ダウンロード</string>
|
||||
|
@ -170,6 +174,15 @@
|
|||
<string name="pref_title_show_replies">返信を表示</string>
|
||||
<string name="pref_title_show_media_preview">メディアのプレビューを表示する</string>
|
||||
|
||||
<string name="notification_channel_mention_name">新しい返信</string>
|
||||
<string name="notification_channel_mention_descriptions">新しい返信の通知</string>
|
||||
<string name="notification_channel_follow_name">新しいフォロワー</string>
|
||||
<string name="notification_channel_follow_description">新しいフォロワーの通知</string>
|
||||
<string name="notification_channel_boost_name">ブースト</string>
|
||||
<string name="notification_channel_boost_description">あなたのトゥートがブーストされたときの通知</string>
|
||||
<string name="notification_channel_favourite_name">お気に入り</string>
|
||||
<string name="notification_channel_favourite_description">あなたのトゥートがお気に入りに登録されたときの通知</string>
|
||||
|
||||
<string name="notification_mention_format">%sさんが返信しました</string>
|
||||
<string name="notification_summary_large">%1$sさん、%2$sさん、%3$sさんと他%4$d人</string>
|
||||
<string name="notification_summary_medium">%1$sさん、%2$sさん、%3$sさん</string>
|
||||
|
@ -180,12 +193,17 @@
|
|||
|
||||
<string name="about_title_activity">このアプリについて</string>
|
||||
<string name="about_application_version">アプリのバージョン:%s</string>
|
||||
<string name="about_tusky_version">Tusky %s</string>
|
||||
<string name="about_tusky_license">Tuskyは無料のオープンソースソフトウェアです。<!--
|
||||
-->GNU General Public License Version 3 の下で使用許諾されています。<!--
|
||||
-->ライセンスはここからご覧いただけます: https://www.gnu.org/licenses/gpl-3.0.ja.html</string>
|
||||
<!-- Commented out line breaks brcause inserting a space after the japanese period is incorrect. -->
|
||||
<string name="about_project_site">
|
||||
プロジェクトのWebサイト:\n
|
||||
プロジェクトのWebサイト(英語):\n
|
||||
https://tusky.keylesspalace.com
|
||||
</string>-->
|
||||
</string>
|
||||
<string name="about_bug_feature_request_site">
|
||||
バグ報告 & 機能リクエスト:\n
|
||||
バグ報告 & 機能リクエスト(英語):\n
|
||||
https://github.com/Vavassor/Tusky/issues
|
||||
</string>
|
||||
<string name="about_tusky_account">Tusky公式アカウント</string>
|
||||
|
@ -200,7 +218,20 @@
|
|||
<string name="no_content">下書きはありません</string>
|
||||
<string name="action_save_one_toot">下書きに保存しました!</string>
|
||||
|
||||
<!--These are for timestamps on statuses. For example: "16s" or "2d"-->
|
||||
<string name="abbreviated_in_years">%d年後</string>
|
||||
<string name="abbreviated_in_days">%d日後</string>
|
||||
<string name="abbreviated_in_hours">%d時間後</string>
|
||||
<string name="abbreviated_in_minutes">%d分後</string>
|
||||
<string name="abbreviated_in_seconds">%d秒後</string>
|
||||
<string name="abbreviated_years_ago">%d年前</string>
|
||||
<string name="abbreviated_days_ago">%d日前</string>
|
||||
<string name="abbreviated_hours_ago">%d時間前</string>
|
||||
<string name="abbreviated_minutes_ago">%d分前</string>
|
||||
<string name="abbreviated_seconds_ago">%d秒前</string>
|
||||
|
||||
<string name="follows_you">あなたをフォロー中</string>
|
||||
<string name="pref_title_alway_show_sensitive_media">常にすべての閲覧注意なコンテンツを表示する</string>
|
||||
<string name="pref_title_alway_show_sensitive_media">閲覧注意のコンテンツを常に表示</string>
|
||||
<string name="replying_to">@%sに返信</string>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="profile_media_column_count">2</integer>
|
||||
</resources>
|
|
@ -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>
|
||||
<string name="load_more_placeholder_text">load more</string>
|
||||
|
||||
|
|
|
@ -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 New Issue