keyword highlight

This commit is contained in:
tateisu 2018-01-04 07:53:37 +09:00
parent 85099b7abe
commit 82756a6938
52 changed files with 2028 additions and 320 deletions

View File

@ -181,6 +181,16 @@
android:name=".ActMutedWord"
android:label="@string/muted_word"
/>
<activity
android:name=".ActHighlightWordList"
android:label="@string/highlight_word"
/>
<activity
android:name=".ActHighlightWordEdit"
android:label="@string/highlight_word"
/>
<activity
android:name=".ActColumnCustomize"
android:label="@string/color_and_background"

View File

@ -48,6 +48,7 @@ import java.io.InputStream;
import jp.juggler.subwaytooter.api.TootApiClient;
import jp.juggler.subwaytooter.api.TootApiResult;
import jp.juggler.subwaytooter.api.TootParser;
import jp.juggler.subwaytooter.api.TootTask;
import jp.juggler.subwaytooter.api.TootTaskRunner;
import jp.juggler.subwaytooter.api.entity.TootAccount;
@ -680,7 +681,7 @@ public class ActAccountSetting extends AppCompatActivity
@Override public TootApiResult background( @NonNull TootApiClient client ){
TootApiResult result = client.request( "/api/v1/accounts/verify_credentials" );
if( result != null && result.object != null ){
data = TootAccount.parse( ActAccountSetting.this, account, result.object );
data = new TootParser( ActAccountSetting.this, account ).account( result.object );
if( data == null ) return new TootApiResult( "TootAccount parse failed." );
}
return result;
@ -741,7 +742,7 @@ public class ActAccountSetting extends AppCompatActivity
TootApiResult result = client.request( "/api/v1/accounts/update_credentials", request_builder );
if( result != null && result.object != null ){
data = TootAccount.parse( ActAccountSetting.this, account, result.object );
data = new TootParser( ActAccountSetting.this, account ).account( result.object );
if( data == null ) return new TootApiResult( "TootAccount parse failed." );
}
return result;

View File

@ -0,0 +1,250 @@
package jp.juggler.subwaytooter;
import android.app.Activity;
import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.Switch;
import android.widget.TextView;
import com.jrummyapps.android.colorpicker.ColorPickerDialog;
import com.jrummyapps.android.colorpicker.ColorPickerDialogListener;
import org.json.JSONException;
import org.json.JSONObject;
import jp.juggler.subwaytooter.table.HighlightWord;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
public class ActHighlightWordEdit extends AppCompatActivity
implements View.OnClickListener, ColorPickerDialogListener, CompoundButton.OnCheckedChangeListener
{
static final LogCategory log = new LogCategory( "ActHighlightWordEdit" );
static final String EXTRA_ITEM = "item";
public static void open( @NonNull Activity activity, int request_code, @NonNull HighlightWord item ){
try{
Intent intent = new Intent( activity, ActHighlightWordEdit.class );
intent.putExtra( EXTRA_ITEM, item.encodeJson().toString() );
activity.startActivityForResult( intent, request_code );
}catch( JSONException ex ){
throw new RuntimeException( ex );
}
}
HighlightWord item;
private void makeResult(){
try{
Intent data = new Intent();
data.putExtra( EXTRA_ITEM, item.encodeJson().toString() );
setResult( RESULT_OK, data );
}catch( JSONException ex ){
throw new RuntimeException( ex );
}
}
@Override public void onBackPressed(){
makeResult();
super.onBackPressed();
}
@Override protected void onCreate( @Nullable Bundle savedInstanceState ){
super.onCreate( savedInstanceState );
App1.setActivityTheme( this, false );
initUI();
try{
if( savedInstanceState != null ){
item = new HighlightWord( new JSONObject( savedInstanceState.getString( EXTRA_ITEM ) ) );
}else{
item = new HighlightWord( new JSONObject( getIntent().getStringExtra( EXTRA_ITEM ) ) );
}
}catch( Throwable ex ){
throw new RuntimeException( "can't loading data", ex );
}
showSampleText();
}
@Override protected void onDestroy(){
super.onDestroy();
}
static final int REQUEST_CODE_NOTIFICATION_SOUND = 2;
@Override protected void onActivityResult( int requestCode, int resultCode, Intent data ){
switch( requestCode ){
default:
super.onActivityResult( requestCode, resultCode, data );
break;
case REQUEST_CODE_NOTIFICATION_SOUND:{
if( resultCode == RESULT_OK ){
// RINGTONE_PICKERからの選択されたデータを取得する
Uri uri = (Uri) Utils.getExtraObject( data, RingtoneManager.EXTRA_RINGTONE_PICKED_URI );
if( uri != null ){
item.sound_uri = uri.toString();
item.sound_type = HighlightWord.SOUND_TYPE_CUSTOM;
swSound.setChecked( true );
}
}
break;
}
}
}
private TextView tvName;
private Switch swSound;
private void initUI(){
setContentView( R.layout.act_highlight_edit );
tvName = findViewById( R.id.tvName );
swSound = findViewById( R.id.swSound );
swSound.setOnCheckedChangeListener( this );
findViewById( R.id.btnTextColorEdit ).setOnClickListener( this );
findViewById( R.id.btnTextColorReset ).setOnClickListener( this );
findViewById( R.id.btnBackgroundColorEdit ).setOnClickListener( this );
findViewById( R.id.btnBackgroundColorReset ).setOnClickListener( this );
findViewById( R.id.btnNotificationSoundEdit ).setOnClickListener( this );
findViewById( R.id.btnNotificationSoundReset ).setOnClickListener( this );
}
boolean bBusy = false;
private void showSampleText(){
bBusy = true;
try{
swSound.setChecked( item.sound_type != HighlightWord.SOUND_TYPE_NONE );
tvName.setText( item.name );
int c = item.color_bg;
if( c == 0 ){
tvName.setBackgroundColor( 0 );
}else{
tvName.setBackgroundColor( c );
}
c = item.color_fg;
if( c == 0 ){
tvName.setTextColor( Styler.getAttributeColor( this, android.R.attr.textColorPrimary ) );
}else{
tvName.setTextColor( c );
}
}finally{
bBusy = false;
}
}
@Override public void onClick( View v ){
switch( v.getId() ){
case R.id.btnTextColorEdit:
openColorPicker( COLOR_DIALOG_ID_TEXT, item.color_fg );
break;
case R.id.btnTextColorReset:
item.color_fg = 0;
showSampleText();
break;
case R.id.btnBackgroundColorEdit:
openColorPicker( COLOR_DIALOG_ID_BACKGROUND, item.color_bg );
break;
case R.id.btnBackgroundColorReset:
item.color_bg = 0;
showSampleText();
break;
case R.id.btnNotificationSoundEdit:
openNotificationSoundPicker();
break;
case R.id.btnNotificationSoundReset:
item.sound_uri = null;
item.sound_type = swSound.isChecked() ? HighlightWord.SOUND_TYPE_DEFAULT : HighlightWord.SOUND_TYPE_NONE;
break;
}
}
@Override public void onCheckedChanged( CompoundButton buttonView, boolean isChecked ){
if( bBusy ) return;
if( ! isChecked ){
item.sound_type = HighlightWord.SOUND_TYPE_NONE;
}else{
item.sound_type = TextUtils.isEmpty( item.sound_uri ) ? HighlightWord.SOUND_TYPE_DEFAULT : HighlightWord.SOUND_TYPE_CUSTOM;
}
}
//////////////////////////////////////////////////////////////////
// using ColorPickerDialog
private static final int COLOR_DIALOG_ID_TEXT = 1;
private static final int COLOR_DIALOG_ID_BACKGROUND = 2;
private void openColorPicker( int id, int initial_color ){
ColorPickerDialog.Builder builder = ColorPickerDialog.newBuilder()
.setDialogType( ColorPickerDialog.TYPE_CUSTOM )
.setAllowPresets( true )
.setShowAlphaSlider( id == COLOR_DIALOG_ID_BACKGROUND )
.setDialogId( id );
if( initial_color != 0 ) builder.setColor( initial_color );
builder.show( this );
}
@Override public void onDialogDismissed( int dialogId ){
}
@Override public void onColorSelected( int dialogId, int color ){
switch( dialogId ){
case COLOR_DIALOG_ID_TEXT:
item.color_fg = 0xff000000 | color;
break;
case COLOR_DIALOG_ID_BACKGROUND:
item.color_bg = color == 0 ? 0x01000000 : color;
break;
}
showSampleText();
}
//////////////////////////////////////////////////////////////////
private void openNotificationSoundPicker(){
Intent intent = new Intent( RingtoneManager.ACTION_RINGTONE_PICKER );
intent.putExtra( RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION );
intent.putExtra( RingtoneManager.EXTRA_RINGTONE_TITLE, R.string.notification_sound );
intent.putExtra( RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, false );
intent.putExtra( RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, false );
try{
Uri uri = TextUtils.isEmpty( item.sound_uri ) ? null : Uri.parse( item.sound_uri );
if( uri != null ){
intent.putExtra( RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, uri );
}
}catch( Throwable ignored ){
}
Intent chooser = Intent.createChooser( intent, getString( R.string.notification_sound ) );
startActivityForResult( chooser, REQUEST_CODE_NOTIFICATION_SOUND );
}
}

View File

@ -0,0 +1,347 @@
package jp.juggler.subwaytooter;
import android.app.Dialog;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.woxthebox.draglistview.DragItem;
import com.woxthebox.draglistview.DragItemAdapter;
import com.woxthebox.draglistview.DragListView;
import com.woxthebox.draglistview.swipe.ListSwipeHelper;
import com.woxthebox.draglistview.swipe.ListSwipeItem;
import org.json.JSONObject;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import jp.juggler.subwaytooter.dialog.DlgTextInput;
import jp.juggler.subwaytooter.table.HighlightWord;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
public class ActHighlightWordList extends AppCompatActivity implements View.OnClickListener {
private static final LogCategory log = new LogCategory( "ActHighlightWordList" );
DragListView listView;
MyListAdapter listAdapter;
// @Override public void onBackPressed(){
// setResult( RESULT_OK );
// super.onBackPressed();
// }
@Override
protected void onCreate( @Nullable Bundle savedInstanceState ){
super.onCreate( savedInstanceState );
App1.setActivityTheme( this, false );
initUI();
loadData();
}
@Override protected void onDestroy(){
super.onDestroy();
stopLastRingtone();
}
private void initUI(){
setContentView( R.layout.act_highlight_list );
Styler.fixHorizontalPadding2( findViewById( R.id.llContent ) );
// リストのアダプター
listAdapter = new MyListAdapter();
// ハンドル部分をドラッグで並べ替えできるRecyclerView
listView = findViewById( R.id.drag_list_view );
listView.setLayoutManager( new LinearLayoutManager( this ) );
listView.setAdapter( listAdapter, false );
listView.setCanDragHorizontally( true );
listView.setDragEnabled( false );
listView.setCustomDragItem( new MyDragItem( this, R.layout.lv_highlight_word ) );
listView.getRecyclerView().setVerticalScrollBarEnabled( true );
// リストを左右スワイプした
listView.setSwipeListener( new ListSwipeHelper.OnSwipeListenerAdapter() {
@Override public void onItemSwipeStarted( ListSwipeItem item ){
// 操作中はリフレッシュ禁止
// mRefreshLayout.setEnabled( false );
}
@Override public void onItemSwipeEnded(
ListSwipeItem item
, ListSwipeItem.SwipeDirection swipedDirection
){
// 操作完了でリフレッシュ許可
// mRefreshLayout.setEnabled( USE_SWIPE_REFRESH );
// 左にスワイプした(右端にBGが見えた) なら要素を削除する
if( swipedDirection == ListSwipeItem.SwipeDirection.LEFT ){
Object o = item.getTag();
if( o instanceof HighlightWord ){
HighlightWord adapterItem = (HighlightWord) o;
adapterItem.delete();
listAdapter.removeItem( listAdapter.getPositionForItem( adapterItem ) );
}
}
}
} );
findViewById( R.id.btnAdd ).setOnClickListener( this );
}
private void loadData(){
ArrayList< HighlightWord > tmp_list = new ArrayList<>();
try{
Cursor cursor = HighlightWord.createCursor();
if( cursor != null ){
try{
while( cursor.moveToNext() ){
HighlightWord item = new HighlightWord( cursor );
tmp_list.add( item );
}
}finally{
cursor.close();
}
}
}catch( Throwable ex ){
log.trace( ex );
}
listAdapter.setItemList( tmp_list );
}
@Override public void onClick( View v ){
switch( v.getId() ){
case R.id.btnAdd:
create();
break;
}
}
// リスト要素のViewHolder
class MyViewHolder extends DragItemAdapter.ViewHolder implements View.OnClickListener {
final TextView tvName;
final View btnSound;
MyViewHolder( final View viewRoot ){
super( viewRoot
, R.id.ivDragHandle // View ID ここを押すとドラッグ操作をすぐに開始する
, false // 長押しでドラッグ開始するなら真
);
tvName = viewRoot.findViewById( R.id.tvName );
btnSound = viewRoot.findViewById( R.id.btnSound );
// リスト要素のビューが ListSwipeItem だった場合Swipe操作を制御できる
if( viewRoot instanceof ListSwipeItem ){
ListSwipeItem lsi = (ListSwipeItem) viewRoot;
lsi.setSwipeInStyle( ListSwipeItem.SwipeInStyle.SLIDE );
lsi.setSupportedSwipeDirection( ListSwipeItem.SwipeDirection.LEFT );
}
}
void bind( HighlightWord item ){
itemView.setTag( item ); // itemView は親クラスのメンバ変数
tvName.setText( item.name );
int c = item.color_bg;
if( c == 0 ){
tvName.setBackgroundColor( 0 );
}else{
tvName.setBackgroundColor( c );
}
c = item.color_fg;
if( c == 0 ){
tvName.setTextColor( Styler.getAttributeColor( ActHighlightWordList.this, android.R.attr.textColorPrimary ) );
}else{
tvName.setTextColor( c );
}
btnSound.setVisibility( item.sound_type == HighlightWord.SOUND_TYPE_NONE ? View.GONE :View.VISIBLE);
btnSound.setOnClickListener( this );
btnSound.setTag( item );
}
// @Override
// public boolean onItemLongClicked( View view ){
// return false;
// }
@Override
public void onItemClicked( View view ){
Object o = view.getTag();
if( o instanceof HighlightWord ){
HighlightWord adapterItem = (HighlightWord) o;
edit( adapterItem );
}
}
@Override public void onClick( View v ){
Object o = v.getTag();
if( o instanceof HighlightWord ){
sound( (HighlightWord) o );
}
}
}
// ドラッグ操作中のデータ
private class MyDragItem extends DragItem {
MyDragItem( Context context, int layoutId ){
super( context, layoutId );
}
@Override
public void onBindDragView( View clickedView, View dragView ){
( (TextView) dragView.findViewById( R.id.tvName ) ).setText(
( (TextView) clickedView.findViewById( R.id.tvName ) ).getText()
);
( dragView.findViewById( R.id.btnSound ) ).setVisibility(
( clickedView.findViewById( R.id.btnSound ) ).getVisibility()
);
dragView.findViewById( R.id.item_layout ).setBackgroundColor(
Styler.getAttributeColor( ActHighlightWordList.this, R.attr.list_item_bg_pressed_dragged )
);
}
}
private class MyListAdapter extends DragItemAdapter< HighlightWord, MyViewHolder > {
MyListAdapter(){
super();
setHasStableIds( true );
setItemList( new ArrayList< HighlightWord >() );
}
@Override
public MyViewHolder onCreateViewHolder( ViewGroup parent, int viewType ){
View view = getLayoutInflater().inflate( R.layout.lv_highlight_word, parent, false );
return new MyViewHolder( view );
}
@Override
public void onBindViewHolder( MyViewHolder holder, int position ){
super.onBindViewHolder( holder, position );
holder.bind( getItemList().get( position ) );
}
@Override
public long getUniqueItemId( int position ){
HighlightWord item = mItemList.get( position ); // mItemList は親クラスのメンバ変数
return item.id;
}
}
private void create(){
DlgTextInput.show( this, getString( R.string.new_item ), "", new DlgTextInput.Callback() {
@Override public void onEmptyError(){
Utils.showToast( ActHighlightWordList.this, true, R.string.word_empty );
}
@Override public void onOK( Dialog dialog, String text ){
HighlightWord item = HighlightWord.load( text );
if( item == null ){
item = new HighlightWord( text );
item.save();
loadData();
}
edit( item );
try{
dialog.dismiss();
}catch( Throwable ignored ){
}
}
} );
}
private void edit( @NonNull HighlightWord item ){
ActHighlightWordEdit.open( this, REQUEST_CODE_EDIT, item );
}
private static final int REQUEST_CODE_EDIT = 1;
@Override protected void onActivityResult( int requestCode, int resultCode, Intent data ){
if( requestCode == REQUEST_CODE_EDIT && resultCode == RESULT_OK && data != null ){
try{
HighlightWord item = new HighlightWord( new JSONObject( data.getStringExtra( ActHighlightWordEdit.EXTRA_ITEM ) ) );
item.save();
loadData();
return;
}catch( Throwable ex ){
throw new RuntimeException( "can't loading data", ex );
}
}
super.onActivityResult( requestCode, resultCode, data );
}
WeakReference< Ringtone > last_ringtone;
private void stopLastRingtone(){
Ringtone r = last_ringtone == null ? null : last_ringtone.get();
if( r != null ){
try{
r.stop();
}catch( Throwable ex ){
log.trace( ex );
}finally{
last_ringtone = null;
}
}
}
private void sound( @NonNull HighlightWord item ){
stopLastRingtone();
if( item.sound_type == HighlightWord.SOUND_TYPE_NONE ) return;
if( item.sound_type == HighlightWord.SOUND_TYPE_CUSTOM
&& ! TextUtils.isEmpty( item.sound_uri )
){
try{
Ringtone ringtone = RingtoneManager.getRingtone( this, Uri.parse( item.sound_uri ) );
if( ringtone != null ){
last_ringtone = new WeakReference<>( ringtone );
ringtone.play();
return;
}
}catch( Throwable ex ){
log.trace( ex );
}
}
Uri uri = RingtoneManager.getDefaultUri( RingtoneManager.TYPE_NOTIFICATION );
try{
Ringtone ringtone = RingtoneManager.getRingtone( this, uri );
if( ringtone != null ){
last_ringtone = new WeakReference<>( ringtone );
ringtone.play();
}
}catch( Throwable ex ){
log.trace( ex );
}
}
}

View File

@ -748,7 +748,10 @@ public class ActMain extends AppCompatActivity
}else if( id == R.id.nav_muted_word ){
startActivity( new Intent( this, ActMutedWord.class ) );
}else if( id == R.id.nav_highlight_word ){
startActivity( new Intent( this, ActHighlightWordList.class ) );
}else if( id == R.id.nav_app_about ){
startActivityForResult( new Intent( this, ActAbout.class ), ActMain.REQUEST_APP_ABOUT );

View File

@ -42,7 +42,7 @@ public class ActMutedApp extends AppCompatActivity {
}
private void initUI(){
setContentView( R.layout.act_mute_app );
setContentView( R.layout.act_word_list );
Styler.fixHorizontalPadding2( findViewById( R.id.llContent ) );

View File

@ -42,7 +42,7 @@ public class ActMutedWord extends AppCompatActivity {
}
private void initUI(){
setContentView( R.layout.act_mute_app );
setContentView( R.layout.act_word_list );
Styler.fixHorizontalPadding2( findViewById( R.id.llContent ) );

View File

@ -64,6 +64,7 @@ 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.api.TootParser;
import jp.juggler.subwaytooter.dialog.AccountPicker;
import jp.juggler.subwaytooter.dialog.DlgDraftPicker;
import jp.juggler.subwaytooter.dialog.DlgTextInput;
@ -71,6 +72,7 @@ import jp.juggler.subwaytooter.table.AcctColor;
import jp.juggler.subwaytooter.table.PostDraft;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.dialog.ActionsDialog;
import jp.juggler.subwaytooter.util.CharacterGroup;
import jp.juggler.subwaytooter.util.DecodeOptions;
import jp.juggler.subwaytooter.util.EmojiDecoder;
import jp.juggler.subwaytooter.util.LinkClickContext;
@ -409,7 +411,8 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener,
sv = intent.getStringExtra( KEY_REPLY_STATUS );
if( sv != null ){
try{
TootStatus reply_status = TootStatus.parse( ActPost.this, account, new JSONObject( sv ) );
TootStatus reply_status = new TootParser( ActPost.this, account).status(new JSONObject( sv ));
if( reply_status != null ){
// CW をリプライ元に合わせる
@ -874,7 +877,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener,
TootApiResult result = client.request( path );
if( result != null && result.object != null ){
TootResults tmp = TootResults.parse( ActPost.this, access_info, result.object );
TootResults tmp = new TootParser( ActPost.this, access_info).results( result.object );
if( tmp != null && tmp.statuses != null && ! tmp.statuses.isEmpty() ){
target_status = tmp.statuses.get( 0 );
}
@ -1351,7 +1354,7 @@ public class ActPost extends AppCompatActivity implements View.OnClickListener,
Editable e = etContent.getEditableText();
int len = e.length();
char last_char = ( len <= 0 ? ' ' : e.charAt( len - 1 ) );
if( ! EmojiDecoder.isWhitespaceBeforeEmoji( last_char ) ){
if( ! CharacterGroup.isWhitespace( last_char ) ){
e.append( " " ).append( pa.attachment.text_url );
}else{
e.append( pa.attachment.text_url );

View File

@ -20,7 +20,6 @@ import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
import com.bumptech.glide.load.model.GlideUrl;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.concurrent.BlockingQueue;
@ -33,6 +32,7 @@ import java.util.concurrent.atomic.AtomicInteger;
import jp.juggler.subwaytooter.api.entity.TootAttachment;
import jp.juggler.subwaytooter.table.AcctColor;
import jp.juggler.subwaytooter.table.AcctSet;
import jp.juggler.subwaytooter.table.HighlightWord;
import jp.juggler.subwaytooter.table.MutedApp;
import jp.juggler.subwaytooter.table.ClientInfo;
import jp.juggler.subwaytooter.table.ContentWarning;
@ -54,7 +54,6 @@ import okhttp3.CacheControl;
import okhttp3.Call;
import okhttp3.CipherSuite;
import okhttp3.ConnectionSpec;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Response;
import uk.co.chrisjenx.calligraphy.CalligraphyConfig;
@ -77,7 +76,7 @@ public class App1 extends Application {
public static final String FILE_PROVIDER_AUTHORITY = "jp.juggler.subwaytooter.FileProvider";
static final String DB_NAME = "app_db";
static final int DB_VERSION = 20;
static final int DB_VERSION = 21;
// 2017/4/25 v10 1=>2 SavedAccount に通知設定を追加
// 2017/4/25 v10 1=>2 NotificationTracking テーブルを追加
@ -97,6 +96,7 @@ public class App1 extends Application {
// 2017/9/23 v161 17=>18 SavedAccountに項目追加
// 2017/9/23 v161 18=>19 ClientInfoテーブルを置き換える
// 2017/12/01 v175 19=>20 UserRelation に項目追加
// 2018/1/03 v197 20=>21 HighlightWord テーブルを追加
private static DBOpenHelper db_open_helper;
@ -125,6 +125,7 @@ public class App1 extends Application {
MutedWord.onDBCreate( db );
PostDraft.onDBCreate( db );
TagSet.onDBCreate( db );
HighlightWord.onDBCreate( db );
}
@Override public void onUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){
@ -142,6 +143,7 @@ public class App1 extends Application {
MutedWord.onDBUpgrade( db, oldVersion, newVersion );
PostDraft.onDBUpgrade( db, oldVersion, newVersion );
TagSet.onDBUpgrade( db, oldVersion, newVersion );
HighlightWord.onDBUpgrade( db, oldVersion, newVersion );
}
}
@ -536,4 +538,11 @@ public class App1 extends Application {
openCustomTab( activity, url );
}
}
public static void sound( @NonNull HighlightWord item ){
if( app_state != null ){
app_state.sound( item );
}
}
}

View File

@ -6,6 +6,9 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.SystemClock;
@ -24,6 +27,7 @@ import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
@ -36,6 +40,7 @@ import java.util.regex.Pattern;
import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.api.entity.TootStatusLike;
import jp.juggler.subwaytooter.table.HighlightWord;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.MyClickableSpan;
@ -484,4 +489,58 @@ public class AppState {
}
};
private WeakReference< Ringtone > last_ringtone;
private void stopLastRingtone(){
Ringtone r = last_ringtone == null ? null : last_ringtone.get();
if( r != null ){
try{
r.stop();
}catch( Throwable ex ){
log.trace( ex );
}finally{
last_ringtone = null;
}
}
}
private long last_sound;
void sound( @NonNull HighlightWord item ){
// 短時間に何度もならないようにする
long now = SystemClock.elapsedRealtime();
if( now - last_sound < 500L ) return;
last_sound = now;
stopLastRingtone();
if( item.sound_type == HighlightWord.SOUND_TYPE_NONE ) return;
if( item.sound_type == HighlightWord.SOUND_TYPE_CUSTOM
&& ! TextUtils.isEmpty( item.sound_uri )
){
try{
Ringtone ringtone = RingtoneManager.getRingtone( context, Uri.parse( item.sound_uri ) );
if( ringtone != null ){
last_ringtone = new WeakReference<>( ringtone );
ringtone.play();
return;
}
}catch( Throwable ex ){
log.trace( ex );
}
}
Uri uri = RingtoneManager.getDefaultUri( RingtoneManager.TYPE_NOTIFICATION );
try{
Ringtone ringtone = RingtoneManager.getRingtone( context, uri );
if( ringtone != null ){
last_ringtone = new WeakReference<>( ringtone );
ringtone.play();
}
}catch( Throwable ex ){
log.trace( ex );
}
}
}

View File

@ -39,6 +39,8 @@ import jp.juggler.subwaytooter.api.entity.TootRelationShip;
import jp.juggler.subwaytooter.api.entity.TootReport;
import jp.juggler.subwaytooter.api.entity.TootResults;
import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.api.TootParser;
import jp.juggler.subwaytooter.api.entity.TootStatusLike;
import jp.juggler.subwaytooter.api.entity.TootTag;
import jp.juggler.subwaytooter.api_msp.MSPClient;
import jp.juggler.subwaytooter.api_msp.entity.MSPToot;
@ -46,6 +48,7 @@ import jp.juggler.subwaytooter.api_tootsearch.TSClient;
import jp.juggler.subwaytooter.api_tootsearch.entity.TSToot;
import jp.juggler.subwaytooter.table.AcctColor;
import jp.juggler.subwaytooter.table.AcctSet;
import jp.juggler.subwaytooter.table.HighlightWord;
import jp.juggler.subwaytooter.table.MutedApp;
import jp.juggler.subwaytooter.table.MutedWord;
import jp.juggler.subwaytooter.table.SavedAccount;
@ -1171,6 +1174,7 @@ import jp.juggler.subwaytooter.util.Utils;
private Pattern column_regex_filter;
private HashSet< String > muted_app;
private WordTrieTree muted_word;
private WordTrieTree highlight_trie;
private void initFilter(){
column_regex_filter = null;
@ -1184,6 +1188,7 @@ import jp.juggler.subwaytooter.util.Utils;
muted_app = MutedApp.getNameSet();
muted_word = MutedWord.getNameSet();
highlight_trie = HighlightWord.getNameSet();
}
private boolean isFiltered( @NonNull TootStatus status ){
@ -1378,6 +1383,8 @@ import jp.juggler.subwaytooter.util.Utils;
fireShowContent();
@SuppressLint("StaticFieldLeak") AsyncTask< Void, Void, TootApiResult > task = this.last_task = new AsyncTask< Void, Void, TootApiResult >() {
TootParser parser = new TootParser( context, access_info).setHighlightTrie( highlight_trie );
TootInstance instance_tmp;
@ -1398,7 +1405,10 @@ import jp.juggler.subwaytooter.util.Utils;
TootApiResult result = client.request( path_base );
if( result != null && result.array != null ){
//
TootStatus.List src = TootStatus.parseList( context, access_info, result.array, true );
TootStatus.List src = new TootParser( context, access_info)
.setPinned( true )
.setHighlightTrie( highlight_trie )
.statusList( result.array );
for( TootStatus status : src ){
log.d( "pinned: %s %s", status.id, status.decoded_content );
@ -1407,52 +1417,7 @@ import jp.juggler.subwaytooter.util.Utils;
list_pinned = new ArrayList<>( src.size() );
addWithFilter( list_pinned, src );
// 1.6rc では以下の理由により40overの固定トゥートを取得することは困難である
// - max_idを指定せずにAPIで取得すると適当な件数のリストが返ってくるソート順はpinした日時max_idはリスト中の最後の要素のIDを返す
// - max_idを指定してAPIで取得するとステータスIDがmax_idより小さい&pinされているトゥートをpin日時順にソートしたものが返ってくる
// - max_idはpin日時を考慮していないのだからページング用のパラメータとしては全く不適切である
// - 取得できるステータスにはpinされた日時は含まれない
// //
// // pinステータスは独自にページ管理する
// long time_start = SystemClock.elapsedRealtime();
// String max_id = parseMaxId( result );
// char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' );
// for( ; ; ){
//
// if( client.isCancelled() ){
// log.d( "loading-statuses-pinned: cancelled." );
// break;
// }
// if( max_id == null ){
// log.d( "loading-statuses-pinned: max_id is null." );
// break;
// }
// if( src.isEmpty() ){
// log.d( "loading-statuses-pinned: previous response is empty." );
// break;
// }
// if( SystemClock.elapsedRealtime() - time_start > LOOP_TIMEOUT ){
// log.d( "loading-statuses-pinned: timeout." );
// break;
// }
//
// String path = path_base + delimiter + "max_id=" + max_id;
// TootApiResult result2 = client.request( path );
// if( result2 == null || result2.array == null ){
// log.d( "loading-statuses-pinned: error or cancelled." );
// break;
// }
//
// src = TootStatus.parseList( context, access_info, result2.array ,true);
// for(TootStatus status : src ){
// log.d("pinned: %s %s",status.id, status.decoded_content);
// }
//
// addWithFilter( list_pinned, src );
//
// // pinnedステータスは独自にページ管理する
// max_id = parseMaxId( result2 );
// }
// pinned tootにはページングの概念はない
}
log.d( "getStatusesPinned: list size=%s", list_pinned == null ? - 1 : list_pinned.size() );
}
@ -1460,12 +1425,14 @@ import jp.juggler.subwaytooter.util.Utils;
ArrayList< Object > list_tmp;
TootApiResult getStatuses( TootApiClient client, String path_base ){
long time_start = SystemClock.elapsedRealtime();
TootApiResult result = client.request( path_base );
if( result != null && result.array != null ){
saveRange( result, true, true );
//
TootStatus.List src = TootStatus.parseList( context, access_info, result.array );
TootStatus.List src = parser.statusList( result.array );
list_tmp = new ArrayList<>( src.size() );
addWithFilter( list_tmp, src );
//
@ -1502,7 +1469,7 @@ import jp.juggler.subwaytooter.util.Utils;
break;
}
src = TootStatus.parseList( context, access_info, result2.array );
src = parser.statusList( result2.array );
addWithFilter( list_tmp, src );
@ -1564,7 +1531,7 @@ import jp.juggler.subwaytooter.util.Utils;
if( result != null && result.array != null ){
saveRange( result, true, true );
//
TootNotification.List src = TootNotification.parseList( context, access_info, result.array );
TootNotification.List src = parser.notificationList( result.array );
list_tmp = new ArrayList<>( src.size() );
addWithFilter( list_tmp, src );
//
@ -1605,7 +1572,7 @@ import jp.juggler.subwaytooter.util.Utils;
break;
}
src = TootNotification.parseList( context, access_info, result2.array );
src = parser.notificationList( result2.array );
addWithFilter( list_tmp, src );
@ -1741,7 +1708,7 @@ import jp.juggler.subwaytooter.util.Utils;
result = client.request(
String.format( Locale.JAPAN, PATH_STATUSES, status_id ) );
if( result == null || result.object == null ) return result;
TootStatus target_status = TootStatus.parse( context, access_info, result.object );
TootStatus target_status = parser.status( result.object );
if( target_status == null ){
return new TootApiResult( "TootStatus parse failed." );
}
@ -1753,7 +1720,7 @@ import jp.juggler.subwaytooter.util.Utils;
if( result == null || result.object == null ) return result;
// 一つのリストにまとめる
TootContext conversation_context = TootContext.parse( context, access_info, result.object );
TootContext conversation_context = parser.context( result.object );
if( conversation_context != null ){
list_tmp = new ArrayList<>( 1 + conversation_context.ancestors.size() + conversation_context.descendants.size() );
if( conversation_context.ancestors != null )
@ -1792,7 +1759,7 @@ import jp.juggler.subwaytooter.util.Utils;
result = client.request( path );
if( result == null || result.object == null ) return result;
TootResults tmp = TootResults.parse( context, access_info, result.object );
TootResults tmp = parser.results( result.object );
if( tmp != null ){
list_tmp = new ArrayList<>();
list_tmp.addAll( tmp.hashtags );
@ -1831,7 +1798,7 @@ import jp.juggler.subwaytooter.util.Utils;
// max_id の更新
max_id = MSPClient.getMaxId( result.array, max_id );
// リストデータの用意
MSPToot.List search_result = MSPToot.parseList( context, access_info, result.array );
MSPToot.List search_result = MSPToot.parseList(parser, result.array );
if( search_result != null ){
list_tmp = new ArrayList<>();
addWithFilter( list_tmp, search_result );
@ -1867,7 +1834,7 @@ import jp.juggler.subwaytooter.util.Utils;
// max_id の更新
max_id = TSClient.getMaxId( result.object, max_id );
// リストデータの用意
TSToot.List search_result = TSToot.parseList( context, access_info, result.object );
TSToot.List search_result = TSToot.parseList( parser, result.object );
list_tmp = new ArrayList<>();
addWithFilter( list_tmp, search_result );
if( search_result.isEmpty() ){
@ -2194,6 +2161,7 @@ import jp.juggler.subwaytooter.util.Utils;
mRefreshLoadingError = null;
@SuppressLint("StaticFieldLeak") AsyncTask< Void, Void, TootApiResult > task = this.last_task = new AsyncTask< Void, Void, TootApiResult >() {
TootParser parser = new TootParser( context, access_info).setHighlightTrie( highlight_trie );
TootApiResult getAccountList( TootApiClient client, String path_base ){
long time_start = SystemClock.elapsedRealtime();
@ -2417,7 +2385,7 @@ import jp.juggler.subwaytooter.util.Utils;
if( result != null && result.array != null ){
saveRange( result, bBottom, ! bBottom );
list_tmp = new ArrayList<>();
TootNotification.List src = TootNotification.parseList( context, access_info, result.array );
TootNotification.List src = parser.notificationList( result.array );
addWithFilter( list_tmp, src );
if( ! bBottom ){
@ -2462,7 +2430,7 @@ import jp.juggler.subwaytooter.util.Utils;
break;
}
src = TootNotification.parseList( context, access_info, result2.array );
src = parser.notificationList( result2.array );
if( ! src.isEmpty() ){
addWithFilter( list_tmp, src );
PollingWorker.injectData( context, access_info.db_id, src );
@ -2507,7 +2475,7 @@ import jp.juggler.subwaytooter.util.Utils;
break;
}
src = TootNotification.parseList( context, access_info, result2.array );
src = parser.notificationList( result2.array );
addWithFilter( list_tmp, src );
@ -2524,6 +2492,7 @@ import jp.juggler.subwaytooter.util.Utils;
ArrayList< Object > list_tmp;
TootApiResult getStatusList( TootApiClient client, String path_base ){
long time_start = SystemClock.elapsedRealtime();
char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' );
@ -2532,7 +2501,7 @@ import jp.juggler.subwaytooter.util.Utils;
TootApiResult result = client.request( addRange( bBottom, path_base ) );
if( result != null && result.array != null ){
saveRange( result, bBottom, ! bBottom );
TootStatus.List src = TootStatus.parseList( context, access_info, result.array );
TootStatus.List src = parser.statusList( result.array );
list_tmp = new ArrayList<>();
addWithFilter( list_tmp, src );
@ -2576,7 +2545,7 @@ import jp.juggler.subwaytooter.util.Utils;
break;
}
src = TootStatus.parseList( context, access_info, result2.array );
src = parser.statusList( result2.array );
addWithFilter( list_tmp, src );
@ -2632,7 +2601,7 @@ import jp.juggler.subwaytooter.util.Utils;
break;
}
src = TootStatus.parseList( context, access_info, result2.array );
src = parser.statusList( result2.array );
addWithFilter( list_tmp, src );
}
}
@ -2773,7 +2742,7 @@ import jp.juggler.subwaytooter.util.Utils;
// max_id の更新
max_id = MSPClient.getMaxId( result.array, max_id );
// リストデータの用意
MSPToot.List search_result = MSPToot.parseList( context, access_info, result.array );
MSPToot.List search_result = MSPToot.parseList( parser, result.array );
if( search_result != null ){
list_tmp = new ArrayList<>();
addWithFilter( list_tmp, search_result );
@ -2813,7 +2782,7 @@ import jp.juggler.subwaytooter.util.Utils;
// max_id の更新
max_id = TSClient.getMaxId( result.object, max_id );
// リストデータの用意
TSToot.List search_result = TSToot.parseList( context, access_info, result.object );
TSToot.List search_result = TSToot.parseList( parser, result.object );
list_tmp = new ArrayList<>();
addWithFilter( list_tmp, search_result );
}
@ -2863,6 +2832,8 @@ import jp.juggler.subwaytooter.util.Utils;
return;
}
// 事前にスクロール位置を覚えておく
ScrollPosition sp = null;
ColumnViewHolder holder = getViewHolder();
@ -2879,6 +2850,16 @@ import jp.juggler.subwaytooter.util.Utils;
}
}else{
for( Object o : list_new ){
if( o instanceof TootStatusLike){
TootStatusLike s = (TootStatusLike) o;
if( s.highlight_sound != null ){
App1.sound( s.highlight_sound );
break;
}
}
}
int status_index = - 1;
for( int i = 0, ie = list_new.size() ; i < ie ; ++ i ){
Object o = list_new.get( i );
@ -2949,6 +2930,9 @@ import jp.juggler.subwaytooter.util.Utils;
final String since_id = gap.since_id;
ArrayList< Object > list_tmp;
TootParser parser = new TootParser( context, access_info).setHighlightTrie( highlight_trie );
TootApiResult getAccountList( TootApiClient client, String path_base ){
long time_start = SystemClock.elapsedRealtime();
char delimiter = ( - 1 != path_base.indexOf( '?' ) ? '&' : '?' );
@ -3076,7 +3060,7 @@ import jp.juggler.subwaytooter.util.Utils;
}
result = r2;
TootNotification.List src = TootNotification.parseList( context, access_info, r2.array );
TootNotification.List src = parser.notificationList( r2.array );
if( src.isEmpty() ){
log.d( "gap-notification: empty." );
@ -3132,7 +3116,7 @@ import jp.juggler.subwaytooter.util.Utils;
// 成功した場合はそれを返したい
result = r2;
TootStatus.List src = TootStatus.parseList( context, access_info, r2.array );
TootStatus.List src = parser.statusList( r2.array );
if( src.size() == 0 ){
// 直前の取得でカラのデータが帰ってきたら終了
log.d( "gap-statuses: empty." );
@ -3324,6 +3308,7 @@ import jp.juggler.subwaytooter.util.Utils;
task.executeOnExecutor( App1.task_executor );
}
private static final int heightSpec = View.MeasureSpec.makeMeasureSpec( 0, View.MeasureSpec.UNSPECIFIED );
private static int getListItemHeight( ListView listView, int idx ){
@ -3465,6 +3450,16 @@ import jp.juggler.subwaytooter.util.Utils;
}
}
for( Object o : list_new ){
if( o instanceof TootStatusLike){
TootStatusLike s = (TootStatusLike) o;
if( s.highlight_sound != null ){
App1.sound( s.highlight_sound );
break;
}
}
}
list_data.addAll( 0, list_new );
fireShowContent();
int added = list_new.size();
@ -3702,6 +3697,7 @@ import jp.juggler.subwaytooter.util.Utils;
app_state.stream_reader.register(
access_info
, stream_path
, highlight_trie
, this
);
}

View File

@ -51,6 +51,7 @@ import jp.juggler.subwaytooter.api.TootApiClient;
import jp.juggler.subwaytooter.api.TootApiResult;
import jp.juggler.subwaytooter.api.entity.TootNotification;
import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.api.TootParser;
import jp.juggler.subwaytooter.table.AcctColor;
import jp.juggler.subwaytooter.table.MutedApp;
import jp.juggler.subwaytooter.table.MutedWord;
@ -1031,6 +1032,8 @@ public class PollingWorker {
){
nr = NotificationTracking.load( account.db_id );
TootParser parser = new TootParser( context,account );
// まずキャッシュされたデータを処理する
if( nr.last_data != null ){
try{
@ -1038,7 +1041,7 @@ public class PollingWorker {
for( int i = array.length() - 1 ; i >= 0 ; -- i ){
if( job.isJobCancelled() ) return;
JSONObject src = array.optJSONObject( i );
update_sub( src, data_list, muted_app, muted_word );
update_sub( src, data_list, muted_app, muted_word ,parser);
}
}catch( JSONException ex ){
log.trace( ex );
@ -1054,6 +1057,7 @@ public class PollingWorker {
client.setAccount( account );
for( int nTry = 0 ; nTry < 4 ; ++ nTry ){
if( job.isJobCancelled() ) return;
@ -1071,7 +1075,7 @@ public class PollingWorker {
JSONArray array = result.array;
for( int i = array.length() - 1 ; i >= 0 ; -- i ){
JSONObject src = array.optJSONObject( i );
update_sub( src, data_list, muted_app, muted_word );
update_sub( src, data_list, muted_app, muted_word ,parser);
}
}catch( JSONException ex ){
log.trace( ex );
@ -1130,6 +1134,7 @@ public class PollingWorker {
, @NonNull ArrayList< Data > data_list
, @NonNull HashSet< String > muted_app
, @NonNull WordTrieTree muted_word
, @NonNull TootParser parser
) throws JSONException{
if( nr.nid_read == 0 || nr.nid_show == 0 ){
@ -1168,7 +1173,7 @@ public class PollingWorker {
return;
}
TootNotification notification = TootNotification.parse( context, account, src );
TootNotification notification = parser.notification( src );
if( notification == null ){
return;
}

View File

@ -21,11 +21,11 @@ import jp.juggler.subwaytooter.api.TootApiClient;
import jp.juggler.subwaytooter.api.TootApiResult;
import jp.juggler.subwaytooter.api.TootTask;
import jp.juggler.subwaytooter.api.TootTaskRunner;
import jp.juggler.subwaytooter.api.entity.TootNotification;
import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.api.TootParser;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
import jp.juggler.subwaytooter.util.WordTrieTree;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
@ -35,7 +35,7 @@ import okhttp3.WebSocketListener;
static final LogCategory log = new LogCategory( "StreamReader" );
static final Pattern reNumber = Pattern.compile( "([-]?\\d+)" );
static final Pattern reAuthorizeError = Pattern.compile( "authorize",Pattern.CASE_INSENSITIVE );
static final Pattern reAuthorizeError = Pattern.compile( "authorize", Pattern.CASE_INSENSITIVE );
interface Callback {
void onStreamingMessage( String event_type, Object o );
@ -45,10 +45,16 @@ import okhttp3.WebSocketListener;
final SavedAccount access_info;
final String end_point;
final LinkedList< Callback > callback_list = new LinkedList<>();
final TootParser parser;
Reader( SavedAccount access_info, String end_point ){
Reader( SavedAccount access_info, String end_point , WordTrieTree highlight_trie){
this.access_info = access_info;
this.end_point = end_point;
this.parser = new TootParser( context, access_info ).setHighlightTrie( highlight_trie );
}
synchronized void updateHighlight( WordTrieTree highlight_trie ){
this.parser.setHighlightTrie( highlight_trie );
}
synchronized void addCallback( @NonNull Callback stream_callback ){
@ -121,7 +127,7 @@ import okhttp3.WebSocketListener;
static final String PAYLOAD = "payload";
// ストリーミングAPIのペイロード部分をTootStatus,TootNotification,整数IDのどれかに解釈する
private Object parsePayload( @NonNull String event, @NonNull JSONObject parent, @NonNull String parent_text ){
synchronized private Object parsePayload( @NonNull String event, @NonNull JSONObject parent, @NonNull String parent_text ){
try{
if( parent.isNull( PAYLOAD ) ){
return null;
@ -135,11 +141,11 @@ import okhttp3.WebSocketListener;
case "update":
// ここを通るケースはまだ確認できていない
return TootStatus.parse( context, access_info, src );
return parser.status( src );
case "notification":
// ここを通るケースはまだ確認できていない
return TootNotification.parse( context, access_info, src );
return parser.notification( src );
default:
// ここを通るケースはまだ確認できていない
@ -161,11 +167,11 @@ import okhttp3.WebSocketListener;
switch( event ){
case "update":
// 2017/8/24 18:37 mastodon.juggler.jpでここを通った
return TootStatus.parse( context, access_info, src );
return parser.status( src );
case "notification":
// 2017/8/24 18:37 mastodon.juggler.jpでここを通った
return TootNotification.parse( context, access_info, src );
return parser.notification( src );
default:
// ここを通るケースはまだ確認できていない
@ -222,14 +228,13 @@ import okhttp3.WebSocketListener;
public void onFailure( WebSocket webSocket, Throwable ex, Response response ){
log.e( ex, "WebSocket onFailure. url=%s .", webSocket.request().url() );
bListening.set( false );
handler.removeCallbacks( proc_reconnect );
if( ex instanceof ProtocolException ){
String msg = ex.getMessage();
if(msg != null && reAuthorizeError.matcher( msg).find() ){
log.e("seems old instance that does not support streaming public timeline without access token. don't retry...");
if( msg != null && reAuthorizeError.matcher( msg ).find() ){
log.e( "seems old instance that does not support streaming public timeline without access token. don't retry..." );
return;
}
}
@ -285,16 +290,17 @@ import okhttp3.WebSocketListener;
this.handler = handler;
}
private Reader prepareReader( @NonNull SavedAccount access_info, @NonNull String end_point ){
private Reader prepareReader( @NonNull SavedAccount access_info, @NonNull String end_point ,WordTrieTree highlight_trie ){
synchronized( reader_list ){
for( Reader reader : reader_list ){
if( reader.access_info.db_id == access_info.db_id
&& reader.end_point.equals( end_point )
){
if( highlight_trie != null ) reader.updateHighlight( highlight_trie );
return reader;
}
}
Reader reader = new Reader( access_info, end_point );
Reader reader = new Reader( access_info, end_point ,highlight_trie);
reader_list.add( reader );
return reader;
}
@ -332,9 +338,9 @@ import okhttp3.WebSocketListener;
}
// onResume ロード完了ののタイミングで登録される
void register( @NonNull SavedAccount access_info, @NonNull String end_point, @NonNull Callback stream_callback ){
void register( @NonNull SavedAccount access_info, @NonNull String end_point, @Nullable WordTrieTree highlight_trie , @NonNull Callback stream_callback ){
final Reader reader = prepareReader( access_info, end_point );
final Reader reader = prepareReader( access_info, end_point ,highlight_trie);
reader.addCallback( stream_callback );
if( ! reader.bListening.get() ){

View File

@ -21,6 +21,7 @@ import jp.juggler.subwaytooter.api.TootTaskRunner;
import jp.juggler.subwaytooter.api.entity.TootResults;
import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.api.entity.TootStatusLike;
import jp.juggler.subwaytooter.api.TootParser;
import jp.juggler.subwaytooter.api_msp.entity.MSPToot;
import jp.juggler.subwaytooter.api_tootsearch.entity.TSToot;
import jp.juggler.subwaytooter.dialog.AccountPicker;
@ -95,7 +96,7 @@ public class Action_Toot {
return result;
}
target_status = null;
TootResults tmp = TootResults.parse( activity, access_info, result.object );
TootResults tmp = new TootParser( activity, access_info).results( result.object );
if( tmp != null ){
if( tmp.statuses != null && ! tmp.statuses.isEmpty() ){
target_status = tmp.statuses.get( 0 );
@ -125,7 +126,7 @@ public class Action_Toot {
)
, request_builder );
if( result != null && result.object != null ){
new_status = TootStatus.parse( activity, access_info, result.object );
new_status = new TootParser( activity, access_info).status( result.object );
}
return result;
@ -265,6 +266,8 @@ public class Action_Toot {
new TootTaskRunner( activity, false ).run( access_info, new TootTask() {
@Override public TootApiResult background( @NonNull TootApiClient client ){
TootParser parser = new TootParser( activity, access_info);
TootApiResult result;
TootStatusLike target_status;
@ -278,7 +281,7 @@ public class Action_Toot {
return result;
}
target_status = null;
TootResults tmp = TootResults.parse( activity, access_info, result.object );
TootResults tmp = parser.results( result.object );
if( tmp != null ){
if( tmp.statuses != null && ! tmp.statuses.isEmpty() ){
target_status = tmp.statuses.get( 0 );
@ -306,7 +309,7 @@ public class Action_Toot {
if( result != null && result.object != null ){
new_status = TootStatus.parse( activity, access_info, result.object );
new_status = parser .status( result.object );
// reblogはreblogを表すStatusを返す
// unreblogはreblogしたStatusを返す
@ -612,7 +615,7 @@ public class Action_Toot {
path = path + "&resolve=1";
result = client.request( path );
if( result != null && result.object != null ){
TootResults tmp = TootResults.parse( activity, access_info, result.object );
TootResults tmp = new TootParser( activity, access_info).results( result.object );
if( tmp != null && tmp.statuses != null && ! tmp.statuses.isEmpty() ){
TootStatus status = tmp.statuses.get( 0 );
local_status_id = status.id;
@ -672,7 +675,7 @@ public class Action_Toot {
)
, request_builder );
if( result != null && result.object != null ){
new_status = TootStatus.parse( activity, access_info, result.object );
new_status = new TootParser( activity, access_info).status( result.object );
}
return result;
@ -759,7 +762,7 @@ public class Action_Toot {
TootApiResult result = client.request( path );
if( result != null && result.object != null ){
TootResults tmp = TootResults.parse( activity, access_info, result.object );
TootResults tmp = new TootParser( activity, access_info).results( result.object );
if( tmp != null && tmp.statuses != null && ! tmp.statuses.isEmpty() ){
local_status = tmp.statuses.get( 0 );
log.d( "status id conversion %s => %s", remote_status_url, local_status.id );
@ -808,7 +811,7 @@ public class Action_Toot {
);
if( result != null && result.object != null ){
local_status = TootStatus.parse( activity, access_info, result.object );
local_status = new TootParser( activity, access_info).status( result.object );
}
return result;

View File

@ -22,6 +22,7 @@ import jp.juggler.subwaytooter.api.entity.TootAccount;
import jp.juggler.subwaytooter.api.entity.TootRelationShip;
import jp.juggler.subwaytooter.api.entity.TootResults;
import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.api.TootParser;
import jp.juggler.subwaytooter.dialog.AccountPicker;
import jp.juggler.subwaytooter.dialog.ReportForm;
import jp.juggler.subwaytooter.table.AcctColor;
@ -224,7 +225,7 @@ public class Action_User {
if( result != null && result.object != null ){
TootResults tmp = TootResults.parse( activity, access_info, result.object );
TootResults tmp = new TootParser( activity, access_info ).results( result.object );
if( tmp != null ){
if( tmp.accounts != null && ! tmp.accounts.isEmpty() ){
who_local = tmp.accounts.get( 0 );

View File

@ -0,0 +1,76 @@
package jp.juggler.subwaytooter.api;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
import jp.juggler.subwaytooter.api.entity.TootAccount;
import jp.juggler.subwaytooter.api.entity.TootContext;
import jp.juggler.subwaytooter.api.entity.TootNotification;
import jp.juggler.subwaytooter.api.entity.TootResults;
import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.WordTrieTree;
public class TootParser {
@NonNull public final Context context;
@NonNull public final SavedAccount access_info;
public TootParser( @NonNull Context context, @NonNull SavedAccount access_info ){
this.context = context;
this.access_info = access_info;
}
////////////////////////////////////////////////////////
// parser options
// プロフィールカラムからpinned TL を読んだ時だけ真
public boolean isPinned;
public TootParser setPinned( boolean isPinned ){
this.isPinned = isPinned;
return this;
}
@Nullable public WordTrieTree highlight_trie;
public TootParser setHighlightTrie( @Nullable WordTrieTree highlight_trie ){
this.highlight_trie = highlight_trie;
return this;
}
/////////////////////////////////////////////////////////
// parser methods
@Nullable public TootAccount account( @Nullable JSONObject src ){
return TootAccount.parse( context,access_info, src );
}
@Nullable public TootStatus status( @Nullable JSONObject src ){
return TootStatus.parse( this, src );
}
@NonNull public TootStatus.List statusList( @Nullable JSONArray array ){
return TootStatus.parseList( this, array );
}
@Nullable public TootNotification notification( @Nullable JSONObject src ){
return TootNotification.parse(this,src);
}
@NonNull public TootNotification.List notificationList( @Nullable JSONArray src ){
return TootNotification.parseList( this, src );
}
@Nullable public TootResults results( @Nullable JSONObject src ){
return TootResults.parse( this, src );
}
@Nullable public TootContext context( @Nullable JSONObject src ){
return TootContext.parse(this, src );
}
}

View File

@ -1,12 +1,11 @@
package jp.juggler.subwaytooter.api.entity;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.json.JSONObject;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.api.TootParser;
import jp.juggler.subwaytooter.util.LogCategory;
public class TootContext {
@ -19,12 +18,12 @@ public class TootContext {
public TootStatus.List descendants;
@Nullable
public static TootContext parse( @NonNull Context context, @NonNull SavedAccount access_info, JSONObject src ){
public static TootContext parse( @NonNull TootParser parser , JSONObject src ){
if( src == null ) return null;
try{
TootContext dst = new TootContext();
dst.ancestors = TootStatus.parseList( context, access_info, src.optJSONArray( "ancestors" ) );
dst.descendants = TootStatus.parseList( context, access_info, src.optJSONArray( "descendants" ) );
dst.ancestors = TootStatus.parseList( parser, src.optJSONArray( "ancestors" ) );
dst.descendants = TootStatus.parseList( parser, src.optJSONArray( "descendants" ) );
return dst;
}catch( Throwable ex ){
log.trace( ex );

View File

@ -1,6 +1,5 @@
package jp.juggler.subwaytooter.api.entity;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@ -9,7 +8,7 @@ import org.json.JSONObject;
import java.util.ArrayList;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.api.TootParser;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
@ -41,16 +40,16 @@ public class TootNotification extends TootId {
public JSONObject json;
@Nullable
public static TootNotification parse( @NonNull Context context, @NonNull SavedAccount access_info, JSONObject src ){
public static TootNotification parse( @NonNull TootParser parser, JSONObject src ){
if( src == null ) return null;
try{
TootNotification dst = new TootNotification();
dst.json = src;
dst.id = Utils.optLongX(src, "id" );
dst.id = Utils.optLongX( src, "id" );
dst.type = Utils.optStringX( src, "type" );
dst.created_at = Utils.optStringX( src, "created_at" );
dst.account = TootAccount.parse( context, access_info, src.optJSONObject( "account" ) );
dst.status = TootStatus.parse( context, access_info, src.optJSONObject( "status" ) );
dst.account = TootAccount.parse( parser.context, parser.access_info, src.optJSONObject( "account" ) );
dst.status = TootStatus.parse( parser, src.optJSONObject( "status" ) );
dst.time_created_at = TootStatus.parseTime( dst.created_at );
@ -73,7 +72,7 @@ public class TootNotification extends TootId {
}
@NonNull
public static List parseList( @NonNull Context context, @NonNull SavedAccount access_info, JSONArray array ){
public static List parseList( @NonNull TootParser parser, JSONArray array ){
List result = new List();
if( array != null ){
int array_size = array.length();
@ -81,7 +80,7 @@ public class TootNotification extends TootId {
for( int i = 0 ; i < array_size ; ++ i ){
JSONObject src = array.optJSONObject( i );
if( src == null ) continue;
TootNotification item = parse( context, access_info, src );
TootNotification item = parse( parser, src );
if( item != null ) result.add( item );
}
}

View File

@ -1,6 +1,5 @@
package jp.juggler.subwaytooter.api.entity;
import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
@ -8,7 +7,7 @@ import org.json.JSONObject;
import java.util.ArrayList;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.api.TootParser;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
@ -26,12 +25,12 @@ public class TootResults {
public ArrayList< String > hashtags;
@Nullable
public static TootResults parse( @NonNull Context context, @NonNull SavedAccount access_info, JSONObject src ){
public static TootResults parse( @NonNull TootParser parser, JSONObject src ){
try{
if( src == null ) return null;
TootResults dst = new TootResults();
dst.accounts = TootAccount.parseList( context, access_info, src.optJSONArray( "accounts" ) );
dst.statuses = TootStatus.parseList( context, access_info, src.optJSONArray( "statuses" ) );
dst.accounts = TootAccount.parseList( parser.context, parser.access_info, src.optJSONArray( "accounts" ) );
dst.statuses = TootStatus.parseList( parser, src.optJSONArray( "statuses" ) );
dst.hashtags = Utils.parseStringArray( src.optJSONArray( "hashtags" ) );
return dst;
}catch( Throwable ex ){

View File

@ -23,6 +23,7 @@ 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.TootParser;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.DecodeOptions;
import jp.juggler.subwaytooter.util.HTMLDecoder;
@ -88,28 +89,19 @@ public class TootStatus extends TootStatusLike {
public NicoEnquete enquete;
@Nullable
public static TootStatus parse( @NonNull Context context, @NonNull SavedAccount access_info, JSONObject src ){
return parse( context,access_info,src,false);
}
@Nullable
public static TootStatus parse( @NonNull Context context, @NonNull SavedAccount access_info, JSONObject src ,boolean bPinned){
/*
bPinned 引数がtrueになるのはプロフィールカラムからpinned TL を読んだ時だけである
*/
public static TootStatus parse( @NonNull TootParser parser, @Nullable JSONObject src ){
if( src == null ) return null;
// log.d( "parse: %s", src.toString() );
try{
TootStatus status = new TootStatus();
status.json = src;
// 絵文字マップは割と最初の方で読み込んでおきたい
status.custom_emojis = CustomEmoji.parseMap( src.optJSONArray( "emojis" ),access_info.host);
status.custom_emojis = CustomEmoji.parseMap( src.optJSONArray( "emojis" ), parser.access_info.host );
status.profile_emojis = NicoProfileEmoji.parseMap( src.optJSONArray( "profile_emojis" ) );
status.account = TootAccount.parse( context, access_info, src.optJSONObject( "account" ) );
status.account = TootAccount.parse( parser.context, parser.access_info, src.optJSONObject( "account" ) );
if( status.account == null ) return null;
@ -117,20 +109,21 @@ public class TootStatus extends TootStatusLike {
status.uri = Utils.optStringX( src, "uri" );
status.url = Utils.optStringX( src, "url" );
status.host_access = access_info.host;
status.host_access = parser.access_info.host;
status.host_original = status.account.getAcctHost();
if( status.host_original == null ){
status.host_original = access_info.host;
status.host_original = parser.access_info.host;
}
status.in_reply_to_id = Utils.optStringX( src, "in_reply_to_id" ); // null
status.in_reply_to_account_id = Utils.optStringX( src, "in_reply_to_account_id" ); // null
status.reblog = TootStatus.parse( context, access_info, src.optJSONObject( "reblog" ) ,false );
/* Pinned TL を取得した時にreblogが登場することはないので、reblogをパースするときのbPinnedはfalseでよい */
status.content = Utils.optStringX( src, "content" );
// Pinned TL を取得した時にreblogが登場することはないのでreblogについてpinned 状態を気にする必要はない
status.reblog = TootStatus.parse( parser, src.optJSONObject( "reblog" ) );
status.created_at = Utils.optStringX( src, "created_at" ); // "2017-04-16T09:37:14.000Z"
status.reblogs_count = Utils.optLongX(src, "reblogs_count" );
status.favourites_count = Utils.optLongX(src, "favourites_count" );
status.reblogs_count = Utils.optLongX( src, "reblogs_count" );
status.favourites_count = Utils.optLongX( src, "favourites_count" );
status.reblogged = src.optBoolean( "reblogged" );
status.favourited = src.optBoolean( "favourited" );
status.sensitive = src.optBoolean( "sensitive" ); // false
@ -140,29 +133,21 @@ public class TootStatus extends TootStatusLike {
status.tags = TootTag.parseList( src.optJSONArray( "tags" ) );
status.application = TootApplication.parse( src.optJSONObject( "application" ) ); // null
status.pinned = bPinned || src.optBoolean( "pinned" );
status.pinned = parser.isPinned || src.optBoolean( "pinned" );
status.setSpoilerText( context, Utils.optStringX( src, "spoiler_text" ) );
status.setSpoilerText( parser, Utils.optStringX( src, "spoiler_text" ) );
status.muted = src.optBoolean( "muted" );
status.language = Utils.optStringX( src, "language" );
status.time_created_at = parseTime( status.created_at );
status.decoded_content = new DecodeOptions()
.setShort( true )
.setDecodeEmoji( true)
.setAttachment( status.media_attachments )
.setCustomEmojiMap( status.custom_emojis )
.setProfileEmojis( status.profile_emojis )
.setLinkTag( status )
.decodeHTML( context, access_info, status.content );
status.setContent( parser, status.media_attachments, Utils.optStringX( src, "content" ) );
// status.decoded_tags = HTMLDecoder.decodeTags( account,status.tags );
status.decoded_mentions = HTMLDecoder.decodeMentions( access_info, status.mentions ,status);
status.enquete = NicoEnquete.parse( context,access_info , status.media_attachments , Utils.optStringX( src, "enquete"),status.id,status.time_created_at,status );
status.decoded_mentions = HTMLDecoder.decodeMentions( parser.access_info, status.mentions, status );
status.enquete = NicoEnquete.parse( parser.context, parser.access_info, status.media_attachments, Utils.optStringX( src, "enquete" ), status.id, status.time_created_at, status );
return status;
}catch( Throwable ex ){
@ -173,12 +158,7 @@ public class TootStatus extends TootStatusLike {
}
@NonNull
public static List parseList( @NonNull Context context, @NonNull SavedAccount access_info, JSONArray array ){
return parseList( context,access_info,array,false );
}
@NonNull
public static List parseList( @NonNull Context context, @NonNull SavedAccount access_info, JSONArray array ,boolean bPinned){
public static List parseList( @NonNull TootParser parser, JSONArray array ){
List result = new List();
if( array != null ){
int array_size = array.length();
@ -186,7 +166,7 @@ public class TootStatus extends TootStatusLike {
for( int i = 0 ; i < array_size ; ++ i ){
JSONObject src = array.optJSONObject( i );
if( src == null ) continue;
TootStatus item = parse( context, access_info, src ,bPinned );
TootStatus item = parse( parser, src );
if( item != null ) result.add( item );
}
}
@ -296,11 +276,11 @@ public class TootStatus extends TootStatusLike {
}
// word mute
if( decoded_content != null && muted_word.containsWord( decoded_content.toString() ) ){
if( muted_word.matchShort( decoded_content ) ){
return true;
}
if( decoded_spoiler_text != null && muted_word.containsWord( decoded_spoiler_text.toString() ) ){
if( muted_word.matchShort( decoded_spoiler_text ) ){
return true;
}

View File

@ -9,10 +9,14 @@ import android.text.TextUtils;
import org.json.JSONObject;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jp.juggler.subwaytooter.api.TootParser;
import jp.juggler.subwaytooter.api_msp.entity.MSPToot;
import jp.juggler.subwaytooter.api_tootsearch.entity.TSToot;
import jp.juggler.subwaytooter.table.HighlightWord;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.DecodeOptions;
import jp.juggler.subwaytooter.util.LogCategory;
@ -20,7 +24,7 @@ import jp.juggler.subwaytooter.util.Utils;
public abstract class TootStatusLike extends TootId {
static final LogCategory log = new LogCategory("TootStatusLike");
static final LogCategory log = new LogCategory( "TootStatusLike" );
//URL to the status page (can be remote)
public String url;
@ -95,8 +99,9 @@ public abstract class TootStatusLike extends TootId {
private static final Pattern reWhitespace = Pattern.compile( "[\\s\\t\\x0d\\x0a]+" );
@Nullable public HighlightWord highlight_sound;
public void setSpoilerText( Context context, String sv ){
public void setSpoilerText( @NonNull TootParser parser, String sv ){
if( TextUtils.isEmpty( sv ) ){
this.spoiler_text = null;
this.decoded_spoiler_text = null;
@ -105,10 +110,38 @@ public abstract class TootStatusLike extends TootId {
// remove white spaces
sv = reWhitespace.matcher( this.spoiler_text ).replaceAll( " " );
// decode emoji code
this.decoded_spoiler_text = new DecodeOptions()
DecodeOptions options = new DecodeOptions()
.setCustomEmojiMap( custom_emojis )
.setProfileEmojis( this.profile_emojis )
.decodeEmoji( context, sv );
.setHighlightTrie(parser.highlight_trie)
;
this.decoded_spoiler_text = options.decodeEmoji( parser.context, sv );
if( options.highlight_sound != null && this.highlight_sound == null ){
this.highlight_sound = options.highlight_sound;
}
}
}
public void setContent( @NonNull TootParser parser, TootAttachment.List list_attachment, @Nullable String content ){
this.content = content;
DecodeOptions options =new DecodeOptions()
.setShort( true )
.setDecodeEmoji( true )
.setCustomEmojiMap( this.custom_emojis )
.setProfileEmojis( this.profile_emojis )
.setLinkTag( this )
.setAttachment( list_attachment )
.setHighlightTrie(parser.highlight_trie)
;
this.decoded_content = options.decodeHTML( parser.context, parser.access_info, content );
if( options.highlight_sound != null && this.highlight_sound == null ){
this.highlight_sound = options.highlight_sound;
}
}
@ -120,8 +153,8 @@ public abstract class TootStatusLike extends TootId {
public Spannable decoded_spoiler_text;
public int originalLineCount;
}
public AutoCW auto_cw;
public AutoCW auto_cw;
// OStatus
static final Pattern reTootUriOS = Pattern.compile( "tag:([^,]*),[^:]*:objectId=(\\d+):objectType=Status", Pattern.CASE_INSENSITIVE );
@ -132,7 +165,7 @@ public abstract class TootStatusLike extends TootId {
// 投稿元タンスでのステータスIDを調べる
public long parseStatusId(){
return TootStatusLike.parseStatusId(this);
return TootStatusLike.parseStatusId( this );
}
// 投稿元タンスでのステータスIDを調べる

View File

@ -9,6 +9,7 @@ import org.json.JSONObject;
import java.util.regex.Matcher;
import jp.juggler.subwaytooter.api.TootParser;
import jp.juggler.subwaytooter.api.entity.TootAccount;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.DecodeOptions;
@ -19,7 +20,7 @@ public class MSPAccount extends TootAccount {
private static final LogCategory log = new LogCategory( "MSPAccount" );
@Nullable
static TootAccount parseAccount( @NonNull Context context, @NonNull SavedAccount access_info, @Nullable JSONObject src ){
static TootAccount parseAccount( @NonNull TootParser parser , @Nullable JSONObject src ){
if( src == null ) return null;
@ -29,7 +30,7 @@ public class MSPAccount extends TootAccount {
dst.avatar = dst.avatar_static = Utils.optStringX( src, "avatar" );
String sv = Utils.optStringX( src, "display_name" );
dst.setDisplayName( context, dst.username, sv );
dst.setDisplayName( parser.context, dst.username, sv );
dst.id = Utils.optLongX( src, "id" );
@ -38,7 +39,7 @@ public class MSPAccount extends TootAccount {
.setShort( true )
.setDecodeEmoji( true )
.setProfileEmojis( dst.profile_emojis )
.decodeHTML( context, access_info, dst.note );
.decodeHTML( parser.context, parser.access_info, dst.note );
if( TextUtils.isEmpty( dst.url ) ){
log.e( "parseAccount: missing url" );

View File

@ -16,6 +16,7 @@ import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import jp.juggler.subwaytooter.api.TootParser;
import jp.juggler.subwaytooter.api.entity.TootStatusLike;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.DecodeOptions;
@ -37,11 +38,11 @@ public class MSPToot extends TootStatusLike {
// private long msp_id;
@Nullable
private static MSPToot parse( @NonNull Context context, SavedAccount access_info, JSONObject src ){
private static MSPToot parse( @NonNull TootParser parser, JSONObject src ){
if( src == null ) return null;
MSPToot dst = new MSPToot();
dst.account = MSPAccount.parseAccount( context, access_info, src.optJSONObject( "account" ) );
dst.account = MSPAccount.parseAccount( parser, src.optJSONObject( "account" ) );
if( dst.account == null ){
log.e( "missing status account" );
return null;
@ -76,26 +77,18 @@ public class MSPToot extends TootStatusLike {
// dst.msp_id = Utils.optLongX(src, "msp_id" );
dst.sensitive = ( src.optInt( "sensitive", 0 ) != 0 );
dst.setSpoilerText( context, Utils.optStringX( src, "spoiler_text" ) );
dst.content = Utils.optStringX( src, "content" );
dst.decoded_content = new DecodeOptions()
.setShort( true )
.setDecodeEmoji( true )
.setCustomEmojiMap( dst.custom_emojis )
.setProfileEmojis( dst.profile_emojis )
.setLinkTag( dst )
.decodeHTML( context, access_info, dst.content );
dst.setSpoilerText( parser, Utils.optStringX( src, "spoiler_text" ) );
dst.setContent( parser, null, Utils.optStringX( src, "content" ) );
return dst;
}
public static List parseList( @NonNull Context context, SavedAccount access_info, JSONArray array ){
public static List parseList( @NonNull TootParser parser, JSONArray array ){
List list = new List();
for( int i = 0, ie = array.length() ; i < ie ; ++ i ){
JSONObject src = array.optJSONObject( i );
if( src == null ) continue;
MSPToot item = parse( context, access_info, src );
MSPToot item = parse( parser, src );
if( item == null ) continue;
list.add( item );
}
@ -146,11 +139,11 @@ public class MSPToot extends TootStatusLike {
// }
//
// word mute
if( decoded_content != null && muted_word.containsWord( decoded_content.toString() ) ){
if( muted_word.matchShort( decoded_content ) ){
return true;
}
if( decoded_spoiler_text != null && muted_word.containsWord( decoded_spoiler_text.toString() ) ){
if( muted_word.matchShort( decoded_spoiler_text ) ){
return true;
}

View File

@ -12,6 +12,7 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.regex.Matcher;
import jp.juggler.subwaytooter.api.TootParser;
import jp.juggler.subwaytooter.api.entity.CustomEmoji;
import jp.juggler.subwaytooter.api.entity.NicoProfileEmoji;
import jp.juggler.subwaytooter.api.entity.TootAccount;
@ -37,11 +38,11 @@ public class TSToot extends TootStatusLike {
public String uri;
@Nullable
private static TSToot parse( @NonNull Context context, SavedAccount access_info, JSONObject src ){
private static TSToot parse( @NonNull TootParser parser, JSONObject src ){
if( src == null ) return null;
TSToot dst = new TSToot();
dst.account = parseAccount( context, access_info, src.optJSONObject( "account" ) );
dst.account = parseAccount( parser, src.optJSONObject( "account" ) );
if( dst.account == null ){
log.e( "missing status account" );
return null;
@ -50,7 +51,7 @@ public class TSToot extends TootStatusLike {
dst.json = src;
// 絵文字マップは割と最初の方で読み込んでおきたい
dst.custom_emojis = CustomEmoji.parseMap( src.optJSONArray( "emojis" ), access_info.host );
dst.custom_emojis = CustomEmoji.parseMap( src.optJSONArray( "emojis" ), parser.access_info.host );
dst.profile_emojis = NicoProfileEmoji.parseMap( src.optJSONArray( "profile_emojis" ) );
dst.url = Utils.optStringX( src, "url" );
@ -65,7 +66,7 @@ public class TSToot extends TootStatusLike {
log.e( "missing status uri or url or host or id" );
return null;
}
// uri から投稿元タンスでのIDを調べる
dst.id = TootStatusLike.parseStatusId( dst );
@ -76,18 +77,8 @@ public class TSToot extends TootStatusLike {
dst.sensitive = src.optBoolean( "sensitive", false );
dst.setSpoilerText( context, Utils.optStringX( src, "spoiler_text" ) );
dst.content = Utils.optStringX( src, "content" );
dst.decoded_content = new DecodeOptions()
.setShort( true )
.setDecodeEmoji( true )
.setCustomEmojiMap( dst.custom_emojis )
.setProfileEmojis( dst.profile_emojis )
.setLinkTag( dst )
.decodeHTML( context, access_info, dst.content );
dst.setSpoilerText( parser, Utils.optStringX( src, "spoiler_text" ) );
dst.setContent( parser, dst.media_attachments, Utils.optStringX( src, "content" ) );
return dst;
}
@ -96,7 +87,7 @@ public class TSToot extends TootStatusLike {
}
@NonNull
public static TSToot.List parseList( @NonNull Context context, SavedAccount access_info, @NonNull JSONObject root ){
public static TSToot.List parseList( @NonNull TootParser parser, @NonNull JSONObject root ){
TSToot.List list = new TSToot.List();
JSONArray array = TSClient.getHits( root );
if( array != null ){
@ -105,11 +96,11 @@ public class TSToot extends TootStatusLike {
JSONObject src = array.optJSONObject( i );
if( src == null ) continue;
JSONObject src2 = src.optJSONObject( "_source" );
TSToot item = parse( context, access_info, src2 );
TSToot item = parse( parser, src2 );
if( item == null ) continue;
list.add( item );
}catch(Throwable ex){
log.trace(ex);
}catch( Throwable ex ){
log.trace( ex );
}
}
}
@ -119,11 +110,11 @@ public class TSToot extends TootStatusLike {
public boolean checkMuted( @SuppressWarnings("UnusedParameters") @NonNull HashSet< String > muted_app, @NonNull WordTrieTree muted_word ){
// word mute
if( decoded_content != null && muted_word.containsWord( decoded_content.toString() ) ){
if( decoded_content != null && muted_word.matchShort( decoded_content.toString() ) ){
return true;
}
if( decoded_spoiler_text != null && muted_word.containsWord( decoded_spoiler_text.toString() ) ){
if( decoded_spoiler_text != null && muted_word.matchShort( decoded_spoiler_text.toString() ) ){
return true;
}
@ -143,9 +134,10 @@ public class TSToot extends TootStatusLike {
}
@Nullable
private static TootAccount parseAccount( @NonNull Context context, @NonNull SavedAccount access_info, @Nullable JSONObject src ){
private static TootAccount parseAccount( @NonNull TootParser parser, @Nullable JSONObject src ){
TootAccount dst = parser.account( src );
TootAccount dst = TootAccount.parse( context, access_info, src );
if( dst != null ){
// tootsearch のアカウントのIDはどのタンス上のものか分からない

View File

@ -35,6 +35,7 @@ import jp.juggler.subwaytooter.api.TootTaskRunner;
import jp.juggler.subwaytooter.api.entity.TootAccount;
import jp.juggler.subwaytooter.api.entity.TootList;
import jp.juggler.subwaytooter.api.entity.TootResults;
import jp.juggler.subwaytooter.api.TootParser;
import jp.juggler.subwaytooter.table.AcctColor;
import jp.juggler.subwaytooter.table.SavedAccount;
import jp.juggler.subwaytooter.util.NetworkEmojiInvalidator;
@ -190,7 +191,7 @@ public class DlgListMember implements View.OnClickListener {
return result;
}
TootResults search_result = TootResults.parse( activity, list_owner, result.object );
TootResults search_result = new TootParser(activity, list_owner).results( result.object );
if( search_result != null ){
for( TootAccount a : search_result.accounts ){
if( target_user_full_acct.equalsIgnoreCase( list_owner.getFullAcct( a ) ) ){

View File

@ -0,0 +1,191 @@
package jp.juggler.subwaytooter.table;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import org.json.JSONException;
import org.json.JSONObject;
import jp.juggler.subwaytooter.App1;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
import jp.juggler.subwaytooter.util.WordTrieTree;
public class HighlightWord {
private static final LogCategory log = new LogCategory( "HighlightWord" );
public static final int SOUND_TYPE_NONE = 0;
public static final int SOUND_TYPE_DEFAULT = 1;
public static final int SOUND_TYPE_CUSTOM = 2;
public static final String table = "highlight_word";
public static final String COL_ID = "_id";
public static final String COL_NAME = "name";
private static final String COL_TIME_SAVE = "time_save";
private static final String COL_COLOR_BG = "color_bg";
private static final String COL_COLOR_FG = "color_fg";
private static final String COL_SOUND_TYPE = "sound_type";
private static final String COL_SOUND_URI = "sound_uri";
public static void onDBCreate( SQLiteDatabase db ){
log.d( "onDBCreate!" );
db.execSQL(
"create table if not exists " + table
+ "(_id INTEGER PRIMARY KEY"
+ ",name text not null"
+ ",time_save integer not null"
+ ",color_bg integer not null default 0"
+ ",color_fg integer not null default 0"
+ ",sound_type integer not null default 1"
+ ",sound_uri text default null"
+ ")"
);
db.execSQL(
"create unique index if not exists " + table + "_name on " + table + "(name)"
);
}
public static void onDBUpgrade( SQLiteDatabase db, int oldVersion, int newVersion ){
if( oldVersion < 21 && newVersion >= 21 ){
onDBCreate( db );
}
}
public long id = -1L;
@NonNull public String name;
public int color_bg;
public int color_fg;
public int sound_type;
@Nullable public String sound_uri;
public JSONObject encodeJson() throws JSONException{
JSONObject dst = new JSONObject( );
dst.put(COL_ID,id);
dst.put(COL_NAME,name);
dst.put(COL_COLOR_BG,color_bg);
dst.put(COL_COLOR_FG,color_fg);
dst.put(COL_SOUND_TYPE,sound_type);
if( sound_uri != null ) dst.put(COL_SOUND_URI,sound_uri);
return dst;
}
public HighlightWord(@NonNull JSONObject src ){
this.id = Utils.optLongX( src,COL_ID );
String sv = Utils.optStringX(src,COL_NAME);
if( TextUtils.isEmpty( sv )) throw new RuntimeException( "HighlightWord: name is empty" );
this.name = sv;
this.color_bg = src.optInt( COL_COLOR_BG );
this.color_fg = src.optInt( COL_COLOR_FG );
this.sound_type = src.optInt( COL_SOUND_TYPE );
this.sound_uri = Utils.optStringX( src,COL_SOUND_URI );
}
public HighlightWord(@NonNull String name){
this.name = name;
this.sound_type = SOUND_TYPE_DEFAULT;
this.color_fg = 0xFFFF0000;
}
public HighlightWord(@NonNull Cursor cursor){
this.id = cursor.getLong( cursor.getColumnIndex( COL_ID ));
this.name = cursor.getString( cursor.getColumnIndex( COL_NAME ));
this.color_bg = cursor.getInt( cursor.getColumnIndex( COL_COLOR_BG ));
this.color_fg = cursor.getInt( cursor.getColumnIndex( COL_COLOR_FG ));
this.sound_type = cursor.getInt( cursor.getColumnIndex( COL_SOUND_TYPE ));
int colIdx_sound_uri = cursor.getColumnIndex( COL_SOUND_URI );
this.sound_uri = cursor.isNull( colIdx_sound_uri ) ? null : cursor.getString( colIdx_sound_uri);
}
private static final String selection_name = COL_NAME+"=?";
private static final String selection_id = COL_ID+"=?";
@Nullable public static HighlightWord load(@NonNull String name){
try{
Cursor cursor = App1.getDB().query( table, null, selection_name, new String[]{ name }, null, null, null );
if( cursor != null ){
try{
if( cursor.moveToNext() ){
return new HighlightWord( cursor );
}
}finally{
cursor.close();
}
}
}catch( Throwable ex ){
log.trace( ex );
}
return null;
}
public void save(){
if( TextUtils.isEmpty( name )) throw new RuntimeException( "HighlightWord: name is empty" );
try{
ContentValues cv = new ContentValues();
cv.put( COL_NAME, name );
cv.put( COL_TIME_SAVE, System.currentTimeMillis() );
cv.put( COL_COLOR_BG, color_bg );
cv.put( COL_COLOR_FG, color_fg );
cv.put( COL_SOUND_TYPE, sound_type );
if( TextUtils.isEmpty(sound_uri) ){
cv.putNull( COL_SOUND_URI );
}else{
cv.put( COL_SOUND_URI, sound_uri );
}
if( id == -1L ){
App1.getDB().replace( table, null, cv );
}else{
App1.getDB().update( table, cv, selection_id, new String[]{ Long.toString( id ) } );
}
}catch( Throwable ex ){
log.e( ex, "save failed." );
}
}
public static Cursor createCursor(){
return App1.getDB().query( table, null, null, null, null, null, COL_NAME + " asc" );
}
public void delete(){
try{
App1.getDB().delete( table, selection_id, new String[]{ Long.toString( id ) } );
}catch( Throwable ex ){
log.e( ex, "delete failed." );
}
}
private static final String[] columns_name = new String[]{ COL_NAME };
@Nullable public static WordTrieTree getNameSet(){
WordTrieTree dst = null;
try{
Cursor cursor = App1.getDB().query( table, columns_name, null, null, null, null, null );
if( cursor != null ){
try{
int idx_name = cursor.getColumnIndex( COL_NAME );
while( cursor.moveToNext() ){
if( dst == null) dst = new WordTrieTree();
String s = cursor.getString( idx_name );
dst.add( s );
}
}finally{
cursor.close();
}
}
}catch( Throwable ex ){
log.trace( ex );
}
return dst;
}
}

View File

@ -0,0 +1,339 @@
package jp.juggler.subwaytooter.util;
import android.support.annotation.NonNull;
import android.support.v4.util.SparseArrayCompat;
import java.util.Locale;
public class CharacterGroup {
// Tokenizerが終端に達したことを示す
static final int END = - 1;
// 同じと見なす文字列の集合
static class Group {
// 文字列の配列
@NonNull final String[] list;
// 代表する文字のコード
final int id;
Group( @NonNull String[] list ){
this.list = list;
this.id = findGroupId();
}
private int findGroupId(){
// グループのIDはグループ中の文字(長さ1)のunicodeのどれか
for( String s : list ){
if( s.length() == 1 ){
return s.charAt( 0 );
}
}
throw new RuntimeException( "group has not id!!" );
}
}
// テキスト中に出現した文字列からグループを探すためのマップ
// キー文字数1
private final SparseArrayCompat< Group > map1 = new SparseArrayCompat<>();
// キー文字数2
private final SparseArrayCompat< Group > map2 = new SparseArrayCompat<>();
// グループをmapに登録する
private void addGroup( @NonNull String[] list ){
// グループを生成
Group g = new Group( list );
// 含まれる各文字列をマップに登録する
for( String s : list ){
int len = s.length();
int v1 = s.charAt( 0 );
SparseArrayCompat< Group > map;
int key;
if( len == 1 ){
map = map1;
key = v1;
}else{
map = map2;
int v2 = s.charAt( 1 );
key = v1 | ( v2 << 16 );
}
Group old = map.get( key );
if( old != null && old != g ){
throw new RuntimeException( String.format( Locale.JAPAN, "group conflict: %s", s ) );
}
map.put( key, g );
}
}
// CharSequence の範囲 から 文字,グループ,終端 のどれかを列挙する
class Tokenizer {
public CharSequence text;
public int end;
// next() を読むと以下の変数が更新される
public int offset;
public int c; // may END or group.id or UTF-16 character
public Group group;
Tokenizer( @NonNull CharSequence text, int start, int end ){
reset( text, start, end );
}
public void reset( CharSequence text, int start, int end ){
this.text = text;
this.offset = start;
this.end = end;
}
public void next(){
int pos = offset;
// 空白を読み飛ばす
while( pos < end && isWhitespace( text.charAt( pos ) ) ) ++ pos;
// 終端までの文字数
int remain = end - pos;
if( remain <= 0 ){
// 空白を読み飛ばしたら終端になった
// 終端の場合末尾の空白はoffsetに含めない
this.group = null;
this.c = END;
return;
}
int v1 = text.charAt( pos );
int v2 = remain > 1 ? text.charAt( pos + 1 ) : 0;
// グループに登録された文字を長い順にチェック
int check_len = remain > 2 ? 2 : remain;
while( check_len > 0 ){
Group g = check_len == 1 ? map1.get( v1 ) : map2.get( v1 | ( v2 << 16 ) );
if( g != null ){
this.group = g;
this.c = g.id;
this.offset = pos + check_len;
return;
}
-- check_len;
}
this.group = null;
this.c = v1;
this.offset = pos + 1;
}
}
Tokenizer tokenizer( CharSequence text, int start, int end ){
return new Tokenizer( text, start, end );
}
public static boolean isWhitespace( int cp ){
switch( cp ){
case 0x0009: // HORIZONTAL TABULATION
case 0x000A: // LINE FEED
case 0x000B: // VERTICAL TABULATION
case 0x000C: // FORM FEED
case 0x000D: // CARRIAGE RETURN
case 0x001C: // FILE SEPARATOR
case 0x001D: // GROUP SEPARATOR
case 0x001E: // RECORD SEPARATOR
case 0x001F: // UNIT SEPARATOR
case 0x0020:
case 0x0085: // next line (latin-1)
case 0x00A0: //非区切りスペース
case 0x1680:
case 0x180E:
case 0x2000:
case 0x2001:
case 0x2002:
case 0x2003:
case 0x2004:
case 0x2005:
case 0x2006:
case 0x2007: //非区切りスペース
case 0x2008:
case 0x2009:
case 0x200A:
case 0x200B:
case 0x200C:
case 0x200D:
case 0x2028: // line separator
case 0x2029: // paragraph separator
case 0x202F: //非区切りスペース
case 0x205F:
case 0x2060:
case 0x3000:
case 0x3164:
case 0xFEFF:
return true;
default:
return Character.isWhitespace( cp );
}
}
// 文字コードから文字列を作る
private static String c2s( char[] tmp, int c ){
tmp[ 0 ] = (char) c;
return new String( tmp, 0, 1 );
}
CharacterGroup(){
char[] tmp = new char[ 1 ];
// 数字
for( int i = 0 ; i < 9 ; ++ i ){
String[] list = new String[ 2 ];
list[ 0 ] = c2s( tmp, '0' + i );
list[ 1 ] = c2s( tmp, '' + i );
addGroup( list );
}
// 英字
for( int i = 0 ; i < 26 ; ++ i ){
String[] list = new String[ 4 ];
list[ 0 ] = c2s( tmp, 'a' + i );
list[ 1 ] = c2s( tmp, 'A' + i );
list[ 2 ] = c2s( tmp, '' + i );
list[ 3 ] = c2s( tmp, '' + i );
addGroup( list );
}
// ハイフン
addGroup( new String[]{
c2s( tmp, 0x002D ), // ASCIIのハイフン
c2s( tmp, 0x30FC ), // 全角カナの長音 Shift_JIS由来
c2s( tmp, 0x2010 ),
c2s( tmp, 0x2011 ),
c2s( tmp, 0x2013 ),
c2s( tmp, 0x2014 ),
c2s( tmp, 0x2015 ), // 全角カナのダッシュ Shift_JIS由来
c2s( tmp, 0x2212 ),
c2s( tmp, 0xFF0d ), // 全角カナの長音 MS932由来
c2s( tmp, 0xFF70 ), // 半角カナの長音 MS932由来
} );
addGroup( new String[]{ "", "!" } );
addGroup( new String[]{ "", "\"" } );
addGroup( new String[]{ "", "#" } );
addGroup( new String[]{ "", "$" } );
addGroup( new String[]{ "", "%" } );
addGroup( new String[]{ "", "&" } );
addGroup( new String[]{ "", "'" } );
addGroup( new String[]{ "", "(" } );
addGroup( new String[]{ "", ")" } );
addGroup( new String[]{ "", "*" } );
addGroup( new String[]{ "", "+" } );
addGroup( new String[]{ "", ",", "", "" } );
addGroup( new String[]{ "", ".", "", "" } );
addGroup( new String[]{ "", "/" } );
addGroup( new String[]{ "", ":" } );
addGroup( new String[]{ "", ";" } );
addGroup( new String[]{ "", "<" } );
addGroup( new String[]{ "", "=" } );
addGroup( new String[]{ "", ">" } );
addGroup( new String[]{ "", "?" } );
addGroup( new String[]{ "", "@" } );
addGroup( new String[]{ "", "[" } );
addGroup( new String[]{ "", "\\", "" } );
addGroup( new String[]{ "", "]" } );
addGroup( new String[]{ "", "^" } );
addGroup( new String[]{ "_", "_" } );
addGroup( new String[]{ "", "`" } );
addGroup( new String[]{ "", "{" } );
addGroup( new String[]{ "", "|", "" } );
addGroup( new String[]{ "", "}" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
// チルダ
addGroup( new String[]{ "~", c2s(tmp,0x301C),c2s(tmp,0xFF5E) } );
// 半角カナの濁音,半濁音は2文字になる
addGroup( new String[]{ "", "", "ガ" } );
addGroup( new String[]{ "", "", "ギ" } );
addGroup( new String[]{ "", "", "グ" } );
addGroup( new String[]{ "", "", "ゲ" } );
addGroup( new String[]{ "", "", "ゴ" } );
addGroup( new String[]{ "", "", "ザ" } );
addGroup( new String[]{ "", "", "ジ" } );
addGroup( new String[]{ "", "", "ズ" } );
addGroup( new String[]{ "", "", "ゼ" } );
addGroup( new String[]{ "", "", "ゾ" } );
addGroup( new String[]{ "", "", "ダ" } );
addGroup( new String[]{ "", "", "ヂ" } );
addGroup( new String[]{ "", "", "ヅ" } );
addGroup( new String[]{ "", "", "デ" } );
addGroup( new String[]{ "", "", "ド" } );
addGroup( new String[]{ "", "", "バ" } );
addGroup( new String[]{ "", "", "ビ" } );
addGroup( new String[]{ "", "", "ブ" } );
addGroup( new String[]{ "", "", "ベ" } );
addGroup( new String[]{ "", "", "ボ" } );
addGroup( new String[]{ "", "", "パ" } );
addGroup( new String[]{ "", "", "ピ" } );
addGroup( new String[]{ "", "", "プ" } );
addGroup( new String[]{ "", "", "ペ" } );
addGroup( new String[]{ "", "", "ポ" } );
addGroup( new String[]{ "", "う゛", "ヴ" } );
addGroup( new String[]{ "", "", "", "", "", "" } );
addGroup( new String[]{ "", "", "", "", "", "" } );
addGroup( new String[]{ "", "", "", "", "", "" } );
addGroup( new String[]{ "", "", "", "", "", "" } );
addGroup( new String[]{ "", "", "", "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "ソ", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "", "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "", "", "", "" } );
addGroup( new String[]{ "", "", "", "", "", "" } );
addGroup( new String[]{ "", "", "", "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
addGroup( new String[]{ "", "", "" } );
}
}

View File

@ -6,9 +6,13 @@ import android.support.annotation.Nullable;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import java.util.ArrayList;
import jp.juggler.subwaytooter.api.TootParser;
import jp.juggler.subwaytooter.api.entity.CustomEmoji;
import jp.juggler.subwaytooter.api.entity.NicoProfileEmoji;
import jp.juggler.subwaytooter.api.entity.TootAttachment;
import jp.juggler.subwaytooter.table.HighlightWord;
@SuppressWarnings("WeakerAccess")
public class DecodeOptions {
@ -62,4 +66,13 @@ public class DecodeOptions {
public Spannable decodeEmoji( @NonNull final Context context, @NonNull final String s ){
return EmojiDecoder.decodeEmoji( context, s, this );
}
// highlight first found
@Nullable public HighlightWord highlight_sound;
@Nullable public WordTrieTree highlight_trie;
public DecodeOptions setHighlightTrie( WordTrieTree highlight_trie ){
this.highlight_trie = highlight_trie;
return this;
}
}

View File

@ -15,6 +15,7 @@ import jp.juggler.subwaytooter.App1;
import jp.juggler.subwaytooter.R;
import jp.juggler.subwaytooter.api.entity.CustomEmoji;
import jp.juggler.subwaytooter.api.entity.NicoProfileEmoji;
import jp.juggler.subwaytooter.table.HighlightWord;
@SuppressWarnings("WeakerAccess")
public class EmojiDecoder {
@ -22,9 +23,38 @@ public class EmojiDecoder {
private static class DecodeEnv {
@NonNull final Context context;
@NonNull final SpannableStringBuilder sb = new SpannableStringBuilder();
@NonNull final DecodeOptions options;
int normal_char_start = -1;
DecodeEnv( @NonNull Context context ){
DecodeEnv( @NonNull Context context ,@NonNull final DecodeOptions options){
this.context = context;
this.options = options;
}
void closeNormalText(){
if( normal_char_start != -1 ){
int end = sb.length();
applyHighlight(normal_char_start,end);
normal_char_start = -1;
}
}
private void applyHighlight( int start , int end ){
if( options.highlight_trie != null ){
ArrayList<WordTrieTree.Match > list = options.highlight_trie.matchList( sb,start,end );
if( list != null ){
for( WordTrieTree.Match range : list ){
HighlightWord word = HighlightWord.load( range.word );
if( word !=null ){
sb.setSpan( new HighlightSpan( word.color_fg,word.color_bg ), range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
if( word.sound_type != HighlightWord.SOUND_TYPE_NONE ){
options.highlight_sound = word;
}
}
}
}
}
}
void addUnicodeString( String s ){
@ -34,6 +64,7 @@ public class EmojiDecoder {
int remain = end - i;
String emoji = null;
Integer image_id = null;
for( int j = EmojiMap201709.utf16_max_length ; j > 0 ; -- j ){
if( j > remain ) continue;
String check = s.substring( i, i + j );
@ -55,6 +86,9 @@ public class EmojiDecoder {
if( image_id == 0 ){
// 絵文字バリエーションシーケンスEVSのU+FE0EVS-15が直後にある場合
// その文字を絵文字化しない
if( normal_char_start == - 1 ){
normal_char_start = sb.length();
}
sb.append( emoji );
}else{
addImageSpan( emoji, image_id );
@ -63,6 +97,9 @@ public class EmojiDecoder {
continue;
}
if( normal_char_start == - 1 ){
normal_char_start = sb.length();
}
int length = Character.charCount( s.codePointAt( i ) );
if( length == 1 ){
sb.append( s.charAt( i ) );
@ -75,6 +112,7 @@ public class EmojiDecoder {
}
void addImageSpan( String text, @DrawableRes int res_id ){
closeNormalText();
int start = sb.length();
sb.append( text );
int end = sb.length();
@ -82,6 +120,7 @@ public class EmojiDecoder {
}
void addNetworkEmojiSpan( String text, @NonNull String url ){
closeNormalText();
int start = sb.length();
sb.append( text );
int end = sb.length();
@ -89,44 +128,7 @@ public class EmojiDecoder {
}
}
public static boolean isWhitespaceBeforeEmoji( int cp ){
switch( cp ){
case 0x0009: // HORIZONTAL TABULATION
case 0x000A: // LINE FEED
case 0x000B: // VERTICAL TABULATION
case 0x000C: // FORM FEED
case 0x000D: // CARRIAGE RETURN
case 0x001C: // FILE SEPARATOR
case 0x001D: // GROUP SEPARATOR
case 0x001E: // RECORD SEPARATOR
case 0x001F: // UNIT SEPARATOR
case 0x0020:
case 0x00A0: //非区切りスペース
case 0x1680:
case 0x180E:
case 0x2000:
case 0x2001:
case 0x2002:
case 0x2003:
case 0x2004:
case 0x2005:
case 0x2006:
case 0x2007: //非区切りスペース
case 0x2008:
case 0x2009:
case 0x200A:
case 0x200B:
case 0x202F: //非区切りスペース
case 0x205F:
case 0x2060:
case 0x3000:
case 0x3164:
case 0xFEFF:
return true;
default:
return Character.isWhitespace( cp );
}
}
public static boolean isShortCodeCharacter( int cp ){
return ( 'A' <= cp && cp <= 'Z' )
@ -165,7 +167,7 @@ public class EmojiDecoder {
}else if( i + width < end && s.codePointAt( i + width ) == '@' ){
// フレニコのプロフ絵文字 :@who: は手前の空白を要求しない
break;
}else if( i == 0 || isWhitespaceBeforeEmoji( s.codePointBefore( i ) ) ){
}else if( i == 0 || CharacterGroup.isWhitespace( s.codePointBefore( i ) ) ){
// ショートコードの手前は始端か改行か空白文字でないとならない
// 空白文字の判定はサーバサイドのそれにあわせる
break;
@ -218,7 +220,7 @@ public class EmojiDecoder {
, @NonNull final String s
, @NonNull DecodeOptions options
){
final DecodeEnv decode_env = new DecodeEnv( context );
final DecodeEnv decode_env = new DecodeEnv( context ,options);
final CustomEmoji.Map custom_map = options.customEmojiMap;
final NicoProfileEmoji.Map profile_emojis = options.profile_emojis;
@ -265,6 +267,8 @@ public class EmojiDecoder {
}
} );
decode_env.closeNormalText();
return decode_env.sb;
}

View File

@ -21,6 +21,7 @@ import jp.juggler.subwaytooter.R;
import jp.juggler.subwaytooter.api.entity.TootAccount;
import jp.juggler.subwaytooter.api.entity.TootAttachment;
import jp.juggler.subwaytooter.api.entity.TootMention;
import jp.juggler.subwaytooter.table.HighlightWord;
import jp.juggler.subwaytooter.table.SavedAccount;
@SuppressWarnings("WeakerAccess")
@ -202,6 +203,9 @@ public class HTMLDecoder {
sb.append( sb_tmp.toString() );
}
end = sb.length();
}else if( sb_tmp != sb ){
// style もscript も読み捨てる
}
@ -213,6 +217,22 @@ public class HTMLDecoder {
String link_text = sb.subSequence( start, end ).toString();
MyClickableSpan span = new MyClickableSpan( account, link_text, href, account.findAcctColor( href ), options.link_tag );
sb.setSpan( span, start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE );
// リンクスパンを設定した後に色をつける
if( options.highlight_trie != null ){
ArrayList<WordTrieTree.Match > list = options.highlight_trie.matchList( sb,start,end );
if( list != null ){
for( WordTrieTree.Match range : list ){
HighlightWord word = HighlightWord.load( range.word );
if( word !=null ){
sb.setSpan( new HighlightSpan( word.color_fg,word.color_bg ), range.start, range.end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE );
if( word.sound_type != HighlightWord.SOUND_TYPE_NONE ){
options.highlight_sound = word;
}
}
}
}
}
}
}
}

View File

@ -0,0 +1,28 @@
package jp.juggler.subwaytooter.util;
import android.text.TextPaint;
import android.text.style.CharacterStyle;
public class HighlightSpan extends CharacterStyle {
public final int color_fg;
public final int color_bg;
HighlightSpan( int color_fg ,int color_bg ){
super();
this.color_fg = color_fg;
this.color_bg = color_bg;
}
@Override public void updateDrawState( TextPaint ds ){
// super.updateDrawState( ds );
if( color_fg != 0 ){
ds.setColor( color_fg );
}
if( color_bg != 0 ){
ds.bgColor = color_bg;
}
}
}

View File

@ -35,6 +35,7 @@ import jp.juggler.subwaytooter.api.entity.CustomEmoji;
import jp.juggler.subwaytooter.api.entity.TootAccount;
import jp.juggler.subwaytooter.api.entity.TootInstance;
import jp.juggler.subwaytooter.api.entity.TootStatus;
import jp.juggler.subwaytooter.api.TootParser;
import jp.juggler.subwaytooter.dialog.DlgConfirm;
import jp.juggler.subwaytooter.dialog.EmojiPicker;
import jp.juggler.subwaytooter.table.AcctColor;
@ -315,7 +316,7 @@ public class PostHelper implements CustomEmojiLister.Callback, EmojiPicker.Callb
TootApiResult result = client.request( "/api/v1/statuses", request_builder );
if( result != null && result.object != null ){
status = TootStatus.parse( activity, account, result.object );
status = new TootParser( activity, account).status( result.object );
if( status != null ){
Spannable s = status.decoded_content;
MyClickableSpan[] span_list = s.getSpans( 0, s.length(), MyClickableSpan.class );
@ -599,7 +600,7 @@ public class PostHelper implements CustomEmojiLister.Callback, EmojiPicker.Callb
}
// : の手前は始端か改行か空白でなければならない
if( last_colon > 0 && ! EmojiDecoder.isWhitespaceBeforeEmoji( src.codePointBefore( last_colon ) ) ){
if( last_colon > 0 && ! CharacterGroup.isWhitespace( src.codePointBefore( last_colon ) ) ){
log.d( "checkEmoji: invalid character before shortcode." );
closeAcctPopup();
return;

View File

@ -173,11 +173,11 @@ public class Utils {
}
}
public static String optStringX( JSONObject src, String key ){
@Nullable public static String optStringX( JSONObject src, String key ){
return src.isNull( key ) ? null : src.optString( key );
}
public static String optStringX( JSONArray src, int key ){
@Nullable public static String optStringX( JSONArray src, int key ){
return src.isNull( key ) ? null : src.optString( key );
}

View File

@ -1,76 +1,143 @@
package jp.juggler.subwaytooter.util;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.util.SparseArrayCompat;
import java.util.ArrayList;
public class WordTrieTree {
static class Match {
String word;
int start;
int end;
}
// private static class Matcher {
//
// // ミュートの場合などは短いマッチでも構わない
// final boolean allowShortMatch;
//
// // マッチ範囲の始端を覚えておく
// int start;
//
// Matcher( boolean allowShortMatch ){
// this.allowShortMatch = allowShortMatch;
// }
//
// void setTokenizer( CharacterGroup grouper, CharSequence src, int start, int end ){
// this.match = null;
// this.start = start;
// if( t == null ){
//
// }else{
// t.reset( src, start, end );
// }
// }
// }
private static final CharacterGroup grouper = new CharacterGroup();
private static class Node {
final SparseArrayCompat< Node > child_nodes = new SparseArrayCompat<>();
// 続くノード
@NonNull final SparseArrayCompat< Node > child_nodes = new SparseArrayCompat<>();
boolean is_end;
// このノードが終端ならマッチした単語の元の表記がある
@Nullable String match_word;
boolean match( String s, int offset, int remain ){
if( is_end ){
// ワードの始端から終端までマッチした
return true;
}
if( remain <= 0 ){
// テスト文字列の終端に達した
return false;
}
int c = s.charAt( offset );
++ offset;
-- remain;
Node n = child_nodes.get( c );
return n != null && n.match( s, offset, remain );
}
public void add( String s, int offset, int remain ){
if( is_end ){
// NGワード用なので既に終端を含むなら後続ノードの情報は不要
return;
}
if( remain <= 0 ){
// 終端マークを設定
is_end = true;
// 後続ノードは不要になる
child_nodes.clear();
return;
}
int c = s.charAt( offset );
++ offset;
-- remain;
// 文字別に後続ノードを作成
Node n = child_nodes.get( c );
if( n == null ) child_nodes.put( c, n = new Node() );
n.add( s, offset, remain );
}
// Trieツリー的には終端単語と続くードの両方が存在する場合がありうる
// たとえば ABC ABCDEF を登録してからABCDEF を探索したら単語 ABC と単語 DEF にマッチする
}
private final Node node_root = new Node();
public void add( @NonNull String s ){
node_root.add( s, 0, s.length() );
// 単語の追加
public void add( @NonNull String s ){
CharacterGroup.Tokenizer t = grouper.tokenizer( s, 0, s.length() );
Node node = node_root;
for( ; ; ){
t.next();
int id = t.c;
if( id == CharacterGroup.END ){
// より長いマッチ単語を覚えておく
if( node.match_word == null || node.match_word.length() < t.text.length() ){
node.match_word = t.text.toString();
}
return;
}
Node child = node.child_nodes.get( t.c );
if( child == null ){
node.child_nodes.put( id, child = new Node() );
}
node = child;
}
}
public boolean containsWord( @NonNull String src ){
for( int i = 0, ie = src.length() ; i < ie ; ++ i ){
if( node_root.match( src, i, ie - i ) ) return true;
// 前方一致でマッチング
@Nullable
private Match match( boolean allowShortMatch, @NonNull CharacterGroup.Tokenizer t ){
int start = t.offset;
Match dst = null;
Node node = node_root;
for( ; ; ){
// このノードは単語の終端でもある
if( node.match_word != null ){
dst = new Match();
dst.word = node.match_word;
dst.start = start;
dst.end = t.offset;
// 最短マッチのみを調べるのなら以降の処理は必要ない
if( allowShortMatch ) break;
}
t.next();
int id = t.c;
if( id == CharacterGroup.END ) break;
Node child = node.child_nodes.get( id );
if( child == null ) break;
node = child;
}
return false;
return dst;
}
public boolean matchShort( @Nullable CharSequence src ){
return null != src && null != matchShort( src, 0, src.length() );
}
private Match matchShort( @NonNull CharSequence src, int start, int end ){
CharacterGroup.Tokenizer t = grouper.tokenizer( src, start, end );
for( int i = start ; i < end ; ++ i ){
int c = src.charAt( i );
if( CharacterGroup.isWhitespace( c ) ) continue;
t.reset( src, i, end );
Match item = match( true, t );
if( item != null ) return item;
}
return null;
}
@Nullable ArrayList< Match > matchList( @NonNull CharSequence src, int start, int end ){
ArrayList< Match > dst = null;
CharacterGroup.Tokenizer t = grouper.tokenizer( src, start, end );
for( int i = start ; i < end ; ++ i ){
int c = src.charAt( i );
if( CharacterGroup.isWhitespace( c ) ) continue;
t.reset( src, i, end );
Match item = match( false, t );
if( item != null ){
if( dst == null ) dst = new ArrayList<>();
dst.add( item );
i = item.end - 1;
}
}
return dst;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 756 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,128 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fadeScrollbars="false"
android:fillViewport="true"
android:padding="12dp"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/highlight_word"
/>
<LinearLayout style="@style/setting_row_form">
<TextView
android:id="@+id/tvName"
style="@style/setting_horizontal_stretch"
android:padding="6dp"
android:gravity="center"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/text_color"
/>
<LinearLayout style="@style/setting_row_form">
<Button
android:id="@+id/btnTextColorEdit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit"
android:textAllCaps="false"
/>
<Button
android:id="@+id/btnTextColorReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset"
android:textAllCaps="false"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/background_color"
/>
<LinearLayout style="@style/setting_row_form">
<Button
android:id="@+id/btnBackgroundColorEdit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit"
android:textAllCaps="false"
/>
<Button
android:id="@+id/btnBackgroundColorReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset"
android:textAllCaps="false"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/notification_sound"
/>
<LinearLayout style="@style/setting_row_form">
<Switch
android:id="@+id/swSound"
style="@style/setting_horizontal_stretch"
android:gravity="center"
/>
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<Button
android:id="@+id/btnNotificationSoundEdit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit"
android:textAllCaps="false"
/>
<Button
android:id="@+id/btnNotificationSoundReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset"
android:textAllCaps="false"
/>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/llContent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<!-- Need to wrap DragListView in another layout for wrap_content to work for some reason -->
<com.woxthebox.draglistview.DragListView
android:id="@+id/drag_list_view"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
>
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:layout_marginTop="4dp"
android:layout_weight="1"
android:gravity="center"
android:text="@string/highlight_desc"
android:textSize="12sp"
/>
<ImageButton
android:id="@+id/btnAdd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/new_item"
android:src="?attr/ic_add"
/>
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<com.woxthebox.draglistview.swipe.ListSwipeItem
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:leftViewId="@+id/item_left"
app:rightViewId="@+id/item_right"
app:swipeViewId="@+id/item_layout">
<TextView
android:id="@+id/item_left"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignBottom="@+id/item_layout"
android:layout_alignTop="@+id/item_layout"
android:background="#0088ff"
android:gravity="center"
android:text="@string/app_name"
android:textColor="@android:color/white"
android:textSize="20sp"/>
<TextView
android:id="@+id/item_right"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignBottom="@+id/item_layout"
android:layout_alignTop="@+id/item_layout"
android:background="?attr/colorColumnListDeleteBackground"
android:gravity="center_vertical"
android:text="@string/delete"
android:textColor="?attr/colorColumnListDeleteText"
android:textSize="20sp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
/>
<LinearLayout
android:id="@id/item_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/column_list_selector"
android:gravity="center_vertical"
android:orientation="horizontal"
>
<ImageView
android:id="@+id/ivDragHandle"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="?attr/colorColumnListDragHandleBackground"
android:contentDescription="@string/drag_handle"
android:scaleType="center"
android:src="?attr/ic_knob"
android:visibility="gone"
/>
<TextView
android:id="@+id/tvName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:textSize="20sp"
android:minHeight="48dp"
android:gravity="center_vertical|start"
android:paddingStart="12dp"
android:paddingEnd="12dp"
/>
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/btnSound"
android:src="?attr/ic_volume_up"
android:contentDescription="@string/check_sound"
/>
</LinearLayout>
</com.woxthebox.draglistview.swipe.ListSwipeItem>

View File

@ -128,6 +128,10 @@
android:id="@+id/nav_muted_word"
android:icon="?attr/ic_setting"
android:title="@string/muted_word"/>
<item
android:id="@+id/nav_highlight_word"
android:icon="?attr/ic_setting"
android:title="@string/highlight_word"/>
<item
android:id="@+id/nav_app_about"

View File

@ -587,8 +587,14 @@
<string name="dont_repeat_download_to_same_url">Can\'t repeat downloading same URL in a short time.</string>
<string name="zooming_of">×%3$.1f\n%1$d×%2$d</string>
<string name="media_attachment_still_uploading">Media uploading has not completed yet.</string>
<string name="highlight_word">Highlight word</string>
<string name="new_item">New item…</string>
<string name="word_empty">Please input keyword.</string>
<string name="already_exist">already exist.</string>
<string name="highlight_desc">Swipe to delete. You may need reload column to check update.</string>
<string name="check_sound">Check sound</string>
<!--<string name="abc_action_bar_home_description">Revenir à l\'accueil</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>-->
<!--<string name="abc_action_bar_home_subtitle_description_format">%1$s, %2$s, %3$s</string>-->
<!--<string name="abc_action_bar_up_description">Revenir en haut de la page</string>-->

View File

@ -874,5 +874,11 @@
<string name="dont_repeat_download_to_same_url">同じURLを数秒以内に繰り返しダウンロードすることはできません</string>
<string name="zooming_of">×%3$.1f\n%1$d×%2$d</string>
<string name="media_attachment_still_uploading">添付メディアのアップロードが終わってません</string>
<string name="highlight_word">Highlight word</string>
<string name="new_item">New item…</string>
<string name="word_empty">Please input keyword.</string>
<string name="already_exist">既に存在します</string>
<string name="highlight_desc">Swipe to delete. You may need reload column to check update.</string>
<string name="check_sound">Check sound</string>
</resources>

View File

@ -133,5 +133,6 @@
<attr name="ic_copy" format="reference" />
<attr name="ic_left" format="reference" />
<attr name="ic_right" format="reference" />
<attr name="ic_volume_up" format="reference" />
</resources>

View File

@ -577,5 +577,11 @@
<string name="dont_repeat_download_to_same_url">Can\'t repeat downloading same URL in a few second.</string>
<string name="zooming_of">×%3$.1f\n%1$d×%2$d</string>
<string name="media_attachment_still_uploading">Media uploading has not completed yet.</string>
<string name="highlight_word">Highlight word</string>
<string name="new_item">New item…</string>
<string name="word_empty">Please input keyword.</string>
<string name="already_exist">already exist.</string>
<string name="highlight_desc">Swipe to delete. You may need reload column to check update.</string>
<string name="check_sound">Check sound</string>
</resources>

View File

@ -106,6 +106,8 @@
<item name="ic_copy">@drawable/ic_copy</item>
<item name="ic_left">@drawable/ic_left</item>
<item name="ic_right">@drawable/ic_right</item>
<item name="ic_volume_up">@drawable/ic_volume_up</item>
</style>
<style name="AppTheme.Light.NoActionBar" parent="AppTheme.Light">
@ -214,6 +216,7 @@
<item name="ic_copy">@drawable/ic_copy_dark</item>
<item name="ic_left">@drawable/ic_left_dark</item>
<item name="ic_right">@drawable/ic_right_dark</item>
<item name="ic_volume_up">@drawable/ic_volume_up_dark</item>
</style>