diff --git a/Chan/AndroidManifest.xml b/Chan/AndroidManifest.xml index 0ae9d23e..ce7212af 100644 --- a/Chan/AndroidManifest.xml +++ b/Chan/AndroidManifest.xml @@ -1,8 +1,8 @@ + + + + + + + + + - Cancel Change + Add Chan Settings Reload Pin Reply + Reply to board + Reply to thread Open in browser Open catalog Share General - - About - Open Source Licenses - Legal information about licenses + About + Open Source Licenses + Legal information about licenses reply replies Save image - Chan Saved image to Saving image failed Cannot make save directory @@ -35,13 +35,11 @@ No network Server inaccessible 404 not found - - No more posts + No more posts Edit my boards Add board - Add - Cancel + Unknown board code Added You already have that board @@ -85,6 +83,16 @@ Developer options + Delete + Delete your post? + Deleting post… + Post deleted + Password incorrect + You must wait longer before deleting this post + You cannot delete a post this old + Error deleting post + Only delete the image + Info Reply @@ -93,4 +101,5 @@ Show clickables Copy text + diff --git a/Chan/res/xml/preference.xml b/Chan/res/xml/preference.xml index 1dc055a2..8c98a726 100644 --- a/Chan/res/xml/preference.xml +++ b/Chan/res/xml/preference.xml @@ -27,12 +27,12 @@ pinDao; public Dao loadableDao; + public Dao savedDao; + + private final Context context; public DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); + this.context = context; + try { pinDao = getDao(Pin.class); loadableDao = getDao(Loadable.class); + savedDao = getDao(SavedReply.class); } catch (SQLException e) { e.printStackTrace(); } @@ -37,8 +46,9 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { try { TableUtils.createTable(connectionSource, Pin.class); TableUtils.createTable(connectionSource, Loadable.class); + TableUtils.createTable(connectionSource, SavedReply.class); } catch (SQLException e) { - Logger.e("Error creating db", e); + Logger.e(TAG, "Error creating db", e); throw new RuntimeException(e); } } @@ -50,9 +60,22 @@ public class DatabaseHelper extends OrmLiteSqliteOpenHelper { } // Drop the tables and recreate them for now + reset(database, connectionSource); + } + + public void reset() { + Logger.i(TAG, "Resetting database!"); + + if (context.deleteDatabase(DATABASE_NAME)) { + Logger.i(TAG, "Deleted database"); + } + } + + private void reset(SQLiteDatabase database, ConnectionSource connectionSource) { try { TableUtils.dropTable(connectionSource, Pin.class, true); TableUtils.dropTable(connectionSource, Loadable.class, true); + TableUtils.dropTable(connectionSource, SavedReply.class, true); onCreate(database, connectionSource); } catch (SQLException e) { diff --git a/Chan/src/org/floens/chan/database/DatabaseManager.java b/Chan/src/org/floens/chan/database/DatabaseManager.java index 6dc9bae0..dd368e69 100644 --- a/Chan/src/org/floens/chan/database/DatabaseManager.java +++ b/Chan/src/org/floens/chan/database/DatabaseManager.java @@ -4,14 +4,18 @@ import java.sql.SQLException; import java.util.List; import org.floens.chan.model.Pin; +import org.floens.chan.model.SavedReply; import org.floens.chan.utils.Logger; import android.content.Context; public class DatabaseManager { + private static final String TAG = "DatabaseManager"; + private static DatabaseManager instance; private final DatabaseHelper helper; + private List savedReplies; public DatabaseManager(Context context) { instance = this; @@ -23,12 +27,52 @@ public class DatabaseManager { return instance; } + public void saveReply(SavedReply saved) { + Logger.e(TAG, "Saving " + saved.board + ", " + saved.no); + + try { + helper.savedDao.create(saved); + } catch (SQLException e) { + Logger.e(TAG, "Error saving reply", e); + } + + loadSavedReplies(); + } + + public SavedReply getSavedReply(String board, int no) { + if (savedReplies == null) { + loadSavedReplies(); + } + + // TODO: optimize this + for (SavedReply r : savedReplies) { + if (r.board.equals(board) && r.no == no) { + return r; + } + } + + return null; + } + + public boolean isSavedReply(String board, int no) { + return getSavedReply(board, no) != null; + } + + private void loadSavedReplies() { + // TODO trim the table if it gets too large + try { + savedReplies = helper.savedDao.queryForAll(); + } catch (SQLException e) { + Logger.e(TAG, "Error loading saved replies", e); + } + } + public void addPin(Pin pin) { try { helper.loadableDao.create(pin.loadable); helper.pinDao.create(pin); } catch (SQLException e) { - Logger.e("Error adding pin to db", e); + Logger.e(TAG, "Error adding pin to db", e); } } @@ -37,7 +81,7 @@ public class DatabaseManager { helper.pinDao.delete(pin); helper.loadableDao.delete(pin.loadable); } catch (SQLException e) { - Logger.e("Error removing pin from db", e); + Logger.e(TAG, "Error removing pin from db", e); } } @@ -46,7 +90,7 @@ public class DatabaseManager { helper.pinDao.update(pin); helper.loadableDao.update(pin.loadable); } catch (SQLException e) { - Logger.e("Error updating pin in db", e); + Logger.e(TAG, "Error updating pin in db", e); } } @@ -60,7 +104,7 @@ public class DatabaseManager { helper.loadableDao.update(pin.loadable); } } catch(SQLException e) { - Logger.e("Error updating pins in db", e); + Logger.e(TAG, "Error updating pins in db", e); } } @@ -72,9 +116,33 @@ public class DatabaseManager { helper.loadableDao.refresh(p.loadable); } } catch (SQLException e) { - Logger.e("Error getting pins from db", e); + Logger.e(TAG, "Error getting pins from db", e); } return list; } + + public String getSummary() { + String o = ""; + + try { + o += "Loadable rows: " + helper.loadableDao.countOf() + "\n"; + o += "Pin rows: " + helper.pinDao.countOf() + "\n"; + o += "SavedReply rows: " + helper.savedDao.countOf() + "\n"; + } catch (SQLException e) { + e.printStackTrace(); + } + + return o; + } + + public void reset() { + helper.reset(); + loadSavedReplies(); + } } + + + + + diff --git a/Chan/src/org/floens/chan/fragment/ReplyFragment.java b/Chan/src/org/floens/chan/fragment/ReplyFragment.java index 39ddecf7..8b01f211 100644 --- a/Chan/src/org/floens/chan/fragment/ReplyFragment.java +++ b/Chan/src/org/floens/chan/fragment/ReplyFragment.java @@ -12,6 +12,7 @@ import org.floens.chan.net.ChanUrls; import org.floens.chan.utils.ChanPreferences; import org.floens.chan.utils.ImageDecoder; import org.floens.chan.utils.LoadView; +import org.floens.chan.utils.Logger; import org.floens.chan.utils.ViewFlipperAnimations; import android.app.Dialog; @@ -21,7 +22,6 @@ import android.content.DialogInterface; import android.graphics.Bitmap; import android.os.Bundle; import android.text.TextUtils; -import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.LayoutInflater; @@ -44,6 +44,8 @@ import com.android.volley.toolbox.NetworkImageView; import com.android.volley.toolbox.StringRequest; public class ReplyFragment extends DialogFragment { + private static final String TAG = "ReplyFragment"; + private int page = 0; private Loadable loadable; @@ -138,7 +140,7 @@ public class ReplyFragment extends DialogFragment { getCaptcha(); } else { - Log.e("Chan", "Loadable in ReplyFragment was null"); + Logger.e(TAG, "Loadable in ReplyFragment was null"); closeReply(); } } @@ -419,7 +421,7 @@ public class ReplyFragment extends DialogFragment { } else if (response.isSuccessful) { shouldSaveDraft = false; Toast.makeText(getActivity(), R.string.reply_success, Toast.LENGTH_SHORT).show(); -// threadFragment.reload(); // TODO +// threadFragment.reload(); // won't work: it takes 4chan a variable time to process the reply closeReply(); } else { if (isVisible()) { diff --git a/Chan/src/org/floens/chan/fragment/ThreadFragment.java b/Chan/src/org/floens/chan/fragment/ThreadFragment.java index 74ae865e..2e5db0ae 100644 --- a/Chan/src/org/floens/chan/fragment/ThreadFragment.java +++ b/Chan/src/org/floens/chan/fragment/ThreadFragment.java @@ -160,7 +160,7 @@ public class ThreadFragment extends Fragment implements ThreadListener { postAdapter.setEndOfLine(true); } else { if (container != null) { - container.setView(threadManager.getTextViewError(error)); + container.setView(threadManager.getLoadErrorTextView(error)); } } } diff --git a/Chan/src/org/floens/chan/imageview/ImageSaver.java b/Chan/src/org/floens/chan/imageview/ImageSaver.java index aede7f33..c4473389 100644 --- a/Chan/src/org/floens/chan/imageview/ImageSaver.java +++ b/Chan/src/org/floens/chan/imageview/ImageSaver.java @@ -7,18 +7,21 @@ import java.io.IOException; import org.floens.chan.ChanApplication; import org.floens.chan.R; import org.floens.chan.net.ByteArrayRequest; +import org.floens.chan.utils.ChanPreferences; +import org.floens.chan.utils.Logger; import android.content.Context; import android.media.MediaScannerConnection; import android.net.Uri; import android.os.Environment; -import android.util.Log; import android.widget.Toast; import com.android.volley.Response; import com.android.volley.VolleyError; public class ImageSaver { + private static final String TAG = "ImageSaver"; + public static void save(final Context context, String imageUrl, final String name, final String extension) { ByteArrayRequest request = new ByteArrayRequest(imageUrl, new Response.Listener() { @Override @@ -44,7 +47,7 @@ public class ImageSaver { throw new IOException(errorReason); } - File path = new File(Environment.getExternalStorageDirectory() + File.separator + context.getString(R.string.image_save_folder_name)); + File path = new File(Environment.getExternalStorageDirectory() + File.separator + ChanPreferences.getImageSaveDirectory()); if (!path.exists()) { if (!path.mkdirs()) { @@ -62,7 +65,7 @@ public class ImageSaver { nextFileNameNumber++; } - Log.i("Chan", "Saving image to: " + file.getPath()); + Logger.i(TAG, "Saving image to: " + file.getPath()); FileOutputStream outputStream = new FileOutputStream(file); outputStream.write(data); @@ -71,7 +74,7 @@ public class ImageSaver { MediaScannerConnection.scanFile(context, new String[] { file.toString() }, null, new MediaScannerConnection.OnScanCompletedListener() { @Override public void onScanCompleted(String path, Uri uri) { - Log.i("Chan", "Media scan succeeded: " + uri); + Logger.i(TAG, "Media scan succeeded: " + uri); } }); diff --git a/Chan/src/org/floens/chan/imageview/activity/ImageViewActivity.java b/Chan/src/org/floens/chan/imageview/activity/ImageViewActivity.java index 63d4e6dc..0220ca7a 100644 --- a/Chan/src/org/floens/chan/imageview/activity/ImageViewActivity.java +++ b/Chan/src/org/floens/chan/imageview/activity/ImageViewActivity.java @@ -7,12 +7,12 @@ import org.floens.chan.adapter.PostAdapter; import org.floens.chan.imageview.ImageSaver; import org.floens.chan.imageview.adapter.ImageViewAdapter; import org.floens.chan.model.Post; +import org.floens.chan.utils.Logger; import android.app.ActionBar; import android.app.Activity; import android.os.Bundle; import android.support.v4.view.ViewPager; -import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.Window; @@ -22,6 +22,8 @@ import android.view.Window; * and then start the activity with startActivity() */ public class ImageViewActivity extends Activity implements ViewPager.OnPageChangeListener { + private static final String TAG = "ImageViewActivity"; + private static PostAdapter postAdapter; private static int selectedId = -1; @@ -75,7 +77,7 @@ public class ImageViewActivity extends Activity implements ViewPager.OnPageChang } } } else { - Log.e("Chan", "Posts in imageview list was null"); + Logger.e(TAG, "Posts in imageview list was null"); finish(); } } diff --git a/Chan/src/org/floens/chan/manager/BoardManager.java b/Chan/src/org/floens/chan/manager/BoardManager.java index 8a1ae35c..f25b2fb2 100644 --- a/Chan/src/org/floens/chan/manager/BoardManager.java +++ b/Chan/src/org/floens/chan/manager/BoardManager.java @@ -8,15 +8,17 @@ import org.floens.chan.R; import org.floens.chan.model.Board; import org.floens.chan.net.BoardsRequest; import org.floens.chan.net.ChanUrls; +import org.floens.chan.utils.Logger; import android.content.Context; -import android.util.Log; import android.widget.Toast; import com.android.volley.Response; import com.android.volley.VolleyError; public class BoardManager { + private static final String TAG = "BoardManager"; + private static BoardManager instance; private final Context context; @@ -196,7 +198,7 @@ public class BoardManager { board.key = splitted[0]; board.value = splitted[1]; if (!board.finish()) { - Log.wtf("Chan", "board.finish in loadfrompreferences threw up"); + Logger.wtf(TAG, "board.finish in loadfrompreferences threw up"); } list.add(board); @@ -219,12 +221,12 @@ public class BoardManager { storeBoardListInDatabase("allBoards", data); allBoards = data; - Log.i("Chan", "Got boards from server"); + Logger.i(TAG, "Got boards from server"); } }, new Response.ErrorListener() { @Override public void onErrorResponse(VolleyError error) { - Log.e("Chan", "Failed to get boards from server"); + Logger.e(TAG, "Failed to get boards from server"); } })); } diff --git a/Chan/src/org/floens/chan/manager/PinnedManager.java b/Chan/src/org/floens/chan/manager/PinnedManager.java index 4095a5d8..38e66253 100644 --- a/Chan/src/org/floens/chan/manager/PinnedManager.java +++ b/Chan/src/org/floens/chan/manager/PinnedManager.java @@ -12,14 +12,11 @@ import android.content.Context; public class PinnedManager { private static PinnedManager instance; - private final Context context; private final List listeners = new ArrayList(); private final List pins; public PinnedManager(Context context) { instance = this; - - this.context = context; pins = DatabaseManager.getInstance().getPinned(); } diff --git a/Chan/src/org/floens/chan/manager/ReplyManager.java b/Chan/src/org/floens/chan/manager/ReplyManager.java index eb14b4af..6d1eedef 100644 --- a/Chan/src/org/floens/chan/manager/ReplyManager.java +++ b/Chan/src/org/floens/chan/manager/ReplyManager.java @@ -4,16 +4,20 @@ import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.Locale; +import java.util.Random; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.floens.chan.activity.ImagePickActivity; +import org.floens.chan.database.DatabaseManager; import org.floens.chan.model.Reply; +import org.floens.chan.model.SavedReply; import org.floens.chan.net.ChanUrls; +import org.floens.chan.utils.Logger; +import org.floens.chan.utils.Utils; import android.content.Context; import android.content.Intent; -import android.os.AsyncTask; import ch.boye.httpclientandroidlib.HttpResponse; import ch.boye.httpclientandroidlib.client.ClientProtocolException; import ch.boye.httpclientandroidlib.client.methods.HttpPost; @@ -30,14 +34,18 @@ import ch.boye.httpclientandroidlib.util.EntityUtils; * To send an reply to 4chan. */ public class ReplyManager { + private static final String TAG = "ReplyManager"; + private static ReplyManager instance; private static final Pattern challengePattern = Pattern.compile("challenge.?:.?'([\\w-]+)'"); + private static final Pattern responsePattern = Pattern.compile(""); private static final int POST_TIMEOUT = 10000; private final Context context; private Reply draft; private FileListener fileListener; + private final Random random = new Random(); public ReplyManager(Context context) { ReplyManager.instance = this; @@ -139,11 +147,74 @@ public class ReplyManager { * @param reply The reply object with all data needed, like captcha and the file. * @param listener The listener, after server response. */ - public void sendReply(Reply reply, ReplyListener listener) { + public void sendDelete(final SavedReply reply, boolean onlyImageDelete, final DeleteListener listener) { + Logger.i(TAG, "Sending delete request: " + reply.board + ", " + reply.no); + + HttpPost httpPost = new HttpPost(ChanUrls.getDeleteUrl(reply.board)); + + MultipartEntity entity = new MultipartEntity(); + + try { + entity.addPart(Integer.toString(reply.no), new StringBody("delete")); + + if (onlyImageDelete) { + entity.addPart("onlyimgdel", new StringBody("on")); + } + + // res not necessary + + entity.addPart("mode", new StringBody("usrdel")); + entity.addPart("pwd", new StringBody(reply.password)); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + return; + } + + httpPost.setEntity(entity); + + sendHttpPost(httpPost, new HttpPostSendListener() { + @Override + public void onReponse(String responseString) { + DeleteResponse e = new DeleteResponse(); + + if (responseString == null) { + e.isNetworkError = true; + } else { + e.responseData = responseString; + + if (responseString.contains("You must wait longer before deleting this post")) { + e.isUserError = true; + e.isTooSoonError = true; + } else if (responseString.contains("Password incorrect")) { + e.isUserError = true; + e.isInvalidPassword = true; + } else if (responseString.contains("You cannot delete a post this old")) { + e.isUserError = true; + e.isTooOldError = true; + } else if (responseString.contains("Updating index")) { + e.isSuccessful = true; + } + } + + listener.onResponse(e); + } + }); + } + + /** + * Send an reply off to the server. + * @param reply The reply object with all data needed, like captcha and the file. + * @param listener The listener, after server response. + */ + public void sendReply(final Reply reply, final ReplyListener listener) { + Logger.i(TAG, "Sending reply request: " + reply.board + ", " + reply.resto); + HttpPost httpPost = new HttpPost(ChanUrls.getPostUrl(reply.board)); MultipartEntity entity = new MultipartEntity(); + reply.password = Long.toHexString(random.nextLong()); + try { entity.addPart("name", new StringBody(reply.name)); entity.addPart("email", new StringBody(reply.email)); @@ -159,7 +230,7 @@ public class ReplyManager { entity.addPart("recaptcha_response_field", new StringBody(reply.captchaResponse)); entity.addPart("mode", new StringBody("regist")); - entity.addPart("pwd", new StringBody("")); + entity.addPart("pwd", new StringBody(reply.password)); if (reply.file != null) { entity.addPart("upfile", new FileBody(reply.file, reply.fileName, "application/octet-stream", "UTF-8")); @@ -171,7 +242,50 @@ public class ReplyManager { httpPost.setEntity(entity); - new SendTask().execute(httpPost, listener); + sendHttpPost(httpPost, new HttpPostSendListener() { + @Override + public void onReponse(String responseString) { + ReplyResponse e = new ReplyResponse(); + + if (responseString == null) { + e.isNetworkError = true; + } else { + e.responseData = responseString; + + if (responseString.contains("No file selected")) { + e.isUserError = true; + e.isFileError = true; + } else if (responseString.contains("You forgot to solve the CAPTCHA") || + responseString.contains("You seem to have mistyped the CAPTCHA")) { + e.isUserError = true; + e.isCaptchaError = true; + } else if (responseString.toLowerCase(Locale.ENGLISH).contains("post successful")) { + e.isSuccessful = true; + } + } + + if (e.isSuccessful) { + Matcher matcher = responsePattern.matcher(e.responseData); + + if (matcher.find() && matcher.groupCount() == 2) { + try { + SavedReply savedReply = new SavedReply(); + savedReply.board = reply.board; + savedReply.no = Integer.parseInt(matcher.group(2)); + savedReply.password = reply.password; + + DatabaseManager.getInstance().saveReply(savedReply); + } catch (NumberFormatException err) { + err.printStackTrace(); + } + } else { + Logger.w(TAG, "No thread & no in the response"); + } + } + + listener.onResponse(e); + } + }); } /** @@ -181,56 +295,41 @@ public class ReplyManager { * that has another namespace: ch.boye.httpclientandroidlib * This lib also has some fixes/improvements of HttpClient for Android. */ - private class SendTask extends AsyncTask { - private ReplyListener listener; - - @Override - protected ReplyResponse doInBackground(Object... params) { - HttpPost post = (HttpPost) params[0]; - listener = (ReplyListener) params[1]; - - HttpParams httpParameters = new BasicHttpParams(); - HttpConnectionParams.setConnectionTimeout(httpParameters, POST_TIMEOUT); - HttpConnectionParams.setSoTimeout(httpParameters, POST_TIMEOUT); - - DefaultHttpClient client = new DefaultHttpClient(httpParameters); - - String responseString = null; - - try { - HttpResponse response = client.execute(post); - responseString = EntityUtils.toString(response.getEntity(), "UTF-8"); - } catch (ClientProtocolException e) { - e.printStackTrace(); - } catch (IOException e) { - e.printStackTrace(); - } - - ReplyResponse e = new ReplyResponse(); - - if (responseString == null) { - e.isNetworkError = true; - } else { - e.responseData = responseString; + private void sendHttpPost(final HttpPost post, final HttpPostSendListener listener) { + new Thread(new Runnable() { + @Override + public void run() { + HttpParams httpParameters = new BasicHttpParams(); + HttpConnectionParams.setConnectionTimeout(httpParameters, POST_TIMEOUT); + HttpConnectionParams.setSoTimeout(httpParameters, POST_TIMEOUT); - if (responseString.contains("No file selected")) { - e.isUserError = true; - e.isFileError = true; - } else if (responseString.contains("You forgot to solve the CAPTCHA") || responseString.contains("You seem to have mistyped the CAPTCHA")) { - e.isUserError = true; - e.isCaptchaError = true; - } else if (responseString.toLowerCase(Locale.ENGLISH).contains("post successful")) { - e.isSuccessful = true; + DefaultHttpClient client = new DefaultHttpClient(httpParameters); + + String responseString = null; + + try { + HttpResponse response = client.execute(post); + responseString = EntityUtils.toString(response.getEntity(), "UTF-8"); + } catch (ClientProtocolException e) { + e.printStackTrace(); + } catch (IOException e) { + e.printStackTrace(); } - } - - return e; - } - - @Override - public void onPostExecute(ReplyResponse response) { - listener.onResponse(response); - } + + final String finalResponseString = responseString; + + Utils.runOnUiThread(new Runnable() { + @Override + public void run() { + listener.onReponse(finalResponseString); + } + }); + } + }).start(); + } + + private static interface HttpPostSendListener { + public void onReponse(String responseString); } public static abstract class FileListener { @@ -245,8 +344,22 @@ public class ReplyManager { public abstract void onFileLoading(); } - public static abstract class ReplyListener { - public abstract void onResponse(ReplyResponse response); + public static interface DeleteListener { + public void onResponse(DeleteResponse response); + } + + public static class DeleteResponse { + public boolean isNetworkError = false; + public boolean isUserError = false; + public boolean isInvalidPassword = false; + public boolean isTooSoonError = false; + public boolean isTooOldError = false; + public boolean isSuccessful = false; + public String responseData = ""; + } + + public static interface ReplyListener { + public void onResponse(ReplyResponse response); } public static class ReplyResponse { diff --git a/Chan/src/org/floens/chan/manager/ThreadManager.java b/Chan/src/org/floens/chan/manager/ThreadManager.java index 9401b6c1..af6b6cb1 100644 --- a/Chan/src/org/floens/chan/manager/ThreadManager.java +++ b/Chan/src/org/floens/chan/manager/ThreadManager.java @@ -5,12 +5,16 @@ import java.util.List; import org.floens.chan.R; import org.floens.chan.activity.ReplyActivity; +import org.floens.chan.database.DatabaseManager; import org.floens.chan.fragment.PostRepliesFragment; import org.floens.chan.fragment.ReplyFragment; +import org.floens.chan.manager.ReplyManager.DeleteListener; +import org.floens.chan.manager.ReplyManager.DeleteResponse; import org.floens.chan.model.Loadable; import org.floens.chan.model.Pin; import org.floens.chan.model.Post; import org.floens.chan.model.PostLinkable; +import org.floens.chan.model.SavedReply; import org.floens.chan.net.ThreadLoader; import org.floens.chan.utils.ChanPreferences; import org.floens.chan.utils.Logger; @@ -20,17 +24,20 @@ import org.floens.chan.watch.WatchLogic.WatchListener; import android.app.Activity; import android.app.AlertDialog; import android.app.FragmentTransaction; +import android.app.ProgressDialog; import android.content.ClipData; import android.content.ClipboardManager; import android.content.Context; import android.content.DialogInterface; +import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.net.Uri; import android.text.TextUtils; -import android.util.Log; import android.view.Gravity; +import android.widget.CheckBox; import android.widget.LinearLayout.LayoutParams; import android.widget.TextView; +import android.widget.Toast; import com.android.volley.NetworkError; import com.android.volley.NoConnectionError; @@ -44,6 +51,8 @@ import com.android.volley.VolleyError; * onDestroy, onPause and onResume must be called from the activity/fragment */ public class ThreadManager implements ThreadLoader.ThreadLoaderListener, WatchListener { + private static final String TAG = "ThreadManager"; + private final Activity activity; private final ThreadLoader threadLoader; private final ThreadManager.ThreadListener threadListener; @@ -81,7 +90,7 @@ public class ThreadManager implements ThreadLoader.ThreadLoaderListener, WatchLi @Override public void onWatchReloadRequested() { - Logger.test("ThreadManager reload requested"); + Logger.d(TAG, "Reload requested"); if (!threadLoader.isLoading()) { threadLoader.start(loadable); @@ -157,7 +166,7 @@ public class ThreadManager implements ThreadLoader.ThreadLoaderListener, WatchLi public void reload() { if (loadable == null) { - Log.e("Chan", "ThreadManager: loadable null"); + Logger.e(TAG, "ThreadManager: loadable null"); } else { if (loadable.isBoardMode()) { loadable.no = 0; @@ -173,7 +182,7 @@ public class ThreadManager implements ThreadLoader.ThreadLoaderListener, WatchLi if (threadLoader.isLoading()) return; if (loadable == null) { - Log.e("Chan", "ThreadManager: loadable null"); + Logger.e(TAG, "ThreadManager: loadable null"); } else { if (loadable.isBoardMode()) { if (!endOfLine) { @@ -200,7 +209,19 @@ public class ThreadManager implements ThreadLoader.ThreadLoaderListener, WatchLi public void onPostLongClicked(final Post post) { AlertDialog.Builder builder = new AlertDialog.Builder(activity); - builder.setItems(R.array.post_options, new DialogInterface.OnClickListener() { + String[] items = null; + + String[] temp = activity.getResources().getStringArray(R.array.post_options); + // Only add the delete option when the post is a saved reply + if (DatabaseManager.getInstance().isSavedReply(post.board, post.no)) { + items = new String[temp.length + 1]; + System.arraycopy(temp, 0, items, 0, temp.length); + items[items.length - 1] = activity.getString(R.string.delete); + } else { + items = temp; + } + + builder.setItems(items, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { switch(which) { @@ -217,7 +238,10 @@ public class ThreadManager implements ThreadLoader.ThreadLoaderListener, WatchLi showPostLinkables(post); break; case 4: // Copy text - copyText(post.comment.toString()); + copyToClipboard(post.comment.toString()); + break; + case 5: // Delete + deletePost(post); break; } } @@ -242,11 +266,11 @@ public class ThreadManager implements ThreadLoader.ThreadLoaderListener, WatchLi } /** - * Returns an TextView containing the appropiate error message + * Returns an TextView containing the appropriate error message * @param error * @return */ - public TextView getTextViewError(VolleyError error) { + public TextView getLoadErrorTextView(VolleyError error) { String errorMessage = ""; if ((error instanceof NoConnectionError) || (error instanceof NetworkError)) { @@ -266,19 +290,21 @@ public class ThreadManager implements ThreadLoader.ThreadLoaderListener, WatchLi return view; } - private void copyText(String comment) { + private void copyToClipboard(String comment) { ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE); - ClipData clip = ClipData.newPlainText("post text", comment); + ClipData clip = ClipData.newPlainText("Post text", comment); clipboard.setPrimaryClip(clip); } private void showPostInfo(Post post) { - String text = "Time: " + post.date; + String text = ""; if (post.hasImage) { - text += "\nFile: " + post.filename + " \nSize: " + post.imageWidth + "x" + post.imageHeight; + text += "File: " + post.filename + " \nSize: " + post.imageWidth + "x" + post.imageHeight + "\n\n"; } + text += "Time: " + post.date ; + if (!TextUtils.isEmpty(post.id)) { text += "\nId: " + post.id; } @@ -466,7 +492,70 @@ public class ThreadManager implements ThreadLoader.ThreadLoaderListener, WatchLi popupQueue.clear(); currentPopupFragment = null; } - + + private void deletePost(final Post post) { + final CheckBox view = new CheckBox(activity); + view.setText(R.string.delete_image_only); + int padding = activity.getResources().getDimensionPixelSize(R.dimen.general_padding); + view.setPadding(padding, padding, padding, padding); + + new AlertDialog.Builder(activity) + .setTitle(R.string.delete_confirm) + .setView(view) + .setPositiveButton(R.string.delete, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + doDeletePost(post, view.isChecked()); + } + }) + .setNegativeButton(R.string.cancel, new OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + } + }) + .show(); + } + + private void doDeletePost(Post post, boolean onlyImageDelete) { + SavedReply reply = DatabaseManager.getInstance().getSavedReply(post.board, post.no); + if (reply == null) { + /*reply = new SavedReply(); + reply.board = "g"; + reply.no = 1234; + reply.password = "boom";*/ + return; + } + + final ProgressDialog dialog = ProgressDialog.show(activity, null, activity.getString(R.string.delete_wait)); + + ReplyManager.getInstance().sendDelete(reply, onlyImageDelete, new DeleteListener() { + @Override + public void onResponse(DeleteResponse response) { + dialog.dismiss(); + + if (response.isNetworkError || response.isUserError) { + int resId = 0; + + if (response.isTooSoonError) { + resId = R.string.delete_too_soon; + } else if (response.isInvalidPassword) { + resId = R.string.delete_password_incorrect; + } else if (response.isTooOldError) { + resId = R.string.delete_too_old; + } else { + resId = R.string.delete_fail; + } + + Toast.makeText(activity, resId, Toast.LENGTH_LONG).show(); + } else if (response.isSuccessful) { + Toast.makeText(activity, R.string.delete_success, Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(activity, R.string.delete_fail, Toast.LENGTH_LONG).show(); + } + } + }); + } + public interface ThreadListener { public void onThreadLoaded(List result); public void onThreadLoadError(VolleyError error); diff --git a/Chan/src/org/floens/chan/model/Post.java b/Chan/src/org/floens/chan/model/Post.java index daab99e5..8caaf7ef 100644 --- a/Chan/src/org/floens/chan/model/Post.java +++ b/Chan/src/org/floens/chan/model/Post.java @@ -24,12 +24,11 @@ import android.text.style.ForegroundColorSpan; */ public class Post { public String board; - public boolean isOP = false; public int no = -1; public int resto = -1; + public boolean isOP = false; public String date; public String name = ""; - private String rawComment; public CharSequence comment = ""; public String subject = ""; public String tim; @@ -50,6 +49,7 @@ public class Post { public String countryName = ""; public long time = 0; public String email = ""; + public boolean isSavedReply = false; /** * This post replies to the these ids @@ -61,12 +61,14 @@ public class Post { */ public List repliesFrom = new ArrayList(); - private PostView linkableListener; public final ArrayList linkables = new ArrayList(); /** * The PostView the Post is currently bound to. */ + private PostView linkableListener; + private String rawComment; + public Post() { } diff --git a/Chan/src/org/floens/chan/model/Reply.java b/Chan/src/org/floens/chan/model/Reply.java index c9356c53..7c358a1b 100644 --- a/Chan/src/org/floens/chan/model/Reply.java +++ b/Chan/src/org/floens/chan/model/Reply.java @@ -16,4 +16,5 @@ public class Reply { public String fileName = ""; public String captchaChallenge = ""; public String captchaResponse = ""; + public String password = ""; } diff --git a/Chan/src/org/floens/chan/model/SavedReply.java b/Chan/src/org/floens/chan/model/SavedReply.java new file mode 100644 index 00000000..aac86c04 --- /dev/null +++ b/Chan/src/org/floens/chan/model/SavedReply.java @@ -0,0 +1,19 @@ +package org.floens.chan.model; + +import com.j256.ormlite.field.DatabaseField; +import com.j256.ormlite.table.DatabaseTable; + +@DatabaseTable +public class SavedReply { + @DatabaseField(generatedId = true) + private int id; + + @DatabaseField + public String board = ""; + + @DatabaseField + public int no; + + @DatabaseField + public String password = ""; +} diff --git a/Chan/src/org/floens/chan/net/ChanReaderRequest.java b/Chan/src/org/floens/chan/net/ChanReaderRequest.java index bccf73da..2f223192 100644 --- a/Chan/src/org/floens/chan/net/ChanReaderRequest.java +++ b/Chan/src/org/floens/chan/net/ChanReaderRequest.java @@ -4,6 +4,7 @@ import java.io.IOException; import java.util.ArrayList; import java.util.List; +import org.floens.chan.database.DatabaseManager; import org.floens.chan.model.Loadable; import org.floens.chan.model.Post; @@ -80,6 +81,8 @@ public class ChanReaderRequest extends JsonReaderRequest> { post.repliesFrom.add(other.no); } } + + post.isSavedReply = DatabaseManager.getInstance().isSavedReply(post.board, post.no); } } diff --git a/Chan/src/org/floens/chan/net/ChanUrls.java b/Chan/src/org/floens/chan/net/ChanUrls.java index dc35ed23..a41da72c 100644 --- a/Chan/src/org/floens/chan/net/ChanUrls.java +++ b/Chan/src/org/floens/chan/net/ChanUrls.java @@ -40,9 +40,14 @@ public class ChanUrls { } public static String getPostUrl(String board) { - return "https://sys.4chan.org/" + board + "/post"; - // return "http://192.168.6.214/Testing/PostEchoer/post.php"; - } + return "https://sys.4chan.org/" + board + "/post"; +// return "http://192.168.6.214/Testing/PostEchoer/post.php"; + } + + public static String getDeleteUrl(String board) { + return "https://sys.4chan.org/" + board + "/imgboard.php"; +// return "http://192.168.6.214/Testing/PostEchoer/post.php"; + } public static String getBoardUrlDesktop(String board) { return "https://boards.4chan.org/" + board + "/"; diff --git a/Chan/src/org/floens/chan/net/ThreadLoader.java b/Chan/src/org/floens/chan/net/ThreadLoader.java index 7f7ffd29..74e1afb0 100644 --- a/Chan/src/org/floens/chan/net/ThreadLoader.java +++ b/Chan/src/org/floens/chan/net/ThreadLoader.java @@ -5,8 +5,8 @@ import java.util.List; import org.floens.chan.ChanApplication; import org.floens.chan.model.Loadable; import org.floens.chan.model.Post; +import org.floens.chan.utils.Logger; -import android.util.Log; import android.util.SparseArray; import com.android.volley.Response; @@ -14,6 +14,8 @@ import com.android.volley.ServerError; import com.android.volley.VolleyError; public class ThreadLoader { + private static final String TAG = "ThreadLoader"; + private final ThreadLoaderListener listener; private ChanReaderRequest loader; private boolean stopped = false; @@ -34,6 +36,8 @@ public class ThreadLoader { // public void start(int mode, String board, int pageOrThreadId) { public void start(Loadable loadable) { + Logger.i(TAG, "Start loading " + loadable.board + ", " + loadable.no); + stop(); stopped = false; @@ -49,6 +53,8 @@ public class ThreadLoader { } public void stop() { + Logger.i(TAG, "Stop loading"); + if (loader != null) { loader.cancel(); loader = null; @@ -97,7 +103,7 @@ public class ThreadLoader { private void onError(VolleyError error) { if (stopped) return; - Log.e("Chan", "VolleyError in ThreadLoader: " + error.getMessage()); + Logger.e(TAG, "Error loading" + error.getMessage(), error); // 404 with more pages already loaded means endofline if ((error instanceof ServerError) && loadable.isBoardMode() && loadable.no > 0) { diff --git a/Chan/src/org/floens/chan/utils/ChanPreferences.java b/Chan/src/org/floens/chan/utils/ChanPreferences.java index 3d4a019a..46ab40b5 100644 --- a/Chan/src/org/floens/chan/utils/ChanPreferences.java +++ b/Chan/src/org/floens/chan/utils/ChanPreferences.java @@ -22,4 +22,8 @@ public class ChanPreferences { public static void setDeveloper(boolean developer) { ChanApplication.getPreferences().edit().putBoolean("preference_developer", developer).commit(); } + + public static String getImageSaveDirectory() { + return "Chan"; + } } diff --git a/Chan/src/org/floens/chan/utils/Logger.java b/Chan/src/org/floens/chan/utils/Logger.java index 55d86c13..e9e6552a 100644 --- a/Chan/src/org/floens/chan/utils/Logger.java +++ b/Chan/src/org/floens/chan/utils/Logger.java @@ -5,66 +5,66 @@ import org.floens.chan.ChanApplication; import android.util.Log; public class Logger { - public static final String TAG = "Chan"; - public static final String TAG_TEST = "ChanTest"; + private static final String TAG = "Chan"; + private static final String TAG_SPACER = " | "; - public static void v(String message) { - Log.v(TAG, message); + public static void v(String tag, String message) { + Log.v(TAG + TAG_SPACER + tag, message); } - public static void v(String message, Throwable throwable) { - Log.v(TAG, message, throwable); + public static void v(String tag, String message, Throwable throwable) { + Log.v(TAG + TAG_SPACER + tag, message, throwable); } - public static void d(String message) { - Log.d(TAG, message); + public static void d(String tag, String message) { + Log.d(TAG + TAG_SPACER + tag, message); } - public static void d(String message, Throwable throwable) { - Log.d(TAG, message, throwable); + public static void d(String tag, String message, Throwable throwable) { + Log.d(TAG + TAG_SPACER + tag, message, throwable); } - public static void i(String message) { - Log.i(TAG, message); + public static void i(String tag, String message) { + Log.i(TAG + TAG_SPACER + tag, message); } - public static void i(String message, Throwable throwable) { - Log.i(TAG, message, throwable); + public static void i(String tag, String message, Throwable throwable) { + Log.i(TAG + TAG_SPACER + tag, message, throwable); } - public static void w(String message) { - Log.w(TAG, message); + public static void w(String tag, String message) { + Log.w(TAG + TAG_SPACER + tag, message); } - public static void w(String message, Throwable throwable) { - Log.w(TAG, message, throwable); + public static void w(String tag, String message, Throwable throwable) { + Log.w(TAG + TAG_SPACER + tag, message, throwable); } - public static void e(String message) { - Log.e(TAG, message); + public static void e(String tag, String message) { + Log.e(TAG + TAG_SPACER + tag, message); } - public static void e(String message, Throwable throwable) { - Log.e(TAG, message, throwable); + public static void e(String tag, String message, Throwable throwable) { + Log.e(TAG + TAG_SPACER + tag, message, throwable); } - - public static void wtf(String message) { - Log.wtf(TAG, message); + + public static void wtf(String tag, String message) { + Log.wtf(TAG + TAG_SPACER + tag, message); } - public static void wtf(String message, Throwable throwable) { - Log.wtf(TAG, message, throwable); + public static void wtf(String tag, String message, Throwable throwable) { + Log.wtf(TAG + TAG_SPACER + tag, message, throwable); } public static void test(String message) { if (ChanApplication.DEVELOPER_MODE) { - Log.i(TAG_TEST, message); + Log.i(TAG + TAG_SPACER + "test", message); } } public static void test(String message, Throwable throwable) { if (ChanApplication.DEVELOPER_MODE) { - Log.i(TAG_TEST, message, throwable); + Log.i(TAG + TAG_SPACER + "test", message, throwable); } } } diff --git a/Chan/src/org/floens/chan/utils/ViewUtils.java b/Chan/src/org/floens/chan/utils/Utils.java similarity index 59% rename from Chan/src/org/floens/chan/utils/ViewUtils.java rename to Chan/src/org/floens/chan/utils/Utils.java index ff9a7721..9c0be8fe 100644 --- a/Chan/src/org/floens/chan/utils/ViewUtils.java +++ b/Chan/src/org/floens/chan/utils/Utils.java @@ -2,9 +2,11 @@ package org.floens.chan.utils; import android.content.res.TypedArray; import android.graphics.drawable.Drawable; +import android.os.Handler; +import android.os.Looper; import android.view.View; -public class ViewUtils { +public class Utils { @SuppressWarnings("deprecation") public static void setPressedDrawable(View view) { TypedArray arr = view.getContext().obtainStyledAttributes( @@ -16,4 +18,13 @@ public class ViewUtils { arr.recycle(); } + + /** + * Causes the runnable to be added to the message queue. + * The runnable will be run on the ui thread. + * @param runnable + */ + public static void runOnUiThread(Runnable runnable) { + new Handler(Looper.getMainLooper()).post(runnable); + } } diff --git a/Chan/src/org/floens/chan/view/PostView.java b/Chan/src/org/floens/chan/view/PostView.java index c3674f1f..a9285b41 100644 --- a/Chan/src/org/floens/chan/view/PostView.java +++ b/Chan/src/org/floens/chan/view/PostView.java @@ -7,7 +7,7 @@ import org.floens.chan.model.Post; import org.floens.chan.model.PostLinkable; import org.floens.chan.net.ChanUrls; import org.floens.chan.utils.IconCache; -import org.floens.chan.utils.ViewUtils; +import org.floens.chan.utils.Utils; import android.app.Activity; import android.content.Context; @@ -226,7 +226,13 @@ public class PostView extends LinearLayout implements View.OnClickListener, View full.setFocusable(true); full.setOnClickListener(this); - ViewUtils.setPressedDrawable(full); + Utils.setPressedDrawable(full); + } + + if (post.isSavedReply) { + full.setBackgroundColor(0xFFD6BAD0); + } else { + full.setBackgroundColor(0x00000000); } } @@ -308,7 +314,7 @@ public class PostView extends LinearLayout implements View.OnClickListener, View // Set the drawable before the padding, because setting the background resets the padding // This behavior differs with 4.4 / 4.1 - ViewUtils.setPressedDrawable(repliesCountView); + Utils.setPressedDrawable(repliesCountView); repliesCountView.setTextColor(Color.argb(255, 100, 100, 100)); repliesCountView.setPadding(postPadding, postPadding, postPadding, postPadding); diff --git a/Chan/src/org/floens/chan/watch/PinWatcher.java b/Chan/src/org/floens/chan/watch/PinWatcher.java index 8a986681..00a198a5 100644 --- a/Chan/src/org/floens/chan/watch/PinWatcher.java +++ b/Chan/src/org/floens/chan/watch/PinWatcher.java @@ -12,6 +12,8 @@ import org.floens.chan.utils.Logger; import com.android.volley.VolleyError; public class PinWatcher implements ThreadLoader.ThreadLoaderListener { + private static final String TAG = "PinWatcher"; + private final ThreadLoader watchLoader; private final Loadable watchLoadable; @@ -45,12 +47,12 @@ public class PinWatcher implements ThreadLoader.ThreadLoaderListener { public void onData(List result) { int count = result.size(); - Logger.i("PinWatcher onData"); - Logger.i("Post size: " + count); + Logger.test("PinWatcher onData"); + Logger.test("Post size: " + count); pin.watchNewCount = count; - Logger.i("Load time: " + (System.currentTimeMillis() - startTime) + "ms"); + Logger.test("Load time: " + (System.currentTimeMillis() - startTime) + "ms"); PinnedService.callOnPinsChanged(); } diff --git a/Chan/src/org/floens/chan/watch/WatchLogic.java b/Chan/src/org/floens/chan/watch/WatchLogic.java index f11c0d67..61d2f9da 100644 --- a/Chan/src/org/floens/chan/watch/WatchLogic.java +++ b/Chan/src/org/floens/chan/watch/WatchLogic.java @@ -6,6 +6,8 @@ import java.util.TimerTask; import org.floens.chan.utils.Logger; public class WatchLogic { + private static final String TAG = "WatchLogic"; + private static final int[] timeouts = {10, 15, 20, 30, 60, 90, 120, 180, 240, 300}; private WatchListener listener; @@ -23,16 +25,16 @@ public class WatchLogic { * Stops the timer and removes the listener */ public void destroy() { + Logger.d(TAG, "Destroy"); this.listener = null; clearTimer(); - Logger.test("WatchLogic destroy()"); } /** * Starts the timer from the beginning */ public void startTimer() { - Logger.test("WatchLogic timer start"); + Logger.d(TAG, "Start timer"); lastLoadTime = now(); selectedTimeout = 0; @@ -43,7 +45,7 @@ public class WatchLogic { * Stops the timer */ public void stopTimer() { - Logger.test("WatchLogic timer paused"); + Logger.d(TAG, "Stop timer"); clearTimer(); } @@ -66,7 +68,7 @@ public class WatchLogic { * @param postCount how many posts there were loaded */ public void onLoaded(int postCount) { - Logger.test("WatchLogic onLoaded: " + (postCount > lastPostCount)); + Logger.d(TAG, "Loaded. Post count " + postCount + ", last " + lastPostCount); if (postCount > lastPostCount) { selectedTimeout = 0; @@ -90,6 +92,8 @@ public class WatchLogic { } private void scheduleTimer() { + Logger.d(TAG, "Scheduling timer"); + clearTimer(); timer.schedule(new TimerTask() { @@ -106,6 +110,8 @@ public class WatchLogic { * Clear all the scheduled runnables */ private void clearTimer() { + Logger.d(TAG, "Clearing timer"); + timer.cancel(); timer.purge(); timer = new Timer(); diff --git a/docs/4chanresponses.txt b/docs/4chanresponses.txt new file mode 100644 index 00000000..675c7d58 --- /dev/null +++ b/docs/4chanresponses.txt @@ -0,0 +1,8 @@ +Error: No file selected. +Error: You forgot to solve the CAPTCHA. Please try again. +Error: You seem to have mistyped the CAPTCHA. Please try again. + + + +Post successful!

Post successful!

+ diff --git a/docs/delform.txt b/docs/delform.txt new file mode 100644 index 00000000..6b95c23f --- /dev/null +++ b/docs/delform.txt @@ -0,0 +1,24 @@ +https://sys.4chan.org/ BOARD /imgboard.php + +mode=usrdel +res=123456789 +onlyimgdel=on (optional) +pwd=qwerty + +90283402934=delete + +example post: +39894014=delete&mode=usrdel&res=39894014&onlyimgdel=on&pwd=qwerty +39894014=delete&mode=usrdel&res=39894014&pwd=_LfF81U0aBiSgQ3MDj9pkJZYr4N27meWb + + + +Responses: +Error: You cannot delete a post this old. +Error: Password incorrect. +Error: You must wait longer before deleting this post. + +When successful: +Updating index...
Updating index...
+ + diff --git a/docs/images/4chanlogo.png b/docs/images/4chanlogo.png new file mode 100644 index 00000000..87c13cf7 Binary files /dev/null and b/docs/images/4chanlogo.png differ diff --git a/docs/images/Icon512.png b/docs/images/Icon512.png new file mode 100644 index 00000000..efceed9c Binary files /dev/null and b/docs/images/Icon512.png differ diff --git a/docs/images/Icon512.psd b/docs/images/Icon512.psd new file mode 100644 index 00000000..00d74cd9 Binary files /dev/null and b/docs/images/Icon512.psd differ diff --git a/docs/images/IconWhite24.png b/docs/images/IconWhite24.png new file mode 100644 index 00000000..8c6c5783 Binary files /dev/null and b/docs/images/IconWhite24.png differ diff --git a/docs/images/IconWhite36.png b/docs/images/IconWhite36.png new file mode 100644 index 00000000..d8eb73be Binary files /dev/null and b/docs/images/IconWhite36.png differ diff --git a/docs/images/IconWhite400.png b/docs/images/IconWhite400.png new file mode 100644 index 00000000..3a73d0fa Binary files /dev/null and b/docs/images/IconWhite400.png differ diff --git a/docs/images/IconWhite48.png b/docs/images/IconWhite48.png new file mode 100644 index 00000000..7a597f1b Binary files /dev/null and b/docs/images/IconWhite48.png differ diff --git a/docs/images/IconWhite512.psd b/docs/images/IconWhite512.psd new file mode 100644 index 00000000..5359e49f Binary files /dev/null and b/docs/images/IconWhite512.psd differ diff --git a/docs/images/IconWhite72.png b/docs/images/IconWhite72.png new file mode 100644 index 00000000..0bae2adf Binary files /dev/null and b/docs/images/IconWhite72.png differ diff --git a/docs/images/Screenshot_1.png b/docs/images/Screenshot_1.png new file mode 100644 index 00000000..5aeb8516 Binary files /dev/null and b/docs/images/Screenshot_1.png differ diff --git a/docs/images/Screenshot_2.png b/docs/images/Screenshot_2.png new file mode 100644 index 00000000..30ec85d4 Binary files /dev/null and b/docs/images/Screenshot_2.png differ diff --git a/docs/images/new screenshots/Screenshot_2014-02-23-00-01-02.png b/docs/images/new screenshots/Screenshot_2014-02-23-00-01-02.png new file mode 100644 index 00000000..8753e59a Binary files /dev/null and b/docs/images/new screenshots/Screenshot_2014-02-23-00-01-02.png differ diff --git a/docs/images/new screenshots/Screenshot_2014-02-23-00-01-29.png b/docs/images/new screenshots/Screenshot_2014-02-23-00-01-29.png new file mode 100644 index 00000000..ba061edf Binary files /dev/null and b/docs/images/new screenshots/Screenshot_2014-02-23-00-01-29.png differ diff --git a/docs/images/new screenshots/Screenshot_2014-02-23-00-01-44.png b/docs/images/new screenshots/Screenshot_2014-02-23-00-01-44.png new file mode 100644 index 00000000..e0a23cdd Binary files /dev/null and b/docs/images/new screenshots/Screenshot_2014-02-23-00-01-44.png differ