diff --git a/app/build.gradle b/app/build.gradle index 6e29ac1..cd14f28 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -12,7 +12,7 @@ android { minSdkVersion 22 targetSdkVersion 30 versionCode 1 - versionName "1.0" + versionName "0.1-alpha" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -34,12 +34,16 @@ android { dependencies { implementation platform('com.google.firebase:firebase-bom:26.1.1') + + // Firebase common libraries implementation 'com.google.firebase:firebase-analytics' implementation 'com.google.firebase:firebase-auth' implementation 'com.google.firebase:firebase-firestore' + + // other stuff implementation 'org.jetbrains:annotations:16.0.2' implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'com.google.android.material:material:1.2.1' + implementation 'com.google.android.material:material:1.3.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' testImplementation 'junit:junit:4.13.1' androidTestImplementation 'androidx.test.ext:junit:1.1.2' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 299c9bd..ffa365e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,7 +6,6 @@ - + - + \ No newline at end of file diff --git a/app/src/main/java/com/iyxan23/sketch/collab/MainActivity.java b/app/src/main/java/com/iyxan23/sketch/collab/MainActivity.java index 49fa67d..1c2fc42 100644 --- a/app/src/main/java/com/iyxan23/sketch/collab/MainActivity.java +++ b/app/src/main/java/com/iyxan23/sketch/collab/MainActivity.java @@ -3,8 +3,16 @@ import android.Manifest; import android.content.Intent; import android.content.pm.PackageManager; +import android.graphics.Typeface; import android.os.Bundle; +import android.text.Editable; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextWatcher; +import android.text.style.StyleSpan; +import android.util.Log; import android.view.View; +import android.widget.EditText; import android.widget.Toast; import androidx.annotation.Nullable; @@ -24,6 +32,8 @@ import com.google.firebase.firestore.QuerySnapshot; import com.google.firebase.firestore.Source; import com.iyxan23.sketch.collab.adapters.ChangesAdapter; +import com.iyxan23.sketch.collab.adapters.SearchAdapter; +import com.iyxan23.sketch.collab.models.SearchItem; import com.iyxan23.sketch.collab.models.SketchwareProject; import com.iyxan23.sketch.collab.models.SketchwareProjectChanges; import com.iyxan23.sketch.collab.online.BrowseActivity; @@ -31,10 +41,15 @@ import org.json.JSONException; import java.util.ArrayList; +import java.util.HashMap; import java.util.concurrent.ExecutionException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; public class MainActivity extends AppCompatActivity { + private static final String TAG = "MainActivity"; + // List of local projects ArrayList localProjects = new ArrayList<>(); @@ -238,16 +253,28 @@ private void initialize() { assert snapshot != null; // snapshot shouldn't be null + Log.d("MainActivity", "documents: " + snapshot.getDocuments()); + Log.d("MainActivity", "project_key: " + project_key); + + // Check if the project doesn't exists in the database. + if (snapshot.getDocuments().size() == 0) continue; + DocumentSnapshot commit_info = snapshot.getDocuments().get(0); if (!project_commit.equals(commit_info.getId())) { // Hmm, looks like this man's project has an older commit, tell him to update his project + Log.d(TAG, "initialize: Old Commit"); } else { + Log.d(TAG, "initialize: Latest commit"); // This mans project has the same commit // Check if this project also has the same shasum String local_shasum = project.sha512sum(); String server_shasum = commit_info.getString("sha512sum"); + Log.d(TAG, "initialize: Checking shasum"); + Log.d(TAG, "shasum local: " + local_shasum); + Log.d(TAG, "shasum server: " + server_shasum); + // Check if they're the same if (!local_shasum.equals(server_shasum)) { // Alright looks like he's got some local updates with the same head commit @@ -288,4 +315,171 @@ private void initialize() { } }).start(); } + + // THE SEARCH / COMMANDS THING ================================================================= + + boolean isOpened = false; + + SearchAdapter s_adapter; + ArrayList s_items; + RecyclerView s_rv; + EditText s_edittext; + + HashMap index = new HashMap<>(); + + String[] commands = new String[] { + "upload", // goes to UploadActivity + "local", // goes to ViewLocalProjectActivity (soon) + "open", // goes to ViewOnlineProjectActivity + "browse", // goes to BrowseActivity + "show code", // goes to BrowseCodeActivity + "commits" // goes to CommitsActivity + }; + + /* Syntaxes: + * + * upload (project name) + * local + * open (project name) + * browse + * show code (project name) + * commits (project name) + */ + + public void search_button_click(View view) { + s_edittext = findViewById(R.id.search_edittext); + + View settings_button = findViewById(R.id.imageView); + View home_textview = findViewById(R.id.home_textview); + View search_autocomplete_layout = findViewById(R.id.inc_search); + View main_content = findViewById(R.id.scrollView2); + + s_rv = findViewById(R.id.search_rv); + + if (isOpened) { + // Do a search + } else { + // Open the thing + settings_button.setVisibility(View.GONE); + home_textview.setVisibility(View.GONE); + s_edittext.setVisibility(View.VISIBLE); + search_autocomplete_layout.setVisibility(View.VISIBLE); + main_content.setVisibility(View.GONE); + + // Initialize some stuff + s_items = new ArrayList<>(); + for (String item: commands) { + s_items.add(new SearchItem(item)); + } + + // Index localProjects to be a HashMap + // Oh yeah, we're including project name and app name + new Thread(() -> { + for (SketchwareProject project : localProjects) { + if (project.metadata == null) + continue; + + index.put(project.metadata.project_name, project.metadata.id); + index.put(project.metadata.app_name, project.metadata.id); + } + }).start(); + + s_adapter = new SearchAdapter(s_items, this); + + s_rv.setAdapter(s_adapter); + s_rv.setLayoutManager(new LinearLayoutManager(this)); + + s_edittext.addTextChangedListener(new TextWatcher() { + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { } + + @Override + public void afterTextChanged(Editable s) { } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + update_rv(s.toString()); + } + }); + } + } + + private void update_rv(String input) { + s_items.clear(); + + Pattern pattern = Pattern.compile(input); + + for (String command: commands) { + boolean matched = false; + + Matcher m = pattern.matcher(command); + + SpannableString command_s = new SpannableString(command); + while (m.find()) { + matched = true; + command_s.setSpan( + new StyleSpan(Typeface.BOLD), + m.start(), + m.end(), + Spannable.SPAN_INCLUSIVE_INCLUSIVE + ); + } + + if (matched) + s_items.add(new SearchItem(command_s, "Command")); + } + + s_adapter.updateView(s_items); + + // Find in local project(s) + for (String name: index.keySet()) { + boolean matched = false; + + Matcher m = pattern.matcher(name); + + SpannableString name_s = new SpannableString(name); + while (m.find()) { + matched = true; + name_s.setSpan( + new StyleSpan(Typeface.BOLD), + m.start(), + m.end(), + Spannable.SPAN_INCLUSIVE_INCLUSIVE + ); + } + + if (matched) + s_items.add(new SearchItem(name_s, "Open Local Project")); + } + + s_adapter.updateView(s_items); + + // TODO: ADD PUBLIC PROJECTS FUNCTIONALITY + } + + @Override + public void onBackPressed() { + s_edittext = findViewById(R.id.search_edittext); + + View settings_button = findViewById(R.id.imageView); + View home_textview = findViewById(R.id.home_textview); + View search_autocomplete_layout = findViewById(R.id.inc_search); + View main_content = findViewById(R.id.scrollView2); + + if (isOpened) { + // Close the search thing + settings_button.setVisibility(View.VISIBLE); + home_textview.setVisibility(View.VISIBLE); + s_edittext.setVisibility(View.GONE); + search_autocomplete_layout.setVisibility(View.GONE); + main_content.setVisibility(View.VISIBLE); + + // Clear some stuff to reduce the memory usage + s_items.clear(); + index.clear(); + + } else { + super.onBackPressed(); + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/iyxan23/sketch/collab/adapters/CommitAdapter.java b/app/src/main/java/com/iyxan23/sketch/collab/adapters/CommitAdapter.java index 54829c7..c0a8ba8 100644 --- a/app/src/main/java/com/iyxan23/sketch/collab/adapters/CommitAdapter.java +++ b/app/src/main/java/com/iyxan23/sketch/collab/adapters/CommitAdapter.java @@ -14,6 +14,7 @@ import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; +import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.firebase.Timestamp; import com.google.firebase.firestore.DocumentSnapshot; import com.iyxan23.sketch.collab.R; @@ -79,13 +80,61 @@ public void onBindViewHolder(@NonNull final ViewHolder holder, final int positio // https://stackoverflow.com/questions/11275034/android-calculating-minutes-hours-days-from-point-in-time CharSequence relativeTimeStr = DateUtils.getRelativeTimeSpanString( - item.timestamp.getSeconds() * 1000, + item.timestamp.getNanoseconds() / 1000, System.currentTimeMillis(), DateUtils.SECOND_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE ); holder.timestamp.setText(relativeTimeStr); + + holder.body.setOnClickListener(v -> { + // Show a bottomsheet + View bottom_sheet_view = LayoutInflater + .from(v.getContext()) + .inflate(R.layout.bottomsheet_commit, null); + + BottomSheetDialog bottomSheetDialog = new BottomSheetDialog(v.getContext()); + + TextView title = bottom_sheet_view.findViewById(R.id.commit_title); + TextView author = bottom_sheet_view.findViewById(R.id.commit_author); + TextView code = bottom_sheet_view.findViewById(R.id.patch_code); + TextView time = bottom_sheet_view.findViewById(R.id.commit_time); + + title.setText(item.name); + author.setText(item.author_username + " (Commit ID: " + item.id + ")"); + + StringBuilder patch = new StringBuilder(); + + // Check if this commit doesn't have any commits + if (item.patch != null) { + for (String key : item.patch.keySet()) { + Log.d(TAG, "onBindViewHolder: key: " + key + " | patch: " + item.patch.get(key)); + + patch.append(key).append(":\n").append(item.patch.get(key)); + } + + } else { + patch.append("This commit doesn't have any patch"); + } + + Log.d(TAG, "onBindViewHolder: result: " + patch.toString()); + + code.setText(patch.toString()); + + CharSequence relativeTimeStr_ = + DateUtils.getRelativeTimeSpanString( + item.timestamp.getNanoseconds() / 1000, + System.currentTimeMillis(), + + DateUtils.SECOND_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE + ); + + time.setText(relativeTimeStr_); + + bottomSheetDialog.setContentView(bottom_sheet_view); + bottomSheetDialog.show(); + }); } @Override diff --git a/app/src/main/java/com/iyxan23/sketch/collab/adapters/SearchAdapter.java b/app/src/main/java/com/iyxan23/sketch/collab/adapters/SearchAdapter.java new file mode 100644 index 0000000..dedf775 --- /dev/null +++ b/app/src/main/java/com/iyxan23/sketch/collab/adapters/SearchAdapter.java @@ -0,0 +1,83 @@ +package com.iyxan23.sketch.collab.adapters; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.text.SpannableString; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.iyxan23.sketch.collab.R; +import com.iyxan23.sketch.collab.models.SearchItem; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; + +public class SearchAdapter extends RecyclerView.Adapter { + private static final String TAG = "BrowseItemAdapter"; + + private ArrayList datas = new ArrayList<>(); + WeakReference activity; + + public SearchAdapter(Activity activity) { + this.activity = new WeakReference<>(activity); + } + + public SearchAdapter(ArrayList datas, Activity activity) { + this.datas = datas; + this.activity = new WeakReference<>(activity); + } + + public void updateView(ArrayList datas) { + this.datas = datas; + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ViewHolder( + LayoutInflater + .from(parent.getContext()) + .inflate(R.layout.rv_search_item, parent, false) + ); + } + + @SuppressLint("SetTextI18n") + @Override + public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) { + Log.d(TAG, "onBindViewHolder: called."); + SearchItem item = datas.get(position); + + holder.title.setText(item.title); + holder.subtitle.setText(item.subtitle); + + holder.body.setOnClickListener(v -> activity.get().startActivity(item.intent)); + } + + @Override + public int getItemCount() { + return datas.size(); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + + TextView title; + TextView subtitle; + + View body; + + public ViewHolder(@NonNull View itemView) { + super(itemView); + title = itemView.findViewById(R.id.search_item_title); + subtitle = itemView.findViewById(R.id.search_item_subtitle); + + body = itemView.findViewById(R.id.search_body); + } + } +} diff --git a/app/src/main/java/com/iyxan23/sketch/collab/adapters/SketchwareProjectAdapter.java b/app/src/main/java/com/iyxan23/sketch/collab/adapters/SketchwareProjectAdapter.java index 14a9598..1feb660 100644 --- a/app/src/main/java/com/iyxan23/sketch/collab/adapters/SketchwareProjectAdapter.java +++ b/app/src/main/java/com/iyxan23/sketch/collab/adapters/SketchwareProjectAdapter.java @@ -89,7 +89,7 @@ public void onBindViewHolder(@NonNull final ViewHolder holder, final int positio @Override public int getItemCount() { - return datas.size(); + return datas == null ? 0 : datas.size(); } public static class ViewHolder extends RecyclerView.ViewHolder { diff --git a/app/src/main/java/com/iyxan23/sketch/collab/helpers/PatchHelper.java b/app/src/main/java/com/iyxan23/sketch/collab/helpers/PatchHelper.java new file mode 100644 index 0000000..2495bdb --- /dev/null +++ b/app/src/main/java/com/iyxan23/sketch/collab/helpers/PatchHelper.java @@ -0,0 +1,99 @@ +package com.iyxan23.sketch.collab.helpers; + +import com.iyxan23.sketch.collab.models.Commit; +import com.iyxan23.sketch.collab.models.SketchwareProject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import name.fraser.neil.plaintext.diff_match_patch; + +public class PatchHelper { + + // FIXME: SOMETHING DOESN'T SEEM RIGHT + public static String reverse_patch(String patch) { + Pattern plus_pattern = Pattern.compile("^\\+", Pattern.MULTILINE); + Pattern minus_pattern = Pattern.compile("^-", Pattern.MULTILINE); + + StringBuilder patch_sb = new StringBuilder(patch); + + // Flip + to - + Matcher matcher = plus_pattern.matcher(patch); + while (matcher.find()) { + patch_sb.setCharAt(matcher.start(), '-'); + } + + // Flip - to + + matcher = minus_pattern.matcher(patch); + while (matcher.find()) { + patch_sb.setCharAt(matcher.start(), '+'); + } + + return patch_sb.toString(); + } + + /** + * This function basically jumps the current patch level to another patch level + * + * @param current The current sketchware project in hashmap + * @param commits ArrayList of commits (should be in a ascending sorted order) + * @param commit_current The current commit index + * @param commit_destination The destination commit index + * @return The current string but in the destination commit index + */ + public static HashMap go_to_commit(HashMap current, ArrayList commits, int commit_current, int commit_destination) { + if (commit_destination >= commits.size()) + throw new IndexOutOfBoundsException("commit_destination shouldn't be bigger than commits size"); + + HashMap project_data = current; + + String[] project_keys = new String[] {"mysc_project", "logic", "view", "library", "resource", "file"}; + + diff_match_patch dmp = new diff_match_patch(); + + if (commit_destination > commit_current) { + // The destination is above us, apply commits that are above the current commit + for (Commit commit: commits.subList(commit_current, commit_destination)) { + if (commit.patch == null) + continue; + + for (String key: project_keys) { + if (!commit.patch.containsKey(key)) continue; + + LinkedList patches = (LinkedList) dmp.patch_fromText(commit.patch.get(key)); + // TODO: CHECK PATCH STATUSES + Object[] result = dmp.patch_apply(patches, project_data.get(key)); + + project_data.put(key, (String) result[0]); + } + } + } else if (commit_destination < commit_current) { + // The destination is below us, apply commits downward + for (Commit commit: commits.subList(commit_destination, commit_current)) { + if (commit.patch == null) + continue; + + for (String key: project_keys) { + if (!commit.patch.containsKey(key)) continue; + + // Reverse the patch coz we're going downward + String reversed_patch = reverse_patch(commit.patch.get(key)); + + LinkedList patches = (LinkedList) dmp.patch_fromText(reversed_patch); + // TODO: CHECK PATCH STATUSES + Object[] result = dmp.patch_apply(patches, project_data.get(key)); + + project_data.put(key, (String) result[0]); + } + } + } else { + return current; + } + + // Return the final product + return project_data; + } +} diff --git a/app/src/main/java/com/iyxan23/sketch/collab/models/SearchItem.java b/app/src/main/java/com/iyxan23/sketch/collab/models/SearchItem.java new file mode 100644 index 0000000..80d950f --- /dev/null +++ b/app/src/main/java/com/iyxan23/sketch/collab/models/SearchItem.java @@ -0,0 +1,37 @@ +package com.iyxan23.sketch.collab.models; + +import android.content.Intent; +import android.text.SpannableString; + +public class SearchItem { + public Intent intent; + + public SpannableString title; + public String subtitle; + + public SearchItem() {} + + public SearchItem(String title) { + this.title = new SpannableString(title); + } + + public SearchItem(String title, String subtitle) { + this.title = new SpannableString(title); + this.subtitle = subtitle; + } + + public SearchItem(SpannableString title) { + this.title = title; + } + + public SearchItem(SpannableString title, String subtitle) { + this.title = title; + this.subtitle = subtitle; + } + + public SearchItem(Intent intent, SpannableString title, String subtitle) { + this.intent = intent; + this.title = title; + this.subtitle = subtitle; + } +} diff --git a/app/src/main/java/com/iyxan23/sketch/collab/models/Userdata.java b/app/src/main/java/com/iyxan23/sketch/collab/models/Userdata.java new file mode 100644 index 0000000..af23f9b --- /dev/null +++ b/app/src/main/java/com/iyxan23/sketch/collab/models/Userdata.java @@ -0,0 +1,84 @@ +package com.iyxan23.sketch.collab.models; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +public class Userdata implements Parcelable { + private String name; + private String uid; + + public Userdata(String name, String uid) { + this.name = name; + this.uid = uid; + } + + protected Userdata(Parcel in) { + name = in.readString(); + uid = in.readString(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public Userdata createFromParcel(Parcel in) { + return new Userdata(in); + } + + @Override + public Userdata[] newArray(int size) { + return new Userdata[size]; + } + }; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUid() { + return uid; + } + + public void setUid(String uid) { + this.uid = uid; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(name); + dest.writeString(uid); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Userdata userdata = (Userdata) o; + + return Objects.equals(name, userdata.name) && + Objects.equals(uid, userdata.uid); + } + + @Override + public int hashCode() { + return Objects.hash(name, uid); + } + + @Override + public String toString() { + return "Userdata{" + + "name='" + name + '\'' + + ", uid='" + uid + '\'' + + '}'; + } +} diff --git a/app/src/main/java/com/iyxan23/sketch/collab/online/BrowseActivity.java b/app/src/main/java/com/iyxan23/sketch/collab/online/BrowseActivity.java index 7b48034..9a09421 100644 --- a/app/src/main/java/com/iyxan23/sketch/collab/online/BrowseActivity.java +++ b/app/src/main/java/com/iyxan23/sketch/collab/online/BrowseActivity.java @@ -1,24 +1,23 @@ package com.iyxan23.sketch.collab.online; import android.os.Bundle; +import android.util.Log; import android.view.View; import android.widget.Toast; -import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import com.google.android.gms.tasks.Continuation; import com.google.android.gms.tasks.Task; import com.google.android.gms.tasks.Tasks; import com.google.firebase.Timestamp; import com.google.firebase.firestore.CollectionReference; +import com.google.firebase.firestore.DocumentReference; import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.Query; -import com.google.firebase.firestore.QueryDocumentSnapshot; import com.google.firebase.firestore.QuerySnapshot; import com.iyxan23.sketch.collab.R; import com.iyxan23.sketch.collab.adapters.BrowseItemAdapter; @@ -35,6 +34,23 @@ public class BrowseActivity extends AppCompatActivity { // Used to cache uid -> names HashMap cached_names = new HashMap<>(); + FirebaseFirestore firestore; + CollectionReference projects; + CollectionReference userdata; + + ArrayList items = new ArrayList<>(); + + BrowseItemAdapter adapter; + + // This variable is used to point at the bottom of our fetches + DocumentSnapshot after; + + // This variable indicates if we're at the bottom of the fetch (or that we've basically fetched every projects), not the recyclerview + boolean is_at_bottom = false; + + // The project count that should be loaded + int project_count_should_be_loaded = 5; + @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -43,7 +59,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { RecyclerView rv = findViewById(R.id.browse_rv); rv.setLayoutManager(new LinearLayoutManager(this)); - BrowseItemAdapter adapter = new BrowseItemAdapter(this); + adapter = new BrowseItemAdapter(this); rv.setAdapter(adapter); if (savedInstanceState != null) { @@ -56,30 +72,74 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { } } - FirebaseFirestore firestore = FirebaseFirestore.getInstance(); - CollectionReference projects = firestore.collection("projects"); - CollectionReference userdata = firestore.collection("userdata"); + firestore = FirebaseFirestore.getInstance(); + projects = firestore.collection("projects"); + userdata = firestore.collection("userdata"); + + new Thread(() -> { + // Only show open source projects + load_projects(project_count_should_be_loaded); + + if (savedInstanceState != null) savedInstanceState.putParcelableArrayList("items", items); + + runOnUiThread(() -> { + findViewById(R.id.progressBar_browse).setVisibility(View.GONE); + adapter.updateView(items); + + // Listener to listen if we're at the bottom of the list, if it is, load more stuff + rv.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(@NotNull RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + + if (!recyclerView.canScrollVertically(1) && newState == RecyclerView.SCROLL_STATE_IDLE) { + // We're at the bottom, load more projects! + load_projects(project_count_should_be_loaded); + } + } + }); + }); + }).start(); + } - // TODO: RECYCLERVIEW PAGINATION + private void load_projects(int count) { + Log.d("BrowseActivity", "load_projects: called."); new Thread(() -> { - Task task = projects.get(); + // Check if we're at the bottom + if (is_at_bottom) + // Don't load more projects :doggo_cheems: + return; + + Query fetch_projects_query = projects + .orderBy("name") + .whereEqualTo("open", true) + .limit(count); + + // Check if the pointer has been set (if it hasn't then this is the first time we call this function) + if (after != null) fetch_projects_query.startAt(after); + + Task task = fetch_projects_query.get(); try { + Log.d("BrowseActivity", "Waiting for query"); Tasks.await(task); } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); - Toast.makeText(BrowseActivity.this, "An error occured while fetching data: " + task.getException().getMessage(), Toast.LENGTH_LONG).show(); + runOnUiThread(() -> Toast.makeText(BrowseActivity.this, "An error occured while fetching data: " + task.getException().getMessage(), Toast.LENGTH_LONG).show()); } if (!task.isSuccessful()) { - Toast.makeText(BrowseActivity.this, "An error occured while retrieving data: " + task.getException().getMessage(), Toast.LENGTH_LONG).show(); + runOnUiThread(() -> Toast.makeText(BrowseActivity.this, "An error occured while retrieving data: " + task.getException().getMessage(), Toast.LENGTH_LONG).show()); return; } - ArrayList items = new ArrayList<>(); + int counter = 0; // This counter is used if we've reached to the bottom of the projects list + // and if we did, don't load more projects for (DocumentSnapshot project: task.getResult().getDocuments()) { - // TODO: OPTIMIZE THIS + counter++; + + Log.d("BrowseActivity", "At loop: " + counter); String username; String uid_uploader = project.getString("author"); @@ -95,7 +155,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { Tasks.await(userdata_fetch); if (!userdata_fetch.isSuccessful()) { - Toast.makeText(BrowseActivity.this, "Error while fetching userdata: " + userdata_fetch.getException().getMessage(), Toast.LENGTH_LONG).show(); + runOnUiThread(() -> Toast.makeText(BrowseActivity.this, "Error while fetching userdata: " + userdata_fetch.getException().getMessage(), Toast.LENGTH_LONG).show()); return; } @@ -108,7 +168,7 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { } catch (ExecutionException | InterruptedException e) { e.printStackTrace(); - Toast.makeText(BrowseActivity.this, "Error while fetching userdata: " + e.getMessage(), Toast.LENGTH_LONG).show(); + runOnUiThread(() -> Toast.makeText(BrowseActivity.this, "Error while fetching userdata: " + e.getMessage(), Toast.LENGTH_LONG).show()); return; } @@ -133,14 +193,17 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { latest_commit_timestamp ) ); - } - if (savedInstanceState != null) savedInstanceState.putParcelableArrayList("items", items); + Log.d("BrowseActivity", "load_projects: UpdateView"); + runOnUiThread(() -> adapter.updateView(items)); - runOnUiThread(() -> { - findViewById(R.id.progressBar_browse).setVisibility(View.GONE); - adapter.updateView(items); - }); + // Set the pointer of the last project loaded + after = project; + } + + // Check if we're at the bottom (means we've laoded less projects than we should've been) + if (counter < count) + is_at_bottom = true; }).start(); } } diff --git a/app/src/main/java/com/iyxan23/sketch/collab/online/BrowseCodeActivity.java b/app/src/main/java/com/iyxan23/sketch/collab/online/BrowseCodeActivity.java index 872dbd6..096e0aa 100644 --- a/app/src/main/java/com/iyxan23/sketch/collab/online/BrowseCodeActivity.java +++ b/app/src/main/java/com/iyxan23/sketch/collab/online/BrowseCodeActivity.java @@ -1,13 +1,16 @@ package com.iyxan23.sketch.collab.online; +import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import android.annotation.SuppressLint; +import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.view.View; +import android.widget.ArrayAdapter; import android.widget.TextView; import android.widget.Toast; @@ -21,12 +24,15 @@ import com.google.firebase.firestore.Source; import com.iyxan23.sketch.collab.R; import com.iyxan23.sketch.collab.Util; +import com.iyxan23.sketch.collab.helpers.PatchHelper; +import com.iyxan23.sketch.collab.models.Commit; import com.iyxan23.sketch.collab.models.SketchwareProject; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; import java.util.LinkedList; import java.util.concurrent.ExecutionException; @@ -41,6 +47,14 @@ public class BrowseCodeActivity extends AppCompatActivity { TextView code_library; TextView code_resource; + ArrayList c_commits = new ArrayList<>(); + ArrayList commit_ids = new ArrayList<>(); + + // Project data in their decrypted string format + HashMap project_data = new HashMap<>(); + + int current_commit_index; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -67,9 +81,6 @@ protected void onCreate(Bundle savedInstanceState) { CollectionReference project_snapshot = firestore.collection("projects").document(project_key).collection("snapshot"); CollectionReference project_commits = firestore.collection("projects").document(project_key).collection("commits"); - // Project data in their decrypted string format - HashMap project_data = new HashMap<>(); - String[] keys = new String[] {"mysc_project", "logic", "view", "library", "resource", "file"}; // Get the snapshot, get the commits, and apply the commits to the snapshot @@ -81,8 +92,13 @@ protected void onCreate(Bundle savedInstanceState) { } diff_match_patch dmp = new diff_match_patch(); + + int index = -1; // -1 coz we're adding the index in the start of the loop + // Apply the patch for (DocumentSnapshot commit: commits) { + index++; + HashMap patch = (HashMap) commit.get("patch"); if (patch == null) continue; @@ -96,21 +112,28 @@ protected void onCreate(Bundle savedInstanceState) { project_data.put(key, (String) result[0]); } + + // Also save the commits, we can use this for the commit history jumping + Commit c = new Commit(); + + c.id = commit.getId(); + c.patch = patch; + + c_commits.add(c); + + // Ah yes, save it to the index so we can find a commit faster + commit_ids.add(commit.getId()); } - runOnUiThread(() -> { - code_logic.setText(project_data.get("logic")); - code_view.setText(project_data.get("view")); - code_file.setText(project_data.get("file")); - code_library.setText(project_data.get("library")); - code_resource.setText(project_data.get("resource")); - }); + updateCode(project_data); } catch (InterruptedException | ExecutionException e) { e.printStackTrace(); Toast.makeText(this, "An error occured: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } + + current_commit_index = commit_ids.size() - 1; }).start(); } @@ -155,4 +178,44 @@ public void chevron_resource(View view) { // Rotate the chevron 180 degree(s) view.setRotation((view.getRotation() + 180) % 360); } + + private void updateCode(HashMap project_data) { + runOnUiThread(() -> { + code_logic.setText(project_data.get("logic")); + code_view.setText(project_data.get("view")); + code_file.setText(project_data.get("file")); + code_library.setText(project_data.get("library")); + code_resource.setText(project_data.get("resource")); + }); + } + + public void open_commit_history(View view) { + AlertDialog.Builder builderSingle = new AlertDialog.Builder(this); + builderSingle.setTitle("Select a commit to jump into: "); + + final ArrayAdapter arrayAdapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1); + arrayAdapter.addAll(commit_ids); + + builderSingle.setNegativeButton("Cancel", (dialog, which) -> dialog.dismiss()); + + builderSingle.setAdapter(arrayAdapter, (dialog, which) -> { + Toast.makeText(this, "Please wait", Toast.LENGTH_SHORT).show(); + + new Thread(() -> { + updateCode(PatchHelper.go_to_commit(project_data, c_commits, current_commit_index, which)); + + runOnUiThread(() -> { + TextView commit_id = findViewById(R.id.commit_id_browse_code); + commit_id.setText("At: " + c_commits.get(which).id); + commit_id.setVisibility(View.VISIBLE); + }); + + current_commit_index = which; + + runOnUiThread(dialog::dismiss); + }).start(); + }); + + builderSingle.show(); + } } \ No newline at end of file diff --git a/app/src/main/java/com/iyxan23/sketch/collab/online/CommitsActivity.java b/app/src/main/java/com/iyxan23/sketch/collab/online/CommitsActivity.java index dcb211b..4cb139b 100644 --- a/app/src/main/java/com/iyxan23/sketch/collab/online/CommitsActivity.java +++ b/app/src/main/java/com/iyxan23/sketch/collab/online/CommitsActivity.java @@ -54,6 +54,7 @@ protected void onCreate(Bundle savedInstanceState) { commit_rv.setLayoutManager(new LinearLayoutManager(this)); commit_rv.setAdapter(adapter); + // TODO: PAGINATION commits_reference .orderBy("timestamp", Query.Direction.DESCENDING) .get() @@ -110,7 +111,7 @@ protected void onCreate(Bundle savedInstanceState) { c.author = commit.getString("author"); c.name = commit.getString("name"); c.sha512sum = commit.getString("sha512sum"); - // c.patch = commit.get("patch", Map.class); // Soon + c.patch = (Map) commit.get("patch"); c.timestamp = commit.getTimestamp("timestamp"); Log.d(TAG, "onCreate: Adding commit " + c.id + "to the list "); diff --git a/app/src/main/java/com/iyxan23/sketch/collab/online/EditProjectActivity.java b/app/src/main/java/com/iyxan23/sketch/collab/online/EditProjectActivity.java index e01bdbc..d10fbac 100644 --- a/app/src/main/java/com/iyxan23/sketch/collab/online/EditProjectActivity.java +++ b/app/src/main/java/com/iyxan23/sketch/collab/online/EditProjectActivity.java @@ -1,24 +1,40 @@ package com.iyxan23.sketch.collab.online; +import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; +import android.app.Activity; +import android.content.Intent; import android.os.Bundle; import android.view.View; +import android.widget.TextView; import android.widget.Toast; import com.google.android.material.textfield.TextInputEditText; import com.google.firebase.firestore.DocumentReference; import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.WriteBatch; import com.iyxan23.sketch.collab.R; +import com.iyxan23.sketch.collab.models.Userdata; +import com.iyxan23.sketch.collab.pickers.UserPicker; + +import java.util.ArrayList; +import java.util.Objects; public class EditProjectActivity extends AppCompatActivity { + ArrayList members = new ArrayList<>(); + ArrayList members_before = new ArrayList<>(); + String description_before; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_edit_project); - String description_before = getIntent().getStringExtra("description"); + description_before = getIntent().getStringExtra("description"); + members = getIntent().getParcelableArrayListExtra("members"); + members_before.addAll(members); TextInputEditText description_edit = findViewById(R.id.description_edit_project_text); description_edit.setText(description_before); @@ -28,6 +44,8 @@ public void save(View view) { TextInputEditText description_edit = findViewById(R.id.description_edit_project_text); description_edit.setEnabled(false); + view.setEnabled(false); + findViewById(R.id.progressbar_edit_project).setVisibility(View.VISIBLE); String project_key = getIntent().getStringExtra("project_key"); @@ -35,18 +53,84 @@ public void save(View view) { FirebaseFirestore firestore = FirebaseFirestore.getInstance(); DocumentReference project_reference = firestore.collection("projects").document(project_key); - project_reference - .update("description", description_edit.getText().toString()) - .addOnCompleteListener(task -> { + WriteBatch writeBatch = firestore.batch(); + + // Check if the description has edited or not + if (!description_edit.getText().toString().equals(description_before)) { + // Nop it's changed, update it + writeBatch.update(project_reference, "description", description_edit.getText().toString()); + } + + // Check if the members list is updated or not + if (!members_before.equals(members)) { + // Yep it has been edited, update it + + // Put all of those members into one String arraylist + ArrayList members_ = new ArrayList<>(); + + for (int i = 0; i < members.size(); i++) { + members_.add(members.get(i).getUid()); + } + + writeBatch.update(project_reference, "members", members_); + } + + // Commit the writeBatch! + writeBatch + .commit() + .addOnSuccessListener(task -> { findViewById(R.id.progressbar_edit_project).setVisibility(View.GONE); - if (task.isSuccessful()) { - Toast.makeText(this, "Updated, refresh the page to see the change", Toast.LENGTH_SHORT).show(); - } else { - Toast.makeText(this, "Error while updating: " + task.getException().getMessage(), Toast.LENGTH_LONG).show(); - } + Toast.makeText(this, "Updated, refresh the page to see the change", Toast.LENGTH_SHORT).show(); finish(); + }) + .addOnFailureListener(e -> { + findViewById(R.id.progressbar_edit_project).setVisibility(View.GONE); + + Toast.makeText(this, "Error while updating: " + e.getMessage(), Toast.LENGTH_LONG).show(); }); } + + final int MEMBER_USER_PICK_REQ_CODE = 20; + + // When the EDIT button is clicked + public void edit_member_click(View view) { + // Open UserPickActivity + Intent pick_user_intent = new Intent(this, UserPicker.class); + pick_user_intent.putExtra("initial_data", members); + startActivityForResult(pick_user_intent, MEMBER_USER_PICK_REQ_CODE); + } + + private void update_members_text() { + TextView members_list = findViewById(R.id.members_list); + + if (members.size() == 0) { + members_list.setText("None"); + return; + } + + StringBuilder result = new StringBuilder(); + + int index = 0; + for (Userdata userdata: members) { + result.append(index == 0 ? "" : ", ").append(userdata.getName()); + index++; + } + + members_list.setText(result); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable @org.jetbrains.annotations.Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == MEMBER_USER_PICK_REQ_CODE) { + if (resultCode == Activity.RESULT_OK) { + members.addAll( Objects.requireNonNull(data) .getParcelableArrayListExtra("selected_users")); + + update_members_text(); + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/iyxan23/sketch/collab/online/UploadActivity.java b/app/src/main/java/com/iyxan23/sketch/collab/online/UploadActivity.java index 0efb378..baf1467 100644 --- a/app/src/main/java/com/iyxan23/sketch/collab/online/UploadActivity.java +++ b/app/src/main/java/com/iyxan23/sketch/collab/online/UploadActivity.java @@ -1,7 +1,13 @@ package com.iyxan23.sketch.collab.online; +import android.app.Activity; +import android.app.AlertDialog; import android.app.ProgressDialog; +import android.content.Intent; import android.os.Bundle; +import android.text.Html; +import android.util.Log; +import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; @@ -17,14 +23,18 @@ import com.google.firebase.firestore.CollectionReference; import com.google.firebase.firestore.DocumentReference; import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.WriteBatch; import com.iyxan23.sketch.collab.R; import com.iyxan23.sketch.collab.Util; import com.iyxan23.sketch.collab.models.SketchwareProject; +import com.iyxan23.sketch.collab.models.Userdata; +import com.iyxan23.sketch.collab.pickers.UserPicker; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; +import java.util.ArrayList; import java.util.HashMap; public class UploadActivity extends AppCompatActivity { @@ -57,6 +67,8 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { TextView projectName = findViewById(R.id.project_name_upload); projectName.setText(swProj.metadata.project_name); + members_list = findViewById(R.id.members_upload); + Button uploadButton = findViewById(R.id.upload_upload); SwitchMaterial isPrivate = findViewById(R.id.private_upload); SwitchMaterial isOpenSource = findViewById(R.id.open_source_upload); @@ -67,19 +79,37 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { FirebaseFirestore firestore = FirebaseFirestore.getInstance(); FirebaseAuth auth = FirebaseAuth.getInstance(); - uploadButton.setOnClickListener(v -> { - // DatabaseReference projectReference = database.getReference("/" + (isPrivate.isChecked() ? "userprojects/" + auth.getUid() + "/projects" : "projects")); - CollectionReference projectRef = firestore.collection("/" + (isPrivate.isChecked() ? "userdata/" + auth.getUid() + "/projects" : "projects")); - // String pushKey = projectReference.push().getKey(); - // DatabaseReference commitsReference = database.getReference("/" + (isPrivate.isChecked() ? "userprojects/" + auth.getUid() + "/commits" : "commits")); + isPrivate.setOnCheckedChangeListener((buttonView, isChecked) -> { + // Private project cannot be open source + isOpenSource.setChecked(!isChecked); + isOpenSource.setEnabled(!isChecked); - // Nullcheck - /* - if (pushKey == null) { - Toast.makeText(UploadActivity.this, "An error occured: pushKey is null", Toast.LENGTH_LONG).show(); + if (isChecked) { + findViewById(R.id.add_member_button).setEnabled(false); + members_list.setText("Members are disabled. To add members, make the project to be public."); + + members_list.animate().setDuration(500).alpha(0.25f); + findViewById(R.id.members_title).animate().setDuration(500).alpha(0.25f); + + members.clear(); + } else { + findViewById(R.id.add_member_button).setEnabled(true); + members_list.setText("None, click the Add button to add Member(s)."); + + members_list.animate().setDuration(500).alpha(1f); + findViewById(R.id.members_title).animate().setDuration(500).alpha(1f); + } + }); + + uploadButton.setOnClickListener(v -> { + if (name.getText().toString().trim().equals("")) { + Toast.makeText(this, "Project name shouldn't be empty!", Toast.LENGTH_SHORT).show(); return; } - */ + + CollectionReference projectRef = firestore.collection("/" + (isPrivate.isChecked() ? "userdata/" + auth.getUid() + "/projects" : "projects")); + + WriteBatch upload_batch = firestore.batch(); HashMap data = new HashMap() {{ put("name", name.getText().toString()); @@ -90,6 +120,19 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { put("latest_commit_timestamp", Timestamp.now()); }}; + // Check if the user has added any members yet + if (!members.isEmpty()) { + // Put all of those members into one String arraylist + ArrayList members_ = new ArrayList<>(); + + for (int i = 0; i < members.size(); i++) { + members_.add(members.get(i).getUid()); + } + + // Put it in the project root + data.put("members", members_); + } + try { data.put("sha512sum", swProj.sha512sum()); } catch (JSONException e) { @@ -117,6 +160,80 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { progressDialog.setIndeterminate(true); progressDialog.show(); + DocumentReference projectRefDoc = projectRef.document(); + CollectionReference snapshotRef = projectRefDoc.collection("logic"); + CollectionReference commitRef = projectRefDoc.collection("commits"); + + // Upload the project metadata + upload_batch.set(projectRefDoc, data); + + // Upload project datas ================================================================ + upload_batch.set( + snapshotRef.document("logic"), + new HashMap() {{ + put("data", Blob.fromBytes(swProj.logic)); + put("shasum", Util.sha512(swProj.logic)); + }} + ); + + upload_batch.set( + snapshotRef.document("view"), + new HashMap() {{ + put("data", Blob.fromBytes(swProj.view)); + put("shasum", Util.sha512(swProj.view)); + }} + ); + + upload_batch.set( + snapshotRef.document("file"), + new HashMap() {{ + put("data", Blob.fromBytes(swProj.file)); + put("shasum", Util.sha512(swProj.file)); + }} + ); + + upload_batch.set( + snapshotRef.document("library"), + new HashMap() {{ + put("data", Blob.fromBytes(swProj.library)); + put("shasum", Util.sha512(swProj.library)); + }} + ); + + upload_batch.set( + snapshotRef.document("resource"), + new HashMap() {{ + put("data", Blob.fromBytes(swProj.resource)); + put("shasum", Util.sha512(swProj.resource)); + }} + ); + + upload_batch.set( + snapshotRef.document("mysc_project"), + new HashMap() {{ + put("data", Blob.fromBytes(swProj.mysc_project)); + put("shasum", Util.sha512(swProj.mysc_project)); + }} + ); + // Upload project datas ===============================================================/ + + // Upload the first initial commit data + upload_batch.set(commitRef.document("initial"), commit_data); + + // Commit the upload! + upload_batch + .commit() + .addOnSuccessListener(result -> { + Toast.makeText(UploadActivity.this, "Project Uploaded", Toast.LENGTH_SHORT).show(); + progressDialog.dismiss(); + finish(); + }) + .addOnFailureListener(e -> { + Toast.makeText(UploadActivity.this, "An error occured: " + e.getMessage(), Toast.LENGTH_LONG).show(); + progressDialog.dismiss(); + }); + + /* projectRef .add(data) .addOnCompleteListener(task -> { @@ -256,36 +373,75 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { finish(); } else { // Sad, it failed - Toast.makeText(UploadActivity.this, "An error occured while uploading: " + task.getException().getMessage(), Toast.LENGTH_LONG).show(); + Toast.makeText(UploadActivity.this, "An error occured while uploading: " + task1.getException().getMessage(), Toast.LENGTH_LONG).show(); } }); }); + */ + }); + } - /* - projectReference - .child(pushKey) - .setValue(data) - .addOnSuccessListener(unused -> - // Update the commit informations - commitsReference - .child(pushKey) - .child("initial") - .setValue(commit_data) - .addOnSuccessListener(unused1 -> { - progressDialog.dismiss(); + public void show_help(View view) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setTitle("Help"); + builder.setMessage( + Html.fromHtml( + "Member:" + + "

Members are users that can directly make changes to your project. This can include your Team, Friends, Partner, and etc.

" + + "

NOTE: Non-Member(s) can also make changes to your project. But, You / Your project's member(s) will need to review the changes manually


" + - Toast.makeText(UploadActivity.this, "Project Uploaded", Toast.LENGTH_LONG).show(); - finish(); - }).addOnFailureListener(e -> { - progressDialog.dismiss(); - Toast.makeText(UploadActivity.this, "An error occured: " + e.getMessage(), Toast.LENGTH_LONG).show(); - }) - ).addOnFailureListener(e -> { - progressDialog.dismiss(); - Toast.makeText(UploadActivity.this, "An error occured: " + e.getMessage(), Toast.LENGTH_LONG).show(); - }); + "Open Source:" + + "

Enabled: People can view your project and your project's source code, only selected member(s) can make changes.

" + + "

Disabled: People cannot view or view your project's source code, only selected member(s) can view / make changes to your project.


" + - */ - }); + "Private:" + + "

Enabled: Only YOU can view / edit the project, members are disabled in this private mode.

" + + "

Disabled: People can view / make changes / contribute to your project depending if it's open source or not.


" + + + "

Still need help? Ask it on https://github.com/Iyxan23/sk-collab/issues

" + ) + ); + + builder.create().show(); + } + + final int MEMBER_USER_PICK_REQ_CODE = 20; + ArrayList members = new ArrayList<>(); + + TextView members_list; + + public void add_member_click(View view) { + view.setEnabled(false); + + Intent pick_user_intent = new Intent(this, UserPicker.class); + pick_user_intent.putExtra("initial_data", members); + startActivityForResult(pick_user_intent, MEMBER_USER_PICK_REQ_CODE); + + view.setEnabled(true); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable @org.jetbrains.annotations.Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (requestCode == MEMBER_USER_PICK_REQ_CODE) { + if (resultCode == Activity.RESULT_OK) { + members = data.getParcelableArrayListExtra("selected_users"); + + Log.d("UploadActivity", "onActivityResult: " + members); + + if (members.size() == 0) { + members_list.setText("None, Click the Add button to add Member(s)"); + return; + } + + boolean is_first = false; + for (Userdata userdata: members) { + members_list.setText((!is_first ? "" : members_list.getText() + ", ") + userdata.getName()); + + is_first = true; + } + } + } } } diff --git a/app/src/main/java/com/iyxan23/sketch/collab/online/ViewOnlineProjectActivity.java b/app/src/main/java/com/iyxan23/sketch/collab/online/ViewOnlineProjectActivity.java index 7972991..a19b3a2 100644 --- a/app/src/main/java/com/iyxan23/sketch/collab/online/ViewOnlineProjectActivity.java +++ b/app/src/main/java/com/iyxan23/sketch/collab/online/ViewOnlineProjectActivity.java @@ -1,47 +1,41 @@ package com.iyxan23.sketch.collab.online; -import android.app.Notification; -import android.app.NotificationChannel; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.app.ProgressDialog; -import android.content.DialogInterface; import android.content.Intent; import android.os.Build; import android.os.Bundle; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; -import com.google.android.gms.tasks.Continuation; -import com.google.android.gms.tasks.Task; -import com.google.android.material.appbar.CollapsingToolbarLayout; -import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; -import com.google.android.material.floatingactionbutton.FloatingActionButton; - -import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; -import android.view.View; -import android.widget.TextView; -import android.widget.Toast; - +import com.google.android.gms.tasks.Task; +import com.google.android.gms.tasks.Tasks; +import com.google.android.material.appbar.CollapsingToolbarLayout; +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; +import com.google.android.material.floatingactionbutton.FloatingActionButton; import com.google.firebase.auth.FirebaseAuth; import com.google.firebase.firestore.CollectionReference; import com.google.firebase.firestore.DocumentReference; import com.google.firebase.firestore.DocumentSnapshot; import com.google.firebase.firestore.FirebaseFirestore; import com.google.firebase.firestore.Query; -import com.google.firebase.firestore.QuerySnapshot; import com.iyxan23.sketch.collab.R; import com.iyxan23.sketch.collab.Util; import com.iyxan23.sketch.collab.databinding.ActivityViewOnlineProjectBinding; import com.iyxan23.sketch.collab.models.SketchwareProject; +import com.iyxan23.sketch.collab.models.Userdata; import com.iyxan23.sketch.collab.services.CloneService; -import org.jetbrains.annotations.NotNull; import org.json.JSONException; import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ExecutionException; public class ViewOnlineProjectActivity extends AppCompatActivity { @@ -52,6 +46,10 @@ public class ViewOnlineProjectActivity extends AppCompatActivity { String project_key; String project_name; + HashMap cached_names = new HashMap<>(); + + ArrayList members = new ArrayList<>(); + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -60,7 +58,11 @@ protected void onCreate(Bundle savedInstanceState) { setContentView(binding.getRoot()); Toolbar toolbar = binding.toolbar; + setSupportActionBar(toolbar); + + Objects.requireNonNull(getSupportActionBar()); + getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayShowHomeEnabled(true); @@ -79,6 +81,7 @@ protected void onCreate(Bundle savedInstanceState) { Intent i = new Intent(this, EditProjectActivity.class); i.putExtra("description", description_); i.putExtra("project_key", project_key); + i.putExtra("members", members); startActivity(i); }); @@ -130,10 +133,10 @@ protected void onCreate(Bundle savedInstanceState) { return project_commits.orderBy("timestamp", Query.Direction.ASCENDING).limit(1).get(); }) */ - .addOnCompleteListener(task -> { + .addOnSuccessListener(result -> { DocumentSnapshot project_data = tmp[0]; DocumentSnapshot uploader_userdata = tmp[1]; - DocumentSnapshot latest_commit = task.getResult().getDocuments().get(0); + DocumentSnapshot latest_commit = result.getDocuments().get(0); TextView commit_end = findViewById(R.id.commit_end); TextView commit_end_id = findViewById(R.id.commit_end_id); @@ -150,6 +153,9 @@ protected void onCreate(Bundle savedInstanceState) { String first_commit_id = "initial"; String first_commit_message = "Initial Commit"; + List members_ = (List) project_data.get("members"); + // Aahhh, we need to fetch the member's usernames, k hold on + project_name = name; description_ = description; @@ -162,9 +168,59 @@ protected void onCreate(Bundle savedInstanceState) { commit_start_id.setText(first_commit_id); description_textview.setText(description); + fetchMembers(members_); + // Hide the progressbar findViewById(R.id.progress_project).setVisibility(View.GONE); - }); + }) + .addOnFailureListener(e -> Toast.makeText(this, "Error while fetching: " + e.getMessage(), Toast.LENGTH_LONG).show()); + } + + private void fetchMembers(List members_) { + // Check if members_ is null bruh + if (members_ == null) { + // kekw we're outta here + return; + } + + CollectionReference userdata = FirebaseFirestore.getInstance().collection("userdata"); + + new Thread(() -> { + for (String uid : members_) { + String username; + if (cached_names.containsKey(uid)) { + username = cached_names.get(uid); + + } else { + // Fetch it's username + Task userdata_fetch = userdata.document(uid).get(); + + try { + Tasks.await(userdata_fetch); + + if (!userdata_fetch.isSuccessful()) { + runOnUiThread(() -> Toast.makeText(this, "Error while fetching userdata: " + userdata_fetch.getException().getMessage(), Toast.LENGTH_LONG).show()); + + return; + } + + DocumentSnapshot user = userdata_fetch.getResult(); + + username = user.getString("name"); + + cached_names.put(uid, username); + + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + runOnUiThread(() -> Toast.makeText(this, "Error while fetching userdata: " + e.getMessage(), Toast.LENGTH_LONG).show()); + + return; + } + } + + members.add(new Userdata(username, uid)); + } + }).start(); } // onClick for the "Browse Code" button @@ -194,6 +250,7 @@ public void cloneOnClick(View v) { dialog.dismiss(); do_clone(); }); + AlertDialog exists_dialog = exists_dialog_builder.create(); new Thread(() -> { diff --git a/app/src/main/java/com/iyxan23/sketch/collab/pickers/UserPicker.java b/app/src/main/java/com/iyxan23/sketch/collab/pickers/UserPicker.java new file mode 100644 index 0000000..2f04ca0 --- /dev/null +++ b/app/src/main/java/com/iyxan23/sketch/collab/pickers/UserPicker.java @@ -0,0 +1,250 @@ +package com.iyxan23.sketch.collab.pickers; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.util.Pair; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.android.gms.tasks.Tasks; +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; +import com.google.firebase.firestore.CollectionReference; +import com.google.firebase.firestore.DocumentSnapshot; +import com.google.firebase.firestore.FirebaseFirestore; +import com.google.firebase.firestore.Query; +import com.google.firebase.firestore.QuerySnapshot; +import com.iyxan23.sketch.collab.R; +import com.iyxan23.sketch.collab.models.Userdata; + +import org.jetbrains.annotations.NotNull; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ExecutionException; + +public class UserPicker extends AppCompatActivity { + + List users; + + FirebaseFirestore firestore = FirebaseFirestore.getInstance(); + CollectionReference users_collection = firestore.collection("userdata"); + + ArrayList initial_data = new ArrayList<>(); + + RecyclerView users_rv; + UserAdapter adapter; + + // TODO: PAGINATION + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_user_picker); + + Intent intent = getIntent(); + + initial_data = intent.getParcelableArrayListExtra("initial_data"); + + ExtendedFloatingActionButton efab = findViewById(R.id.button2); + + users_rv = findViewById(R.id.recycler_view_user_picker); + + // Shrink / expand the ExtendedFloatingActionButton according to the scroll activity + // https://stackoverflow.com/questions/56822412/shrink-and-extend-function-on-new-extendedfloatingactionbutton-in-material-compo + users_rv.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { + if (newState == RecyclerView.SCROLL_STATE_IDLE) { + efab.extend(); + } + + super.onScrollStateChanged(recyclerView, newState); + } + + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (dy > 0 || dy < 0 && efab.isExtended()) { + efab.shrink(); + } + } + }); + + // Run these in a new thread + new Thread(() -> { + try { + // Load users + load_users(); + + // And bind the view, (set recyclerview adapter, update some stuff) + runOnUiThread(this::bind_views); + } catch (ExecutionException | InterruptedException e) { + runOnUiThread(() -> Toast.makeText(this, "Error while fetching: " + e.getMessage(), Toast.LENGTH_LONG).show()); + } + }).start(); + } + + private void load_users() throws ExecutionException, InterruptedException { + QuerySnapshot res = Tasks.await(users_collection.orderBy("name", Query.Direction.ASCENDING).get()); + users = res.getDocuments(); + } + + private void bind_views() { + adapter = new UserAdapter((ArrayList) users, this); + + Log.d("UserPicker", "bind_views: initial_data: " + initial_data); + + if (initial_data != null) { + adapter.picked_users.addAll(initial_data); + adapter.notifyDataSetChanged(); + } + + users_rv.setLayoutManager(new LinearLayoutManager(this)); + users_rv.setAdapter(adapter); + } + + public void update_count() { + TextView count_selected = findViewById(R.id.count_selected); + + count_selected.setText(adapter.picked_users.size() + " selected"); + } + + public void done_button_click(View view) { + Intent resultIntent = new Intent(); + + // Remove ANY duplicates + Set userdatas = new HashSet<>(adapter.picked_users); + adapter.picked_users.clear(); + adapter.picked_users.addAll(userdatas); + + resultIntent.putExtra("selected_users", adapter.picked_users); + setResult(Activity.RESULT_OK, resultIntent); + finish(); + } + + public void go_back(View view) { + // The user cancelled the activity + Intent resultIntent = new Intent(); + setResult(Activity.RESULT_CANCELED, resultIntent); + finish(); + } + + // TODO: ADD SEARCH + + // The UserAdapter used for the UsersRecyclerView + public static class UserAdapter extends RecyclerView.Adapter { + private static final String TAG = "UserAdapter"; + + private ArrayList datas = new ArrayList<>(); + + // ArrayList of Pair of Name and UID + public ArrayList picked_users = new ArrayList<>(); + + WeakReference activity; + + public UserAdapter(Activity activity) { + this.activity = new WeakReference<>(activity); + } + + public UserAdapter(ArrayList datas, Activity activity) { + this.datas = datas; + this.activity = new WeakReference<>(activity); + } + + public void updateView(ArrayList datas) { + this.datas = datas; + notifyDataSetChanged(); + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ViewHolder( + LayoutInflater + .from(parent.getContext()) + .inflate(R.layout.rv_user_pick_item, parent, false) + ); + } + + private boolean userdataExists(Userdata ud) { + for (Userdata picked_user : picked_users) { + if (picked_user.equals(ud)) { + return true; + } + } + return false; + } + + @SuppressLint("SetTextI18n") + @Override + public void onBindViewHolder(@NonNull final ViewHolder holder, final int position) { + Log.d(TAG, "onBindViewHolder: called."); + DocumentSnapshot item = datas.get(position); + + String username = item.getString("name"); + String uid = item.getId(); + + Userdata userdata = new Userdata(username, uid); + + Log.d(TAG, "onBindViewHolder: Userdata: " + userdata); + + if (userdataExists(userdata)) { + // This item is already selected + holder.check.setVisibility(View.VISIBLE); + } else { + // Unselected item + holder.check.setVisibility(View.GONE); + } + + holder.username.setText(item.getString("name")); + + holder.body.setOnClickListener(v -> { + if (userdataExists(userdata)) { + picked_users.remove(userdata); + holder.check.setVisibility(View.GONE); + + Log.d(TAG, "onBindViewHolder: Remove " + username); + } else { + picked_users.add(userdata); + holder.check.setVisibility(View.VISIBLE); + + Log.d(TAG, "onBindViewHolder: Add " + username); + } + }); + } + + @Override + public int getItemCount() { + return datas.size(); + } + + public static class ViewHolder extends RecyclerView.ViewHolder { + + TextView username; + + View check; + View body; + + public ViewHolder(@NonNull View itemView) { + super(itemView); + username = itemView.findViewById(R.id.user_pick_item_username); + check = itemView.findViewById(R.id.user_pick_item_check); + body = itemView.findViewById(R.id.user_pick_item_body); + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_back.xml b/app/src/main/res/drawable/ic_back.xml new file mode 100644 index 0000000..fa122e1 --- /dev/null +++ b/app/src/main/res/drawable/ic_back.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_help.xml b/app/src/main/res/drawable/ic_help.xml new file mode 100644 index 0000000..3d4adce --- /dev/null +++ b/app/src/main/res/drawable/ic_help.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_history.xml b/app/src/main/res/drawable/ic_history.xml new file mode 100644 index 0000000..98a8216 --- /dev/null +++ b/app/src/main/res/drawable/ic_history.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_browse_code.xml b/app/src/main/res/layout/activity_browse_code.xml index b1b8b8a..7430617 100644 --- a/app/src/main/res/layout/activity_browse_code.xml +++ b/app/src/main/res/layout/activity_browse_code.xml @@ -4,6 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:animateLayoutChanges="true" tools:context=".online.BrowseCodeActivity" android:background="@color/colorBackground"> @@ -12,6 +13,7 @@ android:id="@+id/constraintLayout8" android:layout_width="0dp" android:layout_height="wrap_content" + android:animateLayoutChanges="true" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent"> @@ -23,7 +25,7 @@ android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:layout_marginBottom="16dp" - app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintBottom_toTopOf="@+id/commit_id_browse_code" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:srcCompat="@drawable/ic_code_braces" /> @@ -43,15 +45,46 @@ + + + + + app:layout_constraintTop_toBottomOf="@+id/members_list" + app:layout_constraintVertical_bias="0.0"> @@ -106,4 +106,41 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/constraintLayout7" /> + + + + + +