- アプリ設定に「投稿/簡易投稿バーを表示」を追加
This commit is contained in:
tateisu 2017-07-23 18:49:51 +09:00
parent aac510aca5
commit 4ba7cd321a
13 changed files with 699 additions and 398 deletions

View File

@ -9,8 +9,8 @@ android {
applicationId "jp.juggler.subwaytooter"
minSdkVersion 21
targetSdkVersion 25
versionCode 101
versionName "1.0.1"
versionCode 102
versionName "1.0.2"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}

View File

@ -97,6 +97,7 @@ public class ActAppSetting extends AppCompatActivity
Switch swPriorChrome;
Switch swPostButtonBarTop;
Switch swDontDuplicationCheck ;
Switch swQuickTootBar;
Spinner spBackButtonAction;
Spinner spUITheme;
@ -191,6 +192,9 @@ public class ActAppSetting extends AppCompatActivity
swDontDuplicationCheck = (Switch) findViewById( R.id.swDontDuplicationCheck );
swDontDuplicationCheck.setOnCheckedChangeListener( this );
swQuickTootBar = (Switch) findViewById( R.id.swQuickTootBar );
swQuickTootBar.setOnCheckedChangeListener( this );
cbNotificationSound = (CheckBox) findViewById( R.id.cbNotificationSound );
cbNotificationVibration = (CheckBox) findViewById( R.id.cbNotificationVibration );
cbNotificationLED = (CheckBox) findViewById( R.id.cbNotificationLED );
@ -327,6 +331,7 @@ public class ActAppSetting extends AppCompatActivity
swDontCropMediaThumb.setChecked( pref.getBoolean( Pref.KEY_DONT_CROP_MEDIA_THUMBNAIL, false ) );
swPostButtonBarTop.setChecked( pref.getBoolean( Pref.KEY_POST_BUTTON_BAR_AT_TOP, false ) );
swDontDuplicationCheck.setChecked( pref.getBoolean( Pref.KEY_DONT_DUPLICATION_CHECK, false ) );
swQuickTootBar.setChecked( pref.getBoolean( Pref.KEY_QUICK_TOOT_BAR, false ) );
// Switch with default true
swDisableFastScroller.setChecked( pref.getBoolean( Pref.KEY_DISABLE_FAST_SCROLLER, true ) );
@ -387,8 +392,9 @@ public class ActAppSetting extends AppCompatActivity
.putBoolean( Pref.KEY_PRIOR_CHROME, swPriorChrome.isChecked() )
.putBoolean( Pref.KEY_POST_BUTTON_BAR_AT_TOP, swPostButtonBarTop.isChecked() )
.putBoolean( Pref.KEY_DONT_DUPLICATION_CHECK, swDontDuplicationCheck.isChecked() )
.putBoolean( Pref.KEY_QUICK_TOOT_BAR, swQuickTootBar.isChecked() )
.putBoolean( Pref.KEY_NOTIFICATION_SOUND, cbNotificationSound.isChecked() )
.putBoolean( Pref.KEY_NOTIFICATION_VIBRATION, cbNotificationVibration.isChecked() )
.putBoolean( Pref.KEY_NOTIFICATION_LED, cbNotificationLED.isChecked() )

View File

@ -25,6 +25,7 @@ import android.text.TextUtils;
import android.util.DisplayMetrics;
import android.util.JsonReader;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.View;
import android.support.design.widget.NavigationView;
import android.support.v4.view.GravityCompat;
@ -35,9 +36,11 @@ import android.view.MenuItem;
import android.view.ViewParent;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.widget.HorizontalScrollView;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import org.apache.commons.io.IOUtils;
import org.json.JSONException;
@ -77,9 +80,11 @@ import jp.juggler.subwaytooter.dialog.ActionsDialog;
import jp.juggler.subwaytooter.util.LinkClickContext;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.MyClickableSpan;
import jp.juggler.subwaytooter.util.PostHelper;
import jp.juggler.subwaytooter.util.Utils;
import jp.juggler.subwaytooter.view.ColumnStripLinearLayout;
import jp.juggler.subwaytooter.view.GravitySnapHelper;
import jp.juggler.subwaytooter.view.MyEditText;
import okhttp3.Request;
import okhttp3.RequestBody;
@ -160,6 +165,8 @@ public class ActMain extends AppCompatActivity
@Override protected void onDestroy(){
log.d( "onDestroy" );
super.onDestroy();
post_helper.onDestroy();
// このアクティビティに関連する ColumnViewHolder への参照を全カラムから除去する
for( Column c : app_state.column_list ){
c.removeColumnViewHolderByActivity( this );
@ -239,17 +246,7 @@ public class ActMain extends AppCompatActivity
// 各カラムのアカウント設定を読み直す
reloadAccountSetting();
if( ! TextUtils.isEmpty( posted_acct ) ){
int refresh_after_toot = pref.getInt( Pref.KEY_REFRESH_AFTER_TOOT, 0 );
if( refresh_after_toot != Pref.RAT_DONT_REFRESH ){
for( Column column : app_state.column_list ){
SavedAccount a = column.access_info;
if( ! Utils.equalsNullable( a.acct, posted_acct ) ) continue;
column.startRefreshForPost( posted_status_id, refresh_after_toot );
}
}
posted_acct = null;
}
refreshAfterPost();
Uri uri = ActCallback.last_uri.getAndSet( null );
if( uri != null ){
@ -268,6 +265,20 @@ public class ActMain extends AppCompatActivity
updateColumnStripSelection( - 1, - 1f );
}
void refreshAfterPost(){
if( ! TextUtils.isEmpty( posted_acct ) ){
int refresh_after_toot = pref.getInt( Pref.KEY_REFRESH_AFTER_TOOT, 0 );
if( refresh_after_toot != Pref.RAT_DONT_REFRESH ){
for( Column column : app_state.column_list ){
SavedAccount a = column.access_info;
if( ! Utils.equalsNullable( a.acct, posted_acct ) ) continue;
column.startRefreshForPost( posted_status_id, refresh_after_toot );
}
}
posted_acct = null;
}
}
static Intent sent_intent2;
private void handleSentIntent( final Intent intent ){
@ -333,10 +344,57 @@ public class ActMain extends AppCompatActivity
case R.id.btnToot:
performTootButton();
break;
case R.id.btnQuickToot:
performQuickPost(null);
break;
}
}
private void performQuickPost(SavedAccount account){
if( account == null ){
if( pager_adapter != null ){
Column c = app_state.column_list.get( pager.getCurrentItem() );
if( ! c.access_info.isPseudo() ){
account = c.access_info;
}
}
if( account == null ){
AccountPicker.pick( this, false, true, getString( R.string.account_picker_toot ), new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( @NonNull SavedAccount ai ){
performQuickPost( ai );
}
} );
return;
}
}
String visibility = account.visibility;
if( TextUtils.isEmpty( visibility ) ){
visibility = TootStatus.VISIBILITY_PUBLIC;
}
post_helper.content = etQuickToot.getText().toString().trim();
post_helper.spoiler_text = null;
post_helper.visibility = visibility;
post_helper.bNSFW = false;
post_helper.in_reply_to_id = - 1L;
post_helper.attachment_list = null;
Utils.hideKeyboard( this,etQuickToot );
post_helper.post( account, false, false, new PostHelper.Callback() {
@Override public void onPostComplete( SavedAccount target_account, TootStatus status ){
etQuickToot.setText("");
posted_acct = target_account.acct;
posted_status_id =status.id;
refreshAfterPost();
}
});
}
@Override
public void onPageScrolled( int position, float positionOffset, int positionOffsetPixels ){
updateColumnStripSelection( position, positionOffset );
@ -430,6 +488,7 @@ public class ActMain extends AppCompatActivity
}else if( requestCode == REQUEST_CODE_POST ){
if( data != null ){
etQuickToot.setText("");
posted_acct = data.getStringExtra( ActPost.EXTRA_POSTED_ACCT );
posted_status_id = data.getLongExtra( ActPost.EXTRA_POSTED_STATUS_ID, 0L );
}
@ -690,6 +749,12 @@ public class ActMain extends AppCompatActivity
boolean dont_crop_media_thumbnail;
View llQuickTootBar;
MyEditText etQuickToot;
ImageButton btnQuickToot;
PostHelper post_helper;
void initUI(){
setContentView( R.layout.act_main );
@ -727,12 +792,30 @@ public class ActMain extends AppCompatActivity
vFooterDivider2 = findViewById( R.id.vFooterDivider2 );
llColumnStrip = (ColumnStripLinearLayout) findViewById( R.id.llColumnStrip );
svColumnStrip = (HorizontalScrollView) findViewById( R.id.svColumnStrip );
llQuickTootBar = findViewById( R.id.llQuickTootBar );
etQuickToot = (MyEditText) findViewById( R.id. etQuickToot );
btnQuickToot = (ImageButton)findViewById( R.id. btnQuickToot );
if( !pref.getBoolean( Pref.KEY_QUICK_TOOT_BAR ,false ) ){
llQuickTootBar.setVisibility( View.GONE );
}
btnToot.setOnClickListener( this );
btnMenu.setOnClickListener( this );
btnQuickToot.setOnClickListener( this );
etQuickToot.setOnEditorActionListener( new TextView.OnEditorActionListener() {
@Override public boolean onEditorAction( TextView v, int actionId, KeyEvent event ){
if( actionId == EditorInfo.IME_ACTION_SEND ){
btnQuickToot.performClick();
return true;
}
return false;
}
} );
svColumnStrip.setHorizontalFadingEdgeEnabled( true );
post_helper = new PostHelper( this,pref,app_state.handler );
DisplayMetrics dm = getResources().getDisplayMetrics();
float density = dm.density;
@ -814,6 +897,11 @@ public class ActMain extends AppCompatActivity
}
showFooterColor();
post_helper.attachEditText( findViewById( R.id.llFormRoot ), etQuickToot, true,new PostHelper.Callback2() {
@Override public void onTextUpdate(){
}
} );
}
void updateColumnStrip(){
@ -1859,24 +1947,26 @@ public class ActMain extends AppCompatActivity
};
private void performTootButton(){
post_helper.closeAcctPopup();
final String initial_text = etQuickToot.getText().toString();
if( pager_adapter != null ){
Column c = pager_adapter.getColumn( pager.getCurrentItem() );
if( c != null && ! c.access_info.isPseudo() ){
ActPost.open( this, REQUEST_CODE_POST, c.access_info.db_id, "" );
ActPost.open( this, REQUEST_CODE_POST, c.access_info.db_id, initial_text );
return;
}
}else{
long db_id = pref.getLong( Pref.KEY_TABLET_TOOT_DEFAULT_ACCOUNT, - 1L );
SavedAccount a = SavedAccount.loadAccount( log, db_id );
if( a != null ){
ActPost.open( this, REQUEST_CODE_POST, a.db_id, "" );
ActPost.open( this, REQUEST_CODE_POST, a.db_id, initial_text );
return;
}
}
AccountPicker.pick( this, false, true, getString( R.string.account_picker_toot ), new AccountPicker.AccountPickerCallback() {
@Override public void onAccountPicked( @NonNull SavedAccount ai ){
ActPost.open( ActMain.this, REQUEST_CODE_POST, ai.db_id, "" );
ActPost.open( ActMain.this, REQUEST_CODE_POST, ai.db_id, initial_text );
}
} );
}
@ -3289,28 +3379,34 @@ public class ActMain extends AppCompatActivity
if( c == 0 ){
btnMenu.setBackgroundResource( R.drawable.btn_bg_ddd );
btnToot.setBackgroundResource( R.drawable.btn_bg_ddd );
btnQuickToot.setBackgroundResource( R.drawable.btn_bg_ddd );
}else{
int fg = ( footer_button_fg_color != 0
? footer_button_fg_color
: Styler.getAttributeColor( this, R.attr.colorRippleEffect ) );
ViewCompat.setBackground( btnToot, Styler.getAdaptiveRippleDrawable( c, fg ) );
ViewCompat.setBackground( btnMenu, Styler.getAdaptiveRippleDrawable( c, fg ) );
ViewCompat.setBackground( btnQuickToot, Styler.getAdaptiveRippleDrawable( c, fg ) );
}
c = footer_button_fg_color;
if( c == 0 ){
Styler.setIconDefaultColor( this, btnToot, R.attr.ic_edit );
Styler.setIconDefaultColor( this, btnMenu, R.attr.ic_hamburger );
Styler.setIconDefaultColor( this, btnQuickToot, R.attr.btn_post );
}else{
Styler.setIconCustomColor( this, btnToot, c, R.attr.ic_edit );
Styler.setIconCustomColor( this, btnMenu, c, R.attr.ic_hamburger );
Styler.setIconCustomColor( this, btnQuickToot, c,R.attr.btn_post );
}
c = footer_tab_bg_color;
if( c == 0 ){
svColumnStrip.setBackgroundColor( Styler.getAttributeColor( this, R.attr.colorColumnStripBackground ) );
llQuickTootBar.setBackgroundColor( Styler.getAttributeColor( this, R.attr.colorColumnStripBackground ) );
}else{
svColumnStrip.setBackgroundColor( c );
svColumnStrip.setBackgroundColor( Styler.getAttributeColor( this, R.attr.colorColumnStripBackground ) );
}
c = footer_tab_divider_color;

View File

@ -25,10 +25,7 @@ import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.text.Editable;
import android.text.Spannable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.view.ViewGroup;
@ -55,8 +52,6 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jp.juggler.subwaytooter.api.TootApiClient;
import jp.juggler.subwaytooter.api.TootApiResult;
@ -64,18 +59,16 @@ import jp.juggler.subwaytooter.api.entity.TootAttachment;
import jp.juggler.subwaytooter.api.entity.TootMention;
import jp.juggler.subwaytooter.api.entity.TootResults;
import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.dialog.DlgConfirm;
import jp.juggler.subwaytooter.dialog.DlgDraftPicker;
import jp.juggler.subwaytooter.table.AcctColor;
import jp.juggler.subwaytooter.table.AcctSet;
import jp.juggler.subwaytooter.table.PostDraft;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.dialog.ActionsDialog;
import jp.juggler.subwaytooter.table.TagSet;
import jp.juggler.subwaytooter.util.HTMLDecoder;
import jp.juggler.subwaytooter.util.LinkClickContext;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.MyClickableSpan;
import jp.juggler.subwaytooter.util.PostHelper;
import jp.juggler.subwaytooter.view.MyEditText;
import jp.juggler.subwaytooter.view.MyNetworkImageView;
import jp.juggler.subwaytooter.util.PostAttachment;
@ -162,7 +155,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener,
break;
case R.id.btnPost:
performPost( false, false );
performPost();
break;
case R.id.btnRemoveReply:
@ -258,6 +251,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener,
ArrayList< PostAttachment > attachment_list;
AppState app_state;
boolean isPostComplete;
PostHelper post_helper;
@Override
protected void onCreate( @Nullable Bundle savedInstanceState ){
@ -269,6 +263,8 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener,
initUI();
if( account_list.isEmpty() ){
Utils.showToast( this, true, R.string.please_add_account );
finish();
@ -492,8 +488,9 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener,
}
@Override protected void onDestroy(){
handler.removeCallbacks( proc_text_changed );
closeAcctPopup();
post_helper.onDestroy();
super.onDestroy();
}
@ -631,34 +628,14 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener,
}
} );
etContent.addTextChangedListener( new TextWatcher() {
@Override
public void beforeTextChanged( CharSequence s, int start, int count, int after ){
}
@Override
public void onTextChanged( CharSequence s, int start, int before, int count ){
handler.removeCallbacks( proc_text_changed );
handler.postDelayed( proc_text_changed, ( popup != null && popup.isShowing() ? 100L : 1000L ) );
}
@Override
public void afterTextChanged( Editable s ){
post_helper = new PostHelper( this, pref ,app_state.handler );
post_helper.attachEditText( formRoot,etContent,false, new PostHelper.Callback2() {
@Override public void onTextUpdate(){
updateTextCount();
}
} );
etContent.setOnSelectionChangeListener( new MyEditText.OnSelectionChangeListener() {
@Override public void onSelectionChanged( int selStart, int selEnd ){
if( selStart != selEnd ){
// 範囲選択されてるならポップアップは閉じる
log.d( "onSelectionChanged: range selected" );
closeAcctPopup();
}
}
} );
scrollView.getViewTreeObserver().addOnScrollChangedListener( scroll_listener );
@ -668,116 +645,11 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener,
final ViewTreeObserver.OnScrollChangedListener scroll_listener = new ViewTreeObserver.OnScrollChangedListener() {
@Override public void onScrollChanged(){
if( popup != null && popup.isShowing() ){
popup.updatePosition();
}
post_helper.onScrollChanged();
}
};
static final Pattern reCharsNotTag = Pattern.compile( "[\\s\\-+.,:;/]" );
final Runnable proc_text_changed = new Runnable() {
@Override public void run(){
int start = etContent.getSelectionStart();
int end = etContent.getSelectionEnd();
if( start != end ){
closeAcctPopup();
return;
}
String src = etContent.getText().toString();
int count_atMark = 0;
int[] pos_atMark = new int[ 2 ];
for( ; ; ){
if( count_atMark >= 2 ) break;
if( start == 0 ) break;
char c = src.charAt( start - 1 );
if( c == '@' ){
-- start;
pos_atMark[ count_atMark++ ] = start;
continue;
}else if( ( '0' <= c && c <= '9' )
|| ( 'A' <= c && c <= 'Z' )
|| ( 'a' <= c && c <= 'z' )
|| c == '_' || c == '-' || c == '.'
){
-- start;
continue;
}
// その他の文字種が出たら探索打ち切り
break;
}
// 登場した@の数
if( count_atMark == 0 ){
// 次はAcctじゃなくてHashtagの補完を試みる
checkTag();
return;
}else if( count_atMark == 1 ){
start = pos_atMark[ 0 ];
}else if( count_atMark == 2 ){
start = pos_atMark[ 1 ];
}
// 最低でも2文字ないと補完しない
if( end - start < 2 ){
closeAcctPopup();
return;
}
int limit = 100;
String s = src.substring( start, end );
ArrayList< String > acct_list = AcctSet.searchPrefix( s, limit );
log.d( "search for %s, result=%d", s, acct_list.size() );
if( acct_list.isEmpty() ){
closeAcctPopup();
}else{
if( popup == null || ! popup.isShowing() ){
popup = new PopupAutoCompleteAcct( ActPost.this, etContent, formRoot );
}
popup.setList( acct_list, start, end );
}
}
private void checkTag(){
int end = etContent.getSelectionEnd();
String src = etContent.getText().toString();
int last_sharp = src.lastIndexOf( '#', end - 1 );
if( last_sharp == - 1 || end - last_sharp < 3 ){
closeAcctPopup();
return;
}
String part = src.substring( last_sharp + 1, end );
if( reCharsNotTag.matcher( part ).find() ){
closeAcctPopup();
return;
}
int limit = 100;
String s = src.substring( last_sharp + 1, end );
ArrayList< String > tag_list = TagSet.searchPrefix( s, limit );
log.d( "search for %s, result=%d", s, tag_list.size() );
if( tag_list.isEmpty() ){
closeAcctPopup();
}else{
if( popup == null || ! popup.isShowing() ){
popup = new PopupAutoCompleteAcct( ActPost.this, etContent, formRoot );
}
popup.setList( tag_list, last_sharp, end );
}
}
};
PopupAutoCompleteAcct popup;
private void closeAcctPopup(){
if( popup != null ){
popup.dismiss();
popup = null;
}
}
private void updateTextCount(){
String s = etContent.getText().toString();
int count_content = s.codePointCount( 0, s.length() );
@ -1474,219 +1346,34 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener,
///////////////////////////////////////////////////////////////////////////////////////
// post
// [:word:] 単語構成文字 (Letter | Mark | Decimal_Number | Connector_Punctuation)
// [:alpha:] 英字 (Letter | Mark)
static final String word = "[_\\p{L}\\p{M}\\p{Nd}\\p{Pc}]";
static final String alpha = "[_\\p{L}\\p{M}]";
static final Pattern reTag = Pattern.compile(
"(?:^|[^/)\\w])#(" + word + "*" + alpha + word + "*)"
, Pattern.CASE_INSENSITIVE
);
private void performPost( final boolean bConfirmTag, final boolean bConfirmAccount ){
final String content = etContent.getText().toString().trim();
if( TextUtils.isEmpty( content ) ){
Utils.showToast( this, true, R.string.post_error_contents_empty );
return;
}
private void performPost(){
post_helper.content = etContent.getText().toString().trim();
final String spoiler_text;
if( ! cbContentWarning.isChecked() ){
spoiler_text = null;
post_helper.spoiler_text = null;
}else{
spoiler_text = etContentWarning.getText().toString().trim();
if( TextUtils.isEmpty( spoiler_text ) ){
Utils.showToast( this, true, R.string.post_error_contents_warning_empty );
return;
}
post_helper.spoiler_text = etContentWarning.getText().toString().trim();
}
if( ! bConfirmAccount ){
DlgConfirm.open( this
, getString( R.string.confirm_post_from, AcctColor.getNickname( account.acct ) )
, new DlgConfirm.Callback() {
@Override public boolean isConfirmEnabled(){
return account.confirm_post;
}
@Override public void setConfirmEnabled( boolean bv ){
account.confirm_post = bv;
account.saveSetting();
}
@Override public void onOK(){
performPost( bConfirmTag, true );
}
} );
return;
}
post_helper.visibility = this.visibility;
post_helper.bNSFW = cbNSFW.isChecked();
if( ! bConfirmTag ){
Matcher m = reTag.matcher( content );
if( m.find() && ! TootStatus.VISIBILITY_PUBLIC.equals( visibility ) ){
new AlertDialog.Builder( this )
.setCancelable( true )
.setMessage( R.string.hashtag_and_visibility_not_match )
.setNegativeButton( R.string.cancel, null )
.setPositiveButton( R.string.ok, new DialogInterface.OnClickListener() {
@Override public void onClick( DialogInterface dialog, int which ){
//noinspection ConstantConditions
performPost( true, bConfirmAccount );
}
} )
.show();
return;
}
}
post_helper.in_reply_to_id = this.in_reply_to_id;
final StringBuilder sb = new StringBuilder();
post_helper.attachment_list = this.attachment_list;
sb.append( "status=" );
sb.append( Uri.encode( content ) );
sb.append( "&visibility=" );
sb.append( Uri.encode( visibility ) );
if( cbNSFW.isChecked() ){
sb.append( "&sensitive=1" );
}
if( spoiler_text != null ){
sb.append( "&spoiler_text=" );
sb.append( Uri.encode( spoiler_text ) );
}
if( in_reply_to_id != - 1L ){
sb.append( "&in_reply_to_id=" );
sb.append( Long.toString( in_reply_to_id ) );
}
if( attachment_list != null ){
for( PostAttachment pa : attachment_list ){
if( pa.attachment != null ){
sb.append( "&media_ids[]=" ).append( pa.attachment.id );
}
}
}
final ProgressDialog progress = new ProgressDialog( this );
final AsyncTask< Void, Void, TootApiResult > task = new AsyncTask< Void, Void, TootApiResult >() {
final SavedAccount target_account = account;
post_helper.post( account, false, false, new PostHelper.Callback() {
TootStatus status;
@Override protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( ActPost.this, new TootApiClient.Callback() {
@Override public boolean isApiCancelled(){
return isCancelled();
}
@Override public void publishApiProgress( final String s ){
Utils.runOnMainThread( new Runnable() {
@Override public void run(){
progress.setMessage( s );
}
} );
}
} );
@Override public void onPostComplete( SavedAccount target_account, TootStatus status ){
Intent data = new Intent();
data.putExtra( EXTRA_POSTED_ACCT, target_account.acct );
data.putExtra( EXTRA_POSTED_STATUS_ID, status.id );
client.setAccount( target_account );
String post_content = sb.toString();
String digest = Utils.digestSHA256( post_content + target_account.acct );
Request.Builder request_builder = new Request.Builder()
.post( RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, post_content
) );
if( ! pref.getBoolean( Pref.KEY_DONT_DUPLICATION_CHECK, false ) ){
request_builder.header( "Idempotency-Key", digest );
}
TootApiResult result = client.request( "/api/v1/statuses", request_builder );
if( result != null && result.object != null ){
status = TootStatus.parse( log, account, account.host, result.object );
Spannable s = status.decoded_content;
MyClickableSpan[] span_list = s.getSpans( 0, s.length(), MyClickableSpan.class );
if( span_list != null ){
ArrayList< String > tag_list = new ArrayList<>();
for( MyClickableSpan span : span_list ){
int start = s.getSpanStart( span );
int end = s.getSpanEnd( span );
String text = s.subSequence( start, end ).toString();
if( text.startsWith( "#" ) ){
tag_list.add( text.substring( 1 ) );
}
}
int count = tag_list.size();
if( count > 0 ){
TagSet.saveList(
System.currentTimeMillis()
, tag_list.toArray( new String[ count ] )
, 0
, count
);
}
}
}
return result;
}
@Override
protected void onCancelled(){
onPostExecute( null );
}
@Override
protected void onPostExecute( TootApiResult result ){
try{
progress.dismiss();
}catch( Throwable ignored ){
// java.lang.IllegalArgumentException:
// at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:396)
// at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:322)
// at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:116)
// at android.app.Dialog.dismissDialog(Dialog.java:341)
// at android.app.Dialog.dismiss(Dialog.java:324)
// at jp.juggler.subwaytooter.ActMain$10$1.onPostExecute(ActMain.java:867)
// at jp.juggler.subwaytooter.ActMain$10$1.onPostExecute(ActMain.java:837)
}
//noinspection StatementWithEmptyBody
if( result == null ){
// cancelled.
}else if( status != null ){
// 連投してIdempotency が同じだった場合もエラーにはならずここを通る
Intent data = new Intent();
data.putExtra( EXTRA_POSTED_ACCT, target_account.acct );
data.putExtra( EXTRA_POSTED_STATUS_ID, status.id );
setResult( RESULT_OK, data );
isPostComplete = true;
ActPost.this.finish();
}else{
Utils.showToast( ActPost.this, true, result.error );
}
}
};
progress.setIndeterminate( true );
progress.setCancelable( true );
progress.setOnCancelListener( new DialogInterface.OnCancelListener() {
@Override
public void onCancel( DialogInterface dialog ){
task.cancel( true );
setResult( RESULT_OK, data );
isPostComplete = true;
ActPost.this.finish();
}
} );
progress.show();
task.executeOnExecutor( App1.task_executor );
}
/////////////////////////////////////////////////

View File

@ -298,6 +298,7 @@ public class AppDataExporter {
case Pref.KEY_PRIOR_CHROME:
case Pref.KEY_POST_BUTTON_BAR_AT_TOP:
case Pref.KEY_DONT_DUPLICATION_CHECK:
case Pref.KEY_QUICK_TOOT_BAR:
boolean bv = reader.nextBoolean();
e.putBoolean( k, bv );
break;

View File

@ -67,6 +67,8 @@ public class Pref {
public static final String KEY_ACCT_FONT_SIZE = "acct_font_size";
public static final String KEY_DONT_DUPLICATION_CHECK = "dont_duplication_check";
public static final String KEY_QUICK_TOOT_BAR = "quick_toot_bar";
// 項目を追加したらAppDataExporter#importPref のswitch文も更新すること
}

View File

@ -1,4 +1,4 @@
package jp.juggler.subwaytooter;
package jp.juggler.subwaytooter.util;
import android.annotation.SuppressLint;
import android.app.Activity;
@ -6,7 +6,6 @@ import android.support.v4.content.ContextCompat;
import android.text.Layout;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.CheckedTextView;
import android.widget.EditText;
import android.widget.LinearLayout;
@ -14,6 +13,9 @@ import android.widget.PopupWindow;
import java.util.ArrayList;
import jp.juggler.subwaytooter.R;
import jp.juggler.subwaytooter.Styler;
class PopupAutoCompleteAcct {
final Activity activity;
private final EditText etContent;
@ -22,9 +24,11 @@ class PopupAutoCompleteAcct {
private final View formRoot;
private final float density;
private final int popup_width;
private int popup_rows;
private boolean bMainScreen;
void dismiss(){
acct_popup.dismiss();
}
@ -33,13 +37,14 @@ class PopupAutoCompleteAcct {
return acct_popup.isShowing();
}
PopupAutoCompleteAcct( Activity activity, EditText etContent, View formRoot ){
PopupAutoCompleteAcct( Activity activity, EditText etContent, View formRoot, boolean bMainScreen ){
this.activity = activity;
this.etContent = etContent;
this.formRoot = formRoot;
this.bMainScreen = bMainScreen;
this.density = activity.getResources().getDisplayMetrics().density;
popup_width = (int)(0.5f +240f * density );
popup_width = (int) ( 0.5f + 240f * density );
@SuppressLint("InflateParams") View viewRoot =
activity.getLayoutInflater().inflate( R.layout.acct_complete_popup, null, false );
@ -102,26 +107,40 @@ class PopupAutoCompleteAcct {
etContent.getLocationOnScreen( location );
int text_top = location[ 1 ];
formRoot.getLocationOnScreen( location );
int form_top = location[ 1 ];
int form_bottom = location[ 1 ] + formRoot.getHeight();
int popup_top;
int popup_height;
Layout layout = etContent.getLayout();
int popup_top = text_top
+ etContent.getTotalPaddingTop()
+ layout.getLineBottom( layout.getLineCount() - 1 )
- etContent.getScrollY();
if( popup_top < form_top ) popup_top = form_top;
int popup_height = form_bottom - popup_top;
int min = (int) ( 0.5f + 48f * 2f * density );
if( popup_height < min ) popup_height = min;
int max = (int) ( 0.5f + 48f * popup_rows * density );
if( popup_height > max ) popup_height = max;
if( bMainScreen ){
int popup_bottom = text_top + etContent.getTotalPaddingTop() - etContent.getScrollY();
int max = popup_bottom-(int) ( 0.5f + 48f * 1f * density );
int min = (int) ( 0.5f + 48f * 2f * density );
popup_height = (int) ( 0.5f + 48f * popup_rows * density );
if( popup_height < min ) popup_height = min;
if( popup_height > max ) popup_height = max;
popup_top = popup_bottom - popup_height;
}else{
formRoot.getLocationOnScreen( location );
int form_top = location[ 1 ];
int form_bottom = location[ 1 ] + formRoot.getHeight();
Layout layout = etContent.getLayout();
popup_top = text_top
+ etContent.getTotalPaddingTop()
+ layout.getLineBottom( layout.getLineCount() - 1 )
- etContent.getScrollY();
if( popup_top < form_top ) popup_top = form_top;
popup_height = form_bottom - popup_top;
int min = (int) ( 0.5f + 48f * 2f * density );
int max = (int) ( 0.5f + 48f * popup_rows * density );
if( popup_height < min ) popup_height = min;
if( popup_height > max ) popup_height = max;
}
if( acct_popup.isShowing() ){
acct_popup.update( 0, popup_top, popup_width, popup_height );

View File

@ -0,0 +1,433 @@
package jp.juggler.subwaytooter.util;
import android.app.ProgressDialog;
import android.content.DialogInterface;
import android.content.SharedPreferences;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.text.Editable;
import android.text.Spannable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.View;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jp.juggler.subwaytooter.App1;
import jp.juggler.subwaytooter.Pref;
import jp.juggler.subwaytooter.R;
import jp.juggler.subwaytooter.api.TootApiClient;
import jp.juggler.subwaytooter.api.TootApiResult;
import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.dialog.DlgConfirm;
import jp.juggler.subwaytooter.table.AcctColor;
import jp.juggler.subwaytooter.table.AcctSet;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.table.TagSet;
import jp.juggler.subwaytooter.view.MyEditText;
import okhttp3.Request;
import okhttp3.RequestBody;
public class PostHelper {
private static final LogCategory log = new LogCategory( "PostHelper" );
public interface Callback{
void onPostComplete(SavedAccount target_account,TootStatus status);
}
private final AppCompatActivity activity;
private final SharedPreferences pref;
private final Handler handler;
public PostHelper( AppCompatActivity activity ,SharedPreferences pref,Handler handler ){
this.activity = activity;
this.pref = pref;
this.handler = handler;
}
// [:word:] 単語構成文字 (Letter | Mark | Decimal_Number | Connector_Punctuation)
// [:alpha:] 英字 (Letter | Mark)
private static final String word = "[_\\p{L}\\p{M}\\p{Nd}\\p{Pc}]";
private static final String alpha = "[_\\p{L}\\p{M}]";
private static final Pattern reTag = Pattern.compile(
"(?:^|[^/)\\w])#(" + word + "*" + alpha + word + "*)"
, Pattern.CASE_INSENSITIVE
);
public String content;
public String spoiler_text;
public String visibility;
public boolean bNSFW;
public long in_reply_to_id;
public ArrayList< PostAttachment > attachment_list;
public void post( final SavedAccount account,final boolean bConfirmTag, final boolean bConfirmAccount ,final Callback callback){
if( TextUtils.isEmpty( content ) ){
Utils.showToast( activity, true, R.string.post_error_contents_empty );
return;
}
if( spoiler_text != null && spoiler_text.isEmpty() ){
Utils.showToast( activity, true, R.string.post_error_contents_warning_empty );
return;
}
if( ! bConfirmAccount ){
DlgConfirm.open( activity
, activity.getString( R.string.confirm_post_from, AcctColor.getNickname( account.acct ) )
, new DlgConfirm.Callback() {
@Override public boolean isConfirmEnabled(){
return account.confirm_post;
}
@Override public void setConfirmEnabled( boolean bv ){
account.confirm_post = bv;
account.saveSetting();
}
@Override public void onOK(){
post( account,bConfirmTag, true ,callback);
}
} );
return;
}
if( ! bConfirmTag ){
Matcher m = reTag.matcher( content );
if( m.find() && ! TootStatus.VISIBILITY_PUBLIC.equals( visibility ) ){
new AlertDialog.Builder( activity )
.setCancelable( true )
.setMessage( R.string.hashtag_and_visibility_not_match )
.setNegativeButton( R.string.cancel, null )
.setPositiveButton( R.string.ok, new DialogInterface.OnClickListener() {
@Override public void onClick( DialogInterface dialog, int which ){
//noinspection ConstantConditions
post( account,true, bConfirmAccount ,callback);
}
} )
.show();
return;
}
}
final StringBuilder sb = new StringBuilder();
sb.append( "status=" );
sb.append( Uri.encode( content ) );
sb.append( "&visibility=" );
sb.append( Uri.encode( visibility ) );
if( bNSFW ){
sb.append( "&sensitive=1" );
}
if( spoiler_text != null ){
sb.append( "&spoiler_text=" );
sb.append( Uri.encode( spoiler_text ) );
}
if( in_reply_to_id != - 1L ){
sb.append( "&in_reply_to_id=" );
sb.append( Long.toString( in_reply_to_id ) );
}
if( attachment_list != null ){
for( PostAttachment pa : attachment_list ){
if( pa.attachment != null ){
sb.append( "&media_ids[]=" ).append( pa.attachment.id );
}
}
}
final ProgressDialog progress = new ProgressDialog( activity );
final AsyncTask< Void, Void, TootApiResult > task = new AsyncTask< Void, Void, TootApiResult >() {
final SavedAccount target_account = account;
TootStatus status;
@Override protected TootApiResult doInBackground( Void... params ){
TootApiClient client = new TootApiClient( activity, new TootApiClient.Callback() {
@Override public boolean isApiCancelled(){
return isCancelled();
}
@Override public void publishApiProgress( final String s ){
Utils.runOnMainThread( new Runnable() {
@Override public void run(){
progress.setMessage( s );
}
} );
}
} );
client.setAccount( target_account );
String post_content = sb.toString();
String digest = Utils.digestSHA256( post_content + target_account.acct );
Request.Builder request_builder = new Request.Builder()
.post( RequestBody.create(
TootApiClient.MEDIA_TYPE_FORM_URL_ENCODED
, post_content
) );
if( ! pref.getBoolean( Pref.KEY_DONT_DUPLICATION_CHECK, false ) ){
request_builder.header( "Idempotency-Key", digest );
}
TootApiResult result = client.request( "/api/v1/statuses", request_builder );
if( result != null && result.object != null ){
status = TootStatus.parse( log, account, account.host, result.object );
Spannable s = status.decoded_content;
MyClickableSpan[] span_list = s.getSpans( 0, s.length(), MyClickableSpan.class );
if( span_list != null ){
ArrayList< String > tag_list = new ArrayList<>();
for( MyClickableSpan span : span_list ){
int start = s.getSpanStart( span );
int end = s.getSpanEnd( span );
String text = s.subSequence( start, end ).toString();
if( text.startsWith( "#" ) ){
tag_list.add( text.substring( 1 ) );
}
}
int count = tag_list.size();
if( count > 0 ){
TagSet.saveList(
System.currentTimeMillis()
, tag_list.toArray( new String[ count ] )
, 0
, count
);
}
}
}
return result;
}
@Override
protected void onCancelled(){
onPostExecute( null );
}
@Override
protected void onPostExecute( TootApiResult result ){
try{
progress.dismiss();
}catch( Throwable ignored ){
// java.lang.IllegalArgumentException:
// at android.view.WindowManagerGlobal.findViewLocked(WindowManagerGlobal.java:396)
// at android.view.WindowManagerGlobal.removeView(WindowManagerGlobal.java:322)
// at android.view.WindowManagerImpl.removeViewImmediate(WindowManagerImpl.java:116)
// at android.app.Dialog.dismissDialog(Dialog.java:341)
// at android.app.Dialog.dismiss(Dialog.java:324)
// at jp.juggler.subwaytooter.ActMain$10$1.onPostExecute(ActMain.java:867)
// at jp.juggler.subwaytooter.ActMain$10$1.onPostExecute(ActMain.java:837)
}
//noinspection StatementWithEmptyBody
if( result == null ){
// cancelled.
}else if( status != null ){
// 連投してIdempotency が同じだった場合もエラーにはならずここを通る
callback.onPostComplete( target_account,status );
}else{
Utils.showToast( activity, true, result.error );
}
}
};
progress.setIndeterminate( true );
progress.setCancelable( true );
progress.setOnCancelListener( new DialogInterface.OnCancelListener() {
@Override
public void onCancel( DialogInterface dialog ){
task.cancel( true );
}
} );
progress.show();
task.executeOnExecutor( App1.task_executor );
}
public interface Callback2{
void onTextUpdate();
}
private Callback2 callback2;
private MyEditText et;
private PopupAutoCompleteAcct popup;
private View formRoot;
private boolean bMainScreen;
public void closeAcctPopup(){
if( popup != null ){
popup.dismiss();
popup = null;
}
}
public void onScrollChanged(){
if( popup != null && popup.isShowing() ){
popup.updatePosition();
}
}
public void onDestroy(){
handler.removeCallbacks( proc_text_changed );
closeAcctPopup();
}
public void attachEditText( View _formRoot, MyEditText _et, boolean bMainScreen, Callback2 _callback2){
this.formRoot = _formRoot;
this.et = _et;
this.callback2 = _callback2;
this.bMainScreen = bMainScreen;
et.addTextChangedListener( new TextWatcher() {
@Override
public void beforeTextChanged( CharSequence s, int start, int count, int after ){
}
@Override
public void onTextChanged( CharSequence s, int start, int before, int count ){
handler.removeCallbacks( proc_text_changed );
handler.postDelayed( proc_text_changed, ( popup != null && popup.isShowing() ? 100L : 1000L ) );
}
@Override
public void afterTextChanged( Editable s ){
callback2.onTextUpdate();
}
} );
et.setOnSelectionChangeListener( new MyEditText.OnSelectionChangeListener() {
@Override public void onSelectionChanged( int selStart, int selEnd ){
if( selStart != selEnd ){
// 範囲選択されてるならポップアップは閉じる
log.d( "onSelectionChanged: range selected" );
closeAcctPopup();
}
}
} );
}
private static final Pattern reCharsNotTag = Pattern.compile( "[\\s\\-+.,:;/]" );
private final Runnable proc_text_changed = new Runnable() {
@Override public void run(){
int start = et.getSelectionStart();
int end = et.getSelectionEnd();
if( start != end ){
closeAcctPopup();
return;
}
String src = et.getText().toString();
int count_atMark = 0;
int[] pos_atMark = new int[ 2 ];
for( ; ; ){
if( count_atMark >= 2 ) break;
if( start == 0 ) break;
char c = src.charAt( start - 1 );
if( c == '@' ){
-- start;
pos_atMark[ count_atMark++ ] = start;
continue;
}else if( ( '0' <= c && c <= '9' )
|| ( 'A' <= c && c <= 'Z' )
|| ( 'a' <= c && c <= 'z' )
|| c == '_' || c == '-' || c == '.'
){
-- start;
continue;
}
// その他の文字種が出たら探索打ち切り
break;
}
// 登場した@の数
if( count_atMark == 0 ){
// 次はAcctじゃなくてHashtagの補完を試みる
checkTag();
return;
}else if( count_atMark == 1 ){
start = pos_atMark[ 0 ];
}else if( count_atMark == 2 ){
start = pos_atMark[ 1 ];
}
// 最低でも2文字ないと補完しない
if( end - start < 2 ){
closeAcctPopup();
return;
}
int limit = 100;
String s = src.substring( start, end );
ArrayList< String > acct_list = AcctSet.searchPrefix( s, limit );
log.d( "search for %s, result=%d", s, acct_list.size() );
if( acct_list.isEmpty() ){
closeAcctPopup();
}else{
if( popup == null || ! popup.isShowing() ){
popup = new PopupAutoCompleteAcct( activity, et, formRoot,bMainScreen );
}
popup.setList( acct_list, start, end );
}
}
private void checkTag(){
int end = et.getSelectionEnd();
String src = et.getText().toString();
int last_sharp = src.lastIndexOf( '#', end - 1 );
if( last_sharp == - 1 || end - last_sharp < 3 ){
closeAcctPopup();
return;
}
String part = src.substring( last_sharp + 1, end );
if( reCharsNotTag.matcher( part ).find() ){
closeAcctPopup();
return;
}
int limit = 100;
String s = src.substring( last_sharp + 1, end );
ArrayList< String > tag_list = TagSet.searchPrefix( s, limit );
log.d( "search for %s, result=%d", s, tag_list.size() );
if( tag_list.isEmpty() ){
closeAcctPopup();
}else{
if( popup == null || ! popup.isShowing() ){
popup = new PopupAutoCompleteAcct( activity, et, formRoot ,bMainScreen);
}
popup.setList( tag_list, last_sharp, end );
}
}
};
}

View File

@ -287,6 +287,8 @@
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
@ -304,6 +306,22 @@
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/show_quick_toot_bar"
/>
<LinearLayout style="@style/setting_row_form">
<Switch
android:id="@+id/swQuickTootBar"
style="@style/setting_horizontal_stretch"
android:gravity="center"
/>
</LinearLayout>
<View style="@style/setting_divider"/>

View File

@ -16,6 +16,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="@+id/llFormRoot"
>
<FrameLayout
@ -46,10 +47,12 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
<jp.juggler.subwaytooter.view.MyRecyclerView
android:id="@+id/rvPager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
/>
</FrameLayout>
<LinearLayout
@ -63,28 +66,28 @@
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/btn_bg_ddd"
app:srcCompat="?attr/ic_hamburger"
android:contentDescription="@string/menu"
app:srcCompat="?attr/ic_hamburger"
/>
<View
android:id="@+id/vFooterDivider1"
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="?attr/colorImageButton"
android:id="@+id/vFooterDivider1"
/>
<HorizontalScrollView
android:id="@+id/svColumnStrip"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="?attr/colorColumnStripBackground"
android:cacheColorHint="#00000000"
android:fadingEdge="horizontal"
android:fadingEdgeLength="20dp"
android:fillViewport="true"
android:scrollbars="none"
android:background="?attr/colorColumnStripBackground"
android:id="@+id/svColumnStrip"
>
<jp.juggler.subwaytooter.view.ColumnStripLinearLayout
@ -96,23 +99,53 @@
</HorizontalScrollView>
<View
android:id="@+id/vFooterDivider2"
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="?attr/colorImageButton"
android:id="@+id/vFooterDivider2"
/>
<ImageButton
android:id="@+id/btnToot"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/btn_bg_ddd"
app:srcCompat="?attr/ic_edit"
android:contentDescription="@string/toot"
app:srcCompat="?attr/ic_edit"
/>
</LinearLayout>
<LinearLayout
android:id="@+id/llQuickTootBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:baselineAligned="false"
android:orientation="horizontal"
>
<jp.juggler.subwaytooter.view.MyEditText
android:id="@+id/etQuickToot"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:layout_marginStart="6dp"
android:layout_weight="1"
android:inputType="text"
android:maxLines="1"
android:hint="@string/quick_toot_hint"
android:imeOptions="actionSend"
/>
<ImageButton
android:id="@+id/btnQuickToot"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="@drawable/btn_bg_ddd"
android:contentDescription="@string/post"
app:srcCompat="?attr/btn_post"
/>
</LinearLayout>
</LinearLayout>
<!--<android.support.design.widget.FloatingActionButton-->
<!--android:id="@+id/"-->
<!--android:layout_width="wrap_content"-->
@ -133,7 +166,6 @@
<!--app:srcCompat="?attr/ic_menu"-->
<!--/>-->
<!--<android.support.design.widget.CoordinatorLayout-->
<!--android:layout_width="match_parent"-->
<!--android:layout_height="match_parent"-->
@ -148,6 +180,7 @@
android:layout_gravity="start"
android:fitsSystemWindows="true"
app:menu="@menu/menu_navi_drawer"/>
app:menu="@menu/menu_navi_drawer"
/>
<!-- app:headerLayout="@layout/nav_header_act_main" -->
</android.support.v4.widget.DrawerLayout>

View File

@ -388,6 +388,8 @@
<string name="timeline_font_size">Timeline font size (unit:sp. leave empty to default. app restart required)</string>
<string name="acct_font_size">Acct font size (unit:sp. leave empty to default. app restart required)</string>
<string name="dont_add_duplication_check_header">Don\'t add duplication check header</string>
<string name="show_quick_toot_bar">Show \"Quick Toot\" bar (app restart required.)</string>
<string name="quick_toot_hint">Quick Toot</string>
<!--<string name="abc_action_bar_home_description">Revenir à l\'accueil</string>-->
<!--<string name="abc_action_bar_home_description_format">%1$s, %2$s</string>-->

View File

@ -675,5 +675,7 @@
<string name="timeline_font_size">タイムラインのフォントサイズ(単位:sp。空欄でデフォルト。アプリ再起動が必要)</string>
<string name="acct_font_size">Acctのフォントサイズ(単位:sp。空欄でデフォルト。アプリ再起動が必要)</string>
<string name="dont_add_duplication_check_header">重複チェックヘッダを付与しない</string>
<string name="quick_toot_hint">簡易投稿入力</string>
<string name="show_quick_toot_bar">簡易投稿バーを表示(アプリ再起動が必要)</string>
</resources>

View File

@ -383,5 +383,7 @@
<string name="timeline_font_size">Timeline font size (unit:sp. leave empty to default. app restart required)</string>
<string name="acct_font_size">Acct font size (unit:sp. leave empty to default. app restart required)</string>
<string name="dont_add_duplication_check_header">Don\'t add duplication check header</string>
<string name="show_quick_toot_bar">Show \"Quick Toot\" bar (app restart required.)</string>
<string name="quick_toot_hint">Quick Toot</string>
</resources>