diff --git a/README.md b/README.md index 146a9ac..cea0793 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,6 @@ [![GitHub last commit](https://img.shields.io/github/last-commit/Iyxan23/sk-collab.svg?style=flat)]() [![GitHub commit activity](https://img.shields.io/github/commit-activity/w/Iyxan23/sk-collab?style=flat)]() [![GitHub pull-requests](https://img.shields.io/github/issues-pr/Iyxan23/sk-collab.svg)](https://GitHub.com/Iyxan23/sk-collab/pull/) -[![GitHub pull-requests closed](https://img.shields.io/github/issues-pr-closed/Iyxan23/sk-collab.svg)](https://GitHub.com/Iyxan23/sk-collab/pull/) [![PR's Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat)](http://makeapullrequest.com) [![Open Source? Yes!](https://badgen.net/badge/Open%20Source%20%3F/Yes%21/blue?icon=github)](https://github.com/Iyxan23/sk-collab) [![GitHub forks](https://img.shields.io/github/forks/Iyxan23/sk-collab.svg?style=social&label=Fork&maxAge=2592000)](https://GitHub.com/Iyxan23/sk-collab/network/) @@ -37,10 +36,14 @@ SketchCollab break these limits, SketchCollab allows you to edit the project at ## Note I didn't invent version control. SketchCollab is only a version control system for Sketchware projects. +## Building +Steps in building SketchCollab on your own is written in this [wiki](https://github.com/Iyxan23/sk-collab/wiki/Building-Sketch-Collab). + ## Branches - [release](https://github.com/Iyxan23/sk-collab/tree/release): The branch where every release / beta / alpha version will be pushed into - [main](https://github.com/Iyxan23/sk-collab/tree/main): Development branch, checked, and tested - [dev](https://github.com/Iyxan23/sk-collab/tree/dev): Development branch, unstable, unchecked, and untested, bugs are expected in this branch - [new-old-kotlin](https://github.com/Iyxan23/sk-collab/tree/new-old-kotlin): The old Kotlin codebase and unfinished version of SketchCollab - [old-java](https://github.com/Iyxan23/sk-collab/tree/old-java): The old Java and unfinished version of SketchCollab + - [feature/ branches] - Usually used to add big / important features for the app, will soon be merged with dev when it's complete. - [other branches] - Usually used to add breaking features and soon will be merged into dev when it's complete. diff --git a/app/build.gradle b/app/build.gradle index 8bcb739..6e29ac1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -37,7 +37,6 @@ dependencies { implementation 'com.google.firebase:firebase-analytics' implementation 'com.google.firebase:firebase-auth' implementation 'com.google.firebase:firebase-firestore' - implementation 'com.google.firebase:firebase-database' implementation 'org.jetbrains:annotations:16.0.2' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'com.google.android.material:material:1.2.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 75de35b..299c9bd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,7 +7,7 @@ - + + diff --git a/app/src/main/java/com/iyxan23/sketch/collab/CheckActivity.java b/app/src/main/java/com/iyxan23/sketch/collab/CheckActivity.java index c327044..c37c42c 100644 --- a/app/src/main/java/com/iyxan23/sketch/collab/CheckActivity.java +++ b/app/src/main/java/com/iyxan23/sketch/collab/CheckActivity.java @@ -72,8 +72,10 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { // Ight, get the exception and print the stacktrace task.getException().printStackTrace(); - // And also tell it to the user - Toast.makeText(CheckActivity.this, "An error occured: " + task.getException().getMessage(), Toast.LENGTH_LONG).show(); + // And also tell the use that we've failed to connect to the server + loading_text.setText("Failed to reach the server, check if you have an interent connection."); + loading_text.setTextColor(0xFFE51515); + progressbar.setVisibility(View.INVISIBLE); } }); } diff --git a/app/src/main/java/com/iyxan23/sketch/collab/Util.java b/app/src/main/java/com/iyxan23/sketch/collab/Util.java index 0c330a0..9f2828e 100644 --- a/app/src/main/java/com/iyxan23/sketch/collab/Util.java +++ b/app/src/main/java/com/iyxan23/sketch/collab/Util.java @@ -127,23 +127,21 @@ public static String sha512(byte[] input) { // This function should be ran on a different thread public static ArrayList fetch_sketchware_projects() { ArrayList projects = new ArrayList<>(); - ArrayList files = listDir(Environment.getExternalStorageDirectory().getAbsolutePath() + "/.sketchware/data/"); - - for (String project_folder_path: files) { - File project_folder = new File(project_folder_path); + ArrayList files = listDir(Environment.getExternalStorageDirectory().getAbsolutePath() + "/.sketchware/data/"); + for (File project_folder_path: files) { // Just in case - if (project_folder.isFile()) + if (project_folder_path.isFile()) continue; try { - FileInputStream file = new FileInputStream(new File(project_folder.getAbsolutePath() + "/file")); - FileInputStream logic = new FileInputStream(new File(project_folder.getAbsolutePath() + "/logic")); - FileInputStream library = new FileInputStream(new File(project_folder.getAbsolutePath() + "/library")); - FileInputStream view = new FileInputStream(new File(project_folder.getAbsolutePath() + "/view")); - FileInputStream resource = new FileInputStream(new File(project_folder.getAbsolutePath() + "/resource")); + FileInputStream file = new FileInputStream(new File(project_folder_path.getAbsolutePath() + "/file")); + FileInputStream logic = new FileInputStream(new File(project_folder_path.getAbsolutePath() + "/logic")); + FileInputStream library = new FileInputStream(new File(project_folder_path.getAbsolutePath() + "/library")); + FileInputStream view = new FileInputStream(new File(project_folder_path.getAbsolutePath() + "/view")); + FileInputStream resource = new FileInputStream(new File(project_folder_path.getAbsolutePath() + "/resource")); - FileInputStream mysc_project = new FileInputStream(new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/.sketchware/mysc/list/" + project_folder.getName() + "/project")); + FileInputStream mysc_project = new FileInputStream(new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/.sketchware/mysc/list/" + project_folder_path.getName() + "/project")); projects.add(new SketchwareProject(readFile(logic), readFile(view), readFile(resource), readFile(library), readFile(file), readFile(mysc_project))); @@ -176,19 +174,11 @@ public static SketchwareProject get_sketchware_project(int id) { return null; } - public static int getFreeId() { - ArrayList files = listDir(Environment.getExternalStorageDirectory().getAbsolutePath() + "/.sketchware/data/"); + public static int getLatestId() { + ArrayList files = listDir(Environment.getExternalStorageDirectory().getAbsolutePath() + "/.sketchware/data/"); - int anchor = 601; - for (String file : files) { - if (!file.equals(String.valueOf(anchor))) { - // Heres the free id - return anchor; - } - anchor++; - } // Get the last empty ID - return Integer.parseInt(files.get(files.size() - 1)) + 1; + return Integer.parseInt(files.get(files.size() - 1).getName()) + 1; } // Copied from: https://www.journaldev.com/9400/android-external-storage-read-write-save-file @@ -266,6 +256,10 @@ public void run() { } public static void writeFile(File file, byte[] data) throws IOException { + if (!file.exists()) { + file.getParentFile().mkdirs(); + file.createNewFile(); + } FileOutputStream outputStream = new FileOutputStream(file); outputStream.write(data); outputStream.flush(); @@ -316,15 +310,15 @@ public static byte[] joinByteArrays(final byte[] array1, @Nullable byte[] array2 return joinedArray; } - public static ArrayList listDir(String str) { - ArrayList arrayList = new ArrayList<>(); + public static ArrayList listDir(String str) { + ArrayList arrayList = new ArrayList<>(); File file = new File(str); if (file.exists() && !file.isFile()) { File[] listFiles = file.listFiles(); if (listFiles != null && listFiles.length > 0) { arrayList.clear(); - for (File absolutePath : listFiles) { - arrayList.add(absolutePath.getAbsolutePath()); + for (File f : listFiles) { + arrayList.add(f); } } } 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 7b0433b..14a9598 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 @@ -55,6 +55,11 @@ public void onBindViewHolder(@NonNull final ViewHolder holder, final int positio Log.d(TAG, "onBindViewHolder: called."); SketchwareProject project = datas.get(position); + // Fixes #18 + // Check if the project metadata is null, if it is, the project is corrupted and we will + // just continue on with our day - RecyclerView + if (project.metadata == null) return; + holder.title.setText(project.metadata.app_name); holder.subtitle.setText(project.metadata.project_name); holder.details.setText(project.metadata.project_package + "(" + project.metadata.id + ")"); diff --git a/app/src/main/java/com/iyxan23/sketch/collab/models/SketchwareProject.java b/app/src/main/java/com/iyxan23/sketch/collab/models/SketchwareProject.java index 8748598..dc6e32e 100644 --- a/app/src/main/java/com/iyxan23/sketch/collab/models/SketchwareProject.java +++ b/app/src/main/java/com/iyxan23/sketch/collab/models/SketchwareProject.java @@ -48,38 +48,23 @@ public SketchwareProject(byte[] logic, byte[] view, byte[] resource, byte[] libr } public void applyChanges() throws IOException { - String project_folder = Environment.getExternalStorageDirectory().getAbsolutePath() + "/.sketchware/data/" + project_id; - FileOutputStream file = new FileOutputStream(new File(project_folder + "/file")); - FileOutputStream logic = new FileOutputStream(new File(project_folder + "/logic")); - FileOutputStream library = new FileOutputStream(new File(project_folder + "/library")); - FileOutputStream view = new FileOutputStream(new File(project_folder + "/view")); - FileOutputStream resource = new FileOutputStream(new File(project_folder + "/resource")); - - FileOutputStream mysc_project = new FileOutputStream(new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/.sketchware/mysc/list/" + project_id + "/project")); - - file.write(this.file); - file.flush(); - file.close(); - - logic.write(this.logic); - file.flush(); - logic.close(); - - library.write(this.library); - library.flush(); - library.close(); - - view.write(this.view); - view.flush(); - view.close(); - - resource.write(this.resource); - resource.flush(); - resource.close(); - - mysc_project.write(this.mysc_project); - mysc_project.flush(); - mysc_project.close(); + String project_folder = Environment.getExternalStorageDirectory().getAbsolutePath() + "/.sketchware/data/" + project_id + "/"; + + File file = new File(project_folder + "file"); + File logic = new File(project_folder + "logic"); + File library = new File(project_folder + "library"); + File view = new File(project_folder + "view"); + File resource = new File(project_folder + "resource"); + + File mysc_project = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/.sketchware/mysc/list/" + project_id + "/project"); + + Util.writeFile(file , this.file ); + Util.writeFile(logic , this.logic ); + Util.writeFile(library , this.library ); + Util.writeFile(view , this.view ); + Util.writeFile(resource , this.resource ); + + Util.writeFile(mysc_project, this.mysc_project); } @Nullable 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 new file mode 100644 index 0000000..872dbd6 --- /dev/null +++ b/app/src/main/java/com/iyxan23/sketch/collab/online/BrowseCodeActivity.java @@ -0,0 +1,158 @@ +package com.iyxan23.sketch.collab.online; + +import androidx.appcompat.app.AppCompatActivity; + +import android.annotation.SuppressLint; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.android.gms.tasks.Tasks; +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.google.firebase.firestore.Source; +import com.iyxan23.sketch.collab.R; +import com.iyxan23.sketch.collab.Util; +import com.iyxan23.sketch.collab.models.SketchwareProject; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.concurrent.ExecutionException; + +import name.fraser.neil.plaintext.diff_match_patch; + +public class BrowseCodeActivity extends AppCompatActivity { + + TextView code_logic; + TextView code_view; + TextView code_file; + TextView code_library; + TextView code_resource; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_browse_code); + + // Bind these textviews + code_logic = findViewById(R.id.code_logic ); + code_view = findViewById(R.id.code_view ); + code_file = findViewById(R.id.code_file ); + code_library = findViewById(R.id.code_library ); + code_resource = findViewById(R.id.code_resource ); + + // Get the project key and project name + Intent intent = getIntent(); + String project_key = intent.getStringExtra("project_key" ); + String project_name = intent.getStringExtra("project_name" ); + + ((TextView) findViewById(R.id.project_name)).setText("on " + project_name); + + // Fetch the snapshot of the project + new Thread(() -> { + try { + FirebaseFirestore firestore = FirebaseFirestore.getInstance(); + 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 + QuerySnapshot snapshot = Tasks.await(project_snapshot.get()); + QuerySnapshot commits = Tasks.await(project_commits .orderBy("timestamp", Query.Direction.ASCENDING).get()); + + for (DocumentSnapshot doc: snapshot.getDocuments()) { + project_data.put(doc.getId(), Util.decrypt(doc.getBlob("data").toBytes())); + } + + diff_match_patch dmp = new diff_match_patch(); + // Apply the patch + for (DocumentSnapshot commit: commits) { + HashMap patch = (HashMap) commit.get("patch"); + + if (patch == null) continue; + + for (String key: keys) { + if (!patch.containsKey(key)) continue; + + LinkedList patches = (LinkedList) dmp.patch_fromText(patch.get(key)); + // TODO: CHECK PATCH STATUSES + Object[] result = dmp.patch_apply(patches, project_data.get(key)); + + project_data.put(key, (String) result[0]); + } + } + + 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")); + }); + + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + + Toast.makeText(this, "An error occured: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + } + }).start(); + } + + @SuppressLint("WrongConstant") + public void chevron_logic(View view) { + // Kinda hacky, but this code is to toggle the view's visibility between VISIBLE (0x0) and GONE (0x8) + // I don't really like the branching way of doing it, so I'm doing it mathematically + code_logic.setVisibility(code_logic.getVisibility() ^ View.GONE); + + // Rotate the chevron 180 degree(s) + view.setRotation((view.getRotation() + 180) % 360); + } + + @SuppressLint("WrongConstant") + public void chevron_view(View view) { + code_view.setVisibility(code_view.getVisibility() ^ View.GONE); + + // Rotate the chevron 180 degree(s) + view.setRotation((view.getRotation() + 180) % 360); + } + + @SuppressLint("WrongConstant") + public void chevron_file(View view) { + code_file.setVisibility(code_file.getVisibility() ^ View.GONE); + + // Rotate the chevron 180 degree(s) + view.setRotation((view.getRotation() + 180) % 360); + } + + @SuppressLint("WrongConstant") + public void chevron_library(View view) { + code_library.setVisibility(code_library.getVisibility() ^ View.GONE); + + // Rotate the chevron 180 degree(s) + view.setRotation((view.getRotation() + 180) % 360); + } + + @SuppressLint("WrongConstant") + public void chevron_resource(View view) { + code_resource.setVisibility(code_resource.getVisibility() ^ View.GONE); + + // Rotate the chevron 180 degree(s) + view.setRotation((view.getRotation() + 180) % 360); + } +} \ 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 224a793..325fa89 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 @@ -13,11 +13,9 @@ import com.google.android.material.switchmaterial.SwitchMaterial; import com.google.firebase.Timestamp; import com.google.firebase.auth.FirebaseAuth; -import com.google.firebase.database.FirebaseDatabase; import com.google.firebase.firestore.Blob; import com.google.firebase.firestore.CollectionReference; import com.google.firebase.firestore.DocumentReference; -import com.google.firebase.firestore.FieldValue; import com.google.firebase.firestore.FirebaseFirestore; import com.iyxan23.sketch.collab.R; import com.iyxan23.sketch.collab.Util; @@ -28,7 +26,6 @@ import java.io.IOException; import java.util.HashMap; -import java.util.Map; public class UploadActivity extends AppCompatActivity { @@ -67,7 +64,6 @@ protected void onCreate(@Nullable Bundle savedInstanceState) { EditText description = findViewById(R.id.description_upload); EditText name = findViewById(R.id.name_upload); - FirebaseDatabase database = FirebaseDatabase.getInstance(); FirebaseFirestore firestore = FirebaseFirestore.getInstance(); FirebaseAuth auth = FirebaseAuth.getInstance(); 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 fec77b6..7972991 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 @@ -169,7 +169,10 @@ protected void onCreate(Bundle savedInstanceState) { // onClick for the "Browse Code" button public void browseCodeOnClick(View v) { - + Intent i = new Intent(this, BrowseCodeActivity.class); + i.putExtra("project_key", project_key); + i.putExtra("project_name", project_name); + startActivity(i); } // onClick for the "Commits" button diff --git a/app/src/main/java/com/iyxan23/sketch/collab/services/CloneService.java b/app/src/main/java/com/iyxan23/sketch/collab/services/CloneService.java index a6c4e42..4c58785 100644 --- a/app/src/main/java/com/iyxan23/sketch/collab/services/CloneService.java +++ b/app/src/main/java/com/iyxan23/sketch/collab/services/CloneService.java @@ -5,9 +5,12 @@ import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; +import android.content.Context; import android.content.Intent; import android.os.Build; +import android.os.Handler; import android.os.IBinder; +import android.os.Looper; import android.util.Log; import android.widget.Toast; @@ -125,7 +128,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { LinkedList patches = (LinkedList) dmp.patch_fromText(patch.get(key)); // TODO: CHECK PATCH STATUSES - Object[] result = dmp.patch_apply(patches, patch.get(key)); + Object[] result = dmp.patch_apply(patches, project_data.get(key)); project_data.put(key, (String) result[0]); } @@ -155,12 +158,12 @@ public int onStartCommand(Intent intent, int flags, int startId) { */ } - int free_id = Util.getFreeId(); + int free_id = Util.getLatestId(); // Alter the mysc project JSONObject project_json = new JSONObject(project_data.get("mysc_project")); - project_json.put("sc_id", free_id); + project_json.put("sc_id", String.valueOf(free_id)); project_json.put("sk-collab-project-key", project_key); project_json.put("sk-collab-owner", project_metadata.getString("author")); @@ -180,11 +183,21 @@ public int onStartCommand(Intent intent, int flags, int startId) { Util.encrypt(project_data.get("mysc_project").getBytes()) ).applyChanges(); - } catch (InterruptedException | ExecutionException | JSONException | IOException e) { + } catch (InterruptedException e) { // Restore interrupt status. Thread.currentThread().interrupt(); + } catch (ExecutionException | JSONException | IOException e) { + e.printStackTrace(); + + // So it will appear on the debug page + throw new RuntimeException(e); } + // Show a "clone finished" toast + new Handler(Looper.getMainLooper()).post(() -> + Toast.makeText(this, "Clone " + project_name + " finished, check and refresh your sketchware.", Toast.LENGTH_LONG).show() + ); + // Stop the service using the startId, so that we don't stop // the service in the middle of handling another job stopSelf(); diff --git a/app/src/main/res/drawable/ic_check.xml b/app/src/main/res/drawable/ic_check.xml new file mode 100644 index 0000000..2501e9f --- /dev/null +++ b/app/src/main/res/drawable/ic_check.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_chevron_down.xml b/app/src/main/res/drawable/ic_chevron_down.xml new file mode 100644 index 0000000..1aeaa99 --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_down.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_code_braces.xml b/app/src/main/res/drawable/ic_code_braces.xml new file mode 100644 index 0000000..6b5ba25 --- /dev/null +++ b/app/src/main/res/drawable/ic_code_braces.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_browse_code.xml b/app/src/main/res/layout/activity_browse_code.xml new file mode 100644 index 0000000..b1b8b8a --- /dev/null +++ b/app/src/main/res/layout/activity_browse_code.xml @@ -0,0 +1,338 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_check.xml b/app/src/main/res/layout/activity_check.xml index 03cd171..1a115bf 100644 --- a/app/src/main/res/layout/activity_check.xml +++ b/app/src/main/res/layout/activity_check.xml @@ -36,11 +36,14 @@