summaryrefslogtreecommitdiff
path: root/org.fox.ttcomics/src/main
diff options
context:
space:
mode:
Diffstat (limited to 'org.fox.ttcomics/src/main')
-rwxr-xr-xorg.fox.ttcomics/src/main/AndroidManifest.xml60
-rw-r--r--org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/ImageViewTouch.java268
-rw-r--r--org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/ImageViewTouchBase.java509
-rw-r--r--org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/easing/Cubic.java20
-rw-r--r--org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/easing/Easing.java10
-rw-r--r--org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/graphics/FastBitmapDrawable.java84
-rw-r--r--org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/graphics/IBitmapDrawable.java14
-rw-r--r--org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/utils/IDisposable.java6
-rw-r--r--org.fox.ttcomics/src/main/java/org/fox/ttcomics2/Application.java18
-rw-r--r--org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ByteArrayImageDownloader.java29
-rw-r--r--org.fox.ttcomics/src/main/java/org/fox/ttcomics2/CbzComicArchive.java92
-rw-r--r--org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ComicArchive.java29
-rw-r--r--org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ComicFragment.java256
-rw-r--r--org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ComicListFragment.java461
-rw-r--r--org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ComicPager.java204
-rw-r--r--org.fox.ttcomics/src/main/java/org/fox/ttcomics2/CommonActivity.java605
-rw-r--r--org.fox.ttcomics/src/main/java/org/fox/ttcomics2/DatabaseHelper.java68
-rw-r--r--org.fox.ttcomics/src/main/java/org/fox/ttcomics2/DirectoryPicker.java169
-rw-r--r--org.fox.ttcomics/src/main/java/org/fox/ttcomics2/MainActivity.java228
-rw-r--r--org.fox.ttcomics/src/main/java/org/fox/ttcomics2/NaturalOrderComparator.java188
-rw-r--r--org.fox.ttcomics/src/main/java/org/fox/ttcomics2/PreferencesActivity.java156
-rw-r--r--org.fox.ttcomics/src/main/java/org/fox/ttcomics2/SyncClient.java161
-rw-r--r--org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ViewComicActivity.java321
-rw-r--r--org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ViewPager.java38
-rw-r--r--org.fox.ttcomics/src/main/res/anim/appear.xml9
-rw-r--r--org.fox.ttcomics/src/main/res/drawable-hdpi/badimage.pngbin0 -> 49285 bytes
-rwxr-xr-xorg.fox.ttcomics/src/main/res/drawable-hdpi/comic_grid_footer.pngbin0 -> 156 bytes
-rw-r--r--org.fox.ttcomics/src/main/res/drawable-hdpi/ic_action_overflow.pngbin0 -> 225 bytes
-rw-r--r--org.fox.ttcomics/src/main/res/drawable-hdpi/ic_launcher.pngbin0 -> 5484 bytes
-rw-r--r--org.fox.ttcomics/src/main/res/drawable-hdpi/ic_refresh_light.pngbin0 -> 3138 bytes
-rw-r--r--org.fox.ttcomics/src/main/res/drawable-hdpi/ic_settings.pngbin0 -> 1540 bytes
-rw-r--r--org.fox.ttcomics/src/main/res/drawable-hdpi/ic_share_light.pngbin0 -> 1606 bytes
-rw-r--r--org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_action_good.pngbin0 -> 585 bytes
-rw-r--r--org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_action_overflow.pngbin0 -> 280 bytes
-rw-r--r--org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_launcher.pngbin0 -> 7914 bytes
-rw-r--r--org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_refresh_light.pngbin0 -> 3219 bytes
-rw-r--r--org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_search_light.pngbin0 -> 2127 bytes
-rw-r--r--org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_settings.pngbin0 -> 1641 bytes
-rw-r--r--org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_share_light.pngbin0 -> 1780 bytes
-rw-r--r--org.fox.ttcomics/src/main/res/drawable-xxhdpi/ic_launcher.pngbin0 -> 12808 bytes
-rw-r--r--org.fox.ttcomics/src/main/res/drawable/s_badimage.svg519
-rw-r--r--org.fox.ttcomics/src/main/res/drawable/s_launcher.svg523
-rwxr-xr-xorg.fox.ttcomics/src/main/res/layout-sw600dp/activity_main.xml31
-rw-r--r--org.fox.ttcomics/src/main/res/layout-sw600dp/activity_view_comic.xml20
-rwxr-xr-xorg.fox.ttcomics/src/main/res/layout/activity_main.xml24
-rw-r--r--org.fox.ttcomics/src/main/res/layout/activity_view_comic.xml13
-rw-r--r--org.fox.ttcomics/src/main/res/layout/chooser_list.xml33
-rwxr-xr-xorg.fox.ttcomics/src/main/res/layout/comics_grid_row.xml67
-rw-r--r--org.fox.ttcomics/src/main/res/layout/dialog_location.xml14
-rw-r--r--org.fox.ttcomics/src/main/res/layout/fragment_comic.xml20
-rw-r--r--org.fox.ttcomics/src/main/res/layout/fragment_comics_list.xml34
-rwxr-xr-xorg.fox.ttcomics/src/main/res/layout/fragment_comics_pager.xml50
-rw-r--r--org.fox.ttcomics/src/main/res/layout/list_item.xml7
-rw-r--r--org.fox.ttcomics/src/main/res/menu/activity_main.xml15
-rw-r--r--org.fox.ttcomics/src/main/res/menu/activity_view_comic.xml27
-rw-r--r--org.fox.ttcomics/src/main/res/menu/comic_archive_context.xml8
-rw-r--r--org.fox.ttcomics/src/main/res/values-large/dimens.xml7
-rw-r--r--org.fox.ttcomics/src/main/res/values-v11/style.xml6
-rw-r--r--org.fox.ttcomics/src/main/res/values-v21/style.xml6
-rw-r--r--org.fox.ttcomics/src/main/res/values/attrs.xml4
-rw-r--r--org.fox.ttcomics/src/main/res/values/dimens.xml7
-rwxr-xr-xorg.fox.ttcomics/src/main/res/values/strings.xml71
-rwxr-xr-xorg.fox.ttcomics/src/main/res/values/style.xml20
-rw-r--r--org.fox.ttcomics/src/main/res/xml/preferences.xml66
64 files changed, 5595 insertions, 0 deletions
diff --git a/org.fox.ttcomics/src/main/AndroidManifest.xml b/org.fox.ttcomics/src/main/AndroidManifest.xml
new file mode 100755
index 0000000..2af4fa6
--- /dev/null
+++ b/org.fox.ttcomics/src/main/AndroidManifest.xml
@@ -0,0 +1,60 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="org.fox.ttcomics2"
+ android:versionCode="58"
+ android:versionName="1.18" >
+
+ <uses-sdk
+ android:minSdkVersion="16"
+ android:targetSdkVersion="21" />
+
+ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="android.permission.GET_ACCOUNTS" />
+ <uses-permission android:name="android.permission.INTERNET" />
+
+ <application
+ android:name="org.fox.ttcomics2.Application"
+ android:icon="@drawable/ic_launcher"
+ android:label="@string/app_name"
+ android:hardwareAccelerated="true"
+ android:largeHeap="true"
+ android:allowBackup="true">
+ <activity
+ android:theme="@style/AppTheme"
+ android:name=".MainActivity"
+ android:label="@string/title_activity_main" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+
+ <activity
+ android:name=".PreferencesActivity"
+ android:label="@string/title_activity_preferences">
+ </activity>
+
+ <activity
+ android:name=".ViewComicActivity"
+ android:label="@string/title_activity_main">
+ </activity>
+
+ <activity
+ android:name=".CommonActivity"
+ android:label="@string/title_activity_main">
+ </activity>
+
+ <activity
+ android:name=".DirectoryPicker"
+ android:label="@string/title_activity_main">
+ </activity>
+
+ <activity android:name="org.acra.CrashReportDialog"
+ android:theme="@style/DarkDialogTheme"
+ android:launchMode="singleInstance"
+ android:excludeFromRecents="true"
+ android:finishOnTaskLaunch="true" />
+
+ </application>
+
+</manifest> \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/ImageViewTouch.java b/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/ImageViewTouch.java
new file mode 100644
index 0000000..73392b1
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/ImageViewTouch.java
@@ -0,0 +1,268 @@
+package it.sephiroth.android.library.imagezoom;
+
+import android.content.Context;
+import android.graphics.Matrix;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.GestureDetector;
+import android.view.GestureDetector.OnGestureListener;
+import android.view.MotionEvent;
+import android.view.ScaleGestureDetector;
+import android.view.ScaleGestureDetector.OnScaleGestureListener;
+import android.view.ViewConfiguration;
+
+public class ImageViewTouch extends ImageViewTouchBase {
+
+ private static final float SCROLL_DELTA_THRESHOLD = 1.0f;
+ static final float MIN_ZOOM = 0.9f;
+ protected ScaleGestureDetector mScaleDetector;
+ protected GestureDetector mGestureDetector;
+ protected int mTouchSlop;
+ protected float mCurrentScaleFactor;
+ protected float mScaleFactor;
+ protected int mDoubleTapDirection;
+ protected OnGestureListener mGestureListener;
+ protected OnScaleGestureListener mScaleListener;
+ protected boolean mDoubleTapToZoomEnabled = true;
+ protected boolean mScaleEnabled = true;
+ protected boolean mScrollEnabled = true;
+
+ private OnImageViewTouchDoubleTapListener doubleTapListener;
+
+ public interface OnScaleChangedListener {
+ public void onScaleChanged(float scale);
+ }
+
+ protected OnScaleChangedListener mScaleChangedListener;
+
+ public ImageViewTouch( Context context, AttributeSet attrs ) {
+ super( context, attrs );
+ }
+
+ @Override
+ protected void init() {
+ super.init();
+ mTouchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
+ mGestureListener = getGestureListener();
+ mScaleListener = getScaleListener();
+
+ mScaleDetector = new ScaleGestureDetector( getContext(), mScaleListener );
+ mGestureDetector = new GestureDetector( getContext(), mGestureListener, null, true );
+
+ mCurrentScaleFactor = 1f;
+ mDoubleTapDirection = 1;
+ }
+
+ public void setDoubleTapListener( OnImageViewTouchDoubleTapListener doubleTapListener ){
+ this.doubleTapListener = doubleTapListener;
+ }
+
+ public void setDoubleTapToZoomEnabled( boolean value ) {
+ mDoubleTapToZoomEnabled = value;
+ }
+
+ public void setScaleEnabled( boolean value ) {
+ mScaleEnabled = value;
+ }
+
+ public void setScrollEnabled( boolean value ) {
+ mScrollEnabled = value;
+ }
+
+ public boolean getDoubleTapEnabled() {
+ return mDoubleTapToZoomEnabled;
+ }
+
+ protected OnGestureListener getGestureListener() {
+ return new GestureListener();
+ }
+
+ protected OnScaleGestureListener getScaleListener() {
+ return new ScaleListener();
+ }
+
+ @Override
+ protected void onBitmapChanged( Drawable drawable ) {
+ super.onBitmapChanged( drawable );
+
+ float v[] = new float[9];
+ mSuppMatrix.getValues( v );
+ mCurrentScaleFactor = v[Matrix.MSCALE_X];
+ }
+
+ @Override
+ protected void _setImageDrawable( final Drawable drawable, final boolean reset, final Matrix initial_matrix, final float maxZoom ) {
+ super._setImageDrawable( drawable, reset, initial_matrix, maxZoom );
+ mScaleFactor = getMaxZoom() / 3;
+ }
+
+ @Override
+ public boolean onTouchEvent( MotionEvent event ) {
+ mScaleDetector.onTouchEvent( event );
+ if ( !mScaleDetector.isInProgress() ) mGestureDetector.onTouchEvent( event );
+ int action = event.getAction();
+ switch ( action & MotionEvent.ACTION_MASK ) {
+ case MotionEvent.ACTION_UP:
+ if ( getScale() < 1f ) {
+ zoomTo( 1f, 50 );
+ }
+ break;
+ }
+ return true;
+ }
+
+ @Override
+ protected void onZoom( float scale ) {
+ super.onZoom( scale );
+ if ( !mScaleDetector.isInProgress() ) mCurrentScaleFactor = scale;
+
+ if (mScaleChangedListener != null) {
+ mScaleChangedListener.onScaleChanged(mCurrentScaleFactor);
+ }
+ }
+
+ protected float onDoubleTapPost( float scale, float maxZoom ) {
+ if ( mDoubleTapDirection == 1 ) {
+ if (mCurrentScaleFactor - 1.0f < 0.01) { //( scale + ( mScaleFactor * 2 ) ) <= maxZoom
+
+ float scaleFactor = mScaleFactor;
+
+ RectF bitmapRect = getBitmapRect();
+
+ float w = bitmapRect.right - bitmapRect.left;
+ float h = bitmapRect.bottom - bitmapRect.top;
+
+ if (w < getWidth()) {
+ scaleFactor = (float)getWidth() / w - scale;
+ } else if (h < getHeight()) {
+ scaleFactor = (float)getHeight() / h - scale;
+ }
+
+ return scale + scaleFactor;
+ } else {
+ mDoubleTapDirection = -1;
+ return maxZoom;
+ }
+ } else {
+ mDoubleTapDirection = 1;
+ return 1f;
+ }
+ }
+
+ /**
+ * Determines whether this ImageViewTouch can be scrolled.
+ * @param direction
+ * - positive direction value means scroll from right to left,
+ * negative value means scroll from left to right
+ *
+ * @return true if there is some more place to scroll, false - otherwise.
+ */
+ public boolean canScroll(int direction) {
+ RectF bitmapRect = getBitmapRect();
+ updateRect(bitmapRect, mScrollRect);
+ Rect imageViewRect = new Rect();
+ getGlobalVisibleRect(imageViewRect);
+
+ if (bitmapRect.right >= imageViewRect.right) {
+ if (direction < 0) {
+ return Math.abs(bitmapRect.right - imageViewRect.right) > SCROLL_DELTA_THRESHOLD;
+ }
+ }
+
+ double bitmapScrollRectDelta = Math.abs(bitmapRect.left - mScrollRect.left);
+ return bitmapScrollRectDelta > SCROLL_DELTA_THRESHOLD;
+ }
+
+ public class GestureListener extends GestureDetector.SimpleOnGestureListener {
+
+ @Override
+ public boolean onDoubleTap( MotionEvent e ) {
+ Log.i( LOG_TAG, "onDoubleTap. double tap enabled? " + mDoubleTapToZoomEnabled);
+ if (mDoubleTapToZoomEnabled) {
+ float scale = getScale();
+ float targetScale = scale;
+ targetScale = onDoubleTapPost( scale, getMaxZoom() );
+ targetScale = Math.min( getMaxZoom(), Math.max( targetScale, MIN_ZOOM ) );
+ mCurrentScaleFactor = targetScale;
+ zoomTo( targetScale, e.getX(), e.getY(), 200 );
+ invalidate();
+ }
+
+ if( null != doubleTapListener ){
+ doubleTapListener.onDoubleTap();
+ }
+
+ return super.onDoubleTap( e );
+ }
+
+ @Override
+ public void onLongPress( MotionEvent e ) {
+ if ( isLongClickable() ) {
+ if ( !mScaleDetector.isInProgress() ) {
+ setPressed( true );
+ performLongClick();
+ }
+ }
+ }
+
+ @Override
+ public boolean onScroll( MotionEvent e1, MotionEvent e2, float distanceX, float distanceY ) {
+ if ( !mScrollEnabled ) return false;
+
+ if ( e1 == null || e2 == null ) return false;
+ if ( e1.getPointerCount() > 1 || e2.getPointerCount() > 1 ) return false;
+ if ( mScaleDetector.isInProgress() ) return false;
+ if ( getScale() == 1f ) return false;
+ scrollBy( -distanceX, -distanceY );
+ invalidate();
+ return super.onScroll( e1, e2, distanceX, distanceY );
+ }
+
+ @Override
+ public boolean onFling( MotionEvent e1, MotionEvent e2, float velocityX, float velocityY ) {
+ if ( !mScrollEnabled ) return false;
+
+ if ( e1.getPointerCount() > 1 || e2.getPointerCount() > 1 ) return false;
+ if ( mScaleDetector.isInProgress() ) return false;
+
+ float diffX = e2.getX() - e1.getX();
+ float diffY = e2.getY() - e1.getY();
+
+ if ( Math.abs( velocityX ) > 800 || Math.abs( velocityY ) > 800 ) {
+ scrollBy( diffX / 2, diffY / 2, 300 );
+ invalidate();
+ }
+ return super.onFling( e1, e2, velocityX, velocityY );
+ }
+ }
+
+ public class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
+
+ @SuppressWarnings("unused")
+ @Override
+ public boolean onScale( ScaleGestureDetector detector ) {
+ float span = detector.getCurrentSpan() - detector.getPreviousSpan();
+ float targetScale = mCurrentScaleFactor * detector.getScaleFactor();
+ if ( mScaleEnabled ) {
+ targetScale = Math.min( getMaxZoom(), Math.max( targetScale, MIN_ZOOM ) );
+ zoomTo( targetScale, detector.getFocusX(), detector.getFocusY() );
+ mCurrentScaleFactor = Math.min( getMaxZoom(), Math.max( targetScale, MIN_ZOOM ) );
+ mDoubleTapDirection = 1;
+ invalidate();
+ return true;
+ }
+ return false;
+ }
+ }
+
+ public interface OnImageViewTouchDoubleTapListener {
+ void onDoubleTap();
+ }
+
+ public void setOnScaleChangedListener(OnScaleChangedListener listener) {
+ mScaleChangedListener = listener;
+ }
+}
diff --git a/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/ImageViewTouchBase.java b/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/ImageViewTouchBase.java
new file mode 100644
index 0000000..8452a21
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/ImageViewTouchBase.java
@@ -0,0 +1,509 @@
+package it.sephiroth.android.library.imagezoom;
+
+import it.sephiroth.android.library.imagezoom.easing.Cubic;
+import it.sephiroth.android.library.imagezoom.easing.Easing;
+import it.sephiroth.android.library.imagezoom.graphics.FastBitmapDrawable;
+import it.sephiroth.android.library.imagezoom.utils.IDisposable;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.widget.ImageView;
+
+/**
+ * Base View to manage image zoom/scrool/pinch operations
+ *
+ * @author alessandro
+ *
+ */
+public class ImageViewTouchBase extends ImageView implements IDisposable {
+
+ public interface OnBitmapChangedListener {
+
+ void onBitmapChanged( Drawable drawable );
+ };
+
+ public static final String LOG_TAG = "image";
+
+ protected Easing mEasing = new Cubic();
+ protected Matrix mBaseMatrix = new Matrix();
+ protected Matrix mSuppMatrix = new Matrix();
+ protected Handler mHandler = new Handler();
+ protected Runnable mOnLayoutRunnable = null;
+ protected float mMaxZoom;
+ protected final Matrix mDisplayMatrix = new Matrix();
+ protected final float[] mMatrixValues = new float[9];
+ protected int mThisWidth = -1, mThisHeight = -1;
+ protected boolean mFitToScreen = false;
+ protected boolean mFitToWidth = false;
+ final protected float MAX_ZOOM = 3.0f;
+
+ protected RectF mBitmapRect = new RectF();
+ protected RectF mCenterRect = new RectF();
+ protected RectF mScrollRect = new RectF();
+
+ private OnBitmapChangedListener mListener;
+
+ public ImageViewTouchBase( Context context ) {
+ super( context );
+ init();
+ }
+
+ public ImageViewTouchBase( Context context, AttributeSet attrs ) {
+ super( context, attrs );
+ init();
+ }
+
+ public void setOnBitmapChangedListener( OnBitmapChangedListener listener ) {
+ mListener = listener;
+ }
+
+ protected void init() {
+ setScaleType( ImageView.ScaleType.MATRIX );
+ }
+
+ public void clear() {
+ setImageBitmap( null, true );
+ }
+
+ public void setFitToScreen( boolean value ) {
+ if ( value != mFitToScreen ) {
+ mFitToScreen = value;
+ requestLayout();
+ }
+ }
+
+ public void setFitToWidth( boolean value ) {
+ if ( value != mFitToWidth ) {
+ mFitToWidth = value;
+ requestLayout();
+ }
+ }
+
+ @Override
+ protected void onLayout( boolean changed, int left, int top, int right, int bottom ) {
+ super.onLayout( changed, left, top, right, bottom );
+ mThisWidth = right - left;
+ mThisHeight = bottom - top;
+ Runnable r = mOnLayoutRunnable;
+ if ( r != null ) {
+ mOnLayoutRunnable = null;
+ r.run();
+ }
+ if ( getDrawable() != null ) {
+ if ( mFitToScreen )
+ getProperBaseMatrix2( getDrawable(), mBaseMatrix );
+ else
+ getProperBaseMatrix( getDrawable(), mBaseMatrix );
+ setImageMatrix( getImageViewMatrix() );
+
+ if (mFitToWidth) zoomToWidth();
+ }
+ }
+
+ @Override
+ public void setImageBitmap( Bitmap bm ) {
+ setImageBitmap( bm, true );
+ }
+
+ @Override
+ public void setImageResource( int resId ) {
+ setImageDrawable( getContext().getResources().getDrawable( resId ) );
+ }
+
+ /**
+ * Set the new image to display and reset the internal matrix.
+ *
+ * @param bitmap
+ * - the {@link Bitmap} to display
+ * @param reset
+ * - if true the image bounds will be recreated, otherwise the old {@link Matrix} is used to display the new bitmap
+ * @see #setImageBitmap(Bitmap)
+ */
+ public void setImageBitmap( final Bitmap bitmap, final boolean reset ) {
+ setImageBitmap( bitmap, reset, null );
+ }
+
+ /**
+ * Similar to {@link #setImageBitmap(Bitmap, boolean)} but an optional view {@link Matrix} can be passed to determine the new
+ * bitmap view matrix.<br />
+ * This method is useful if you need to restore a Bitmap with the same zoom/pan values from a previous state
+ *
+ * @param bitmap
+ * - the {@link Bitmap} to display
+ * @param reset
+ * @param matrix
+ * - the {@link Matrix} to be used to display the new bitmap
+ *
+ * @see #setImageBitmap(Bitmap, boolean)
+ * @see #setImageBitmap(Bitmap)
+ * @see #getImageViewMatrix()
+ * @see #getDisplayMatrix()
+ */
+ public void setImageBitmap( final Bitmap bitmap, final boolean reset, Matrix matrix ) {
+ setImageBitmap( bitmap, reset, matrix, -1 );
+ }
+
+ /**
+ *
+ * @param bitmap
+ * @param reset
+ * @param matrix
+ * @param maxZoom
+ * - maximum zoom allowd during zoom gestures
+ *
+ * @see #setImageBitmap(Bitmap, boolean, Matrix)
+ */
+ public void setImageBitmap( final Bitmap bitmap, final boolean reset, Matrix matrix, float maxZoom ) {
+
+ Log.i( LOG_TAG, "setImageBitmap: " + bitmap );
+
+ if ( bitmap != null )
+ setImageDrawable( new FastBitmapDrawable( bitmap ), reset, matrix, maxZoom );
+ else
+ setImageDrawable( null, reset, matrix, maxZoom );
+ }
+
+ @Override
+ public void setImageDrawable( Drawable drawable ) {
+ setImageDrawable( drawable, true, null, -1 );
+ }
+
+ public void setImageDrawable( final Drawable drawable, final boolean reset, final Matrix initial_matrix, final float maxZoom ) {
+
+ final int viewWidth = getWidth();
+
+ if ( viewWidth <= 0 ) {
+ mOnLayoutRunnable = new Runnable() {
+
+ @Override
+ public void run() {
+ setImageDrawable( drawable, reset, initial_matrix, maxZoom );
+ }
+ };
+ return;
+ }
+
+ _setImageDrawable( drawable, reset, initial_matrix, maxZoom );
+ }
+
+ protected void _setImageDrawable( final Drawable drawable, final boolean reset, final Matrix initial_matrix, final float maxZoom ) {
+
+ if ( drawable != null ) {
+ if ( mFitToScreen )
+ getProperBaseMatrix2( drawable, mBaseMatrix );
+ else
+ getProperBaseMatrix( drawable, mBaseMatrix );
+ super.setImageDrawable( drawable );
+
+ if (mFitToWidth) zoomToWidth();
+
+ } else {
+ mBaseMatrix.reset();
+ super.setImageDrawable( null );
+ }
+
+ if ( reset ) {
+ mSuppMatrix.reset();
+ if ( initial_matrix != null ) {
+ mSuppMatrix = new Matrix( initial_matrix );
+ }
+ }
+
+ setImageMatrix( getImageViewMatrix() );
+
+ if ( maxZoom < 1 )
+ mMaxZoom = maxZoom();
+ else
+ mMaxZoom = maxZoom;
+
+ onBitmapChanged( drawable );
+ }
+
+ protected void onBitmapChanged( final Drawable bitmap ) {
+ if ( mListener != null ) {
+ mListener.onBitmapChanged( bitmap );
+ }
+ }
+
+ protected float maxZoom() {
+ final Drawable drawable = getDrawable();
+
+ if ( drawable == null ) {
+ return 1F;
+ }
+
+ float fw = (float) drawable.getIntrinsicWidth() / (float) mThisWidth;
+ float fh = (float) drawable.getIntrinsicHeight() / (float) mThisHeight;
+ float max = Math.max( fw, fh ) * 4;
+ return max;
+ }
+
+ public float getMaxZoom() {
+ return mMaxZoom;
+ }
+
+ public Matrix getImageViewMatrix() {
+ mDisplayMatrix.set( mBaseMatrix );
+ mDisplayMatrix.postConcat( mSuppMatrix );
+ return mDisplayMatrix;
+ }
+
+ /**
+ * Returns the current image display matrix. This matrix can be used in the next call to the
+ * {@link #setImageBitmap(Bitmap, boolean, Matrix)} to restore the same view state of the previous {@link Bitmap}
+ *
+ * @return
+ */
+ public Matrix getDisplayMatrix() {
+ return new Matrix( mSuppMatrix );
+ }
+
+ /**
+ * Setup the base matrix so that the image is centered and scaled properly.
+ *
+ * @param bitmap
+ * @param matrix
+ */
+ protected void getProperBaseMatrix( Drawable drawable, Matrix matrix ) {
+ float viewWidth = getWidth();
+ float viewHeight = getHeight();
+ float w = drawable.getIntrinsicWidth();
+ float h = drawable.getIntrinsicHeight();
+ matrix.reset();
+
+ if ( w > viewWidth || h > viewHeight ) {
+ float widthScale = Math.min( viewWidth / w, 2.0f );
+ float heightScale = Math.min( viewHeight / h, 2.0f );
+ float scale = Math.min( widthScale, heightScale );
+ matrix.postScale( scale, scale );
+ float tw = ( viewWidth - w * scale ) / 2.0f;
+ float th = ( viewHeight - h * scale ) / 2.0f;
+ matrix.postTranslate( tw, th );
+ } else {
+ float tw = ( viewWidth - w ) / 2.0f;
+ float th = ( viewHeight - h ) / 2.0f;
+ matrix.postTranslate( tw, th );
+ }
+ }
+
+ /**
+ * Setup the base matrix so that the image is centered and scaled properly.
+ *
+ * @param bitmap
+ * @param matrix
+ */
+ protected void getProperBaseMatrix2( Drawable bitmap, Matrix matrix ) {
+ float viewWidth = getWidth();
+ float viewHeight = getHeight();
+ float w = bitmap.getIntrinsicWidth();
+ float h = bitmap.getIntrinsicHeight();
+ matrix.reset();
+ float widthScale = Math.min( viewWidth / w, MAX_ZOOM );
+ float heightScale = Math.min( viewHeight / h, MAX_ZOOM );
+ float scale = Math.min( widthScale, heightScale );
+ matrix.postScale( scale, scale );
+ matrix.postTranslate( ( viewWidth - w * scale ) / 2.0f, ( viewHeight - h * scale ) / 2.0f );
+ }
+
+ protected float getValue( Matrix matrix, int whichValue ) {
+ matrix.getValues( mMatrixValues );
+ return mMatrixValues[whichValue];
+ }
+
+ protected RectF getBitmapRect() {
+ final Drawable drawable = getDrawable();
+
+ if ( drawable == null ) return null;
+ Matrix m = getImageViewMatrix();
+ mBitmapRect.set( 0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight() );
+ m.mapRect( mBitmapRect );
+ return mBitmapRect;
+ }
+
+ protected float getScale( Matrix matrix ) {
+ return getValue( matrix, Matrix.MSCALE_X );
+ }
+
+ public float getRotation() {
+ return 0;
+ }
+
+ public float getScale() {
+ return getScale( mSuppMatrix );
+ }
+
+ protected void center( boolean horizontal, boolean vertical ) {
+ // Log.i(LOG_TAG, "center");
+ final Drawable drawable = getDrawable();
+
+ if ( drawable == null ) return;
+ RectF rect = getCenter( horizontal, vertical );
+ if ( rect.left != 0 || rect.top != 0 ) {
+ postTranslate( rect.left, rect.top );
+ }
+ }
+
+ protected RectF getCenter( boolean horizontal, boolean vertical ) {
+ final Drawable drawable = getDrawable();
+
+ if ( drawable == null ) return new RectF( 0, 0, 0, 0 );
+
+ RectF rect = getBitmapRect();
+ float height = rect.height();
+ float width = rect.width();
+ float deltaX = 0, deltaY = 0;
+ if ( vertical ) {
+ int viewHeight = getHeight();
+ if ( height < viewHeight ) {
+ deltaY = ( viewHeight - height ) / 2 - rect.top;
+ } else if ( rect.top > 0 ) {
+ deltaY = -rect.top;
+ } else if ( rect.bottom < viewHeight ) {
+ deltaY = getHeight() - rect.bottom;
+ }
+ }
+ if ( horizontal ) {
+ int viewWidth = getWidth();
+ if ( width < viewWidth ) {
+ deltaX = ( viewWidth - width ) / 2 - rect.left;
+ } else if ( rect.left > 0 ) {
+ deltaX = -rect.left;
+ } else if ( rect.right < viewWidth ) {
+ deltaX = viewWidth - rect.right;
+ }
+ }
+ mCenterRect.set( deltaX, deltaY, 0, 0 );
+ return mCenterRect;
+ }
+
+ protected void postTranslate( float deltaX, float deltaY ) {
+ mSuppMatrix.postTranslate( deltaX, deltaY );
+ setImageMatrix( getImageViewMatrix() );
+ }
+
+ protected void postScale( float scale, float centerX, float centerY ) {
+ mSuppMatrix.postScale( scale, scale, centerX, centerY );
+ setImageMatrix( getImageViewMatrix() );
+ }
+
+ protected void zoomTo( float scale ) {
+ float cx = getWidth() / 2F;
+ float cy = getHeight() / 2F;
+ zoomTo( scale, cx, cy );
+ }
+
+ public void zoomTo( float scale, float durationMs ) {
+ float cx = getWidth() / 2F;
+ float cy = getHeight() / 2F;
+ zoomTo( scale, cx, cy, durationMs );
+ }
+
+ protected void zoomTo( float scale, float centerX, float centerY ) {
+ if ( scale > mMaxZoom ) scale = mMaxZoom;
+ float oldScale = getScale();
+ float deltaScale = scale / oldScale;
+ postScale( deltaScale, centerX, centerY );
+ onZoom( getScale() );
+ center( true, true );
+ }
+
+ protected void onZoom( float scale ) {}
+
+ public void scrollBy( float x, float y ) {
+ panBy( x, y );
+ }
+
+ protected void panBy( double dx, double dy ) {
+ RectF rect = getBitmapRect();
+ mScrollRect.set( (float) dx, (float) dy, 0, 0 );
+ updateRect( rect, mScrollRect );
+ postTranslate( mScrollRect.left, mScrollRect.top );
+ center( true, true );
+ }
+
+ protected void updateRect( RectF bitmapRect, RectF scrollRect ) {
+ float width = getWidth();
+ float height = getHeight();
+
+ if ( bitmapRect.top >= 0 && bitmapRect.bottom <= height ) scrollRect.top = 0;
+ if ( bitmapRect.left >= 0 && bitmapRect.right <= width ) scrollRect.left = 0;
+ if ( bitmapRect.top + scrollRect.top >= 0 && bitmapRect.bottom > height ) scrollRect.top = (int) ( 0 - bitmapRect.top );
+ if ( bitmapRect.bottom + scrollRect.top <= ( height - 0 ) && bitmapRect.top < 0 )
+ scrollRect.top = (int) ( ( height - 0 ) - bitmapRect.bottom );
+ if ( bitmapRect.left + scrollRect.left >= 0 ) scrollRect.left = (int) ( 0 - bitmapRect.left );
+ if ( bitmapRect.right + scrollRect.left <= ( width - 0 ) ) scrollRect.left = (int) ( ( width - 0 ) - bitmapRect.right );
+ // Log.d( LOG_TAG, "scrollRect(2): " + scrollRect.toString() );
+ }
+
+ protected void scrollBy( float distanceX, float distanceY, final double durationMs ) {
+ final double dx = distanceX;
+ final double dy = distanceY;
+ final long startTime = System.currentTimeMillis();
+ mHandler.post( new Runnable() {
+
+ double old_x = 0;
+ double old_y = 0;
+
+ @Override
+ public void run() {
+ long now = System.currentTimeMillis();
+ double currentMs = Math.min( durationMs, now - startTime );
+ double x = mEasing.easeOut( currentMs, 0, dx, durationMs );
+ double y = mEasing.easeOut( currentMs, 0, dy, durationMs );
+ panBy( ( x - old_x ), ( y - old_y ) );
+ old_x = x;
+ old_y = y;
+ if ( currentMs < durationMs ) {
+ mHandler.post( this );
+ } else {
+ RectF centerRect = getCenter( true, true );
+ if ( centerRect.left != 0 || centerRect.top != 0 ) scrollBy( centerRect.left, centerRect.top );
+ }
+ }
+ } );
+ }
+
+ protected void zoomTo( float scale, final float centerX, final float centerY, final float durationMs ) {
+ // Log.i( LOG_TAG, "zoomTo: " + scale + ", " + centerX + ": " + centerY );
+ final long startTime = System.currentTimeMillis();
+ final float incrementPerMs = ( scale - getScale() ) / durationMs;
+ final float oldScale = getScale();
+ mHandler.post( new Runnable() {
+
+ @Override
+ public void run() {
+ long now = System.currentTimeMillis();
+ float currentMs = Math.min( durationMs, now - startTime );
+ float target = oldScale + ( incrementPerMs * currentMs );
+ zoomTo( target, centerX, centerY );
+ if ( currentMs < durationMs ) {
+ mHandler.post( this );
+ } else {
+ // if ( getScale() < 1f ) {}
+ }
+ }
+ } );
+ }
+
+ @Override
+ public void dispose() {
+ clear();
+ }
+
+ protected void zoomToWidth() {
+ RectF bitmapRect = getBitmapRect();
+
+ float w = bitmapRect.right - bitmapRect.left;
+
+ if (w < getWidth()) {
+ float scale = (float)getWidth() / w;
+
+ zoomTo(scale, 0f, 0f);
+ }
+ }
+}
diff --git a/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/easing/Cubic.java b/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/easing/Cubic.java
new file mode 100644
index 0000000..6f7e87d
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/easing/Cubic.java
@@ -0,0 +1,20 @@
+package it.sephiroth.android.library.imagezoom.easing;
+
+public class Cubic implements Easing {
+
+ @Override
+ public double easeOut( double time, double start, double end, double duration ) {
+ return end * ( ( time = time / duration - 1.0 ) * time * time + 1.0 ) + start;
+ }
+
+ @Override
+ public double easeIn( double time, double start, double end, double duration ) {
+ return end * ( time /= duration ) * time * time + start;
+ }
+
+ @Override
+ public double easeInOut( double time, double start, double end, double duration ) {
+ if ( ( time /= duration / 2.0 ) < 1.0 ) return end / 2.0 * time * time * time + start;
+ return end / 2.0 * ( ( time -= 2.0 ) * time * time + 2.0 ) + start;
+ }
+}
diff --git a/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/easing/Easing.java b/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/easing/Easing.java
new file mode 100644
index 0000000..202e9d9
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/easing/Easing.java
@@ -0,0 +1,10 @@
+package it.sephiroth.android.library.imagezoom.easing;
+
+public interface Easing {
+
+ double easeOut( double time, double start, double end, double duration );
+
+ double easeIn( double time, double start, double end, double duration );
+
+ double easeInOut( double time, double start, double end, double duration );
+}
diff --git a/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/graphics/FastBitmapDrawable.java b/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/graphics/FastBitmapDrawable.java
new file mode 100644
index 0000000..8afc38e
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/graphics/FastBitmapDrawable.java
@@ -0,0 +1,84 @@
+package it.sephiroth.android.library.imagezoom.graphics;
+
+import java.io.InputStream;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.drawable.Drawable;
+
+/**
+ * Fast bitmap drawable. Does not support states. it only
+ * support alpha and colormatrix
+ * @author alessandro
+ *
+ */
+public class FastBitmapDrawable extends Drawable implements IBitmapDrawable {
+
+ protected Bitmap mBitmap;
+ protected Paint mPaint;
+
+ public FastBitmapDrawable( Bitmap b ) {
+ mBitmap = b;
+ mPaint = new Paint();
+ mPaint.setDither( true );
+ mPaint.setFilterBitmap( true );
+ }
+
+ public FastBitmapDrawable( Resources res, InputStream is ){
+ this(BitmapFactory.decodeStream(is));
+ }
+
+ @Override
+ public void draw( Canvas canvas ) {
+ canvas.drawBitmap( mBitmap, 0.0f, 0.0f, mPaint );
+ }
+
+ @Override
+ public int getOpacity() {
+ return PixelFormat.TRANSLUCENT;
+ }
+
+ @Override
+ public void setAlpha( int alpha ) {
+ mPaint.setAlpha( alpha );
+ }
+
+ @Override
+ public void setColorFilter( ColorFilter cf ) {
+ mPaint.setColorFilter( cf );
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return mBitmap.getWidth();
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return mBitmap.getHeight();
+ }
+
+ @Override
+ public int getMinimumWidth() {
+ return mBitmap.getWidth();
+ }
+
+ @Override
+ public int getMinimumHeight() {
+ return mBitmap.getHeight();
+ }
+
+ public void setAntiAlias( boolean value ){
+ mPaint.setAntiAlias( value );
+ invalidateSelf();
+ }
+
+ @Override
+ public Bitmap getBitmap() {
+ return mBitmap;
+ }
+} \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/graphics/IBitmapDrawable.java b/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/graphics/IBitmapDrawable.java
new file mode 100644
index 0000000..5a2892a
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/graphics/IBitmapDrawable.java
@@ -0,0 +1,14 @@
+package it.sephiroth.android.library.imagezoom.graphics;
+
+import it.sephiroth.android.library.imagezoom.ImageViewTouchBase;
+import android.graphics.Bitmap;
+
+/**
+ * Base interface used in the {@link ImageViewTouchBase} view
+ * @author alessandro
+ *
+ */
+public interface IBitmapDrawable {
+
+ Bitmap getBitmap();
+}
diff --git a/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/utils/IDisposable.java b/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/utils/IDisposable.java
new file mode 100644
index 0000000..da991a7
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/it/sephiroth/android/library/imagezoom/utils/IDisposable.java
@@ -0,0 +1,6 @@
+package it.sephiroth.android.library.imagezoom.utils;
+
+public interface IDisposable {
+
+ void dispose();
+}
diff --git a/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/Application.java b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/Application.java
new file mode 100644
index 0000000..8cf7c7a
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/Application.java
@@ -0,0 +1,18 @@
+package org.fox.ttcomics2;
+
+import org.acra.ACRA;
+import org.acra.ReportingInteractionMode;
+import org.acra.annotation.ReportsCrashes;
+
+@ReportsCrashes(formKey = "", mode = ReportingInteractionMode.DIALOG,
+ excludeMatchingSharedPreferencesKeys = {"password"},
+ resDialogText = R.string.crash_dialog_text,
+ formUri = "http://tt-rss.org/acra/submit/")
+public class Application extends android.app.Application {
+ @Override
+ public final void onCreate() {
+ super.onCreate();
+ ACRA.init(this);
+ }
+
+}
diff --git a/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ByteArrayImageDownloader.java b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ByteArrayImageDownloader.java
new file mode 100644
index 0000000..9b7f920
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ByteArrayImageDownloader.java
@@ -0,0 +1,29 @@
+package org.fox.ttcomics2;
+
+import android.content.Context;
+
+import com.nostra13.universalimageloader.core.download.BaseImageDownloader;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class ByteArrayImageDownloader extends BaseImageDownloader {
+ private static final String SCHEME_STREAM = "stream";
+ private static final String STREAM_URI_PREFIX = SCHEME_STREAM + "://";
+
+ public ByteArrayImageDownloader(Context context) {
+ super(context);
+ }
+
+ @Override
+ protected InputStream getStreamFromOtherSource(String imageUri, Object extra) throws IOException {
+ if (imageUri.startsWith(STREAM_URI_PREFIX)) {
+ InputStream is = new ByteArrayInputStream((byte[])extra);
+
+ return is;
+ } else {
+ return super.getStreamFromOtherSource(imageUri, extra);
+ }
+ }
+}
diff --git a/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/CbzComicArchive.java b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/CbzComicArchive.java
new file mode 100644
index 0000000..e533ff3
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/CbzComicArchive.java
@@ -0,0 +1,92 @@
+package org.fox.ttcomics2;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+public class CbzComicArchive extends ComicArchive {
+ private final String TAG = this.getClass().getSimpleName();
+
+ private String m_fileName;
+ private ZipFile m_zipFile;
+ private int m_count;
+ private ArrayList<ZipEntry> m_entries = new ArrayList<ZipEntry>();
+
+ @Override
+ public int getCount() {
+ return m_count;
+ }
+
+ @Override
+ public InputStream getItem(int index) throws IOException {
+ return m_zipFile.getInputStream(m_entries.get(index));
+ }
+
+ protected void initialize() throws IOException {
+ m_zipFile = new ZipFile(m_fileName);
+
+ Enumeration<? extends ZipEntry> e = m_zipFile.entries();
+
+ while (e.hasMoreElements()) {
+ ZipEntry ze = e.nextElement();
+ if (!ze.isDirectory() && isValidComic(ze.getName())) {
+ m_entries.add(ze);
+ m_count++;
+ }
+ }
+
+ Collections.sort(m_entries, new NaturalOrderComparator());
+ }
+
+ public CbzComicArchive(String fileName) throws IOException {
+ m_fileName = fileName;
+
+ initialize();
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel out, int flags) {
+ out.writeString(m_fileName);
+ }
+
+ public void readFromParcel(Parcel in) {
+ m_fileName = in.readString();
+ }
+
+ public CbzComicArchive(Parcel in) throws IOException {
+ readFromParcel(in);
+
+ initialize();
+ }
+
+ @SuppressWarnings("rawtypes")
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator() {
+ public CbzComicArchive createFromParcel(Parcel in) {
+ try {
+ return new CbzComicArchive(in);
+ } catch (IOException e) {
+ e.printStackTrace();
+
+ return null;
+ }
+ }
+
+ public CbzComicArchive[] newArray(int size) {
+ return new CbzComicArchive[size];
+ }
+ };
+
+}
diff --git a/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ComicArchive.java b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ComicArchive.java
new file mode 100644
index 0000000..f85263c
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ComicArchive.java
@@ -0,0 +1,29 @@
+package org.fox.ttcomics2;
+
+import android.os.Parcelable;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public abstract class ComicArchive implements Parcelable {
+ public abstract int getCount();
+ public abstract InputStream getItem(int index) throws IOException;
+ public boolean isValidComic(String fileName) {
+ return fileName.toLowerCase().matches(".*\\.(jpe?g|bmp|gif|png)$");
+ }
+ public byte[] getByteArray(int m_page) throws IOException {
+ InputStream is = getItem(m_page);
+
+ int size = 16384;
+ byte[] buf = new byte[size];
+ int len = 0;
+
+ ByteArrayOutputStream bos = new ByteArrayOutputStream();
+ buf = new byte[size];
+ while ((len = is.read(buf, 0, size)) != -1)
+ bos.write(buf, 0, len);
+
+ return bos.toByteArray();
+ }
+}
diff --git a/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ComicFragment.java b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ComicFragment.java
new file mode 100644
index 0000000..64a88a5
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ComicFragment.java
@@ -0,0 +1,256 @@
+package org.fox.ttcomics2;
+
+
+import android.app.Activity;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.v4.app.Fragment;
+import android.support.v7.app.ActionBar;
+import android.view.GestureDetector;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.nostra13.universalimageloader.core.DisplayImageOptions;
+import com.nostra13.universalimageloader.core.ImageLoader;
+
+import java.io.IOException;
+
+import it.sephiroth.android.library.imagezoom.ImageViewTouch;
+
+public class ComicFragment extends Fragment implements GestureDetector.OnDoubleTapListener {
+ private final String TAG = this.getClass().getSimpleName();
+
+ private SharedPreferences m_prefs;
+ private int m_page;
+ private ViewComicActivity m_activity;
+ private GestureDetector m_detector;
+ private boolean m_thumbnail = false;
+ private ComicArchive m_archive;
+
+ public ComicFragment() {
+ super();
+ }
+
+ public void initialize(ComicArchive archive, int page) {
+ m_archive = archive;
+ m_page = page;
+ }
+
+ public void setPage(int page) {
+ m_page = page;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ View view = inflater.inflate(R.layout.fragment_comic, container, false);
+
+ final ImageViewTouch image = (ImageViewTouch) view.findViewById(R.id.comic_image);
+
+ if (savedInstanceState != null) {
+ m_page = savedInstanceState.getInt("page");
+ m_thumbnail = savedInstanceState.getBoolean("thumbnail");
+ m_archive = savedInstanceState.getParcelable("archive");
+ }
+
+ image.setFitToScreen(true);
+
+ ImageLoader imageLoader = ImageLoader.getInstance();
+
+ if (m_prefs.getBoolean("fit_to_width", false)) {
+ image.setFitToWidth(true);
+ }
+
+ try {
+ byte[] buf = m_archive.getByteArray(m_page);
+
+ DisplayImageOptions options = new DisplayImageOptions.Builder()
+ .showImageOnFail(R.drawable.badimage)
+ .extraForDownloader(buf)
+ .build();
+
+ imageLoader.displayImage("stream://" + m_page, image, options);
+
+ } catch (IOException e) {
+ image.setImageResource(R.drawable.badimage);
+ e.printStackTrace();
+ }
+
+ image.setOnScaleChangedListener(new ImageViewTouch.OnScaleChangedListener() {
+ @Override
+ public void onScaleChanged(float scale) {
+ // TODO: shared scale change?
+ }
+ });
+
+ image.setOnTouchListener(new View.OnTouchListener() {
+ @Override
+ public boolean onTouch(View view, MotionEvent event) {
+ return m_detector.onTouchEvent(event);
+ }
+ });
+
+ //}
+
+ /* TextView page = (TextView) view.findViewById(R.id.comic_page);
+
+ if (page != null) {
+ page.setText(String.valueOf(m_page+1));
+ } */
+
+ return view;
+
+ }
+
+ private void onLeftSideTapped() {
+ ImageViewTouch image = (ImageViewTouch) getView().findViewById(R.id.comic_image);
+
+ if (image != null) {
+ boolean atLeftEdge = !image.canScroll(1);
+
+ if (atLeftEdge) {
+ m_activity.selectPreviousComic();
+ }
+ }
+ }
+
+ public boolean canScroll(int direction) {
+ ImageViewTouch image = (ImageViewTouch) getView().findViewById(R.id.comic_image);
+
+ if (image != null) {
+ return image.canScroll(direction);
+ } else {
+ return false;
+ }
+ }
+
+ private void onRightSideTapped() {
+ ImageViewTouch image = (ImageViewTouch) getView().findViewById(R.id.comic_image);
+
+ if (image != null) {
+ boolean atRightEdge = !image.canScroll(-1);
+
+ if (atRightEdge) {
+ m_activity.selectNextComic();
+ }
+ }
+ }
+
+ @Override
+ public void setUserVisibleHint(boolean isVisibleToUser) {
+ super.setUserVisibleHint(isVisibleToUser);
+
+ //setThumbnail(!isVisibleToUser); disabled
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ m_prefs = PreferenceManager.getDefaultSharedPreferences(activity.getApplicationContext());
+ m_activity = (ViewComicActivity) activity;
+
+ m_detector = new GestureDetector(m_activity, new GestureDetector.OnGestureListener() {
+
+ @Override
+ public boolean onSingleTapUp(MotionEvent e) {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public void onShowPress(MotionEvent e) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX,
+ float distanceY) {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public void onLongPress(MotionEvent e) {
+ // TODO Auto-generated method stub
+
+ }
+
+ @Override
+ public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX,
+ float velocityY) {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public boolean onDown(MotionEvent e) {
+ // TODO Auto-generated method stub
+ return false;
+ }
+ });
+
+ m_detector.setOnDoubleTapListener(this);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle out) {
+ super.onSaveInstanceState(out);
+ out.putInt("page", m_page);
+ out.putBoolean("thumbnail", m_thumbnail);
+ out.putParcelable("archive", m_archive);
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public boolean onDoubleTapEvent(MotionEvent e) {
+ // TODO Auto-generated method stub
+ return false;
+ }
+
+ @Override
+ public boolean onSingleTapConfirmed(MotionEvent e) {
+
+ int width = getView().getWidth();
+
+ int x = Math.round(e.getX());
+
+ if (x <= width/10) {
+ onLeftSideTapped();
+ } else if (x >= width-(width/10)) {
+ onRightSideTapped();
+ } else {
+ ActionBar bar = m_activity.getSupportActionBar();
+
+ if (bar.isShowing()) {
+ bar.hide();
+ m_activity.hideSeekBar(true);
+
+ if (m_prefs.getBoolean("use_full_screen", false)) {
+ m_activity.hideSystemUI(true);
+ }
+
+ } else {
+ m_activity.hideSeekBar(false);
+
+ if (m_prefs.getBoolean("use_full_screen", false)) {
+ m_activity.hideSystemUI(false);
+ }
+
+ bar.show();
+ }
+ }
+
+ return false;
+ }
+
+}
diff --git a/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ComicListFragment.java b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ComicListFragment.java
new file mode 100644
index 0000000..9b766f7
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ComicListFragment.java
@@ -0,0 +1,461 @@
+package org.fox.ttcomics2;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.provider.BaseColumns;
+import android.support.v4.app.Fragment;
+import android.support.v4.widget.SimpleCursorAdapter;
+import android.support.v4.widget.SwipeRefreshLayout;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.LayoutInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.AdapterView.AdapterContextMenuInfo;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.GridView;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import com.nostra13.universalimageloader.core.ImageLoader;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class ComicListFragment extends Fragment implements OnItemClickListener {
+ private final String TAG = this.getClass().getSimpleName();
+
+ private final static int SIZE_DIR = -100;
+
+ // corresponds to tab indexes
+ private final static int MODE_ALL = 0;
+ private final static int MODE_UNREAD = 1;
+ private final static int MODE_UNFINISHED = 2;
+ private final static int MODE_READ = 3;
+
+ private CommonActivity m_activity;
+ private SharedPreferences m_prefs;
+ private ComicsListAdapter m_adapter;
+ private int m_mode = 0;
+ private String m_baseDirectory = "";
+ private SwipeRefreshLayout m_swipeLayout;
+
+ public ComicListFragment() {
+ super();
+ }
+
+ public void setBaseDirectory(String baseDirectory) {
+ m_baseDirectory = baseDirectory;
+ }
+
+ public void setMode(int mode) {
+ m_mode = mode;
+ }
+
+ private class ComicsListAdapter extends SimpleCursorAdapter {
+ public ComicsListAdapter(Context context, int layout, Cursor c,
+ String[] from, int[] to, int flags) {
+ super(context, layout, c, from, to, flags);
+ // TODO Auto-generated constructor stub
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ View v = convertView;
+
+ Cursor c = (Cursor) getItem(position);
+
+ String filePath = c.getString(c.getColumnIndex("path"));
+ String fileBaseName = c.getString(c.getColumnIndex("filename"));
+ String firstChild = c.getString(c.getColumnIndex("firstchild"));
+
+ int lastPos = m_activity.getLastPosition(filePath + "/" + fileBaseName);
+ int size = m_activity.getSize(filePath + "/" + fileBaseName);
+
+ if (v == null) {
+ LayoutInflater vi = (LayoutInflater)getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ v = vi.inflate(R.layout.comics_grid_row, null);
+ }
+
+ TextView name = (TextView) v.findViewById(R.id.file_name);
+
+ if (name != null) {
+ name.setText(fileBaseName);
+ }
+
+ TextView info = (TextView) v.findViewById(R.id.file_progress_info);
+
+ if (info != null) {
+ if (size != -1 && size != SIZE_DIR) {
+ if (lastPos == size - 1) {
+ info.setText(getString(R.string.file_finished));
+ } else if (lastPos > 0) {
+ info.setText(getString(R.string.file_progress_info, lastPos + 1, size, (int) (lastPos / (float) size * 100f)));
+ } else {
+ info.setText(getString(R.string.file_unread, size));
+ }
+ info.setVisibility(View.VISIBLE);
+ } else if (size == SIZE_DIR) {
+ info.setText(getString(R.string.list_type_directory));
+ info.setVisibility(View.VISIBLE);
+ } else {
+ info.setText(getString(R.string.list_type_unknown));
+ info.setVisibility(View.VISIBLE);
+ }
+ }
+
+ ProgressBar progressBar = (ProgressBar) v.findViewById(R.id.file_progress_bar);
+
+ if (progressBar != null) {
+ if (size != -1 && size != SIZE_DIR) {
+ progressBar.setMax(size - 1);
+ progressBar.setProgress(lastPos);
+ progressBar.setEnabled(true);
+ progressBar.setVisibility(View.VISIBLE);
+ } else {
+ progressBar.setProgress(0);
+ progressBar.setMax(0);
+ progressBar.setEnabled(false);
+ progressBar.setVisibility(View.VISIBLE);
+ }
+ }
+
+ /* ImageView overflow = (ImageView) v.findViewById(R.id.comic_overflow);
+
+ if (overflow != null) {
+ if (size == SIZE_DIR) {
+ overflow.setVisibility(View.GONE);
+ } else {
+ overflow.setOnClickListener(new View.OnClickListener() {
+
+ @Override
+ public void onClick(View v) {
+ getActivity().openContextMenu(v);
+ }
+ });
+ }
+ } */
+
+ File thumbnailFile = new File(m_activity.getCacheFileName(firstChild != null ? firstChild : filePath + "/" + fileBaseName));
+
+ ImageView thumbnail = (ImageView) v.findViewById(R.id.thumbnail);
+
+ if (thumbnail != null && thumbnailFile != null) {
+
+ ImageLoader imageLoader = ImageLoader.getInstance();
+ imageLoader.displayImage("file://" + thumbnailFile.getAbsolutePath(), thumbnail);
+ }
+
+ return v;
+ }
+ }
+
+ public int dpToPx(int dp) {
+ DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+ int px = Math.round(dp * (displayMetrics.xdpi / DisplayMetrics.DENSITY_DEFAULT));
+ return px;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ View view = inflater.inflate(R.layout.fragment_comics_list, container, false);
+
+ if (savedInstanceState != null) {
+ m_mode = savedInstanceState.getInt("mode");
+ m_baseDirectory = savedInstanceState.getString("baseDir");
+ }
+
+ m_swipeLayout = (SwipeRefreshLayout) view.findViewById(R.id.comics_swipe_container);
+
+ m_swipeLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
+ @Override
+ public void onRefresh() {
+ rescan(true);
+ }
+ });
+
+ m_swipeLayout.setColorScheme(android.R.color.holo_green_dark,
+ android.R.color.holo_red_dark,
+ android.R.color.holo_blue_dark,
+ android.R.color.holo_orange_dark);
+
+ m_adapter = new ComicsListAdapter(getActivity(), R.layout.comics_grid_row, createCursor(),
+ new String[] { "filename" }, new int[] { R.id.file_name }, 0);
+
+ if (view.findViewById(R.id.comics_list) != null) {
+ ListView list = (ListView) view.findViewById(R.id.comics_list);
+ list.setAdapter(m_adapter);
+ list.setEmptyView(view.findViewById(R.id.no_comics));
+ list.setOnItemClickListener(this);
+
+ registerForContextMenu(list);
+
+ } else {
+ GridView grid = (GridView) view.findViewById(R.id.comics_grid);
+ grid.setAdapter(m_adapter);
+ grid.setEmptyView(view.findViewById(R.id.no_comics));
+ grid.setOnItemClickListener(this);
+
+ registerForContextMenu(grid);
+
+ }
+
+ return view;
+ }
+
+ @Override
+ public void onCreateContextMenu(ContextMenu menu, View v,
+ ContextMenuInfo menuInfo) {
+
+ getActivity().getMenuInflater().inflate(R.menu.comic_archive_context, menu);
+
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo)menuInfo;
+
+ Cursor c = (Cursor) m_adapter.getItem(info.position);
+
+ if (c != null) {
+ menu.setHeaderTitle(c.getString(c.getColumnIndex("filename")));
+ }
+
+ super.onCreateContextMenu(menu, v, menuInfo);
+ }
+
+ @Override
+ public boolean onContextItemSelected(MenuItem item) {
+ AdapterContextMenuInfo info = (AdapterContextMenuInfo) item
+ .getMenuInfo();
+
+ Cursor c = (Cursor) m_adapter.getItem(info.position);
+ String fileName = c.getString(c.getColumnIndex("path")) + "/" + c.getString(c.getColumnIndex("filename"));
+
+ switch (item.getItemId()) {
+ case R.id.menu_reset_progress:
+ if (fileName != null) {
+ m_activity.resetProgress(fileName);
+ m_adapter.notifyDataSetChanged();
+ }
+ return true;
+ case R.id.menu_mark_as_read:
+
+ if (fileName != null) {
+ m_activity.setLastPosition(fileName, m_activity.getSize(fileName)-1);
+ m_adapter.notifyDataSetChanged();
+ }
+
+ return true;
+ default:
+ Log.d(TAG, "onContextItemSelected, unhandled id=" + item.getItemId());
+ return super.onContextItemSelected(item);
+ }
+ }
+
+ private Cursor createCursor() {
+ String baseDir = m_baseDirectory.length() == 0 ? m_prefs.getString("comics_directory", "") : m_baseDirectory;
+
+ String selection;
+ String selectionArgs[];
+
+ switch (m_mode) {
+ case MODE_READ:
+ selection = "path = ? AND position = size - 1";
+ selectionArgs = new String[] { baseDir };
+ break;
+ case MODE_UNFINISHED:
+ selection = "path = ? AND position < size AND position > 0 AND position != size - 1";
+ selectionArgs = new String[] { baseDir };
+ break;
+ case MODE_UNREAD:
+ selection = "path = ? AND position = 0 AND size != ?";
+ selectionArgs = new String[] { baseDir, String.valueOf(SIZE_DIR) };
+ break;
+ default:
+ selection = "path = ?";
+ selectionArgs = new String[] { baseDir };
+ }
+
+ if (!m_prefs.getBoolean("enable_rar", false)) {
+ selection += " AND (UPPER(filename) NOT LIKE '%.CBR' AND UPPER(filename) NOT LIKE '%.RAR')";
+ }
+
+ return m_activity.getReadableDb().query("comics_cache", new String[] { BaseColumns._ID, "filename", "path",
+ "(SELECT path || '/' || filename FROM comics_cache AS t2 WHERE t2.path = comics_cache.path || '/' || comics_cache.filename AND filename != '' ORDER BY filename LIMIT 1) AS firstchild" },
+ selection, selectionArgs, null, null, "size != " + SIZE_DIR + ", filename, size = " + SIZE_DIR + ", filename");
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ m_activity = (CommonActivity)activity;
+
+ m_prefs = PreferenceManager.getDefaultSharedPreferences(activity.getApplicationContext());
+ }
+
+ protected void rescan(final boolean fullRescan) {
+ AsyncTask<String, Integer, Integer> rescanTask = new AsyncTask<String, Integer, Integer>() {
+
+ @Override
+ protected void onProgressUpdate(Integer... progress) {
+ if (isAdded()) {
+ m_activity.setProgress(Math.round(((float)progress[0] / (float)progress[1]) * 10000));
+ }
+ }
+
+ @Override
+ protected Integer doInBackground(String... params) {
+ String comicsDir = params[0];
+
+ File dir = new File(comicsDir);
+
+ int fileIndex = 0;
+
+ if (dir.isDirectory()) {
+ File archives[] = dir.listFiles();
+
+ java.util.Arrays.sort(archives);
+
+ for (File archive : archives) {
+ String filePath = archive.getAbsolutePath();
+
+ if (archive.isDirectory()) {
+ m_activity.setSize(filePath, SIZE_DIR);
+
+ } else if (archive.getName().toLowerCase().matches(".*\\.(cbz|zip|cbr|rar)") && isAdded() && m_activity != null &&
+ m_activity.getWritableDb() != null && m_activity.getWritableDb().isOpen()) {
+ try {
+ int size = m_activity.getSize(filePath);
+
+ if (size == -1 || fullRescan) {
+
+ ComicArchive cba = null;
+
+ if (archive.getName().toLowerCase().matches(".*\\.(cbz|zip)")) {
+ cba = new CbzComicArchive(filePath);
+ }
+
+ if (cba != null && cba.getCount() > 0) {
+ // Get cover
+
+ try {
+ File thumbnailFile = new File(m_activity.getCacheFileName(filePath));
+
+ if (m_activity.isStorageWritable() && (!thumbnailFile.exists() || fullRescan)) {
+ InputStream is = cba.getItem(0);
+
+ if (is != null) {
+ FileOutputStream fos = new FileOutputStream(thumbnailFile);
+
+ byte[] buffer = new byte[1024];
+ int len;
+ while ((len = is.read(buffer)) != -1) {
+ fos.write(buffer, 0, len);
+ }
+
+ fos.close();
+ is.close();
+ }
+ }
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ size = cba.getCount();
+
+ m_activity.setSize(filePath, size);
+ }
+ }
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } catch (SQLException e) {
+ e.printStackTrace();
+ }
+ }
+
+ ++fileIndex;
+
+ publishProgress(Integer.valueOf(fileIndex), Integer.valueOf(archives.length));
+ }
+ }
+
+ if (isAdded() && m_activity != null) {
+ m_activity.cleanupCache(false);
+ m_activity.cleanupSqliteCache(comicsDir);
+ }
+
+ return fileIndex; //m_files.size();
+ }
+
+ @Override
+ protected void onPostExecute(Integer result) {
+ if (isAdded() && m_adapter != null) {
+ m_adapter.changeCursor(createCursor());
+ m_adapter.notifyDataSetChanged();
+
+ if (m_swipeLayout != null) m_swipeLayout.setRefreshing(false);
+ }
+ }
+ };
+
+ String comicsDir = m_prefs.getString("comics_directory", null);
+
+ if (comicsDir != null && m_activity.isStorageAvailable()) {
+ if (m_swipeLayout != null) m_swipeLayout.setRefreshing(true);
+
+ rescanTask.execute(m_baseDirectory.length() > 0 ? m_baseDirectory : comicsDir);
+ }
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ m_adapter.notifyDataSetChanged();
+
+ String comicsDir = m_prefs.getString("comics_directory", "");
+
+ if (m_activity.getCachedItemCount(m_baseDirectory.length() > 0 ? m_baseDirectory : comicsDir) == 0) {
+ rescan(false);
+ } else {
+ m_adapter.notifyDataSetChanged();
+ }
+ }
+
+ public void onItemClick(AdapterView<?> av, View view, int position, long id) {
+ //Log.d(TAG, "onItemClick position=" + position);
+
+ Cursor c = (Cursor) m_adapter.getItem(position);
+ String fileName = c.getString(c.getColumnIndex("path")) + "/" + c.getString(c.getColumnIndex("filename"));
+
+ if (fileName != null) {
+ m_activity.onComicArchiveSelected(fileName);
+ }
+ }
+
+
+ @Override
+ public void onSaveInstanceState(Bundle out) {
+ super.onSaveInstanceState(out);
+
+ out.putInt("mode", m_mode);
+ out.putString("baseDir", m_baseDirectory);
+ }
+
+}
diff --git a/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ComicPager.java b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ComicPager.java
new file mode 100644
index 0000000..6ad4a5d
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ComicPager.java
@@ -0,0 +1,204 @@
+package org.fox.ttcomics2;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.v4.app.Fragment;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentStatePagerAdapter;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.SeekBar;
+import android.widget.TextView;
+
+import java.io.IOException;
+
+public class ComicPager extends Fragment {
+ private String m_fileName;
+ private SharedPreferences m_prefs;
+ private final String TAG = this.getClass().getSimpleName();
+ private ComicArchive m_archive;
+ private CommonActivity m_activity;
+ private SeekBar m_seekBar;
+ private TextView m_currentPage;
+ private TextView m_totalPages;
+
+ public void hideSeekBar(boolean hide) {
+ m_seekBar.setVisibility(hide ? View.GONE : View.VISIBLE);
+ m_currentPage.setVisibility(hide ? View.GONE : View.VISIBLE);
+ m_totalPages.setVisibility(hide ? View.GONE : View.VISIBLE);
+ }
+
+ private class PagerAdapter extends FragmentStatePagerAdapter {
+ public PagerAdapter(FragmentManager fm) {
+ super(fm);
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ ComicFragment cf = new ComicFragment();
+ cf.initialize(m_archive, position);
+
+ return cf;
+ }
+
+ @Override
+ public int getCount() {
+ return m_archive.getCount();
+ }
+ }
+
+ private PagerAdapter m_adapter;
+
+ public ComicPager() {
+ super();
+ }
+
+ public ComicArchive getArchive() {
+ return m_archive;
+ }
+
+ public int getCount() {
+ return m_adapter.getCount();
+ }
+
+ public int getPosition() {
+ ViewPager pager = (ViewPager) getView().findViewById(R.id.comics_pager);
+
+ if (pager != null) {
+ return pager.getCurrentItem();
+ }
+
+ return 0;
+ }
+
+ public String getFileName() {
+ return m_fileName;
+ }
+
+ public void setCurrentItem(int item) {
+ ViewPager pager = (ViewPager) getView().findViewById(R.id.comics_pager);
+
+ if (pager != null) {
+ try {
+ pager.setCurrentItem(item);
+ m_currentPage.setText(String.valueOf(item + 1));
+ m_totalPages.setText(String.valueOf(m_archive.getCount()));
+ } catch (IndexOutOfBoundsException e) {
+ e.printStackTrace();
+ }
+ }
+
+ }
+
+ public void setFileName(String fileName) {
+ m_fileName = fileName;
+ }
+
+ @SuppressLint("NewApi")
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+
+ final View view = inflater.inflate(R.layout.fragment_comics_pager, container, false);
+
+ m_adapter = new PagerAdapter(getActivity().getSupportFragmentManager());
+
+ final ViewPager pager = (ViewPager) view.findViewById(R.id.comics_pager);
+
+ if (savedInstanceState != null) {
+ m_fileName = savedInstanceState.getString("fileName");
+ }
+
+ try {
+ if (m_fileName.toLowerCase().matches(".*\\.(cbz|zip)")) {
+ m_archive = new CbzComicArchive(m_fileName);
+ }
+
+ final int position = m_activity.getLastPosition(m_fileName);
+
+ m_currentPage = (TextView) view.findViewById(R.id.comics_page);
+ m_totalPages = (TextView) view.findViewById(R.id.comics_total_pages);
+
+ m_currentPage.setText(String.valueOf(position + 1));
+ m_totalPages.setText(String.valueOf(m_archive.getCount()));
+
+ m_seekBar = (SeekBar) view.findViewById(R.id.comics_seek_bar);
+ m_seekBar.setMax(m_archive.getCount()-1);
+ m_seekBar.setProgress(position);
+ m_seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
+ @Override
+ public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
+ if (fromUser) {
+ setCurrentItem(progress);
+ }
+ }
+
+ @Override
+ public void onStartTrackingTouch(SeekBar seekBar) {
+
+ }
+
+ @Override
+ public void onStopTrackingTouch(SeekBar seekBar) {
+
+ }
+ });
+
+ pager.setAdapter(m_adapter);
+ pager.setCurrentItem(position);
+
+ m_activity.onComicSelected(m_fileName, position);
+
+ if (m_prefs.getBoolean("use_full_screen", false)) {
+ m_activity.hideSystemUI(true);
+ hideSeekBar(true);
+ }
+
+ } catch (IOException e) {
+ m_activity.toast(R.string.error_could_not_open_comic_archive);
+ e.printStackTrace();
+ }
+
+ pager.setOnPageChangeListener(new ViewPager.OnPageChangeListener() {
+ public void onPageSelected(int position) {
+
+ m_activity.onComicSelected(m_fileName, position);
+ m_seekBar.setProgress(position);
+
+ if (m_prefs.getBoolean("use_full_screen", false)) {
+ m_activity.hideSystemUI(true);
+ }
+ }
+
+ public void onPageScrolled(int arg0, float arg1, int arg2) {
+ // TODO Auto-generated method stub
+ }
+
+ public void onPageScrollStateChanged(int arg0) {
+ // TODO Auto-generated method stub
+ }
+ });
+
+ return view;
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+
+ m_activity = (CommonActivity) activity;
+
+ m_prefs = PreferenceManager.getDefaultSharedPreferences(activity.getApplicationContext());
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle out) {
+ super.onSaveInstanceState(out);
+
+ out.putString("fileName", m_fileName);
+ }
+
+}
diff --git a/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/CommonActivity.java b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/CommonActivity.java
new file mode 100644
index 0000000..2095d87
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/CommonActivity.java
@@ -0,0 +1,605 @@
+package org.fox.ttcomics2;
+
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteStatement;
+import android.graphics.BitmapFactory;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.preference.PreferenceManager;
+import android.provider.BaseColumns;
+import android.support.v7.app.ActionBarActivity;
+import android.util.Log;
+import android.view.Display;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Toast;
+
+import com.nostra13.universalimageloader.core.DisplayImageOptions;
+import com.nostra13.universalimageloader.core.ImageLoader;
+import com.nostra13.universalimageloader.core.ImageLoaderConfiguration;
+import com.nostra13.universalimageloader.core.display.FadeInBitmapDisplayer;
+
+import java.io.File;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Date;
+
+public class CommonActivity extends ActionBarActivity {
+ private final String TAG = this.getClass().getSimpleName();
+
+ protected static final String FRAG_COMICS_PAGER = "comic_pager";
+ protected static final String FRAG_COMICS_LIST = "comics_list";
+
+ protected final static int REQUEST_SHARE = 1;
+ protected static final int REQUEST_VIEWCOMIC = 2;
+
+ protected SharedPreferences m_prefs;
+ protected SyncClient m_syncClient = new SyncClient();
+
+ private boolean m_smallScreenMode = true;
+
+ private boolean m_storageAvailable;
+ private boolean m_storageWritable;
+
+ private SQLiteDatabase m_readableDb;
+ private SQLiteDatabase m_writableDb;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ if (!ImageLoader.getInstance().isInited()) {
+
+ DisplayImageOptions options = new DisplayImageOptions.Builder()
+ .cacheInMemory(true)
+ .resetViewBeforeLoading(true)
+ .cacheOnDisk(false)
+ .displayer(new FadeInBitmapDisplayer(250))
+ .build();
+
+ ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this)
+ .defaultDisplayImageOptions(options)
+ .imageDownloader(new ByteArrayImageDownloader(this))
+ .build();
+
+ ImageLoader.getInstance().init(config);
+ }
+
+ m_prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+
+ initDatabase();
+ initSync();
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ String state = Environment.getExternalStorageState();
+
+ if (Environment.MEDIA_MOUNTED.equals(state)) {
+ m_storageAvailable = true;
+ m_storageWritable = true;
+ } else if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
+ m_storageAvailable = true;
+ m_storageWritable = false;
+ } else {
+ m_storageAvailable = false;
+ m_storageWritable = false;
+ }
+
+ initSync();
+ }
+
+ private void initSync() {
+ if (m_prefs.getBoolean("use_position_sync", false)) {
+ String googleAccount = getGoogleAccount();
+
+ if (googleAccount != null) {
+ m_syncClient.setOwner(googleAccount);
+ } else {
+ if (Build.HARDWARE.contains("goldfish")) {
+ m_syncClient.setOwner("TEST-ACCOUNT");
+
+ //toast(R.string.sync_running_in_test_mode);
+ } else {
+ m_syncClient.setOwner(null);
+ toast(R.string.error_sync_no_account);
+
+ SharedPreferences.Editor editor = m_prefs.edit();
+ editor.putBoolean("use_position_sync", false);
+ editor.commit();
+ }
+ }
+ } else {
+ m_syncClient.setOwner(null);
+ }
+ }
+
+ protected void setSmallScreen(boolean smallScreen) {
+ Log.d(TAG, "m_smallScreenMode=" + smallScreen);
+ m_smallScreenMode = smallScreen;
+ }
+
+ public boolean isSmallScreen() {
+ return m_smallScreenMode;
+ }
+
+
+ public void onComicArchiveSelected(String fileName) {
+ //
+ }
+
+ public Cursor findComicByFileName(String fileName) {
+ File file = new File(fileName);
+
+ Cursor c = getReadableDb().query("comics_cache", null,
+ "filename = ? AND path = ?",
+ new String[] { file.getName(), file.getParentFile().getAbsolutePath() }, null, null, null);
+
+ if (c.moveToFirst()) {
+ return c;
+ } else {
+ c.close();
+
+ SQLiteStatement stmt = getWritableDb().compileStatement("INSERT INTO comics_cache " +
+ "(filename, path, position, max_position, size, checksum) VALUES (?, ?, 0, 0, -1, '')");
+ stmt.bindString(1, file.getName());
+ stmt.bindString(2, file.getParentFile().getAbsolutePath());
+ stmt.execute();
+
+ c = getReadableDb().query("comics_cache", null,
+ "filename = ? AND path = ?",
+ new String[] { file.getName(), file.getParentFile().getAbsolutePath() }, null, null, null);
+
+ if (c.moveToFirst()) {
+ return c;
+ } else {
+ c.close();
+ }
+ }
+
+ return null;
+ }
+
+ public void setSize(String fileName, int size) {
+ Cursor c = findComicByFileName(fileName);
+
+ if (c != null) {
+ c.close();
+
+ File file = new File(fileName);
+
+ SQLiteStatement stmt = getWritableDb().compileStatement("UPDATE comics_cache SET size = ? WHERE filename = ? AND path = ?");
+ stmt.bindLong(1, size);
+ stmt.bindString(2, file.getName());
+ stmt.bindString(3, file.getParentFile().getAbsolutePath());
+ stmt.execute();
+ stmt.close();
+ }
+ }
+
+ public void setChecksum(String fileName, String checksum) {
+ Cursor c = findComicByFileName(fileName);
+
+ if (c != null) {
+ c.close();
+
+ File file = new File(fileName);
+
+ SQLiteStatement stmt = getWritableDb().compileStatement("UPDATE comics_cache SET checksum = ? WHERE filename = ? AND path = ?");
+ stmt.bindString(1, checksum);
+ stmt.bindString(2, file.getName());
+ stmt.bindString(3, file.getParentFile().getAbsolutePath());
+ stmt.execute();
+ stmt.close();
+ }
+ }
+
+ public void resetProgress(final String fileName) {
+
+ if (m_prefs.getBoolean("use_position_sync", false) && m_syncClient.hasOwner()) {
+ setLastPosition(fileName, 0);
+ setLastMaxPosition(fileName, 0);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(CommonActivity.this);
+ builder.setMessage(R.string.reset_remove_synced_progress)
+ .setCancelable(true)
+ .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ m_syncClient.clearData(sha1(new File(fileName).getName()));
+ }
+ })
+ .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+ AlertDialog alert = builder.create();
+ alert.show();
+
+ }
+
+ }
+
+ public void setLastPosition(String fileName, int position) {
+ int lastPosition = getLastPosition(fileName);
+
+ Cursor c = findComicByFileName(fileName);
+
+ if (c != null) {
+ c.close();
+
+ File file = new File(fileName);
+
+ SQLiteStatement stmt = getWritableDb().compileStatement("UPDATE comics_cache SET position = ?, max_position = ? WHERE filename = ? AND path = ?");
+ stmt.bindLong(1, position);
+ stmt.bindLong(2, Math.max(position, lastPosition));
+ stmt.bindString(3, file.getName());
+ stmt.bindString(4, file.getParentFile().getAbsolutePath());
+ stmt.execute();
+ stmt.close();
+ }
+
+ }
+
+ public void setLastMaxPosition(String fileName, int position) {
+
+ Cursor c = findComicByFileName(fileName);
+
+ if (c != null) {
+ c.close();
+
+ File file = new File(fileName);
+
+ SQLiteStatement stmt = getWritableDb().compileStatement("UPDATE comics_cache SET max_position = ? WHERE filename = ? AND path = ?");
+ stmt.bindLong(1, position);
+ stmt.bindString(2, file.getName());
+ stmt.bindString(3, file.getParentFile().getAbsolutePath());
+ stmt.execute();
+ stmt.close();
+ }
+ }
+
+ public String getChecksum(String fileName) {
+ String checksum = null;
+
+ File file = new File(fileName);
+
+ Cursor c = getReadableDb().query("comics_cache", new String[] { "checksum" },
+ "filename = ? AND path = ?",
+ new String[] { file.getName(), file.getParentFile().getAbsolutePath() }, null, null, null);
+
+ if (c.moveToFirst()) {
+ checksum = c.getString(c.getColumnIndex("checksum"));
+ }
+
+ c.close();
+
+ return checksum;
+
+ }
+
+
+ public int getLastPosition(String fileName) {
+ int position = 0;
+
+ File file = new File(fileName);
+
+ Cursor c = getReadableDb().query("comics_cache", new String[] { "position" },
+ "filename = ? AND path = ?",
+ new String[] { file.getName(), file.getParentFile().getAbsolutePath() }, null, null, null);
+
+ if (c.moveToFirst()) {
+ position = c.getInt(c.getColumnIndex("position"));
+ }
+
+ c.close();
+
+ return position;
+
+ }
+
+ public int getCachedItemCount(String baseDir) {
+ Cursor c = getReadableDb().query("comics_cache", new String[] { "COUNT(*)" },
+ "path = ?",
+ new String[] { baseDir }, null, null, null);
+
+ c.moveToFirst();
+ int count = c.getInt(0);
+ c.close();
+
+ //Log.d(TAG, "getCachedItemCount:" + baseDir + "=" + count);
+
+ return count;
+ }
+
+ public int getMaxPosition(String fileName) {
+ int position = 0;
+
+ File file = new File(fileName);
+
+ Cursor c = getReadableDb().query("comics_cache", new String[] { "max_position" },
+ "filename = ? AND path = ?",
+ new String[] { file.getName(), file.getParentFile().getAbsolutePath() }, null, null, null);
+
+ if (c.moveToFirst()) {
+ position = c.getInt(c.getColumnIndex("max_position"));
+ }
+
+ c.close();
+
+ return position;
+ }
+
+ public int getSize(String fileName) {
+ int size = -1;
+
+ File file = new File(fileName);
+
+ Cursor c = getReadableDb().query("comics_cache", new String[] { "size" },
+ "filename = ? AND path = ?",
+ new String[] { file.getName(), file.getParentFile().getAbsolutePath() }, null, null, null);
+
+ if (c.moveToFirst()) {
+ size = c.getInt(c.getColumnIndex("size"));
+ }
+
+ c.close();
+
+ //Log.d(TAG, "getSize:" + fileName + "=" + size);
+
+ return size;
+ }
+
+ public void onComicSelected(String fileName, int position) {
+ setLastPosition(fileName, position);
+ }
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_rescan:
+ ComicListFragment frag = (ComicListFragment) getSupportFragmentManager().findFragmentByTag(FRAG_COMICS_LIST);
+
+ if (frag != null && frag.isAdded()) {
+ frag.rescan(true);
+ }
+
+ return true;
+ case R.id.menu_settings:
+ Intent intent = new Intent(CommonActivity.this,
+ PreferencesActivity.class);
+ startActivityForResult(intent, 0);
+ return true;
+ default:
+ Log.d(TAG,
+ "onOptionsItemSelected, unhandled id=" + item.getItemId());
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ public boolean isPortrait() {
+ Display display = getWindowManager().getDefaultDisplay();
+
+ int width = display.getWidth();
+ int height = display.getHeight();
+
+ return width < height;
+ }
+
+ protected static String md5(String s) {
+ try {
+ MessageDigest digest = java.security.MessageDigest.getInstance("MD5");
+ digest.update(s.getBytes());
+ byte messageDigest[] = digest.digest();
+
+ StringBuffer hexString = new StringBuffer();
+ for (int i=0; i<messageDigest.length; i++)
+ hexString.append(Integer.toHexString(0xFF & messageDigest[i]));
+
+ return hexString.toString();
+
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+
+ return null;
+ }
+
+ protected static String sha1(String s) {
+ if (s != null) {
+ try {
+ MessageDigest digest = java.security.MessageDigest.getInstance("SHA1");
+ digest.update(s.getBytes());
+ byte messageDigest[] = digest.digest();
+
+ StringBuffer hexString = new StringBuffer();
+ for (int i=0; i<messageDigest.length; i++)
+ hexString.append(Integer.toHexString(0xFF & messageDigest[i]));
+
+ return hexString.toString();
+
+ } catch (NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+ }
+
+ return null;
+ }
+
+ public String getCacheFileName(String fileName) {
+ String hash = md5(fileName);
+
+ File file = new File(getExternalCacheDir().getAbsolutePath() + "/" + hash + ".png");
+
+ return file.getAbsolutePath();
+ }
+
+ public String getGoogleAccount() {
+ AccountManager manager = (AccountManager) getSystemService(ACCOUNT_SERVICE);
+ Account[] list = manager.getAccounts();
+
+ for (Account account: list) {
+ if (account.type.equalsIgnoreCase("com.google")) {
+ return account.name;
+ }
+ }
+ return null;
+ }
+
+ public void toast(int msgId) {
+ try {
+ Toast toast = Toast.makeText(CommonActivity.this, msgId, Toast.LENGTH_SHORT);
+ toast.show();
+ } catch (RuntimeException e) {
+ // might happen if UI lags
+ }
+ }
+
+ public void toast(String msg) {
+ try {
+ Toast toast = Toast.makeText(CommonActivity.this, msg, Toast.LENGTH_SHORT);
+ toast.show();
+ } catch (RuntimeException e) {
+ // might happen if UI lags
+ }
+ }
+
+ public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
+ // Raw height and width of image
+ final int height = options.outHeight;
+ final int width = options.outWidth;
+ int inSampleSize = 1;
+
+ if (height > reqHeight || width > reqWidth) {
+ if (width > height) {
+ inSampleSize = Math.round((float)height / (float)reqHeight);
+ } else {
+ inSampleSize = Math.round((float)width / (float)reqWidth);
+ }
+ }
+ return inSampleSize;
+ }
+
+
+ public void cleanupCache(boolean deleteAll) {
+ if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
+ File cachePath = getExternalCacheDir();
+
+ long now = new Date().getTime();
+
+ if (cachePath.isDirectory()) {
+ for (File file : cachePath.listFiles()) {
+ if (deleteAll || now - file.lastModified() > 1000*60*60*24*7) {
+ file.delete();
+ }
+ }
+ }
+ }
+ }
+
+ public boolean isStorageAvailable() {
+ return m_storageAvailable;
+ }
+
+ public boolean isStorageWritable() {
+ return m_storageWritable;
+ }
+
+ private void initDatabase() {
+ DatabaseHelper dh = new DatabaseHelper(getApplicationContext());
+
+ m_writableDb = dh.getWritableDatabase();
+ m_readableDb = dh.getReadableDatabase();
+ }
+
+ public synchronized SQLiteDatabase getReadableDb() {
+ return m_readableDb;
+ }
+
+ public synchronized SQLiteDatabase getWritableDb() {
+ return m_writableDb;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ m_readableDb.close();
+ m_writableDb.close();
+ }
+
+ public void selectPreviousComic() {
+ ComicPager frag = (ComicPager) getSupportFragmentManager().findFragmentByTag(FRAG_COMICS_PAGER);
+
+ if (frag != null && frag.isAdded() && frag.getPosition() > 0) {
+ frag.setCurrentItem(frag.getPosition() - 1);
+ }
+ }
+
+ public void selectNextComic() {
+ ComicPager frag = (ComicPager) getSupportFragmentManager().findFragmentByTag(FRAG_COMICS_PAGER);
+
+ if (frag != null && frag.isAdded() && frag.getPosition() < frag.getCount()-1) {
+ frag.setCurrentItem(frag.getPosition() + 1);
+ }
+ }
+
+ public void cleanupSqliteCache(String baseDir) {
+ Cursor c = getReadableDb().query("comics_cache", null,
+ null, null, null, null, null);
+
+ if (c.moveToFirst()) {
+ while (!c.isAfterLast()) {
+ int id = c.getInt(c.getColumnIndex(BaseColumns._ID));
+ String fileName = c.getString(c.getColumnIndex("path")) + "/" + c.getString(c.getColumnIndex("filename"));
+
+ File file = new File(fileName);
+
+ if (!file.exists()) {
+ SQLiteStatement stmt = getWritableDb().compileStatement("DELETE FROM comics_cache WHERE " + BaseColumns._ID + " = ?");
+ stmt.bindLong(1, id);
+ stmt.execute();
+ }
+
+ c.moveToNext();
+ }
+ }
+
+ c.close();
+ }
+
+ public void hideSystemUI(boolean hide) {
+ View decorView = getWindow().getDecorView();
+
+ if (hide) {
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.KITKAT) {
+
+ decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_IMMERSIVE);
+
+ } else {
+ decorView.setSystemUiVisibility(View.STATUS_BAR_HIDDEN | View.SYSTEM_UI_FLAG_LOW_PROFILE);
+ }
+
+ getSupportActionBar().hide();
+
+ } else {
+ decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
+ }
+ }
+
+}
diff --git a/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/DatabaseHelper.java b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/DatabaseHelper.java
new file mode 100644
index 0000000..bf66fec
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/DatabaseHelper.java
@@ -0,0 +1,68 @@
+package org.fox.ttcomics2;
+
+import java.io.File;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteStatement;
+import android.provider.BaseColumns;
+
+public class DatabaseHelper extends SQLiteOpenHelper {
+
+ public static final String DATABASE_NAME = "ComicsCache.db";
+ public static final int DATABASE_VERSION = 2;
+
+ public DatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS comics_cache;");
+
+ db.execSQL("CREATE TABLE IF NOT EXISTS comics_cache (" +
+ BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
+ "filename TEXT, " +
+ "path TEXT, " + //v2
+ "checksum TEXT, " + //v2
+ "size INTEGER, " +
+ "position INTEGER, " +
+ "max_position INTEGER" +
+ ");");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (oldVersion == 1 && newVersion == 2) {
+
+ db.execSQL("ALTER TABLE comics_cache ADD COLUMN path TEXT;");
+ db.execSQL("ALTER TABLE comics_cache ADD COLUMN checksum TEXT;");
+
+ Cursor c = db.query("comics_cache", null,
+ null, null, null, null, null);
+
+ if (c.moveToFirst()) {
+ while (!c.isAfterLast()) {
+ int id = c.getInt(c.getColumnIndex(BaseColumns._ID));
+ String fileName = c.getString(c.getColumnIndex("filename"));
+
+ File file = new File(fileName);
+
+ SQLiteStatement stmt = db.compileStatement("UPDATE comics_cache SET filename = ?, path = ? WHERE " + BaseColumns._ID + " = ?");
+ stmt.bindString(1, file.getName());
+ stmt.bindString(2, file.getParentFile().getAbsolutePath());
+ stmt.bindLong(3, id);
+ stmt.execute();
+
+ c.moveToNext();
+ }
+ }
+
+ c.close();
+ }
+ }
+
+}
diff --git a/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/DirectoryPicker.java b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/DirectoryPicker.java
new file mode 100644
index 0000000..0c75f09
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/DirectoryPicker.java
@@ -0,0 +1,169 @@
+package org.fox.ttcomics2;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+
+import android.app.ListActivity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Environment;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.ArrayAdapter;
+import android.widget.Button;
+import android.widget.ListView;
+import android.widget.Toast;
+
+/**
+Copyright (C) 2011 by Brad Greco <[email protected]>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+ */
+
+public class DirectoryPicker extends ListActivity {
+
+ public static final String START_DIR = "startDir";
+ public static final String ONLY_DIRS = "onlyDirs";
+ public static final String SHOW_HIDDEN = "showHidden";
+ public static final String CHOSEN_DIRECTORY = "chosenDir";
+ public static final int PICK_DIRECTORY = 43522432;
+ private File dir;
+ private boolean showHidden = false;
+ private boolean onlyDirs = true ;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ Bundle extras = getIntent().getExtras();
+ dir = Environment.getExternalStorageDirectory();
+ if (extras != null) {
+ String preferredStartDir = extras.getString(START_DIR);
+ showHidden = extras.getBoolean(SHOW_HIDDEN, false);
+ onlyDirs = extras.getBoolean(ONLY_DIRS, true);
+ if(preferredStartDir != null) {
+ File startDir = new File(preferredStartDir);
+ if(startDir.isDirectory()) {
+ dir = startDir;
+ }
+ }
+ }
+
+ setContentView(R.layout.chooser_list);
+ setTitle(dir.getAbsolutePath());
+ Button btnChoose = (Button) findViewById(R.id.btnChoose);
+ String name = dir.getName();
+ if(name.length() == 0)
+ name = "/";
+ btnChoose.setText(getString(R.string.picker_choose, name));
+ btnChoose.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ returnDir(dir.getAbsolutePath());
+ }
+ });
+
+ Button btnParent = (Button) findViewById(R.id.btnParent);
+ btnParent.setOnClickListener(new View.OnClickListener() {
+ public void onClick(View v) {
+ Intent intent = new Intent(DirectoryPicker.this, DirectoryPicker.class);
+ intent.putExtra(DirectoryPicker.START_DIR, dir.getParent());
+ intent.putExtra(DirectoryPicker.SHOW_HIDDEN, showHidden);
+ intent.putExtra(DirectoryPicker.ONLY_DIRS, onlyDirs);
+ startActivityForResult(intent, PICK_DIRECTORY);
+ }
+ });
+
+ if (dir.getParent() == null) {
+ btnParent.setVisibility(View.GONE);
+ }
+
+ ListView lv = getListView();
+ lv.setTextFilterEnabled(true);
+
+ if(!dir.canRead()) {
+ Context context = getApplicationContext();
+ String msg = getString(R.string.error_could_not_read_folder_contents_);
+ Toast toast = Toast.makeText(context, msg, Toast.LENGTH_LONG);
+ toast.show();
+ return;
+ }
+
+ final ArrayList<File> files = filter(dir.listFiles(), onlyDirs, showHidden);
+ String[] names = names(files);
+ setListAdapter(new ArrayAdapter<String>(this, R.layout.list_item, names));
+
+
+ lv.setOnItemClickListener(new OnItemClickListener() {
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ if(!files.get(position).isDirectory())
+ return;
+ String path = files.get(position).getAbsolutePath();
+ Intent intent = new Intent(DirectoryPicker.this, DirectoryPicker.class);
+ intent.putExtra(DirectoryPicker.START_DIR, path);
+ intent.putExtra(DirectoryPicker.SHOW_HIDDEN, showHidden);
+ intent.putExtra(DirectoryPicker.ONLY_DIRS, onlyDirs);
+ startActivityForResult(intent, PICK_DIRECTORY);
+ }
+ });
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if(requestCode == PICK_DIRECTORY && resultCode == RESULT_OK) {
+ Bundle extras = data.getExtras();
+ String path = (String) extras.get(DirectoryPicker.CHOSEN_DIRECTORY);
+ returnDir(path);
+ }
+ }
+
+ private void returnDir(String path) {
+ Intent result = new Intent();
+ result.putExtra(CHOSEN_DIRECTORY, path);
+ setResult(RESULT_OK, result);
+ finish();
+ }
+
+ public ArrayList<File> filter(File[] file_list, boolean onlyDirs, boolean showHidden) {
+ ArrayList<File> files = new ArrayList<File>();
+
+ for(File file: file_list) {
+ if(onlyDirs && !file.isDirectory())
+ continue;
+ if(!showHidden && file.isHidden())
+ continue;
+ files.add(file);
+ }
+ Collections.sort(files);
+
+ return files;
+ }
+
+ public String[] names(ArrayList<File> files) {
+ String[] names = new String[files.size()];
+ int i = 0;
+ for(File file: files) {
+ names[i] = file.getName();
+ i++;
+ }
+ return names;
+ }
+}
+
diff --git a/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/MainActivity.java b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/MainActivity.java
new file mode 100644
index 0000000..9dda29b
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/MainActivity.java
@@ -0,0 +1,228 @@
+package org.fox.ttcomics2;
+
+
+import android.annotation.SuppressLint;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v4.app.FragmentTransaction;
+import android.support.v7.app.ActionBar;
+import android.util.Log;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import java.io.File;
+
+import it.neokree.materialtabs.MaterialTab;
+import it.neokree.materialtabs.MaterialTabHost;
+import it.neokree.materialtabs.MaterialTabListener;
+
+public class MainActivity extends CommonActivity implements MaterialTabListener {
+ private final String TAG = this.getClass().getSimpleName();
+ private final static int TRIAL_DAYS = 7;
+
+ private int m_selectedTab;
+ private String m_baseDirectory = "";
+ private String m_fileName = "";
+ private MaterialTabHost tabHost;
+
+ @SuppressLint("NewApi")
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.activity_main);
+ setTitle(R.string.app_name);
+
+ tabHost = (MaterialTabHost) this.findViewById(R.id.materialTabHost);
+
+ if (savedInstanceState == null) {
+ m_selectedTab = getIntent().getIntExtra("selectedTab", 0);
+
+ Log.d(TAG, "selTab=" + m_selectedTab);
+
+ ComicListFragment frag = new ComicListFragment();
+ frag.setMode(m_selectedTab);
+
+ if (getIntent().getStringExtra("baseDir") != null) {
+ m_baseDirectory = getIntent().getStringExtra("baseDir");
+ frag.setBaseDirectory(m_baseDirectory);
+ }
+
+ FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
+ ft.replace(R.id.comics_list, frag, FRAG_COMICS_LIST);
+ ft.commit();
+
+ m_selectedTab = -1;
+ } else {
+ m_selectedTab = -1;
+ m_baseDirectory = savedInstanceState.getString("baseDir");
+ m_fileName = savedInstanceState.getString("fileName");
+ }
+
+ tabHost.addTab(tabHost.newTab()
+ .setText(getString(R.string.tab_all_comics))
+ .setTabListener(this));
+
+ tabHost.addTab(tabHost.newTab()
+ .setText(getString(R.string.tab_unread))
+ .setTabListener(this));
+
+ tabHost.addTab(tabHost.newTab()
+ .setText(getString(R.string.tab_unfinished))
+ .setTabListener(this));
+
+ tabHost.addTab(tabHost.newTab()
+ .setText(getString(R.string.tab_read))
+ .setTabListener(this));
+
+ if (savedInstanceState != null) {
+ m_selectedTab = savedInstanceState.getInt("selectedTab");
+ } else {
+ m_selectedTab = getIntent().getIntExtra("selectedTab", 0);
+ }
+
+ if (m_selectedTab != -1)
+ tabHost.setSelectedNavigationItem(m_selectedTab);
+
+ ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayHomeAsUpEnabled(m_baseDirectory.length() > 0);
+
+ if (m_prefs.getString("comics_directory", null) == null) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setMessage(R.string.dialog_need_prefs_message)
+ .setCancelable(false)
+ .setPositiveButton(R.string.dialog_need_prefs_preferences, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ // launch preferences
+
+ Intent intent = new Intent(MainActivity.this,
+ PreferencesActivity.class);
+ startActivityForResult(intent, 0);
+ }
+ })
+ .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+ AlertDialog alert = builder.create();
+ alert.show();
+ }
+
+ //((ViewGroup)findViewById(R.id.comics_list)).setLayoutTransition(new LayoutTransition());
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.activity_main, menu);
+
+ return true;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle out) {
+ super.onSaveInstanceState(out);
+
+ out.putInt("selectedTab", m_selectedTab);
+ out.putString("baseDir", m_baseDirectory);
+ out.putString("fileName", m_fileName);
+ }
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case android.R.id.home:
+ if (m_baseDirectory.length() > 0) {
+ finish();
+ }
+ return true;
+ default:
+ Log.d(TAG,
+ "onOptionsItemSelected, unhandled id=" + item.getItemId());
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ @Override
+ public void onComicArchiveSelected(String fileName) {
+ super.onComicArchiveSelected(fileName);
+
+ File file = new File(fileName);
+
+ if (file.isDirectory()) {
+ Intent intent = new Intent(MainActivity.this,
+ MainActivity.class);
+
+ intent.putExtra("baseDir", fileName);
+ intent.putExtra("selectedTab", m_selectedTab);
+
+ startActivityForResult(intent, 0);
+
+ } else if (file.canRead()) {
+ Intent intent = new Intent(MainActivity.this,
+ ViewComicActivity.class);
+
+ intent.putExtra("fileName", fileName);
+ m_fileName = fileName;
+
+ startActivityForResult(intent, REQUEST_VIEWCOMIC);
+ } else {
+ toast(getString(R.string.error_cant_open_file, fileName));
+
+ ComicListFragment frag = (ComicListFragment) getSupportFragmentManager().findFragmentByTag(FRAG_COMICS_LIST);
+
+ if (frag != null && frag.isAdded()) {
+ frag.rescan(true);
+ }
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ if (requestCode == REQUEST_VIEWCOMIC) {
+ Log.d(TAG, "finished viewing comic: " + m_fileName);
+
+ if (m_prefs.getBoolean("use_position_sync", false) && m_syncClient.hasOwner()) {
+ toast(R.string.sync_uploading);
+ m_syncClient.setPosition(sha1(new File(m_fileName).getName()), getLastPosition(m_fileName));
+ }
+ }
+
+ System.gc();
+
+ super.onActivityResult(requestCode, resultCode, intent);
+ }
+
+ @Override
+ public void onTabSelected(MaterialTab tab) {
+
+ tabHost.setSelectedNavigationItem(tab.getPosition());
+
+ FragmentTransaction sft = getSupportFragmentManager().beginTransaction();
+
+ if (m_selectedTab != tab.getPosition() && m_selectedTab != -1) {
+
+ ComicListFragment frag = new ComicListFragment();
+ frag.setMode(tab.getPosition());
+
+ frag.setBaseDirectory(m_baseDirectory);
+
+ sft.replace(R.id.comics_list, frag, FRAG_COMICS_LIST);
+ }
+
+ m_selectedTab = tab.getPosition();
+
+ sft.commit();
+ }
+
+ @Override
+ public void onTabReselected(MaterialTab materialTab) {
+
+ }
+
+ @Override
+ public void onTabUnselected(MaterialTab materialTab) {
+
+ }
+}
diff --git a/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/NaturalOrderComparator.java b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/NaturalOrderComparator.java
new file mode 100644
index 0000000..d7522f9
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/NaturalOrderComparator.java
@@ -0,0 +1,188 @@
+package org.fox.ttcomics2;
+
+/*
+ NaturalOrderComparator.java -- Perform 'natural order' comparisons of strings in Java.
+ Copyright (C) 2003 by Pierre-Luc Paour <[email protected]>
+
+ Based on the C version by Martin Pool, of which this is more or less a straight conversion.
+ Copyright (C) 2000 by Martin Pool <[email protected]>
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+ */
+
+import java.util.*;
+
+public class NaturalOrderComparator implements Comparator
+{
+ int compareRight(String a, String b)
+ {
+ int bias = 0;
+ int ia = 0;
+ int ib = 0;
+
+ // The longest run of digits wins. That aside, the greatest
+ // value wins, but we can't know that it will until we've scanned
+ // both numbers to know that they have the same magnitude, so we
+ // remember it in BIAS.
+ for (;; ia++, ib++)
+ {
+ char ca = charAt(a, ia);
+ char cb = charAt(b, ib);
+
+ if (!Character.isDigit(ca) && !Character.isDigit(cb))
+ {
+ return bias;
+ }
+ else if (!Character.isDigit(ca))
+ {
+ return -1;
+ }
+ else if (!Character.isDigit(cb))
+ {
+ return +1;
+ }
+ else if (ca < cb)
+ {
+ if (bias == 0)
+ {
+ bias = -1;
+ }
+ }
+ else if (ca > cb)
+ {
+ if (bias == 0)
+ bias = +1;
+ }
+ else if (ca == 0 && cb == 0)
+ {
+ return bias;
+ }
+ }
+ }
+
+ public int compare(Object o1, Object o2)
+ {
+ String a = o1.toString();
+ String b = o2.toString();
+
+ int ia = 0, ib = 0;
+ int nza = 0, nzb = 0;
+ char ca, cb;
+ int result;
+
+ while (true)
+ {
+ // only count the number of zeroes leading the last number compared
+ nza = nzb = 0;
+
+ ca = charAt(a, ia);
+ cb = charAt(b, ib);
+
+ // skip over leading spaces or zeros
+ while (Character.isSpaceChar(ca) || ca == '0')
+ {
+ if (ca == '0')
+ {
+ nza++;
+ }
+ else
+ {
+ // only count consecutive zeroes
+ nza = 0;
+ }
+
+ ca = charAt(a, ++ia);
+ }
+
+ while (Character.isSpaceChar(cb) || cb == '0')
+ {
+ if (cb == '0')
+ {
+ nzb++;
+ }
+ else
+ {
+ // only count consecutive zeroes
+ nzb = 0;
+ }
+
+ cb = charAt(b, ++ib);
+ }
+
+ // process run of digits
+ if (Character.isDigit(ca) && Character.isDigit(cb))
+ {
+ if ((result = compareRight(a.substring(ia), b.substring(ib))) != 0)
+ {
+ return result;
+ }
+ }
+
+ if (ca == 0 && cb == 0)
+ {
+ // The strings compare the same. Perhaps the caller
+ // will want to call strcmp to break the tie.
+ return nza - nzb;
+ }
+
+ if (ca < cb)
+ {
+ return -1;
+ }
+ else if (ca > cb)
+ {
+ return +1;
+ }
+
+ ++ia;
+ ++ib;
+ }
+ }
+
+ static char charAt(String s, int i)
+ {
+ if (i >= s.length())
+ {
+ return 0;
+ }
+ else
+ {
+ return s.charAt(i);
+ }
+ }
+
+ public static void main(String[] args)
+ {
+ String[] strings = new String[] { "1-2", "1-02", "1-20", "10-20", "fred", "jane", "pic01",
+ "pic2", "pic02", "pic02a", "pic3", "pic4", "pic 4 else", "pic 5", "pic05", "pic 5",
+ "pic 5 something", "pic 6", "pic 7", "pic100", "pic100a", "pic120", "pic121",
+ "pic02000", "tom", "x2-g8", "x2-y7", "x2-y08", "x8-y8" };
+
+ List orig = Arrays.asList(strings);
+
+ System.out.println("Original: " + orig);
+
+ List scrambled = Arrays.asList(strings);
+ Collections.shuffle(scrambled);
+
+ System.out.println("Scrambled: " + scrambled);
+
+ Collections.sort(scrambled, new NaturalOrderComparator());
+
+ System.out.println("Sorted: " + scrambled);
+ }
+} \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/PreferencesActivity.java b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/PreferencesActivity.java
new file mode 100644
index 0000000..f6db9cc
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/PreferencesActivity.java
@@ -0,0 +1,156 @@
+package org.fox.ttcomics2;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.app.AlertDialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Environment;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceManager;
+import android.widget.Toast;
+
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Locale;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+public class PreferencesActivity extends PreferenceActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ final SharedPreferences prefs = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext());
+
+ addPreferencesFromResource(R.xml.preferences);
+
+ Preference dirPref = findPreference("comics_directory");
+ dirPref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ Intent intent = new Intent(PreferencesActivity.this, DirectoryPicker.class);
+
+ intent.putExtra(DirectoryPicker.START_DIR, prefs.getString("comics_directory",
+ Environment.getExternalStorageDirectory().getAbsolutePath()));
+
+ startActivityForResult(intent, DirectoryPicker.PICK_DIRECTORY);
+ return true;
+ }
+ });
+
+ Preference clearPref = findPreference("clear_sync_data");
+ clearPref.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(PreferencesActivity.this);
+ builder.setMessage(R.string.dialog_clear_data_title)
+ .setCancelable(false)
+ .setPositiveButton(R.string.dialog_clear_data, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+
+ String googleAccount = getGoogleAccount();
+ SyncClient m_syncClient = new SyncClient();
+
+ if (googleAccount != null) {
+ m_syncClient.setOwner(googleAccount);
+ } else {
+ if (Build.HARDWARE.equals("goldfish")) {
+ m_syncClient.setOwner("TEST-ACCOUNT");
+ } else {
+ m_syncClient.setOwner(null);
+
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putBoolean("use_position_sync", false);
+ editor.commit();
+
+ Toast toast = Toast.makeText(PreferencesActivity.this, R.string.error_sync_no_account, Toast.LENGTH_SHORT);
+ toast.show();
+ }
+ }
+
+ if (m_syncClient.hasOwner()) {
+ m_syncClient.clearData();
+ }
+ }
+ })
+ .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+ AlertDialog alert = builder.create();
+ alert.show();
+
+ return false;
+ }
+ });
+
+ String version = "?";
+ int versionCode = -1;
+ String buildTimestamp = "N/A";
+
+ try {
+ PackageInfo packageInfo = getPackageManager().
+ getPackageInfo(getPackageName(), 0);
+
+ version = packageInfo.versionName;
+ versionCode = packageInfo.versionCode;
+
+ ApplicationInfo appInfo = getPackageManager().
+ getApplicationInfo(getPackageName(), 0);
+
+ ZipFile zf = new ZipFile(appInfo.sourceDir);
+ ZipEntry ze = zf.getEntry("classes.dex");
+ long time = ze.getTime();
+
+ buildTimestamp = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss",
+ Locale.getDefault()).format(time);
+
+ } catch (PackageManager.NameNotFoundException e) {
+ e.printStackTrace();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ findPreference("version").setSummary(getString(R.string.prefs_version, version, versionCode));
+ findPreference("build_timestamp").setSummary(getString(R.string.prefs_build_timestamp, buildTimestamp));
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ if(requestCode == DirectoryPicker.PICK_DIRECTORY && resultCode == RESULT_OK) {
+ Bundle extras = data.getExtras();
+ String path = (String) extras.get(DirectoryPicker.CHOSEN_DIRECTORY);
+
+ SharedPreferences prefs = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext());
+
+ SharedPreferences.Editor editor = prefs.edit();
+ editor.putString("comics_directory", path);
+ editor.commit();
+
+ }
+ }
+
+ public String getGoogleAccount() {
+ AccountManager manager = (AccountManager) getSystemService(ACCOUNT_SERVICE);
+ Account[] list = manager.getAccounts();
+
+ for (Account account: list) {
+ if (account.type.equalsIgnoreCase("com.google")) {
+ return account.name;
+ }
+ }
+ return null;
+ }
+}
diff --git a/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/SyncClient.java b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/SyncClient.java
new file mode 100644
index 0000000..7d4c64a
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/SyncClient.java
@@ -0,0 +1,161 @@
+package org.fox.ttcomics2;
+
+import java.io.InputStreamReader;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+
+import android.os.AsyncTask;
+import android.util.Log;
+
+public class SyncClient {
+ public interface PositionReceivedListener {
+ void onPositionReceived(int position);
+ }
+
+ private class HttpTask extends AsyncTask<String, Integer, Boolean> {
+ protected String m_response = null;
+ protected int m_responseCode = -1;
+
+ @Override
+ protected Boolean doInBackground(String... params) {
+
+ String requestStr = null;
+ String op = params[0];
+
+ if (op.equals("set")) {
+ requestStr = String.format("op=set&owner=%1$s&hash=%2$s&position=%3$s", m_owner, params[1], params[2]);
+ } else if (op.equals("get")) {
+ requestStr = String.format("op=get&owner=%1$s&hash=%2$s", m_owner, params[1]);
+ } else if (op.equals("clear")) {
+ if (params.length > 1) {
+ requestStr = String.format("op=clear&owner=%1$s&hash=%2$s", m_owner, params[1]);
+ } else {
+ requestStr = String.format("op=clear&owner=%1$s", m_owner);
+ }
+ }
+
+ requestStr += "&version=2";
+
+ Log.d(TAG, requestStr);
+
+ if (requestStr == null) return false;
+
+ try {
+ byte[] postData = requestStr.getBytes("UTF-8");
+
+ URL url = new URL(SYNC_ENDPOINT);
+
+ HttpURLConnection conn = (HttpURLConnection) url.openConnection();
+ conn.setDoInput(true);
+ conn.setDoOutput(true);
+ conn.setUseCaches(false);
+ conn.setRequestMethod("POST");
+
+ OutputStream out = conn.getOutputStream();
+ out.write(postData);
+ out.close();
+
+ m_responseCode = conn.getResponseCode();
+
+ if (m_responseCode == HttpURLConnection.HTTP_OK) {
+ StringBuffer response = new StringBuffer();
+ InputStreamReader in = new InputStreamReader(conn.getInputStream(), "UTF-8");
+
+ char[] buf = new char[1024];
+ int read = 0;
+
+ while ((read = in.read(buf)) >= 0) {
+ response.append(buf, 0, read);
+ }
+
+ //Log.d(TAG, "<<< " + response);
+
+ m_response = response.toString();
+
+ if (response.indexOf("ERROR") == -1) {
+ return true;
+ } else {
+ return false;
+ }
+ } else {
+ Log.d(TAG, "HTTP error, code: " + m_responseCode);
+ }
+
+ conn.disconnect();
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ return false;
+ }
+
+ }
+
+ private final String TAG = this.getClass().getSimpleName();
+ private static final String SYNC_ENDPOINT = "http://tt-rss.org/tcrsync/";
+ private String m_owner = null;
+
+ public void setOwner(String owner) {
+ m_owner = CommonActivity.sha1(owner);
+ }
+
+ public int getPosition(String hash, final PositionReceivedListener listener) {
+ if (m_owner != null) {
+ Log.d(TAG, "Requesting sync data...");
+
+ HttpTask task = new HttpTask() {
+ @Override
+ protected void onPostExecute(Boolean result) {
+ if (result) {
+ try {
+ listener.onPositionReceived(Integer.valueOf(m_response));
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ };
+
+ task.execute("get", hash);
+
+ }
+ return -1;
+ }
+
+ public void setPosition(String hash, int position) {
+ if (m_owner != null) {
+ Log.d(TAG, "Uploading sync data...");
+
+ HttpTask task = new HttpTask();
+
+ task.execute("set", hash, String.valueOf(position));
+ }
+ }
+
+ public void clearData() {
+ if (m_owner != null) {
+ Log.d(TAG, "Clearing sync data...");
+
+ HttpTask task = new HttpTask();
+
+ task.execute("clear");
+ }
+ }
+
+ public void clearData(String hash) {
+ if (m_owner != null) {
+ Log.d(TAG, "Clearing sync data: " + hash);
+
+ HttpTask task = new HttpTask();
+
+ task.execute("clear", hash);
+ }
+ }
+
+ public boolean hasOwner() {
+ return m_owner != null;
+ }
+
+}
diff --git a/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ViewComicActivity.java b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ViewComicActivity.java
new file mode 100644
index 0000000..abbf835
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ViewComicActivity.java
@@ -0,0 +1,321 @@
+package org.fox.ttcomics2;
+
+
+import android.annotation.SuppressLint;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.res.Configuration;
+import android.net.Uri;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.v4.app.FragmentTransaction;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.NumberPicker;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class ViewComicActivity extends CommonActivity {
+ private final String TAG = this.getClass().getSimpleName();
+
+ private String m_fileName;
+ private String m_tmpFileName;
+
+ @SuppressLint("NewApi")
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ m_prefs = PreferenceManager
+ .getDefaultSharedPreferences(getApplicationContext());
+
+ setTheme(m_prefs.getBoolean("use_dark_theme", false) ? R.style.ViewDarkTheme : R.style.ViewLightTheme);
+
+ super.onCreate(savedInstanceState);
+
+ if (m_prefs.getBoolean("prevent_screen_sleep", false)) {
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
+ }
+
+ if (m_prefs.getBoolean("use_full_screen", false)) {
+ hideSystemUI(true);
+ }
+
+ setContentView(R.layout.activity_view_comic);
+
+ if (savedInstanceState == null) {
+ m_fileName = getIntent().getStringExtra("fileName");
+
+ ComicPager cp = new ComicPager();
+ cp.setFileName(m_fileName);
+
+ FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
+ ft.replace(R.id.comics_pager_container, cp, FRAG_COMICS_PAGER);
+ ft.commit();
+ } else {
+ m_fileName = savedInstanceState.getString("fileName");
+ m_tmpFileName = savedInstanceState.getString("tmpFileName");
+ }
+
+ setOrientationLock(isOrientationLocked(), true);
+
+ getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+ setTitle(new File(m_fileName).getName());
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.activity_view_comic, menu);
+
+ menu.findItem(R.id.menu_sync_location).setVisible(m_prefs.getBoolean("use_position_sync", false) && m_syncClient.hasOwner());
+
+ return true;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle out) {
+ super.onSaveInstanceState(out);
+
+ out.putString("fileName", m_fileName);
+ out.putString("tmpFileName", m_tmpFileName);
+ }
+
+ @Override
+ public void onComicSelected(String fileName, int position) {
+ super.onComicSelected(fileName, position);
+ }
+
+ public void onPause() {
+ super.onPause();
+
+ // upload progress
+ if (m_prefs.getBoolean("use_position_sync", false) && m_syncClient.hasOwner()) {
+ //toast(R.string.sync_uploading);
+ m_syncClient.setPosition(sha1(new File(m_fileName).getName()), getLastPosition(m_fileName));
+ }
+ }
+
+ private void shareComic() {
+
+ ComicPager pager = (ComicPager) getSupportFragmentManager().findFragmentByTag(FRAG_COMICS_PAGER);
+
+ if (pager != null) {
+
+ try {
+ File tmpFile = File.createTempFile("trcshare" + sha1(m_fileName + " " + pager.getPosition()), ".jpg", getExternalCacheDir());
+
+ Log.d(TAG, "FILE=" + tmpFile);
+
+ InputStream is = pager.getArchive().getItem(pager.getPosition());
+
+ FileOutputStream fos = new FileOutputStream(tmpFile);
+
+ byte[] buffer = new byte[1024];
+ int len = 0;
+ while ((len = is.read(buffer)) != -1) {
+ fos.write(buffer, 0, len);
+ }
+
+ fos.close();
+ is.close();
+
+ Intent shareIntent = new Intent(Intent.ACTION_SEND);
+
+ shareIntent.setType("image/jpeg");
+ shareIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(tmpFile));
+
+ m_tmpFileName = tmpFile.getAbsolutePath();
+
+ startActivityForResult(Intent.createChooser(shareIntent, getString(R.string.share_comic)), REQUEST_SHARE);
+
+ } catch (IOException e) {
+ toast(getString(R.string.error_could_not_prepare_file_for_sharing));
+ e.printStackTrace();
+ }
+
+ }
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ if (requestCode == REQUEST_SHARE) {
+ File tmpFile = new File(m_tmpFileName);
+
+ if (tmpFile.exists()) {
+ tmpFile.delete();
+ }
+
+ }
+ super.onActivityResult(requestCode, resultCode, intent);
+ }
+
+ protected boolean isOrientationLocked() {
+ return m_prefs.getBoolean("prefs_lock_orientation", false);
+ }
+
+ private void setOrientationLock(boolean locked, boolean restoreLast) {
+ if (locked) {
+
+ int currentOrientation = restoreLast ? m_prefs.getInt("last_orientation", getResources().getConfiguration().orientation) :
+ getResources().getConfiguration().orientation;
+
+ if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
+ } else {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
+ }
+ } else {
+ setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR);
+ }
+
+ if (locked != isOrientationLocked()) {
+ SharedPreferences.Editor editor = m_prefs.edit();
+ editor.putBoolean("prefs_lock_orientation", locked);
+ editor.putInt("last_orientation", getResources().getConfiguration().orientation);
+ editor.commit();
+ }
+ }
+
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_share:
+ shareComic();
+ return true;
+ case R.id.menu_toggle_orientation_lock:
+ setOrientationLock(!isOrientationLocked(), false);
+ return true;
+ case R.id.menu_sync_location:
+ m_syncClient.getPosition(sha1(new File(m_fileName).getName()), new SyncClient.PositionReceivedListener() {
+ @Override
+ public void onPositionReceived(final int position) {
+ final ComicPager pager = (ComicPager) getSupportFragmentManager().findFragmentByTag(FRAG_COMICS_PAGER);
+
+ if (pager != null && pager.isAdded()) {
+ int localPosition = pager.getPosition();
+
+ if (position > localPosition) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(ViewComicActivity.this);
+ builder.setMessage(getString(R.string.sync_server_has_further_page, localPosition+1, position+1))
+ .setCancelable(true)
+ .setPositiveButton(R.string.dialog_open_page, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ pager.setCurrentItem(position);
+ }
+ })
+ .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int id) {
+ dialog.cancel();
+ }
+ });
+ AlertDialog alert = builder.create();
+ alert.show();
+
+ } else {
+ toast(R.string.error_sync_no_data);
+ }
+
+ }
+ }
+ });
+ return true;
+ case R.id.menu_go_location:
+ Dialog dialog = new Dialog(ViewComicActivity.this);
+ AlertDialog.Builder builder = new AlertDialog.Builder(ViewComicActivity.this)
+ .setTitle("Go to...")
+ .setItems(
+ new String[] {
+ getString(R.string.dialog_location_beginning),
+ getString(R.string.dialog_location_furthest),
+ getString(R.string.dialog_location_location),
+ getString(R.string.dialog_location_end)
+ },
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog,
+ int which) {
+
+ final ComicPager cp = (ComicPager) getSupportFragmentManager().findFragmentByTag(CommonActivity.FRAG_COMICS_PAGER);
+
+ switch (which) {
+ case 0:
+ cp.setCurrentItem(0);
+ break;
+ case 1:
+ cp.setCurrentItem(getMaxPosition(m_fileName));
+ break;
+ case 2:
+ if (true) {
+ LayoutInflater inflater = getLayoutInflater();
+ View contentView = inflater.inflate(R.layout.dialog_location, null);
+
+ final NumberPicker picker = (NumberPicker) contentView.findViewById(R.id.number_picker);
+
+ picker.setMinValue(1);
+ picker.setMaxValue(getSize(m_fileName));
+ picker.setValue(cp.getPosition() + 1);
+
+ Dialog seekDialog = new Dialog(ViewComicActivity.this);
+ AlertDialog.Builder seekBuilder = new AlertDialog.Builder(ViewComicActivity.this)
+ .setTitle(R.string.dialog_open_location)
+ .setView(contentView)
+ .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+ }
+ }).setPositiveButton(R.string.dialog_open_location, new DialogInterface.OnClickListener() {
+
+ public void onClick(DialogInterface dialog, int which) {
+ dialog.cancel();
+
+ cp.setCurrentItem(picker.getValue()-1);
+
+ }
+ });
+
+ seekDialog = seekBuilder.create();
+ seekDialog.show();
+ }
+
+ break;
+ case 3:
+ cp.setCurrentItem(cp.getCount() - 1);
+ break;
+ }
+
+ dialog.cancel();
+ }
+ });
+
+ dialog = builder.create();
+ dialog.show();
+
+ return true;
+ case android.R.id.home:
+ finish();
+ return true;
+ default:
+ Log.d(TAG,
+ "onOptionsItemSelected, unhandled id=" + item.getItemId());
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ public void hideSeekBar(boolean hide) {
+ ComicPager pager = (ComicPager) getSupportFragmentManager().findFragmentByTag(FRAG_COMICS_PAGER);
+
+ if (pager != null) {
+ pager.hideSeekBar(hide);
+ }
+ }
+
+}
diff --git a/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ViewPager.java b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ViewPager.java
new file mode 100644
index 0000000..91feea1
--- /dev/null
+++ b/org.fox.ttcomics/src/main/java/org/fox/ttcomics2/ViewPager.java
@@ -0,0 +1,38 @@
+package org.fox.ttcomics2;
+
+import it.sephiroth.android.library.imagezoom.ImageViewTouch;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+public class ViewPager extends android.support.v4.view.ViewPager {
+ public ViewPager(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
+ if (v instanceof ImageViewTouch) {
+ ImageViewTouch ivt = (ImageViewTouch) v;
+ try {
+ return ivt.canScroll(dx);
+ } catch (NullPointerException e) {
+ // bad image, etc
+ return false;
+ }
+ } else {
+ return super.canScroll(v, checkV, dx, x, y);
+ }
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ return super.onTouchEvent(event);
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent event) {
+ return super.onInterceptTouchEvent(event);
+ }
+} \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/res/anim/appear.xml b/org.fox.ttcomics/src/main/res/anim/appear.xml
new file mode 100644
index 0000000..a60e055
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/anim/appear.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<set xmlns:android="http://schemas.android.com/apk/res/android" android:interpolator="@android:anim/accelerate_interpolator">
+ <alpha
+ android:fromAlpha="0"
+ android:toAlpha="1"
+ android:duration="150"
+ />
+</set>
+
diff --git a/org.fox.ttcomics/src/main/res/drawable-hdpi/badimage.png b/org.fox.ttcomics/src/main/res/drawable-hdpi/badimage.png
new file mode 100644
index 0000000..84facc9
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/drawable-hdpi/badimage.png
Binary files differ
diff --git a/org.fox.ttcomics/src/main/res/drawable-hdpi/comic_grid_footer.png b/org.fox.ttcomics/src/main/res/drawable-hdpi/comic_grid_footer.png
new file mode 100755
index 0000000..c387044
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/drawable-hdpi/comic_grid_footer.png
Binary files differ
diff --git a/org.fox.ttcomics/src/main/res/drawable-hdpi/ic_action_overflow.png b/org.fox.ttcomics/src/main/res/drawable-hdpi/ic_action_overflow.png
new file mode 100644
index 0000000..002fc4b
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/drawable-hdpi/ic_action_overflow.png
Binary files differ
diff --git a/org.fox.ttcomics/src/main/res/drawable-hdpi/ic_launcher.png b/org.fox.ttcomics/src/main/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 0000000..e5a62ee
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/org.fox.ttcomics/src/main/res/drawable-hdpi/ic_refresh_light.png b/org.fox.ttcomics/src/main/res/drawable-hdpi/ic_refresh_light.png
new file mode 100644
index 0000000..bb9d855
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/drawable-hdpi/ic_refresh_light.png
Binary files differ
diff --git a/org.fox.ttcomics/src/main/res/drawable-hdpi/ic_settings.png b/org.fox.ttcomics/src/main/res/drawable-hdpi/ic_settings.png
new file mode 100644
index 0000000..3e4580e
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/drawable-hdpi/ic_settings.png
Binary files differ
diff --git a/org.fox.ttcomics/src/main/res/drawable-hdpi/ic_share_light.png b/org.fox.ttcomics/src/main/res/drawable-hdpi/ic_share_light.png
new file mode 100644
index 0000000..c329f58
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/drawable-hdpi/ic_share_light.png
Binary files differ
diff --git a/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_action_good.png b/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_action_good.png
new file mode 100644
index 0000000..49b85ca
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_action_good.png
Binary files differ
diff --git a/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_action_overflow.png b/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_action_overflow.png
new file mode 100644
index 0000000..cfe1287
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_action_overflow.png
Binary files differ
diff --git a/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_launcher.png b/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..6904f59
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_refresh_light.png b/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_refresh_light.png
new file mode 100644
index 0000000..a7fdc0d
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_refresh_light.png
Binary files differ
diff --git a/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_search_light.png b/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_search_light.png
new file mode 100644
index 0000000..3549f84
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_search_light.png
Binary files differ
diff --git a/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_settings.png b/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_settings.png
new file mode 100644
index 0000000..09b0148
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_settings.png
Binary files differ
diff --git a/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_share_light.png b/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_share_light.png
new file mode 100644
index 0000000..15549b0
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/drawable-xhdpi/ic_share_light.png
Binary files differ
diff --git a/org.fox.ttcomics/src/main/res/drawable-xxhdpi/ic_launcher.png b/org.fox.ttcomics/src/main/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f05d2
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/org.fox.ttcomics/src/main/res/drawable/s_badimage.svg b/org.fox.ttcomics/src/main/res/drawable/s_badimage.svg
new file mode 100644
index 0000000..010d5d1
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/drawable/s_badimage.svg
@@ -0,0 +1,519 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ inkscape:export-ydpi="696"
+ inkscape:export-xdpi="696"
+ inkscape:export-filename="C:\Users\fox\Documents\Projects\Tiny-Comics-Reader\org.fox.ttcomics\src\main\res\drawable-hdpi\badimage.png"
+ sodipodi:docname="s_badimage.svg"
+ inkscape:version="0.91 r13725"
+ version="1.1"
+ id="svg2"
+ height="96"
+ width="96">
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient3799">
+ <stop
+ style="stop-color:#ff0000;stop-opacity:1"
+ offset="0"
+ id="stop3801" />
+ <stop
+ style="stop-color:#ffdd00;stop-opacity:1"
+ offset="1"
+ id="stop3803" />
+ </linearGradient>
+ <filter
+ id="filter3007"
+ inkscape:label="Drop shadow"
+ width="1.5"
+ height="1.5"
+ x="-.25"
+ y="-.25">
+ <feGaussianBlur
+ id="feGaussianBlur3009"
+ in="SourceAlpha"
+ stdDeviation="2"
+ result="blur" />
+ <feColorMatrix
+ id="feColorMatrix3011"
+ result="bluralpha"
+ type="matrix"
+ values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.75 0 " />
+ <feOffset
+ id="feOffset3013"
+ in="bluralpha"
+ dx="1"
+ dy="1"
+ result="offsetBlur" />
+ <feMerge
+ id="feMerge3015">
+ <feMergeNode
+ id="feMergeNode3017"
+ in="offsetBlur" />
+ <feMergeNode
+ id="feMergeNode3019"
+ in="SourceGraphic" />
+ </feMerge>
+ </filter>
+ <filter
+ id="filter4167"
+ inkscape:label="Colorize"
+ inkscape:menu="Color"
+ inkscape:menu-tooltip="Blend image or object with a flood color and set lightness and contrast"
+ x="0"
+ y="0"
+ width="1"
+ height="1"
+ color-interpolation-filters="sRGB">
+ <feColorMatrix
+ id="feColorMatrix4169"
+ values="1"
+ in="SourceGraphic"
+ type="saturate"
+ result="result2" />
+ <feFlood
+ id="feFlood4171"
+ flood-color="rgb(254,102,0)"
+ flood-opacity="1"
+ result="result1" />
+ <feBlend
+ id="feBlend4173"
+ in2="result2"
+ mode="multiply"
+ in="result1"
+ result="result3" />
+ <feComposite
+ id="feComposite4175"
+ in2="SourceGraphic"
+ operator="in"
+ k2="1"
+ result="result4" />
+ </filter>
+ <filter
+ style="color-interpolation-filters:sRGB;"
+ inkscape:label="Drop Shadow"
+ id="filter4404">
+ <feFlood
+ flood-opacity="0.498039"
+ flood-color="rgb(0,0,0)"
+ result="flood"
+ id="feFlood4406" />
+ <feComposite
+ in="flood"
+ in2="SourceGraphic"
+ operator="in"
+ result="composite1"
+ id="feComposite4408" />
+ <feGaussianBlur
+ in="composite1"
+ stdDeviation="0.5"
+ result="blur"
+ id="feGaussianBlur4410" />
+ <feOffset
+ dx="1"
+ dy="1"
+ result="offset"
+ id="feOffset4412" />
+ <feComposite
+ in="SourceGraphic"
+ in2="offset"
+ operator="over"
+ result="composite2"
+ id="feComposite4414" />
+ </filter>
+ <filter
+ style="color-interpolation-filters:sRGB;"
+ inkscape:label="Drop Shadow"
+ id="filter4416">
+ <feFlood
+ flood-opacity="0.498039"
+ flood-color="rgb(0,0,0)"
+ result="flood"
+ id="feFlood4418" />
+ <feComposite
+ in="flood"
+ in2="SourceGraphic"
+ operator="in"
+ result="composite1"
+ id="feComposite4420" />
+ <feGaussianBlur
+ in="composite1"
+ stdDeviation="0.5"
+ result="blur"
+ id="feGaussianBlur4422" />
+ <feOffset
+ dx="1"
+ dy="1"
+ result="offset"
+ id="feOffset4424" />
+ <feComposite
+ in="SourceGraphic"
+ in2="offset"
+ operator="over"
+ result="fbSourceGraphic"
+ id="feComposite4426" />
+ <feColorMatrix
+ result="fbSourceGraphicAlpha"
+ in="fbSourceGraphic"
+ values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"
+ id="feColorMatrix4428" />
+ <feFlood
+ id="feFlood4430"
+ flood-opacity="0.498039"
+ flood-color="rgb(0,0,0)"
+ result="flood"
+ in="fbSourceGraphic" />
+ <feComposite
+ id="feComposite4432"
+ in2="fbSourceGraphic"
+ in="flood"
+ operator="in"
+ result="composite1" />
+ <feGaussianBlur
+ id="feGaussianBlur4434"
+ in="composite1"
+ stdDeviation="0.5"
+ result="blur" />
+ <feOffset
+ id="feOffset4436"
+ dx="1"
+ dy="1"
+ result="offset" />
+ <feComposite
+ id="feComposite4438"
+ in2="offset"
+ in="fbSourceGraphic"
+ operator="over"
+ result="composite2" />
+ </filter>
+ <filter
+ style="color-interpolation-filters:sRGB;"
+ inkscape:label="Drop Shadow"
+ id="filter4440">
+ <feFlood
+ flood-opacity="0.498039"
+ flood-color="rgb(0,0,0)"
+ result="flood"
+ id="feFlood4442" />
+ <feComposite
+ in="flood"
+ in2="SourceGraphic"
+ operator="in"
+ result="composite1"
+ id="feComposite4444" />
+ <feGaussianBlur
+ in="composite1"
+ stdDeviation="0.5"
+ result="blur"
+ id="feGaussianBlur4446" />
+ <feOffset
+ dx="1"
+ dy="1"
+ result="offset"
+ id="feOffset4448" />
+ <feComposite
+ in="SourceGraphic"
+ in2="offset"
+ operator="over"
+ result="composite2"
+ id="feComposite4450" />
+ </filter>
+ <filter
+ style="color-interpolation-filters:sRGB;"
+ inkscape:label="Drop Shadow"
+ id="filter4857">
+ <feFlood
+ flood-opacity="0.498039"
+ flood-color="rgb(0,0,0)"
+ result="flood"
+ id="feFlood4859" />
+ <feComposite
+ in="flood"
+ in2="SourceGraphic"
+ operator="in"
+ result="composite1"
+ id="feComposite4861" />
+ <feGaussianBlur
+ in="composite1"
+ stdDeviation="3"
+ result="blur"
+ id="feGaussianBlur4863" />
+ <feOffset
+ dx="1"
+ dy="1"
+ result="offset"
+ id="feOffset4865" />
+ <feComposite
+ in="SourceGraphic"
+ in2="offset"
+ operator="over"
+ result="composite2"
+ id="feComposite4867" />
+ </filter>
+ <filter
+ style="color-interpolation-filters:sRGB;"
+ inkscape:label="Drop Shadow"
+ id="filter4869">
+ <feFlood
+ flood-opacity="0.498039"
+ flood-color="rgb(0,0,0)"
+ result="flood"
+ id="feFlood4871" />
+ <feComposite
+ in="flood"
+ in2="SourceGraphic"
+ operator="in"
+ result="composite1"
+ id="feComposite4873" />
+ <feGaussianBlur
+ in="composite1"
+ stdDeviation="3"
+ result="blur"
+ id="feGaussianBlur4875" />
+ <feOffset
+ dx="1"
+ dy="1"
+ result="offset"
+ id="feOffset4877" />
+ <feComposite
+ in="SourceGraphic"
+ in2="offset"
+ operator="over"
+ result="composite2"
+ id="feComposite4879" />
+ </filter>
+ <filter
+ style="color-interpolation-filters:sRGB;"
+ inkscape:label="Drop Shadow"
+ id="filter4881">
+ <feFlood
+ flood-opacity="0.498039"
+ flood-color="rgb(0,0,0)"
+ result="flood"
+ id="feFlood4883" />
+ <feComposite
+ in="flood"
+ in2="SourceGraphic"
+ operator="in"
+ result="composite1"
+ id="feComposite4885" />
+ <feGaussianBlur
+ in="composite1"
+ stdDeviation="3"
+ result="blur"
+ id="feGaussianBlur4887" />
+ <feOffset
+ dx="1"
+ dy="1"
+ result="offset"
+ id="feOffset4889" />
+ <feComposite
+ in="SourceGraphic"
+ in2="offset"
+ operator="over"
+ result="composite2"
+ id="feComposite4891" />
+ </filter>
+ <filter
+ style="color-interpolation-filters:sRGB;"
+ inkscape:label="Drop Shadow"
+ id="filter5007">
+ <feFlood
+ flood-opacity="0.321569"
+ flood-color="rgb(0,0,0)"
+ result="flood"
+ id="feFlood5009" />
+ <feComposite
+ in="flood"
+ in2="SourceGraphic"
+ operator="in"
+ result="composite1"
+ id="feComposite5011" />
+ <feGaussianBlur
+ in="composite1"
+ stdDeviation="3"
+ result="blur"
+ id="feGaussianBlur5013" />
+ <feOffset
+ dx="1"
+ dy="1"
+ result="offset"
+ id="feOffset5015" />
+ <feComposite
+ in="offset"
+ in2="offset"
+ operator="atop"
+ result="composite2"
+ id="feComposite5017" />
+ </filter>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3799"
+ id="linearGradient6959"
+ x1="-72.715965"
+ y1="990.30542"
+ x2="-71.995934"
+ y2="1013.8391"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3799"
+ id="linearGradient49795"
+ x1="-72.715965"
+ y1="990.30542"
+ x2="-71.995934"
+ y2="1013.8391"
+ gradientUnits="userSpaceOnUse" />
+ </defs>
+ <sodipodi:namedview
+ inkscape:snap-global="false"
+ inkscape:snap-nodes="false"
+ inkscape:window-maximized="1"
+ inkscape:window-y="-8"
+ inkscape:window-x="-8"
+ inkscape:window-height="1137"
+ inkscape:window-width="1920"
+ inkscape:guide-bbox="true"
+ showguides="true"
+ showgrid="false"
+ inkscape:current-layer="svg2"
+ inkscape:document-units="px"
+ inkscape:cy="37.454658"
+ inkscape:cx="-18.732881"
+ inkscape:zoom="3.959798"
+ inkscape:pageshadow="2"
+ inkscape:pageopacity="0"
+ borderopacity="1.0"
+ bordercolor="#666666"
+ pagecolor="#ffffff"
+ id="base">
+ <sodipodi:guide
+ orientation="1,0"
+ position="7.5761441,54.043161"
+ id="guide2993" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="54.548238,88.13581"
+ id="guide2995" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="88.388348,62.881996"
+ id="guide2997" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="59.599,8.0812204"
+ id="guide2999" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-956.36218)">
+ <path
+ inkscape:transform-center-y="-0.78624016"
+ inkscape:transform-center-x="-0.38354235"
+ d="m 79.59389,1025.628 -14.797549,-0.5906 -1.802635,14.6992 -12.129199,-8.497 -9.463466,11.3912 -5.609914,-13.7056 -14.119712,4.4665 2.690479,-14.5629 -14.29305,-3.8762 10.136663,-10.7965 -9.928445,-10.98825 14.364529,-3.6023 -2.411629,-14.61165 14.031759,4.73561 5.870861,-13.59592 9.244004,11.56998 12.289396,-8.26359 1.521342,14.73098 14.806134,-0.30763 -6.684334,13.21499 12.622029,7.74598 -12.767782,7.5034 z"
+ inkscape:randomized="0"
+ inkscape:rounded="0"
+ inkscape:flatsided="false"
+ sodipodi:arg2="0.86635568"
+ sodipodi:arg1="0.58075635"
+ sodipodi:r2="27.064737"
+ sodipodi:r1="38.66391"
+ sodipodi:cy="1004.4148"
+ sodipodi:cx="47.269009"
+ sodipodi:sides="11"
+ id="path49973"
+ style="opacity:1;fill:#ff0000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ sodipodi:type="star"
+ transform="matrix(0.85439132,-0.05510671,0.05510671,0.85439132,-48.457117,148.91105)" />
+ <path
+ sodipodi:type="star"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path49971"
+ sodipodi:sides="11"
+ sodipodi:cx="47.269009"
+ sodipodi:cy="1004.4148"
+ sodipodi:r1="38.66391"
+ sodipodi:r2="27.064737"
+ sodipodi:arg1="0.58075635"
+ sodipodi:arg2="0.86635568"
+ inkscape:flatsided="false"
+ inkscape:rounded="0"
+ inkscape:randomized="0"
+ d="m 79.59389,1025.628 -14.797549,-0.5906 -1.802635,14.6992 -12.129199,-8.497 -9.463466,11.3912 -5.609914,-13.7056 -14.119712,4.4665 2.690479,-14.5629 -14.29305,-3.8762 10.136663,-10.7965 -9.928445,-10.98825 14.364529,-3.6023 -2.411629,-14.61165 14.031759,4.73561 5.870861,-13.59592 9.244004,11.56998 12.289396,-8.26359 1.521342,14.73098 14.806134,-0.30763 -6.684334,13.21499 12.622029,7.74598 -12.767782,7.5034 z"
+ inkscape:transform-center-x="0.14286093"
+ inkscape:transform-center-y="-0.18702017"
+ transform="matrix(0.84435138,-0.1417463,0.1417463,0.84435138,-135.0647,163.12814)" />
+ <path
+ transform="matrix(0.67017835,-0.11250683,0.11250683,0.67017835,-97.447266,336.85142)"
+ inkscape:transform-center-y="-0.14843901"
+ inkscape:transform-center-x="0.1133964"
+ d="m 79.59389,1025.628 -14.797549,-0.5906 -1.802635,14.6992 -12.129199,-8.497 -9.463466,11.3912 -5.609914,-13.7056 -14.119712,4.4665 2.690479,-14.5629 -14.29305,-3.8762 10.136663,-10.7965 -9.928445,-10.98825 14.364529,-3.6023 -2.411629,-14.61165 14.031759,4.73561 5.870861,-13.59592 9.244004,11.56998 12.289396,-8.26359 1.521342,14.73098 14.806134,-0.30763 -6.684334,13.21499 12.622029,7.74598 -12.767782,7.5034 z"
+ inkscape:randomized="0"
+ inkscape:rounded="0"
+ inkscape:flatsided="false"
+ sodipodi:arg2="0.86635568"
+ sodipodi:arg1="0.58075635"
+ sodipodi:r2="27.064737"
+ sodipodi:r1="38.66391"
+ sodipodi:cy="1004.4148"
+ sodipodi:cx="47.269009"
+ sodipodi:sides="11"
+ id="path50018"
+ style="opacity:1;fill:#ffea92;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ sodipodi:type="star" />
+ <g
+ id="g49985"
+ transform="translate(31.067894,-5.7610247)">
+ <text
+ transform="matrix(0.99123282,-0.13212683,0.13212683,0.99123282,0,0)"
+ sodipodi:linespacing="127%"
+ id="text3801-2"
+ y="1016.485"
+ x="-126.51662"
+ style="font-style:normal;font-weight:normal;font-size:36.53679657px;line-height:126.99999809%;font-family:Sans;letter-spacing:0px;word-spacing:0px;opacity:1;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ xml:space="preserve"><tspan
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:28px;line-height:126.99999809%;font-family:'BD Cartoon Shout';-inkscape-font-specification:'BD Cartoon Shout';fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ y="1016.485"
+ x="-126.51662"
+ id="tspan3803-4"
+ sodipodi:role="line">?</tspan></text>
+ <text
+ transform="matrix(0.99123282,-0.13212683,0.13212683,0.99123282,0,0)"
+ sodipodi:linespacing="127%"
+ id="text3801"
+ y="1014.6411"
+ x="-128.74716"
+ style="font-style:normal;font-weight:normal;font-size:36.53679657px;line-height:126.99999809%;font-family:Sans;letter-spacing:0px;word-spacing:0px;opacity:1;fill:url(#linearGradient6959);fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ xml:space="preserve"><tspan
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:28px;line-height:126.99999809%;font-family:'BD Cartoon Shout';-inkscape-font-specification:'BD Cartoon Shout';fill:url(#linearGradient6959);fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ y="1014.6411"
+ x="-128.74716"
+ id="tspan3803"
+ sodipodi:role="line">?</tspan></text>
+ </g>
+ </g>
+</svg>
diff --git a/org.fox.ttcomics/src/main/res/drawable/s_launcher.svg b/org.fox.ttcomics/src/main/res/drawable/s_launcher.svg
new file mode 100644
index 0000000..4b90414
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/drawable/s_launcher.svg
@@ -0,0 +1,523 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!-- Created with Inkscape (http://www.inkscape.org/) -->
+
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ width="96"
+ height="96"
+ id="svg2"
+ version="1.1"
+ inkscape:version="0.91 r13725"
+ sodipodi:docname="s_launcher.svg"
+ inkscape:export-filename="C:\Users\fox\Documents\Projects\Tiny-Comics-Reader\org.fox.ttcomics\src\main\res\drawable-xxhdpi\ic_launcher.png"
+ inkscape:export-xdpi="135"
+ inkscape:export-ydpi="135">
+ <defs
+ id="defs4">
+ <linearGradient
+ id="linearGradient3799">
+ <stop
+ style="stop-color:#ff0000;stop-opacity:1"
+ offset="0"
+ id="stop3801" />
+ <stop
+ style="stop-color:#ffdd00;stop-opacity:1"
+ offset="1"
+ id="stop3803" />
+ </linearGradient>
+ <filter
+ id="filter3007"
+ inkscape:label="Drop shadow"
+ width="1.5"
+ height="1.5"
+ x="-.25"
+ y="-.25">
+ <feGaussianBlur
+ id="feGaussianBlur3009"
+ in="SourceAlpha"
+ stdDeviation="2"
+ result="blur" />
+ <feColorMatrix
+ id="feColorMatrix3011"
+ result="bluralpha"
+ type="matrix"
+ values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 0.75 0 " />
+ <feOffset
+ id="feOffset3013"
+ in="bluralpha"
+ dx="1"
+ dy="1"
+ result="offsetBlur" />
+ <feMerge
+ id="feMerge3015">
+ <feMergeNode
+ id="feMergeNode3017"
+ in="offsetBlur" />
+ <feMergeNode
+ id="feMergeNode3019"
+ in="SourceGraphic" />
+ </feMerge>
+ </filter>
+ <filter
+ id="filter4167"
+ inkscape:label="Colorize"
+ inkscape:menu="Color"
+ inkscape:menu-tooltip="Blend image or object with a flood color and set lightness and contrast"
+ x="0"
+ y="0"
+ width="1"
+ height="1"
+ color-interpolation-filters="sRGB">
+ <feColorMatrix
+ id="feColorMatrix4169"
+ values="1"
+ in="SourceGraphic"
+ type="saturate"
+ result="result2" />
+ <feFlood
+ id="feFlood4171"
+ flood-color="rgb(254,102,0)"
+ flood-opacity="1"
+ result="result1" />
+ <feBlend
+ id="feBlend4173"
+ in2="result2"
+ mode="multiply"
+ in="result1"
+ result="result3" />
+ <feComposite
+ id="feComposite4175"
+ in2="SourceGraphic"
+ operator="in"
+ k2="1"
+ result="result4" />
+ </filter>
+ <filter
+ style="color-interpolation-filters:sRGB;"
+ inkscape:label="Drop Shadow"
+ id="filter4404">
+ <feFlood
+ flood-opacity="0.498039"
+ flood-color="rgb(0,0,0)"
+ result="flood"
+ id="feFlood4406" />
+ <feComposite
+ in="flood"
+ in2="SourceGraphic"
+ operator="in"
+ result="composite1"
+ id="feComposite4408" />
+ <feGaussianBlur
+ in="composite1"
+ stdDeviation="0.5"
+ result="blur"
+ id="feGaussianBlur4410" />
+ <feOffset
+ dx="1"
+ dy="1"
+ result="offset"
+ id="feOffset4412" />
+ <feComposite
+ in="SourceGraphic"
+ in2="offset"
+ operator="over"
+ result="composite2"
+ id="feComposite4414" />
+ </filter>
+ <filter
+ style="color-interpolation-filters:sRGB;"
+ inkscape:label="Drop Shadow"
+ id="filter4416">
+ <feFlood
+ flood-opacity="0.498039"
+ flood-color="rgb(0,0,0)"
+ result="flood"
+ id="feFlood4418" />
+ <feComposite
+ in="flood"
+ in2="SourceGraphic"
+ operator="in"
+ result="composite1"
+ id="feComposite4420" />
+ <feGaussianBlur
+ in="composite1"
+ stdDeviation="0.5"
+ result="blur"
+ id="feGaussianBlur4422" />
+ <feOffset
+ dx="1"
+ dy="1"
+ result="offset"
+ id="feOffset4424" />
+ <feComposite
+ in="SourceGraphic"
+ in2="offset"
+ operator="over"
+ result="fbSourceGraphic"
+ id="feComposite4426" />
+ <feColorMatrix
+ result="fbSourceGraphicAlpha"
+ in="fbSourceGraphic"
+ values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"
+ id="feColorMatrix4428" />
+ <feFlood
+ id="feFlood4430"
+ flood-opacity="0.498039"
+ flood-color="rgb(0,0,0)"
+ result="flood"
+ in="fbSourceGraphic" />
+ <feComposite
+ id="feComposite4432"
+ in2="fbSourceGraphic"
+ in="flood"
+ operator="in"
+ result="composite1" />
+ <feGaussianBlur
+ id="feGaussianBlur4434"
+ in="composite1"
+ stdDeviation="0.5"
+ result="blur" />
+ <feOffset
+ id="feOffset4436"
+ dx="1"
+ dy="1"
+ result="offset" />
+ <feComposite
+ id="feComposite4438"
+ in2="offset"
+ in="fbSourceGraphic"
+ operator="over"
+ result="composite2" />
+ </filter>
+ <filter
+ style="color-interpolation-filters:sRGB;"
+ inkscape:label="Drop Shadow"
+ id="filter4440">
+ <feFlood
+ flood-opacity="0.498039"
+ flood-color="rgb(0,0,0)"
+ result="flood"
+ id="feFlood4442" />
+ <feComposite
+ in="flood"
+ in2="SourceGraphic"
+ operator="in"
+ result="composite1"
+ id="feComposite4444" />
+ <feGaussianBlur
+ in="composite1"
+ stdDeviation="0.5"
+ result="blur"
+ id="feGaussianBlur4446" />
+ <feOffset
+ dx="1"
+ dy="1"
+ result="offset"
+ id="feOffset4448" />
+ <feComposite
+ in="SourceGraphic"
+ in2="offset"
+ operator="over"
+ result="composite2"
+ id="feComposite4450" />
+ </filter>
+ <filter
+ style="color-interpolation-filters:sRGB;"
+ inkscape:label="Drop Shadow"
+ id="filter4857">
+ <feFlood
+ flood-opacity="0.498039"
+ flood-color="rgb(0,0,0)"
+ result="flood"
+ id="feFlood4859" />
+ <feComposite
+ in="flood"
+ in2="SourceGraphic"
+ operator="in"
+ result="composite1"
+ id="feComposite4861" />
+ <feGaussianBlur
+ in="composite1"
+ stdDeviation="3"
+ result="blur"
+ id="feGaussianBlur4863" />
+ <feOffset
+ dx="1"
+ dy="1"
+ result="offset"
+ id="feOffset4865" />
+ <feComposite
+ in="SourceGraphic"
+ in2="offset"
+ operator="over"
+ result="composite2"
+ id="feComposite4867" />
+ </filter>
+ <filter
+ style="color-interpolation-filters:sRGB;"
+ inkscape:label="Drop Shadow"
+ id="filter4869">
+ <feFlood
+ flood-opacity="0.498039"
+ flood-color="rgb(0,0,0)"
+ result="flood"
+ id="feFlood4871" />
+ <feComposite
+ in="flood"
+ in2="SourceGraphic"
+ operator="in"
+ result="composite1"
+ id="feComposite4873" />
+ <feGaussianBlur
+ in="composite1"
+ stdDeviation="3"
+ result="blur"
+ id="feGaussianBlur4875" />
+ <feOffset
+ dx="1"
+ dy="1"
+ result="offset"
+ id="feOffset4877" />
+ <feComposite
+ in="SourceGraphic"
+ in2="offset"
+ operator="over"
+ result="composite2"
+ id="feComposite4879" />
+ </filter>
+ <filter
+ style="color-interpolation-filters:sRGB;"
+ inkscape:label="Drop Shadow"
+ id="filter4881">
+ <feFlood
+ flood-opacity="0.498039"
+ flood-color="rgb(0,0,0)"
+ result="flood"
+ id="feFlood4883" />
+ <feComposite
+ in="flood"
+ in2="SourceGraphic"
+ operator="in"
+ result="composite1"
+ id="feComposite4885" />
+ <feGaussianBlur
+ in="composite1"
+ stdDeviation="3"
+ result="blur"
+ id="feGaussianBlur4887" />
+ <feOffset
+ dx="1"
+ dy="1"
+ result="offset"
+ id="feOffset4889" />
+ <feComposite
+ in="SourceGraphic"
+ in2="offset"
+ operator="over"
+ result="composite2"
+ id="feComposite4891" />
+ </filter>
+ <filter
+ style="color-interpolation-filters:sRGB;"
+ inkscape:label="Drop Shadow"
+ id="filter5007">
+ <feFlood
+ flood-opacity="0.321569"
+ flood-color="rgb(0,0,0)"
+ result="flood"
+ id="feFlood5009" />
+ <feComposite
+ in="flood"
+ in2="SourceGraphic"
+ operator="in"
+ result="composite1"
+ id="feComposite5011" />
+ <feGaussianBlur
+ in="composite1"
+ stdDeviation="3"
+ result="blur"
+ id="feGaussianBlur5013" />
+ <feOffset
+ dx="1"
+ dy="1"
+ result="offset"
+ id="feOffset5015" />
+ <feComposite
+ in="offset"
+ in2="offset"
+ operator="atop"
+ result="composite2"
+ id="feComposite5017" />
+ </filter>
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3799"
+ id="linearGradient6959"
+ x1="-72.715965"
+ y1="990.30542"
+ x2="-71.995934"
+ y2="1013.8391"
+ gradientUnits="userSpaceOnUse" />
+ <linearGradient
+ inkscape:collect="always"
+ xlink:href="#linearGradient3799"
+ id="linearGradient49795"
+ x1="-72.715965"
+ y1="990.30542"
+ x2="-71.995934"
+ y2="1013.8391"
+ gradientUnits="userSpaceOnUse" />
+ </defs>
+ <sodipodi:namedview
+ id="base"
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1.0"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:zoom="3.959798"
+ inkscape:cx="-18.732881"
+ inkscape:cy="37.454658"
+ inkscape:document-units="px"
+ inkscape:current-layer="layer1"
+ showgrid="false"
+ showguides="true"
+ inkscape:guide-bbox="true"
+ inkscape:window-width="1920"
+ inkscape:window-height="1137"
+ inkscape:window-x="-8"
+ inkscape:window-y="-8"
+ inkscape:window-maximized="1"
+ inkscape:snap-nodes="false"
+ inkscape:snap-global="false">
+ <sodipodi:guide
+ orientation="1,0"
+ position="7.5761441,54.043161"
+ id="guide2993" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="54.548238,88.13581"
+ id="guide2995" />
+ <sodipodi:guide
+ orientation="1,0"
+ position="88.388348,62.881996"
+ id="guide2997" />
+ <sodipodi:guide
+ orientation="0,1"
+ position="59.599,8.0812204"
+ id="guide2999" />
+ </sodipodi:namedview>
+ <metadata
+ id="metadata7">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title />
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <g
+ inkscape:label="Layer 1"
+ inkscape:groupmode="layer"
+ id="layer1"
+ transform="translate(0,-956.36218)">
+ <g
+ id="g4210"
+ transform="matrix(1.0817445,0,0,1.0817445,-3.8552003,-82.146781)">
+ <path
+ transform="matrix(0.88223715,-0.05690272,0.05690272,0.88223715,-51.64252,121.01245)"
+ sodipodi:type="star"
+ style="opacity:1;fill:#ff0000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.5;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path49973"
+ sodipodi:sides="11"
+ sodipodi:cx="47.269009"
+ sodipodi:cy="1004.4148"
+ sodipodi:r1="38.66391"
+ sodipodi:r2="27.064737"
+ sodipodi:arg1="0.58075635"
+ sodipodi:arg2="0.86635568"
+ inkscape:flatsided="false"
+ inkscape:rounded="0"
+ inkscape:randomized="0"
+ d="m 79.59389,1025.628 -14.797549,-0.5906 -1.802635,14.6992 -12.129199,-8.497 -9.463466,11.3912 -5.609914,-13.7056 -14.119712,4.4665 2.690479,-14.5629 -14.29305,-3.8762 10.136663,-10.7965 -9.928445,-10.98825 14.364529,-3.6023 -2.411629,-14.61165 14.031759,4.73561 5.870861,-13.59592 9.244004,11.56998 12.289396,-8.26359 1.521342,14.73098 14.806134,-0.30763 -6.684334,13.21499 12.622029,7.74598 -12.767782,7.5034 z"
+ inkscape:transform-center-x="-0.39604598"
+ inkscape:transform-center-y="-0.81186163" />
+ <path
+ transform="matrix(0.87186999,-0.14636601,0.14636601,0.87186999,-141.07277,135.69289)"
+ inkscape:transform-center-y="-0.19312114"
+ inkscape:transform-center-x="0.14752387"
+ d="m 79.59389,1025.628 -14.797549,-0.5906 -1.802635,14.6992 -12.129199,-8.497 -9.463466,11.3912 -5.609914,-13.7056 -14.119712,4.4665 2.690479,-14.5629 -14.29305,-3.8762 10.136663,-10.7965 -9.928445,-10.98825 14.364529,-3.6023 -2.411629,-14.61165 14.031759,4.73561 5.870861,-13.59592 9.244004,11.56998 12.289396,-8.26359 1.521342,14.73098 14.806134,-0.30763 -6.684334,13.21499 12.622029,7.74598 -12.767782,7.5034 z"
+ inkscape:randomized="0"
+ inkscape:rounded="0"
+ inkscape:flatsided="false"
+ sodipodi:arg2="0.86635568"
+ sodipodi:arg1="0.58075635"
+ sodipodi:r2="27.064737"
+ sodipodi:r1="38.66391"
+ sodipodi:cy="1004.4148"
+ sodipodi:cx="47.269009"
+ sodipodi:sides="11"
+ id="path49971"
+ style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ sodipodi:type="star" />
+ <path
+ sodipodi:type="star"
+ style="opacity:1;fill:#ffea92;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:butt;stroke-linejoin:bevel;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ id="path50018"
+ sodipodi:sides="11"
+ sodipodi:cx="47.269009"
+ sodipodi:cy="1004.4148"
+ sodipodi:r1="38.66391"
+ sodipodi:r2="27.064737"
+ sodipodi:arg1="0.58075635"
+ sodipodi:arg2="0.86635568"
+ inkscape:flatsided="false"
+ inkscape:rounded="0"
+ inkscape:randomized="0"
+ d="m 79.59389,1025.628 -14.797549,-0.5906 -1.802635,14.6992 -12.129199,-8.497 -9.463466,11.3912 -5.609914,-13.7056 -14.119712,4.4665 2.690479,-14.5629 -14.29305,-3.8762 10.136663,-10.7965 -9.928445,-10.98825 14.364529,-3.6023 -2.411629,-14.61165 14.031759,4.73561 5.870861,-13.59592 9.244004,11.56998 12.289396,-8.26359 1.521342,14.73098 14.806134,-0.30763 -6.684334,13.21499 12.622029,7.74598 -12.767782,7.5034 z"
+ inkscape:transform-center-x="0.1170925"
+ inkscape:transform-center-y="-0.15327484"
+ transform="matrix(0.69202042,-0.11617359,0.11617359,0.69202042,-102.22933,315.07806)" />
+ </g>
+ <g
+ id="g49985"
+ transform="matrix(1.0325914,0,0,1.0325914,-1.6061161,-32.751826)">
+ <text
+ transform="matrix(0.99123282,-0.13212683,0.13212683,0.99123282,0,0)"
+ sodipodi:linespacing="127%"
+ id="text3801-2"
+ y="1016.485"
+ x="-126.51662"
+ style="font-style:normal;font-weight:normal;font-size:36.53679657px;line-height:126.99999809%;font-family:Sans;letter-spacing:0px;word-spacing:0px;opacity:1;fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ xml:space="preserve"><tspan
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:28px;line-height:126.99999809%;font-family:'BD Cartoon Shout';-inkscape-font-specification:'BD Cartoon Shout';fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ y="1016.485"
+ x="-126.51662"
+ id="tspan3803-4"
+ sodipodi:role="line">POW!</tspan></text>
+ <text
+ transform="matrix(0.99123282,-0.13212683,0.13212683,0.99123282,0,0)"
+ sodipodi:linespacing="127%"
+ id="text3801"
+ y="1014.6411"
+ x="-128.74716"
+ style="font-style:normal;font-weight:normal;font-size:36.53679657px;line-height:126.99999809%;font-family:Sans;letter-spacing:0px;word-spacing:0px;opacity:1;fill:url(#linearGradient6959);fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ xml:space="preserve"><tspan
+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:28px;line-height:126.99999809%;font-family:'BD Cartoon Shout';-inkscape-font-specification:'BD Cartoon Shout';fill:url(#linearGradient6959);fill-opacity:1;stroke:#000000;stroke-width:0.5;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
+ y="1014.6411"
+ x="-128.74716"
+ id="tspan3803"
+ sodipodi:role="line">POW!</tspan></text>
+ </g>
+ </g>
+</svg>
diff --git a/org.fox.ttcomics/src/main/res/layout-sw600dp/activity_main.xml b/org.fox.ttcomics/src/main/res/layout-sw600dp/activity_main.xml
new file mode 100755
index 0000000..63a7abf
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/layout-sw600dp/activity_main.xml
@@ -0,0 +1,31 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/FrameLayout1"
+ android:fitsSystemWindows="true"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_height="match_parent">
+
+ <it.neokree.materialtabs.MaterialTabHost
+ android:id="@+id/materialTabHost"
+ android:layout_width="match_parent"
+ android:layout_height="48dp"
+ app:textColor="#FFFFFF"
+ app:primaryColor="#FF6F00"
+ app:accentColor="#F8CE00" />
+
+ <FrameLayout
+ android:id="@+id/comics_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ </FrameLayout>
+
+ <TextView
+ android:id="@+id/tablet_layout_hack"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:text="" />
+
+</LinearLayout>
diff --git a/org.fox.ttcomics/src/main/res/layout-sw600dp/activity_view_comic.xml b/org.fox.ttcomics/src/main/res/layout-sw600dp/activity_view_comic.xml
new file mode 100644
index 0000000..ba9bbd3
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/layout-sw600dp/activity_view_comic.xml
@@ -0,0 +1,20 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/FrameLayout2"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <FrameLayout
+ android:id="@+id/comics_pager_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+ </FrameLayout>
+
+
+ <TextView
+ android:id="@+id/tablet_layout_hack"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:text="" />
+
+</FrameLayout> \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/res/layout/activity_main.xml b/org.fox.ttcomics/src/main/res/layout/activity_main.xml
new file mode 100755
index 0000000..0fdd5f0
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/layout/activity_main.xml
@@ -0,0 +1,24 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/FrameLayout1"
+ android:fitsSystemWindows="true"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_height="match_parent">
+
+ <it.neokree.materialtabs.MaterialTabHost
+ android:id="@+id/materialTabHost"
+ android:layout_width="match_parent"
+ android:layout_height="48dp"
+ app:textColor="#FFFFFF"
+ app:primaryColor="#FF6F00"
+ app:accentColor="#F8CE00" />
+
+ <FrameLayout
+ android:id="@+id/comics_list"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ </FrameLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/res/layout/activity_view_comic.xml b/org.fox.ttcomics/src/main/res/layout/activity_view_comic.xml
new file mode 100644
index 0000000..ea696a2
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/layout/activity_view_comic.xml
@@ -0,0 +1,13 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/FrameLayout2"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+
+ <FrameLayout
+ android:id="@+id/comics_pager_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+
+ </FrameLayout>
+
+</FrameLayout> \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/res/layout/chooser_list.xml b/org.fox.ttcomics/src/main/res/layout/chooser_list.xml
new file mode 100644
index 0000000..f4b4a12
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/layout/chooser_list.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:orientation="vertical" >
+
+ <ListView
+ android:id="@android:id/list"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_weight="1" />
+
+ <LinearLayout
+ android:id="@+id/linearLayout2"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content" >
+
+ <Button
+ android:id="@+id/btnParent"
+ android:layout_width="fill_parent"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:text="@string/open_parent" />
+
+ <Button
+ android:id="@+id/btnChoose"
+ android:layout_weight="1"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:text="@string/choose" />
+ </LinearLayout>
+
+</LinearLayout> \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/res/layout/comics_grid_row.xml b/org.fox.ttcomics/src/main/res/layout/comics_grid_row.xml
new file mode 100755
index 0000000..91d27cd
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/layout/comics_grid_row.xml
@@ -0,0 +1,67 @@
+<?xml version="1.0" encoding="utf-8"?>
+<android.support.v7.widget.CardView
+ xmlns:card_view="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/comics_list_row"
+ android:layout_width="160dp"
+ android:layout_height="210dp"
+ android:gravity="center_vertical"
+ card_view:cardUseCompatPadding="true"
+ card_view:cardElevation="2dp"
+ tools:ignore="HardcodedText" >
+
+ <FrameLayout
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center_horizontal"
+ android:layout_weight="0"
+ android:padding="0dp" >
+
+ <ImageView
+ android:layout_width="match_parent"
+ android:layout_height="200dp"
+ android:id="@+id/thumbnail"
+ android:scaleType="centerCrop"
+ android:src="@drawable/badimage" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|center_horizontal"
+ android:gravity="center_horizontal|bottom"
+ android:background="@drawable/comic_grid_footer"
+ android:padding="4dp"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/file_name"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:ellipsize="middle"
+ android:singleLine="true"
+ android:text="Comic_1980.cbz"
+ android:textColor="@android:color/primary_text_dark"
+ android:textSize="11sp" />
+
+ <ProgressBar
+ android:id="@+id/file_progress_bar"
+ style="?android:attr/progressBarStyleHorizontal"
+ android:layout_width="96dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+
+ <TextView
+ android:id="@+id/file_progress_info"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="@android:color/primary_text_dark"
+ android:layout_weight="0"
+ android:gravity="right"
+ android:text="12 of 325"
+ android:textSize="8sp" />
+ </LinearLayout>
+
+ </FrameLayout>
+ </android.support.v7.widget.CardView> \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/res/layout/dialog_location.xml b/org.fox.ttcomics/src/main/res/layout/dialog_location.xml
new file mode 100644
index 0000000..cabbf9a
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/layout/dialog_location.xml
@@ -0,0 +1,14 @@
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/linearLayout1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" >
+
+
+
+ <NumberPicker
+ android:id="@+id/number_picker"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+
+</LinearLayout> \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/res/layout/fragment_comic.xml b/org.fox.ttcomics/src/main/res/layout/fragment_comic.xml
new file mode 100644
index 0000000..c9dbbfc
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/layout/fragment_comic.xml
@@ -0,0 +1,20 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/FrameLayout3"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <it.sephiroth.android.library.imagezoom.ImageViewTouch
+ android:id="@+id/comic_image"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" />
+
+ <!-- <TextView
+ android:id="@+id/comic_page"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom|right"
+ android:layout_margin="4dp"
+ android:alpha="0.25"
+ android:text="25" /> -->
+
+</FrameLayout> \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/res/layout/fragment_comics_list.xml b/org.fox.ttcomics/src/main/res/layout/fragment_comics_list.xml
new file mode 100644
index 0000000..7c7a8b5
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/layout/fragment_comics_list.xml
@@ -0,0 +1,34 @@
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/FrameLayout4"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent">
+
+ <android.support.v4.widget.SwipeRefreshLayout
+ android:id="@+id/comics_swipe_container"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent" >
+ <GridView
+ android:id="@+id/comics_grid"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:columnWidth="160dp"
+ android:numColumns="auto_fit" >
+ </GridView>
+ </android.support.v4.widget.SwipeRefreshLayout>
+
+ <LinearLayout
+ android:id="@+id/no_comics"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/textView1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:text="@string/error_no_comic_archives_found_" />
+ </LinearLayout>
+
+</FrameLayout> \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/res/layout/fragment_comics_pager.xml b/org.fox.ttcomics/src/main/res/layout/fragment_comics_pager.xml
new file mode 100755
index 0000000..3ac2f7b
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/layout/fragment_comics_pager.xml
@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+android:id="@+id/comics_pager_container"
+android:layout_width="fill_parent"
+android:layout_height="fill_parent" >
+
+ <org.fox.ttcomics2.ViewPager
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:id="@+id/comics_pager"
+ android:layout_above="@+id/comics_seek_bar" />
+
+ <SeekBar
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:id="@+id/comics_seek_bar"
+ android:layout_alignParentBottom="true"
+ android:layout_toRightOf="@+id/comics_page"
+ android:layout_toLeftOf="@+id/comics_total_pages"
+ android:layout_toStartOf="@+id/comics_total_pages" />
+
+ <TextView
+ android:layout_width="60dp"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:text="1123"
+ android:id="@+id/comics_total_pages"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentRight="true"
+ android:layout_alignParentEnd="true"
+ android:singleLine="true"
+ android:layout_alignTop="@+id/comics_seek_bar"
+ android:gravity="center" />
+
+ <TextView
+ android:layout_width="60dp"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:text="1"
+ android:id="@+id/comics_page"
+ android:singleLine="true"
+ android:gravity="center"
+ android:layout_below="@+id/comics_pager"
+ android:layout_alignParentLeft="true"
+ android:layout_alignParentStart="true"
+ android:layout_alignParentBottom="true" />
+
+
+</RelativeLayout> \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/res/layout/list_item.xml b/org.fox.ttcomics/src/main/res/layout/list_item.xml
new file mode 100644
index 0000000..1fb2f8c
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/layout/list_item.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="utf-8"?>
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:padding="10dp"
+ android:textSize="16sp" >
+</TextView> \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/res/menu/activity_main.xml b/org.fox.ttcomics/src/main/res/menu/activity_main.xml
new file mode 100644
index 0000000..a7ca037
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/menu/activity_main.xml
@@ -0,0 +1,15 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ <item android:id="@+id/menu_rescan"
+ android:title="@string/menu_rescan"
+ android:icon="@drawable/ic_refresh_light"
+ app:showAsAction="ifRoom" />
+
+ <item android:id="@+id/menu_settings"
+ android:title="@string/menu_settings"
+ android:icon="@drawable/ic_settings"
+ app:showAsAction="ifRoom"
+ android:orderInCategory="100"
+ />
+</menu>
diff --git a/org.fox.ttcomics/src/main/res/menu/activity_view_comic.xml b/org.fox.ttcomics/src/main/res/menu/activity_view_comic.xml
new file mode 100644
index 0000000..1e5fc29
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/menu/activity_view_comic.xml
@@ -0,0 +1,27 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:ugh="http://schemas.android.com/apk/res-auto">
+ <item android:id="@+id/menu_settings"
+ android:title="@string/menu_settings"
+ android:icon="@drawable/ic_settings"
+ android:fitsSystemWindows="true"
+ android:orderInCategory="100"
+ ugh:showAsAction="never" />
+
+ <item android:id="@+id/menu_share"
+ android:title="@string/menu_share"
+ android:icon="@drawable/ic_share_light"
+ ugh:showAsAction="ifRoom" />
+
+ <item android:id="@+id/menu_go_location"
+ android:title="@string/menu_go_location"
+ ugh:showAsAction="never" />
+
+ <item android:id="@+id/menu_sync_location"
+ android:title="@string/menu_sync_location"
+ ugh:showAsAction="never" />
+
+ <item android:id="@+id/menu_toggle_orientation_lock"
+ android:title="@string/menu_toggle_orientation_lock"
+ ugh:showAsAction="never" />
+
+</menu>
diff --git a/org.fox.ttcomics/src/main/res/menu/comic_archive_context.xml b/org.fox.ttcomics/src/main/res/menu/comic_archive_context.xml
new file mode 100644
index 0000000..9a39898
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/menu/comic_archive_context.xml
@@ -0,0 +1,8 @@
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@+id/menu_mark_as_read"
+ android:title="Mark as read" />
+
+ <item android:id="@+id/menu_reset_progress"
+ android:title="Reset progress" />
+
+</menu>
diff --git a/org.fox.ttcomics/src/main/res/values-large/dimens.xml b/org.fox.ttcomics/src/main/res/values-large/dimens.xml
new file mode 100644
index 0000000..1ae597c
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/values-large/dimens.xml
@@ -0,0 +1,7 @@
+<resources>
+
+ <dimen name="padding_small">8dp</dimen>
+ <dimen name="padding_medium">16dp</dimen>
+ <dimen name="padding_large">16dp</dimen>
+
+</resources> \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/res/values-v11/style.xml b/org.fox.ttcomics/src/main/res/values-v11/style.xml
new file mode 100644
index 0000000..54a7530
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/values-v11/style.xml
@@ -0,0 +1,6 @@
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <style name="DarkDialogTheme" parent="android:Theme.Holo.Dialog">
+ </style>
+
+</resources> \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/res/values-v21/style.xml b/org.fox.ttcomics/src/main/res/values-v21/style.xml
new file mode 100644
index 0000000..c3ac29a
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/values-v21/style.xml
@@ -0,0 +1,6 @@
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <style name="DarkDialogTheme" parent="android:Theme.Material.Dialog">
+ </style>
+
+</resources> \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/res/values/attrs.xml b/org.fox.ttcomics/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..35c14ff
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/values/attrs.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+</resources> \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/res/values/dimens.xml b/org.fox.ttcomics/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..1dc0322
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/values/dimens.xml
@@ -0,0 +1,7 @@
+<resources>
+
+ <dimen name="padding_small">8dp</dimen>
+ <dimen name="padding_medium">8dp</dimen>
+ <dimen name="padding_large">16dp</dimen>
+
+</resources> \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/res/values/strings.xml b/org.fox.ttcomics/src/main/res/values/strings.xml
new file mode 100755
index 0000000..c626d44
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/values/strings.xml
@@ -0,0 +1,71 @@
+<resources>
+
+ <string name="app_name">Pow! Comics Reader</string>
+ <string name="hello_world">Hello world!</string>
+ <string name="menu_settings">Settings</string>
+ <string name="menu_go_location">Go to</string>
+ <string name="title_activity_main">Comics Reader</string>
+ <string name="title_activity_preferences">Settings</string>
+ <string name="prefs_comics_directory">Comics directory</string>
+ <string name="prefs_general">General</string>
+ <!-- <string name="prefs_dim_status_bar">Dim status bar</string> -->
+ <string name="file_progress_info">%1$d of %2$d (%3$d%%)</string>
+ <string name="menu_rescan">Reload</string>
+ <string name="tab_all_comics">All comics</string>
+ <string name="tab_unread">Unread</string>
+ <string name="tab_read">Finished</string>
+ <string name="dialog_need_prefs_message">Please configure base directory for comics archives.</string>
+ <string name="dialog_need_prefs_preferences">Settings</string>
+ <string name="cancel">Cancel</string>
+ <string name="comics_directory_default">/mnt/sdcard/Comics</string>
+ <string name="prefs_reading">Reading</string>
+ <string name="prefs_dark_theme">Dark theme</string>
+ <string name="dialog_open_location">Open location</string>
+ <string name="dialog_location_beginning">Beginning</string>
+ <string name="dialog_location_furthest">Furthest read location</string>
+ <string name="dialog_location_location">Location…</string>
+ <string name="dialog_location_end">End</string>
+ <string name="tab_unfinished">Unfinished</string>
+ <string name="menu_share">Share</string>
+ <string name="prefs_use_full_screen">Fullscreen mode</string>
+ <string name="error_could_not_prepare_file_for_sharing">Could not prepare file for sharing</string>
+ <string name="error_could_not_open_comic_archive">Could not open comic archive.</string>
+ <string name="error_out_of_memory">Out of memory</string>
+ <string name="error_loading_image">Error loading image</string>
+ <string name="error_could_not_read_folder_contents_">Could not read folder contents.</string>
+ <string name="picker_choose">Choose %1$s</string>
+ <string name="sync_server_has_further_page">You are currently on page %1$d. Furthest read page stored on the server is %2$d. Open it instead?</string>
+ <string name="dialog_open_page">Open page</string>
+ <string name="menu_sync_location">Sync to last page read</string>
+ <string name="share_comic">Share comic</string>
+ <string name="choose">Choose</string>
+ <string name="error_no_comic_archives_found_">No comic archives found.</string>
+ <string name="prefs_use_position_sync">Sync last read pages</string>
+ <string name="prefs_use_position_sync_summary">Requires at least one Google account on the device. No personally identifiable information is sent.</string>
+ <string name="error_sync_no_account">No Google account found, sync disabled.</string>
+ <string name="sync_uploading">Uploading sync data…</string>
+ <string name="error_sync_no_data">No information stored or you are on the furthest read page.</string>
+ <string name="dialog_clear_data">Clear data</string>
+ <string name="dialog_clear_data_title">Clear all remotely stored sync data?</string>
+ <string name="prefs_sync">Sync</string>
+ <string name="prefs_clear_sync_data">Clear sync data</string>
+ <string name="prefs_clear_sync_data_summary">Removes all remotely stored sync data.</string>
+ <string name="open_parent">Open parent</string>
+ <string name="file_unread">Unread (%1$d pages)</string>
+ <string name="file_finished">Finished</string>
+ <string name="menu_toggle_orientation_lock">(Un)lock orientation</string>
+ <string name="prefs_comics_directory_summary">Base directory for comic archives.</string>
+ <string name="prefs_fit_to_width">Fit to width</string>
+ <string name="sync_running_in_test_mode">Sync running in test mode.</string>
+ <string name="reset_remove_synced_progress">Would you like to remove synced progress too?</string>
+ <string name="error_cant_open_file">Can\'t open file: %1$s</string>
+ <string name="prefs_prevent_screen_sleep">Keep screen awake</string>
+ <string name="error_other_error">Error: unknown error (see log)</string>
+ <string name="list_type_directory">Directory</string>
+ <string name="list_type_unknown">Unknown</string>
+ <string name="prefs_version">%1$s (%2$d)</string>
+ <string name="prefs_version_title">Version</string>
+ <string name="prefs_build_timestamp">%1$s</string>
+ <string name="prefs_build_timestamp_title">Build timestamp</string>
+ <string name="crash_dialog_text">Unfortunately, Pow! Comics Reader has stopped. Submit crash report to tt-rss.org?</string>
+</resources> \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/res/values/style.xml b/org.fox.ttcomics/src/main/res/values/style.xml
new file mode 100755
index 0000000..92d0730
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/values/style.xml
@@ -0,0 +1,20 @@
+<resources>
+
+ <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+ <item name="colorPrimary">#FF0000</item>
+ <item name="colorPrimaryDark">#990000</item>
+ <item name="colorAccent">#FF7500</item>
+ </style>
+
+ <style name="ViewLightTheme" parent="AppTheme">
+
+ </style>
+
+ <style name="ViewDarkTheme" parent="Theme.AppCompat">
+ <item name="colorPrimary">#600000</item>
+ <item name="colorPrimaryDark">#300000</item>
+ <item name="colorAccent">#FF6600</item>
+ </style>
+
+ <style name="DarkDialogTheme" parent="android:Theme"></style>
+</resources> \ No newline at end of file
diff --git a/org.fox.ttcomics/src/main/res/xml/preferences.xml b/org.fox.ttcomics/src/main/res/xml/preferences.xml
new file mode 100644
index 0000000..6f90c16
--- /dev/null
+++ b/org.fox.ttcomics/src/main/res/xml/preferences.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
+
+ <PreferenceCategory android:title="@string/prefs_general" >
+ <Preference
+ android:key="comics_directory"
+ android:hint="@string/comics_directory_default"
+ android:summary="@string/prefs_comics_directory_summary"
+ android:title="@string/prefs_comics_directory" >
+ </Preference>
+ </PreferenceCategory>
+ <PreferenceCategory android:title="@string/prefs_sync" >
+ <CheckBoxPreference
+ android:defaultValue="false"
+ android:key="use_position_sync"
+ android:title="@string/prefs_use_position_sync"
+ android:summary="@string/prefs_use_position_sync_summary"
+ />
+
+ <Preference
+ android:dependency="use_position_sync"
+ android:key="clear_sync_data"
+ android:title="@string/prefs_clear_sync_data"
+ android:summary="@string/prefs_clear_sync_data_summary" >
+ </Preference>
+
+ </PreferenceCategory>
+
+ <PreferenceCategory android:title="@string/prefs_reading" android:key="prefs_reading">
+ <CheckBoxPreference
+ android:defaultValue="false"
+ android:key="use_dark_theme"
+ android:title="@string/prefs_dark_theme" />
+
+ <!-- <CheckBoxPreference
+ android:defaultValue="false"
+ android:key="dim_status_bar"
+ android:title="@string/prefs_dim_status_bar" /> -->
+
+ <CheckBoxPreference
+ android:defaultValue="false"
+ android:key="use_full_screen"
+ android:title="@string/prefs_use_full_screen" />
+
+ <CheckBoxPreference
+ android:defaultValue="false"
+ android:key="fit_to_width"
+ android:title="@string/prefs_fit_to_width" />
+
+ <CheckBoxPreference
+ android:defaultValue="false"
+ android:key="prevent_screen_sleep"
+ android:title="@string/prefs_prevent_screen_sleep" />
+
+ </PreferenceCategory>
+
+ <Preference
+ android:key="version"
+ android:enabled="false"
+ android:title="@string/prefs_version_title" />
+
+ <Preference
+ android:key="build_timestamp"
+ android:enabled="false"
+ android:title="@string/prefs_build_timestamp_title" />
+</PreferenceScreen> \ No newline at end of file