diff --git a/.drone.yml b/.drone.yml index 7bbce823..c3b82520 100644 --- a/.drone.yml +++ b/.drone.yml @@ -66,7 +66,7 @@ steps: - name: build image: nextcloudci/android:android-49 commands: - - ./gradlew build + - ./gradlew assembleFreeRelease - name: sign image: nextcloudci/android:android-49 @@ -78,7 +78,7 @@ steps: KEY_PASS: from_secret: KEY_PASS OUTPUT: signed.apk - GITEA: https://gitea.com + INSTANCE: https://codeberg.org KS_FILE: ci_keystore.jks KS_REPO: from_secret: KS_REPO diff --git a/.gitignore b/.gitignore index bdebb1e1..93cf4a2d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ # Release dir app/release/* +# Pro dir +app/pro/* + # Files for the ART/Dalvik VM *.dex diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 87f8685d..53608c4d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -20,7 +20,7 @@ build: - master - tags script: - - ./gradlew build + - ./gradlew assembleFreeRelease artifacts: paths: - app/build/outputs/ @@ -34,7 +34,7 @@ sign: - tags variables: OUTPUT: "signed.apk" - GITEA: "https://gitea.com" + INSTANCE: "https://codeberg.org" KS_FILE: "ci_keystore.jks" script: - ./scripts/sign-build.sh @@ -48,6 +48,7 @@ latest: stage: publish only: - master + - tags variables: WEBDAV_USERNAME: "GitNexBot" PLUGIN_FILE: "signed.apk" diff --git a/README.md b/README.md index 535fb875..f9f3f54f 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,13 @@ # GitNex - Android client for Gitea -GitNex is a free, open-source Android client for Git repository management tool Gitea. Gitea is a community managed fork of Gogs, lightweight code hosting solution written in Go. +GitNex is a free/paid, open-source Android client for Git repository management tool Gitea. Gitea is a community managed fork of Gogs, lightweight code hosting solution written in Go. GitNex is licensed under GPLv3 License. See the LICENSE file for the full license text. No trackers are used and source code is available here for anyone to audit. ## Downloads [Get it on F-droid](https://f-droid.org/en/packages/org.mian.gitnex/) -[Get it on Google Play](https://play.google.com/store/apps/details?id=org.mian.gitnex) +[Get it on Google Play](https://play.google.com/store/apps/details?id=org.mian.gitnex.pro) [Download builds and releases](https://cloud.swatian.com/s/DN7E5xxtaw4fRbE) ## Note about Gitea version @@ -21,11 +21,10 @@ Check the versions [compatibility page](https://codeberg.org/gitnex/GitNex/wiki/ ## Build from source Option 1 - Download the source code, open it in Android Studio and build it there. -Option 2 - Open terminal(Linux) and cd to the project dir. Run `./gradlew build`. +Option 2 - Open terminal(Linux) and cd to the project dir. Run `./gradlew assembleFree`. ## Features - Multiple accounts support -- Repositories / issues/ org list - File and directory browser - File viewer - Create files @@ -34,6 +33,7 @@ Option 2 - Open terminal(Linux) and cd to the project dir. Run `./gradlew build` - Files diff for PRs - Notifications - Drafts +- Repositories / issues / org list - [MANY MORE](https://codeberg.org/gitnex/GitNex/wiki/Features) ## Contributing diff --git a/app/build.gradle b/app/build.gradle index 24fca5fa..76a1495e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,15 +1,28 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { applicationId "org.mian.gitnex" minSdkVersion 21 - targetSdkVersion 29 - versionCode 317 - versionName "3.2.0-dev" + targetSdkVersion 30 + versionCode 320 + versionName "3.2.0" + multiDexEnabled true testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } + dexOptions { + javaMaxHeapSize "4g" + } + flavorDimensions "default" + productFlavors { + free { + applicationId "org.mian.gitnex" + } + pro { + applicationId "org.mian.gitnex.pro" + } + } buildFeatures { viewBinding = true } @@ -40,7 +53,7 @@ configurations { dependencies { def lifecycle_version = '2.3.0-alpha07' - def markwon_version = '4.5.1' + def markwon_version = '4.6.0' def work_version = "2.4.0" def acra = "5.5.0" @@ -54,14 +67,14 @@ dependencies { androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' implementation "com.github.vihtarb:tooltip:0.2.0" - implementation 'com.squareup.okhttp3:okhttp:4.8.1' + implementation 'com.squareup.okhttp3:okhttp:4.9.0' implementation "com.google.code.gson:gson:2.8.6" implementation "com.squareup.picasso:picasso:2.71828" implementation "com.amulyakhare:com.amulyakhare.textdrawable:1.0.1" implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' implementation 'com.squareup.retrofit2:converter-scalars:2.9.0' - implementation 'com.squareup.okhttp3:logging-interceptor:4.8.1' + implementation 'com.squareup.okhttp3:logging-interceptor:4.9.0' implementation 'org.ocpsoft.prettytime:prettytime:4.0.5.Final' implementation "com.vdurmont:emoji-java:5.1.1" implementation "com.pes.materialcolorpicker:library:1.2.5" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index da640a82..d7a57f36 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -85,6 +85,7 @@ + diff --git a/app/src/main/app_logo-playstore.png b/app/src/main/app_logo-playstore.png new file mode 100644 index 00000000..f78fac3d Binary files /dev/null and b/app/src/main/app_logo-playstore.png differ diff --git a/app/src/main/app_logo-web.png b/app/src/main/app_logo-web.png deleted file mode 100644 index 68a598c8..00000000 Binary files a/app/src/main/app_logo-web.png and /dev/null differ diff --git a/app/src/main/java/org/mian/gitnex/activities/AddNewAccountActivity.java b/app/src/main/java/org/mian/gitnex/activities/AddNewAccountActivity.java index 6f757200..cc89cb43 100644 --- a/app/src/main/java/org/mian/gitnex/activities/AddNewAccountActivity.java +++ b/app/src/main/java/org/mian/gitnex/activities/AddNewAccountActivity.java @@ -114,10 +114,7 @@ public class AddNewAccountActivity extends BaseActivity { URI rawInstanceUrl = UrlBuilder.fromString(UrlHelper.fixScheme(instanceUrlET, "http")).toUri(); - URI instanceUrlWithProtocol = UrlBuilder.fromUri(rawInstanceUrl).withPath(PathsHelper.join(rawInstanceUrl.getPath())) - .withScheme(protocol.toLowerCase()).toUri(); - - URI instanceUrl = UrlBuilder.fromUri(instanceUrlWithProtocol).withPath(PathsHelper.join(instanceUrlWithProtocol.getPath(), "/api/v1/")) + URI instanceUrl = UrlBuilder.fromUri(rawInstanceUrl).withScheme(protocol.toLowerCase()).withPath(PathsHelper.join(rawInstanceUrl.getPath(), "/api/v1/")) .toUri(); versionCheck(instanceUrl.toString(), loginToken); diff --git a/app/src/main/java/org/mian/gitnex/activities/BaseActivity.java b/app/src/main/java/org/mian/gitnex/activities/BaseActivity.java index 4c2a0939..e6f4c608 100644 --- a/app/src/main/java/org/mian/gitnex/activities/BaseActivity.java +++ b/app/src/main/java/org/mian/gitnex/activities/BaseActivity.java @@ -136,11 +136,6 @@ public abstract class BaseActivity extends AppCompatActivity { tinyDb.putString("draftsCommentsDeletionEnabledInit", "yes"); } - if(!tinyDb.getString("instanceUrlWithProtocol").endsWith("/")) { - - tinyDb.putString("instanceUrlWithProtocol", tinyDb.getString("instanceUrlWithProtocol") + "/"); - } - if (tinyDb.getBoolean("crashReportingEnabled")) { CoreConfigurationBuilder ACRABuilder = new CoreConfigurationBuilder(this); diff --git a/app/src/main/java/org/mian/gitnex/activities/CreatePullRequestActivity.java b/app/src/main/java/org/mian/gitnex/activities/CreatePullRequestActivity.java new file mode 100644 index 00000000..e066deb5 --- /dev/null +++ b/app/src/main/java/org/mian/gitnex/activities/CreatePullRequestActivity.java @@ -0,0 +1,431 @@ +package org.mian.gitnex.activities; + +import android.app.DatePickerDialog; +import android.app.Dialog; +import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import androidx.annotation.NonNull; +import org.mian.gitnex.R; +import org.mian.gitnex.adapters.LabelsListAdapter; +import org.mian.gitnex.clients.RetrofitClient; +import org.mian.gitnex.databinding.ActivityCreatePrBinding; +import org.mian.gitnex.databinding.CustomLabelsSelectionDialogBinding; +import org.mian.gitnex.helpers.AppUtil; +import org.mian.gitnex.helpers.Authorization; +import org.mian.gitnex.helpers.StaticGlobalVariables; +import org.mian.gitnex.helpers.TinyDB; +import org.mian.gitnex.helpers.Toasty; +import org.mian.gitnex.helpers.Version; +import org.mian.gitnex.models.Branches; +import org.mian.gitnex.models.CreatePullRequest; +import org.mian.gitnex.models.Labels; +import org.mian.gitnex.models.Milestones; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.List; +import okhttp3.ResponseBody; +import retrofit2.Call; +import retrofit2.Callback; + +/** + * Author M M Arif + */ + +public class CreatePullRequestActivity extends BaseActivity implements LabelsListAdapter.LabelsListAdapterListener { + + private View.OnClickListener onClickListener; + private Context ctx = this; + private Context appCtx; + private TinyDB tinyDb; + private ActivityCreatePrBinding viewBinding; + private CustomLabelsSelectionDialogBinding labelsBinding; + private int resultLimit = StaticGlobalVariables.resultLimitOldGiteaInstances; + private Dialog dialogLabels; + private String labelsSetter; + private ArrayList labelsIds = new ArrayList<>(); + private ArrayList assignees = new ArrayList<>(); + private int milestoneId; + + private String instanceUrl; + private String loginUid; + private String instanceToken; + private String repoOwner; + private String repoName; + + private LabelsListAdapter labelsAdapter; + + List milestonesList = new ArrayList<>(); + List branchesList = new ArrayList<>(); + List labelsList = new ArrayList<>(); + + public CreatePullRequestActivity() { + } + + @Override + protected int getLayoutResourceId(){ + return R.layout.activity_create_pr; + } + + @Override + public void onCreate(Bundle savedInstanceState) { + + super.onCreate(savedInstanceState); + appCtx = getApplicationContext(); + tinyDb = new TinyDB(appCtx); + + viewBinding = ActivityCreatePrBinding.inflate(getLayoutInflater()); + View view = viewBinding.getRoot(); + setContentView(view); + + instanceUrl = tinyDb.getString("instanceUrl"); + loginUid = tinyDb.getString("loginUid"); + String repoFullName = tinyDb.getString("repoFullName"); + String[] parts = repoFullName.split("/"); + repoOwner = parts[0]; + repoName = parts[1]; + instanceToken = "token " + tinyDb.getString(loginUid + "-token"); + + // require gitea 1.12 or higher + if(new Version(tinyDb.getString("giteaVersion")).higherOrEqual("1.12.0")) { + resultLimit = StaticGlobalVariables.resultLimitNewGiteaInstances; + } + + labelsAdapter = new LabelsListAdapter(labelsList, CreatePullRequestActivity.this); + + ImageView closeActivity = findViewById(R.id.close); + + initCloseListener(); + closeActivity.setOnClickListener(onClickListener); + + viewBinding.prDueDate.setOnClickListener(dueDate -> + setDueDate() + ); + + disableProcessButton(); + + getMilestones(instanceUrl, instanceToken, repoOwner, repoName, loginUid, resultLimit); + getBranches(instanceUrl, instanceToken, repoOwner, repoName, loginUid); + + viewBinding.prLabels.setOnClickListener(prLabels -> + showLabels() + ); + + viewBinding.createPr.setOnClickListener(createPr -> + processPullRequest() + ); + } + + private void processPullRequest() { + + String prTitle = String.valueOf(viewBinding.prTitle.getText()); + String prDescription = String.valueOf(viewBinding.prBody.getText()); + String mergeInto = viewBinding.mergeIntoBranchSpinner.getText().toString(); + String pullFrom = viewBinding.pullFromBranchSpinner.getText().toString(); + String dueDate = String.valueOf(viewBinding.prDueDate.getText()); + + assignees.add(""); + + if (labelsIds.size() == 0) { + labelsIds.add(0); + } + + if (dueDate.matches("")) { + dueDate = null; + } + else { + dueDate = AppUtil.customDateCombine(AppUtil.customDateFormat(dueDate)); + } + + if(prTitle.matches("")) { + + Toasty.error(ctx, getString(R.string.titleError)); + } + else if(mergeInto.matches("")) { + + Toasty.error(ctx, getString(R.string.mergeIntoError)); + } + else if(pullFrom.matches("")) { + + Toasty.error(ctx, getString(R.string.pullFromError)); + } + else if(pullFrom.equals(mergeInto)) { + + Toasty.error(ctx, getString(R.string.sameBranchesError)); + } + else { + createPullRequest(prTitle, prDescription, mergeInto, pullFrom, milestoneId, dueDate, assignees); + } + //Log.e("processPullRequest", String.valueOf(milestoneId)); + } + + private void createPullRequest(String prTitle, String prDescription, String mergeInto, String pullFrom, int milestoneId, String dueDate, ArrayList assignees) { + + CreatePullRequest createPullRequest = new CreatePullRequest(prTitle, prDescription, loginUid, mergeInto, pullFrom, milestoneId, dueDate, assignees, labelsIds); + + Call transferCall = RetrofitClient + .getInstance(instanceUrl, ctx) + .getApiInterface() + .createPullRequest(instanceToken, repoOwner, repoName, createPullRequest); + + transferCall.enqueue(new Callback() { + + @Override + public void onResponse(@NonNull Call call, @NonNull retrofit2.Response response) { + + disableProcessButton(); + + if (response.code() == 201) { + + Toasty.success(ctx, getString(R.string.prCreateSuccess)); + finish(); + } + else if (response.code() == 409 && response.message().equals("Conflict")) { + + enableProcessButton(); + Toasty.error(ctx, getString(R.string.prAlreadyExists)); + } + else if (response.code() == 404) { + + enableProcessButton(); + Toasty.error(ctx, getString(R.string.apiNotFound)); + } + else { + + enableProcessButton(); + Toasty.error(ctx, getString(R.string.genericError)); + } + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + enableProcessButton(); + Toasty.error(ctx, getString(R.string.genericServerResponseError)); + } + }); + } + + @Override + public void labelsStringData(ArrayList data) { + + labelsSetter = String.valueOf(data); + viewBinding.prLabels.setText(labelsSetter.replace("]", "").replace("[", "")); + } + + @Override + public void labelsIdsData(ArrayList data) { + + labelsIds = data; + } + + private void showLabels() { + + dialogLabels = new Dialog(ctx, R.style.ThemeOverlay_MaterialComponents_Dialog_Alert); + + if (dialogLabels.getWindow() != null) { + dialogLabels.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); + } + + labelsBinding = CustomLabelsSelectionDialogBinding.inflate(LayoutInflater.from(ctx)); + + View view = labelsBinding.getRoot(); + dialogLabels.setContentView(view); + + labelsBinding.cancel.setOnClickListener(editProperties -> + dialogLabels.dismiss() + ); + + Call> call = RetrofitClient + .getInstance(instanceUrl, ctx) + .getApiInterface() + .getlabels(instanceToken, repoOwner, repoName); + + call.enqueue(new Callback>() { + + @Override + public void onResponse(@NonNull Call> call, @NonNull retrofit2.Response> response) { + + labelsList.clear(); + List labelsList_ = response.body(); + + labelsBinding.progressBar.setVisibility(View.GONE); + labelsBinding.dialogFrame.setVisibility(View.VISIBLE); + + if (response.code() == 200) { + + assert labelsList_ != null; + if(labelsList_.size() > 0) { + for (int i = 0; i < labelsList_.size(); i++) { + + labelsList.add(new Labels(labelsList_.get(i).getId(), labelsList_.get(i).getName())); + + } + } + else { + + dialogLabels.dismiss(); + Toasty.warning(ctx, getString(R.string.noLabelsFound)); + } + + labelsBinding.labelsRecyclerView.setAdapter(labelsAdapter); + + } + else { + + Toasty.error(ctx, getString(R.string.genericError)); + } + + } + + @Override + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + + Toasty.error(ctx, getString(R.string.genericServerResponseError)); + } + }); + + dialogLabels.show(); + + } + + private void getBranches(String instanceUrl, String instanceToken, String repoOwner, String repoName, String loginUid) { + + Call> call = RetrofitClient + .getInstance(instanceUrl, ctx) + .getApiInterface() + .getBranches(Authorization.returnAuthentication(ctx, loginUid, instanceToken), repoOwner, repoName); + + call.enqueue(new Callback>() { + + @Override + public void onResponse(@NonNull Call> call, @NonNull retrofit2.Response> response) { + + if(response.isSuccessful()) { + if(response.code() == 200) { + + List branchesList_ = response.body(); + + assert branchesList_ != null; + if(branchesList_.size() > 0) { + for (int i = 0; i < branchesList_.size(); i++) { + + Branches data = new Branches( + branchesList_.get(i).getName() + ); + branchesList.add(data); + + } + } + + ArrayAdapter adapter = new ArrayAdapter<>(CreatePullRequestActivity.this, + R.layout.list_spinner_items, branchesList); + + viewBinding.mergeIntoBranchSpinner.setAdapter(adapter); + viewBinding.pullFromBranchSpinner.setAdapter(adapter); + enableProcessButton(); + + } + } + + } + + @Override + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + + Toasty.error(ctx, getString(R.string.genericServerResponseError)); + } + }); + + } + + private void getMilestones(String instanceUrl, String instanceToken, String repoOwner, String repoName, String loginUid, int resultLimit) { + + String msState = "open"; + Call> call = RetrofitClient + .getInstance(instanceUrl, ctx) + .getApiInterface() + .getMilestones(Authorization.returnAuthentication(ctx, loginUid, instanceToken), repoOwner, repoName, 1, resultLimit, msState); + + call.enqueue(new Callback>() { + + @Override + public void onResponse(@NonNull Call> call, @NonNull retrofit2.Response> response) { + + if(response.code() == 200) { + + List milestonesList_ = response.body(); + + milestonesList.add(new Milestones(0,getString(R.string.issueCreatedNoMilestone))); + assert milestonesList_ != null; + if(milestonesList_.size() > 0) { + for (int i = 0; i < milestonesList_.size(); i++) { + + //Don't translate "open" is a enum + if(milestonesList_.get(i).getState().equals("open")) { + Milestones data = new Milestones( + milestonesList_.get(i).getId(), + milestonesList_.get(i).getTitle() + ); + milestonesList.add(data); + } + + } + } + + ArrayAdapter adapter = new ArrayAdapter<>(CreatePullRequestActivity.this, + R.layout.list_spinner_items, milestonesList); + + viewBinding.milestonesSpinner.setAdapter(adapter); + enableProcessButton(); + + viewBinding.milestonesSpinner.setOnItemClickListener ((parent, view, position, id) -> + + milestoneId = milestonesList.get(position).getId() + ); + + } + } + + @Override + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + + Toasty.error(ctx, getString(R.string.genericServerResponseError)); + } + }); + + } + + private void setDueDate() { + + final Calendar c = Calendar.getInstance(); + int mYear = c.get(Calendar.YEAR); + final int mMonth = c.get(Calendar.MONTH); + final int mDay = c.get(Calendar.DAY_OF_MONTH); + + DatePickerDialog datePickerDialog = new DatePickerDialog(this, + (view, year, monthOfYear, dayOfMonth) -> viewBinding.prDueDate.setText(getString(R.string.setDueDate, year, (monthOfYear + 1), dayOfMonth)), mYear, mMonth, mDay); + datePickerDialog.show(); + } + + private void initCloseListener() { + + onClickListener = view -> finish(); + } + + private void disableProcessButton() { + + viewBinding.createPr.setEnabled(false); + } + + private void enableProcessButton() { + + viewBinding.createPr.setEnabled(true); + } +} diff --git a/app/src/main/java/org/mian/gitnex/activities/LoginActivity.java b/app/src/main/java/org/mian/gitnex/activities/LoginActivity.java index f55d67e8..cac8891c 100644 --- a/app/src/main/java/org/mian/gitnex/activities/LoginActivity.java +++ b/app/src/main/java/org/mian/gitnex/activities/LoginActivity.java @@ -167,16 +167,12 @@ public class LoginActivity extends BaseActivity { URI rawInstanceUrl = UrlBuilder.fromString(UrlHelper.fixScheme(instanceUrlET.getText().toString(), "http")).toUri(); - URI instanceUrlWithProtocol = UrlBuilder.fromUri(rawInstanceUrl).withPath(PathsHelper.join(rawInstanceUrl.getPath())) - .withScheme(protocol.name().toLowerCase()).toUri(); - - URI instanceUrl = UrlBuilder.fromUri(instanceUrlWithProtocol).withPath(PathsHelper.join(instanceUrlWithProtocol.getPath(), "/api/v1/")) + URI instanceUrl = UrlBuilder.fromUri(rawInstanceUrl).withScheme(protocol.name().toLowerCase()).withPath(PathsHelper.join(rawInstanceUrl.getPath(), "/api/v1/")) .toUri(); tinyDB.putString("loginType", loginType.name().toLowerCase()); tinyDB.putString("instanceUrlRaw", instanceUrlET.getText().toString()); tinyDB.putString("instanceUrl", instanceUrl.toString()); - tinyDB.putString("instanceUrlWithProtocol", instanceUrlWithProtocol.toString()); if(instanceUrlET.getText().toString().equals("")) { diff --git a/app/src/main/java/org/mian/gitnex/activities/MainActivity.java b/app/src/main/java/org/mian/gitnex/activities/MainActivity.java index 5187649c..61922179 100644 --- a/app/src/main/java/org/mian/gitnex/activities/MainActivity.java +++ b/app/src/main/java/org/mian/gitnex/activities/MainActivity.java @@ -1,15 +1,14 @@ package org.mian.gitnex.activities; import android.app.Activity; -import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.graphics.Typeface; -import android.net.Uri; import android.os.Bundle; import android.util.Log; +import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; @@ -32,11 +31,10 @@ import org.mian.gitnex.clients.PicassoService; import org.mian.gitnex.clients.RetrofitClient; import org.mian.gitnex.database.api.UserAccountsApi; import org.mian.gitnex.database.models.UserAccount; -import org.mian.gitnex.fragments.AboutFragment; import org.mian.gitnex.fragments.AdministrationFragment; import org.mian.gitnex.fragments.BottomSheetDraftsFragment; import org.mian.gitnex.fragments.DraftsFragment; -import org.mian.gitnex.fragments.ExploreRepositoriesFragment; +import org.mian.gitnex.fragments.ExploreFragment; import org.mian.gitnex.fragments.MyRepositoriesFragment; import org.mian.gitnex.fragments.NotificationsFragment; import org.mian.gitnex.fragments.OrganizationsFragment; @@ -55,6 +53,7 @@ import org.mian.gitnex.helpers.TinyDB; import org.mian.gitnex.helpers.Toasty; import org.mian.gitnex.helpers.Version; import org.mian.gitnex.models.GiteaVersion; +import org.mian.gitnex.models.NotificationCount; import org.mian.gitnex.models.UserInfo; import java.util.ArrayList; import java.util.List; @@ -82,6 +81,14 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig private Context appCtx; private Typeface myTypeface; + private String instanceUrl; + private String loginUid; + private String instanceToken; + + private View hView; + private MenuItem navNotifications; + private TextView notificationCounter; + @Override protected int getLayoutResourceId() { @@ -100,9 +107,9 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig Intent mainIntent = getIntent(); String launchFragment = mainIntent.getStringExtra("launchFragment"); - final String instanceUrl = tinyDb.getString("instanceUrl"); - final String loginUid = tinyDb.getString("loginUid"); - final String instanceToken = "token " + tinyDb.getString(loginUid + "-token"); + instanceUrl = tinyDb.getString("instanceUrl"); + loginUid = tinyDb.getString("loginUid"); + instanceToken = "token " + tinyDb.getString(loginUid + "-token"); if(tinyDb.getString("dateFormat").isEmpty()) { tinyDb.putString("dateFormat", "pretty"); @@ -169,7 +176,7 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig else if(fragmentById instanceof OrganizationsFragment) { toolbarTitle.setText(getResources().getString(R.string.pageTitleOrganizations)); } - else if(fragmentById instanceof ExploreRepositoriesFragment) { + else if(fragmentById instanceof ExploreFragment) { toolbarTitle.setText(getResources().getString(R.string.pageTitleExplore)); } else if(fragmentById instanceof NotificationsFragment) { @@ -178,9 +185,6 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig else if(fragmentById instanceof ProfileFragment) { toolbarTitle.setText(getResources().getString(R.string.pageTitleProfile)); } - else if(fragmentById instanceof AboutFragment) { - toolbarTitle.setText(getResources().getString(R.string.pageTitleAbout)); - } else if(fragmentById instanceof DraftsFragment) { toolbarTitle.setText(getResources().getString(R.string.titleDrafts)); } @@ -191,10 +195,15 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig toolbarTitle.setText(getResources().getString(R.string.pageTitleUserAccounts)); } + getNotificationsCount(instanceUrl, instanceToken); + drawer = findViewById(R.id.drawer_layout); NavigationView navigationView = findViewById(R.id.nav_view); navigationView.setNavigationItemSelectedListener(this); - final View hView = navigationView.getHeaderView(0); + hView = navigationView.getHeaderView(0); + + Menu menu = navigationView.getMenu(); + navNotifications = menu.findItem(R.id.nav_notifications); ActionBarDrawerToggle toggle = new ActionBarDrawerToggle(this, drawer, toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close); @@ -204,6 +213,7 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig @Override public void onDrawerOpened(@NonNull View drawerView) { + getNotificationsCount(instanceUrl, instanceToken); } @Override @@ -295,6 +305,7 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig toolbarTitle.setText(getResources().getString(R.string.pageTitleProfile)); getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, new ProfileFragment()).commit(); + navigationView.setCheckedItem(R.id.nav_profile); drawer.closeDrawers(); }); @@ -374,7 +385,7 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig case 5: toolbarTitle.setText(getResources().getString(R.string.pageTitleExplore)); - getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, new ExploreRepositoriesFragment()).commit(); + getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, new ExploreFragment()).commit(); navigationView.setCheckedItem(R.id.nav_explore); break; @@ -439,6 +450,11 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig } } + public void setActionBarTitle(String title) { + + toolbarTitle.setText(title); + } + @Override public void onButtonClicked(String text) { @@ -528,15 +544,6 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); break; - case R.id.nav_about: - toolbarTitle.setText(getResources().getString(R.string.pageTitleAbout)); - getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, new AboutFragment()).commit(); - break; - - case R.id.nav_rate_app: - rateThisApp(); - break; - case R.id.nav_starred_repos: toolbarTitle.setText(getResources().getString(R.string.pageTitleStarredRepos)); getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, new StarredRepositoriesFragment()).commit(); @@ -544,7 +551,7 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig case R.id.nav_explore: toolbarTitle.setText(getResources().getString(R.string.pageTitleExplore)); - getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, new ExploreRepositoriesFragment()).commit(); + getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, new ExploreFragment()).commit(); break; case R.id.nav_notifications: @@ -568,16 +575,6 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig return true; } - public void rateThisApp() { - - try { - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + getPackageName()))); - } - catch(ActivityNotFoundException e) { - startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + getPackageName()))); - } - } - public static void logout(Activity activity, Context ctx) { TinyDB tinyDb = new TinyDB(ctx.getApplicationContext()); @@ -705,4 +702,31 @@ public class MainActivity extends BaseActivity implements NavigationView.OnNavig } + private void getNotificationsCount(String instanceUrl, String token) { + + Call call = RetrofitClient.getInstance(instanceUrl, ctx).getApiInterface().checkUnreadNotifications(token); + + call.enqueue(new Callback() { + + @Override + public void onResponse(@NonNull Call call, @NonNull retrofit2.Response response) { + + NotificationCount notificationCount = response.body(); + + if(response.code() == 200) { + + assert notificationCount != null; + notificationCounter = navNotifications.getActionView().findViewById(R.id.counterBadgeNotification); + notificationCounter.setText(String.valueOf(notificationCount.getCounter())); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + Log.e("onFailure-notification", t.toString()); + } + }); + } + } diff --git a/app/src/main/java/org/mian/gitnex/activities/RepoDetailActivity.java b/app/src/main/java/org/mian/gitnex/activities/RepoDetailActivity.java index e3743446..80b00689 100644 --- a/app/src/main/java/org/mian/gitnex/activities/RepoDetailActivity.java +++ b/app/src/main/java/org/mian/gitnex/activities/RepoDetailActivity.java @@ -406,6 +406,10 @@ public class RepoDetailActivity extends BaseActivity implements BottomSheetRepoF startActivity(new Intent(RepoDetailActivity.this, RepositorySettingsActivity.class)); break; + case "newPullRequest": + startActivity(new Intent(RepoDetailActivity.this, CreatePullRequestActivity.class)); + break; + } } diff --git a/app/src/main/java/org/mian/gitnex/activities/RepositorySettingsActivity.java b/app/src/main/java/org/mian/gitnex/activities/RepositorySettingsActivity.java index d6dff0b5..e4c0ff96 100644 --- a/app/src/main/java/org/mian/gitnex/activities/RepositorySettingsActivity.java +++ b/app/src/main/java/org/mian/gitnex/activities/RepositorySettingsActivity.java @@ -394,6 +394,9 @@ public class RepositorySettingsActivity extends BaseActivity { if (response.code() == 200) { + tinyDb.putBoolean("hasIssues", repoEnableIssues); + tinyDb.putBoolean("hasPullRequests", repoEnablePr); + dialogProp.dismiss(); Toasty.success(ctx, getString(R.string.repoPropertiesSaveSuccess)); diff --git a/app/src/main/java/org/mian/gitnex/adapters/ExploreRepositoriesAdapter.java b/app/src/main/java/org/mian/gitnex/adapters/ExploreRepositoriesAdapter.java index 5cc2e917..92be07df 100644 --- a/app/src/main/java/org/mian/gitnex/adapters/ExploreRepositoriesAdapter.java +++ b/app/src/main/java/org/mian/gitnex/adapters/ExploreRepositoriesAdapter.java @@ -318,4 +318,9 @@ public class ExploreRepositoriesAdapter extends RecyclerView.Adapter { + + private List labels; + private ArrayList labelsStrings = new ArrayList<>(); + private ArrayList labelsIds = new ArrayList<>(); + + private LabelsListAdapterListener labelsListener; + + public interface LabelsListAdapterListener { + + void labelsStringData(ArrayList data); + void labelsIdsData(ArrayList data); + } + + public LabelsListAdapter(List labelsMain, LabelsListAdapterListener labelsListener) { + + this.labels = labelsMain; + this.labelsListener = labelsListener; + } + + static class LabelsViewHolder extends RecyclerView.ViewHolder { + + private CheckBox labelSelection; + + private LabelsViewHolder(View itemView) { + super(itemView); + + labelSelection = itemView.findViewById(R.id.labelSelection); + + } + } + + @NonNull + @Override + public LabelsListAdapter.LabelsViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.custom_labels_list, parent, false); + return new LabelsListAdapter.LabelsViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull LabelsListAdapter.LabelsViewHolder holder, int position) { + + Labels currentItem = labels.get(position); + + holder.labelSelection.setText(currentItem.getName()); + + for(int i = 0; i < labelsIds.size(); i++) { + + if(labelsStrings.contains(currentItem.getName())) { + + holder.labelSelection.setChecked(true); + } + } + + holder.labelSelection.setOnCheckedChangeListener((buttonView, isChecked) -> { + + if(isChecked) { + + labelsStrings.add(currentItem.getName()); + labelsIds.add(currentItem.getId()); + } + else { + + labelsStrings.remove(currentItem.getName()); + labelsIds.remove(Integer.valueOf(currentItem.getId())); + } + + labelsListener.labelsStringData(labelsStrings); + labelsListener.labelsIdsData(labelsIds); + }); + } + + @Override + public int getItemCount() { + return labels.size(); + } +} diff --git a/app/src/main/java/org/mian/gitnex/adapters/SearchIssuesAdapter.java b/app/src/main/java/org/mian/gitnex/adapters/SearchIssuesAdapter.java new file mode 100644 index 00000000..379029e4 --- /dev/null +++ b/app/src/main/java/org/mian/gitnex/adapters/SearchIssuesAdapter.java @@ -0,0 +1,172 @@ +package org.mian.gitnex.adapters; + +import android.content.Context; +import android.content.Intent; +import android.text.Html; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import org.mian.gitnex.R; +import org.mian.gitnex.activities.IssueDetailActivity; +import org.mian.gitnex.clients.PicassoService; +import org.mian.gitnex.database.api.RepositoriesApi; +import org.mian.gitnex.database.models.Repository; +import org.mian.gitnex.helpers.ClickListener; +import org.mian.gitnex.helpers.RoundedTransformation; +import org.mian.gitnex.helpers.TimeHelper; +import org.mian.gitnex.helpers.TinyDB; +import org.mian.gitnex.models.Issues; +import org.ocpsoft.prettytime.PrettyTime; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.Locale; + +/** + * Author M M Arif + */ + +public class SearchIssuesAdapter extends RecyclerView.Adapter { + + private List searchedList; + private Context mCtx; + private TinyDB tinyDb; + + public SearchIssuesAdapter(List dataList, Context mCtx) { + + this.mCtx = mCtx; + this.searchedList = dataList; + this.tinyDb = new TinyDB(mCtx); + } + + class SearchViewHolder extends RecyclerView.ViewHolder { + + private TextView issueNumber; + private ImageView issueAssigneeAvatar; + private TextView issueTitle; + private TextView issueCreatedTime; + private TextView issueCommentsCount; + private TextView repoFullName; + + private SearchViewHolder(View itemView) { + + super(itemView); + + issueNumber = itemView.findViewById(R.id.issueNumber); + issueAssigneeAvatar = itemView.findViewById(R.id.assigneeAvatar); + issueTitle = itemView.findViewById(R.id.issueTitle); + issueCommentsCount = itemView.findViewById(R.id.issueCommentsCount); + issueCreatedTime = itemView.findViewById(R.id.issueCreatedTime); + repoFullName = itemView.findViewById(R.id.repoFullName); + + issueTitle.setOnClickListener(v -> { + + Context context = v.getContext(); + + Intent intent = new Intent(context, IssueDetailActivity.class); + intent.putExtra("issueNumber", issueNumber.getText()); + + tinyDb.putString("issueNumber", issueNumber.getText().toString()); + tinyDb.putString("issueType", "Issue"); + + tinyDb.putString("repoFullName", repoFullName.getText().toString()); + + String[] parts = repoFullName.getText().toString().split("/"); + final String repoOwner = parts[0]; + final String repoName = parts[1]; + + int currentActiveAccountId = tinyDb.getInt("currentActiveAccountId"); + RepositoriesApi repositoryData = new RepositoriesApi(context); + + Integer count = repositoryData.checkRepository(currentActiveAccountId, repoOwner, repoName); + + if(count == 0) { + + long id = repositoryData.insertRepository(currentActiveAccountId, repoOwner, repoName); + tinyDb.putLong("repositoryId", id); + + } + else { + + Repository data = repositoryData.getRepository(currentActiveAccountId, repoOwner, repoName); + tinyDb.putLong("repositoryId", data.getRepositoryId()); + + } + + context.startActivity(intent); + }); + } + + } + + @NonNull + @Override + public SearchIssuesAdapter.SearchViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_issues, parent, false); + return new SearchIssuesAdapter.SearchViewHolder(v); + } + + @Override + public void onBindViewHolder(@NonNull final SearchIssuesAdapter.SearchViewHolder holder, int position) { + + Issues currentItem = searchedList.get(position); + + String locale = tinyDb.getString("locale"); + String timeFormat = tinyDb.getString("dateFormat"); + + if(!currentItem.getUser().getFull_name().equals("")) { + holder.issueAssigneeAvatar.setOnClickListener(new ClickListener(mCtx.getResources().getString(R.string.issueCreator) + currentItem.getUser().getFull_name(), mCtx)); + } + else { + holder.issueAssigneeAvatar.setOnClickListener(new ClickListener(mCtx.getResources().getString(R.string.issueCreator) + currentItem.getUser().getLogin(), mCtx)); + } + + PicassoService + .getInstance(mCtx).get().load(currentItem.getUser().getAvatar_url()).placeholder(R.drawable.loader_animated).transform(new RoundedTransformation(8, 0)).resize(120, 120).centerCrop().into(holder.issueAssigneeAvatar); + + String issueNumber_ = "" + currentItem.getRepository().getFull_name() + mCtx.getResources().getString(R.string.hash) + currentItem.getNumber() + ""; + holder.issueTitle.setText(Html.fromHtml(issueNumber_ + " " + currentItem.getTitle())); + + holder.issueNumber.setText(String.valueOf(currentItem.getNumber())); + holder.issueCommentsCount.setText(String.valueOf(currentItem.getComments())); + holder.repoFullName.setText(currentItem.getRepository().getFull_name()); + + switch(timeFormat) { + case "pretty": { + PrettyTime prettyTime = new PrettyTime(new Locale(locale)); + String createdTime = prettyTime.format(currentItem.getCreated_at()); + holder.issueCreatedTime.setText(createdTime); + holder.issueCreatedTime.setOnClickListener(new ClickListener(TimeHelper.customDateFormatForToastDateFormat(currentItem.getCreated_at()), mCtx)); + break; + } + case "normal": { + DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd '" + mCtx.getResources().getString(R.string.timeAtText) + "' HH:mm", new Locale(locale)); + String createdTime = formatter.format(currentItem.getCreated_at()); + holder.issueCreatedTime.setText(createdTime); + break; + } + case "normal1": { + DateFormat formatter = new SimpleDateFormat("dd-MM-yyyy '" + mCtx.getResources().getString(R.string.timeAtText) + "' HH:mm", new Locale(locale)); + String createdTime = formatter.format(currentItem.getCreated_at()); + holder.issueCreatedTime.setText(createdTime); + break; + } + } + } + + @Override + public int getItemCount() { + + return searchedList.size(); + } + + public void notifyDataChanged() { + + notifyDataSetChanged(); + } +} diff --git a/app/src/main/java/org/mian/gitnex/fragments/AboutFragment.java b/app/src/main/java/org/mian/gitnex/fragments/AboutFragment.java index 1f888ee2..9f43bd12 100644 --- a/app/src/main/java/org/mian/gitnex/fragments/AboutFragment.java +++ b/app/src/main/java/org/mian/gitnex/fragments/AboutFragment.java @@ -6,14 +6,13 @@ import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.TextView; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.fragment.app.Fragment; import org.mian.gitnex.R; +import org.mian.gitnex.activities.MainActivity; +import org.mian.gitnex.databinding.FragmentAboutBinding; import org.mian.gitnex.helpers.AppUtil; import org.mian.gitnex.helpers.TinyDB; -import java.util.Objects; /** * Author M M Arif @@ -21,30 +20,19 @@ import java.util.Objects; public class AboutFragment extends Fragment { - public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { - - View v = inflater.inflate(R.layout.fragment_about, container, false); + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + FragmentAboutBinding viewBinding = FragmentAboutBinding.inflate(inflater, container, false); TinyDB tinyDb = new TinyDB(getContext()); - final TextView appVerBuild; - final TextView donationLink; - final TextView donationLinkPatreon; - final TextView translateLink; - final TextView appWebsite; - final TextView appRepo; + viewBinding.appVersion.setText(AppUtil.getAppVersion(requireContext())); + viewBinding.userServerVersion.setText(tinyDb.getString("giteaVersion")); + viewBinding.appBuild.setText(String.valueOf(AppUtil.getAppBuildNo(requireContext()))); - appVerBuild = v.findViewById(R.id.appVerBuild); - TextView viewTextGiteaVersion = v.findViewById(R.id.giteaVersion); - donationLink = v.findViewById(R.id.donationLink); - donationLinkPatreon = v.findViewById(R.id.donationLinkPatreon); - translateLink = v.findViewById(R.id.translateLink); - appWebsite = v.findViewById(R.id.appWebsite); - appRepo = v.findViewById(R.id.appRepo); + ((MainActivity) requireActivity()).setActionBarTitle(getResources().getString(R.string.pageTitleAbout)); - appVerBuild.setText(getString(R.string.appVerBuild, AppUtil.getAppVersion(Objects.requireNonNull(getContext())), AppUtil.getAppBuildNo(getContext()))); - - donationLink.setOnClickListener(v1 -> { + viewBinding.donationLinkLiberapay.setOnClickListener(v1 -> { Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); @@ -53,7 +41,7 @@ public class AboutFragment extends Fragment { startActivity(intent); }); - donationLinkPatreon.setOnClickListener(v12 -> { + viewBinding.donationLinkPatreon.setOnClickListener(v12 -> { Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); @@ -62,7 +50,7 @@ public class AboutFragment extends Fragment { startActivity(intent); }); - translateLink.setOnClickListener(v13 -> { + viewBinding.translateLink.setOnClickListener(v13 -> { Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); @@ -71,7 +59,7 @@ public class AboutFragment extends Fragment { startActivity(intent); }); - appWebsite.setOnClickListener(v14 -> { + viewBinding.appWebsite.setOnClickListener(v14 -> { Intent intent = new Intent(); intent.setAction(Intent.ACTION_VIEW); @@ -80,19 +68,15 @@ public class AboutFragment extends Fragment { startActivity(intent); }); - appRepo.setOnClickListener(v15 -> { + if(AppUtil.isPro(requireContext())) { - Intent intent = new Intent(); - intent.setAction(Intent.ACTION_VIEW); - intent.addCategory(Intent.CATEGORY_BROWSABLE); - intent.setData(Uri.parse(getResources().getString(R.string.appRepoLink))); - startActivity(intent); - }); + viewBinding.supportHeader.setVisibility(View.GONE); + viewBinding.dividerSupport.setVisibility(View.GONE); + viewBinding.donationLinkLiberapay.setVisibility(View.GONE); + viewBinding.donationLinkPatreon.setVisibility(View.GONE); + } - String commit = getResources().getString(R.string.commitPage) + tinyDb.getString("giteaVersion"); - viewTextGiteaVersion.setText(commit); - - return v; + return viewBinding.getRoot(); } } diff --git a/app/src/main/java/org/mian/gitnex/fragments/BottomSheetRepoFragment.java b/app/src/main/java/org/mian/gitnex/fragments/BottomSheetRepoFragment.java index dd097632..f99ea3a1 100644 --- a/app/src/main/java/org/mian/gitnex/fragments/BottomSheetRepoFragment.java +++ b/app/src/main/java/org/mian/gitnex/fragments/BottomSheetRepoFragment.java @@ -43,6 +43,7 @@ public class BottomSheetRepoFragment extends BottomSheetDialogFragment { TextView copyRepoUrl = v.findViewById(R.id.copyRepoUrl); View repoSettingsDivider = v.findViewById(R.id.repoSettingsDivider); TextView repoSettings = v.findViewById(R.id.repoSettings); + TextView createPullRequest = v.findViewById(R.id.createPullRequest); createLabel.setOnClickListener(v112 -> { @@ -64,6 +65,20 @@ public class BottomSheetRepoFragment extends BottomSheetDialogFragment { createIssue.setVisibility(View.GONE); } + if(tinyDb.getBoolean("hasPullRequests")) { + + createPullRequest.setVisibility(View.VISIBLE); + createPullRequest.setOnClickListener(vPr -> { + + bmListener.onButtonClicked("newPullRequest"); + dismiss(); + }); + } + else { + + createPullRequest.setVisibility(View.GONE); + } + createMilestone.setOnClickListener(v13 -> { bmListener.onButtonClicked("newMilestone"); diff --git a/app/src/main/java/org/mian/gitnex/fragments/ExploreFragment.java b/app/src/main/java/org/mian/gitnex/fragments/ExploreFragment.java new file mode 100644 index 00000000..47d3c23e --- /dev/null +++ b/app/src/main/java/org/mian/gitnex/fragments/ExploreFragment.java @@ -0,0 +1,129 @@ +package org.mian.gitnex.fragments; + +import android.content.Context; +import android.graphics.Typeface; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; +import androidx.viewpager.widget.ViewPager; +import com.google.android.material.tabs.TabLayout; +import org.mian.gitnex.R; +import org.mian.gitnex.activities.MainActivity; +import org.mian.gitnex.helpers.TinyDB; + +/** + * Author M M Arif + */ + +public class ExploreFragment extends Fragment { + + private Context ctx; + private TinyDB tinyDB; + + private int tabsCount; + public ViewPager mViewPager; + + @Nullable + @Override + public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + + View v = inflater.inflate(R.layout.fragment_explore, container,false); + ctx = getContext(); + tinyDB = new TinyDB(ctx); + + ((MainActivity) requireActivity()).setActionBarTitle(getResources().getString(R.string.navExplore)); + + TabLayout tabLayout = v.findViewById(R.id.tabsExplore); + + ViewGroup viewGroup = (ViewGroup) tabLayout.getChildAt(0); + tabsCount = viewGroup.getChildCount(); + + Typeface myTypeface; + + switch(tinyDB.getInt("customFontId", -1)) { + + case 0: + myTypeface = Typeface.createFromAsset(ctx.getAssets(), "fonts/roboto.ttf"); + break; + + case 2: + myTypeface = Typeface.createFromAsset(ctx.getAssets(), "fonts/sourcecodeproregular.ttf"); + break; + + default: + myTypeface = Typeface.createFromAsset(ctx.getAssets(), "fonts/manroperegular.ttf"); + break; + + } + + for(int j = 0; j < tabsCount; j++) { + + ViewGroup vgTab = (ViewGroup) viewGroup.getChildAt(j); + int tabChildCount = vgTab.getChildCount(); + + for(int i = 0; i < tabChildCount; i++) { + + View tabViewChild = vgTab.getChildAt(i); + + if(tabViewChild instanceof TextView) { + ((TextView) tabViewChild).setTypeface(myTypeface); + } + } + } + + mViewPager = v.findViewById(R.id.containerExplore); + + mViewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout)); + tabLayout.addOnTabSelectedListener(new TabLayout.ViewPagerOnTabSelectedListener(mViewPager)); + + SectionsPagerAdapter mSectionsPagerAdapter = new SectionsPagerAdapter(getChildFragmentManager()); + mViewPager.setAdapter(mSectionsPagerAdapter); + + return v; + } + + public class SectionsPagerAdapter extends FragmentPagerAdapter { + + SectionsPagerAdapter(FragmentManager fm) { + + super(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT); + } + + @NonNull + @Override + public Fragment getItem(int position) { + + Fragment fragment = null; + + switch(position) { + + case 0: // Repositories + fragment = new ExploreRepositoriesFragment(); + break; + + case 1: // Issues + fragment = new SearchIssuesFragment(); + break; + } + + assert fragment != null; + return fragment; + + } + + @Override + public int getCount() { + + return tabsCount; + } + + } + +} diff --git a/app/src/main/java/org/mian/gitnex/fragments/ExploreRepositoriesFragment.java b/app/src/main/java/org/mian/gitnex/fragments/ExploreRepositoriesFragment.java index dc354883..75bf2c84 100644 --- a/app/src/main/java/org/mian/gitnex/fragments/ExploreRepositoriesFragment.java +++ b/app/src/main/java/org/mian/gitnex/fragments/ExploreRepositoriesFragment.java @@ -1,27 +1,37 @@ package org.mian.gitnex.fragments; +import android.app.Dialog; import android.content.Context; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; import android.net.Uri; import android.os.Bundle; import android.util.Log; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; -import android.widget.ProgressBar; -import android.widget.TextView; +import android.view.inputmethod.InputMethodManager; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; -import androidx.recyclerview.widget.DividerItemDecoration; import androidx.recyclerview.widget.LinearLayoutManager; -import androidx.recyclerview.widget.RecyclerView; import org.mian.gitnex.R; import org.mian.gitnex.adapters.ExploreRepositoriesAdapter; import org.mian.gitnex.clients.RetrofitClient; +import org.mian.gitnex.databinding.CustomExploreRepositoriesDialogBinding; +import org.mian.gitnex.databinding.FragmentExploreRepoBinding; +import org.mian.gitnex.helpers.AppUtil; import org.mian.gitnex.helpers.Authorization; +import org.mian.gitnex.helpers.InfiniteScrollListener; +import org.mian.gitnex.helpers.StaticGlobalVariables; import org.mian.gitnex.helpers.TinyDB; +import org.mian.gitnex.helpers.Version; import org.mian.gitnex.models.ExploreRepositories; import org.mian.gitnex.models.UserRepositories; +import java.util.ArrayList; import java.util.List; import java.util.Objects; import retrofit2.Call; @@ -29,189 +39,300 @@ import retrofit2.Callback; import retrofit2.Response; /** - * Template Author Author M M Arif + * Template Author M M Arif * Author 6543 */ public class ExploreRepositoriesFragment extends Fragment { - private static String repoNameF = "param2"; - private static String repoOwnerF = "param1"; - private ProgressBar mProgressBar; - private RecyclerView mRecyclerView; - private TextView noData; - private TextView searchKeyword; - private Boolean repoTypeInclude = true; + private FragmentExploreRepoBinding viewBinding; + private Context ctx; + private TinyDB tinyDb; + + private int pageCurrentIndex = 1; + private boolean repoTypeInclude = true; private String sort = "updated"; private String order = "desc"; - private int limit = 50; + private int limit = 10; + private List dataList; + private ExploreRepositoriesAdapter adapter; - private OnFragmentInteractionListener mListener; + private String instanceUrl; + private String loginUid; + private String instanceToken; - public ExploreRepositoriesFragment() { - - } - - public static ExploreRepositoriesFragment newInstance(String param1, String param2) { - - ExploreRepositoriesFragment fragment = new ExploreRepositoriesFragment(); - Bundle args = new Bundle(); - args.putString(repoOwnerF, param1); - args.putString(repoNameF, param2); - fragment.setArguments(args); - return fragment; - } - - @Override - public void onCreate(Bundle savedInstanceState) { - - super.onCreate(savedInstanceState); - if(getArguments() != null) { - String repoName = getArguments().getString(repoNameF); - String repoOwner = getArguments().getString(repoOwnerF); - } - } + private Dialog dialogFilterOptions; + private CustomExploreRepositoriesDialogBinding filterBinding; @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - final View v = inflater.inflate(R.layout.fragment_explore_repo, container, false); - //setHasOptionsMenu(true); + viewBinding = FragmentExploreRepoBinding.inflate(inflater, container, false); + setHasOptionsMenu(true); - TinyDB tinyDb = new TinyDB(getContext()); - final String instanceUrl = tinyDb.getString("instanceUrl"); - final String loginUid = tinyDb.getString("loginUid"); - final String instanceToken = "token " + tinyDb.getString(loginUid + "-token"); + ctx = getContext(); + tinyDb = new TinyDB(getContext()); - searchKeyword = v.findViewById(R.id.searchKeyword); - noData = v.findViewById(R.id.noData); - mProgressBar = v.findViewById(R.id.progress_bar); - mRecyclerView = v.findViewById(R.id.recyclerViewReposSearch); + instanceUrl = tinyDb.getString("instanceUrl"); + loginUid = tinyDb.getString("loginUid"); + instanceToken = "token " + tinyDb.getString(loginUid + "-token"); - mProgressBar.setVisibility(View.VISIBLE); + dataList = new ArrayList<>(); + adapter = new ExploreRepositoriesAdapter(dataList, ctx); - searchKeyword.setOnEditorActionListener((v1, actionId, event) -> { + tinyDb.putBoolean("exploreRepoIncludeTopic", false); + tinyDb.putBoolean("exploreRepoIncludeDescription", false); + tinyDb.putBoolean("exploreRepoIncludeTemplate", false); + tinyDb.putBoolean("exploreRepoOnlyArchived", false); + + // if gitea is 1.12 or higher use the new limit + if(new Version(tinyDb.getString("giteaVersion")).higherOrEqual("1.12.0")) { + limit = StaticGlobalVariables.resultLimitNewGiteaInstances; + } + + LinearLayoutManager linearLayoutManager = new LinearLayoutManager(ctx); + + viewBinding.recyclerViewReposSearch.setHasFixedSize(true); + viewBinding.recyclerViewReposSearch.setLayoutManager(linearLayoutManager); + viewBinding.recyclerViewReposSearch.setAdapter(adapter); + + viewBinding.searchKeyword.setOnEditorActionListener((v1, actionId, event) -> { if(actionId == EditorInfo.IME_ACTION_SEND) { - if(!searchKeyword.getText().toString().equals("")) { - mProgressBar.setVisibility(View.VISIBLE); - mRecyclerView.setVisibility(View.GONE); - loadSearchReposList(instanceUrl, instanceToken, loginUid, searchKeyword.getText().toString(), repoTypeInclude, sort, order, getContext(), limit); + + if(!Objects.requireNonNull(viewBinding.searchKeyword.getText()).toString().equals("")) { + + InputMethodManager imm = (InputMethodManager) requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(viewBinding.searchKeyword.getWindowToken(), 0); + + // if gitea is 1.12 or higher use the new limit + if(new Version(tinyDb.getString("giteaVersion")).higherOrEqual("1.12.0")) { + limit = StaticGlobalVariables.resultLimitNewGiteaInstances; + } + else { + limit = 10; + } + + pageCurrentIndex = 1; + viewBinding.progressBar.setVisibility(View.VISIBLE); + loadData(false, viewBinding.searchKeyword.getText().toString(), tinyDb.getBoolean("exploreRepoIncludeTopic"), tinyDb.getBoolean("exploreRepoIncludeDescription"), tinyDb.getBoolean("exploreRepoIncludeTemplate"), tinyDb.getBoolean("exploreRepoOnlyArchived")); } } return false; }); - int limitDefault = 25; - loadDefaultList(instanceUrl, instanceToken, loginUid, repoTypeInclude, sort, order, getContext(), limitDefault); - - return v; - - } - - private void loadDefaultList(String instanceUrl, String instanceToken, String loginUid, Boolean repoTypeInclude, String sort, String order, final Context context, int limit) { - - Call call = RetrofitClient.getInstance(instanceUrl, getContext()).getApiInterface().queryRepos(Authorization.returnAuthentication(getContext(), loginUid, instanceToken), null, repoTypeInclude, sort, order, limit); - - call.enqueue(new Callback() { + viewBinding.recyclerViewReposSearch.addOnScrollListener(new InfiniteScrollListener(pageCurrentIndex, linearLayoutManager) { @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - - if(response.isSuccessful()) { - assert response.body() != null; - getReposList(response.body().getSearchedData(), context); - } - else { - Log.i("onResponse", String.valueOf(response.code())); - } + public void onScrolledToEnd(int firstVisibleItemPosition) { + pageCurrentIndex++; + loadData(true, Objects.requireNonNull(viewBinding.searchKeyword.getText()).toString(), tinyDb.getBoolean("exploreRepoIncludeTopic"), tinyDb.getBoolean("exploreRepoIncludeDescription"), tinyDb.getBoolean("exploreRepoIncludeTemplate"), tinyDb.getBoolean("exploreRepoOnlyArchived")); } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - Log.i("onFailure", Objects.requireNonNull(t.getMessage())); - } - }); - } + viewBinding.pullToRefresh.setOnRefreshListener(() -> { - private void loadSearchReposList(String instanceUrl, String instanceToken, String loginUid, String searchKeyword, Boolean repoTypeInclude, String sort, String order, final Context context, int limit) { - - Call call = RetrofitClient.getInstance(instanceUrl, getContext()).getApiInterface().queryRepos(Authorization.returnAuthentication(getContext(), loginUid, instanceToken), searchKeyword, repoTypeInclude, sort, order, limit); - - call.enqueue(new Callback() { - - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - - if(response.isSuccessful()) { - assert response.body() != null; - getReposList(response.body().getSearchedData(), context); - } - else { - Log.i("onResponse", String.valueOf(response.code())); - } + pageCurrentIndex = 1; + // if gitea is 1.12 or higher use the new limit + if(new Version(tinyDb.getString("giteaVersion")).higherOrEqual("1.12.0")) { + limit = StaticGlobalVariables.resultLimitNewGiteaInstances; + } + else { + limit = 10; } - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - Log.i("onFailure", Objects.requireNonNull(t.getMessage())); - } - + loadData(false, Objects.requireNonNull(viewBinding.searchKeyword.getText()).toString(), tinyDb.getBoolean("exploreRepoIncludeTopic"), tinyDb.getBoolean("exploreRepoIncludeDescription"), tinyDb.getBoolean("exploreRepoIncludeTemplate"), tinyDb.getBoolean("exploreRepoOnlyArchived")); }); + loadData(false, "", tinyDb.getBoolean("exploreRepoIncludeTopic"), tinyDb.getBoolean("exploreRepoIncludeDescription"), tinyDb.getBoolean("exploreRepoIncludeTemplate"), tinyDb.getBoolean("exploreRepoOnlyArchived")); + + return viewBinding.getRoot(); + } - private void getReposList(List dataList, Context context) { + private void loadData(boolean append, String searchKeyword, boolean exploreRepoIncludeTopic, boolean exploreRepoIncludeDescription, boolean exploreRepoIncludeTemplate, boolean exploreRepoOnlyArchived) { - ExploreRepositoriesAdapter adapter = new ExploreRepositoriesAdapter(dataList, context); + viewBinding.noData.setVisibility(View.GONE); - mRecyclerView.setVisibility(View.VISIBLE); + int apiCallDefaultLimit = 10; + // if gitea is 1.12 or higher use the new limit + if(new Version(tinyDb.getString("giteaVersion")).higherOrEqual("1.12.0")) { + apiCallDefaultLimit = StaticGlobalVariables.resultLimitNewGiteaInstances; + } - mRecyclerView.setHasFixedSize(true); - mRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - DividerItemDecoration dividerItemDecoration = new DividerItemDecoration(mRecyclerView.getContext(), DividerItemDecoration.VERTICAL); - mRecyclerView.addItemDecoration(dividerItemDecoration); + if(apiCallDefaultLimit > limit) { + return; + } - if(adapter.getItemCount() > 0) { - - mRecyclerView.setAdapter(adapter); - noData.setVisibility(View.GONE); - mProgressBar.setVisibility(View.GONE); + if(pageCurrentIndex == 1 || !append) { + dataList.clear(); + adapter.notifyDataSetChanged(); + viewBinding.pullToRefresh.setRefreshing(false); + viewBinding.progressBar.setVisibility(View.VISIBLE); } else { - noData.setVisibility(View.VISIBLE); - mProgressBar.setVisibility(View.GONE); - + viewBinding.loadingMoreView.setVisibility(View.VISIBLE); } + Call call = RetrofitClient.getInstance(instanceUrl, getContext()).getApiInterface().queryRepos(Authorization.returnAuthentication(getContext(), loginUid, instanceToken), searchKeyword, repoTypeInclude, sort, order, exploreRepoIncludeTopic, exploreRepoIncludeDescription, exploreRepoIncludeTemplate, exploreRepoOnlyArchived, limit, pageCurrentIndex); + + call.enqueue(new Callback() { + + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + + if(response.code() == 200) { + + assert response.body() != null; + + limit = response.body().getSearchedData().size(); + + if(!append) { + + dataList.clear(); + } + + dataList.addAll(response.body().getSearchedData()); + adapter.notifyDataSetChanged(); + + } + else { + + dataList.clear(); + adapter.notifyDataChanged(); + viewBinding.noData.setVisibility(View.VISIBLE); + + } + + onCleanup(); + + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + + Log.e("onFailure", Objects.requireNonNull(t.getMessage())); + onCleanup(); + + } + + private void onCleanup() { + + AppUtil.setMultiVisibility(View.GONE, viewBinding.loadingMoreView, viewBinding.progressBar); + + if(dataList.isEmpty()) { + + viewBinding.noData.setVisibility(View.VISIBLE); + } + } + }); + } + + @Override + public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) { + + menu.clear(); + inflater.inflate(R.menu.filter_menu, menu); + super.onCreateOptionsMenu(menu, inflater); + + MenuItem filter = menu.findItem(R.id.filter); + + filter.setOnMenuItemClickListener(filter_ -> { + + showFilterOptions(); + return false; + }); + } - public void onButtonPressed(Uri uri) { + private void showFilterOptions() { - if(mListener != null) { - mListener.onFragmentInteraction(uri); + dialogFilterOptions = new Dialog(ctx, R.style.ThemeOverlay_MaterialComponents_Dialog_Alert); + + if (dialogFilterOptions.getWindow() != null) { + + dialogFilterOptions.getWindow().setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); } + + filterBinding = CustomExploreRepositoriesDialogBinding.inflate(LayoutInflater.from(ctx)); + + View view = filterBinding.getRoot(); + dialogFilterOptions.setContentView(view); + + filterBinding.includeTopic.setOnClickListener(includeTopic -> { + + if(filterBinding.includeTopic.isChecked()) { + + tinyDb.putBoolean("exploreRepoIncludeTopic", true); + } + else { + + tinyDb.putBoolean("exploreRepoIncludeTopic", false); + } + }); + + filterBinding.includeDesc.setOnClickListener(includeDesc -> { + + if(filterBinding.includeDesc.isChecked()) { + + tinyDb.putBoolean("exploreRepoIncludeDescription", true); + } + else { + + tinyDb.putBoolean("exploreRepoIncludeDescription", false); + } + }); + + filterBinding.includeTemplate.setOnClickListener(includeTemplate -> { + + if(filterBinding.includeTemplate.isChecked()) { + + tinyDb.putBoolean("exploreRepoIncludeTemplate", true); + } + else { + + tinyDb.putBoolean("exploreRepoIncludeTemplate", false); + } + }); + + filterBinding.onlyArchived.setOnClickListener(onlyArchived -> { + + if(filterBinding.onlyArchived.isChecked()) { + + tinyDb.putBoolean("exploreRepoOnlyArchived", true); + } + else { + + tinyDb.putBoolean("exploreRepoOnlyArchived", false); + } + }); + + + filterBinding.includeTopic.setChecked(tinyDb.getBoolean("exploreRepoIncludeTopic")); + filterBinding.includeDesc.setChecked(tinyDb.getBoolean("exploreRepoIncludeDescription")); + filterBinding.includeTemplate.setChecked(tinyDb.getBoolean("exploreRepoIncludeTemplate")); + filterBinding.onlyArchived.setChecked(tinyDb.getBoolean("exploreRepoOnlyArchived")); + + filterBinding.cancel.setOnClickListener(editProperties -> { + dialogFilterOptions.dismiss(); + }); + + dialogFilterOptions.show(); } @Override public void onDetach() { super.onDetach(); - mListener = null; } public interface OnFragmentInteractionListener { void onFragmentInteraction(Uri uri); - } } diff --git a/app/src/main/java/org/mian/gitnex/fragments/IssuesFragment.java b/app/src/main/java/org/mian/gitnex/fragments/IssuesFragment.java index 77a68ab9..4ab393d3 100644 --- a/app/src/main/java/org/mian/gitnex/fragments/IssuesFragment.java +++ b/app/src/main/java/org/mian/gitnex/fragments/IssuesFragment.java @@ -32,7 +32,6 @@ import org.mian.gitnex.interfaces.ApiInterface; import org.mian.gitnex.models.Issues; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; @@ -112,7 +111,7 @@ public class IssuesFragment extends Fragment { recyclerView.setLayoutManager(new LinearLayoutManager(context)); recyclerView.setAdapter(adapter); - ((RepoDetailActivity) Objects.requireNonNull(getActivity())).setFragmentRefreshListener(issueState -> { + ((RepoDetailActivity) requireActivity()).setFragmentRefreshListener(issueState -> { if(issueState.equals("closed")) { menu.getItem(1).setIcon(R.drawable.ic_filter_closed); diff --git a/app/src/main/java/org/mian/gitnex/fragments/MilestonesFragment.java b/app/src/main/java/org/mian/gitnex/fragments/MilestonesFragment.java index 91323f7f..ea15a89f 100644 --- a/app/src/main/java/org/mian/gitnex/fragments/MilestonesFragment.java +++ b/app/src/main/java/org/mian/gitnex/fragments/MilestonesFragment.java @@ -27,7 +27,6 @@ import org.mian.gitnex.interfaces.ApiInterface; import org.mian.gitnex.models.Milestones; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; @@ -104,7 +103,7 @@ public class MilestonesFragment extends Fragment { }, 50)); - ((RepoDetailActivity) Objects.requireNonNull(getActivity())).setFragmentRefreshListenerMilestone(milestoneState -> { + ((RepoDetailActivity) requireActivity()).setFragmentRefreshListenerMilestone(milestoneState -> { if(milestoneState.equals("closed")) { menu.getItem(1).setIcon(R.drawable.ic_filter_closed); diff --git a/app/src/main/java/org/mian/gitnex/fragments/NotificationsFragment.java b/app/src/main/java/org/mian/gitnex/fragments/NotificationsFragment.java index a07f442a..1fb685d3 100644 --- a/app/src/main/java/org/mian/gitnex/fragments/NotificationsFragment.java +++ b/app/src/main/java/org/mian/gitnex/fragments/NotificationsFragment.java @@ -36,7 +36,6 @@ import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.List; -import java.util.Objects; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; @@ -79,7 +78,7 @@ public class NotificationsFragment extends Fragment implements NotificationsAdap View v = inflater.inflate(R.layout.fragment_notifications, container, false); setHasOptionsMenu(true); - activity = Objects.requireNonNull(getActivity()); + activity = requireActivity(); context = getContext(); tinyDB = new TinyDB(context); diff --git a/app/src/main/java/org/mian/gitnex/fragments/RepoInfoFragment.java b/app/src/main/java/org/mian/gitnex/fragments/RepoInfoFragment.java index bdf79e30..58e96a16 100644 --- a/app/src/main/java/org/mian/gitnex/fragments/RepoInfoFragment.java +++ b/app/src/main/java/org/mian/gitnex/fragments/RepoInfoFragment.java @@ -351,6 +351,13 @@ public class RepoInfoFragment extends Fragment { tinyDb.putBoolean("hasIssues", true); } + if(repoInfo.isHas_pull_requests()) { + tinyDb.putBoolean("hasPullRequests", repoInfo.isHas_pull_requests()); + } + else { + tinyDb.putBoolean("hasPullRequests", false); + } + tinyDb.putString("repoHtmlUrl", repoInfo.getHtml_url()); mProgressBar.setVisibility(View.GONE); diff --git a/app/src/main/java/org/mian/gitnex/fragments/SearchIssuesFragment.java b/app/src/main/java/org/mian/gitnex/fragments/SearchIssuesFragment.java new file mode 100644 index 00000000..5bb77e07 --- /dev/null +++ b/app/src/main/java/org/mian/gitnex/fragments/SearchIssuesFragment.java @@ -0,0 +1,190 @@ +package org.mian.gitnex.fragments; + +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import org.mian.gitnex.adapters.SearchIssuesAdapter; +import org.mian.gitnex.clients.RetrofitClient; +import org.mian.gitnex.databinding.FragmentSearchIssuesBinding; +import org.mian.gitnex.helpers.AppUtil; +import org.mian.gitnex.helpers.Authorization; +import org.mian.gitnex.helpers.InfiniteScrollListener; +import org.mian.gitnex.helpers.TinyDB; +import org.mian.gitnex.models.Issues; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +/** + * Author M M Arif + */ + +public class SearchIssuesFragment extends Fragment { + + private Context ctx; + private TinyDB tinyDb; + private FragmentSearchIssuesBinding viewBinding; + private SearchIssuesAdapter adapter; + private List dataList; + + private String instanceUrl; + private String loginUid; + private String instanceToken; + + private int apiCallCurrentValue = 10; + private int pageCurrentIndex = 1; + private String type = "issues"; + private String state = "open"; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + + viewBinding = FragmentSearchIssuesBinding.inflate(inflater, container, false); + setHasOptionsMenu(true); + + ctx = getContext(); + tinyDb = new TinyDB(getContext()); + + instanceUrl = tinyDb.getString("instanceUrl"); + loginUid = tinyDb.getString("loginUid"); + instanceToken = "token " + tinyDb.getString(loginUid + "-token"); + + dataList = new ArrayList<>(); + adapter = new SearchIssuesAdapter(dataList, ctx); + + LinearLayoutManager linearLayoutManager = new LinearLayoutManager(ctx); + + viewBinding.recyclerViewSearchIssues.setHasFixedSize(true); + viewBinding.recyclerViewSearchIssues.setLayoutManager(linearLayoutManager); + viewBinding.recyclerViewSearchIssues.setAdapter(adapter); + + viewBinding.searchKeyword.setOnEditorActionListener((v1, actionId, event) -> { + + if(actionId == EditorInfo.IME_ACTION_SEND) { + + if(!Objects.requireNonNull(viewBinding.searchKeyword.getText()).toString().equals("")) { + + InputMethodManager imm = (InputMethodManager) requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(viewBinding.searchKeyword.getWindowToken(), 0); + + pageCurrentIndex = 1; + apiCallCurrentValue = 10; + viewBinding.progressBar.setVisibility(View.VISIBLE); + loadData(false, viewBinding.searchKeyword.getText().toString()); + } + } + + return false; + }); + + viewBinding.recyclerViewSearchIssues.addOnScrollListener(new InfiniteScrollListener(pageCurrentIndex, linearLayoutManager) { + + @Override + public void onScrolledToEnd(int firstVisibleItemPosition) { + + pageCurrentIndex++; + loadData(true, Objects.requireNonNull(viewBinding.searchKeyword.getText()).toString()); + + } + }); + + viewBinding.pullToRefresh.setOnRefreshListener(() -> { + + pageCurrentIndex = 1; + apiCallCurrentValue = 10; + loadData(false, Objects.requireNonNull(viewBinding.searchKeyword.getText()).toString()); + + }); + + loadData(false, ""); + + return viewBinding.getRoot(); + } + + private void loadData(boolean append, String searchKeyword) { + + viewBinding.noData.setVisibility(View.GONE); + + int apiCallDefaultLimit = 10; + if(apiCallDefaultLimit > apiCallCurrentValue) { + return; + } + + if(pageCurrentIndex == 1 || !append) { + + dataList.clear(); + adapter.notifyDataSetChanged(); + viewBinding.pullToRefresh.setRefreshing(false); + viewBinding.progressBar.setVisibility(View.VISIBLE); + } + else { + + viewBinding.loadingMoreView.setVisibility(View.VISIBLE); + } + + Call> call = RetrofitClient.getInstance(instanceUrl, getContext()).getApiInterface().queryIssues( + Authorization.returnAuthentication(getContext(), loginUid, instanceToken), searchKeyword, type, state, pageCurrentIndex); + + call.enqueue(new Callback>() { + + @Override + public void onResponse(@NonNull Call> call, @NonNull Response> response) { + + if(response.code() == 200) { + + assert response.body() != null; + apiCallCurrentValue = response.body().size(); + + if(!append) { + + dataList.clear(); + } + + dataList.addAll(response.body()); + adapter.notifyDataSetChanged(); + + } + else { + + dataList.clear(); + adapter.notifyDataChanged(); + viewBinding.noData.setVisibility(View.VISIBLE); + + } + + onCleanup(); + + } + + @Override + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + + Log.e("onFailure", Objects.requireNonNull(t.getMessage())); + onCleanup(); + + } + + private void onCleanup() { + + AppUtil.setMultiVisibility(View.GONE, viewBinding.loadingMoreView, viewBinding.progressBar); + + if(dataList.isEmpty()) { + + viewBinding.noData.setVisibility(View.VISIBLE); + } + } + }); + } + +} diff --git a/app/src/main/java/org/mian/gitnex/fragments/SettingsFragment.java b/app/src/main/java/org/mian/gitnex/fragments/SettingsFragment.java index 2548857d..93bc84f5 100644 --- a/app/src/main/java/org/mian/gitnex/fragments/SettingsFragment.java +++ b/app/src/main/java/org/mian/gitnex/fragments/SettingsFragment.java @@ -1,6 +1,8 @@ package org.mian.gitnex.fragments; +import android.content.ActivityNotFoundException; import android.content.Intent; +import android.net.Uri; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -17,7 +19,6 @@ import org.mian.gitnex.activities.SettingsReportsActivity; import org.mian.gitnex.activities.SettingsSecurityActivity; import org.mian.gitnex.activities.SettingsTranslationActivity; import org.mian.gitnex.helpers.TinyDB; -import java.util.Objects; /** * Author M M Arif @@ -37,6 +38,8 @@ public class SettingsFragment extends Fragment { LinearLayout securityFrame = v.findViewById(R.id.securityFrame); LinearLayout languagesFrame = v.findViewById(R.id.languagesFrame); LinearLayout reportsFrame = v.findViewById(R.id.reportsFrame); + LinearLayout rateAppFrame = v.findViewById(R.id.rateAppFrame); + LinearLayout aboutAppFrame = v.findViewById(R.id.aboutAppFrame); appearanceFrame.setOnClickListener(v1 -> startActivity(new Intent(getContext(), SettingsAppearanceActivity.class))); @@ -50,10 +53,24 @@ public class SettingsFragment extends Fragment { reportsFrame.setOnClickListener(v1 -> startActivity(new Intent(getContext(), SettingsReportsActivity.class))); + rateAppFrame.setOnClickListener(aboutApp -> rateThisApp()); + + aboutAppFrame.setOnClickListener(aboutApp -> requireActivity().getSupportFragmentManager().beginTransaction().replace(R.id.fragment_container, new AboutFragment()).commit()); + return v; } + public void rateThisApp() { + + try { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + requireActivity().getPackageName()))); + } + catch(ActivityNotFoundException e) { + startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://play.google.com/store/apps/details?id=" + requireActivity().getPackageName()))); + } + } + @Override public void onResume() { @@ -62,8 +79,8 @@ public class SettingsFragment extends Fragment { TinyDB tinyDb = new TinyDB(getContext()); if(tinyDb.getBoolean("refreshParent")) { - Objects.requireNonNull(getActivity()).recreate(); - getActivity().overridePendingTransition(0, 0); + requireActivity().recreate(); + requireActivity().overridePendingTransition(0, 0); tinyDb.putBoolean("refreshParent", false); } diff --git a/app/src/main/java/org/mian/gitnex/helpers/AppUtil.java b/app/src/main/java/org/mian/gitnex/helpers/AppUtil.java index 79b37417..6eedfa4b 100644 --- a/app/src/main/java/org/mian/gitnex/helpers/AppUtil.java +++ b/app/src/main/java/org/mian/gitnex/helpers/AppUtil.java @@ -76,6 +76,10 @@ public class AppUtil { } } + public static boolean isPro(Context context) { + return context.getPackageName().equals("org.mian.gitnex.pro"); + } + public int charactersLength(String str) { return str.length(); diff --git a/app/src/main/java/org/mian/gitnex/helpers/ClickListener.java b/app/src/main/java/org/mian/gitnex/helpers/ClickListener.java index 8c950a95..78a298ff 100644 --- a/app/src/main/java/org/mian/gitnex/helpers/ClickListener.java +++ b/app/src/main/java/org/mian/gitnex/helpers/ClickListener.java @@ -3,7 +3,6 @@ package org.mian.gitnex.helpers; import android.content.Context; import android.view.LayoutInflater; import android.view.View; -import android.view.ViewGroup; import android.widget.TextView; import android.widget.Toast; import org.mian.gitnex.R; @@ -18,17 +17,16 @@ public class ClickListener implements View.OnClickListener { private Context mCtx; public ClickListener(String infoText, Context mCtx) { + this.infoText = infoText; this.mCtx = mCtx; } @Override - public void onClick(View v) - { + public void onClick(View v) { LayoutInflater inflater1 = LayoutInflater.from(mCtx); - View layout = inflater1.inflate(R.layout.custom_toast_success, - (ViewGroup) v.findViewById(R.id.custom_toast_container)); + View layout = inflater1.inflate(R.layout.custom_toast_info, v.findViewById(R.id.custom_toast_container)); TextView text = layout.findViewById(R.id.toastText); text.setText(infoText); diff --git a/app/src/main/java/org/mian/gitnex/interfaces/ApiInterface.java b/app/src/main/java/org/mian/gitnex/interfaces/ApiInterface.java index 6d109af7..dcf3ab38 100644 --- a/app/src/main/java/org/mian/gitnex/interfaces/ApiInterface.java +++ b/app/src/main/java/org/mian/gitnex/interfaces/ApiInterface.java @@ -7,6 +7,7 @@ import org.mian.gitnex.models.Collaborators; import org.mian.gitnex.models.Commits; import org.mian.gitnex.models.CreateIssue; import org.mian.gitnex.models.CreateLabel; +import org.mian.gitnex.models.CreatePullRequest; import org.mian.gitnex.models.DeleteFile; import org.mian.gitnex.models.EditFile; import org.mian.gitnex.models.Emails; @@ -19,6 +20,7 @@ import org.mian.gitnex.models.Labels; import org.mian.gitnex.models.MergePullRequest; import org.mian.gitnex.models.Milestones; import org.mian.gitnex.models.NewFile; +import org.mian.gitnex.models.NotificationCount; import org.mian.gitnex.models.NotificationThread; import org.mian.gitnex.models.OrgOwner; import org.mian.gitnex.models.Organization; @@ -93,7 +95,7 @@ public interface ApiInterface { Call markNotificationThreadsAsRead(@Header("Authorization") String token, @Query("last_read_at") String last_read_at, @Query("all") Boolean all, @Query("status-types") String[] statusTypes, @Query("to-status") String toStatus); @GET("notifications/new") // Check if unread notifications exist - Call checkUnreadNotifications(@Header("Authorization") String token); + Call checkUnreadNotifications(@Header("Authorization") String token); @GET("notifications/threads/{id}") // Get notification thread by ID Call getNotificationThread(@Header("Authorization") String token, @Path("id") Integer id); @@ -264,7 +266,10 @@ public interface ApiInterface { Call> getRepoWatchers(@Header("Authorization") String token, @Path("owner") String ownerName, @Path("repo") String repoName); @GET("repos/search") // get all the repos which match the query string - Call queryRepos(@Header("Authorization") String token, @Query("q") String searchKeyword, @Query("private") Boolean repoTypeInclude, @Query("sort") String sort, @Query("order") String order, @Query("limit") int limit); + Call queryRepos(@Header("Authorization") String token, @Query("q") String searchKeyword, @Query("private") Boolean repoTypeInclude, @Query("sort") String sort, @Query("order") String order, @Query("topic") boolean topic, @Query("includeDesc") boolean includeDesc, @Query("template") boolean template, @Query("archived") boolean archived, @Query("limit") int limit, @Query("page") int page); + + @GET("repos/issues/search") // get all the issues which match the query string + Call> queryIssues(@Header("Authorization") String token, @Query("q") String searchKeyword, @Query("type") String type, @Query("state") String state, @Query("page") int page); @POST("repos/{owner}/{repo}/contents/{file}") // create new file Call createNewFile(@Header("Authorization") String token, @Path("owner") String ownerName, @Path("repo") String repoName, @Path("file") String fileName, @Body NewFile jsonStr); @@ -320,6 +325,9 @@ public interface ApiInterface { @POST("repos/{owner}/{repo}/pulls/{index}/merge") // merge a pull request Call mergePullRequest(@Header("Authorization") String token, @Path("owner") String ownerName, @Path("repo") String repoName, @Path("index") int index, @Body MergePullRequest jsonStr); + @POST("repos/{owner}/{repo}/pulls") // create a pull request + Call createPullRequest(@Header("Authorization") String token, @Path("owner") String ownerName, @Path("repo") String repoName, @Body CreatePullRequest jsonStr); + @GET("repos/{owner}/{repo}/commits") // get all commits Call> getRepositoryCommits(@Header("Authorization") String token, @Path("owner") String owner, @Path("repo") String repo, @Query("page") int page, @Query("sha") String branchName, @Query("limit") int limit); diff --git a/app/src/main/java/org/mian/gitnex/models/CreatePullRequest.java b/app/src/main/java/org/mian/gitnex/models/CreatePullRequest.java new file mode 100644 index 00000000..91c4b074 --- /dev/null +++ b/app/src/main/java/org/mian/gitnex/models/CreatePullRequest.java @@ -0,0 +1,36 @@ +package org.mian.gitnex.models; + +import java.util.ArrayList; + +/** + * Author M M Arif + */ + +public class CreatePullRequest { + + private String title; + private String body; + private String assignee; + private String base; + private String head; + private int milestone; + private String due_date; + private String message; + + private ArrayList assignees; + private ArrayList labels; + + public CreatePullRequest(String title, String body, String assignee, String base, String head, int milestone, String due_date, ArrayList assignees, ArrayList labels) { + + this.title = title; + this.body = body; + this.assignee = assignee; + this.base = base; // merge into branch + this.head = head; // pull from branch + this.milestone = milestone; + this.due_date = due_date; + this.assignees = assignees; + this.labels = labels; + } + +} diff --git a/app/src/main/java/org/mian/gitnex/models/Issues.java b/app/src/main/java/org/mian/gitnex/models/Issues.java index 940592a8..b5269176 100644 --- a/app/src/main/java/org/mian/gitnex/models/Issues.java +++ b/app/src/main/java/org/mian/gitnex/models/Issues.java @@ -27,6 +27,7 @@ public class Issues { private pullRequestObject pull_request; private milestoneObject milestone; private List assignees; + private repositoryObject repository; public Issues(String body) { this.body = body; @@ -193,6 +194,35 @@ public class Issues { } } + public static class repositoryObject { + + private int id; + private String full_name; + private String name; + private String owner; + + public int getId() { + + return id; + } + + public String getFull_name() { + + return full_name; + } + + public String getName() { + + return name; + } + + public String getOwner() { + + return owner; + } + + } + public int getId() { return id; } @@ -267,4 +297,9 @@ public class Issues { this.html_url = html_url; } + public repositoryObject getRepository() { + + return repository; + } + } diff --git a/app/src/main/java/org/mian/gitnex/models/Labels.java b/app/src/main/java/org/mian/gitnex/models/Labels.java index 0569a8f9..87ca1aaa 100644 --- a/app/src/main/java/org/mian/gitnex/models/Labels.java +++ b/app/src/main/java/org/mian/gitnex/models/Labels.java @@ -21,6 +21,11 @@ public class Labels { this.labels = labels; } + public Labels(int id, String name) { + this.id = id; + this.name = name; + } + public int getId() { return id; } diff --git a/app/src/main/java/org/mian/gitnex/models/NotificationCount.java b/app/src/main/java/org/mian/gitnex/models/NotificationCount.java new file mode 100644 index 00000000..840a00cb --- /dev/null +++ b/app/src/main/java/org/mian/gitnex/models/NotificationCount.java @@ -0,0 +1,19 @@ +package org.mian.gitnex.models; + +import com.google.gson.annotations.SerializedName; + +/** + * Author M M Arif + */ + +public class NotificationCount { + + @SerializedName("new") + private int counter; + + public int getCounter() { + + return counter; + } + +} diff --git a/app/src/main/res/drawable/app_logo_background.xml b/app/src/main/res/drawable/app_logo_background.xml index fd4d71c7..e009ebe7 100644 --- a/app/src/main/res/drawable/app_logo_background.xml +++ b/app/src/main/res/drawable/app_logo_background.xml @@ -1,46 +1,78 @@ + - - - - - - - - - - + android:viewportWidth="108" + android:viewportHeight="108"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_create_pr.xml b/app/src/main/res/layout/activity_create_pr.xml new file mode 100644 index 00000000..72dae40a --- /dev/null +++ b/app/src/main/res/layout/activity_create_pr.xml @@ -0,0 +1,246 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +