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