diff --git a/app/build.gradle b/app/build.gradle index 3ac028c1e..fe40546fd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,8 +7,8 @@ android { defaultConfig { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 50 - versionName "2.0.4" + versionCode 51 + versionName "2.0.5" } lintOptions { @@ -62,6 +62,7 @@ android { compile 'com.cocosw:bottomsheet:1.+@aar' compile project(':libraries:MarkdownView') compile 'com.commit451:PhotoView:1.2.4' + compile 'com.joanzapata.iconify:android-iconify-material-community:2.2.1' testCompile 'junit:junit:4.12' testCompile 'org.robolectric:robolectric:3.0' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 641affb17..2c750199b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,8 +1,8 @@ + android:versionCode="51" + android:versionName="2.0.5"> diff --git a/app/src/main/java/com/seafile/seadroid2/SeadroidApplication.java b/app/src/main/java/com/seafile/seadroid2/SeadroidApplication.java index 54f0d0265..52125b377 100644 --- a/app/src/main/java/com/seafile/seadroid2/SeadroidApplication.java +++ b/app/src/main/java/com/seafile/seadroid2/SeadroidApplication.java @@ -5,6 +5,8 @@ import android.app.Application; import android.content.Context; +import com.joanzapata.iconify.Iconify; +import com.joanzapata.iconify.fonts.MaterialCommunityModule; import com.nostra13.universalimageloader.cache.disc.impl.UnlimitedDiscCache; import com.nostra13.universalimageloader.cache.disc.naming.Md5FileNameGenerator; import com.nostra13.universalimageloader.core.ImageLoader; @@ -19,6 +21,8 @@ public class SeadroidApplication extends Application { public void onCreate() { super.onCreate(); + Iconify.with(new MaterialCommunityModule()); + SeadroidApplication.context = getApplicationContext(); initImageLoader(getApplicationContext()); diff --git a/app/src/main/java/com/seafile/seadroid2/SeafConnection.java b/app/src/main/java/com/seafile/seadroid2/SeafConnection.java index 948fe6c23..5549b1ed8 100644 --- a/app/src/main/java/com/seafile/seadroid2/SeafConnection.java +++ b/app/src/main/java/com/seafile/seadroid2/SeafConnection.java @@ -266,6 +266,46 @@ public String getRepos() throws SeafException { } } + public String getEvents(int start) throws SeafException { + try { + String apiPath = String.format("api2/events/"); + + Map params = Maps.newHashMap(); + params.put("start", start); + HttpRequest req = prepareApiGetRequest(apiPath, params); + checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); + + String result = new String(req.bytes(), "UTF-8"); + return result; + } catch (SeafException e) { + throw e; + } catch (HttpRequestException e) { + throw getSeafExceptionFromHttpRequestException(e); + } catch (IOException e) { + throw SeafException.networkException; + } + } + + public String getHistoryChanges(String repoID, String commitId) throws SeafException { + try { + String apiPath = String.format("api2/repo_history_changes/%s/", repoID); + Map params = Maps.newHashMap(); + params.put("commit_id", commitId); + HttpRequest req = prepareApiGetRequest(apiPath, params); + checkRequestResponseStatus(req, HttpURLConnection.HTTP_OK); + + String result = new String(req.bytes(), "UTF-8"); + + return result; + } catch (SeafException e) { + throw e; + } catch (HttpRequestException e) { + throw getSeafExceptionFromHttpRequestException(e); + } catch (IOException e) { + throw SeafException.networkException; + } + } + public String getStarredFiles() throws SeafException { try { HttpRequest req = prepareApiGetRequest("api2/starredfiles/"); diff --git a/app/src/main/java/com/seafile/seadroid2/data/CommitDetails.java b/app/src/main/java/com/seafile/seadroid2/data/CommitDetails.java new file mode 100644 index 000000000..7ec27075b --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/data/CommitDetails.java @@ -0,0 +1,108 @@ +package com.seafile.seadroid2.data; + +import com.google.common.collect.Lists; +import com.seafile.seadroid2.util.Utils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.List; + +/** + * Commit details for activities history changes + */ +public class CommitDetails { + public List addedFiles; + public List deletedFiles; + public List modifiedFiles; + public List renamedFiles; + public List addedDirs; + public List deletedDirs; + + public CommitDetails() { + addedFiles = Lists.newArrayList(); + deletedFiles = Lists.newArrayList(); + modifiedFiles = Lists.newArrayList(); + renamedFiles = Lists.newArrayList(); + addedDirs = Lists.newArrayList(); + deletedDirs = Lists.newArrayList(); + } + + public static CommitDetails fromJson(String json) throws JSONException { + final JSONObject jsonObject = Utils.parseJsonObject(json); + final JSONArray addedFiles = jsonObject.optJSONArray("added_files"); + final JSONArray deletedFiles = jsonObject.optJSONArray("deleted_files"); + final JSONArray modifiedFiles = jsonObject.optJSONArray("modified_files"); + final JSONArray renamedFiles = jsonObject.optJSONArray("renamed_files"); + final JSONArray addedDirs = jsonObject.optJSONArray("added_dirs"); + final JSONArray deletedDirs = jsonObject.optJSONArray("deleted_dirs"); + + CommitDetails details = new CommitDetails(); + processFileList(details.addedFiles, addedFiles); + processFileList(details.deletedFiles, deletedFiles); + processFileList(details.modifiedFiles, modifiedFiles); + processFileList(details.renamedFiles, renamedFiles); + processFileList(details.addedDirs, addedDirs); + processFileList(details.deletedDirs, deletedDirs); + + return details; + } + + private static void processFileList(List list, JSONArray jsonArray) throws JSONException { + if (jsonArray == null) return; + + for (int i = 0; i < jsonArray.length(); i++) { + list.add(jsonArray.getString(i)); + } + } + + public List getDeletedDirs() { + return deletedDirs; + } + + public void setDeletedDirs(List deletedDirs) { + this.deletedDirs = deletedDirs; + } + + public List getRenamedFiles() { + return renamedFiles; + } + + public void setRenamedFiles(List renamedFiles) { + this.renamedFiles = renamedFiles; + } + + public List getModifiedFiles() { + return modifiedFiles; + } + + public void setModifiedFiles(List modifiedFiles) { + this.modifiedFiles = modifiedFiles; + } + + public List getAddedFiles() { + return addedFiles; + } + + public void setAddedFiles(List addedFiles) { + this.addedFiles = addedFiles; + } + + public List getDeletedFiles() { + return deletedFiles; + } + + public void setDeletedFiles(List deletedFiles) { + this.deletedFiles = deletedFiles; + } + + public List getAddedDirs() { + return addedDirs; + } + + public void setAddedDirs(List addedDirs) { + this.addedDirs = addedDirs; + } + +} diff --git a/app/src/main/java/com/seafile/seadroid2/data/DataManager.java b/app/src/main/java/com/seafile/seadroid2/data/DataManager.java index 415948d93..de90840ba 100644 --- a/app/src/main/java/com/seafile/seadroid2/data/DataManager.java +++ b/app/src/main/java/com/seafile/seadroid2/data/DataManager.java @@ -3,6 +3,7 @@ import android.os.Environment; import android.util.Log; import android.util.Pair; + import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.seafile.seadroid2.R; @@ -12,6 +13,7 @@ import com.seafile.seadroid2.account.Account; import com.seafile.seadroid2.account.AccountInfo; import com.seafile.seadroid2.util.Utils; + import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -764,6 +766,52 @@ public void move(String srcRepoId, String srcDir, String srcFn, String dstRepoId } + public SeafActivities getEvents(int start) throws SeafException, JSONException { + if (!Utils.isNetworkOn()) { + throw SeafException.networkException; + } + + final String json = sc.getEvents(start); + + if (json == null) return null; + + final JSONObject object = Utils.parseJsonObject(json); + final int moreOffset = object.getInt("more_offset"); + final boolean more = object.getBoolean("more"); + final List events = parseEvents(json); + return new SeafActivities(events, moreOffset, more); + + } + + public String getHistoryChanges(String repoId, String commitId) throws SeafException { + return sc.getHistoryChanges(repoId, commitId); + } + + public List parseEvents(String json) { + try { + // may throw ClassCastException + JSONArray array = Utils.parseJsonArrayByKey(json, "events"); + if (array.length() == 0) + return Lists.newArrayListWithCapacity(0); + + ArrayList events = Lists.newArrayList(); + for (int i = 0; i < array.length(); i++) { + JSONObject obj = array.getJSONObject(i); + SeafEvent event = SeafEvent.fromJson(obj); + if (event != null) + events.add(event); + } + return events; + } catch (JSONException e) { + Log.e(DEBUG_TAG, "parse json error"); + return null; + } catch (Exception e) { + // other exception, for example ClassCastException + Log.e(DEBUG_TAG, "parseEvents exception"); + return null; + } + } + private static class PasswordInfo { String password; long timestamp; diff --git a/app/src/main/java/com/seafile/seadroid2/data/EventDetailsFileItem.java b/app/src/main/java/com/seafile/seadroid2/data/EventDetailsFileItem.java new file mode 100644 index 000000000..65429d41d --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/data/EventDetailsFileItem.java @@ -0,0 +1,55 @@ +package com.seafile.seadroid2.data; + +public class EventDetailsFileItem { + + public enum EType { + FILE_ADDED, + FILE_DELETED, + FILE_MODIFIED, + FILE_RENAMED, + DIR_ADDED, + DIR_DELETED + } + + private String path; + private EType eType; + private SeafEvent event; + + public EventDetailsFileItem(SeafEvent event, String path, EType etype) { + this.path = path; + this.eType = etype; + this.event = event; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public EType geteType() { + return eType; + } + + public SeafEvent getEvent() { + return event; + } + + public void setEvent(SeafEvent event) { + this.event = event; + } + + public boolean isFileOpenable() { + return eType == EType.FILE_ADDED || + eType == EType.FILE_MODIFIED || + eType == EType.FILE_RENAMED || + eType == EType.DIR_ADDED; + } + + public boolean isDir() { + return eType == EType.DIR_ADDED; + } + +} diff --git a/app/src/main/java/com/seafile/seadroid2/data/EventDetailsTree.java b/app/src/main/java/com/seafile/seadroid2/data/EventDetailsTree.java new file mode 100644 index 000000000..e0a0cc736 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/data/EventDetailsTree.java @@ -0,0 +1,52 @@ +package com.seafile.seadroid2.data; + +import com.google.common.collect.Lists; +import com.seafile.seadroid2.data.EventDetailsFileItem.EType; + +import java.util.List; + +/** + * Event details tree of each commit + */ +public class EventDetailsTree { + private List items; + private SeafEvent event; + + public EventDetailsTree(SeafEvent event) { + this.items = Lists.newArrayList(); + this.event = event; + } + + public List setCommitDetails(CommitDetails details) { + + items.clear(); + + processEventCategory(details.addedFiles, "Added files", EType.FILE_ADDED); + processEventCategory(details.deletedFiles, "Deleted files", EType.FILE_DELETED); + processEventCategory(details.modifiedFiles, "Modified files", EType.FILE_MODIFIED); + + processEventCategory(details.addedDirs, "Added folders", EType.DIR_ADDED); + processEventCategory(details.deletedDirs, "Deleted folders", EType.DIR_DELETED); + + // renamed files is a list of (before rename, after rename) pair + List renamedFiles = Lists.newArrayList(); + for (int i = 1, n = details.renamedFiles.size(); i < n; i += 2) { + final String rename = details.renamedFiles.get(i); + renamedFiles.add(rename); + } + processEventCategory(renamedFiles, "Renamed files", EType.FILE_RENAMED); + + return items; + } + + private void processEventCategory(List files, String desc, EType etype) { + if (files == null || files.isEmpty()) { + return; + } + + for (int i = 0, n = files.size(); i < n; i++) { + EventDetailsFileItem item = new EventDetailsFileItem(event, files.get(i), etype); + items.add(item); + } + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/data/SeafActivities.java b/app/src/main/java/com/seafile/seadroid2/data/SeafActivities.java new file mode 100644 index 000000000..d54dc6f9a --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/data/SeafActivities.java @@ -0,0 +1,45 @@ +package com.seafile.seadroid2.data; + +import java.util.List; + +/** + * Seafile Activities data model + */ +public class SeafActivities { + + public List events; + + public boolean more; + + public int offset; + + public List getEvents() { + return events; + } + + public void setEvents(List events) { + this.events = events; + } + + public boolean isMore() { + return more; + } + + public void setMore(boolean more) { + this.more = more; + } + + public int getOffset() { + return offset; + } + + public void setOffset(int offset) { + this.offset = offset; + } + + public SeafActivities(List events, int offset, boolean more) { + this.events = events; + this.offset = offset; + this.more = more; + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/data/SeafEvent.java b/app/src/main/java/com/seafile/seadroid2/data/SeafEvent.java new file mode 100644 index 000000000..62c5767e0 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/data/SeafEvent.java @@ -0,0 +1,336 @@ +package com.seafile.seadroid2.data; + +import android.util.Log; + +import com.seafile.seadroid2.R; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Seafile event entity + */ +public class SeafEvent implements SeafItem { + public static final String DEBUG_TAG = SeafItem.class.getSimpleName(); + + public static final String EVENT_TYPE_REPO_CREATE = "repo-create"; + public static final String EVENT_TYPE_REPO_DELETE = "repo-delete"; + + // true for events like a file upload by unregistered user from a + // uploadable link + private boolean anonymous; + private String repo_id; + private String author; + private String nick; + private long time; + private String etype; + private String repo_name; + private String desc; + private String commit_id; + private String date; + private String name; + private String time_relative; + private String converted_cmmt_desc; + private String avatar; + private boolean repo_encrypted; + private boolean more_files; + + public static SeafEvent fromJson(JSONObject obj) { + SeafEvent event = new SeafEvent(); + try { + event.author = obj.optString("author"); + if (event.author.isEmpty()) { + event.author = "anonymous"; + event.anonymous = true; + } else { + event.anonymous = false; + } + + event.repo_id = obj.optString("repo_id"); + event.nick = obj.optString("nick"); + if (event.nick.isEmpty()) { + event.nick = "anonymous"; + } + + event.etype = obj.optString("etype"); + event.repo_name = obj.optString("repo_name"); + event.time = obj.getLong("time"); + event.avatar = obj.optString("avatar"); + event.commit_id = obj.optString("commit_id"); + event.date = obj.optString("date"); + event.name = obj.optString("name"); + event.time_relative = obj.optString("time_relative"); + event.converted_cmmt_desc = obj.optString("converted_cmmt_desc"); + event.repo_encrypted = obj.getBoolean("repo_encrypted"); + event.more_files = obj.getBoolean("more_files"); + + event.desc = obj.optString("desc"); + if (event.etype.equals(EVENT_TYPE_REPO_CREATE)) { + event.desc = String.format("Created library \"%s\"", event.repo_name); + } else if (event.etype.equals(EVENT_TYPE_REPO_DELETE)) { + event.desc = String.format("Deleted library \"%s\"", event.repo_name); + } + + event.desc = translateCommitDesc(event.desc); + return event; + } catch (JSONException e) { + Log.d(DEBUG_TAG, e.getMessage()); + return null; + } + } + + private static Matcher fullMatch(Pattern pattern, String str) { + Matcher matcher = pattern.matcher(str); + return matcher.matches() ? matcher : null; + } + + public static String translateCommitDesc(String value) { + if (value.startsWith("Reverted repo")) { + value.replace("repo", "library"); + } + + if (value.startsWith("Reverted library")) { + return value.replace("Reverted library to status at", "Reverted library to status at"); + } else if (value.startsWith("Reverted file")) { + String regex = "Reverted file \"(.*)\" to status at (.*)"; + Pattern pattern = Pattern.compile(regex); + Matcher matcher; + if ((matcher = fullMatch(pattern, value)) != null) { + String name = matcher.group(1); + String time = matcher.group(2); + return String.format("Reverted file \"%s\" to status at %s.", name, time); + } + + } else if (value.startsWith("Recovered deleted directory")) { + return value.replace("Recovered deleted directory", "Recovered deleted directory"); + } else if (value.startsWith("Changed library")) { + return value.replace("Changed library name or description", "Changed library name or description"); + } else if (value.startsWith("Merged") || value.startsWith("Auto merge")) { + return "Auto merge by seafile system"; + } + + final String[] lines = value.split("\n"); + StringBuilder out = new StringBuilder(); + + for (int i = 0; i < lines.length; i++) { + final String translateLine = translateLine(lines[i]); + out.append(translateLine); + // should avoid append for the last item + if (i < lines.length - 1) out.append("\n"); + } + + return out.toString(); + } + + private static String translateLine(String line) { + // String regex = String.format("(%s) \"(.*)\"\\s?(and ([0-9]+) more (files|directories))?", getOperations()); + // String regex = String.format("(%s).* ".*\..*"\s+(and ([0-9]+) more (files|directories))?", getOperations()); + String regex = String.format("(%s).* \"\\S+\\.\\S+\"\\s+(and ([0-9]+) more (files|directories))?", getOperations()); + Pattern pattern = Pattern.compile(regex); + + Matcher matcher; + if ((matcher = fullMatch(pattern, line)) == null) { + return line; + } + + String op = matcher.group(1); + String file_name = matcher.group(2); + String has_more = matcher.group(3); + String n_more = matcher.group(4); + String more_type = matcher.group(5); + + String op_trans = (getVerbsMap().get(op) == null ? op : getVerbsMap().get(op)); + + String type, ret; + // has more may be null caused a crash + if (has_more.length() > 0) { + if (more_type.equals("files")) { + type = "files"; + } else { + type = "directories"; + } + + String more = String.format("and %s more", n_more); + ret = String.format("%s \"%s\" %s %s.", op_trans, file_name, more, type); + } else { + ret = String.format("%s \"%s\".", op_trans, file_name); + } + + return ret; + } + + private static HashMap verbsMap = null; + private static HashMap getVerbsMap() { + if (verbsMap == null) { + verbsMap = new HashMap<>(); + verbsMap.put("Added", "Added"); + verbsMap.put("Deleted", "Deleted"); + verbsMap.put("Removed", "Removed"); + verbsMap.put("Modified", "Modified"); + verbsMap.put("Renamed", "Renamed"); + verbsMap.put("Moved", "Moved"); + verbsMap.put("Added directory", "Added directory"); + verbsMap.put("Removed directory", "Removed directory"); + verbsMap.put("Renamed directory", "Renamed directory"); + verbsMap.put("Moved directory", "Moved directory"); + } + + return verbsMap; + } + + private static String getOperations() { + return "Added|Deleted|Removed|Modified|Renamed|Moved|Added directory|Removed directory|Renamed directory|Moved directory"; + } + + public boolean isAnonymous() { + return anonymous; + } + + public void setAnonymous(boolean anonymous) { + this.anonymous = anonymous; + } + + public void setRepo_id(String repo_id) { + this.repo_id = repo_id; + } + + public void setAuthor(String author) { + this.author = author; + } + + public void setNick(String nick) { + this.nick = nick; + } + + public void setTime(int time) { + this.time = time; + } + + public void setEtype(String etype) { + this.etype = etype; + } + + public void setRepo_name(String repo_name) { + this.repo_name = repo_name; + } + + public void setDesc(String desc) { + this.desc = desc; + } + + public String getRepo_id() { + return repo_id; + } + + public String getAuthor() { + return author; + } + + public String getNick() { + return nick; + } + + public long getTime() { + return time; + } + + public String getEtype() { + return etype; + } + + public String getRepo_name() { + return repo_name; + } + + public String getDesc() { + return desc; + } + + @Override + public String getTitle() { + return desc; + } + + @Override + public String getSubtitle() { + return nick; + } + + @Override + public int getIcon() { + return R.drawable.repo; + } + + public void setTime(long time) { + this.time = time; + } + + public String getCommit_id() { + return commit_id; + } + + public void setCommit_id(String commit_id) { + this.commit_id = commit_id; + } + + public String getDate() { + return date; + } + + public void setDate(String date) { + this.date = date; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getTime_relative() { + return time_relative; + } + + public void setTime_relative(String time_relative) { + this.time_relative = time_relative; + } + + public String getConverted_cmmt_desc() { + return converted_cmmt_desc; + } + + public void setConverted_cmmt_desc(String converted_cmmt_desc) { + this.converted_cmmt_desc = converted_cmmt_desc; + } + + public String getAvatar() { + return avatar; + } + + public void setAvatar(String avatar) { + this.avatar = avatar; + } + + public boolean isRepo_encrypted() { + return repo_encrypted; + } + + public void setRepo_encrypted(boolean repo_encrypted) { + this.repo_encrypted = repo_encrypted; + } + + public boolean isMore_files() { + return more_files; + } + + public void setMore_files(boolean more_files) { + this.more_files = more_files; + } + +} diff --git a/app/src/main/java/com/seafile/seadroid2/data/ServerInfo.java b/app/src/main/java/com/seafile/seadroid2/data/ServerInfo.java index bacc8aff2..50eb73309 100644 --- a/app/src/main/java/com/seafile/seadroid2/data/ServerInfo.java +++ b/app/src/main/java/com/seafile/seadroid2/data/ServerInfo.java @@ -21,6 +21,24 @@ public ServerInfo(String url, String version, String features) { this.features = features; } + protected ServerInfo(Parcel in) { + url = in.readString(); + version = in.readString(); + features = in.readString(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public ServerInfo createFromParcel(Parcel in) { + return new ServerInfo(in); + } + + @Override + public ServerInfo[] newArray(int size) { + return new ServerInfo[size]; + } + }; + /** * * @return Server version. Might be null diff --git a/app/src/main/java/com/seafile/seadroid2/ui/activity/BrowserActivity.java b/app/src/main/java/com/seafile/seadroid2/ui/activity/BrowserActivity.java index 703d262ab..e4e049afb 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/activity/BrowserActivity.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/activity/BrowserActivity.java @@ -38,6 +38,7 @@ import android.view.MenuItem; import android.view.View; import android.view.Window; +import android.widget.FrameLayout; import com.google.common.collect.Lists; import com.seafile.seadroid2.R; @@ -113,7 +114,6 @@ public class BrowserActivity extends BaseActivity private static final String DEBUG_TAG = "BrowserActivity"; public static final String ACTIONBAR_PARENT_PATH = "/"; private static final String UPLOAD_TASKS_VIEW = "UploadTasks"; - private static final String FILES_VIEW = "Files"; public static final String OPEN_FILE_DIALOG_FRAGMENT_TAG = "openfile_fragment"; public static final String PASSWORD_DIALOG_FRAGMENT_TAG = "password_fragment"; @@ -132,6 +132,7 @@ public class BrowserActivity extends BaseActivity private int currentPosition = 0; private SeafileTabsAdapter adapter; private View mLayout; + private FrameLayout container; private boolean boolPermissionGranted = false; private TabLayout mTabLayout; @@ -144,7 +145,6 @@ public class BrowserActivity extends BaseActivity TransferReceiver mTransferReceiver; SettingsManager settingsMgr; AccountManager accountManager; - private String currentSelectedItem = FILES_VIEW; FetchFileDialog fetchFileDialog = null; @@ -223,6 +223,7 @@ protected void onCreate(Bundle savedInstanceState) { } setContentView(R.layout.tabs_main); mLayout = findViewById(R.id.main_layout); + container = (FrameLayout) findViewById(R.id.bottom_sheet_container); setSupportActionBar(getActionBarToolbar()); // enable ActionBar app icon to behave as action back getSupportActionBar().setDisplayHomeAsUpEnabled(true); @@ -341,6 +342,10 @@ public void onTabReselected(TabLayout.Tab tab) { requestReadExternalStoragePermission(); } + public FrameLayout getContainer() { + return container; + } + private void finishAndStartAccountsActivity() { Intent newIntent = new Intent(this, AccountsActivity.class); newIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); @@ -1596,7 +1601,7 @@ public void onBackPressed() { return; } - if (currentSelectedItem == FILES_VIEW && currentPosition == INDEX_LIBRARY_TAB) { + if (currentPosition == INDEX_LIBRARY_TAB) { if (navContext.inRepo()) { if (navContext.isRepoRoot()) { navContext.setRepoID(null); @@ -1615,9 +1620,13 @@ public void onBackPressed() { } else super.onBackPressed(); - } else { + } else if (currentPosition == INDEX_ACTIVITIES_TAB) { + if (getActivitiesFragment().isBottomSheetShown()) { + getActivitiesFragment().hideBottomSheet(); + } else + super.onBackPressed(); + } else super.onBackPressed(); - } } @Override diff --git a/app/src/main/java/com/seafile/seadroid2/ui/adapter/ActivitiesItemAdapter.java b/app/src/main/java/com/seafile/seadroid2/ui/adapter/ActivitiesItemAdapter.java new file mode 100644 index 000000000..414dd128f --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/adapter/ActivitiesItemAdapter.java @@ -0,0 +1,209 @@ +package com.seafile.seadroid2.ui.adapter; + +import android.support.annotation.NonNull; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.google.common.collect.Lists; +import com.nostra13.universalimageloader.core.DisplayImageOptions; +import com.nostra13.universalimageloader.core.ImageLoader; +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.data.SeafEvent; +import com.seafile.seadroid2.data.SeafItem; +import com.seafile.seadroid2.ui.activity.BrowserActivity; +import com.seafile.seadroid2.ui.widget.CircleImageView; +import com.seafile.seadroid2.util.Utils; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Adapter for Activities tab + */ +public class ActivitiesItemAdapter extends BaseAdapter { + public static final String DEBUG_TAG = ActivitiesItemAdapter.class.getSimpleName(); + + public static final int REFRESH_ON_NONE = 0; + public static final int REFRESH_ON_PULL_DOWN = 1; + public static final int REFRESH_ON_PULL_UP = 2; + private int state = REFRESH_ON_NONE; + + private ArrayList items; + private BrowserActivity mActivity; + private ImageLoader loader; + private DisplayImageOptions options; + + public ActivitiesItemAdapter(BrowserActivity activity) { + this.mActivity = activity; + items = Lists.newArrayList(); + loader = ImageLoader.getInstance(); + options = new DisplayImageOptions.Builder() + .extraForDownloader(mActivity.getAccount()) + .showStubImage(R.drawable.default_avatar) + .showImageOnLoading(R.drawable.default_avatar) + .showImageForEmptyUri(R.drawable.default_avatar) + .showImageOnFail(R.drawable.default_avatar) + .resetViewBeforeLoading() + .cacheInMemory(true) + .cacheOnDisk(true) + .considerExifParams(true) + .build(); + } + + @Override + public int getCount() { + return items.size() + 1; + } + + public void clear() { + items.clear(); + } + + public void add(SeafEvent entry) { + items.add(entry); + } + + public void notifyChanged() { + notifyDataSetChanged(); + } + + @Override + public SeafItem getItem(int position) { + return items.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + public void setItems(List events) { + items.clear(); + items.addAll(events); + } + + private LinearLayout mFooterView; + + public void setFooterViewLoading(boolean more) { + ProgressBar progress = (ProgressBar) mFooterView.findViewById(R.id.progressbar); + TextView text = (TextView) mFooterView.findViewById(R.id.text); + if (more) { + mFooterView.setVisibility(View.VISIBLE); + progress.setVisibility(View.VISIBLE); + text.setVisibility(View.VISIBLE); + } else { + progress.setVisibility(View.GONE); + mFooterView.setVisibility(View.GONE); + text.setVisibility(View.GONE); + } + } + + public void setState(int state) { + this.state = state; + } + + public View getFooterView() { + return this.mFooterView; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (position == getCount() - 1) { + this.mFooterView = (LinearLayout) LayoutInflater.from(parent.getContext()).inflate(R.layout.footer_load_more, null); + switch (state) { + case REFRESH_ON_NONE: + case REFRESH_ON_PULL_DOWN: + setFooterViewLoading(false); + break; + case REFRESH_ON_PULL_UP: + setFooterViewLoading(true); + break; + } + return mFooterView; + } + if (position < 0) { + position = 0; + } + + final SeafEvent item = items.get(position); + View view = convertView; + // TODO optimize by setting tags + final ViewHolder viewHolder; + + view = LayoutInflater.from(mActivity).inflate(R.layout.list_item_activities, null); + TextView title = (TextView) view.findViewById(R.id.tv_activities_mod_desc); + TextView nick = (TextView) view.findViewById(R.id.tv_activities_nick); + TextView date = (TextView) view.findViewById(R.id.tv_activities_date); + TextView repoName = (TextView) view.findViewById(R.id.tv_activities_repo_name); + CircleImageView icon = (CircleImageView) view.findViewById(R.id.iv_activities_avatar); + viewHolder = new ViewHolder(title, nick, date, repoName, icon); + view.setTag(viewHolder); + + if (!TextUtils.isEmpty(item.getAvatar())) { + final String avatar = parseAvatar(item.getAvatar()); + loader.displayImage(avatar, viewHolder.icon, options); + } else { + // show a place holder indicating the error + loader.displayImage(item.getAvatar(), viewHolder.icon, options); + } + + viewHolder.title.setText(item.getDesc()); + viewHolder.nick.setText(item.getNick()); + + if (!TextUtils.isEmpty(item.getTime_relative())) { + final String relative = parseRelativeTime(item.getTime_relative()); + viewHolder.date.setText(relative); + viewHolder.date.setVisibility(View.VISIBLE); + } else { + viewHolder.date.setVisibility(View.GONE); + } + viewHolder.repoName.setText(item.getRepo_name()); + return view; + } + + private String parseAvatar(@NonNull String avatar) { + // + String re1 = ".*?"; // Non-greedy match on filler + String re2 = "(src)"; // Variable Name 1 + String re3 = ".*?"; // Non-greedy match on filler + String re4 = "((?:\\/[\\w\\.\\-]+)+)"; // Unix Path 1 + + Pattern p = Pattern.compile(re1 + re2 + re3 + re4, Pattern.CASE_INSENSITIVE | Pattern.DOTALL); + Matcher m = p.matcher(avatar); + if (m.find()) { + String avatarPath = m.group(2); + return Utils.pathJoin(mActivity.getAccount().getServer(), avatarPath); + } else return avatar; + } + + private String parseRelativeTime(@NonNull String relativeTime) { + String regex = "(<[^>]+>)"; + final String[] split = relativeTime.split(regex); + if (split.length > 1) { + return split[1]; + } else return relativeTime; + } + + private class ViewHolder { + TextView title, nick, date, repoName; + CircleImageView icon; + + public ViewHolder(TextView title, TextView nick, TextView date, TextView repoName, CircleImageView icon) { + super(); + this.icon = icon; + this.title = title; + this.nick = nick; + this.date = date; + this.repoName = repoName; + } + } + +} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/adapter/BottomSheetAdapter.java b/app/src/main/java/com/seafile/seadroid2/ui/adapter/BottomSheetAdapter.java new file mode 100644 index 000000000..e85c71684 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/adapter/BottomSheetAdapter.java @@ -0,0 +1,86 @@ +package com.seafile.seadroid2.ui.adapter; + +import android.content.Context; +import android.graphics.Color; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.TextView; + +import com.joanzapata.iconify.fonts.MaterialCommunityIcons; +import com.joanzapata.iconify.widget.IconTextView; +import com.seafile.seadroid2.R; +import com.seafile.seadroid2.data.EventDetailsFileItem; +import com.seafile.seadroid2.data.EventDetailsTree; + +import java.util.List; + +public class BottomSheetAdapter extends BaseAdapter { + private List items; + private Context context; + + public BottomSheetAdapter(Context context, List items) { + this.items = items; + this.context = context; + } + + @Override + public int getCount() { + return items.size(); + } + + @Override + public Object getItem(int i) { + return items.get(i); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View contentView, ViewGroup viewGroup) { + ViewHolder holder; + if (contentView == null) { + holder = new ViewHolder(); + contentView = View.inflate(context, R.layout.list_item_diff, null); + holder.file = (TextView) contentView.findViewById(R.id.tv_diff_file_name); + holder.icon = (IconTextView) contentView.findViewById(R.id.tv_diff_icon); + contentView.setTag(holder); + } else { + holder = (ViewHolder) contentView.getTag(); + } + + final EventDetailsFileItem eventDetailsFileItem = items.get(position); + + holder.file.setText(eventDetailsFileItem.getPath()); + switch (eventDetailsFileItem.geteType()) { + case FILE_ADDED: + case DIR_ADDED: + holder.file.setTextColor(Color.parseColor("#6CC644")); + holder.icon.setText("{" + MaterialCommunityIcons.mdi_plus.key() + " #6CC644}"); + break; + case FILE_MODIFIED: + holder.file.setTextColor(Color.parseColor("#D0B44C")); + holder.icon.setText("{" + MaterialCommunityIcons.mdi_pencil.key() + " #D0B44C}"); + break; + case FILE_RENAMED: + holder.file.setTextColor(Color.parseColor("#677A85")); + holder.icon.setText("{" + MaterialCommunityIcons.mdi_arrow_right.key() + " #677A85}"); + break; + case FILE_DELETED: + case DIR_DELETED: + holder.file.setTextColor(Color.parseColor("#BD2C00")); + holder.icon.setText("{" + MaterialCommunityIcons.mdi_minus.key() + " #BD2C00}"); + break; + } + + return contentView; + } + + static class ViewHolder{ + public TextView file; + public IconTextView icon; + } +} diff --git a/app/src/main/java/com/seafile/seadroid2/ui/fragment/ActivitiesFragment.java b/app/src/main/java/com/seafile/seadroid2/ui/fragment/ActivitiesFragment.java index 3c8028e1e..74312f758 100644 --- a/app/src/main/java/com/seafile/seadroid2/ui/fragment/ActivitiesFragment.java +++ b/app/src/main/java/com/seafile/seadroid2/ui/fragment/ActivitiesFragment.java @@ -2,65 +2,82 @@ import android.app.Activity; import android.content.Intent; -import android.net.http.SslCertificate; -import android.net.http.SslError; +import android.os.AsyncTask; import android.os.Bundle; +import android.support.annotation.Nullable; import android.support.v4.app.Fragment; +import android.support.v4.widget.SwipeRefreshLayout; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.animation.AnimationUtils; -import android.webkit.JsResult; -import android.webkit.SslErrorHandler; -import android.webkit.WebChromeClient; -import android.webkit.WebView; -import android.webkit.WebViewClient; +import android.widget.AbsListView; +import android.widget.AdapterView; import android.widget.FrameLayout; -import android.widget.Toast; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.RelativeLayout; +import com.google.common.collect.Lists; import com.seafile.seadroid2.R; -import com.seafile.seadroid2.account.Account; +import com.seafile.seadroid2.SeafException; +import com.seafile.seadroid2.data.CommitDetails; import com.seafile.seadroid2.data.DataManager; +import com.seafile.seadroid2.data.EventDetailsFileItem; +import com.seafile.seadroid2.data.EventDetailsTree; +import com.seafile.seadroid2.data.SeafActivities; +import com.seafile.seadroid2.data.SeafEvent; import com.seafile.seadroid2.data.SeafRepo; -import com.seafile.seadroid2.ssl.CertsManager; import com.seafile.seadroid2.ui.NavContext; import com.seafile.seadroid2.ui.ToastUtils; import com.seafile.seadroid2.ui.activity.BrowserActivity; import com.seafile.seadroid2.ui.activity.FileActivity; +import com.seafile.seadroid2.ui.adapter.ActivitiesItemAdapter; +import com.seafile.seadroid2.ui.adapter.BottomSheetAdapter; import com.seafile.seadroid2.ui.dialog.TaskDialog; +import com.seafile.seadroid2.util.ConcurrentAsyncTask; import com.seafile.seadroid2.util.Utils; -import java.io.UnsupportedEncodingException; -import java.net.URLDecoder; -import java.security.cert.X509Certificate; -import java.util.HashMap; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; +import org.json.JSONException; + +import java.util.List; public class ActivitiesFragment extends Fragment { private static final String DEBUG_TAG = "ActivitiesFragment"; - - private static final String ACTIVITIES_URL = "api2/html/events/"; - - private WebView webView = null; - private FrameLayout mWebViewContainer = null; - private View mProgressContainer; + public static final int REFRESH_ON_NONE = 0; + public static final int REFRESH_ON_PULL_DOWN = 1; + public static final int REFRESH_ON_PULL_UP = 2; + private static int mRefreshType = REFRESH_ON_NONE; + + private BrowserActivity mActivity; + private SwipeRefreshLayout refreshLayout; + private ListView listView; + private ActivitiesItemAdapter adapter; + + private RelativeLayout ppwContainerView; + private RelativeLayout ppw; + private View underLine, maskView; + + private List events; + private boolean boolShown = false; + private int offset; + + public boolean isBottomSheetShown() { + return boolShown; + } @Override public void onAttach(Activity activity) { super.onAttach(activity); - Log.d(DEBUG_TAG, "ActivitiesFragment Attached"); + // Log.d(DEBUG_TAG, "ActivitiesFragment Attached"); + mActivity = (BrowserActivity) getActivity(); } @Override public void onDetach() { super.onDetach(); - } - - private BrowserActivity getBrowserActivity() { - return (BrowserActivity)getActivity(); + mActivity = null; } @Override @@ -70,104 +87,314 @@ public View onCreateView(LayoutInflater inflater, ViewGroup container, } @Override - public void onPause() { - Log.d(DEBUG_TAG, "onPause"); - super.onPause(); - - if (webView != null) { - mWebViewContainer.removeView(webView); - } + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + refreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.swiperefresh); + listView = (ListView) view.findViewById(R.id.activities_listview); + events = Lists.newArrayList(); } @Override - public void onActivityCreated(Bundle savedInstanceState) { - Log.d(DEBUG_TAG, "onActivityCreated"); + public void onActivityCreated(final Bundle savedInstanceState) { + // Log.d(DEBUG_TAG, "onActivityCreated"); + + refreshLayout.setColorSchemeResources(R.color.fancy_orange); + refreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { + @Override + public void onRefresh() { + mRefreshType = REFRESH_ON_PULL_DOWN; + offset = 0; + refreshView(); + } + }); + + adapter = new ActivitiesItemAdapter(mActivity); + listView.setAdapter(adapter); + listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView adapterView, View view, int position, long l) { + final SeafEvent seafEvent = (SeafEvent) adapterView.getItemAtPosition(position); + if (mActivity == null) return; + + final String repoId = seafEvent.getRepo_id(); + final String repoName = seafEvent.getRepo_name(); + + if (seafEvent.isRepo_encrypted() && !DataManager.getRepoPasswordSet(repoId)) { + final SeafRepo repo = mActivity.getDataManager().getCachedRepoByID(repoId); + + if (repo == null) { + ToastUtils.show(mActivity, getString(R.string.repo_not_found)); + return; + } + + String password = DataManager.getRepoPassword(repoId); + mActivity.showPasswordDialog(repoName, repoId, + new TaskDialog.TaskDialogListener() { + @Override + public void onTaskSuccess() { + switchTab(repoId, repoName, repo.getRootDirID()); + } + }, password); + } else { + LoadHistoryChangesTask task = new LoadHistoryChangesTask(seafEvent); + ConcurrentAsyncTask.execute(task); + } + } + }); - mWebViewContainer = (FrameLayout)getView().findViewById(R.id.webViewContainer); - mProgressContainer = getView().findViewById(R.id.progressContainer); + listView.setOnScrollListener(new AbsListView.OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView view, int i) { + if (adapter == null || adapter.getCount() == 0) { + return; + } + boolean scrollEnd = false; + try { + if (view.getPositionForView(adapter.getFooterView()) == view.getLastVisiblePosition()) scrollEnd = true; + } catch (Exception e) { + scrollEnd = false; + } - if (webView == null) { - webView = new WebView(getBrowserActivity()); - initWebView(); - loadActivitiesPage(); - } + if (mRefreshType == REFRESH_ON_NONE && scrollEnd && offset > 0) { + refreshView(); + mRefreshType = REFRESH_ON_PULL_UP; + adapter.setFooterViewLoading(true); + } else { + adapter.setFooterViewLoading(false); + } + + adapter.setState(mRefreshType); + } + + @Override + public void onScroll(AbsListView absListView, int i, int i1, int i2) {} + }); - getBrowserActivity().supportInvalidateOptionsMenu(); + mRefreshType = REFRESH_ON_PULL_DOWN; + offset = 0; + refreshView(); + + mActivity.supportInvalidateOptionsMenu(); super.onActivityCreated(savedInstanceState); } - @Override - public void onResume() { - // We add the webView on create and remove it on pause, so as to make - // it retain state. Otherwise the webview will reload the url every - // time the tab is switched. - super.onResume(); - mWebViewContainer.addView(webView); + public void refreshView() { + new LoadEventsTask().execute(); } - public void refreshView() { - loadActivitiesPage(); + public void hideBottomSheet() { + switchMenu(); } - private void initWebView() { - webView.getSettings().setJavaScriptEnabled(true); - webView.setWebViewClient(new MyWebViewClient()); + public void switchMenu() { + if (mActivity == null || ppw == null || ppwContainerView == null || maskView == null || underLine == null) { + boolShown = false; + return; + } - webView.setWebChromeClient(new MyWebChromeClient()); + final FrameLayout container = mActivity.getContainer(); + if (!boolShown) { + container.removeView(ppwContainerView); + container.addView(ppwContainerView); + ppw.setVisibility(View.VISIBLE); + ppw.setAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.menu_in)); + underLine.setVisibility(View.VISIBLE); + maskView.setVisibility(View.VISIBLE); + maskView.setAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.mask_in)); + } else { + ppw.setVisibility(View.GONE); + ppw.setAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.menu_out)); + underLine.setVisibility(View.GONE); + container.removeView(underLine); + maskView.setVisibility(View.GONE); + maskView.setAnimation(AnimationUtils.loadAnimation(getContext(), R.anim.mask_out)); + } + + boolShown = !boolShown; } - private void loadActivitiesPage() { - showPageLoading(true); - Account account = getBrowserActivity().getAccount(); - String url = account.getServer() + ACTIVITIES_URL; + private void showChangesDialog(final List items) { + int maskColor = 0x88888888; - webView.loadUrl(url, getExtraHeaders()); - } + if (boolShown && ppwContainerView != null) { + switchMenu(); + return; + } + + ppwContainerView = new RelativeLayout(mActivity); + ppwContainerView.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); + + underLine = new View(getContext()); + underLine.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, Utils.dip2px(mActivity, 1.0f))); + underLine.setBackgroundColor(getResources().getColor(R.color.divider_color)); + underLine.setVisibility(View.GONE); + ppwContainerView.addView(underLine, 0); + + ppw = (RelativeLayout) View.inflate(mActivity, R.layout.ppw_history_changes, null); - private Map getExtraHeaders() { - Account account = getBrowserActivity().getAccount(); - String token = "Token " + account.getToken(); - Map headers = new HashMap(); - headers.put("Authorization", token); + maskView = new View(getContext()); + maskView.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)); + maskView.setBackgroundColor(maskColor); + maskView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + switchMenu(); + } + }); + maskView.setVisibility(View.GONE); + ppwContainerView.addView(maskView, 1); + ListView listView = (ListView) ppw.findViewById(R.id.lv_history_changes); + final BottomSheetAdapter adapter = new BottomSheetAdapter(mActivity, items); + listView.setAdapter(adapter); + listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + final EventDetailsFileItem fileItem = items.get(position); + onItemClicked(fileItem); + switchMenu(); + } + }); - return headers; + ppw.setVisibility(View.GONE); + ppwContainerView.addView(ppw, 2); + + switchMenu(); } - private void showPageLoading(boolean pageLoading) { - if (getBrowserActivity() == null) { + private void onItemClicked(EventDetailsFileItem fileItem) { + if (fileItem == null) { return; } - if (!pageLoading) { - mProgressContainer.startAnimation(AnimationUtils.loadAnimation( - getBrowserActivity(), android.R.anim.fade_out)); - webView.startAnimation(AnimationUtils.loadAnimation( - getBrowserActivity(), android.R.anim.fade_in)); - mProgressContainer.setVisibility(View.GONE); - webView.setVisibility(View.VISIBLE); + if (fileItem.isFileOpenable()) { + openLocalFile(fileItem); + } + } + + private void openLocalFile(EventDetailsFileItem fileItem) { + if (fileItem.isDir()) { + viewRepo(fileItem.getEvent().getRepo_id()); } else { - mProgressContainer.startAnimation(AnimationUtils.loadAnimation( - getBrowserActivity(), android.R.anim.fade_in)); - webView.startAnimation(AnimationUtils.loadAnimation( - getBrowserActivity(), android.R.anim.fade_out)); + viewFile(fileItem.getEvent().getRepo_id(), fileItem.getPath()); + } + } + + class LoadEventsTask extends AsyncTask { + SeafException err; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + } - mProgressContainer.setVisibility(View.VISIBLE); - webView.setVisibility(View.INVISIBLE); + @Override + protected SeafActivities doInBackground(Void... voids) { + if (mActivity == null) return null; + + try { + // Log.d(DEBUG_TAG, "offset " + offset); + return mActivity.getDataManager().getEvents(offset); + } catch (SeafException e) { + err = e; + e.printStackTrace(); + return null; + } catch (JSONException e) { + e.printStackTrace(); + return null; + } + } + + @Override + protected void onPostExecute(SeafActivities result) { + super.onPostExecute(result); + refreshLayout.setRefreshing(false); + if (result == null) { + if (err != null) { + ToastUtils.show(mActivity, err.getMessage()); + } + return; + } + + if (mRefreshType == REFRESH_ON_PULL_DOWN) { + events = result.getEvents(); + } else { + if (offset == result.getOffset()) { + // duplicate data + // Log.d(DEBUG_TAG, "duplicate data " + offset); + return; + } + + // Log.d(DEBUG_TAG, "return offset " + offset); + events.addAll(result.getEvents()); + } + + mRefreshType = REFRESH_ON_NONE; + + offset = result.getOffset(); + if (!result.isMore()) { + ToastUtils.show(mActivity, getString(R.string.no_more_activities)); + return; + } + + adapter.setState(mRefreshType); + adapter.setItems(events); + adapter.notifyDataSetChanged(); + } + } + + class LoadHistoryChangesTask extends AsyncTask { + private SeafException err; + private SeafEvent event; + + public LoadHistoryChangesTask(SeafEvent event) { + this.event = event; + } + + @Override + protected CommitDetails doInBackground(String... params) { + try { + final String ret = mActivity.getDataManager().getHistoryChanges(event.getRepo_id(), event.getCommit_id()); + return CommitDetails.fromJson(ret); + } catch (SeafException e) { + err = e; + e.printStackTrace(); + return null; + } catch (JSONException e) { + e.printStackTrace(); + return null; + } + } + + @Override + protected void onPostExecute(CommitDetails ret) { + super.onPostExecute(ret); + if (ret == null) { + if (err != null) { + Log.e(DEBUG_TAG, err.getCode() + err.getMessage()); + ToastUtils.show(mActivity, err.getMessage()); + } + return; + } + + final EventDetailsTree tree = new EventDetailsTree(event); + final List items = tree.setCommitDetails(ret); + + showChangesDialog(items); } } private void viewRepo(final String repoID) { - final SeafRepo repo = getBrowserActivity().getDataManager().getCachedRepoByID(repoID); + final SeafRepo repo = mActivity.getDataManager().getCachedRepoByID(repoID); if (repo == null) { - ToastUtils.show(getBrowserActivity(), getString(R.string.repo_not_found)); + ToastUtils.show(mActivity, getString(R.string.repo_not_found)); return; } if (repo.encrypted && !DataManager.getRepoPasswordSet(repo.id)) { String password = DataManager.getRepoPassword(repo.id); - getBrowserActivity().showPasswordDialog(repo.name, repo.id, + mActivity.showPasswordDialog(repo.name, repo.id, new TaskDialog.TaskDialogListener() { @Override public void onTaskSuccess() { @@ -181,16 +408,16 @@ public void onTaskSuccess() { } private void viewFile(final String repoID, final String path) { - final SeafRepo repo = getBrowserActivity().getDataManager().getCachedRepoByID(repoID); + final SeafRepo repo = mActivity.getDataManager().getCachedRepoByID(repoID); if (repo == null) { - ToastUtils.show(getBrowserActivity(), R.string.library_not_found); + ToastUtils.show(mActivity, R.string.library_not_found); return; } if (repo.encrypted && !DataManager.getRepoPasswordSet(repo.id)) { String password = DataManager.getRepoPassword(repo.id); - getBrowserActivity().showPasswordDialog(repo.name, repo.id, + mActivity.showPasswordDialog(repo.name, repo.id, new TaskDialog.TaskDialogListener() { @Override public void onTaskSuccess() { @@ -204,115 +431,24 @@ public void onTaskSuccess() { } private void switchTab(String repoID, String repoName, String repoDir) { - NavContext nav = getBrowserActivity().getNavContext(); + NavContext nav = mActivity.getNavContext(); nav.setRepoID(repoID); nav.setRepoName(repoName); nav.setDir("/", repoDir); // switch to LIBRARY TAB - getBrowserActivity().setCurrentPosition(BrowserActivity.INDEX_LIBRARY_TAB); + mActivity.setCurrentPosition(BrowserActivity.INDEX_LIBRARY_TAB); } private void openFile(String repoID, String repoName, String filePath) { - int taskID = getBrowserActivity().getTransferService().addDownloadTask(getBrowserActivity().getAccount(), repoName, repoID, filePath); + // Log.d(DEBUG_TAG, "open fiel " + repoName + filePath); + int taskID = mActivity.getTransferService().addDownloadTask(mActivity.getAccount(), repoName, repoID, filePath); Intent intent = new Intent(getActivity(), FileActivity.class); intent.putExtra("repoName", repoName); intent.putExtra("repoID", repoID); intent.putExtra("filePath", filePath); - intent.putExtra("account", getBrowserActivity().getAccount()); + intent.putExtra("account", mActivity.getAccount()); intent.putExtra("taskID", taskID); - getBrowserActivity().startActivityForResult(intent, BrowserActivity.DOWNLOAD_FILE_REQUEST); - } - - private class MyWebViewClient extends WebViewClient { - // Display error messages - @Override - public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { - if (getBrowserActivity() != null) { - Toast.makeText(getBrowserActivity(), "Error: " + description, Toast.LENGTH_SHORT).show(); - showPageLoading(false); - } - } - - // Ignore SSL certificate validate - @Override - public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { - BrowserActivity mActivity = getBrowserActivity(); - if (mActivity == null) { - return; - } - - Account account = mActivity.getAccount(); - - SslCertificate sslCert = error.getCertificate(); - X509Certificate savedCert = CertsManager.instance().getCertificate(account); - - if (Utils.isSameCert(sslCert, savedCert)) { - Log.d(DEBUG_TAG, "trust this cert"); - handler.proceed(); - } else { - Log.d(DEBUG_TAG, "cert is not trusted"); - ToastUtils.show(mActivity, R.string.ssl_error); - showPageLoading(false); - } - } - - @Override - public boolean shouldOverrideUrlLoading(WebView webView, String url) { - Log.d(DEBUG_TAG, "loading url " + url); - String API_URL_PREFIX= "api://"; - if (!url.startsWith(API_URL_PREFIX)) { - return false; - } - - String req = url.substring(API_URL_PREFIX.length(), url.length()); - - Pattern REPO_PATTERN = Pattern.compile("repos/([-a-f0-9]{36})/?"); - Pattern REPO_FILE_PATTERN = Pattern.compile("repo/([-a-f0-9]{36})/files/\\?p=(.+)"); - Matcher matcher; - - if ((matcher = fullMatch(REPO_PATTERN, req)) != null) { - String repoID = matcher.group(1); - viewRepo(repoID); - - } else if ((matcher = fullMatch(REPO_FILE_PATTERN, req)) != null) { - String repoID = matcher.group(1); - - try { - String path = URLDecoder.decode(matcher.group(2), "UTF-8"); - viewFile(repoID, path); - } catch (UnsupportedEncodingException e) { - // Ignore - } - } - - return true; - } - - @Override - public void onPageFinished(WebView webView, String url) { - Log.d(DEBUG_TAG, "onPageFinished " + url); - if (getBrowserActivity() != null) { - String js = String.format("javascript:setToken('%s')", - getBrowserActivity().getAccount().getToken()); - webView.loadUrl(js); - } - showPageLoading(false); - } - } - - private static Matcher fullMatch(Pattern pattern, String str) { - Matcher matcher = pattern.matcher(str); - return matcher.matches() ? matcher : null; - } - - private class MyWebChromeClient extends WebChromeClient { - - // For debug js - @Override - public boolean onJsAlert(WebView view, String url, String message, JsResult result) { - Log.d(DEBUG_TAG, "alert: " + message); - return super.onJsAlert(view, url, message, result); - } + mActivity.startActivityForResult(intent, BrowserActivity.DOWNLOAD_FILE_REQUEST); } } diff --git a/app/src/main/java/com/seafile/seadroid2/ui/widget/CircleImageView.java b/app/src/main/java/com/seafile/seadroid2/ui/widget/CircleImageView.java new file mode 100644 index 000000000..ed7c31c80 --- /dev/null +++ b/app/src/main/java/com/seafile/seadroid2/ui/widget/CircleImageView.java @@ -0,0 +1,243 @@ +package com.seafile.seadroid2.ui.widget; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.widget.ImageView; + +import com.seafile.seadroid2.R; + +/** + * A fast circular ImageView perfect for profile images + * + * styleable attr are border_width and border_color + */ +public class CircleImageView extends ImageView { + + private static final ScaleType SCALE_TYPE = ScaleType.CENTER_CROP; + + private static final Bitmap.Config BITMAP_CONFIG = Bitmap.Config.ARGB_8888; + private static final int COLORDRAWABLE_DIMENSION = 1; + + private static final int DEFAULT_BORDER_WIDTH = 0; + private static final int DEFAULT_BORDER_COLOR = Color.BLACK; + + private final RectF mDrawableRect = new RectF(); + private final RectF mBorderRect = new RectF(); + + private final Matrix mShaderMatrix = new Matrix(); + private final Paint mBitmapPaint = new Paint(); + private final Paint mBorderPaint = new Paint(); + + private int mBorderColor = DEFAULT_BORDER_COLOR; + private int mBorderWidth = DEFAULT_BORDER_WIDTH; + + private Bitmap mBitmap; + private BitmapShader mBitmapShader; + private int mBitmapWidth; + private int mBitmapHeight; + + private float mDrawableRadius; + private float mBorderRadius; + + private boolean mReady; + private boolean mSetupPending; + + public CircleImageView(Context context) { + super(context); + } + + public CircleImageView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CircleImageView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + super.setScaleType(SCALE_TYPE); + + TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0); + + mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView_border_width, DEFAULT_BORDER_WIDTH); + mBorderColor = a.getColor(R.styleable.CircleImageView_border_color, DEFAULT_BORDER_COLOR); + + a.recycle(); + + mReady = true; + + if (mSetupPending) { + setup(); + mSetupPending = false; + } + } + + @Override + public ScaleType getScaleType() { + return SCALE_TYPE; + } + + @Override + public void setScaleType(ScaleType scaleType) { + if (scaleType != SCALE_TYPE) { + throw new IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType)); + } + } + + @Override + protected void onDraw(Canvas canvas) { + if (getDrawable() == null) { + return; + } + + canvas.drawCircle(getWidth() / 2, getHeight() / 2, mDrawableRadius, mBitmapPaint); + if (mBorderWidth != 0) { + canvas.drawCircle(getWidth() / 2, getHeight() / 2, mBorderRadius, mBorderPaint); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + setup(); + } + + public int getBorderColor() { + return mBorderColor; + } + + public void setBorderColor(int borderColor) { + if (borderColor == mBorderColor) { + return; + } + + mBorderColor = borderColor; + mBorderPaint.setColor(mBorderColor); + invalidate(); + } + + public int getBorderWidth() { + return mBorderWidth; + } + + public void setBorderWidth(int borderWidth) { + if (borderWidth == mBorderWidth) { + return; + } + + mBorderWidth = borderWidth; + setup(); + } + + @Override + public void setImageBitmap(Bitmap bm) { + super.setImageBitmap(bm); + mBitmap = bm; + setup(); + } + + @Override + public void setImageDrawable(Drawable drawable) { + super.setImageDrawable(drawable); + mBitmap = getBitmapFromDrawable(drawable); + setup(); + } + + @Override + public void setImageResource(int resId) { + super.setImageResource(resId); + mBitmap = getBitmapFromDrawable(getDrawable()); + setup(); + } + + private Bitmap getBitmapFromDrawable(Drawable drawable) { + if (drawable == null) { + return null; + } + + if (drawable instanceof BitmapDrawable) { + return ((BitmapDrawable) drawable).getBitmap(); + } + + try { + Bitmap bitmap; + + if (drawable instanceof ColorDrawable) { + bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG); + } else { + bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), BITMAP_CONFIG); + } + + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } catch (OutOfMemoryError e) { + return null; + } + } + + private void setup() { + if (!mReady) { + mSetupPending = true; + return; + } + + if (mBitmap == null) { + return; + } + + mBitmapShader = new BitmapShader(mBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP); + + mBitmapPaint.setAntiAlias(true); + mBitmapPaint.setShader(mBitmapShader); + + mBorderPaint.setStyle(Paint.Style.STROKE); + mBorderPaint.setAntiAlias(true); + mBorderPaint.setColor(mBorderColor); + mBorderPaint.setStrokeWidth(mBorderWidth); + + mBitmapHeight = mBitmap.getHeight(); + mBitmapWidth = mBitmap.getWidth(); + + mBorderRect.set(0, 0, getWidth(), getHeight()); + mBorderRadius = Math.min((mBorderRect.height() - mBorderWidth) / 2, (mBorderRect.width() - mBorderWidth) / 2); + + mDrawableRect.set(mBorderWidth, mBorderWidth, mBorderRect.width() - mBorderWidth, mBorderRect.height() - mBorderWidth); + mDrawableRadius = Math.min(mDrawableRect.height() / 2, mDrawableRect.width() / 2); + + updateShaderMatrix(); + invalidate(); + } + + private void updateShaderMatrix() { + float scale; + float dx = 0; + float dy = 0; + + mShaderMatrix.set(null); + + if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) { + scale = mDrawableRect.height() / (float) mBitmapHeight; + dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f; + } else { + scale = mDrawableRect.width() / (float) mBitmapWidth; + dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f; + } + + mShaderMatrix.setScale(scale, scale); + mShaderMatrix.postTranslate((int) (dx + 0.5f) + mBorderWidth, (int) (dy + 0.5f) + mBorderWidth); + + mBitmapShader.setLocalMatrix(mShaderMatrix); + } + +} diff --git a/app/src/main/java/com/seafile/seadroid2/util/Utils.java b/app/src/main/java/com/seafile/seadroid2/util/Utils.java index fcdeb36e1..c4632d5db 100644 --- a/app/src/main/java/com/seafile/seadroid2/util/Utils.java +++ b/app/src/main/java/com/seafile/seadroid2/util/Utils.java @@ -30,6 +30,7 @@ import com.seafile.seadroid2.SeadroidApplication; import com.seafile.seadroid2.data.SeafRepo; import com.seafile.seadroid2.fileschooser.SelectableFile; +import com.seafile.seadroid2.ui.activity.BrowserActivity; import org.json.JSONArray; import org.json.JSONException; @@ -808,4 +809,14 @@ private static boolean isSameDN(SslCertificate.DName dName1, SslCertificate.DNam return dName1.getDName().equals(dName2.getDName()); } } + + public static int dip2px(Context context, float dip) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (dip * scale + 0.5f); + } + + public static int px2dip(Context context, float pxValue) { + final float scale = context.getResources().getDisplayMetrics().density; + return (int) (pxValue / scale + 0.5f); + } } diff --git a/app/src/main/res/anim/mask_in.xml b/app/src/main/res/anim/mask_in.xml new file mode 100644 index 000000000..38aabea23 --- /dev/null +++ b/app/src/main/res/anim/mask_in.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/mask_out.xml b/app/src/main/res/anim/mask_out.xml new file mode 100644 index 000000000..f5ade3980 --- /dev/null +++ b/app/src/main/res/anim/mask_out.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/menu_in.xml b/app/src/main/res/anim/menu_in.xml new file mode 100644 index 000000000..6042e4977 --- /dev/null +++ b/app/src/main/res/anim/menu_in.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/menu_out.xml b/app/src/main/res/anim/menu_out.xml new file mode 100644 index 000000000..84184c5cc --- /dev/null +++ b/app/src/main/res/anim/menu_out.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi/loading.png b/app/src/main/res/drawable-hdpi/loading.png new file mode 100644 index 000000000..a0a914382 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/loading.png differ diff --git a/app/src/main/res/drawable/progressloading.xml b/app/src/main/res/drawable/progressloading.xml new file mode 100644 index 000000000..8216dc121 --- /dev/null +++ b/app/src/main/res/drawable/progressloading.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activities_fragment.xml b/app/src/main/res/layout/activities_fragment.xml index 750bd735e..5fd981987 100644 --- a/app/src/main/res/layout/activities_fragment.xml +++ b/app/src/main/res/layout/activities_fragment.xml @@ -1,29 +1,17 @@ + + - - - - - - + android:layout_height="match_parent" /> + - - diff --git a/app/src/main/res/layout/footer_load_more.xml b/app/src/main/res/layout/footer_load_more.xml new file mode 100644 index 000000000..d56fa16f1 --- /dev/null +++ b/app/src/main/res/layout/footer_load_more.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_activities.xml b/app/src/main/res/layout/list_item_activities.xml new file mode 100644 index 000000000..45b8a5d92 --- /dev/null +++ b/app/src/main/res/layout/list_item_activities.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/list_item_diff.xml b/app/src/main/res/layout/list_item_diff.xml new file mode 100644 index 000000000..9ddee9ebf --- /dev/null +++ b/app/src/main/res/layout/list_item_diff.xml @@ -0,0 +1,21 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/ppw_history_changes.xml b/app/src/main/res/layout/ppw_history_changes.xml new file mode 100644 index 000000000..72efc9891 --- /dev/null +++ b/app/src/main/res/layout/ppw_history_changes.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/tabs_main.xml b/app/src/main/res/layout/tabs_main.xml index 5894d5093..3e44a849b 100644 --- a/app/src/main/res/layout/tabs_main.xml +++ b/app/src/main/res/layout/tabs_main.xml @@ -1,34 +1,39 @@ - + android:layout_height="match_parent"> - + android:layout_height="match_parent"> - - - - + android:layout_height="wrap_content"> - + + + + + + - \ No newline at end of file + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 349aeade6..268697768 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -171,6 +171,11 @@ Zum Aktualisieren einmal antippen Hochladen abgeschlossen Hochladen abgebrochen Hochladen fehlgeschlagen + Die Datei ist schon vorhanden + Ein Element namens \"%s\" existiert bereits an diesem Ort.\n Wollen Sie es mit dem von Ihnen hochgeladenen ersetzen? + Alle ersetzen + Beide behalten + Ersetzen vor %d Tagen vor %d Stunden vor %d Minuten @@ -437,4 +442,6 @@ Zum Aktualisieren einmal antippen Es wurden keine Elemente ausgewählt + Um Dateien zu bearbeiten bitte Speicherrechte erteilen. + Berechtigung wurde nicht erteilt diff --git a/app/src/main/res/values-es-rAR/strings.xml b/app/src/main/res/values-es-rAR/strings.xml index ae112d5fd..c3cdd7622 100644 --- a/app/src/main/res/values-es-rAR/strings.xml +++ b/app/src/main/res/values-es-rAR/strings.xml @@ -173,6 +173,7 @@ Un ítem llamado \"%s\" ya existe en esta ubicación.\n ¿Desea reemplazarlo con el que está subiendo? Reemplazar todo Mantener ambos + Reemplazar hace %d días hace %d horas hace %d minutos @@ -441,4 +442,6 @@ Para procesar archivos, conceda permiso de almacenamiento para accederlos. El permiso no fue concedido + Detalle de las modificaciones + No hay mas actividades diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index ae112d5fd..c3cdd7622 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -173,6 +173,7 @@ Un ítem llamado \"%s\" ya existe en esta ubicación.\n ¿Desea reemplazarlo con el que está subiendo? Reemplazar todo Mantener ambos + Reemplazar hace %d días hace %d horas hace %d minutos @@ -441,4 +442,6 @@ Para procesar archivos, conceda permiso de almacenamiento para accederlos. El permiso no fue concedido + Detalle de las modificaciones + No hay mas actividades diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index cc492e457..085523067 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -5,7 +5,7 @@ Modifier Effacer Effacer - Êtes vous certain de vouloir effacer ce fichier ou ce dossier ? + Êtes-vous certain de vouloir effacer ce fichier ou ce dossier ? Effacé avec succès Effacer le fichier Effacer le dossier @@ -46,11 +46,11 @@ Erreur inconnue Nom de serveur invalide L\'adresse du serveur ne peut être vide - Email ne peut être vide + E-mail ne peut être vide Le mot de passe ne peut être vide Échec de la connexion - Erreur d\'email ou de mot de passe - L\'authentification a expirée. reconnectez vous. + Erreur d\'e-mail ou de mot de passe + L\'authentification a expirée, reconnectez-vous. Transfert Transféré Mis à jour @@ -65,7 +65,7 @@ Effacer toutes les taches terminées Favori Télécharger - Transfert + Envoi Partager le lien Exporter Effacer @@ -108,7 +108,7 @@ Aucune application disponible Ajouter à Seafile OK - Merci de créer un compte dans Seafile pour commencer + Veuillez d\'abord créer un compte sur Seafile Choisir un compte Choisir une bibliothèque Choisissez un dossier @@ -140,9 +140,9 @@ taille de fichier La bibliothèque a été effacée. Exporter ce fichier - Aucun transfert en cours - Aucune tâche de téléchargement - Note: Le nom de serveur peut contenir un port. Par exemple, www.exemple.com:8000 + Aucun envoi en cours + Aucun téléchargement en cours + Note : Le nom de serveur peut contenir un port. Par exemple, www.exemple.com:8000 Copier le lien Partager un lien vers ce fichier Partager un lien vers ce dossier @@ -150,7 +150,7 @@ Le lien est prêt à être collé/transmis. Renommer le fichier Renommer le dossier - Renommage réussi. + Renommé avec succès. Fichiers Envois @@ -169,6 +169,11 @@ Envoi terminé Envoi annulé Échec de l\'envoi + Le fichier existe déjà + Un élément nommé \"%s\" existe déjà à cet emplacement. \n Voulez-vous le remplacer avec celui que vous avez envoyé ? + Remplacer tout + Garder les deux + Remplacer il y a %d jours il y a %d heures il y a %d minutes @@ -176,7 +181,7 @@ À l\'instant Erreur de chargement des fichiers préférés Ouvrir comme - sélection les fichiers à transférer + Sélectionner les fichiers à envoyer Le stockage a été supprimé Erreur lors de la sélection du fichier @@ -186,7 +191,7 @@ Dossier vide Choisir un fichier Échec de la vérification de sécurité - La certificat de %s n\'est pas vérifié. Voulez-vous continuer? + La certificat de %s n\'est pas vérifié. Voulez-vous continuer ? %s utilise un certificat non valide. La connexion n\'est pas sécurisée. Voulez-vous continuer? Connexion non sécurisée Publié à @@ -202,7 +207,7 @@ Non Se souvenir de mon choix Copié avec succès - Copié avec succès + Déplacé avec succès Erreur interne du serveur Copie du fichier en cours Déplacement du fichier en cours @@ -218,7 +223,7 @@ Impossible de trouver cette bibliothèque. Elle est peut être effacée ou cryptée. Compte Infos utilisateur - Chargement en cours... + Chargement... Espace utilisé Déconnexion Cliquez pour vous déconnecter @@ -236,11 +241,11 @@ 5 échecs, réessayer dans 30 secondes Réessayer dans %1$d seconde - Réessayer dans %1$d secondes + Vous pouvez réessayer dans %1$d secondes Modèle incorrect, essayer %1$d fois - Modèle incorrect, essayer encore %1$d fois + Modèle incorrect, vous pouvez encore essayer %1$d fois Le nombre de points reliés n\'est pas suffisant, réessayer Réessayer @@ -248,11 +253,11 @@ OK Dessinez votre modèle de geste de verrouillage Apprendre comment dessiner un geste de verrouillage - Reliez au moins 4 points, réessayer! + Reliez au moins 4 points, réessayez ! Modèle enregistré Dessinez un modèle de geste de verrouillage Confirmez votre modèle - Le modèle ne correspond pas, réessayez! + Le modèle ne correspond pas, réessayez ! Tapez OK pour enregistrer votre modèle Relâcher le doigt quand le dessin est terminé Le modèle de geste de verrouillage a été enregistré avec succès @@ -260,7 +265,7 @@ Retour Suivant - Sauter + Ignorer Envoi des photos Service d\'envoi démarré @@ -271,7 +276,7 @@ Plan de données autorisé Envoi de photos uniquement par défaut Vidéos incluses - Changer la bibliothèque destinataire + Changer la bibliothèque de destination Choisissez d\'abord une bibliothèque Activer l\'envoi de photos Changer d\'album @@ -279,18 +284,18 @@ La bibliothèque sélectionnée n’existe pas Options avancées Options d\'envoi personnalisées - Téléversement personnalisé des albums + Envoi personnalisé d\'albums Choisir les albums photos - Scan automatique du périphérique + Scan automatique de l\'appareil Aide à la configuration - Merci d\'utiliser l\'aide à la configuration des envois de photos/vidéos. \nPour vous aider, commencer par démarrer le service d\'envoi de photos/vidéos, vous serez guidé dans vos premiers pas. + Merci d\'utiliser l\'aide à la configuration des envois de photos/vidéos. \nPour vous aider, commencez par démarrer le service d\'envoi de photos/vidéos, vous serez guidé dans vos premiers pas. Comment envoyer Wi-Fi seulement Wi-Fi ou plan de données - Choisissez votre option préférée. Vous pourrez changer les options dans Paramètres + Choisissez votre option préférée. Vous pourrez changer ces options plus tard dans Paramètres. Quoi envoyer - Seulement des photos + Seulement les photos Photos et vidéos Sélectionnez des albums @@ -301,31 +306,31 @@ Choisir un compte %s a été sélectionné Vers dossier parent - Deviner automatiquement les albums de la caméra - Laissez moi choisir mon album photos - Choisissez votre compte et votre bibliothèque préférés. Vous pourrez changer ces options dans Paramètres. + Détecter automatiquement les albums de l\'appareil photo + Laissez-moi choisir mon album photos + Choisissez votre compte et votre bibliothèque préférés. Vous pourrez changer ces options plus tard dans Paramètres. Terminer Seafile envoie maintenant les fichiers depuis vos dossiers sélectionnés. \n Vous pouvez fermer l\'application pendant cette opération. Glisser vers la gauche pour continuer Cliquez pour fermer l\'aide à la configuration. Chargement... - Camera photos et vidéos - Échec du téléversement de la caméra + Photos et vidéos de l’appareil photo + Échec d\'envoi de appareil photo Le dépôt sur le serveur n’existe pas Erreur d\'authentification. %s (renommé par Seafile) - A PROPOS + À PROPOS Version - A propos de l\'auteur + À propos de l\'auteur Client Seafile Android

%s

-
Contactez nous
+
Contactez-nous

Twitter: Seafile
- Email: support@seafile.com
+ E-mail: support@seafile.com

Contribution

https://github.com/haiwen/seadroid.git

@@ -336,14 +341,14 @@ Taille du cache Effacer le cache Voulez-vous effacer le cache ? - Succès de l’effacement du cache + Cache effacé avec succès Échec de l\'effacement du cache 0 KB Appuyer pour rafraichir... Relâcher pour rafraichir... - Chargement en cours... - Dernière mise à jour: + Chargement... + Dernière mise à jour : il y a %d secondes il y a %d minutes il y a %d heures @@ -362,20 +367,20 @@ La saisie du serveur est vide ! Serveur doit commencer par http:// ou https:// - Erreur: + Erreur : Connexion shibboleth Entrez le serveur... Connexion avec shibboleth - Chargement en cours... + Chargement... Erreur de connexion réseau, réessayer plus tard ! - Annuler toutes les taches + Annuler toutes les tâches Redémarrer - Relancer les taches en échec - Redémarrer les taches annulées - Effacer les taches en échec + Relancer les tâches échouées + Redémarrer les tâches annulées + Effacer les tâches qui ont échoué Effacer toutes les tâches - Effacer les taches terminées + Effacer les tâches terminées Liste des téléchargements Liste d\'envois Tous les fichiers ont été téléchargés @@ -402,7 +407,7 @@ Rechercher Recherche en cours... Saisissez des mots clés - Aucune occurence trouvée + Aucune occurrence trouvée Oups, la recherche n\'est pas supportée par ce serveur Bibliothèque introuvable. Elle a peut être été effacée @@ -414,10 +419,10 @@ Envoi des fichiers... Téléchargement des fichiers... - Téléversement terminé + Envoi terminé Téléversement de %1$d fichier (%2$d%%) - Téléversement de %1$d fichiers (%2$d%%) + Envoi de %1$d fichiers (%2$d%%) Téléchargement de %1$d fichier (%2$d%%) @@ -429,10 +434,12 @@ Bibliothèque cryptée non supportée Remplacer le fichier existant - Ce fichier existe déjà, Voulez vous le remplacer ? + Ce fichier existe déjà, voulez-vous le remplacer ? Le fichier existe déjà Aucun élément sélectionné + Pour traiter les fichiers, permettre l\'autorisation de stockage pour y accéder. + L\'autorisation n\'a pas été accordée diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 987e2d856..bb111f0db 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -172,6 +172,7 @@ \"%s\" という名前のファイルはすでにこの場所に存在します。\n アップロードしたファイルで置き換えますか? すべて置き換え 両方残す + 置き換え %d 日前 %d 時間前 %d 分前 @@ -432,4 +433,6 @@ ファイルを処理するには、ファイルにアクセスするストレージのアクセス許可が必要です。 アクセス許可が付与されていません + 更新の詳細 + これ以上アクティビティはありません diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 41e83c2c5..15031600b 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -168,6 +168,11 @@ 업로드가 끝났습니다 업로드를 취소했습니다 업로드에 실패했습니다 + 파일이 이미 있습니다 + \"%s\" 항목이 이 위치에 이미 있습니다.\n새로 업로드하는 항목으로 바꾸시겠습니까? + 모두 바꾸기 + 둘 다 유지 + 바꾸기 %d일 전 %d시간 전 %d분 전 @@ -426,4 +431,6 @@ 선택한 항목이 없습니다 + 파일을 처리하려면, 파일에 접근할 저장소 권한이 필요합니다. + 권한을 부여하지 않음 diff --git a/app/src/main/res/values-nl-rNL/strings.xml b/app/src/main/res/values-nl-rNL/strings.xml index 79534ee2c..f60d60752 100644 --- a/app/src/main/res/values-nl-rNL/strings.xml +++ b/app/src/main/res/values-nl-rNL/strings.xml @@ -169,6 +169,7 @@ Bestand bestaat reeds Alles vervangen Beide behouden + Vervangen %d dagen geleden %d uren geleden %d minuten geleden diff --git a/app/src/main/res/values-pl-rPL/strings.xml b/app/src/main/res/values-pl-rPL/strings.xml index aec284e10..a0a1302dd 100644 --- a/app/src/main/res/values-pl-rPL/strings.xml +++ b/app/src/main/res/values-pl-rPL/strings.xml @@ -174,6 +174,7 @@ Element o nazwie \"%s\" już istnieje w tej lokalizacji.\n Czy chcesz go zastąpić tym, który właśnie przesyłasz? Zamień wszystkie Zachowaj oba + Zamień %d dni temu %d godzin temu %d minut temu @@ -450,4 +451,6 @@ Aby obsługiwać pliki zezwól aplikacji na dostęp do pamięci. Nie przyznano uprawnień + Szczegóły modyfikacji + Brak więcej aktywności diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index dee2becf0..cbb4b3bf0 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -175,6 +175,7 @@ Элемент с именем \"%s\" уже существует в этом месте.\n Вы хотите заменить его на загружаемый ? Заменить все Оставить оба + Заменить %d дней назад %d часов назад %d минут назад @@ -458,4 +459,6 @@ Email: support@seafile.com
Для обработки файлов, позволить получить доступ к их хранилищу Разрешение не было предоставлено + Подробности изменений + Не больше действий diff --git a/app/src/main/res/values-sw600dp/dimens.xml b/app/src/main/res/values-sw600dp/dimens.xml index 362ab7f04..b8e9b0c49 100644 --- a/app/src/main/res/values-sw600dp/dimens.xml +++ b/app/src/main/res/values-sw600dp/dimens.xml @@ -87,4 +87,6 @@ 80dp + 300dp + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 68f1c6c32..8e97f6538 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -432,4 +432,6 @@ 在处理文件之前,请允许应用获取存储权限 未获得权限 + 修改详情 + 没有更多活动了 diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index cd9b7747d..0c35871a8 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -61,4 +61,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 6ca5628da..bc0c8a7f0 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -11,6 +11,7 @@ #00000000 #80FFFFFF #80000000 + #d2dcdc #f4f5f6 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 39f87cf8a..ec28f564a 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -170,6 +170,7 @@ 10dp + 5dp 10dp 15dp 20dp @@ -184,4 +185,12 @@ 48dp 72dp + 100dp + 200dp + + 14sp + 16sp + 18sp + 20sp + 22sp \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 416bdd31b..2cf06d788 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -471,4 +471,7 @@ To process files, allow storage permission to access them. Permission was not granted + + Modification Details + No more activities diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 8bcd3c8f2..aaf876e92 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -115,4 +115,12 @@ #ff000000 14sp + + + diff --git a/build.gradle b/build.gradle index e1f77807a..60fd60684 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:2.0.0-beta6' + classpath 'com.android.tools.build:gradle:2.1.0-alpha3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files