diff --git a/README.md b/README.md index 6c0d7299..abc4842c 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,10 @@ JSON Configuration: GitNex from version **12.0.0** supports HTTP Basic Authentication for servers behind reverse proxies (nginx, Apache). [Read the Wiki](https://codeberg.org/gitnex/GitNex/wiki/HTTP-Basic-Auth-for-Reverse-Proxies) +## Self-signed certificates + +GitNex supports self-signed certificates. You can read more [here](https://codeberg.org/gitnex/GitNex/wiki/FAQ#does-gitnex-support-self-signed-certificates). + ## Links - [Website](https://gitnex.com) diff --git a/app/build.gradle b/app/build.gradle index 4956a13d..29de85ea 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,5 +1,5 @@ plugins { - id "com.diffplug.spotless" version "8.1.0" + id "com.diffplug.spotless" version "8.2.1" } apply plugin: 'com.android.application' @@ -8,8 +8,8 @@ android { applicationId = "org.mian.gitnex" minSdkVersion 26 targetSdkVersion 36 - versionCode = 1200 - versionName = "12.0.0" + versionCode = 1295 + versionName = "13.0.0-dev" multiDexEnabled = true testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" compileSdk = 36 @@ -82,8 +82,8 @@ dependencies { implementation 'androidx.viewpager2:viewpager2:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:2.2.1' implementation "androidx.legacy:legacy-support-v4:1.0.0" - implementation "androidx.navigation:navigation-fragment:2.9.6" - implementation "androidx.navigation:navigation-ui:2.9.6" + implementation "androidx.navigation:navigation-fragment:2.9.7" + implementation "androidx.navigation:navigation-ui:2.9.7" implementation "androidx.lifecycle:lifecycle-viewmodel:2.10.0" testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.3.0' @@ -96,7 +96,7 @@ dependencies { implementation 'com.squareup.retrofit2:converter-scalars:3.0.0' implementation 'com.squareup.okhttp3:logging-interceptor:5.3.2' implementation 'org.ocpsoft.prettytime:prettytime:5.0.7.Final' - implementation "com.github.skydoves:colorpickerview:2.3.0" + implementation "com.github.skydoves:colorpickerview:2.4.0" implementation "io.noties.markwon:core:4.6.2" implementation "io.noties.markwon:ext-latex:4.6.2" implementation "io.noties.markwon:ext-strikethrough:4.6.2" @@ -125,13 +125,13 @@ dependencies { implementation 'ch.acra:acra-notification:5.13.1' implementation 'androidx.room:room-runtime:2.8.4' annotationProcessor 'androidx.room:room-compiler:2.8.4' - implementation "androidx.work:work-runtime:2.11.0" + implementation "androidx.work:work-runtime:2.11.1" implementation "io.mikael:urlbuilder:2.0.9" implementation "org.codeberg.gitnex-garage:emoji-java:v5.1.2" //noinspection GradleDependency coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.5" implementation 'androidx.biometric:biometric:1.1.0' - //noinspection GradleDependency + //noinspection NewerVersionAvailable,GradleDependency implementation 'com.github.chrisvest:stormpot:2.4.2' implementation 'androidx.browser:browser:1.9.0' implementation 'com.google.android.flexbox:flexbox:3.0.0' diff --git a/app/src/main/java/org/mian/gitnex/activities/CreateRepoActivity.java b/app/src/main/java/org/mian/gitnex/activities/CreateRepoActivity.java index 4abd5e1f..5dbe3a0d 100644 --- a/app/src/main/java/org/mian/gitnex/activities/CreateRepoActivity.java +++ b/app/src/main/java/org/mian/gitnex/activities/CreateRepoActivity.java @@ -4,6 +4,7 @@ import android.os.Handler; import android.os.Looper; import android.view.MenuItem; +import android.view.View; import android.widget.ArrayAdapter; import androidx.annotation.NonNull; import java.util.ArrayList; @@ -16,6 +17,8 @@ import org.gitnex.tea4j.v2.models.Organization; import org.gitnex.tea4j.v2.models.Repository; import org.mian.gitnex.R; +import org.mian.gitnex.api.clients.ApiRetrofitClient; +import org.mian.gitnex.api.models.license.License; import org.mian.gitnex.clients.RetrofitClient; import org.mian.gitnex.databinding.ActivityCreateRepoBinding; import org.mian.gitnex.helpers.AlertDialogs; @@ -25,21 +28,23 @@ import retrofit2.Callback; /** - * @author M M Arif + * @author mmarif */ public class CreateRepoActivity extends BaseActivity { - // https://github.com/go-gitea/gitea/blob/52cfd2743c0e85b36081cf80a850e6a5901f1865/models/repo.go#L964-L967 final List reservedRepoNames = Arrays.asList(".", ".."); final Pattern reservedRepoPatterns = Pattern.compile("\\.(git|wiki)$"); List organizationsList = new ArrayList<>(); List issueLabelsList = new ArrayList<>(); - List licenseList = new ArrayList<>(); + List licenseDisplayList = new ArrayList<>(); + List licenseKeyList = new ArrayList<>(); + List gitignoreList = new ArrayList<>(); private ActivityCreateRepoBinding activityCreateRepoBinding; private String loginUid; private String selectedOwner; private String selectedIssueLabels; private String selectedLicense; + private String selectedGitignore; @Override public void onCreate(Bundle savedInstanceState) { @@ -59,14 +64,14 @@ public void onCreate(Bundle savedInstanceState) { MenuItem markdown = activityCreateRepoBinding.topAppBar.getMenu().getItem(1); markdown.setVisible(false); - String[] licenses = getResources().getStringArray(R.array.licenses); - Collections.addAll(licenseList, licenses); getLicenses(); issueLabelsList.add(getString(R.string.advanced)); issueLabelsList.add(getString(R.string.defaultText)); getIssueLabels(); + getGitignoreTemplates(); + activityCreateRepoBinding.topAppBar.setOnMenuItemClickListener( menuItem -> { int id = menuItem.getItemId(); @@ -170,6 +175,10 @@ private void createNewRepository( createRepository.setTemplate(repoAsTemplate); createRepository.setLicense(selectedLicense); + if (selectedGitignore != null && !selectedGitignore.isEmpty()) { + createRepository.setGitignores(selectedGitignore); + } + Call call; if (selectedOwner.equals(loginUid)) { @@ -237,15 +246,127 @@ private void getIssueLabels() { } private void getLicenses() { + Call> call = ApiRetrofitClient.getInstance(ctx).getLicenses(); + + call.enqueue( + new Callback<>() { + @Override + public void onResponse( + @NonNull Call> call, + @NonNull retrofit2.Response> response) { + if (response.isSuccessful() && response.body() != null) { + List licenses = response.body(); + if (!licenses.isEmpty()) { + licenseDisplayList.clear(); + licenseKeyList.clear(); + + licenseDisplayList.add(getString(R.string.no_license)); + licenseKeyList.add(""); + + for (License license : licenses) { + licenseDisplayList.add(license.getName()); + licenseKeyList.add(license.getKey()); + } + + setupLicenseDropdown(); + } else { + hideLicenseDropdown(); + } + } else { + hideLicenseDropdown(); + } + } + + @Override + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + hideLicenseDropdown(); + } + }); + } + + private void setupLicenseDropdown() { ArrayAdapter adapter = new ArrayAdapter<>( - CreateRepoActivity.this, R.layout.list_spinner_items, licenseList); + CreateRepoActivity.this, R.layout.list_spinner_items, licenseDisplayList); activityCreateRepoBinding.licenses.setAdapter(adapter); activityCreateRepoBinding.licenses.setOnItemClickListener( - (parent, view, position, id) -> selectedLicense = licenseList.get(position)); + (parent, view, position, id) -> { + if (position == 0) { + selectedLicense = null; + } else { + selectedLicense = licenseKeyList.get(position); + } + }); + + activityCreateRepoBinding.licenses.setText(licenseDisplayList.get(0), false); + selectedLicense = null; + } + + private void hideLicenseDropdown() { + activityCreateRepoBinding.licenseFrame.setVisibility(View.GONE); + selectedLicense = null; + } + + private void getGitignoreTemplates() { + Call> call = ApiRetrofitClient.getInstance(ctx).getGitignoreTemplates(); + + call.enqueue( + new Callback<>() { + @Override + public void onResponse( + @NonNull Call> call, + @NonNull retrofit2.Response> response) { + + if (response.isSuccessful() && response.body() != null) { + List templates = response.body(); + if (!templates.isEmpty()) { + Collections.sort(templates); + gitignoreList.addAll(templates); + + gitignoreList.add(0, getString(R.string.no_template)); + + setupGitignoreDropdown(); + } else { + hideGitignoreDropdown(); + } + } else { + hideGitignoreDropdown(); + } + } + + @Override + public void onFailure(@NonNull Call> call, @NonNull Throwable t) { + hideGitignoreDropdown(); + } + }); + } + + private void setupGitignoreDropdown() { + ArrayAdapter adapter = + new ArrayAdapter<>( + CreateRepoActivity.this, R.layout.list_spinner_items, gitignoreList); + + activityCreateRepoBinding.gitignoreTemplates.setAdapter(adapter); + + activityCreateRepoBinding.gitignoreTemplates.setOnItemClickListener( + (parent, view, position, id) -> { + if (position == 0) { + selectedGitignore = null; + } else { + selectedGitignore = gitignoreList.get(position); + } + }); + + activityCreateRepoBinding.gitignoreTemplates.setText(gitignoreList.get(0), false); + selectedGitignore = null; + } + + private void hideGitignoreDropdown() { + activityCreateRepoBinding.gitignoreFrame.setVisibility(View.GONE); + selectedGitignore = null; } private void getOrganizations(final String userLogin) { 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 afc0627f..88ac66e0 100644 --- a/app/src/main/java/org/mian/gitnex/activities/MainActivity.java +++ b/app/src/main/java/org/mian/gitnex/activities/MainActivity.java @@ -16,6 +16,8 @@ import androidx.navigation.NavOptions; import androidx.navigation.fragment.NavHostFragment; import androidx.navigation.ui.NavigationUI; +import androidx.work.OneTimeWorkRequest; +import androidx.work.WorkManager; import com.bumptech.glide.Glide; import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.google.android.material.badge.BadgeDrawable; @@ -44,6 +46,9 @@ import org.mian.gitnex.helpers.ChangeLog; import org.mian.gitnex.helpers.TinyDB; import org.mian.gitnex.helpers.Toasty; +import org.mian.gitnex.notifications.Notifications; +import org.mian.gitnex.notifications.NotificationsBadge; +import org.mian.gitnex.notifications.NotificationsBadgeWorker; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; @@ -54,7 +59,7 @@ public class MainActivity extends BaseActivity implements NotificationsFragment.NotificationCountListener { - private ActivityMainBinding binding; + public ActivityMainBinding binding; private TinyDB tinyDB; private NavController navController; private boolean noConnection; @@ -112,6 +117,13 @@ public void onCreate(Bundle savedInstanceState) { } } + loadSavedBadgeCount(); + if (Boolean.parseBoolean( + AppDatabaseSettings.getSettingsValue( + this, AppDatabaseSettings.APP_NOTIFICATIONS_KEY))) { + Notifications.startBadgeWorker(this); + } + setSupportActionBar(binding.toolbar); binding.toolbar.setVisibility(View.GONE); binding.toolbar.invalidate(); @@ -277,6 +289,17 @@ public void onUserAccountsLoaded() {} }); DetailFragment.refProfile = false; } + + getNotificationsCount(); + loadSavedBadgeCount(); + WorkManager.getInstance(this) + .enqueue(new OneTimeWorkRequest.Builder(NotificationsBadgeWorker.class).build()); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + Notifications.stopBadgeWorker(this); } @SuppressLint("NotifyDataSetChanged") @@ -498,7 +521,7 @@ private void handleLaunchFragments(Intent mainIntent) { navController.navigate(R.id.repositoriesFragment, null, navOptions); break; case "org": - navController.navigate(R.id.action_to_organizations, null, navOptions); + navController.navigate(R.id.organizationsFragment, null, navOptions); break; case "notification": binding.toolbarTitle.setText( @@ -514,7 +537,7 @@ private void handleLaunchFragments(Intent mainIntent) { startActivity(intentProfile); break; case "admin": - navController.navigate(R.id.action_to_administration, null, navOptions); + navController.navigate(R.id.administrationFragment, null, navOptions); break; } return; @@ -538,27 +561,35 @@ private void handleLaunchFragments(Intent mainIntent) { } private void navigateToDefaultFragment() { + int homeScreenValue; + try { + homeScreenValue = + Integer.parseInt( + AppDatabaseSettings.getSettingsValue( + this, AppDatabaseSettings.APP_HOME_SCREEN_KEY)); + } catch (NumberFormatException e) { + homeScreenValue = 0; + } + NavOptions navOptions = new NavOptions.Builder() - .setPopUpTo(R.id.nav_graph, true) + .setPopUpTo(navController.getGraph().getId(), true) .setLaunchSingleTop(true) .build(); - switch (Integer.parseInt( - AppDatabaseSettings.getSettingsValue( - this, AppDatabaseSettings.APP_HOME_SCREEN_KEY))) { + switch (homeScreenValue) { case 1: binding.toolbarTitle.setText(getResources().getString(R.string.navMyRepos)); - navController.navigate(R.id.nav_graph, null, navOptions); + navController.navigate(R.id.myRepositoriesFragment, null, navOptions); break; case 2: binding.toolbarTitle.setText( getResources().getString(R.string.pageTitleStarredRepos)); - navController.navigate(R.id.action_to_starredRepositories, null, navOptions); + navController.navigate(R.id.starredRepositoriesFragment, null, navOptions); break; case 3: binding.toolbarTitle.setText(getResources().getString(R.string.navOrg)); - navController.navigate(R.id.action_to_organizations, null, navOptions); + navController.navigate(R.id.organizationsFragment, null, navOptions); break; case 4: binding.toolbarTitle.setText(getResources().getString(R.string.navRepos)); @@ -575,15 +606,15 @@ private void navigateToDefaultFragment() { break; case 7: binding.toolbarTitle.setText(getResources().getString(R.string.navMyIssues)); - navController.navigate(R.id.action_to_myIssues, null, navOptions); + navController.navigate(R.id.myIssuesFragment, null, navOptions); break; case 8: binding.toolbarTitle.setText(getResources().getString(R.string.navMostVisited)); - navController.navigate(R.id.action_to_mostVisitedRepos, null, navOptions); + navController.navigate(R.id.mostVisitedReposFragment, null, navOptions); break; case 9: binding.toolbarTitle.setText(getResources().getString(R.string.navNotes)); - navController.navigate(R.id.action_to_notes, null, navOptions); + navController.navigate(R.id.notesFragment, null, navOptions); break; case 10: binding.toolbarTitle.setText(getResources().getString(R.string.activities)); @@ -592,7 +623,7 @@ private void navigateToDefaultFragment() { case 11: binding.toolbarTitle.setText( getResources().getString(R.string.navWatchedRepositories)); - navController.navigate(R.id.action_to_watchedRepositories, null, navOptions); + navController.navigate(R.id.watchedRepositoriesFragment, null, navOptions); break; default: navController.navigate(R.id.homeDashboardFragment, null, navOptions); @@ -600,6 +631,37 @@ private void navigateToDefaultFragment() { } } + private void loadSavedBadgeCount() { + TinyDB tinyDB = TinyDB.getInstance(this); + int currentAccountId = tinyDB.getInt("currentActiveAccountId", -1); + + if (currentAccountId > 0) { + int savedCount = NotificationsBadge.getBadgeCount(this, currentAccountId); + if (savedCount > 0) { + updateBadgeUI(savedCount); + } + } + } + + private void updateBadgeUI(int count) { + runOnUiThread( + () -> { + if (count > 0) { + BadgeDrawable badge = + binding.bottomNavigation.getOrCreateBadge( + R.id.notificationsFragment); + badge.setNumber(count); + badge.setBackgroundColor(getThemeColor(R.attr.primaryTextColor)); + badge.setBadgeTextColor(getThemeColor(R.attr.materialCardBackgroundColor)); + badge.setVisible(true); + } else { + if (binding.bottomNavigation.getBadge(R.id.notificationsFragment) != null) { + binding.bottomNavigation.removeBadge(R.id.notificationsFragment); + } + } + }); + } + public void getNotificationsCount() { Call call = RetrofitClient.getApiInterface(this).notifyNewAvailable(); call.enqueue( @@ -608,29 +670,31 @@ public void getNotificationsCount() { public void onResponse( @NonNull Call call, @NonNull Response response) { - NotificationCount notificationCount = response.body(); - if (response.code() == 200 - && notificationCount != null - && notificationCount.getNew() > 0) { - BadgeDrawable badge = - binding.bottomNavigation.getOrCreateBadge( - R.id.notificationsFragment); - badge.setNumber(Math.toIntExact(notificationCount.getNew())); - badge.setBackgroundColor(getThemeColor(R.attr.primaryTextColor)); - badge.setBadgeTextColor( - getThemeColor(R.attr.materialCardBackgroundColor)); + if (response.code() == 200 && response.body() != null) { + int newCount = Math.toIntExact(response.body().getNew()); + + TinyDB tinyDB = TinyDB.getInstance(MainActivity.this); + int accountId = tinyDB.getInt("currentActiveAccountId", -1); + if (accountId > 0) { + NotificationsBadge.saveBadgeCount( + MainActivity.this, accountId, newCount); + } + + updateBadgeUI(newCount); } else { - binding.bottomNavigation.removeBadge(R.id.notificationsFragment); + updateBadgeUI(0); } } @Override public void onFailure( - @NonNull Call call, @NonNull Throwable t) {} + @NonNull Call call, @NonNull Throwable t) { + loadSavedBadgeCount(); + } }); } - private int getThemeColor(int attr) { + public int getThemeColor(int attr) { TypedValue typedValue = new TypedValue(); getTheme().resolveAttribute(attr, typedValue, true); return typedValue.data; diff --git a/app/src/main/java/org/mian/gitnex/api/clients/ApiInterface.java b/app/src/main/java/org/mian/gitnex/api/clients/ApiInterface.java index d6f9a9ca..a44cedaf 100644 --- a/app/src/main/java/org/mian/gitnex/api/clients/ApiInterface.java +++ b/app/src/main/java/org/mian/gitnex/api/clients/ApiInterface.java @@ -2,6 +2,7 @@ import java.util.List; import org.mian.gitnex.api.models.contents.RepoGetContentsList; +import org.mian.gitnex.api.models.license.License; import org.mian.gitnex.api.models.settings.RepositoryGlobal; import org.mian.gitnex.api.models.topics.Topics; import retrofit2.Call; @@ -44,4 +45,10 @@ Call deleteRepoTopic( @GET("settings/repository") // get repository global settings Call getRepositoryGlobalSettings(); + + @GET("gitignore/templates") // get all gitignore templates + Call> getGitignoreTemplates(); + + @GET("licenses") // get all licenses + Call> getLicenses(); } diff --git a/app/src/main/java/org/mian/gitnex/api/models/license/License.java b/app/src/main/java/org/mian/gitnex/api/models/license/License.java new file mode 100644 index 00000000..eef8dbb5 --- /dev/null +++ b/app/src/main/java/org/mian/gitnex/api/models/license/License.java @@ -0,0 +1,50 @@ +package org.mian.gitnex.api.models.license; + +import com.google.gson.annotations.SerializedName; + +/** + * @author mmarif + */ +public class License { + + @SerializedName("key") + private String key; + + @SerializedName("name") + private String name; + + @SerializedName("url") + private String url; + + public License() {} + + public License(String key, String name, String url) { + this.key = key; + this.name = name; + this.url = url; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } +} 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 cf1ac2c8..fdd21502 100644 --- a/app/src/main/java/org/mian/gitnex/fragments/NotificationsFragment.java +++ b/app/src/main/java/org/mian/gitnex/fragments/NotificationsFragment.java @@ -129,14 +129,12 @@ public void onScrollStateChanged( .enqueue( (SimpleCallback>) (call, voidResponse) -> { + View fragmentRootView = getView(); if (voidResponse.isPresent() && voidResponse.get().isSuccessful()) { SnackBar.success( context, - requireActivity() - .findViewById( - android.R.id - .content), + fragmentRootView, getString( R.string .markedNotificationsAsRead)); @@ -153,12 +151,7 @@ public void onScrollStateChanged( "205")) { SnackBar.success( context, - requireActivity() - .findViewById( - android - .R - .id - .content), + fragmentRootView, getString( R.string .markedNotificationsAsRead)); @@ -175,12 +168,7 @@ public void onScrollStateChanged( () -> SnackBar.error( context, - requireActivity() - .findViewById( - android - .R - .id - .content), + fragmentRootView, getString( R .string 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 14417124..637a3744 100644 --- a/app/src/main/java/org/mian/gitnex/fragments/RepoInfoFragment.java +++ b/app/src/main/java/org/mian/gitnex/fragments/RepoInfoFragment.java @@ -68,6 +68,7 @@ public class RepoInfoFragment extends Fragment { private LinearLayout pageContent; private FragmentRepoInfoBinding binding; private RepositoryContext repository; + boolean isAdmin; public RepoInfoFragment() {} @@ -96,6 +97,8 @@ public View onCreateView( setRepoInfo(locale); + isAdmin = repository.getPermissions() != null && repository.getPermissions().isAdmin(); + loadRepoTopics(); binding.addTopicChip.setOnClickListener(v -> showAddTopicDialog()); @@ -514,19 +517,37 @@ public void onResponse( if (isAdded()) { switch (response.code()) { case 200: + List topics = new ArrayList<>(); if (response.body() != null && !response.body().getTopics().isEmpty()) { + topics = response.body().getTopics(); + } + + boolean hasTopics = !topics.isEmpty(); + + if (hasTopics || isAdmin) { binding.repoTopicsContainer.setVisibility(View.VISIBLE); - displayTopics(response.body().getTopics()); + displayTopics(topics); } else { binding.repoTopicsContainer.setVisibility(View.GONE); } break; case 401: AlertDialogs.authorizationTokenRevokedDialog(ctx); + if (isAdmin) { + binding.repoTopicsContainer.setVisibility(View.VISIBLE); + displayTopics(new ArrayList<>()); + } else { + binding.repoTopicsContainer.setVisibility(View.GONE); + } break; default: - binding.repoTopicsContainer.setVisibility(View.GONE); + if (isAdmin) { + binding.repoTopicsContainer.setVisibility(View.VISIBLE); + displayTopics(new ArrayList<>()); + } else { + binding.repoTopicsContainer.setVisibility(View.GONE); + } break; } } @@ -535,8 +556,13 @@ public void onResponse( @Override public void onFailure(@NonNull Call call, @NonNull Throwable t) { if (isAdded()) { + if (isAdmin) { + binding.repoTopicsContainer.setVisibility(View.VISIBLE); + displayTopics(new ArrayList<>()); + } else { + binding.repoTopicsContainer.setVisibility(View.GONE); + } Toasty.error(ctx, ctx.getString(R.string.errorLoadingTopics)); - binding.repoTopicsContainer.setVisibility(View.GONE); } } }); @@ -573,15 +599,25 @@ private void displayTopics(List topics) { plusChip.setStateListAnimator(null); plusChip.setElevation(0f); - binding.repoTopicsChipGroup.addView(plusChip); + if (isAdmin) { + binding.repoTopicsChipGroup.addView(plusChip); + } } private Chip createTopicChip(String topic, int backgroundColor) { Chip chip = new Chip(ctx); + chip.setCheckable(false); + chip.setClickable(false); + chip.setSelected(false); chip.setText(topic); - chip.setCloseIconVisible(true); - chip.setCloseIconTint( - ColorStateList.valueOf(ContextCompat.getColor(ctx, R.color.colorRed))); + chip.setCloseIconVisible(isAdmin); + + if (isAdmin) { + chip.setCloseIconTint( + ColorStateList.valueOf(ContextCompat.getColor(ctx, R.color.colorRed))); + chip.setOnCloseIconClickListener(v -> deleteTopic(topic)); + } + chip.setChipBackgroundColor(ColorStateList.valueOf(backgroundColor)); chip.setTextColor(isLightColor(backgroundColor) ? Color.BLACK : Color.WHITE); @@ -593,8 +629,6 @@ private Chip createTopicChip(String topic, int backgroundColor) { getResources().getDimension(R.dimen.dimen8dp)) .build()); - chip.setOnCloseIconClickListener(v -> deleteTopic(topic)); - return chip; } diff --git a/app/src/main/java/org/mian/gitnex/notifications/Notifications.java b/app/src/main/java/org/mian/gitnex/notifications/Notifications.java index 5dafde2d..2144119a 100644 --- a/app/src/main/java/org/mian/gitnex/notifications/Notifications.java +++ b/app/src/main/java/org/mian/gitnex/notifications/Notifications.java @@ -67,6 +67,27 @@ public static void stopWorker(Context context) { WorkManager.getInstance(context).cancelAllWorkByTag(Constants.notificationsWorkerId); } + public static void startBadgeWorker(Context context) { + Constraints constraints = + new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build(); + + PeriodicWorkRequest badgeWorkRequest = + new PeriodicWorkRequest.Builder( + NotificationsBadgeWorker.class, 15, TimeUnit.MINUTES) + .setConstraints(constraints) + .build(); + + WorkManager.getInstance(context) + .enqueueUniquePeriodicWork( + "notification_badge_updates", + ExistingPeriodicWorkPolicy.KEEP, + badgeWorkRequest); + } + + public static void stopBadgeWorker(Context context) { + WorkManager.getInstance(context).cancelUniqueWork("notification_badge_updates"); + } + public static void startWorker(Context context) { int delay; diff --git a/app/src/main/java/org/mian/gitnex/notifications/NotificationsBadge.java b/app/src/main/java/org/mian/gitnex/notifications/NotificationsBadge.java new file mode 100644 index 00000000..df99b95c --- /dev/null +++ b/app/src/main/java/org/mian/gitnex/notifications/NotificationsBadge.java @@ -0,0 +1,37 @@ +package org.mian.gitnex.notifications; + +import android.content.Context; +import android.content.SharedPreferences; +import org.mian.gitnex.helpers.TinyDB; + +/** + * @author mmarif + */ +public class NotificationsBadge { + + private static final String PREFS_NAME = "notification_badge_prefs"; + private static final String KEY_PREFIX = "badge_count_"; + + public static void saveBadgeCount(Context context, int accountId, int count) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + prefs.edit().putInt(KEY_PREFIX + accountId, count).apply(); + } + + public static int getBadgeCount(Context context, int accountId) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + return prefs.getInt(KEY_PREFIX + accountId, 0); + } + + public static void clearAllBadgeCounts(Context context) { + SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + prefs.edit().clear().apply(); + } + + public static void updateBadgeUI(Context context, int count) { + TinyDB tinyDB = TinyDB.getInstance(context); + int accountId = tinyDB.getInt("currentActiveAccountId", -1); + if (accountId > 0) { + saveBadgeCount(context, accountId, count); + } + } +} diff --git a/app/src/main/java/org/mian/gitnex/notifications/NotificationsBadgeWorker.java b/app/src/main/java/org/mian/gitnex/notifications/NotificationsBadgeWorker.java new file mode 100644 index 00000000..508302c4 --- /dev/null +++ b/app/src/main/java/org/mian/gitnex/notifications/NotificationsBadgeWorker.java @@ -0,0 +1,78 @@ +package org.mian.gitnex.notifications; + +import android.content.Context; +import androidx.annotation.NonNull; +import androidx.work.Worker; +import androidx.work.WorkerParameters; +import org.gitnex.tea4j.v2.models.NotificationCount; +import org.mian.gitnex.clients.RetrofitClient; +import org.mian.gitnex.database.api.BaseApi; +import org.mian.gitnex.database.api.UserAccountsApi; +import org.mian.gitnex.database.models.UserAccount; +import org.mian.gitnex.helpers.TinyDB; +import retrofit2.Call; +import retrofit2.Response; + +/** + * @author mmarif + */ +public class NotificationsBadgeWorker extends Worker { + + private final Context context; + + public NotificationsBadgeWorker(@NonNull Context context, @NonNull WorkerParameters params) { + super(context, params); + this.context = context; + } + + @NonNull @Override + public Result doWork() { + updateNotificationBadgeForAllAccounts(); + return Result.success(); + } + + private void updateNotificationBadgeForAllAccounts() { + UserAccountsApi userAccountsApi = BaseApi.getInstance(context, UserAccountsApi.class); + if (userAccountsApi == null) return; + + for (UserAccount account : userAccountsApi.loggedInUserAccounts()) { + updateBadgeForAccount(account); + } + } + + private void updateBadgeForAccount(UserAccount account) { + try { + Call call = + RetrofitClient.getApiInterface( + context, + account.getInstanceUrl(), + "token " + account.getToken(), + null) + .notifyNewAvailable(); + + Response response = call.execute(); + + if (response.isSuccessful() && response.body() != null) { + int newCount = Math.toIntExact(response.body().getNew()); + + NotificationsBadge.saveBadgeCount(context, account.getAccountId(), newCount); + + if (isCurrentActiveAccount(account)) { + NotificationsBadge.updateBadgeUI(context, newCount); + } + } else { + NotificationsBadge.saveBadgeCount(context, account.getAccountId(), 0); + if (isCurrentActiveAccount(account)) { + NotificationsBadge.updateBadgeUI(context, 0); + } + } + } catch (Exception ignored) { + } + } + + private boolean isCurrentActiveAccount(UserAccount account) { + TinyDB tinyDB = TinyDB.getInstance(context); + int currentAccountId = tinyDB.getInt("currentActiveAccountId", -1); + return account.getAccountId() == currentAccountId; + } +} diff --git a/app/src/main/res/drawable/ic_loader.xml b/app/src/main/res/drawable/ic_loader.xml index 3d7fcd54..51121b5d 100644 --- a/app/src/main/res/drawable/ic_loader.xml +++ b/app/src/main/res/drawable/ic_loader.xml @@ -1,9 +1,14 @@ - - - + + + diff --git a/app/src/main/res/layout/activity_create_repo.xml b/app/src/main/res/layout/activity_create_repo.xml index 2d8f9d32..2921aa0a 100644 --- a/app/src/main/res/layout/activity_create_repo.xml +++ b/app/src/main/res/layout/activity_create_repo.xml @@ -169,6 +169,29 @@ + + + + + + @@ -144,6 +143,7 @@ app:cardCornerRadius="@dimen/dimen16dp" app:cardElevation="@dimen/dimen0dp" app:strokeWidth="@dimen/dimen0dp" + android:layout_marginTop="@dimen/dimen16dp" android:visibility="gone" app:cardBackgroundColor="@android:color/transparent"> diff --git a/app/src/main/res/values/settings.xml b/app/src/main/res/values/settings.xml index 7f5b17e9..c3ac69f6 100644 --- a/app/src/main/res/values/settings.xml +++ b/app/src/main/res/values/settings.xml @@ -130,184 +130,4 @@ @string/pollingDelay45Minutes @string/pollingDelay1Hour - - - 0BSD - AAL - ADSL - AFL-1.1 - AFL-1.1 - AFL-2.0 - AFL-3.0 - AGPL-1.0-only - AGPL-1.0-or-later - AGPL-3.0-only - AGPL-3.0-or-later - AML - APL-1.0 - APSL-1.0 - APSL-2.0 - Adobe-2006 - Aladdin - Apache-1.0 - Apache-1.1 - Apache-2.0 - Artistic-1.0 - Artistic-2.0 - BSD-1-Clause - BSD-2-Clause - BSD-2-Clause-Patent - BSD-2-Clause-Views - BSD-3-Clause - BSD-3-Clause-Attribution - BSD-3-Clause-Clear - BSD-3-Clause-LBNL - BSD-3-Clause-Modification - BSD-3-Clause-No-Military-License - BSD-3-Clause-No-Nuclear-License - BSD-3-Clause-No-Nuclear-License-2014 - BSD-3-Clause-No-Nuclear-Warranty - BSD-3-Clause-Open-MPI - BSD-3-Clause-Sun - BSD-4-Clause-Shortened - BSD-4-Clause-UC - BSD-4.3RENO - BSD-4.3TAHOE - BSD-Advertising-Acknowledgement - BSD-Attribution-HPND-disclaimer - BSD-Protection - BSD-Source-Code - BSD-Systemics - BitTorrent-1.0 - BitTorrent-1.1 - CC-BY-1.0 - CC-BY-2.0 - CC-BY-2.5 - CC-BY-2.5-AU - CC-BY-3.0 - CC-BY-3.0-AT - CC-BY-3.0-DE - CC-BY-4.0 - CC-BY-NC-1.0 - CC-BY-NC-2.0 - CC-BY-NC-2.5 - CC-BY-NC-3.0 - CC-BY-NC-4.0 - Community-Spec-1.0 - Cube - D-FSL-1.0 - DL-DE-BY-2.0 - ECL-1.0 - ECL-2.0 - EFL-1.0 - EFL-2.0 - Elastic-2.0 - FreeBSD-DOC - GD - GFDL-1.1-only - GFDL-1.1-or-later - GFDL-1.2-only - GFDL-1.2-or-later - GFDL-1.3-only - GFDL-1.3-or-later - GPL-1.0-only - GPL-1.0-or-later - GPL-2.0-only - GPL-2.0-or-later - GPL-3.0-interface-exception - GPL-3.0-only - GPL-3.0-or-later - GPL-CC-1.0 - GStreamer-exception-2008 - Glide - HP-1989 - IBM-pibs - ICU - IPL-1.0 - ImageMagick - Intel - Intel-ACPI - Interbase-1.0 - JSON - LAL-1.3 - LGPL-2.0-only - LGPL-2.0-or-later - LGPL-2.1-only - LGPL-2.1-or-later - LGPL-3.0-only - LGPL-3.0-or-later - LGPLLR - LLGPL - LPL-1.0 - LPL-1.02 - LPPL-1.0 - LPPL-1.3a - LPPL-1.3c - Libpng - Linux-OpenIB - MIT - MIT-0 - MIT-CMU - MIT-Festival - MIT-Modern-Variant - MIT-Wu - MIT-advertising - MIT-open-group - MPL-1.0 - MPL-2.0 - MirOS - NASA-1.3 - NTP - Nokia - OCLC-2.0 - OFL-1.0 - OFL-1.1 - OLDAP-1.1 - OLDAP-2.0 - OLDAP-2.8 - OML - OSL-1.0 - OSL-2.0 - OSL-3.0 - OpenSSL - PHP-3.0 - PSF-2.0 - PostgreSQL - Python-2.0 - QPL-1.0 - Qt-GPL-exception-1.0 - Qt-GPL-exception-1.1 - RPL-1.1 - RPL-1.5 - Ruby - SGI-B-1.0 - SGI-B-2.0 - SSH-OpenSSH - Sendmail - TCL - UnixCrypt - Unlicense - Vim - W3C - WTFPL - X11 - XFree86-1.1 - Xdebug-1.03 - Xerox - YPL-1.0 - ZPL-1.1 - ZPL-2.0 - Zed - Zimbra-1.4 - Zlib - bzip2-1.0.6 - copyleft-next-0.3.1 - curl - gnuplot - libpng-2.0 - libselinux-1.0 - w3m - xpp - zlib-acknowledgement - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 99353064..326b1563 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1073,4 +1073,8 @@ Show URL confirmation dialog Show a confirmation dialog before opening external links in Markdown rendered contents + + Gitignore Template + No template + No license diff --git a/app/src/main/res/xml/changelog.xml b/app/src/main/res/xml/changelog.xml index 961cc943..b45d6147 100644 --- a/app/src/main/res/xml/changelog.xml +++ b/app/src/main/res/xml/changelog.xml @@ -1,26 +1,15 @@ - + - HTTP basic authentication (more in README.md) - Pinch to zoom in and out of images in issue/PR/comment popups - PIN/password as a fallback for biometric authentication - URL opening prompt popup (Settings → General) - Issue templates for new issues + DEV - Zig language support in files and the languages bar - Added pagination to labels for the issues filter - Improved pinned issues scrolling - UI improvements across the app + DEV - Fixed bottom navigation colors - Fixed releases and tags view - Fixed topics add button UI - Fixed showing 'no data found' for most visited repositories for a split second - Fixed comment content formatting in Activities + DEV diff --git a/build.gradle b/build.gradle index e57c5dfa..2662ac34 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.11.2' + classpath 'com.android.tools.build:gradle:8.13.2' } } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 495f41ed..2353a707 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Jul 11 23:34:25 PKT 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists