Add image selection to album downloading

Update the ImageSaver to use a task based approach.
Do not use DownloadManager for album downloads, also fixes saving to an external sd card problems.
A persistent notification is now shown while downloading, to avoid clover being killed when downloading.
multisite
Floens 10 years ago
parent 6c99d8cc2e
commit b83082081e
  1. 2
      Clover/app/proguard.cfg
  2. 7
      Clover/app/src/main/AndroidManifest.xml
  3. 4
      Clover/app/src/main/java/org/floens/chan/core/cache/FileCache.java
  4. 219
      Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaveTask.java
  5. 213
      Clover/app/src/main/java/org/floens/chan/core/saver/ImageSaver.java
  6. 271
      Clover/app/src/main/java/org/floens/chan/ui/controller/AlbumDownloadController.java
  7. 30
      Clover/app/src/main/java/org/floens/chan/ui/controller/ImageViewerController.java
  8. 2
      Clover/app/src/main/java/org/floens/chan/ui/layout/ThreadLayout.java
  9. 108
      Clover/app/src/main/java/org/floens/chan/ui/service/SavingNotification.java
  10. 2
      Clover/app/src/main/java/org/floens/chan/ui/theme/Theme.java
  11. 75
      Clover/app/src/main/java/org/floens/chan/ui/view/GridRecyclerView.java
  12. 16
      Clover/app/src/main/java/org/floens/chan/utils/AndroidUtils.java
  13. 307
      Clover/app/src/main/java/org/floens/chan/utils/ImageSaver.java
  14. 37
      Clover/app/src/main/java/org/floens/chan/utils/RecyclerUtils.java
  15. BIN
      Clover/app/src/main/res/drawable-hdpi/ic_check_circle_white_24dp.png
  16. BIN
      Clover/app/src/main/res/drawable-hdpi/ic_radio_button_unchecked_white_24dp.png
  17. BIN
      Clover/app/src/main/res/drawable-hdpi/ic_select_all_white_24dp.png
  18. BIN
      Clover/app/src/main/res/drawable-mdpi/ic_check_circle_white_24dp.png
  19. BIN
      Clover/app/src/main/res/drawable-mdpi/ic_radio_button_unchecked_white_24dp.png
  20. BIN
      Clover/app/src/main/res/drawable-mdpi/ic_select_all_white_24dp.png
  21. BIN
      Clover/app/src/main/res/drawable-xhdpi/ic_check_circle_white_24dp.png
  22. BIN
      Clover/app/src/main/res/drawable-xhdpi/ic_radio_button_unchecked_white_24dp.png
  23. BIN
      Clover/app/src/main/res/drawable-xhdpi/ic_select_all_white_24dp.png
  24. BIN
      Clover/app/src/main/res/drawable-xxhdpi/ic_check_circle_white_24dp.png
  25. BIN
      Clover/app/src/main/res/drawable-xxhdpi/ic_radio_button_unchecked_white_24dp.png
  26. BIN
      Clover/app/src/main/res/drawable-xxhdpi/ic_select_all_white_24dp.png
  27. BIN
      Clover/app/src/main/res/drawable-xxxhdpi/ic_check_circle_white_24dp.png
  28. BIN
      Clover/app/src/main/res/drawable-xxxhdpi/ic_radio_button_unchecked_white_24dp.png
  29. BIN
      Clover/app/src/main/res/drawable-xxxhdpi/ic_select_all_white_24dp.png
  30. 39
      Clover/app/src/main/res/layout/cell_album_download.xml
  31. 44
      Clover/app/src/main/res/layout/controller_album_download.xml
  32. 2
      Clover/app/src/main/res/layout/controller_navigation_image_viewer.xml
  33. 17
      Clover/app/src/main/res/values/strings.xml
  34. 2
      Clover/build.gradle
  35. 4
      Clover/gradle/wrapper/gradle-wrapper.properties

@ -149,3 +149,5 @@
}
-keep public class * extends android.support.design.**
-keep public class android.support.v7.widget.RecyclerView

@ -15,8 +15,9 @@ 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 <http://www.gnu.org/licenses/>.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
<manifest
package="org.floens.chan"
xmlns:android="http://schemas.android.com/apk/res/android"
android:installLocation="auto">
<uses-permission android:name="android.permission.INTERNET"/>
@ -63,6 +64,10 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
android:name=".ui.service.WatchNotifier"
android:exported="false"/>
<service
android:name=".ui.service.SavingNotification"
android:exported="false"/>
</application>
</manifest>

@ -295,6 +295,10 @@ public class FileCache {
}
}
public Future<?> getFuture() {
return future;
}
private void setFuture(Future<?> future) {
this.future = future;
}

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
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<ImageSaveTask> 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;
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
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<AlbumDownloadItem> 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<ImageSaveTask> 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<PostImage> 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<AlbumDownloadCell> {
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);
}
}
}

@ -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<PostImage> all = presenter.getAllPostImages();
List<ImageSaver.DownloadPair> 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) {

@ -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();

@ -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 <http://www.gnu.org/licenses/>.
*/
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 {
}
}

@ -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);

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}
}

@ -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<ResolveInfo> resolveInfos = pm.queryIntentActivities(intent, 0);
List<Intent> 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) {

@ -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 <http://www.gnu.org/licenses/>.
*/
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<DownloadPair> 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<DownloadPair> 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<Pair> 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<Long> 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;
}
}

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 738 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 927 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?><!--
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 <http://www.gnu.org/licenses/>.
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="0dp"
tools:ignore="ContentDescription,RtlHardcoded,RtlSymmetry">
<org.floens.chan.ui.view.ThumbnailView
android:id="@+id/thumbnail_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="5dp"/>
<ImageView
android:id="@+id/checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left|top"
android:paddingLeft="7dp"
android:paddingTop="7dp"/>
</FrameLayout>

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?><!--
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 <http://www.gnu.org/licenses/>.
-->
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?backcolor">
<org.floens.chan.ui.view.GridRecyclerView
android:id="@+id/recycler_view"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#ff000000"
android:clipToPadding="false"
android:padding="5dp"
android:paddingBottom="72dp"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical"/>
<android.support.design.widget.FloatingActionButton
android:id="@+id/download"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|bottom"
android:layout_margin="16dp"
android:src="@drawable/ic_file_download_white_24dp"/>
</android.support.design.widget.CoordinatorLayout>

@ -25,7 +25,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="@dimen/toolbar_height"
android:background="#e9000000"
android:background="#87000000"
tools:ignore="UnusedAttribute" />
<FrameLayout

@ -86,10 +86,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<string name="open_thread_confirmation">Open this thread?</string>
<string name="open_link_failed">No applications found to open link</string>
<string name="image_save_succeeded">Saved image to</string>
<string name="image_save_failed">Saving image failed</string>
<string name="image_save_directory_error">Cannot make save directory</string>
<string name="thumbnail_load_failed_network">Error</string>
<string name="thumbnail_load_failed_server">404</string>
<string name="image_preview_failed">Failed to show image</string>
@ -97,7 +93,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<string name="image_failed_big_image">Deepzoom loading failed</string>
<string name="image_not_found">Image not found</string>
<string name="image_open_failed">Failed to open image</string>
<string name="image_download_confirm">%1$s images will be downloaded to %2$s</string>
<string name="image_spoiler_filename">Spoiler image</string>
<string name="thread_board_select_add">Add more&#8230;</string>
@ -254,6 +249,18 @@ If the pattern matches then the post can be hidden or highlighted.&lt;br>
<string name="filters_screen">Filters</string>
<string name="album_download_screen">Select images (%1$d / %2$d)</string>
<string name="album_download_none_checked">Please select images to download</string>
<string name="album_download_confirm">%1$s will be downloaded to the folder %2$s</string>
<string name="image_save_notification_downloading">Downloading images</string>
<string name="image_save_notification_cancel">Tap to cancel</string>
<string name="image_save_saved">Image saved</string>
<string name="image_save_as">Saved as %1$s</string>
<string name="image_save_succeeded">Saved image to</string>
<string name="image_save_failed">Saving image failed</string>
<string name="image_save_directory_error">Cannot make save directory</string>
<string name="settings_screen">Settings</string>
<string name="settings_group_general">General</string>
<string name="settings_board_edit">Boards</string>

@ -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'
}
}

@ -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

Loading…
Cancel
Save