(試験実装)アプリ設定に「添付メディア/内蔵メディアビューアを使う」を追加

This commit is contained in:
tateisu 2017-12-21 20:33:35 +09:00
parent 30f069a445
commit 503cef6578
68 changed files with 1301 additions and 45 deletions

View File

@ -88,6 +88,9 @@ dependencies {
compile 'com.astuetz:pagerslidingtabstrip:1.0.1'
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
implementation 'com.google.android.exoplayer:exoplayer:r2.5.4'
}
apply plugin: 'com.google.gms.google-services'

View File

@ -204,6 +204,10 @@
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
/>
<activity
android:name=".ActMediaViewer"
/>
<meta-data
android:name="android.max_aspect"
android:value="100.0"
@ -221,6 +225,14 @@
/>
</provider>
<receiver android:name=".DownloadReceiver">
<intent-filter>
<action android:name="android.intent.action.DOWNLOAD_NOTIFICATION_CLICKED" />
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
</intent-filter>
</receiver>
<!-- okhttp3クライアントを指定する必要があるため、マニフェスト経由での組み込みは行わない -->
<!--<meta-data-->
<!--android:name="com.bumptech.glide.integration.okhttp3.OkHttpGlideModule"-->

View File

@ -106,6 +106,7 @@ public class ActAppSetting extends AppCompatActivity
Switch swShortAcctLocalUser;
Switch swDisableEmojiAnimation;
Switch swAllowNonSpaceBeforeEmojiShortcode;
Switch swUseInternalMediaViewer;
Spinner spBackButtonAction;
Spinner spUITheme;
@ -231,6 +232,8 @@ public class ActAppSetting extends AppCompatActivity
swAllowNonSpaceBeforeEmojiShortcode= findViewById( R.id.swAllowNonSpaceBeforeEmojiShortcode );
swAllowNonSpaceBeforeEmojiShortcode.setOnCheckedChangeListener( this );
swUseInternalMediaViewer= findViewById( R.id.swUseInternalMediaViewer );
swUseInternalMediaViewer.setOnCheckedChangeListener( this );
cbNotificationSound = findViewById( R.id.cbNotificationSound );
cbNotificationVibration = findViewById( R.id.cbNotificationVibration );
@ -395,7 +398,7 @@ public class ActAppSetting extends AppCompatActivity
swShortAcctLocalUser.setChecked( pref.getBoolean( Pref.KEY_SHORT_ACCT_LOCAL_USER, false ) );
swDisableEmojiAnimation.setChecked( pref.getBoolean( Pref.KEY_DISABLE_EMOJI_ANIMATION, false ) );
swAllowNonSpaceBeforeEmojiShortcode.setChecked( pref.getBoolean( Pref.KEY_ALLOW_NON_SPACE_BEFORE_EMOJI_SHORTCODE, false ) );
swUseInternalMediaViewer.setChecked( pref.getBoolean( Pref.KEY_USE_INTERNAL_MEDIA_VIEWER, false ) );
// Switch with default true
swDisableFastScroller.setChecked( pref.getBoolean( Pref.KEY_DISABLE_FAST_SCROLLER, true ) );
swPriorChrome.setChecked( pref.getBoolean( Pref.KEY_PRIOR_CHROME, true ) );
@ -470,6 +473,7 @@ public class ActAppSetting extends AppCompatActivity
.putBoolean( Pref.KEY_SHORT_ACCT_LOCAL_USER, swShortAcctLocalUser.isChecked() )
.putBoolean( Pref.KEY_DISABLE_EMOJI_ANIMATION, swDisableEmojiAnimation.isChecked() )
.putBoolean( Pref.KEY_ALLOW_NON_SPACE_BEFORE_EMOJI_SHORTCODE, swAllowNonSpaceBeforeEmojiShortcode.isChecked() )
.putBoolean( Pref.KEY_USE_INTERNAL_MEDIA_VIEWER, swUseInternalMediaViewer.isChecked() )
.putBoolean( Pref.KEY_NOTIFICATION_SOUND, cbNotificationSound.isChecked() )

View File

@ -1825,28 +1825,8 @@ public class ActMain extends AppCompatActivity
}
}
do{
if( pref.getBoolean( Pref.KEY_PRIOR_CHROME, true ) ){
try{
// 初回はChrome指定で試す
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setToolbarColor( Styler.getAttributeColor( this, R.attr.colorPrimary ) ).setShowTitle( true );
CustomTabsIntent customTabsIntent = builder.build();
customTabsIntent.intent.setComponent( new ComponentName( "com.android.chrome", "com.google.android.apps.chrome.Main" ) );
customTabsIntent.launchUrl( this, Uri.parse( url ) );
break;
}catch( Throwable ex2 ){
log.e( ex2, "openChromeTab: missing chrome. retry to other application." );
}
}
// chromeがないなら ResolverActivity でアプリを選択させる
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setToolbarColor( Styler.getAttributeColor( this, R.attr.colorPrimary ) ).setShowTitle( true );
CustomTabsIntent customTabsIntent = builder.build();
customTabsIntent.launchUrl( this, Uri.parse( url ) );
}while( false );
App1.openCustomTab( this,url);
}catch( Throwable ex ){
// log.trace( ex );

View File

@ -0,0 +1,570 @@
package jp.juggler.subwaytooter;
import android.annotation.SuppressLint;
import android.app.DownloadManager;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
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.view.Window;
import android.widget.TextView;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.extractor.ExtractorsFactory;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.ui.SimpleExoPlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
import jp.juggler.subwaytooter.api.TootApiResult;
import jp.juggler.subwaytooter.api.TootApiTask;
import jp.juggler.subwaytooter.api.entity.TootAttachment;
import jp.juggler.subwaytooter.dialog.ActionsDialog;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
import jp.juggler.subwaytooter.view.PinchBitmapView;
import okhttp3.Call;
import okhttp3.Response;
public class ActMediaViewer extends AppCompatActivity implements View.OnClickListener {
static final LogCategory log = new LogCategory( "ActMediaViewer" );
static String encodeMediaList( @Nullable TootAttachment.List list ){
JSONArray a = new JSONArray();
if( list != null ){
for( TootAttachment ta : list ){
try{
JSONObject item = ta.encodeJSON();
a.put( item );
}catch( JSONException ex ){
log.e( ex, "encode failed." );
}
}
}
return a.toString();
}
static TootAttachment.List decodeMediaList( @Nullable String src ){
TootAttachment.List dst_list = new TootAttachment.List();
if( src != null ){
try{
JSONArray a = new JSONArray( src );
for( int i = 0, ie = a.length() ; i < ie ; ++ i ){
JSONObject obj = a.optJSONObject( i );
TootAttachment ta = TootAttachment.parse( obj );
if( ta != null ) dst_list.add( ta );
}
}catch( Throwable ex ){
log.e( ex, "decodeMediaList failed." );
}
}
return dst_list;
}
static final String EXTRA_IDX = "idx";
static final String EXTRA_DATA = "data";
public static void open( @NonNull ActMain activity, @NonNull TootAttachment.List list, int idx ){
Intent intent = new Intent( activity, ActMediaViewer.class );
intent.putExtra( EXTRA_IDX, idx );
JSONArray a = new JSONArray();
for( TootAttachment ta : list ){
try{
JSONObject item = ta.encodeJSON();
a.put( item );
}catch( JSONException ex ){
log.e( ex, "encode failed." );
}
}
intent.putExtra( EXTRA_DATA, encodeMediaList( list ) );
activity.startActivity( intent );
}
int idx;
TootAttachment.List media_list;
@Override protected void onSaveInstanceState( Bundle outState ){
super.onSaveInstanceState( outState );
outState.putInt( EXTRA_IDX, idx );
outState.putString( EXTRA_DATA, encodeMediaList( media_list ) );
}
@Override protected void onCreate( @Nullable Bundle savedInstanceState ){
super.onCreate( savedInstanceState );
App1.setActivityTheme( this, true );
requestWindowFeature( Window.FEATURE_NO_TITLE );
if( savedInstanceState == null ){
Intent intent = getIntent();
this.idx = intent.getIntExtra( EXTRA_IDX, idx );
this.media_list = decodeMediaList( intent.getStringExtra( EXTRA_DATA ) );
}else{
this.idx = savedInstanceState.getInt( EXTRA_IDX );
this.media_list = decodeMediaList( savedInstanceState.getString( EXTRA_DATA ) );
}
if( idx < 0 || idx >= media_list.size() ) idx = 0;
initUI();
load();
}
@Override protected void onDestroy(){
super.onDestroy();
pbvImage.setBitmap( null );
exoPlayer.release();
exoPlayer = null;
}
PinchBitmapView pbvImage;
View btnPrevious;
View btnNext;
TextView tvError;
SimpleExoPlayer exoPlayer;
SimpleExoPlayerView exoView;
void initUI(){
setContentView( R.layout.act_media_viewer );
pbvImage = findViewById( R.id.pbvImage );
btnPrevious = findViewById( R.id.btnPrevious );
btnNext = findViewById( R.id.btnNext );
exoView = findViewById( R.id.exoView );
tvError = findViewById( R.id.tvError );
boolean enablePaging = media_list.size() > 1;
btnPrevious.setEnabled( enablePaging );
btnNext.setEnabled( enablePaging );
btnPrevious.setAlpha( enablePaging ? 1f : 0.3f );
btnNext.setAlpha( enablePaging ? 1f : 0.3f );
btnPrevious.setOnClickListener( this );
btnNext.setOnClickListener( this );
findViewById( R.id.btnDownload ).setOnClickListener( this );
findViewById( R.id.btnMore ).setOnClickListener( this );
// findViewById( R.id.btnBrowser ).setOnClickListener( this );
// findViewById( R.id.btnShare ).setOnClickListener( this );
// findViewById( R.id.btnCopy ).setOnClickListener( this );
pbvImage.setCallback( new PinchBitmapView.Callback() {
@Override public void onSwipe( int delta ){
if( isDestroyed() ) return;
loadDelta( delta );
}
} );
exoPlayer = ExoPlayerFactory.newSimpleInstance( this, new DefaultTrackSelector());
exoView.setPlayer( exoPlayer );
exoPlayer.addListener( new Player.EventListener() {
@Override public void onTimelineChanged( Timeline timeline, Object manifest ){
log.d("exoPlayer onTimelineChanged");
}
@Override
public void onTracksChanged( TrackGroupArray trackGroups, TrackSelectionArray trackSelections ){
log.d("exoPlayer onTracksChanged");
}
@Override public void onLoadingChanged( boolean isLoading ){
log.d("exoPlayer onLoadingChanged");
}
@Override public void onPlayerStateChanged( boolean playWhenReady, int playbackState ){
log.d("exoPlayer onPlayerStateChanged %s %s",playWhenReady,playbackState);
}
@Override public void onRepeatModeChanged( int repeatMode ){
log.d("exoPlayer onRepeatModeChanged %d",repeatMode);
}
@Override public void onPlayerError( ExoPlaybackException error ){
log.d("exoPlayer onPlayerError");
Utils.showToast( ActMediaViewer.this,error,"player error." );
}
@Override public void onPositionDiscontinuity(){
log.d("exoPlayer onPositionDiscontinuity");
}
@Override
public void onPlaybackParametersChanged( PlaybackParameters playbackParameters ){
log.d("exoPlayer onPlaybackParametersChanged");
}
} );
}
void loadDelta( int delta ){
if( media_list.size() < 2 ) return;
int size = media_list.size();
idx = ( idx + size + delta ) % size;
load();
}
void load(){
exoPlayer.stop();
if( media_list.isEmpty() ){
pbvImage.setVisibility( View.GONE );
exoView.setVisibility( View.GONE );
tvError.setVisibility( View.VISIBLE );
tvError.setText( R.string.media_attachment_empty );
return;
}
TootAttachment ta = media_list.get( idx );
// TODO ta.description をどこかに表示する
if( TootAttachment.TYPE_IMAGE.equals( ta.type )){
loadBitmap(ta);
}else if( TootAttachment.TYPE_VIDEO.equals( ta.type ) || TootAttachment.TYPE_GIFV.equals( ta.type ) ){
pbvImage.setVisibility( View.GONE );
loadVideo(ta);
}else{
// maybe TYPE_UNKNOWN
showError( getString( R.string.media_attachment_type_error, ta.type ) );
}
}
void showError(@NonNull String message){
exoView.setVisibility( View.GONE );
pbvImage.setVisibility( View.GONE );
tvError.setVisibility( View.VISIBLE );
tvError.setText( message );
}
@SuppressLint("StaticFieldLeak")
void loadVideo(TootAttachment ta){
final String url = ta.getLargeUrl( App1.pref );
if( url == null ){
showError( "missing media attachment url.");
return;
}
tvError.setVisibility( View.GONE );
pbvImage.setVisibility( View.GONE );
exoView.setVisibility( View.VISIBLE );
DefaultBandwidthMeter defaultBandwidthMeter =new DefaultBandwidthMeter();
ExtractorsFactory extractorsFactory = new DefaultExtractorsFactory();
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(
this
, Util.getUserAgent(this, getString(R.string.app_name))
, defaultBandwidthMeter
);
MediaSource mediaSource = new ExtractorMediaSource( Uri.parse( url )
, dataSourceFactory
, extractorsFactory
, App1.getAppState( this ).handler
, new ExtractorMediaSource.EventListener() {
@Override public void onLoadError( IOException error ){
showError( Utils.formatError( error,"load error." ));
}
}
);
exoPlayer.prepare(mediaSource);
exoPlayer.setPlayWhenReady( true);
if( TootAttachment.TYPE_GIFV.equals( ta.type ) ){
exoPlayer.setRepeatMode( Player.REPEAT_MODE_ALL );
}else{
exoPlayer.setRepeatMode( Player.REPEAT_MODE_OFF );
}
}
@SuppressLint("StaticFieldLeak")
void loadBitmap(TootAttachment ta){
final String url = ta.getLargeUrl( App1.pref );
if( url == null ){
showError( "missing media attachment url.");
return;
}
tvError.setVisibility( View.GONE );
exoView.setVisibility( View.GONE );
pbvImage.setVisibility( View.VISIBLE );
pbvImage.setBitmap( null );
new TootApiTask( this, true ) {
private final BitmapFactory.Options options = new BitmapFactory.Options();
private Bitmap decodeBitmap( byte[] data, @SuppressWarnings("SameParameterValue") int pixel_max ){
publishApiProgress( "image decoding.." );
options.inJustDecodeBounds = true;
options.inScaled = false;
options.outWidth = 0;
options.outHeight = 0;
BitmapFactory.decodeByteArray( data, 0, data.length, options );
int w = options.outWidth;
int h = options.outHeight;
if( w <= 0 || h <= 0 ){
log.e( "can't decode bounds." );
return null;
}
int bits = 0;
while( w > pixel_max || h > pixel_max ){
++ bits;
w >>= 1;
h >>= 1;
}
options.inJustDecodeBounds = false;
options.inSampleSize = 1 << bits;
return BitmapFactory.decodeByteArray( data, 0, data.length, options );
}
@NonNull TootApiResult getHttpCached( @NonNull String url ){
Response response;
try{
okhttp3.Request request = new okhttp3.Request.Builder()
.url( url )
.cacheControl( App1.CACHE_5MIN )
.build();
publishApiProgress( getString( R.string.request_api, request.method(), url ) );
Call call = App1.ok_http_client2.newCall( request );
response = call.execute();
}catch( Throwable ex ){
return new TootApiResult( Utils.formatError( ex, "network error." ) );
}
if( ! response.isSuccessful() ){
return new TootApiResult( Utils.formatResponse( response, "response error" ) );
}
try{
//noinspection ConstantConditions
data = response.body().bytes();
return new TootApiResult( "" );
}catch( Throwable ex ){
return new TootApiResult( Utils.formatError( ex, "content error." ) );
}
}
byte[] data;
Bitmap bitmap;
@Override protected TootApiResult doInBackground( Void... voids ){
TootApiResult result = getHttpCached( url );
if( data == null ) return result;
this.bitmap = decodeBitmap( data, 2048 );
if( bitmap == null ) return new TootApiResult( "image decode failed." );
return result;
}
@Override protected void handleResult( @Nullable TootApiResult result ){
if( bitmap != null ){
pbvImage.setBitmap( bitmap );
return;
}
if( result != null ) Utils.showToast( ActMediaViewer.this, true, result.error );
}
}.executeOnExecutor( App1.task_executor );
}
@Override public void onClick( View v ){
try{
switch( v.getId() ){
case R.id.btnPrevious:
loadDelta( - 1 );
break;
case R.id.btnNext:
loadDelta( + 1 );
break;
case R.id.btnDownload:
download( media_list.get( idx ) );
break;
// case R.id.btnBrowser:
// share( Intent.ACTION_VIEW, media_list.get( idx ) );
// break;
// case R.id.btnShare:
// share( Intent.ACTION_SEND, media_list.get( idx ) );
// break;
// case R.id.btnCopy:
// copy( media_list.get( idx ) );
// break;
case R.id.btnMore:
more( media_list.get( idx ) );
break;
}
}catch( Throwable ex ){
Utils.showToast( this, ex, "action failed." );
}
}
void download( @NonNull TootAttachment ta ){
String url = ta.getLargeUrl( App1.pref );
if( url == null ) return;
DownloadManager downLoadManager = (DownloadManager) getSystemService( DOWNLOAD_SERVICE );
if( downLoadManager == null ){
Utils.showToast( this, false, "download manager is not on your device." );
return;
}
String fname = url.replaceFirst( "https?://", "" ).replaceAll( "[^.\\w\\d_-]+", "-" );
if( fname.length() >= 20 ) fname = fname.substring( fname.length() - 20 );
DownloadManager.Request request = new DownloadManager.Request( Uri.parse( url ) );
request.setDestinationInExternalPublicDir( Environment.DIRECTORY_DOWNLOADS, fname );
request.setTitle( fname );
request.setAllowedNetworkTypes( DownloadManager.Request.NETWORK_MOBILE | DownloadManager.Request.NETWORK_WIFI );
//メディアスキャンを許可する
request.allowScanningByMediaScanner();
//ダウンロード中ダウンロード完了時にも通知を表示する
request.setNotificationVisibility( DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED );
downLoadManager.enqueue( request );
Utils.showToast( this, false, R.string.downloading );
}
void share( String action, @NonNull TootAttachment ta ){
String url = ta.getLargeUrl( App1.pref );
if( url == null ) return;
try{
Intent intent = new Intent( action );
intent.addFlags( Intent.FLAG_ACTIVITY_NEW_TASK );
if( action.equals( Intent.ACTION_SEND ) ){
intent.setType( "text/plain" );
intent.putExtra( Intent.EXTRA_TEXT, url );
}else{
intent.setData( Uri.parse( url ) );
}
startActivity( intent );
}catch( Throwable ex ){
Utils.showToast( this, ex, "can't open app." );
}
}
void copy( @NonNull TootAttachment ta ){
String url = ta.getLargeUrl( App1.pref );
if( url == null ) return;
ClipboardManager cm = (ClipboardManager) getSystemService( CLIPBOARD_SERVICE );
if( cm == null ){
Utils.showToast( this, false, "can't access to ClipboardManager" );
return;
}
try{
//クリップボードに格納するItemを作成
ClipData.Item item = new ClipData.Item( url );
String[] mimeType = new String[ 1 ];
mimeType[ 0 ] = ClipDescription.MIMETYPE_TEXT_PLAIN;
//クリップボードに格納するClipDataオブジェクトの作成
ClipData cd = new ClipData( new ClipDescription( "media URL", mimeType ), item );
//クリップボードにデータを格納
cm.setPrimaryClip( cd );
Utils.showToast( this, false, R.string.url_is_copied );
}catch( Throwable ex ){
Utils.showToast( this, ex, "clipboard access failed." );
}
}
void more( @NonNull TootAttachment ta ){
ActionsDialog ad = new ActionsDialog();
ad.addAction( getString( R.string.open_in_browser ), new Runnable() {
@Override public void run(){
share( Intent.ACTION_VIEW, media_list.get( idx ) );
}
} );
ad.addAction( getString( R.string.share_url ), new Runnable() {
@Override public void run(){
share( Intent.ACTION_SEND, media_list.get( idx ) );
}
} );
ad.addAction( getString( R.string.copy_url ), new Runnable() {
@Override public void run(){
copy( media_list.get( idx ) );
}
} );
addMoreMenu( ad, "url", ta.url, Intent.ACTION_VIEW );
addMoreMenu( ad, "remote_url", ta.remote_url, Intent.ACTION_VIEW );
addMoreMenu( ad, "preview_url", ta.preview_url, Intent.ACTION_VIEW );
addMoreMenu( ad, "text_url", ta.text_url, Intent.ACTION_VIEW );
ad.show( this, null );
}
void addMoreMenu( ActionsDialog ad, String caption_prefix, final String url, final String action ){
if( TextUtils.isEmpty( url ) ) return;
String caption = getString( R.string.open_browser_of, caption_prefix );
ad.addAction( caption, new Runnable() {
@Override public void run(){
try{
Intent intent = new Intent( action, Uri.parse( url ) );
intent.addFlags( Intent.FLAG_ACTIVITY_NEW_TASK );
startActivity( intent );
}catch( Throwable ex ){
Utils.showToast( ActMediaViewer.this, ex, "can't open app." );
}
}
} );
}
}

View File

@ -3,12 +3,16 @@ package jp.juggler.subwaytooter;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Application;
import android.content.ComponentName;
import android.content.Context;
import android.content.SharedPreferences;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.customtabs.CustomTabsIntent;
import android.text.TextUtils;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
@ -26,6 +30,7 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
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.MutedApp;
@ -220,7 +225,7 @@ public class App1 extends Application {
public static OkHttpClient ok_http_client;
private static OkHttpClient ok_http_client2;
public static OkHttpClient ok_http_client2;
// public static final boolean USE_OLD_EMOJIONE = false;
// public static Typeface typeface_emoji;
@ -490,4 +495,38 @@ public class App1 extends Application {
allow_non_space_before_emoji_shortcode = pref.getBoolean( Pref.KEY_ALLOW_NON_SPACE_BEFORE_EMOJI_SHORTCODE,false );
}
// Chrome Custom Tab を開く
public static void openCustomTab( @NonNull Activity activity, @NonNull String url ){
try{
if( pref.getBoolean( Pref.KEY_PRIOR_CHROME, true ) ){
try{
// 初回はChrome指定で試す
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setToolbarColor( Styler.getAttributeColor( activity, R.attr.colorPrimary ) ).setShowTitle( true );
CustomTabsIntent customTabsIntent = builder.build();
customTabsIntent.intent.setComponent( new ComponentName( "com.android.chrome", "com.google.android.apps.chrome.Main" ) );
customTabsIntent.launchUrl( activity, Uri.parse( url ) );
return;
}catch( Throwable ex2 ){
log.e( ex2, "openChromeTab: missing chrome. retry to other application." );
}
}
// chromeがないなら ResolverActivity でアプリを選択させる
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setToolbarColor( Styler.getAttributeColor( activity, R.attr.colorPrimary ) ).setShowTitle( true );
CustomTabsIntent customTabsIntent = builder.build();
customTabsIntent.launchUrl( activity, Uri.parse( url ) );
}catch(Throwable ex){
log.e( ex, "openCustomTab: failed." );
}
}
public static void openCustomTab( @NonNull Activity activity, @NonNull TootAttachment ta){
String url = ta.getLargeUrl(pref);
if( url != null ){
openCustomTab( activity, url );
}
}
}

View File

@ -322,6 +322,7 @@ public class AppDataExporter {
case Pref.KEY_SHORT_ACCT_LOCAL_USER:
case Pref.KEY_DISABLE_EMOJI_ANIMATION:
case Pref.KEY_ALLOW_NON_SPACE_BEFORE_EMOJI_SHORTCODE:
case Pref.KEY_USE_INTERNAL_MEDIA_VIEWER:
boolean bv = reader.nextBoolean();
e.putBoolean( k, bv );
break;

View File

@ -0,0 +1,44 @@
package jp.juggler.subwaytooter;
import android.app.DownloadManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import jp.juggler.subwaytooter.util.Utils;
public class DownloadReceiver extends BroadcastReceiver {
@Override public void onReceive( Context context, Intent intent ){
if( intent == null ) return;
String action = intent.getAction();
if( action == null ) return;
if( DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals( action )){
long id = intent.getLongExtra( DownloadManager.EXTRA_DOWNLOAD_ID, 0L );
DownloadManager downloadManager = (DownloadManager) context.getSystemService( Context.DOWNLOAD_SERVICE );
if( downloadManager != null ){
DownloadManager.Query query = new DownloadManager.Query();
query.setFilterById( id );
Cursor cursor = downloadManager.query( query );
try{
if( cursor.moveToFirst() ){
int idx_status = cursor.getColumnIndex( DownloadManager.COLUMN_STATUS );
int idx_title = cursor.getColumnIndex( DownloadManager.COLUMN_TITLE );
String title = cursor.getString( idx_title );
if( DownloadManager.STATUS_SUCCESSFUL == cursor.getInt( idx_status ) ){
Utils.showToast( context, false, context.getString( R.string.download_complete, title ) );
}else{
Utils.showToast( context, false, context.getString( R.string.download_failed, title ) );
}
}
}finally{
cursor.close();
}
}
}
}
}

View File

@ -922,22 +922,14 @@ class ItemViewHolder implements View.OnClickListener, View.OnLongClickListener {
if( media_attachments == null ) return;
TootAttachment a = media_attachments.get( i );
String sv;
if( Pref.pref( activity ).getBoolean( Pref.KEY_PRIOR_LOCAL_URL, false ) ){
sv = a.url;
if( TextUtils.isEmpty( sv ) ){
sv = a.remote_url;
}
}else{
sv = a.remote_url;
if( TextUtils.isEmpty( sv ) ){
sv = a.url;
}
if( App1.pref.getBoolean( Pref. KEY_USE_INTERNAL_MEDIA_VIEWER,false) ){
ActMediaViewer.open( activity, media_attachments, i );
return;
}
int pos = activity.nextPosition( column );
activity.openChromeTab( pos, access_info, sv, false );
TootAttachment a = media_attachments.get( i );
App1.openCustomTab( activity, a);
}catch( Throwable ex ){
log.trace( ex );
}

View File

@ -20,7 +20,7 @@ public class Pref {
static final String KEY_DONT_CONFIRM_BEFORE_CLOSE_COLUMN = "DontConfirmBeforeCloseColumn";
static final String KEY_BACK_BUTTON_ACTION = "back_button_action";
static final String KEY_PRIOR_LOCAL_URL = "prior_local_url";
public static final String KEY_PRIOR_LOCAL_URL = "prior_local_url";
static final String KEY_DISABLE_FAST_SCROLLER = "disable_fast_scroller";
static final String KEY_UI_THEME = "ui_theme";
static final String KEY_SIMPLE_LIST = "simple_list";
@ -56,7 +56,7 @@ public class Pref {
static final String KEY_STREAM_LISTENER_CONFIG_DATA = "stream_listener_config_data";
static final String KEY_TABLET_TOOT_DEFAULT_ACCOUNT = "tablet_toot_default_account";
static final String KEY_PRIOR_CHROME = "prior_chrome";
public static final String KEY_PRIOR_CHROME = "prior_chrome";
static final String KEY_POST_BUTTON_BAR_AT_TOP = "post_button_bar_at_top";
@ -95,6 +95,8 @@ public class Pref {
public static final String KEY_ALLOW_NON_SPACE_BEFORE_EMOJI_SHORTCODE = "allow_non_space_before_emoji_shortcode";
public static final String KEY_MEDIA_SIZE_MAX = "max_media_size";
public static final String KEY_USE_INTERNAL_MEDIA_VIEWER = "use_internal_media_viewer";
// 項目を追加したらAppDataExporter#importPref のswitch文も更新すること
}

View File

@ -1,20 +1,24 @@
package jp.juggler.subwaytooter.api.entity;
import android.content.SharedPreferences;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.ArrayList;
import jp.juggler.subwaytooter.Pref;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
public class TootAttachment {
private static final LogCategory log = new LogCategory( "TootAttachment" );
public static class List extends ArrayList< TootAttachment > {
}
@ -52,7 +56,7 @@ public class TootAttachment {
try{
TootAttachment dst = new TootAttachment();
dst.json = src;
dst.id = Utils.optLongX(src, "id" );
dst.id = Utils.optLongX( src, "id" );
dst.type = Utils.optStringX( src, "type" );
dst.url = Utils.optStringX( src, "url" );
dst.remote_url = Utils.optStringX( src, "remote_url" );
@ -67,6 +71,18 @@ public class TootAttachment {
}
}
@NonNull public JSONObject encodeJSON() throws JSONException{
JSONObject dst = new JSONObject();
dst.put( "id", Long.toString( id ) );
if( type != null ) dst.put( "type", type );
if( url != null ) dst.put( "url", url );
if( remote_url != null ) dst.put( "remote_url", remote_url );
if( preview_url != null ) dst.put( "preview_url", preview_url );
if( text_url != null ) dst.put( "text_url", text_url );
if( description != null ) dst.put( "description", description );
return dst;
}
@NonNull public static List parseList( JSONArray array ){
List result = new List();
if( array != null ){
@ -82,6 +98,24 @@ public class TootAttachment {
return result;
}
@Nullable public String getLargeUrl( SharedPreferences pref ){
String sv;
if( pref.getBoolean( Pref.KEY_PRIOR_LOCAL_URL, false ) ){
sv = this.url;
if( TextUtils.isEmpty( sv ) ){
sv = this.remote_url;
}
}else{
sv = this.remote_url;
if( TextUtils.isEmpty( sv ) ){
sv = this.url;
}
}
return sv;
}
}
// v1.3 から 添付ファイルの画像のピクセルサイズが取得できるようになった

View File

@ -2,6 +2,7 @@ package jp.juggler.subwaytooter.util;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import java.io.ByteArrayInputStream;
@ -35,6 +36,7 @@ import android.os.Looper;
import android.os.storage.StorageManager;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.customtabs.CustomTabsIntent;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
@ -64,6 +66,11 @@ import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import it.sephiroth.android.library.exif2.ExifInterface;
import jp.juggler.subwaytooter.ActMain;
import jp.juggler.subwaytooter.App1;
import jp.juggler.subwaytooter.Pref;
import jp.juggler.subwaytooter.R;
import jp.juggler.subwaytooter.Styler;
import okhttp3.Response;
import okhttp3.ResponseBody;
@ -874,7 +881,6 @@ public class Utils {
return sb.toString().replaceAll( "\n+", "\n" );
}
public interface ScanViewCallback {
void onScanView( View v );
@ -1225,4 +1231,7 @@ public class Utils {
Bundle b = data.getExtras();
return b == null ? null : b.get( key );
}
}

View File

@ -0,0 +1,377 @@
package jp.juggler.subwaytooter.view;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import jp.juggler.subwaytooter.util.LogCategory;
import jp.juggler.subwaytooter.util.Utils;
public class PinchBitmapView extends View {
static final LogCategory log = new LogCategory( "PinchImageView" );
public interface Callback {
void onSwipe( int delta );
}
public PinchBitmapView( Context context ){
this( context, null );
init( context );
}
public PinchBitmapView( Context context, AttributeSet attrs ){
this( context, attrs, 0 );
init( context );
}
public PinchBitmapView( Context context, AttributeSet attrs, int defStyle ){
super( context, attrs, defStyle );
init( context );
}
@Override public boolean onTouchEvent( MotionEvent ev ){
return handleTouchEvent( ev );
}
private Bitmap bitmap;
private final Matrix matrix = new Matrix();
private final Paint paint = new Paint();
float swipe_velocity;
Callback callback;
public void setCallback( Callback callback ){
this.callback = callback;
}
private void init( Context context ){
paint.setFilterBitmap( true );
swipe_velocity = 100f * context.getResources().getDisplayMetrics().density;
}
@Override protected void onSizeChanged( int w, int h, int oldw, int oldh ){
super.onSizeChanged( w, h, oldw, oldh );
initializeScale();
}
public void setBitmap( Bitmap b ){
if( bitmap != null ){
bitmap.recycle();
}
this.bitmap = b;
initializeScale();
}
void initializeScale(){
if( bitmap != null && ! bitmap.isRecycled() ){
this.bitmap_w = bitmap.getWidth();
this.bitmap_h = bitmap.getHeight();
if( bitmap_w < 1f ) bitmap_w = 1f;
if( bitmap_h < 1f ) bitmap_h = 1f;
view_w = this.getWidth();
view_h = this.getHeight();
if( view_w < 1f ) view_w = 1f;
if( view_h < 1f ) view_h = 1f;
this.bitmap_aspect = bitmap_w / bitmap_h;
this.view_aspect = view_w / view_h;
if( view_aspect > bitmap_aspect ){
current_scale = view_h / bitmap_h;
}else{
current_scale = view_w / bitmap_w;
}
float draw_w = bitmap_w * current_scale;
float draw_h = bitmap_h * current_scale;
current_trans_x = ( view_w - draw_w ) / 2f;
current_trans_y = ( view_h - draw_h ) / 2f;
}
invalidate();
}
@Override protected void onDraw( Canvas canvas ){
super.onDraw( canvas );
if( bitmap != null && ! bitmap.isRecycled() ){
matrix.reset();
matrix.postScale( current_scale, current_scale );
matrix.postTranslate( current_trans_x, current_trans_y );
canvas.drawBitmap( bitmap, matrix, paint );
}
}
boolean handleTouchEvent( MotionEvent ev ){
if( bitmap == null || bitmap.isRecycled() ) return false;
if( velocityTracker != null ){
velocityTracker.addMovement( ev );
}
int action = ev.getAction();
switch( action ){
case MotionEvent.ACTION_DOWN:
if( velocityTracker != null ){
velocityTracker.recycle();
velocityTracker = null;
}
velocityTracker = VelocityTracker.obtain();
bDrag = false;
bImageMoved = false;
startTracking( ev );
break;
case MotionEvent.ACTION_POINTER_DOWN:
case MotionEvent.ACTION_POINTER_UP:
bDrag = true;
startTracking( ev );
break;
case MotionEvent.ACTION_MOVE:
nextTracking( ev );
break;
case MotionEvent.ACTION_UP:
nextTracking( ev );
if( ! bDrag ){
performClick();
}else if( ! bImageMoved ){
velocityTracker.computeCurrentVelocity( 1000 );
float xv = velocityTracker.getXVelocity();
log.d( "velocity %f", xv );
if( xv >= swipe_velocity ){
Utils.runOnMainThread( new Runnable() {
@Override public void run(){
if( callback != null ) callback.onSwipe( - 1 );
}
} );
}else if( xv <= - swipe_velocity ){
Utils.runOnMainThread( new Runnable() {
@Override public void run(){
if( callback != null ) callback.onSwipe( 1 );
}
} );
}
}
if( velocityTracker != null ){
velocityTracker.recycle();
velocityTracker = null;
}
break;
}
return true;
}
float touch_start_x;
float touch_start_y;
float touch_start_radius;
float touch_start_trans_x;
float touch_start_trans_y;
float touch_start_scale;
float view_aspect;
float bitmap_aspect;
float scale_min;
float scale_max;
float view_w;
float view_h;
float bitmap_w;
float bitmap_h;
float current_scale;
float current_trans_x;
float current_trans_y;
boolean bDrag;
boolean bImageMoved = false;
float drag_width;
int last_pointer_count;
final PointerAvg pos = new PointerAvg();
VelocityTracker velocityTracker;
static class PointerAvg {
float avg_x;
float avg_y;
float max_radius;
int count;
void update( MotionEvent ev ){
count = ev.getPointerCount();
if( count <= 1 ){
avg_x = ev.getX();
avg_y = ev.getY();
max_radius = 0f;
}else{
avg_x = 0f;
avg_y = 0f;
for( int i = 0 ; i < count ; ++ i ){
avg_x += ev.getX( i );
avg_y += ev.getY( i );
}
avg_x /= count;
avg_y /= count;
max_radius = 0f;
for( int i = 0 ; i < count ; ++ i ){
float dx = ev.getX( i ) - avg_x;
float dy = ev.getY( i ) - avg_y;
float delta = dx * dx + dy * dy;
if( delta > max_radius ) max_radius = delta;
}
max_radius = (float) Math.sqrt( max_radius );
if( max_radius < 0.5f ) max_radius = 0.5f;
}
}
}
void startTracking( MotionEvent ev ){
pos.update( ev );
last_pointer_count = pos.count;
touch_start_x = pos.avg_x;
touch_start_y = pos.avg_y;
touch_start_radius = pos.max_radius;
touch_start_trans_x = current_trans_x;
touch_start_trans_y = current_trans_y;
touch_start_scale = current_scale;
view_w = this.getWidth();
view_h = this.getHeight();
if( view_w < 1f ) view_w = 1f;
if( view_h < 1f ) view_h = 1f;
view_aspect = view_w / view_h;
if( view_aspect > bitmap_aspect ){
// ビューの方が横長画像の方が縦長
// 縦方向のサイズを使って最小スケールを決める
scale_min = view_h / bitmap_h / 2;
// ビューの方が横長画像の方が縦長
// 横方向のサイズを使って最大スケールを決める
scale_max = view_w / bitmap_w * 8;
}else{
scale_min = view_w / bitmap_w / 2;
scale_max = view_h / bitmap_h * 8;
}
if( scale_max < scale_min ) scale_max = scale_min * 4;
drag_width = getResources().getDisplayMetrics().density * 8f;
}
final Matrix tracking_matrix = new Matrix();
final Matrix tracking_matrix_inv = new Matrix();
final float[] points_dst = new float[ 2 ];
final float[] points_src = new float[ 2 ];
void nextTracking( MotionEvent ev ){
pos.update( ev );
if( pos.count != last_pointer_count ){
log.d( "nextTracking: pointer count changed" );
startTracking( ev );
return;
}
if( pos.count > 1 ){
// pos.avg_x,y が画像の座標空間でどこに位置するか調べる
tracking_matrix.reset();
tracking_matrix.postScale( current_scale, current_scale );
tracking_matrix.postTranslate( current_trans_x, current_trans_y );
tracking_matrix.invert( tracking_matrix_inv );
points_src[ 0 ] = pos.avg_x;
points_src[ 1 ] = pos.avg_y;
tracking_matrix_inv.mapPoints( points_dst, points_src );
float avg_on_image_x = points_dst[ 0 ];
float avg_on_image_y = points_dst[ 1 ];
// update scale
float new_scale = touch_start_scale * pos.max_radius / touch_start_radius;
new_scale = new_scale < scale_min ? scale_min : new_scale > scale_max ? scale_max : new_scale;
current_scale = new_scale;
// pos.avg_x,y が画像の座標空間でどこに位置するか再び調べる
tracking_matrix.reset();
tracking_matrix.postScale( current_scale, current_scale );
tracking_matrix.postTranslate( current_trans_x, current_trans_y );
tracking_matrix.invert( tracking_matrix_inv );
points_src[ 0 ] = pos.avg_x;
points_src[ 1 ] = pos.avg_y;
tracking_matrix_inv.mapPoints( points_dst, points_src );
float avg_on_image_x2 = points_dst[ 0 ];
float avg_on_image_y2 = points_dst[ 1 ];
// ズレた分 * scaleだけ移動させるとスケール変更時にタッチ中心がスクロールしないのではないか
float delta_x = avg_on_image_x2 - avg_on_image_x;
float delta_y = avg_on_image_y2 - avg_on_image_y;
touch_start_trans_x += current_scale * delta_x;
touch_start_trans_y += current_scale * delta_y;
invalidate();
}
// 平行移動
{
// start時から指を動かした量
float move_x = pos.avg_x - touch_start_x;
float move_y = pos.avg_y - touch_start_y;
if( Math.abs( move_x ) >= drag_width || Math.abs( move_y ) >= drag_width ){
bDrag = true;
}
// 画像の移動量
float trans_x = touch_start_trans_x + move_x;
float trans_y = touch_start_trans_y + move_y;
// 画像サイズとビューサイズを使って移動可能範囲をクリッピング
float draw_w = bitmap_w * current_scale;
float draw_h = bitmap_h * current_scale;
if( draw_w <= view_w ){
float remain = view_w - draw_w;
trans_x = remain / 2f;
}else{
float remain = draw_w - view_w;
trans_x = trans_x >= 0f ? 0f : trans_x < - remain ? - remain : trans_x;
}
if( draw_h <= view_h ){
float remain = view_h - draw_h;
trans_y = remain / 2f;
}else{
float remain = draw_h - view_h;
trans_y = trans_y >= 0f ? 0f : trans_y < - remain ? - remain : trans_y;
}
if( current_trans_x != trans_x || current_trans_y != trans_y ){
bImageMoved = true;
}
// TODO trans_x,trans_y を画像の移動量に反映させる
current_trans_x = trans_x;
current_trans_y = trans_y;
invalidate();
}
}
@Override public boolean performClick(){
initializeScale();
return true;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 426 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 250 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 608 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 292 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 396 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 948 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 828 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 804 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 710 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 409 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -494,6 +494,24 @@
android:text="@string/media_attachment"
/>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/use_internal_media_viewer"
/>
<LinearLayout style="@style/setting_row_form">
<Switch
android:id="@+id/swUseInternalMediaViewer"
style="@style/setting_horizontal_stretch"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView

View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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="match_parent"
android:orientation="vertical"
>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:id="@+id/flContent"
>
<jp.juggler.subwaytooter.view.PinchBitmapView
android:layout_height="match_parent"
android:layout_width="match_parent"
android:id="@+id/pbvImage"
/>
<com.google.android.exoplayer2.ui.SimpleExoPlayerView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/exoView"
/>
<TextView
android:layout_height="match_parent"
android:layout_width="match_parent"
android:id="@+id/tvError"
/>
</FrameLayout>
<com.google.android.flexbox.FlexboxLayout
android:id="@+id/flFooter"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:flexWrap="wrap"
app:alignContent="center"
>
<ImageButton
android:layout_width="48dp"
android:layout_height="48dp"
android:minWidth="48dp"
android:src="?attr/ic_left"
android:id="@+id/btnPrevious"
android:contentDescription="@string/previous"
/>
<ImageButton
android:layout_width="48dp"
android:layout_height="48dp"
android:minWidth="48dp"
android:src="?attr/ic_right"
android:id="@+id/btnNext"
android:contentDescription="@string/next"
/>
<ImageButton
android:layout_width="48dp"
android:layout_height="48dp"
android:minWidth="48dp"
android:src="?attr/ic_download"
android:id="@+id/btnDownload"
android:contentDescription="@string/download"
/>
<ImageButton
android:layout_width="48dp"
android:layout_height="48dp"
android:minWidth="48dp"
android:src="?attr/btn_more"
android:id="@+id/btnMore"
android:contentDescription="@string/more"
/>
<!--<ImageButton-->
<!--android:layout_width="48dp"-->
<!--android:layout_height="48dp"-->
<!--android:minWidth="48dp"-->
<!--android:src="?attr/ic_browser"-->
<!--android:id="@+id/btnBrowser"-->
<!--android:contentDescription="@string/browser"-->
<!--/>-->
<!--<ImageButton-->
<!--android:layout_width="48dp"-->
<!--android:layout_height="48dp"-->
<!--android:minWidth="48dp"-->
<!--android:src="?attr/ic_share"-->
<!--android:id="@+id/btnShare"-->
<!--android:contentDescription="@string/share"-->
<!--/>-->
<!--<ImageButton-->
<!--android:layout_width="48dp"-->
<!--android:layout_height="48dp"-->
<!--android:minWidth="48dp"-->
<!--android:src="?attr/ic_copy"-->
<!--android:id="@+id/btnCopy"-->
<!--android:contentDescription="@string/copy"-->
<!--/>-->
</com.google.android.flexbox.FlexboxLayout>
</LinearLayout>

View File

@ -567,7 +567,24 @@
<string name="toot_search_ts_of">Toot search(ts) \"%1$s\"</string>
<string name="tootsearch">tootsearch(JP)</string>
<string name="cant_handle_uri_of">Subway Tooter can\'t handle URI \"%1$s\". Please select other app.</string>
<!--<string name="abc_action_bar_home_description">Revenir à l\'accueil</string>-->
<string name="use_internal_media_viewer">Use built-in media viewer</string>
<string name="previous">Previous</string>
<string name="next">Next</string>
<string name="download">Download</string>
<string name="downloading">Downloading…</string>
<string name="url_is_copied">URL copied to clipboard</string>
<string name="download_complete">Download completed. %1$s</string>
<string name="download_failed">Download failed. %1$s</string>
<string name="open_browser_of">Open \"%1$s\" in browser</string>
<string name="open_in_browser">Open in browser</string>
<string name="share_url">Share URL</string>
<string name="copy_url">Copy URL to clipboard</string>
<string name="media_attachment_empty">Missing media attachments.</string>
<string name="media_attachment_type_error">The type of media attachment is \"%1$s\". ST can\'t handle it, but you can try open in browser.</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

@ -855,4 +855,20 @@
<string name="toot_search_ts_of">トゥート検索(ts) \"%1$s\"</string>
<string name="tootsearch">tootsearch</string>
<string name="cant_handle_uri_of">Subway Tooter は URI \"%1$s\"を扱えません。別のアプリを選んでください。</string>
<string name="use_internal_media_viewer">内蔵メディアビューアを使う</string>
<string name="previous"></string>
<string name="next"></string>
<string name="download">ダウンロード</string>
<string name="downloading">ダウンロード中…</string>
<string name="url_is_copied">URLをクリップボードにコピーしました</string>
<string name="download_complete">ダウンロード完了 %1$s</string>
<string name="download_failed">ダウンロード失敗 %1$s</string>
<string name="open_browser_of">\"%1$s\"をブラウザで開く</string>
<string name="open_in_browser">ブラウザで開く</string>
<string name="share_url">URLを共有</string>
<string name="copy_url">URLをクリップボードにコピー</string>
<string name="media_attachment_empty">Missing media attachments.</string>
<string name="media_attachment_type_error">The type of media attachment is \"%1$s\". ST can\'t handle it, but you can try open in browser.</string>
</resources>

View File

@ -127,4 +127,11 @@
<attr name="ic_list_tl" format="reference" />
<attr name="ic_list_member" format="reference" />
<attr name="ic_download" format="reference" />
<attr name="ic_browser" format="reference" />
<attr name="ic_share" format="reference" />
<attr name="ic_copy" format="reference" />
<attr name="ic_left" format="reference" />
<attr name="ic_right" format="reference" />
</resources>

View File

@ -559,4 +559,18 @@
<string name="media_attachment_max_byte_size">Media attachment maximum bytes size (unit:Mega bytes. default is 8)</string>
<string name="tootsearch">tootsearch(JP)</string>
<string name="cant_handle_uri_of">Subway Tooter can\'t handle URI \"%1$s\". Please select other app.</string>
<string name="use_internal_media_viewer">Use built-in media viewer</string>
<string name="previous">Previous</string>
<string name="next">Next</string>
<string name="download">Download</string>
<string name="downloading">Downloading…</string>
<string name="url_is_copied">URL copied to clipboard</string>
<string name="download_complete">Download completed. %1$s</string>
<string name="download_failed">Download failed. %1$s</string>
<string name="open_browser_of">Open \"%1$s\" in browser</string>
<string name="open_in_browser">Open in browser</string>
<string name="share_url">Share URL</string>
<string name="copy_url">Copy URL to clipboard</string>
<string name="media_attachment_empty">Missing media attachments.</string>
<string name="media_attachment_type_error">The type of media attachment is \"%1$s\". ST can\'t handle it, but you can try open in browser.</string>
</resources>

View File

@ -98,6 +98,14 @@
<item name="ic_list_tl">@drawable/ic_list_tl</item>
<item name="ic_list_member">@drawable/ic_list_member</item>
<item name="ic_download">@drawable/ic_download</item>
<item name="ic_browser">@drawable/ic_browser</item>
<item name="ic_share">@drawable/ic_share</item>
<item name="ic_copy">@drawable/ic_copy</item>
<item name="ic_left">@drawable/ic_left</item>
<item name="ic_right">@drawable/ic_right</item>
</style>
<style name="AppTheme.Light.NoActionBar" parent="AppTheme.Light">
@ -198,6 +206,13 @@
<item name="ic_list_tl">@drawable/ic_list_tl_dark</item>
<item name="ic_list_member">@drawable/ic_list_member_dark</item>
<item name="ic_download">@drawable/ic_download_dark</item>
<item name="ic_browser">@drawable/ic_browser_dark</item>
<item name="ic_share">@drawable/ic_share_dark</item>
<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>
</style>
<style name="AppTheme.Dark.NoActionBar" parent="AppTheme.Dark">