Add a better save location picker

Taken from another project, might as well use it here.
This layout did allow switching between storage devices, but I found out
how the storage framework works now, and it's too much work to get it
to work right now, so the selection of storage is hidden. Reading works
fine but writing requires a lot more code. Might pick it up later if
there's a good library that does it for us. All the current code uses
the normal File api so that has to be rewritten afaik.

Closes #202
multisite
Floens 9 years ago
parent 796ad088bf
commit b2f525ffd0
  1. 14
      Clover/app/build.gradle
  2. 13
      Clover/app/src/main/AndroidManifest.xml
  3. 29
      Clover/app/src/main/java/org/floens/chan/core/model/FileItem.java
  4. 17
      Clover/app/src/main/java/org/floens/chan/core/model/FileItems.java
  5. 112
      Clover/app/src/main/java/org/floens/chan/core/saver/FileWatcher.java
  6. 31
      Clover/app/src/main/java/org/floens/chan/core/saver/StorageHelper.java
  7. 16
      Clover/app/src/main/java/org/floens/chan/core/settings/ChanSettings.java
  8. 4
      Clover/app/src/main/java/org/floens/chan/core/settings/Setting.java
  9. 129
      Clover/app/src/main/java/org/floens/chan/ui/adapter/FilesAdapter.java
  10. 33
      Clover/app/src/main/java/org/floens/chan/ui/controller/AdvancedSettingsController.java
  11. 18
      Clover/app/src/main/java/org/floens/chan/ui/controller/MainSettingsController.java
  12. 136
      Clover/app/src/main/java/org/floens/chan/ui/controller/SaveLocationController.java
  13. 201
      Clover/app/src/main/java/org/floens/chan/ui/fragment/FolderPickFragment.java
  14. 141
      Clover/app/src/main/java/org/floens/chan/ui/layout/FilesLayout.java
  15. 13
      Clover/app/src/main/java/org/floens/chan/utils/RecyclerUtils.java
  16. 9
      Clover/app/src/main/res/drawable-mdpi/ic_chevron_left_black_24dp.xml
  17. 9
      Clover/app/src/main/res/drawable-xxhdpi/ic_folder_black_24dp.xml
  18. 22
      Clover/app/src/main/res/layout/cell_file.xml
  19. 67
      Clover/app/src/main/res/layout/controller_save_location.xml
  20. 10
      Clover/app/src/main/res/values/strings.xml
  21. 2
      Clover/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'

@ -35,19 +35,20 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<!--Dual Window support for Samsung and LG-->
<meta-data
android:name="com.lge.support.SPLIT_WINDOW"
android:value="true"/>
android:value="true" />
<uses-library
android:required="false"
android:name="com.sec.android.app.multiwindow"/>
android:name="com.sec.android.app.multiwindow"
android:required="false" />
<meta-data
android:name="com.sec.android.support.multiwindow"
android:value="true"/>
android:value="true" />
<meta-data
android:name="com.sec.android.multiwindow.DEFAULT_SIZE_W"
android:value="632.0dip"/>
android:value="632.0dip" />
<meta-data
android:name="com.sec.android.multiwindow.DEFAULT_SIZE_H"
android:value="598.0dip"/>
android:value="598.0dip" />
<activity
android:name=".ui.activity.BoardActivity"

@ -0,0 +1,29 @@
package org.floens.chan.core.model;
import org.floens.chan.core.saver.StorageHelper;
import java.io.File;
public class FileItem {
public File file;
public FileItem(File file) {
this.file = file;
}
public boolean isFile() {
return file.isFile();
}
public boolean isFolder() {
return file.isDirectory();
}
public boolean canNavigate() {
return StorageHelper.canNavigate(file);
}
public boolean canOpen() {
return StorageHelper.canOpen(file);
}
}

@ -0,0 +1,17 @@
package org.floens.chan.core.model;
import java.io.File;
import java.util.List;
public class FileItems {
public final File path;
public final List<FileItem> fileItems;
public final boolean canNavigateUp;
public FileItems(File path, List<FileItem> fileItems, boolean canNavigateUp) {
this.path = path;
this.fileItems = fileItems;
this.canNavigateUp = canNavigateUp;
}
}

@ -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<FileItem> FILE_COMPARATOR = new Comparator<FileItem>() {
@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<FileItem> folderList = new ArrayList<>();
List<FileItem> 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<FileItem> 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);
}
}

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

@ -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<String>() {
@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<T> {
public final Setting<T> setting;
public SettingChanged(Setting<T> setting) {
this.setting = setting;
}
}
}

@ -42,6 +42,10 @@ public abstract class Setting<T> {
return def;
}
public String getKey() {
return key;
}
public void addCallback(SettingCallback<T> callback) {
this.callbacks.add(callback);
}

@ -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<RecyclerView.ViewHolder> {
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);
}
}

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

@ -64,6 +64,7 @@ public class MainSettingsController extends SettingsController implements Toolba
private ListSettingView<ChanSettings.MediaAutoLoadMode> 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();

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

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> adapter;
boolean hasParent = false;
private FolderPickListener listener;
private File currentPath;
private List<File> 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<String>(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<File> 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<File> 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);
}
}

@ -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<String, FileItemHistory> 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);
}
}

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

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M15.41,7.41L14,6l-6,6 6,6 1.41,-1.41L10.83,12z"/>
</vector>

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M10,4H4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V8c0,-1.1 -0.9,-2 -2,-2h-8l-2,-2z"/>
</vector>

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="@drawable/item_background"
android:orientation="horizontal">
<ImageView
android:id="@+id/image"
android:layout_width="56dp"
android:layout_height="match_parent"
android:scaleType="center" />
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:padding="16dp"
android:textSize="18sp" />
</LinearLayout>

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<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.layout.FilesLayout
android:id="@+id/files_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?backcolor"
android:elevation="4dp">
<LinearLayout
android:id="@+id/back_layout"
android:layout_width="wrap_content"
android:layout_height="56dp"
android:layout_gravity="left"
android:background="@drawable/item_background"
android:elevation="8dp"
android:orientation="horizontal"
android:paddingRight="22dp">
<ImageView
android:id="@+id/back_image"
android:layout_width="56dp"
android:layout_height="match_parent"
android:scaleType="center"
android:src="@drawable/ic_chevron_left_black_24dp" />
<TextView
android:id="@+id/back_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:text="@string/up"
android:textSize="18sp" />
</LinearLayout>
</FrameLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:clipChildren="false"
android:clipToPadding="false" />
</org.floens.chan.ui.layout.FilesLayout>
<android.support.design.widget.FloatingActionButton
android:id="@+id/set_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="right|bottom"
android:layout_margin="16dp"
android:src="@drawable/ic_done_white_24dp" />
</android.support.design.widget.CoordinatorLayout>

@ -67,6 +67,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<string name="add">Add</string>
<string name="close">Close</string>
<string name="back">Back</string>
<string name="up">Up</string>
<string name="ok">OK</string>
<string name="exit">Exit</string>
<string name="delete">Delete</string>
@ -326,6 +327,15 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
<string name="filters_screen">Filters</string>
<string name="save_location_screen">Save location</string>
<string name="write_permission_required_title">Storage permission required</string>
<string name="write_permission_required">
"Permission to access storage is required for browsing files.
Re-enable this permission in the app settings if you permanently disabled it."</string>
<string name="write_permission_app_settings">App settings</string>
<string name="write_permission_grant">Grant</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>

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

Loading…
Cancel
Save