diff --git a/Clover/app/build.gradle b/Clover/app/build.gradle
index f7b1af74..39b0b97e 100644
--- a/Clover/app/build.gradle
+++ b/Clover/app/build.gradle
@@ -84,13 +84,13 @@ android {
}
dependencies {
- compile 'com.android.support:support-v13:23.2.1'
- compile 'com.android.support:appcompat-v7:23.2.1'
- compile 'com.android.support:recyclerview-v7:23.2.1'
- compile 'com.android.support:cardview-v7:23.2.1'
- compile 'com.android.support:support-annotations:23.2.1'
- compile 'com.android.support:design:23.2.1'
- compile 'com.android.support:customtabs:23.2.1'
+ compile 'com.android.support:support-v13:23.4.0'
+ compile 'com.android.support:appcompat-v7:23.4.0'
+ compile 'com.android.support:recyclerview-v7:23.4.0'
+ compile 'com.android.support:cardview-v7:23.4.0'
+ compile 'com.android.support:support-annotations:23.4.0'
+ compile 'com.android.support:design:23.4.0'
+ compile 'com.android.support:customtabs:23.4.0'
compile 'org.jsoup:jsoup:1.8.3'
compile 'com.j256.ormlite:ormlite-core:4.48'
diff --git a/Clover/app/src/main/AndroidManifest.xml b/Clover/app/src/main/AndroidManifest.xml
index 4ec033dd..52e330fb 100644
--- a/Clover/app/src/main/AndroidManifest.xml
+++ b/Clover/app/src/main/AndroidManifest.xml
@@ -31,24 +31,25 @@ along with this program. If not, see .
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Chan.Theme">
-
+
+ android:value="true" />
+ android:name="com.sec.android.app.multiwindow"
+ android:required="false" />
+
+ android:value="true" />
+ android:value="632.0dip" />
-
+ android:value="598.0dip" />
+
fileItems;
+
+ public final boolean canNavigateUp;
+
+ public FileItems(File path, List fileItems, boolean canNavigateUp) {
+ this.path = path;
+ this.fileItems = fileItems;
+ this.canNavigateUp = canNavigateUp;
+ }
+}
diff --git a/Clover/app/src/main/java/org/floens/chan/core/saver/FileWatcher.java b/Clover/app/src/main/java/org/floens/chan/core/saver/FileWatcher.java
new file mode 100644
index 00000000..4ab84094
--- /dev/null
+++ b/Clover/app/src/main/java/org/floens/chan/core/saver/FileWatcher.java
@@ -0,0 +1,112 @@
+package org.floens.chan.core.saver;
+
+import android.os.FileObserver;
+import android.util.Log;
+
+import org.floens.chan.core.model.FileItem;
+import org.floens.chan.core.model.FileItems;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class FileWatcher {
+ private static final String TAG = "FileWatcher";
+
+ private static final Comparator FILE_COMPARATOR = new Comparator() {
+ @Override
+ public int compare(FileItem a, FileItem b) {
+ return a.file.getName().compareToIgnoreCase(b.file.getName());
+ }
+ };
+
+ private final FileWatcherCallback callback;
+ boolean initialized = false;
+ private File startingPath;
+
+ private File currentPath;
+
+ private AFileObserver fileObserver;
+
+ public FileWatcher(FileWatcherCallback callback, File startingPath) {
+ this.callback = callback;
+ this.startingPath = startingPath;
+ }
+
+ public void initialize() {
+ initialized = true;
+ navigateTo(startingPath);
+ }
+
+ public File getCurrentPath() {
+ return currentPath;
+ }
+
+ public void navigateUp() {
+ File parentFile = currentPath.getParentFile();
+ if (parentFile != null && StorageHelper.canNavigate(parentFile)) {
+ navigateTo(parentFile);
+ }
+ }
+
+ public void navigateTo(File to) {
+ if (!StorageHelper.canNavigate(to)) {
+ throw new IllegalArgumentException("Cannot navigate to " + to.getAbsolutePath());
+ }
+
+ if (fileObserver != null) {
+ fileObserver.stopWatching();
+ fileObserver = null;
+ }
+
+ // TODO: fileobserver is broken
+// int mask = FileObserver.CREATE | FileObserver.DELETE;
+// fileObserver = new AFileObserver(to.getAbsolutePath(), mask);
+// fileObserver = new AFileObserver("/sdcard/");
+// fileObserver.startWatching();
+
+ currentPath = to;
+
+ File[] files = currentPath.listFiles();
+
+ List folderList = new ArrayList<>();
+ List fileList = new ArrayList<>();
+ for (File file : files) {
+ if (StorageHelper.canNavigate(file)) {
+ folderList.add(new FileItem(file));
+ } else if (file.isFile()) {
+ fileList.add(new FileItem(file));
+ }
+ }
+ Collections.sort(folderList, FILE_COMPARATOR);
+ Collections.sort(fileList, FILE_COMPARATOR);
+ List items = new ArrayList<>(folderList.size() + fileList.size());
+ items.addAll(folderList);
+ items.addAll(fileList);
+
+ boolean canNavigateUp = StorageHelper.canNavigate(currentPath.getParentFile());
+
+ callback.onFiles(new FileItems(currentPath, items, canNavigateUp));
+ }
+
+ private class AFileObserver extends FileObserver {
+ public AFileObserver(String path) {
+ super(path);
+ }
+
+ public AFileObserver(String path, int mask) {
+ super(path, mask);
+ }
+
+ @Override
+ public void onEvent(int event, String path) {
+ Log.d(TAG, "onEvent() called with: " + "event = [" + event + "], path = [" + path + "]");
+ }
+ }
+
+ public interface FileWatcherCallback {
+ void onFiles(FileItems fileItems);
+ }
+}
diff --git a/Clover/app/src/main/java/org/floens/chan/core/saver/StorageHelper.java b/Clover/app/src/main/java/org/floens/chan/core/saver/StorageHelper.java
new file mode 100644
index 00000000..4118a97a
--- /dev/null
+++ b/Clover/app/src/main/java/org/floens/chan/core/saver/StorageHelper.java
@@ -0,0 +1,31 @@
+package org.floens.chan.core.saver;
+
+import java.io.File;
+
+public class StorageHelper {
+ private static final String TAG = "StorageHelper";
+
+ public static boolean canNavigate(File file) {
+ return file != null && !isDirectoryBlacklisted(file) && file.exists()
+ && file.isDirectory() && file.canRead();
+ }
+
+ public static boolean isDirectoryBlacklisted(File file) {
+ String absolutePath = file.getAbsolutePath();
+ switch (absolutePath) {
+ case "/storage":
+ return true;
+ case "/storage/emulated":
+ return true;
+ case "/storage/emulated/0/0":
+ return true;
+ case "/storage/emulated/legacy":
+ return true;
+ }
+ return false;
+ }
+
+ public static boolean canOpen(File file) {
+ return file != null && file.exists() && file.isFile() && file.canRead();
+ }
+}
diff --git a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java
index 83f13a7e..d41e53bc 100644
--- a/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java
+++ b/Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java
@@ -31,6 +31,8 @@ import java.io.File;
import java.net.InetSocketAddress;
import java.net.Proxy;
+import de.greenrobot.event.EventBus;
+
public class ChanSettings {
public enum MediaAutoLoadMode implements OptionSettingItem {
// ALways auto load, either wifi or mobile
@@ -200,6 +202,12 @@ public class ChanSettings {
developer = new BooleanSetting(p, "preference_developer", false);
saveLocation = new StringSetting(p, "preference_image_save_location", Environment.getExternalStorageDirectory() + File.separator + "Clover");
+ saveLocation.addCallback(new Setting.SettingCallback() {
+ @Override
+ public void onValueChange(Setting setting, String value) {
+ EventBus.getDefault().post(new SettingChanged<>(saveLocation));
+ }
+ });
saveOriginalFilename = new BooleanSetting(p, "preference_image_save_original", false);
shareUrl = new BooleanSetting(p, "preference_image_share_url", false);
networkHttps = new BooleanSetting(p, "preference_network_https", true);
@@ -342,4 +350,12 @@ public class ChanSettings {
this.accentColor = accentColor;
}
}
+
+ public static class SettingChanged {
+ public final Setting setting;
+
+ public SettingChanged(Setting setting) {
+ this.setting = setting;
+ }
+ }
}
diff --git a/Clover/app/src/main/java/org/floens/chan/core/settings/Setting.java b/Clover/app/src/main/java/org/floens/chan/core/settings/Setting.java
index 6114df02..a3b0ad65 100644
--- a/Clover/app/src/main/java/org/floens/chan/core/settings/Setting.java
+++ b/Clover/app/src/main/java/org/floens/chan/core/settings/Setting.java
@@ -42,6 +42,10 @@ public abstract class Setting {
return def;
}
+ public String getKey() {
+ return key;
+ }
+
public void addCallback(SettingCallback callback) {
this.callbacks.add(callback);
}
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/adapter/FilesAdapter.java b/Clover/app/src/main/java/org/floens/chan/ui/adapter/FilesAdapter.java
new file mode 100644
index 00000000..5c6cad11
--- /dev/null
+++ b/Clover/app/src/main/java/org/floens/chan/ui/adapter/FilesAdapter.java
@@ -0,0 +1,129 @@
+package org.floens.chan.ui.adapter;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.v4.graphics.drawable.DrawableCompat;
+import android.support.v7.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import org.floens.chan.R;
+import org.floens.chan.core.model.FileItem;
+import org.floens.chan.core.model.FileItems;
+
+import static org.floens.chan.utils.AndroidUtils.getAttrColor;
+
+public class FilesAdapter extends RecyclerView.Adapter {
+ private static final int ITEM_TYPE_FOLDER = 0;
+ private static final int ITEM_TYPE_FILE = 1;
+
+ private FileItem highlightedItem;
+ private FileItems fileItems;
+ private Callback callback;
+
+ public FilesAdapter(Callback callback) {
+ this.callback = callback;
+ }
+
+ public void setFiles(FileItems fileItems) {
+ this.fileItems = fileItems;
+ notifyDataSetChanged();
+ }
+
+ public void setHighlightedItem(FileItem highlightedItem) {
+ this.highlightedItem = highlightedItem;
+ }
+
+ @Override
+ public FileViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ return new FileViewHolder(LayoutInflater.from(parent.getContext())
+ .inflate(R.layout.cell_file, parent, false));
+ }
+
+ @SuppressWarnings({"ConstantConditions", "deprecation"})
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ int itemViewType = getItemViewType(position);
+ switch (itemViewType) {
+ case ITEM_TYPE_FILE:
+ case ITEM_TYPE_FOLDER: {
+ boolean isFile = itemViewType == ITEM_TYPE_FILE;
+
+ FileItem item = getItem(position);
+ FileViewHolder fileViewHolder = ((FileViewHolder) holder);
+ fileViewHolder.text.setText(item.file.getName());
+
+ Context context = holder.itemView.getContext();
+
+ if (isFile) {
+ fileViewHolder.image.setVisibility(View.GONE);
+ } else {
+ fileViewHolder.image.setVisibility(View.VISIBLE);
+ Drawable drawable = DrawableCompat.wrap(
+ context.getResources().getDrawable(R.drawable.ic_folder_black_24dp));
+ DrawableCompat.setTint(drawable, getAttrColor(context, R.attr.text_color_secondary));
+ fileViewHolder.image.setImageDrawable(drawable);
+ }
+
+ boolean highlighted = highlightedItem != null && highlightedItem.file.equals(item.file);
+ if (highlighted) {
+ fileViewHolder.itemView.setBackgroundColor(0x0e000000);
+ } else {
+ fileViewHolder.itemView.setBackgroundResource(R.drawable.item_background);
+ }
+
+ break;
+ }
+ }
+ }
+
+ @Override
+ public int getItemCount() {
+ return fileItems.fileItems.size();
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ FileItem item = getItem(position);
+ if (item.isFile()) {
+ return ITEM_TYPE_FILE;
+ } else if (item.isFolder()) {
+ return ITEM_TYPE_FOLDER;
+ } else {
+ return ITEM_TYPE_FILE;
+ }
+ }
+
+ public FileItem getItem(int position) {
+ return fileItems.fileItems.get(position);
+ }
+
+ private void onItemClicked(FileItem fileItem) {
+ callback.onFileItemClicked(fileItem);
+ }
+
+ public class FileViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
+ private ImageView image;
+ private TextView text;
+
+ public FileViewHolder(View itemView) {
+ super(itemView);
+ image = (ImageView) itemView.findViewById(R.id.image);
+ text = (TextView) itemView.findViewById(R.id.text);
+ itemView.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ FileItem item = getItem(getAdapterPosition());
+ onItemClicked(item);
+ }
+ }
+
+ public interface Callback {
+ void onFileItemClicked(FileItem fileItem);
+ }
+}
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/AdvancedSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/AdvancedSettingsController.java
index 5611d6f1..5b62993b 100644
--- a/Clover/app/src/main/java/org/floens/chan/ui/controller/AdvancedSettingsController.java
+++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/AdvancedSettingsController.java
@@ -17,34 +17,26 @@
*/
package org.floens.chan.ui.controller;
-import android.app.Activity;
import android.content.Context;
-import android.support.v7.app.AlertDialog;
-import android.view.View;
import android.widget.LinearLayout;
import org.floens.chan.R;
import org.floens.chan.core.settings.ChanSettings;
import org.floens.chan.ui.activity.StartActivity;
-import org.floens.chan.ui.fragment.FolderPickFragment;
import org.floens.chan.ui.helper.RefreshUIMessage;
import org.floens.chan.ui.settings.BooleanSettingView;
import org.floens.chan.ui.settings.IntegerSettingView;
-import org.floens.chan.ui.settings.LinkSettingView;
import org.floens.chan.ui.settings.SettingView;
import org.floens.chan.ui.settings.SettingsController;
import org.floens.chan.ui.settings.SettingsGroup;
import org.floens.chan.ui.settings.StringSettingView;
-import java.io.File;
-
import de.greenrobot.event.EventBus;
public class AdvancedSettingsController extends SettingsController {
private static final String TAG = "AdvancedSettingsController";
private boolean needRestart;
- private LinkSettingView saveLocation;
private SettingView newCaptcha;
private SettingView enableReplyFab;
private SettingView neverHideToolbar;
@@ -93,27 +85,6 @@ public class AdvancedSettingsController extends SettingsController {
private void populatePreferences() {
SettingsGroup settings = new SettingsGroup(R.string.settings_group_advanced);
- // TODO change this to a presenting controller
- saveLocation = (LinkSettingView) settings.add(new LinkSettingView(this, R.string.setting_save_folder, 0, new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- File dir = new File(ChanSettings.saveLocation.get());
- if (!dir.mkdirs() && !dir.isDirectory()) {
- new AlertDialog.Builder(context).setMessage(R.string.setting_save_folder_error_create_folder).show();
- } else {
- FolderPickFragment frag = FolderPickFragment.newInstance(new FolderPickFragment.FolderPickListener() {
- @Override
- public void folderPicked(File path) {
- ChanSettings.saveLocation.set(path.getAbsolutePath());
- setSaveLocationDescription();
- }
- }, dir);
- ((Activity) context).getFragmentManager().beginTransaction().add(frag, null).commit();
- }
- }
- }));
- setSaveLocationDescription();
-
newCaptcha = settings.add(new BooleanSettingView(this, ChanSettings.postNewCaptcha, R.string.setting_use_new_captcha, R.string.setting_use_new_captcha_description));
settings.add(new BooleanSettingView(this, ChanSettings.saveOriginalFilename, R.string.setting_save_original_filename, 0));
controllersSwipeable = settings.add(new BooleanSettingView(this, ChanSettings.controllerSwipeable, R.string.setting_controller_swipeable, 0));
@@ -142,8 +113,4 @@ public class AdvancedSettingsController extends SettingsController {
proxy.add(new IntegerSettingView(this, ChanSettings.proxyPort, R.string.setting_proxy_port, R.string.setting_proxy_port));
groups.add(proxy);
}
-
- private void setSaveLocationDescription() {
- saveLocation.setDescription(ChanSettings.saveLocation.get());
- }
}
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java
index 49742ba4..1b7e06df 100644
--- a/Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java
+++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java
@@ -64,6 +64,7 @@ public class MainSettingsController extends SettingsController implements Toolba
private ListSettingView videoAutoLoadView;
private LinkSettingView boardEditorView;
+ private LinkSettingView saveLocation;
private LinkSettingView watchLink;
private LinkSettingView passLink;
private int clickCount;
@@ -144,6 +145,12 @@ public class MainSettingsController extends SettingsController implements Toolba
updateBoardLinkDescription();
}
+ public void onEvent(ChanSettings.SettingChanged setting) {
+ if (setting.setting == ChanSettings.saveLocation) {
+ setSaveLocationDescription();
+ }
+ }
+
@Override
public void onMenuItemClicked(ToolbarMenuItem item) {
}
@@ -258,6 +265,13 @@ public class MainSettingsController extends SettingsController implements Toolba
navigationController.pushController(new FiltersController(context));
}
}));
+ saveLocation = (LinkSettingView) browsing.add(new LinkSettingView(this, R.string.save_location_screen, 0, new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ navigationController.pushController(new SaveLocationController(context));
+ }
+ }));
+ setSaveLocationDescription();
browsing.add(new BooleanSettingView(this, ChanSettings.openLinkConfirmation, R.string.setting_open_link_confirmation, 0));
browsing.add(new BooleanSettingView(this, ChanSettings.autoRefreshThread, R.string.setting_auto_refresh_thread, 0));
@@ -410,6 +424,10 @@ public class MainSettingsController extends SettingsController implements Toolba
boardEditorView.setDescription(context.getResources().getQuantityString(R.plurals.board, savedBoards.size(), savedBoards.size()));
}
+ private void setSaveLocationDescription() {
+ saveLocation.setDescription(ChanSettings.saveLocation.get());
+ }
+
private void updateVideoLoadModes() {
ChanSettings.MediaAutoLoadMode currentImageLoadMode = ChanSettings.imageAutoLoadNetwork.get();
ChanSettings.MediaAutoLoadMode[] modes = ChanSettings.MediaAutoLoadMode.values();
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/controller/SaveLocationController.java b/Clover/app/src/main/java/org/floens/chan/ui/controller/SaveLocationController.java
new file mode 100644
index 00000000..c4b8efb8
--- /dev/null
+++ b/Clover/app/src/main/java/org/floens/chan/ui/controller/SaveLocationController.java
@@ -0,0 +1,136 @@
+package org.floens.chan.ui.controller;
+
+import android.Manifest;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.net.Uri;
+import android.provider.Settings;
+import android.support.design.widget.FloatingActionButton;
+import android.support.v7.app.AlertDialog;
+import android.view.View;
+
+import org.floens.chan.R;
+import org.floens.chan.controller.Controller;
+import org.floens.chan.core.model.FileItem;
+import org.floens.chan.core.model.FileItems;
+import org.floens.chan.core.saver.FileWatcher;
+import org.floens.chan.core.settings.ChanSettings;
+import org.floens.chan.ui.activity.StartActivity;
+import org.floens.chan.ui.adapter.FilesAdapter;
+import org.floens.chan.ui.helper.RuntimePermissionsHelper;
+import org.floens.chan.ui.layout.FilesLayout;
+import org.floens.chan.utils.AndroidUtils;
+
+import java.io.File;
+
+public class SaveLocationController extends Controller implements FileWatcher.FileWatcherCallback, FilesAdapter.Callback, FilesLayout.Callback, View.OnClickListener {
+ private static final String TAG = "SaveLocationController";
+
+ private FilesLayout filesLayout;
+ private FloatingActionButton setButton;
+
+ private RuntimePermissionsHelper runtimePermissionsHelper;
+ private boolean gotPermission = false;
+
+ private FileWatcher fileWatcher;
+ private FileItems fileItems;
+
+ public SaveLocationController(Context context) {
+ super(context);
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+
+ navigationItem.setTitle(R.string.save_location_screen);
+
+ view = inflateRes(R.layout.controller_save_location);
+ filesLayout = (FilesLayout) view.findViewById(R.id.files_layout);
+ filesLayout.setCallback(this);
+ setButton = (FloatingActionButton) view.findViewById(R.id.set_button);
+ setButton.setOnClickListener(this);
+
+ File saveLocation = new File(ChanSettings.saveLocation.get());
+ fileWatcher = new FileWatcher(this, saveLocation);
+
+ runtimePermissionsHelper = ((StartActivity) context).getRuntimePermissionsHelper();
+ gotPermission = hasPermission();
+ if (gotPermission) {
+ initialize();
+ } else {
+ requestPermission();
+ }
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v == setButton) {
+ File currentPath = fileWatcher.getCurrentPath();
+ ChanSettings.saveLocation.set(currentPath.getAbsolutePath());
+ navigationController.popController();
+ }
+ }
+
+ @Override
+ public void onFiles(FileItems fileItems) {
+ this.fileItems = fileItems;
+ filesLayout.setFiles(fileItems);
+ }
+
+ @Override
+ public void onBackClicked() {
+ fileWatcher.navigateUp();
+ }
+
+ @Override
+ public void onFileItemClicked(FileItem fileItem) {
+ if (fileItem.canNavigate()) {
+ fileWatcher.navigateTo(fileItem.file);
+ }
+ // Else ignore, we only do folder selection here
+ }
+
+ private boolean hasPermission() {
+ return runtimePermissionsHelper.hasPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE);
+ }
+
+ private void requestPermission() {
+ runtimePermissionsHelper.requestPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE, new RuntimePermissionsHelper.Callback() {
+ @Override
+ public void onRuntimePermissionResult(boolean granted) {
+ gotPermission = granted;
+ if (gotPermission) {
+ initialize();
+ } else {
+ new AlertDialog.Builder(context)
+ .setTitle(R.string.write_permission_required_title)
+ .setMessage(R.string.write_permission_required)
+ .setCancelable(false)
+ .setNeutralButton(R.string.write_permission_app_settings, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ requestPermission();
+ Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+ Uri.parse("package:" + context.getPackageName()));
+ AndroidUtils.openIntent(intent);
+ }
+ })
+ .setPositiveButton(R.string.write_permission_grant, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ requestPermission();
+ }
+ })
+ .show();
+ }
+ }
+ });
+ }
+
+ private void initialize() {
+ filesLayout.initialize();
+ fileWatcher.initialize();
+ }
+}
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/fragment/FolderPickFragment.java b/Clover/app/src/main/java/org/floens/chan/ui/fragment/FolderPickFragment.java
deleted file mode 100644
index ad160752..00000000
--- a/Clover/app/src/main/java/org/floens/chan/ui/fragment/FolderPickFragment.java
+++ /dev/null
@@ -1,201 +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.ui.fragment;
-
-import android.app.DialogFragment;
-import android.os.Bundle;
-import android.os.Environment;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-import android.widget.FrameLayout;
-import android.widget.ListView;
-import android.widget.TextView;
-
-import org.floens.chan.R;
-import org.floens.chan.ui.theme.ThemeHelper;
-
-import java.io.File;
-import java.util.ArrayList;
-import java.util.List;
-
-public class FolderPickFragment extends DialogFragment {
- private TextView statusPath;
- private ListView listView;
- private ArrayAdapter adapter;
- boolean hasParent = false;
-
- private FolderPickListener listener;
- private File currentPath;
- private List directories;
- private FrameLayout okButton;
- private TextView okButtonIcon;
-
- public static FolderPickFragment newInstance(FolderPickListener listener, File startingPath) {
- FolderPickFragment fragment = new FolderPickFragment();
- fragment.listener = listener;
- fragment.currentPath = startingPath;
- return fragment;
- }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- if (listener == null || currentPath == null) {
- dismiss();
- }
-
- setStyle(STYLE_NO_TITLE, 0);
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
- if (listener == null || currentPath == null) {
- return null;
- }
-
- View container = inflater.inflate(R.layout.fragment_folder_pick, parent);
-
- statusPath = (TextView) container.findViewById(R.id.folder_status);
- listView = (ListView) container.findViewById(R.id.folder_list);
- okButton = (FrameLayout) container.findViewById(R.id.pick_ok);
- okButtonIcon = (TextView) container.findViewById(R.id.pick_ok_icon);
-
- container.findViewById(R.id.pick_back).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- dismiss();
- }
- });
-
- okButton.findViewById(R.id.pick_ok).setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- listener.folderPicked(currentPath);
- dismiss();
- }
- });
-
- if (!ThemeHelper.getInstance().getTheme().isLightTheme) {
- ((TextView) container.findViewById(R.id.pick_back_icon)).setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_arrow_back_white_24dp, 0, 0, 0);
- ((TextView) container.findViewById(R.id.pick_ok_icon)).setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_done_white_24dp, 0, 0, 0);
- }
-
- adapter = new ArrayAdapter(inflater.getContext(), 0) {
- @Override
- public View getView(final int position, View convertView, ViewGroup parent) {
- if (convertView == null) {
- convertView = LayoutInflater.from(getContext()).inflate(android.R.layout.simple_list_item_1, null);
- }
- TextView text = (TextView) convertView;
-
- String name = getItem(position);
-
- text.setText(name);
-
- text.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- if (hasParent) {
- if (position == 0) {
- File parent = currentPath.getParentFile();
- moveTo(parent);
- } else if (position > 0 && position <= directories.size()) {
- File dir = directories.get(position - 1);
- moveTo(dir);
- }
- } else {
- if (position >= 0 && position < directories.size()) {
- File dir = directories.get(position);
- moveTo(dir);
- }
- }
- }
- });
-
- return text;
- }
- };
-
- listView.setAdapter(adapter);
-
- if (currentPath == null || !currentPath.exists()) {
- currentPath = Environment.getExternalStorageDirectory();
- }
-
- moveTo(currentPath);
-
- return container;
- }
-
- private boolean validPath(File path) {
- return path != null && path.isDirectory() && path.canRead() && path.canWrite();
- }
-
- private void moveTo(File path) {
- if (path != null && path.isDirectory()) {
- File[] listFiles = path.listFiles();
- if (listFiles != null) {
- currentPath = path;
- statusPath.setText(currentPath.getAbsolutePath());
- List dirs = new ArrayList<>();
- for (File file : path.listFiles()) {
- if (file.isDirectory()) {
- dirs.add(file);
- }
- }
-
- setDirs(dirs);
- }
- }
-
- validState();
- }
-
- private void validState() {
- if (validPath(currentPath)) {
- okButton.setEnabled(true);
- okButtonIcon.setEnabled(true);
- } else {
- okButton.setEnabled(false);
- okButtonIcon.setEnabled(false);
- }
- }
-
- private void setDirs(List dirs) {
- directories = dirs;
- adapter.clear();
-
- if (currentPath.getParent() != null) {
- adapter.add("..");
- hasParent = true;
- } else {
- hasParent = false;
- }
- for (File file : dirs) {
- adapter.add(file.getName());
- }
- adapter.notifyDataSetChanged();
- }
-
- public interface FolderPickListener {
- void folderPicked(File path);
- }
-}
diff --git a/Clover/app/src/main/java/org/floens/chan/ui/layout/FilesLayout.java b/Clover/app/src/main/java/org/floens/chan/ui/layout/FilesLayout.java
new file mode 100644
index 00000000..9b679d6a
--- /dev/null
+++ b/Clover/app/src/main/java/org/floens/chan/ui/layout/FilesLayout.java
@@ -0,0 +1,141 @@
+package org.floens.chan.ui.layout;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.support.v4.graphics.drawable.DrawableCompat;
+import android.support.v7.widget.LinearLayoutManager;
+import android.support.v7.widget.RecyclerView;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.floens.chan.R;
+import org.floens.chan.core.model.FileItem;
+import org.floens.chan.core.model.FileItems;
+import org.floens.chan.ui.adapter.FilesAdapter;
+import org.floens.chan.utils.RecyclerUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.floens.chan.utils.AndroidUtils.getAttrColor;
+
+public class FilesLayout extends LinearLayout implements FilesAdapter.Callback, View.OnClickListener {
+ private ViewGroup backLayout;
+ private ImageView backImage;
+ private TextView backText;
+ private RecyclerView recyclerView;
+
+ private LinearLayoutManager layoutManager;
+ private FilesAdapter filesAdapter;
+
+ private Map history = new HashMap<>();
+ private FileItemHistory currentHistory;
+ private FileItems currentFileItems;
+
+ private Callback callback;
+
+ public FilesLayout(Context context) {
+ this(context, null);
+ }
+
+ public FilesLayout(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public FilesLayout(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+ backLayout = (ViewGroup) findViewById(R.id.back_layout);
+ backImage = (ImageView) backLayout.findViewById(R.id.back_image);
+ backImage.setImageDrawable(DrawableCompat.wrap(backImage.getDrawable()));
+ backText = (TextView) backLayout.findViewById(R.id.back_text);
+ recyclerView = (RecyclerView) findViewById(R.id.recycler);
+
+ backLayout.setOnClickListener(this);
+ }
+
+ public void initialize() {
+ layoutManager = new LinearLayoutManager(getContext());
+ recyclerView.setLayoutManager(layoutManager);
+
+ filesAdapter = new FilesAdapter(this);
+ recyclerView.setAdapter(filesAdapter);
+ }
+
+ public void setCallback(Callback callback) {
+ this.callback = callback;
+ }
+
+ public void setFiles(FileItems fileItems) {
+ // Save the associated list position
+ if (currentFileItems != null) {
+ int[] indexTop = RecyclerUtils.getIndexAndTop(recyclerView);
+ currentHistory.index = indexTop[0];
+ currentHistory.top = indexTop[1];
+ history.put(currentFileItems.path.getAbsolutePath(), currentHistory);
+ }
+
+ filesAdapter.setFiles(fileItems);
+ currentFileItems = fileItems;
+
+ // Restore any previous list position
+ currentHistory = history.get(fileItems.path.getAbsolutePath());
+ if (currentHistory != null) {
+ layoutManager.scrollToPositionWithOffset(currentHistory.index, currentHistory.top);
+ filesAdapter.setHighlightedItem(currentHistory.clickedItem);
+ } else {
+ currentHistory = new FileItemHistory();
+ filesAdapter.setHighlightedItem(null);
+ }
+
+ boolean enabled = fileItems.canNavigateUp;
+ backLayout.setEnabled(enabled);
+ Drawable wrapped = DrawableCompat.wrap(backImage.getDrawable());
+ backImage.setImageDrawable(wrapped);
+ int color = getAttrColor(getContext(), enabled ? R.attr.text_color_primary : R.attr.text_color_hint);
+ DrawableCompat.setTint(wrapped, color);
+ backText.setEnabled(enabled);
+ backText.setTextColor(color);
+ }
+
+ public RecyclerView getRecyclerView() {
+ return recyclerView;
+ }
+
+ public ViewGroup getBackLayout() {
+ return backLayout;
+ }
+
+ @Override
+ public void onFileItemClicked(FileItem fileItem) {
+ currentHistory.clickedItem = fileItem;
+ callback.onFileItemClicked(fileItem);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view == backLayout) {
+ currentHistory.clickedItem = null;
+ callback.onBackClicked();
+ }
+ }
+
+ private class FileItemHistory {
+ int index, top;
+ FileItem clickedItem;
+ }
+
+ public interface Callback {
+ void onBackClicked();
+
+ void onFileItemClicked(FileItem fileItem);
+ }
+}
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
index c3ea9bfc..26d55b92 100644
--- a/Clover/app/src/main/java/org/floens/chan/utils/RecyclerUtils.java
+++ b/Clover/app/src/main/java/org/floens/chan/utils/RecyclerUtils.java
@@ -18,6 +18,7 @@
package org.floens.chan.utils;
import android.support.v7.widget.RecyclerView;
+import android.view.View;
import java.lang.reflect.Field;
@@ -34,4 +35,16 @@ public class RecyclerUtils {
Logger.e(TAG, "Error clearing RecyclerView cache with reflection", e);
}
}
+
+ public static int[] getIndexAndTop(RecyclerView recyclerView) {
+ int index = 0, top = 0;
+ if (recyclerView.getLayoutManager().getChildCount() > 0) {
+ View topChild = recyclerView.getLayoutManager().getChildAt(0);
+ index = ((RecyclerView.LayoutParams) topChild.getLayoutParams()).getViewLayoutPosition();
+ RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) topChild.getLayoutParams();
+ RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
+ top = layoutManager.getDecoratedTop(topChild) - params.topMargin - recyclerView.getPaddingTop();
+ }
+ return new int[]{index, top};
+ }
}
diff --git a/Clover/app/src/main/res/drawable-mdpi/ic_chevron_left_black_24dp.xml b/Clover/app/src/main/res/drawable-mdpi/ic_chevron_left_black_24dp.xml
new file mode 100644
index 00000000..e6bb3ca9
--- /dev/null
+++ b/Clover/app/src/main/res/drawable-mdpi/ic_chevron_left_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/Clover/app/src/main/res/drawable-xxhdpi/ic_folder_black_24dp.xml b/Clover/app/src/main/res/drawable-xxhdpi/ic_folder_black_24dp.xml
new file mode 100644
index 00000000..d7c6145c
--- /dev/null
+++ b/Clover/app/src/main/res/drawable-xxhdpi/ic_folder_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/Clover/app/src/main/res/layout/cell_file.xml b/Clover/app/src/main/res/layout/cell_file.xml
new file mode 100644
index 00000000..91e1e278
--- /dev/null
+++ b/Clover/app/src/main/res/layout/cell_file.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/Clover/app/src/main/res/layout/controller_save_location.xml b/Clover/app/src/main/res/layout/controller_save_location.xml
new file mode 100644
index 00000000..842cede3
--- /dev/null
+++ b/Clover/app/src/main/res/layout/controller_save_location.xml
@@ -0,0 +1,67 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Clover/app/src/main/res/values/strings.xml b/Clover/app/src/main/res/values/strings.xml
index 679a7d66..8331d928 100644
--- a/Clover/app/src/main/res/values/strings.xml
+++ b/Clover/app/src/main/res/values/strings.xml
@@ -67,6 +67,7 @@ along with this program. If not, see .
Add
Close
Back
+ Up
OK
Exit
Delete
@@ -326,6 +327,15 @@ along with this program. If not, see .
Filters
+ Save location
+ Storage permission required
+
+"Permission to access storage is required for browsing files.
+
+Re-enable this permission in the app settings if you permanently disabled it."
+ App settings
+ Grant
+
Select images (%1$d / %2$d)
Please select images to download
%1$s will be downloaded to the folder %2$s
diff --git a/Clover/build.gradle b/Clover/build.gradle
index 856d3d7f..aa141380 100644
--- a/Clover/build.gradle
+++ b/Clover/build.gradle
@@ -4,7 +4,7 @@ buildscript {
jcenter()
}
dependencies {
- classpath 'com.android.tools.build:gradle:2.1.0'
+ classpath 'com.android.tools.build:gradle:2.1.2'
}
}