diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 9ce22b2f..8c531213 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -153,6 +153,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMain.java b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java index 1a3f57a7..4aed6713 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMain.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMain.java @@ -31,6 +31,8 @@ import android.widget.ImageButton; import android.widget.ImageView; import android.widget.LinearLayout; +import com.yasesprox.android.transcommusdk.TransCommuActivity; + import org.json.JSONException; import org.json.JSONObject; @@ -311,7 +313,7 @@ public class ActMain extends AppCompatActivity if( requestCode == REQUEST_APP_SETTING ){ showFooterColor(); } - + super.onActivityResult( requestCode, resultCode, data ); } @@ -469,6 +471,11 @@ 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_translation ){ +// Intent intent = new Intent(this, TransCommuActivity.class); +// intent.putExtra(TransCommuActivity.APPLICATION_CODE_EXTRA, "FJlDoBKitg"); +// this.startActivity(intent); +// // Handle the camera action // }else if( id == R.id.nav_gallery ){ // @@ -751,21 +758,71 @@ public class ActMain extends AppCompatActivity // プロフURL if( "https".equals( uri.getScheme() ) ){ if( uri.getPath().startsWith( "/@" ) ){ + // ステータスをアプリ内で開く + Matcher m = reStatusPage.matcher( uri.toString() ); + if( m.find() ){ + try{ + // https://mastodon.juggler.jp/@SubwayTooter/(status_id) + final String host = m.group( 1 ); + final long status_id = Long.parseLong( m.group( 3 ), 10 ); + + ArrayList< SavedAccount > account_list_same_host = new ArrayList<>(); + + for( SavedAccount a : SavedAccount.loadAccountList( log ) ){ + if( host.equalsIgnoreCase( a.host ) ){ + account_list_same_host.add( a ); + } + } + + // ソートする + Collections.sort( account_list_same_host, new Comparator< SavedAccount >() { + @Override public int compare( SavedAccount a, SavedAccount b ){ + return String.CASE_INSENSITIVE_ORDER.compare( AcctColor.getNickname( a.acct ), AcctColor.getNickname( b.acct ) ); + } + } ); + + + if( account_list_same_host.isEmpty() ){ + account_list_same_host.add( addPseudoAccount( host ) ); + } + + + AccountPicker.pick( this, true, true + , getString( R.string.open_status_from ) + , account_list_same_host + , new AccountPicker.AccountPickerCallback() { + @Override public void onAccountPicked( final SavedAccount ai ){ + openStatus( ai, status_id ); + } + } ); + + }catch( Throwable ex ){ + Utils.showToast( this, ex, "can't parse status id." ); + } + return; + } + // ユーザページをアプリ内で開く - Matcher m = reUserPage.matcher( uri.toString() ); + m = reUserPage.matcher( uri.toString() ); if( m.find() ){ // https://mastodon.juggler.jp/@SubwayTooter final String host = m.group( 1 ); final String user = Uri.decode( m.group( 2 ) ); - ArrayList< SavedAccount > account_list = SavedAccount.loadAccountList( log ); + ArrayList< SavedAccount > account_list_same_host = new ArrayList<>(); - - for( SavedAccount a : account_list ){ + for( SavedAccount a : SavedAccount.loadAccountList( log ) ){ if( host.equalsIgnoreCase( a.host ) ){ account_list_same_host.add( a ); } } + // ソートする + Collections.sort( account_list_same_host, new Comparator< SavedAccount >() { + @Override public int compare( SavedAccount a, SavedAccount b ){ + return String.CASE_INSENSITIVE_ORDER.compare( AcctColor.getNickname( a.acct ), AcctColor.getNickname( b.acct ) ); + } + } ); + if( account_list_same_host.isEmpty() ){ account_list_same_host.add( addPseudoAccount( host ) ); } @@ -1058,10 +1115,6 @@ public class ActMain extends AppCompatActivity } ); } - public void performConversation( SavedAccount access_info, TootStatus status ){ - addColumn( access_info, Column.TYPE_CONVERSATION, status.id ); - } - private void performAddTimeline( boolean bAllowPseudo, final int type, final Object... args ){ AccountPicker.pick( this, bAllowPseudo, true , getString( R.string.account_picker_add_timeline_of, Column.getColumnTypeName( this, type ) ) @@ -1080,10 +1133,6 @@ public class ActMain extends AppCompatActivity } ); } - public void openHashTag( SavedAccount access_info, String tag ){ - addColumn( access_info, Column.TYPE_HASHTAG, tag ); - } - public void performMuteApp( @NonNull TootApplication application ){ MutedApp.save( application.name ); for( Column column : pager_adapter.column_list ){ @@ -1170,7 +1219,8 @@ public class ActMain extends AppCompatActivity } static final Pattern reHashTag = Pattern.compile( "\\Ahttps://([^/]+)/tags/([^?#]+)\\z" ); - static final Pattern reUserPage = Pattern.compile( "\\Ahttps://([^/]+)/@([^?#]+)\\z" ); + static final Pattern reUserPage = Pattern.compile( "\\Ahttps://([^/]+)/@([^?#/]+)\\z" ); + static final Pattern reStatusPage = Pattern.compile( "\\Ahttps://([^/]+)/@([^?#/]+)/(\\d+)\\z" ); public void openChromeTab( final SavedAccount access_info, final String url, boolean noIntercept ){ try{ @@ -1192,6 +1242,26 @@ public class ActMain extends AppCompatActivity } } + // ステータスページをアプリから開く + m = reStatusPage.matcher( url ); + if( m.find() ){ + try{ + // https://mastodon.juggler.jp/@SubwayTooter/(status_id) + final String host = m.group( 1 ); + final long status_id = Long.parseLong( m.group( 3 ), 10 ); + if( host.equalsIgnoreCase( access_info.host ) ){ + openStatus( access_info, status_id ); + return; + }else{ + openStatusOtherInstance( access_info, url, host, status_id ); + return; + } + }catch( Throwable ex ){ + Utils.showToast( this, ex, "can't parse status id." ); + } + return; + } + // ユーザページをアプリ内で開く m = reUserPage.matcher( url ); if( m.find() ){ @@ -1210,6 +1280,8 @@ public class ActMain extends AppCompatActivity } ); return; } + + } try{ @@ -1236,6 +1308,67 @@ public class ActMain extends AppCompatActivity } } + public void openStatus( @NonNull SavedAccount access_info, @NonNull TootStatus status ){ + openStatus( access_info, status.id ); + } + + public void openStatus( @NonNull SavedAccount access_info, long status_id ){ + addColumn( access_info, Column.TYPE_CONVERSATION, status_id ); + } + + private void openStatusOtherInstance( final SavedAccount access_info, final String url, final String host, final long status_id ){ + ActionsDialog dialog = new ActionsDialog(); + + // ブラウザで表示する + dialog.addAction( getString( R.string.open_web_on_host, host ), new Runnable() { + @Override public void run(){ + openChromeTab( access_info, url, true ); + } + } ); + + // 同タンスのアカウント + ArrayList< SavedAccount > account_list = new ArrayList<>( ); + for( SavedAccount a: SavedAccount.loadAccountList( log ) ){ + if( host.equalsIgnoreCase( a.host ) ){ + account_list.add(a); + } + } + + // ソートする + Collections.sort( account_list, new Comparator< SavedAccount >() { + @Override public int compare( SavedAccount a, SavedAccount b ){ + return String.CASE_INSENSITIVE_ORDER.compare( AcctColor.getNickname( a.acct ), AcctColor.getNickname( b.acct ) ); + } + } ); + + for( SavedAccount a : account_list ){ + final SavedAccount _a = a; + dialog.addAction( getString( R.string.open_in_account, a.acct ), new Runnable() { + @Override public void run(){ + openStatus( _a, status_id ); + } + } ); + } + + // アカウントがないなら、疑似ホストを作る選択肢 + if( account_list .isEmpty() ){ + dialog.addAction( getString( R.string.open_in_pseudo_account, "?@" + host ), new Runnable() { + @Override public void run(){ + SavedAccount sa = addPseudoAccount( host ); + if( sa != null ){ + openStatus( sa, status_id ); + } + } + } ); + } + + dialog.show( this, getString( R.string.open_status_from ) ); + } + + public void openHashTag( SavedAccount access_info, String tag ){ + addColumn( access_info, Column.TYPE_HASHTAG, tag ); + } + // 他インスタンスのハッシュタグの表示 private void openHashTagOtherInstance( final SavedAccount access_info, final String url, final String host, final String tag ){ diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActText.java b/app/src/main/java/jp/juggler/subwaytooter/ActText.java index 8bd12122..07dfc85b 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActText.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ActText.java @@ -63,6 +63,7 @@ public class ActText extends AppCompatActivity implements View.OnClickListener { @Override protected void onCreate( @Nullable Bundle savedInstanceState ){ super.onCreate( savedInstanceState ); + App1.setActivityTheme( this, false ); initUI(); diff --git a/app/src/main/java/jp/juggler/subwaytooter/Column.java b/app/src/main/java/jp/juggler/subwaytooter/Column.java index 3b5955a2..a95cc1be 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/Column.java +++ b/app/src/main/java/jp/juggler/subwaytooter/Column.java @@ -36,6 +36,7 @@ import jp.juggler.subwaytooter.table.MutedApp; import jp.juggler.subwaytooter.table.MutedWord; import jp.juggler.subwaytooter.table.SavedAccount; import jp.juggler.subwaytooter.table.UserRelation; +import jp.juggler.subwaytooter.util.BucketList; import jp.juggler.subwaytooter.util.LogCategory; import jp.juggler.subwaytooter.util.MyListView; import jp.juggler.subwaytooter.util.ScrollPosition; @@ -706,7 +707,7 @@ class Column { String task_progress; - final ArrayList< Object > list_data = new ArrayList<>(); + final BucketList< Object > list_data = new BucketList<>(); private static boolean hasMedia( TootStatus status ){ if( status == null ) return false; @@ -1815,9 +1816,7 @@ class Column { } int added = list_new.size(); - list_new.addAll( list_data ); - list_data.clear(); - list_data.addAll( list_new ); + list_data.addAll( 0, list_new ); fireShowContent(); if( status_index >= 0 && refresh_after_toot == Pref.RAT_REFRESH_SCROLL ){ diff --git a/app/src/main/java/jp/juggler/subwaytooter/ItemListAdapter.java b/app/src/main/java/jp/juggler/subwaytooter/ItemListAdapter.java index 30cc9a4f..7a3d6a65 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ItemListAdapter.java +++ b/app/src/main/java/jp/juggler/subwaytooter/ItemListAdapter.java @@ -5,14 +5,14 @@ import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.BaseAdapter; -import java.util.ArrayList; +import java.util.List; import jp.juggler.subwaytooter.util.MyListView; class ItemListAdapter extends BaseAdapter implements AdapterView.OnItemClickListener { private final Column column; private final ActMain activity; - private final ArrayList< Object > list; + private final List< Object > list; ItemListAdapter( ActMain activity,Column column ){ this.activity = activity; diff --git a/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.java b/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.java index 591220a1..f3edcbe5 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.java +++ b/app/src/main/java/jp/juggler/subwaytooter/StatusButtons.java @@ -106,7 +106,7 @@ class StatusButtons implements View.OnClickListener { if( close_window != null ) close_window.dismiss(); switch( v.getId() ){ case R.id.btnConversation: - activity.performConversation( access_info, status ); + activity.openStatus( access_info, status ); break; case R.id.btnReply: if( access_info.isPseudo() ){ diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/BucketList.java b/app/src/main/java/jp/juggler/subwaytooter/util/BucketList.java new file mode 100644 index 00000000..652dc237 --- /dev/null +++ b/app/src/main/java/jp/juggler/subwaytooter/util/BucketList.java @@ -0,0 +1,224 @@ +package jp.juggler.subwaytooter.util; + +import android.support.annotation.NonNull; + +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.RandomAccess; + +public class BucketList < E > + extends AbstractList< E > + implements Iterable< E >, RandomAccess +{ + + private static final int DEFAULT_BUCKET_CAPACITY = 1024; + + @SuppressWarnings("WeakerAccess") + public BucketList( int initialStep ){ + mStep = initialStep; + } + + @SuppressWarnings("WeakerAccess") + public BucketList(){ + this( DEFAULT_BUCKET_CAPACITY ); + } + + + private static class Bucket < E > extends ArrayList< E > { + int total_start; + int total_end; + + Bucket( int capacity ){ + super( capacity ); + } + } + + private final ArrayList< Bucket< E > > groups = new ArrayList<>(); + private int mSize; + private int mStep; + + private void updateIndex(){ + int n = 0; + for( Bucket< E > bucket : groups ){ + bucket.total_start = n; + bucket.total_end = n = n + bucket.size(); + } + mSize = n; + } + + private static class BucketPos { + int group_index; + int bucket_index; + + BucketPos(){ + } + + BucketPos( int gi, int bi ){ + this.group_index = gi; + this.bucket_index = bi; + } + } + + private static final ThreadLocal< BucketPos > pos_internal = new ThreadLocal< BucketPos >() { + @Override protected BucketPos initialValue(){ + return new BucketPos(); + } + }; + + private BucketPos findPos( BucketPos dst, int total_index ){ + + if( total_index < 0 || total_index >= mSize ){ + throw new ArrayIndexOutOfBoundsException( "findPos: bad index=" + total_index + ", size=" + mSize ); + } + + // binary search + int gs = 0; + int ge = groups.size(); + for( ; ; ){ + int gi = ( gs + ge ) >> 1; + Bucket< E > group = groups.get( gi ); + if( total_index < group.total_start ){ + ge = gi; + }else if( total_index >= group.total_end ){ + gs = gi + 1; + }else{ + if( dst == null ) dst = new BucketPos(); + dst.group_index = gi; + dst.bucket_index = total_index - group.total_start; + return dst; + } + } + } + + @Override + public void clear(){ + groups.clear(); + mSize = 0; + } + + // 末尾への追加 + @Override + public boolean addAll( @NonNull Collection< ? extends E > c ){ + int c_size = c.size(); + if( c_size == 0 ) return false; + + // 最後のバケツに収まるなら、最後のバケツの中に追加する + if( groups.size() > 0 ){ + Bucket< E > bucket = groups.get( groups.size() - 1 ); + if( bucket.size() + c_size <= mStep ){ + bucket.addAll( c ); + bucket.total_end += c_size; + mSize += c_size; + return true; + } + } + // 新しいバケツを作って、そこに追加する + Bucket< E > bucket = new Bucket<>( mStep ); + bucket.addAll( c ); + bucket.total_start = mSize; + bucket.total_end = mSize + c_size; + mSize += c_size; + groups.add( bucket ); + return true; + } + + @Override + public boolean addAll( int index, @NonNull Collection< ? extends E > c ){ + + // indexが終端なら、終端に追加する + // バケツがカラの場合もここ + if( index == mSize ){ + return addAll( c ); + } + + int c_size = c.size(); + if( c_size == 0 ) return false; + + BucketPos pos = findPos( pos_internal.get(), index ); + Bucket< E > bucket = groups.get( pos.group_index ); + + // 挿入位置がバケツの先頭ではないか、バケツのサイズに問題がないなら + if( pos.bucket_index > 0 || bucket.size() + c_size <= mStep ){ + // バケツの中に挿入する + bucket.addAll( pos.bucket_index, c ); + }else{ + // 新しいバケツを作って、そこに追加する + bucket = new Bucket<>( mStep ); + bucket.addAll( c ); + groups.add( pos.group_index, bucket ); + } + updateIndex(); + return true; + } + + public E remove( int index ){ + BucketPos pos = findPos( pos_internal.get(), index ); + Bucket< E > bucket = groups.get( pos.group_index ); + E data = bucket.remove( pos.bucket_index ); + updateIndex(); + return data; + } + + public int size(){ + return mSize; + } + + public boolean isEmpty(){ + return 0 == mSize; + } + + public E get( int idx ){ + BucketPos pos = findPos( pos_internal.get(), idx ); + return groups.get( pos.group_index ).get( pos.bucket_index ); + } + + private class MyIterator implements Iterator< E > { + private final BucketPos pos; // indicates next read point + + MyIterator(){ + pos = new BucketPos( 0, 0 ); + } + + @Override public boolean hasNext(){ + for( ; ; ){ + if( pos.group_index >= groups.size() ){ + return false; + } + Bucket< E > bucket = groups.get( pos.group_index ); + if( pos.bucket_index >= bucket.size() ){ + pos.bucket_index = 0; + ++ pos.group_index; + continue; + } + return true; + } + } + + @Override public E next(){ + for( ; ; ){ + if( pos.group_index >= groups.size() ){ + throw new NoSuchElementException(); + } + Bucket< E > bucket = groups.get( pos.group_index ); + if( pos.bucket_index >= bucket.size() ){ + pos.bucket_index = 0; + ++ pos.group_index; + continue; + } + return bucket.get( pos.bucket_index++ ); + } + } + + @Override public void remove(){ + throw new UnsupportedOperationException(); + } + } + + @NonNull @Override public Iterator< E > iterator(){ + return new MyIterator(); + } + +} diff --git a/app/src/main/res/layout/lv_status.xml b/app/src/main/res/layout/lv_status.xml index d2ac4cf3..df19c208 100644 --- a/app/src/main/res/layout/lv_status.xml +++ b/app/src/main/res/layout/lv_status.xml @@ -26,6 +26,7 @@ android:layout_height="32dp" android:layout_marginEnd="4dp" android:scaleType="fitEnd" + android:importantForAccessibility="no" /> - - - - -