diff --git a/Clover/app/proguard.cfg b/Clover/app/proguard.cfg
index 11489a3d..95702e91 100644
--- a/Clover/app/proguard.cfg
+++ b/Clover/app/proguard.cfg
@@ -149,3 +149,5 @@
}
-keep public class * extends android.support.design.**
+
+-keep public class android.support.v7.widget.RecyclerView
diff --git a/Clover/app/src/main/AndroidManifest.xml b/Clover/app/src/main/AndroidManifest.xml
index 25e0e102..bdc92a86 100644
--- a/Clover/app/src/main/AndroidManifest.xml
+++ b/Clover/app/src/main/AndroidManifest.xml
@@ -15,14 +15,15 @@ GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
-->
-
-
-
-
-
+
+
+
+
.
android:label="@string/app_name">
-
-
+
+
-
-
+
+
-
-
+
+
-
-
-
-
-
+
+
+
+
+
-
+
+ android:exported="false"/>
+
+
diff --git a/Clover/app/src/main/java/org/floens/chan/core/cache/FileCache.java b/Clover/app/src/main/java/org/floens/chan/core/cache/FileCache.java
index be2ae6e4..1f7698ae 100644
--- a/Clover/app/src/main/java/org/floens/chan/core/cache/FileCache.java
+++ b/Clover/app/src/main/java/org/floens/chan/core/cache/FileCache.java
@@ -295,6 +295,10 @@ public class FileCache {
}
}
+ public Future> getFuture() {
+ return future;
+ }
+
private void setFuture(Future> future) {
this.future = future;
}
diff --git a/Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaveTask.java b/Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaveTask.java
new file mode 100644
index 00000000..d7ea8132
--- /dev/null
+++ b/Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaveTask.java
@@ -0,0 +1,219 @@
+/*
+ * Clover - 4chan browser https://github.com/Floens/Clover/
+ * Copyright (C) 2014 Floens
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.floens.chan.core.saver;
+
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.media.MediaScannerConnection;
+import android.net.Uri;
+
+import org.floens.chan.Chan;
+import org.floens.chan.core.cache.FileCache;
+import org.floens.chan.core.model.PostImage;
+import org.floens.chan.utils.AndroidUtils;
+import org.floens.chan.utils.IOUtils;
+import org.floens.chan.utils.ImageDecoder;
+import org.floens.chan.utils.Logger;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import static org.floens.chan.utils.AndroidUtils.dp;
+import static org.floens.chan.utils.AndroidUtils.getAppContext;
+
+public class ImageSaveTask implements Runnable, FileCache.DownloadedCallback {
+ private static final String TAG = "ImageSaveTask";
+
+ private PostImage postImage;
+ private ImageSaveTaskCallback callback;
+ private File destination;
+ private boolean share;
+ private boolean makeBitmap;
+ private Bitmap bitmap;
+ private boolean showToast;
+
+ private boolean success = false;
+
+ public ImageSaveTask(PostImage postImage) {
+ this.postImage = postImage;
+ }
+
+ public void setCallback(ImageSaveTaskCallback callback) {
+ this.callback = callback;
+ }
+
+ public PostImage getPostImage() {
+ return postImage;
+ }
+
+ public void setDestination(File destination) {
+ this.destination = destination;
+ }
+
+ public File getDestination() {
+ return destination;
+ }
+
+ public void setShare(boolean share) {
+ this.share = share;
+ }
+
+ public void setMakeBitmap(boolean makeBitmap) {
+ this.makeBitmap = makeBitmap;
+ }
+
+ public boolean isMakeBitmap() {
+ return makeBitmap;
+ }
+
+ public Bitmap getBitmap() {
+ return bitmap;
+ }
+
+ public void setShowToast(boolean showToast) {
+ this.showToast = showToast;
+ }
+
+ public boolean isShowToast() {
+ return showToast;
+ }
+
+ @Override
+ public void run() {
+ try {
+ Logger.test("Start! " + toString());
+
+ if (destination.exists()) {
+ onDestination();
+ } else {
+ FileCache.FileCacheDownloader fileCacheDownloader = Chan.getFileCache().downloadFile(postImage.imageUrl, this);
+ // If the fileCacheDownloader is null then the callbacks were already executed here,
+ // else wait for the download to finish to avoid that the next task is immediately executed.
+ if (fileCacheDownloader != null) {
+ // If the file is now downloading
+ fileCacheDownloader.getFuture().get();
+ }
+ }
+
+ Logger.test("End! " + toString());
+ } catch (InterruptedException e) {
+ onInterrupted();
+ } catch (Exception e) {
+ Logger.e(TAG, "Uncaught exception", e);
+ } finally {
+ postFinished(success);
+ }
+ }
+
+ @Override
+ public void onProgress(long downloaded, long total, boolean done) {
+ }
+
+ @Override
+ public void onFail(boolean notFound) {
+ }
+
+ @Override
+ public void onSuccess(File file) {
+ copyToDestination(file);
+ onDestination();
+ }
+
+ private void onInterrupted() {
+ if (destination.exists()) {
+ if (!destination.delete()) {
+ Logger.e(TAG, "Could not delete destination after an interrupt");
+ }
+ }
+ }
+
+ private void onDestination() {
+ scanDestination();
+ if (makeBitmap) {
+ bitmap = ImageDecoder.decodeFile(destination, dp(512), dp(256));
+ }
+ }
+
+ private void copyToDestination(File source) {
+ InputStream is = null;
+ OutputStream os = null;
+ try {
+ File parent = destination.getParentFile();
+ if (!parent.mkdirs() && !parent.isDirectory()) {
+ throw new IOException("Could not create parent directory");
+ }
+
+ if (destination.isDirectory()) {
+ throw new IOException("Destination file is already a directory");
+ }
+
+ is = new FileInputStream(source);
+ os = new FileOutputStream(destination);
+ IOUtils.copy(is, os);
+ } catch (IOException e) {
+ Logger.e(TAG, "Error writing to file", e);
+ } finally {
+ IOUtils.closeQuietly(is);
+ IOUtils.closeQuietly(os);
+ }
+ success = true;
+ }
+
+ private void scanDestination() {
+ MediaScannerConnection.scanFile(getAppContext(), new String[]{destination.getAbsolutePath()}, null, new MediaScannerConnection.OnScanCompletedListener() {
+ @Override
+ public void onScanCompleted(String path, final Uri uri) {
+ // Runs on a binder thread
+ AndroidUtils.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ afterScan(uri);
+ }
+ });
+ }
+ });
+ }
+
+ private void afterScan(final Uri uri) {
+ Logger.d(TAG, "Media scan succeeded: " + uri);
+
+ if (share) {
+ Intent intent = new Intent(Intent.ACTION_SEND);
+ intent.setType("image/*");
+ intent.putExtra(Intent.EXTRA_STREAM, uri);
+ AndroidUtils.openIntent(intent);
+ }
+ }
+
+ private void postFinished(final boolean success) {
+ AndroidUtils.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ callback.imageSaveTaskFinished(ImageSaveTask.this, success);
+ }
+ });
+ }
+
+ public interface ImageSaveTaskCallback {
+ void imageSaveTaskFinished(ImageSaveTask task, boolean success);
+ }
+}
diff --git a/Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaver.java b/Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaver.java
new file mode 100644
index 00000000..d3625e32
--- /dev/null
+++ b/Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaver.java
@@ -0,0 +1,213 @@
+/*
+ * Clover - 4chan browser https://github.com/Floens/Clover/
+ * Copyright (C) 2014 Floens
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.floens.chan.core.saver;
+
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.Intent;
+import android.support.v7.app.NotificationCompat;
+import android.widget.Toast;
+
+import org.floens.chan.R;
+import org.floens.chan.core.model.PostImage;
+import org.floens.chan.core.settings.ChanSettings;
+import org.floens.chan.ui.service.SavingNotification;
+
+import java.io.File;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.regex.Pattern;
+
+import de.greenrobot.event.EventBus;
+
+import static org.floens.chan.utils.AndroidUtils.getAppContext;
+import static org.floens.chan.utils.AndroidUtils.getString;
+
+public class ImageSaver implements ImageSaveTask.ImageSaveTaskCallback {
+ private static final String TAG = "ImageSaver";
+ private static final int NOTIFICATION_ID = 3;
+ private static final int MAX_NAME_LENGTH = 50;
+ private static final Pattern REPEATED_UNDERSCORES_PATTERN = Pattern.compile("_+");
+ private static final Pattern SAFE_CHARACTERS_PATTERN = Pattern.compile("[^a-zA-Z0-9._]");
+ private static final ImageSaver instance = new ImageSaver();
+
+ private NotificationManager notificationManager;
+ private ExecutorService executor = Executors.newSingleThreadExecutor();
+ private int doneTasks = 0;
+ private int totalTasks = 0;
+
+ public static ImageSaver getInstance() {
+ return instance;
+ }
+
+ private ImageSaver() {
+ EventBus.getDefault().register(this);
+ notificationManager = (NotificationManager) getAppContext().getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ public void startBundledTask(String subFolder, List tasks) {
+ for (ImageSaveTask task : tasks) {
+ PostImage postImage = task.getPostImage();
+ String fileName = filterName(postImage.originalName + "." + postImage.extension);
+ task.setDestination(new File(getSaveLocation() + File.separator + subFolder + File.separator + fileName));
+
+ startTask(task);
+ }
+ updateNotification();
+ }
+
+ public String getSubFolder(String name) {
+ String filtered = filterName(name);
+ filtered = filtered.substring(0, Math.min(filtered.length(), MAX_NAME_LENGTH));
+ return filtered;
+ }
+
+ public void startDownloadTask(ImageSaveTask task) {
+ PostImage postImage = task.getPostImage();
+ String name = ChanSettings.saveOriginalFilename.get() ? postImage.originalName : postImage.filename;
+ String fileName = filterName(name + "." + postImage.extension);
+ task.setDestination(findUnusedFileName(new File(getSaveLocation(), fileName), false));
+
+// task.setMakeBitmap(true);
+ task.setShowToast(true);
+
+ startTask(task);
+ updateNotification();
+ }
+
+ public File getSaveLocation() {
+ return new File(ChanSettings.saveLocation.get());
+ }
+
+ @Override
+ public void imageSaveTaskFinished(ImageSaveTask task, boolean success) {
+ doneTasks++;
+ if (doneTasks == totalTasks) {
+ totalTasks = 0;
+ doneTasks = 0;
+ }
+ updateNotification();
+
+ if (task.isMakeBitmap()) {
+ showImageSaved(task);
+ }
+ if (task.isShowToast()) {
+ showToast(task);
+ }
+ }
+
+ public void onEvent(SavingNotification.SavingCancelRequestMessage message) {
+ cancelAll();
+ }
+
+ private void startTask(ImageSaveTask task) {
+ task.setCallback(this);
+
+ totalTasks++;
+ executor.execute(task);
+ }
+
+ private void cancelAll() {
+ executor.shutdownNow();
+ executor = Executors.newSingleThreadExecutor();
+
+ totalTasks = 0;
+ doneTasks = 0;
+ updateNotification();
+ }
+
+ private void updateNotification() {
+ Intent service = new Intent(getAppContext(), SavingNotification.class);
+ if (totalTasks == 0) {
+ getAppContext().stopService(service);
+ } else {
+ service.putExtra(SavingNotification.DONE_TASKS_KEY, doneTasks);
+ service.putExtra(SavingNotification.TOTAL_TASKS_KEY, totalTasks);
+ getAppContext().startService(service);
+ }
+ }
+
+ private void showImageSaved(ImageSaveTask task) {
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(getAppContext());
+ builder.setSmallIcon(R.drawable.ic_stat_notify);
+ builder.setContentTitle(getString(R.string.image_save_saved));
+ String savedAs = getAppContext().getString(R.string.image_save_as, task.getDestination().getName());
+ builder.setContentText(savedAs);
+ builder.setPriority(NotificationCompat.PRIORITY_HIGH);
+ builder.setStyle(new NotificationCompat.BigPictureStyle()
+ .bigPicture(task.getBitmap())
+ .setSummaryText(savedAs));
+
+ notificationManager.notify(NOTIFICATION_ID, builder.build());
+ }
+
+ private void showToast(ImageSaveTask task) {
+ String savedAs = getAppContext().getString(R.string.image_save_as, task.getDestination().getName());
+ Toast.makeText(getAppContext(), savedAs, Toast.LENGTH_LONG).show();
+ }
+
+ private String filterName(String name) {
+ name = name.replace(' ', '_');
+ name = SAFE_CHARACTERS_PATTERN.matcher(name).replaceAll("");
+ name = REPEATED_UNDERSCORES_PATTERN.matcher(name).replaceAll("_");
+ if (name.length() == 0) {
+ name = "_";
+ }
+ return name;
+ }
+
+ private File findUnusedFileName(File start, boolean directory) {
+ String base;
+ String extension;
+
+ if (directory) {
+ base = start.getAbsolutePath();
+ extension = null;
+ } else {
+ String[] splitted = start.getAbsolutePath().split("\\.(?=[^\\.]+$)");
+ if (splitted.length == 2) {
+ base = splitted[0];
+ extension = "." + splitted[1];
+ } else {
+ base = splitted[0];
+ extension = ".";
+ }
+ }
+
+ File test;
+ if (directory) {
+ test = new File(base);
+ } else {
+ test = new File(base + extension);
+ }
+
+ int index = 0;
+ int tries = 0;
+ while (test.exists() && tries++ < 100) {
+ if (directory) {
+ test = new File(base + "_" + index);
+ } else {
+ test = new File(base + "_" + index + extension);
+ }
+ index++;
+ }
+
+ return test;
+ }
+}
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/AlbumDownloadController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/AlbumDownloadController.java
new file mode 100644
index 00000000..2207f06b
--- /dev/null
+++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/AlbumDownloadController.java
@@ -0,0 +1,271 @@
+/*
+ * Clover - 4chan browser https://github.com/Floens/Clover/
+ * Copyright (C) 2014 Floens
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.floens.chan.ui.controller;
+
+import android.content.Context;
+import android.content.DialogInterface;
+import android.graphics.drawable.Drawable;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v4.graphics.drawable.DrawableCompat;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.ImageView;
+
+import org.floens.chan.R;
+import org.floens.chan.controller.Controller;
+import org.floens.chan.core.model.Loadable;
+import org.floens.chan.core.model.PostImage;
+import org.floens.chan.core.saver.ImageSaveTask;
+import org.floens.chan.core.saver.ImageSaver;
+import org.floens.chan.ui.theme.ThemeHelper;
+import org.floens.chan.ui.toolbar.ToolbarMenu;
+import org.floens.chan.ui.toolbar.ToolbarMenuItem;
+import org.floens.chan.ui.view.FloatingMenuItem;
+import org.floens.chan.ui.view.GridRecyclerView;
+import org.floens.chan.ui.view.ThumbnailView;
+import org.floens.chan.utils.RecyclerUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import static org.floens.chan.utils.AndroidUtils.dp;
+
+public class AlbumDownloadController extends Controller implements ToolbarMenuItem.ToolbarMenuItemCallback, View.OnClickListener {
+ private static final int CHECK_ALL = 1;
+
+ private GridRecyclerView recyclerView;
+ private GridLayoutManager gridLayoutManager;
+ private FloatingActionButton download;
+
+ private List items = new ArrayList<>();
+ private Loadable loadable;
+
+ private boolean allChecked = true;
+ private AlbumAdapter adapter;
+ private ImageSaver imageSaver;
+
+ public AlbumDownloadController(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ imageSaver = ImageSaver.getInstance();
+
+ view = inflateRes(R.layout.controller_album_download);
+
+ updateTitle();
+
+ navigationItem.menu = new ToolbarMenu(context);
+ navigationItem.menu.addItem(new ToolbarMenuItem(context, this, CHECK_ALL, R.drawable.ic_select_all_white_24dp));
+
+ download = (FloatingActionButton) view.findViewById(R.id.download);
+ download.setOnClickListener(this);
+ recyclerView = (GridRecyclerView) view.findViewById(R.id.recycler_view);
+ recyclerView.setHasFixedSize(true);
+ gridLayoutManager = new GridLayoutManager(context, 3);
+ recyclerView.setLayoutManager(gridLayoutManager);
+ recyclerView.setSpanWidth(dp(90));
+
+ adapter = new AlbumAdapter();
+ recyclerView.setAdapter(adapter);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == download) {
+ int checkCount = getCheckCount();
+ if (checkCount == 0) {
+ new AlertDialog.Builder(context)
+ .setMessage(R.string.album_download_none_checked)
+ .setPositiveButton(R.string.ok, null)
+ .show();
+ } else {
+ final String folderForAlbum = imageSaver.getSubFolder(loadable.title);
+
+ String message = context.getString(R.string.album_download_confirm,
+ context.getResources().getQuantityString(R.plurals.image, checkCount, checkCount),
+ folderForAlbum);
+
+ new AlertDialog.Builder(context)
+ .setMessage(message)
+ .setNegativeButton(R.string.cancel, null)
+ .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ List tasks = new ArrayList<>(items.size());
+ for (AlbumDownloadItem item : items) {
+ if (item.checked) {
+ tasks.add(new ImageSaveTask(item.postImage));
+ }
+ }
+
+ imageSaver.startBundledTask(folderForAlbum, tasks);
+ navigationController.popController();
+ }
+ })
+ .show();
+ }
+ }
+ }
+
+ @Override
+ public void onMenuItemClicked(ToolbarMenuItem menuItem) {
+ if ((Integer) menuItem.getId() == CHECK_ALL) {
+ RecyclerUtils.clearRecyclerCache(recyclerView);
+
+ for (int i = 0, itemsSize = items.size(); i < itemsSize; i++) {
+ AlbumDownloadItem item = items.get(i);
+ if (item.checked == allChecked) {
+ item.checked = !allChecked;
+ AlbumDownloadCell cell = (AlbumDownloadCell) recyclerView.findViewHolderForAdapterPosition(i);
+ if (cell != null) {
+ setItemChecked(cell, item.checked, true);
+ }
+ }
+ }
+ updateAllChecked();
+ updateTitle();
+ }
+ }
+
+ @Override
+ public void onSubMenuItemClicked(ToolbarMenuItem parent, FloatingMenuItem item) {
+ }
+
+ public void setPostImages(Loadable loadable, List postImages) {
+ this.loadable = loadable;
+ for (int i = 0, postImagesSize = postImages.size(); i < postImagesSize; i++) {
+ PostImage postImage = postImages.get(i);
+ items.add(new AlbumDownloadItem(postImage, true, i));
+ }
+ }
+
+ private void updateTitle() {
+ navigationItem.title = context.getString(R.string.album_download_screen, getCheckCount(), items.size());
+ navigationItem.updateTitle();
+ }
+
+ private void updateAllChecked() {
+ allChecked = getCheckCount() == items.size();
+ }
+
+ private int getCheckCount() {
+ int checkCount = 0;
+ for (AlbumDownloadItem item : items) {
+ if (item.checked) {
+ checkCount++;
+ }
+ }
+ return checkCount;
+ }
+
+ private static class AlbumDownloadItem {
+ public PostImage postImage;
+ public boolean checked;
+ public int id;
+
+ public AlbumDownloadItem(PostImage postImage, boolean checked, int id) {
+ this.postImage = postImage;
+ this.checked = checked;
+ this.id = id;
+ }
+ }
+
+ private class AlbumAdapter extends RecyclerView.Adapter {
+ public AlbumAdapter() {
+ setHasStableIds(true);
+ }
+
+ @Override
+ public AlbumDownloadCell onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new AlbumDownloadCell(LayoutInflater.from(parent.getContext()).inflate(R.layout.cell_album_download, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(AlbumDownloadCell holder, int position) {
+ AlbumDownloadItem item = items.get(position);
+
+ holder.thumbnailView.setUrl(item.postImage.thumbnailUrl, dp(100), dp(100));
+ setItemChecked(holder, item.checked, false);
+ }
+
+ @Override
+ public int getItemCount() {
+ return items.size();
+ }
+
+ @Override
+ public long getItemId(int position) {
+ return items.get(position).id;
+ }
+ }
+
+ private class AlbumDownloadCell extends RecyclerView.ViewHolder implements View.OnClickListener {
+ private ImageView checkbox;
+ private ThumbnailView thumbnailView;
+
+ public AlbumDownloadCell(View itemView) {
+ super(itemView);
+ itemView.getLayoutParams().height = recyclerView.getRealSpanWidth();
+ checkbox = (ImageView) itemView.findViewById(R.id.checkbox);
+ thumbnailView = (ThumbnailView) itemView.findViewById(R.id.thumbnail_view);
+ itemView.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ int adapterPosition = getAdapterPosition();
+ AlbumDownloadItem item = items.get(adapterPosition);
+ item.checked = !item.checked;
+ updateAllChecked();
+ updateTitle();
+ setItemChecked(this, item.checked, true);
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ private void setItemChecked(AlbumDownloadCell cell, boolean checked, boolean animated) {
+ float scale = checked ? 0.75f : 1f;
+ if (animated) {
+ cell.thumbnailView.animate().scaleX(scale).scaleY(scale)
+ .setInterpolator(new DecelerateInterpolator(3f)).setDuration(500).start();
+ } else {
+ cell.thumbnailView.setScaleX(scale);
+ cell.thumbnailView.setScaleY(scale);
+ }
+
+ Drawable drawable = context.getResources().getDrawable(checked ? R.drawable.ic_check_circle_white_24dp :
+ R.drawable.ic_radio_button_unchecked_white_24dp);
+
+ if (checked) {
+ Drawable wrapped = DrawableCompat.wrap(drawable);
+ DrawableCompat.setTint(wrapped, ThemeHelper.PrimaryColor.BLUE.color);
+ cell.checkbox.setImageDrawable(wrapped);
+ } else {
+ cell.checkbox.setImageDrawable(drawable);
+ }
+ }
+}
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java
index b1ee40ec..ccb72aca 100644
--- a/Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java
+++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java
@@ -32,7 +32,6 @@ import android.graphics.Color;
import android.graphics.Point;
import android.graphics.PointF;
import android.os.Build;
-import android.text.TextUtils;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
@@ -53,6 +52,7 @@ import org.floens.chan.chan.ImageSearch;
import org.floens.chan.controller.Controller;
import org.floens.chan.core.model.PostImage;
import org.floens.chan.core.presenter.ImageViewerPresenter;
+import org.floens.chan.core.saver.ImageSaveTask;
import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.ui.adapter.ImageViewerAdapter;
import org.floens.chan.ui.toolbar.Toolbar;
@@ -67,7 +67,7 @@ import org.floens.chan.ui.view.OptionalSwipeViewPager;
import org.floens.chan.ui.view.ThumbnailView;
import org.floens.chan.ui.view.TransitionImageView;
import org.floens.chan.utils.AndroidUtils;
-import org.floens.chan.utils.ImageSaver;
+import org.floens.chan.core.saver.ImageSaver;
import org.floens.chan.utils.Logger;
import java.util.ArrayList;
@@ -190,20 +190,9 @@ public class ImageViewerController extends Controller implements View.OnClickLis
break;
case SAVE_ALBUM:
List all = presenter.getAllPostImages();
- List list = new ArrayList<>();
-
- String folderName = presenter.getLoadable().title;
- if (TextUtils.isEmpty(folderName)) {
- folderName = String.valueOf(presenter.getLoadable().no);
- }
-
- String filename;
- for (PostImage post : all) {
- filename = (ChanSettings.saveOriginalFilename.get() ? postImage.originalName : postImage.filename) + "." + post.extension;
- list.add(new ImageSaver.DownloadPair(post.imageUrl, filename));
- }
-
- ImageSaver.getInstance().saveAll(context, folderName, list);
+ AlbumDownloadController albumDownloadController = new AlbumDownloadController(context);
+ albumDownloadController.setPostImages(presenter.getLoadable(), all);
+ navigationController.pushController(albumDownloadController);
break;
}
}
@@ -212,10 +201,9 @@ public class ImageViewerController extends Controller implements View.OnClickLis
if (share && ChanSettings.shareUrl.get()) {
AndroidUtils.shareLink(postImage.imageUrl);
} else {
- ImageSaver.getInstance().saveImage(context, postImage.imageUrl,
- ChanSettings.saveOriginalFilename.get() ? postImage.originalName : postImage.filename,
- postImage.extension,
- share);
+ ImageSaveTask task = new ImageSaveTask(postImage);
+ task.setShare(share);
+ ImageSaver.getInstance().startDownloadTask(task);
}
}
@@ -468,7 +456,7 @@ public class ImageViewerController extends Controller implements View.OnClickLis
}
private void setBackgroundAlpha(float alpha) {
- view.setBackgroundColor(Color.argb((int) (alpha * TRANSITION_FINAL_ALPHA * 255f), 0, 0, 0));
+ navigationController.view.setBackgroundColor(Color.argb((int) (alpha * TRANSITION_FINAL_ALPHA * 255f), 0, 0, 0));
if (Build.VERSION.SDK_INT >= 21) {
if (alpha == 0f) {
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java
index 5dc3497f..413cd9e9 100644
--- a/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java
+++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java
@@ -253,7 +253,7 @@ public class ThreadLayout extends CoordinatorLayout implements ThreadPresenter.T
}
public void clipboardPost(Post post) {
- ClipboardManager clipboard = (ClipboardManager) AndroidUtils.getAppRes().getSystemService(Context.CLIPBOARD_SERVICE);
+ ClipboardManager clipboard = (ClipboardManager) AndroidUtils.getAppContext().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("Post text", post.comment.toString());
clipboard.setPrimaryClip(clip);
Toast.makeText(getContext(), R.string.post_text_copied, Toast.LENGTH_SHORT).show();
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/service/SavingNotification.java b/Clover/app/src/main/java/org/floens/chan/ui/service/SavingNotification.java
new file mode 100644
index 00000000..0e9a81f0
--- /dev/null
+++ b/Clover/app/src/main/java/org/floens/chan/ui/service/SavingNotification.java
@@ -0,0 +1,108 @@
+/*
+ * Clover - 4chan browser https://github.com/Floens/Clover/
+ * Copyright (C) 2014 Floens
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.floens.chan.ui.service;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.IBinder;
+import android.support.annotation.Nullable;
+import android.support.v7.app.NotificationCompat;
+
+import org.floens.chan.R;
+
+import de.greenrobot.event.EventBus;
+
+import static org.floens.chan.utils.AndroidUtils.getAppContext;
+
+public class SavingNotification extends Service {
+ public static final String DONE_TASKS_KEY = "done_tasks";
+ public static final String TOTAL_TASKS_KEY = "total_tasks";
+ private static final String CANCEL_KEY = "cancel";
+
+ private static final int NOTIFICATION_ID = 2;
+
+ private NotificationManager notificationManager;
+
+ private boolean inForeground = false;
+ private int doneTasks;
+ private int totalTasks;
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ notificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ notificationManager.cancel(NOTIFICATION_ID);
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent != null && intent.getExtras() != null) {
+ Bundle extras = intent.getExtras();
+
+ if (extras.getBoolean(CANCEL_KEY)) {
+ EventBus.getDefault().post(new SavingCancelRequestMessage());
+ } else {
+ doneTasks = extras.getInt(DONE_TASKS_KEY);
+ totalTasks = extras.getInt(TOTAL_TASKS_KEY);
+
+ if (!inForeground) {
+ startForeground(NOTIFICATION_ID, getNotification());
+ inForeground = true;
+ } else {
+ notificationManager.notify(NOTIFICATION_ID, getNotification());
+ }
+ }
+ }
+
+ return START_STICKY;
+ }
+
+ private Notification getNotification() {
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(getAppContext());
+ builder.setSmallIcon(R.drawable.ic_stat_notify);
+ builder.setContentTitle(getString(R.string.image_save_notification_downloading));
+ builder.setContentText(getString(R.string.image_save_notification_cancel));
+ builder.setProgress(totalTasks, doneTasks, false);
+ builder.setContentInfo(doneTasks + "/" + totalTasks);
+
+ Intent intent = new Intent(this, SavingNotification.class);
+ intent.putExtra(CANCEL_KEY, true);
+ PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+ builder.setContentIntent(pendingIntent);
+
+ return builder.build();
+ }
+
+ public static class SavingCancelRequestMessage {
+ }
+}
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/theme/Theme.java b/Clover/app/src/main/java/org/floens/chan/ui/theme/Theme.java
index b6c2b406..c17d1d2c 100644
--- a/Clover/app/src/main/java/org/floens/chan/ui/theme/Theme.java
+++ b/Clover/app/src/main/java/org/floens/chan/ui/theme/Theme.java
@@ -88,7 +88,7 @@ public class Theme {
}
private void resolveSpanColors() {
- Resources.Theme theme = AndroidUtils.getAppRes().getResources().newTheme();
+ Resources.Theme theme = AndroidUtils.getAppContext().getResources().newTheme();
theme.applyStyle(R.style.Chan_Theme, true);
theme.applyStyle(resValue, true);
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/view/GridRecyclerView.java b/Clover/app/src/main/java/org/floens/chan/ui/view/GridRecyclerView.java
new file mode 100644
index 00000000..421192ff
--- /dev/null
+++ b/Clover/app/src/main/java/org/floens/chan/ui/view/GridRecyclerView.java
@@ -0,0 +1,75 @@
+/*
+ * Clover - 4chan browser https://github.com/Floens/Clover/
+ * Copyright (C) 2014 Floens
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.floens.chan.ui.view;
+
+import android.content.Context;
+import android.support.v7.widget.GridLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+
+/**
+ * A RecyclerView with a GridLayoutManager that manages the span count by dividing the width of the
+ * view with the value set by {@link #setSpanWidth(int)}.
+ */
+public class GridRecyclerView extends RecyclerView {
+ private GridLayoutManager gridLayoutManager;
+ private int spanWidth;
+ private int realSpanWidth;
+
+ public GridRecyclerView(Context context) {
+ super(context);
+ }
+
+ public GridRecyclerView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public GridRecyclerView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public void setLayoutManager(GridLayoutManager gridLayoutManager) {
+ this.gridLayoutManager = gridLayoutManager;
+ super.setLayoutManager(gridLayoutManager);
+ }
+
+ /**
+ * Set the width of each span in pixels.
+ *
+ * @param spanWidth width of each span in pixels.
+ */
+ public void setSpanWidth(int spanWidth) {
+ this.spanWidth = spanWidth;
+ }
+
+ public int getRealSpanWidth() {
+ return realSpanWidth;
+ }
+
+ @Override
+ protected void onMeasure(int widthSpec, int heightSpec) {
+ super.onMeasure(widthSpec, heightSpec);
+ int spanCount = Math.max(1, getMeasuredWidth() / spanWidth);
+ gridLayoutManager.setSpanCount(spanCount);
+ int oldRealSpanWidth = realSpanWidth;
+ realSpanWidth = getMeasuredWidth() / spanCount;
+ if (realSpanWidth != oldRealSpanWidth) {
+ getAdapter().notifyDataSetChanged();
+ }
+ }
+}
diff --git a/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java b/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java
index f5d7a1ce..33f24f5f 100644
--- a/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java
+++ b/Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java
@@ -69,14 +69,14 @@ public class AndroidUtils {
ROBOTO_MEDIUM = getTypeface("Roboto-Medium.ttf");
ROBOTO_MEDIUM_ITALIC = getTypeface("Roboto-MediumItalic.ttf");
- connectivityManager = (ConnectivityManager) getAppRes().getSystemService(Context.CONNECTIVITY_SERVICE);
+ connectivityManager = (ConnectivityManager) getAppContext().getSystemService(Context.CONNECTIVITY_SERVICE);
}
public static Resources getRes() {
return Chan.con.getResources();
}
- public static Context getAppRes() {
+ public static Context getAppContext() {
return Chan.con;
}
@@ -108,12 +108,12 @@ public class AndroidUtils {
* @param link url to open
*/
public static void openLink(String link) {
- PackageManager pm = getAppRes().getPackageManager();
+ PackageManager pm = getAppContext().getPackageManager();
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link));
ComponentName resolvedActivity = intent.resolveActivity(pm);
- boolean thisAppIsDefault = resolvedActivity.getPackageName().equals(getAppRes().getPackageName());
+ boolean thisAppIsDefault = resolvedActivity.getPackageName().equals(getAppContext().getPackageName());
if (!thisAppIsDefault) {
openIntent(intent);
} else {
@@ -121,7 +121,7 @@ public class AndroidUtils {
List resolveInfos = pm.queryIntentActivities(intent, 0);
List filteredIntents = new ArrayList<>(resolveInfos.size());
for (ResolveInfo info : resolveInfos) {
- if (!info.activityInfo.packageName.equals(getAppRes().getPackageName())) {
+ if (!info.activityInfo.packageName.equals(getAppContext().getPackageName())) {
Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(link));
i.setPackage(info.activityInfo.packageName);
filteredIntents.add(i);
@@ -149,15 +149,15 @@ public class AndroidUtils {
public static void openIntent(Intent intent) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- if (intent.resolveActivity(getAppRes().getPackageManager()) != null) {
- getAppRes().startActivity(intent);
+ if (intent.resolveActivity(getAppContext().getPackageManager()) != null) {
+ getAppContext().startActivity(intent);
} else {
openIntentFailed();
}
}
private static void openIntentFailed() {
- Toast.makeText(getAppRes(), R.string.open_link_failed, Toast.LENGTH_LONG).show();
+ Toast.makeText(getAppContext(), R.string.open_link_failed, Toast.LENGTH_LONG).show();
}
public static int getAttrColor(Context context, int attr) {
diff --git a/Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java b/Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java
deleted file mode 100644
index ea361bb7..00000000
--- a/Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java
+++ /dev/null
@@ -1,307 +0,0 @@
-/*
- * Clover - 4chan browser https://github.com/Floens/Clover/
- * Copyright (C) 2014 Floens
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.floens.chan.utils;
-
-import android.app.AlertDialog;
-import android.app.DownloadManager;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.media.MediaScannerConnection;
-import android.net.Uri;
-import android.widget.Toast;
-
-import org.floens.chan.Chan;
-import org.floens.chan.R;
-import org.floens.chan.core.cache.FileCache;
-import org.floens.chan.core.settings.ChanSettings;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
-
-public class ImageSaver {
- private static final String TAG = "ImageSaver";
- private static final ImageSaver instance = new ImageSaver();
-
- public static ImageSaver getInstance() {
- return instance;
- }
-
- private BroadcastReceiver receiver;
-
- public void saveAll(final Context context, String folderName, final List list) {
- final File subFolder = new File(ChanSettings.saveLocation.get() + File.separator + filterName(folderName));
-
- String text = context.getString(R.string.image_download_confirm, Integer.toString(list.size()), subFolder.getAbsolutePath());
-
- new AlertDialog.Builder(context).setMessage(text).setNegativeButton(R.string.cancel, null)
- .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- listDownload(context, subFolder, list);
- }
- }).show();
- }
-
- public void saveImage(final Context context, String imageUrl, final String name, final String extension, final boolean share) {
- Chan.getFileCache().downloadFile(imageUrl, new FileCache.DownloadedCallback() {
- @Override
- @SuppressWarnings("deprecation")
- public void onProgress(long downloaded, long total, boolean done) {
- }
-
- @Override
- public void onSuccess(final File source) {
- onFileDownloaded(context, name, extension, source, share);
- }
-
- @Override
- public void onFail(boolean notFound) {
- Toast.makeText(context, R.string.image_open_failed, Toast.LENGTH_LONG).show();
- }
- });
- }
-
- private void onFileDownloaded(final Context context, final String name, final String extension, final File downloaded, boolean share) {
- if (share) {
- scanFile(context, downloaded, true);
- } else {
- /*if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
- Toast.makeText(context, R.string.image_save_not_mounted, Toast.LENGTH_LONG).show();
- return;
- }*/
-
- new Thread(new Runnable() {
- @Override
- public void run() {
- File saveDir = new File(ChanSettings.saveLocation.get());
-
- if (!saveDir.isDirectory() && !saveDir.mkdirs()) {
- showToast(context, context.getString(R.string.image_save_directory_error));
- return;
- }
-
- String fileName = filterName(name + "." + extension);
- File destination = findUnused(new File(saveDir, fileName), false);
-
- final boolean success = storeImage(downloaded, destination);
- if (success) {
- scanFile(context, destination, false);
- showToast(context, context.getString(R.string.image_save_succeeded) + " " + destination.getAbsolutePath());
- } else {
- showToast(context, context.getString(R.string.image_save_failed));
- }
- }
- }).start();
- }
- }
-
- private void showToast(final Context context, final String message) {
- AndroidUtils.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- Toast.makeText(context, message, Toast.LENGTH_LONG).show();
- }
- });
- }
-
- private void listDownload(Context context, File subFolder, final List list) {
- final DownloadManager dm = (DownloadManager) context.getSystemService(Context.DOWNLOAD_SERVICE);
-
- if (!subFolder.isDirectory() && !subFolder.mkdirs()) {
- Toast.makeText(context, R.string.image_save_directory_error, Toast.LENGTH_LONG).show();
- return;
- }
-
- final List files = new ArrayList<>(list.size());
- for (DownloadPair uri : list) {
- File destination = new File(subFolder, filterName(uri.imageName));
- if (destination.exists()) continue;
-
- Pair p = new Pair();
- p.uri = Uri.parse(uri.imageUrl);
- p.file = destination;
- files.add(p);
- }
-
- final AtomicBoolean stopped = new AtomicBoolean(false);
- final List ids = new ArrayList<>();
-
- new Thread(new Runnable() {
- @Override
- public void run() {
- for (Pair pair : files) {
- if (stopped.get()) {
- break;
- }
-
- DownloadManager.Request request;
- try {
- request = new DownloadManager.Request(pair.uri);
- } catch (IllegalArgumentException e) {
- continue;
- }
-
- request.setDestinationUri(Uri.fromFile(pair.file));
- request.setVisibleInDownloadsUi(false);
- request.allowScanningByMediaScanner();
-
- synchronized (stopped) {
- if (stopped.get()) {
- break;
- }
- ids.add(dm.enqueue(request));
- }
- }
- }
- }).start();
-
- receiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- if (DownloadManager.ACTION_NOTIFICATION_CLICKED.equals(intent.getAction())) {
- synchronized (stopped) {
- stopped.set(true);
-
- for (Long id : ids) {
- dm.remove(id);
- }
- }
- } else if (DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(intent.getAction())) {
- if (receiver != null) {
- context.unregisterReceiver(receiver);
- receiver = null;
- }
- }
- }
- };
-
- IntentFilter filter = new IntentFilter();
- filter.addAction(DownloadManager.ACTION_NOTIFICATION_CLICKED);
- filter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
-
- context.registerReceiver(receiver, filter);
- }
-
- private String filterName(String name) {
- return name.replaceAll("[^a-zA-Z0-9.]", "_");
- }
-
- private File findUnused(File start, boolean isDir) {
- String base;
- String extension;
-
- if (isDir) {
- base = start.getAbsolutePath();
- extension = null;
- } else {
- String[] splitted = start.getAbsolutePath().split("\\.(?=[^\\.]+$)");
- if (splitted.length == 2) {
- base = splitted[0];
- extension = "." + splitted[1];
- } else {
- base = splitted[0];
- extension = ".";
- }
- }
-
- File test;
- if (isDir) {
- test = new File(base);
- } else {
- test = new File(base + extension);
- }
- int index = 0;
- int tries = 0;
- while (test.exists() && tries++ < 100) {
- if (isDir) {
- test = new File(base + "_" + index);
- } else {
- test = new File(base + "_" + index + extension);
- }
- index++;
- }
-
- return test;
- }
-
- private boolean storeImage(final File source, final File destination) {
- boolean res = true;
- InputStream is = null;
- OutputStream os = null;
- try {
- is = new FileInputStream(source);
- os = new FileOutputStream(destination);
- IOUtils.copy(is, os);
- } catch (IOException e) {
- res = false;
- } finally {
- IOUtils.closeQuietly(is);
- IOUtils.closeQuietly(os);
- }
-
- return res;
- }
-
- private void scanFile(final Context context, final File file, final boolean shareAfterwards) {
- MediaScannerConnection.scanFile(context, new String[]{file.getAbsolutePath()}, null,
- new MediaScannerConnection.OnScanCompletedListener() {
- @Override
- public void onScanCompleted(String unused, final Uri uri) {
- AndroidUtils.runOnUiThread(new Runnable() {
- @Override
- public void run() {
- Logger.d(TAG, "Media scan succeeded: " + uri);
-
- if (shareAfterwards) {
- Intent intent = new Intent(Intent.ACTION_SEND);
- intent.setType("image/*");
- intent.putExtra(Intent.EXTRA_STREAM, uri);
- context.startActivity(Intent.createChooser(intent, context.getString(R.string.action_share)));
- }
- }
- });
- }
- }
- );
- }
-
- public static class DownloadPair {
- public String imageUrl;
- public String imageName;
-
- public DownloadPair(String uri, String name) {
- this.imageUrl = uri;
- this.imageName = name;
- }
- }
-
- private static class Pair {
- public Uri uri;
- public File file;
- }
-}
diff --git a/Clover/app/src/main/java/org/floens/chan/utils/RecyclerUtils.java b/Clover/app/src/main/java/org/floens/chan/utils/RecyclerUtils.java
new file mode 100644
index 00000000..c3ea9bfc
--- /dev/null
+++ b/Clover/app/src/main/java/org/floens/chan/utils/RecyclerUtils.java
@@ -0,0 +1,37 @@
+/*
+ * Clover - 4chan browser https://github.com/Floens/Clover/
+ * Copyright (C) 2014 Floens
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.floens.chan.utils;
+
+import android.support.v7.widget.RecyclerView;
+
+import java.lang.reflect.Field;
+
+public class RecyclerUtils {
+ private static final String TAG = "RecyclerUtils";
+
+ public static void clearRecyclerCache(RecyclerView recyclerView) {
+ try {
+ Field field = RecyclerView.class.getDeclaredField("mRecycler");
+ field.setAccessible(true);
+ RecyclerView.Recycler recycler = (RecyclerView.Recycler) field.get(recyclerView);
+ recycler.clear();
+ } catch (Exception e) {
+ Logger.e(TAG, "Error clearing RecyclerView cache with reflection", e);
+ }
+ }
+}
diff --git a/Clover/app/src/main/res/drawable-hdpi/ic_check_circle_white_24dp.png b/Clover/app/src/main/res/drawable-hdpi/ic_check_circle_white_24dp.png
new file mode 100644
index 00000000..74efe263
Binary files /dev/null and b/Clover/app/src/main/res/drawable-hdpi/ic_check_circle_white_24dp.png differ
diff --git a/Clover/app/src/main/res/drawable-hdpi/ic_radio_button_unchecked_white_24dp.png b/Clover/app/src/main/res/drawable-hdpi/ic_radio_button_unchecked_white_24dp.png
new file mode 100644
index 00000000..413fc171
Binary files /dev/null and b/Clover/app/src/main/res/drawable-hdpi/ic_radio_button_unchecked_white_24dp.png differ
diff --git a/Clover/app/src/main/res/drawable-hdpi/ic_select_all_white_24dp.png b/Clover/app/src/main/res/drawable-hdpi/ic_select_all_white_24dp.png
new file mode 100644
index 00000000..2d971a94
Binary files /dev/null and b/Clover/app/src/main/res/drawable-hdpi/ic_select_all_white_24dp.png differ
diff --git a/Clover/app/src/main/res/drawable-mdpi/ic_check_circle_white_24dp.png b/Clover/app/src/main/res/drawable-mdpi/ic_check_circle_white_24dp.png
new file mode 100644
index 00000000..29469f5b
Binary files /dev/null and b/Clover/app/src/main/res/drawable-mdpi/ic_check_circle_white_24dp.png differ
diff --git a/Clover/app/src/main/res/drawable-mdpi/ic_radio_button_unchecked_white_24dp.png b/Clover/app/src/main/res/drawable-mdpi/ic_radio_button_unchecked_white_24dp.png
new file mode 100644
index 00000000..e74f040b
Binary files /dev/null and b/Clover/app/src/main/res/drawable-mdpi/ic_radio_button_unchecked_white_24dp.png differ
diff --git a/Clover/app/src/main/res/drawable-mdpi/ic_select_all_white_24dp.png b/Clover/app/src/main/res/drawable-mdpi/ic_select_all_white_24dp.png
new file mode 100644
index 00000000..966938b9
Binary files /dev/null and b/Clover/app/src/main/res/drawable-mdpi/ic_select_all_white_24dp.png differ
diff --git a/Clover/app/src/main/res/drawable-xhdpi/ic_check_circle_white_24dp.png b/Clover/app/src/main/res/drawable-xhdpi/ic_check_circle_white_24dp.png
new file mode 100644
index 00000000..4c233ae9
Binary files /dev/null and b/Clover/app/src/main/res/drawable-xhdpi/ic_check_circle_white_24dp.png differ
diff --git a/Clover/app/src/main/res/drawable-xhdpi/ic_radio_button_unchecked_white_24dp.png b/Clover/app/src/main/res/drawable-xhdpi/ic_radio_button_unchecked_white_24dp.png
new file mode 100644
index 00000000..f1a967f9
Binary files /dev/null and b/Clover/app/src/main/res/drawable-xhdpi/ic_radio_button_unchecked_white_24dp.png differ
diff --git a/Clover/app/src/main/res/drawable-xhdpi/ic_select_all_white_24dp.png b/Clover/app/src/main/res/drawable-xhdpi/ic_select_all_white_24dp.png
new file mode 100644
index 00000000..b99012a3
Binary files /dev/null and b/Clover/app/src/main/res/drawable-xhdpi/ic_select_all_white_24dp.png differ
diff --git a/Clover/app/src/main/res/drawable-xxhdpi/ic_check_circle_white_24dp.png b/Clover/app/src/main/res/drawable-xxhdpi/ic_check_circle_white_24dp.png
new file mode 100644
index 00000000..542a64b3
Binary files /dev/null and b/Clover/app/src/main/res/drawable-xxhdpi/ic_check_circle_white_24dp.png differ
diff --git a/Clover/app/src/main/res/drawable-xxhdpi/ic_radio_button_unchecked_white_24dp.png b/Clover/app/src/main/res/drawable-xxhdpi/ic_radio_button_unchecked_white_24dp.png
new file mode 100644
index 00000000..d1c733d2
Binary files /dev/null and b/Clover/app/src/main/res/drawable-xxhdpi/ic_radio_button_unchecked_white_24dp.png differ
diff --git a/Clover/app/src/main/res/drawable-xxhdpi/ic_select_all_white_24dp.png b/Clover/app/src/main/res/drawable-xxhdpi/ic_select_all_white_24dp.png
new file mode 100644
index 00000000..162ab984
Binary files /dev/null and b/Clover/app/src/main/res/drawable-xxhdpi/ic_select_all_white_24dp.png differ
diff --git a/Clover/app/src/main/res/drawable-xxxhdpi/ic_check_circle_white_24dp.png b/Clover/app/src/main/res/drawable-xxxhdpi/ic_check_circle_white_24dp.png
new file mode 100644
index 00000000..816c5909
Binary files /dev/null and b/Clover/app/src/main/res/drawable-xxxhdpi/ic_check_circle_white_24dp.png differ
diff --git a/Clover/app/src/main/res/drawable-xxxhdpi/ic_radio_button_unchecked_white_24dp.png b/Clover/app/src/main/res/drawable-xxxhdpi/ic_radio_button_unchecked_white_24dp.png
new file mode 100644
index 00000000..e32217db
Binary files /dev/null and b/Clover/app/src/main/res/drawable-xxxhdpi/ic_radio_button_unchecked_white_24dp.png differ
diff --git a/Clover/app/src/main/res/drawable-xxxhdpi/ic_select_all_white_24dp.png b/Clover/app/src/main/res/drawable-xxxhdpi/ic_select_all_white_24dp.png
new file mode 100644
index 00000000..896e1ac2
Binary files /dev/null and b/Clover/app/src/main/res/drawable-xxxhdpi/ic_select_all_white_24dp.png differ
diff --git a/Clover/app/src/main/res/layout/cell_album_download.xml b/Clover/app/src/main/res/layout/cell_album_download.xml
new file mode 100644
index 00000000..548ca3a9
--- /dev/null
+++ b/Clover/app/src/main/res/layout/cell_album_download.xml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
diff --git a/Clover/app/src/main/res/layout/controller_album_download.xml b/Clover/app/src/main/res/layout/controller_album_download.xml
new file mode 100644
index 00000000..38796d46
--- /dev/null
+++ b/Clover/app/src/main/res/layout/controller_album_download.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
diff --git a/Clover/app/src/main/res/layout/controller_navigation_image_viewer.xml b/Clover/app/src/main/res/layout/controller_navigation_image_viewer.xml
index 9bd0305b..405e7f13 100644
--- a/Clover/app/src/main/res/layout/controller_navigation_image_viewer.xml
+++ b/Clover/app/src/main/res/layout/controller_navigation_image_viewer.xml
@@ -25,7 +25,7 @@ along with this program. If not, see .
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="@dimen/toolbar_height"
- android:background="#e9000000"
+ android:background="#87000000"
tools:ignore="UnusedAttribute" />
.
Open this thread?
No applications found to open link
- Saved image to
- Saving image failed
- Cannot make save directory
-
Error
404
Failed to show image
@@ -97,7 +93,6 @@ along with this program. If not, see .
Deepzoom loading failed
Image not found
Failed to open image
- %1$s images will be downloaded to %2$s
Spoiler image
Add more…
@@ -254,6 +249,18 @@ If the pattern matches then the post can be hidden or highlighted.<br>
Filters
+ Select images (%1$d / %2$d)
+ Please select images to download
+ %1$s will be downloaded to the folder %2$s
+
+ Downloading images
+ Tap to cancel
+ Image saved
+ Saved as %1$s
+ Saved image to
+ Saving image failed
+ Cannot make save directory
+
Settings
General
Boards
diff --git a/Clover/build.gradle b/Clover/build.gradle
index dba1720a..7769d34d 100644
--- a/Clover/build.gradle
+++ b/Clover/build.gradle
@@ -4,7 +4,7 @@ buildscript {
mavenCentral()
}
dependencies {
- classpath 'com.android.tools.build:gradle:1.2.3'
+ classpath 'com.android.tools.build:gradle:1.3.0'
}
}
diff --git a/Clover/gradle/wrapper/gradle-wrapper.properties b/Clover/gradle/wrapper/gradle-wrapper.properties
index 3d4a8f81..83bfc660 100644
--- a/Clover/gradle/wrapper/gradle-wrapper.properties
+++ b/Clover/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Thu May 14 11:43:47 CEST 2015
+#Tue Aug 11 17:05:27 CEST 2015
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.4-bin.zip