Gitignores, improve notification badge count, fix home screen selection, libs updates (#1573)

closes #1569

Reviewed-on: https://codeberg.org/gitnex/GitNex/pulls/1573
Co-authored-by: M M Arif <mmarif@swatian.com>
Co-committed-by: M M Arif <mmarif@swatian.com>
This commit is contained in:
M M Arif
2026-01-31 20:00:47 +01:00
committed by M M Arif
parent fc3537cbeb
commit d472791cc7
19 changed files with 519 additions and 274 deletions

View File

@@ -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'

View File

@@ -4,6 +4,7 @@ import android.os.Bundle;
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.CreateRepoOption;
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.Call;
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<String> reservedRepoNames = Arrays.asList(".", "..");
final Pattern reservedRepoPatterns = Pattern.compile("\\.(git|wiki)$");
List<String> organizationsList = new ArrayList<>();
List<String> issueLabelsList = new ArrayList<>();
List<String> licenseList = new ArrayList<>();
List<String> licenseDisplayList = new ArrayList<>();
List<String> licenseKeyList = new ArrayList<>();
List<String> 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 class CreateRepoActivity extends BaseActivity {
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 @@ public class CreateRepoActivity extends BaseActivity {
createRepository.setTemplate(repoAsTemplate);
createRepository.setLicense(selectedLicense);
if (selectedGitignore != null && !selectedGitignore.isEmpty()) {
createRepository.setGitignores(selectedGitignore);
}
Call<Repository> call;
if (selectedOwner.equals(loginUid)) {
@@ -237,15 +246,127 @@ public class CreateRepoActivity extends BaseActivity {
}
private void getLicenses() {
Call<List<License>> call = ApiRetrofitClient.getInstance(ctx).getLicenses();
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<List<License>> call,
@NonNull retrofit2.Response<List<License>> response) {
if (response.isSuccessful() && response.body() != null) {
List<License> 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<List<License>> call, @NonNull Throwable t) {
hideLicenseDropdown();
}
});
}
private void setupLicenseDropdown() {
ArrayAdapter<String> 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<List<String>> call = ApiRetrofitClient.getInstance(ctx).getGitignoreTemplates();
call.enqueue(
new Callback<>() {
@Override
public void onResponse(
@NonNull Call<List<String>> call,
@NonNull retrofit2.Response<List<String>> response) {
if (response.isSuccessful() && response.body() != null) {
List<String> 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<List<String>> call, @NonNull Throwable t) {
hideGitignoreDropdown();
}
});
}
private void setupGitignoreDropdown() {
ArrayAdapter<String> 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) {

View File

@@ -16,6 +16,8 @@ import androidx.navigation.NavController;
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.AppUtil;
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 @@ import retrofit2.Response;
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 class MainActivity extends BaseActivity
}
}
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 class MainActivity extends BaseActivity
});
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 @@ public class MainActivity extends BaseActivity
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 @@ public class MainActivity extends BaseActivity
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 @@ public class MainActivity extends BaseActivity
}
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 @@ public class MainActivity extends BaseActivity
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 @@ public class MainActivity extends BaseActivity
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 @@ public class MainActivity extends BaseActivity
}
}
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<NotificationCount> call = RetrofitClient.getApiInterface(this).notifyNewAvailable();
call.enqueue(
@@ -608,29 +670,31 @@ public class MainActivity extends BaseActivity
public void onResponse(
@NonNull Call<NotificationCount> call,
@NonNull Response<NotificationCount> 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<NotificationCount> call, @NonNull Throwable t) {}
@NonNull Call<NotificationCount> 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;

View File

@@ -2,6 +2,7 @@ package org.mian.gitnex.api.clients;
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 @@ public interface ApiInterface {
@GET("settings/repository") // get repository global settings
Call<RepositoryGlobal> getRepositoryGlobalSettings();
@GET("gitignore/templates") // get all gitignore templates
Call<List<String>> getGitignoreTemplates();
@GET("licenses") // get all licenses
Call<List<License>> getLicenses();
}

View File

@@ -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;
}
}

View File

@@ -129,14 +129,12 @@ public class NotificationsFragment extends Fragment
.enqueue(
(SimpleCallback<List<NotificationThread>>)
(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 class NotificationsFragment extends Fragment
"205")) {
SnackBar.success(
context,
requireActivity()
.findViewById(
android
.R
.id
.content),
fragmentRootView,
getString(
R.string
.markedNotificationsAsRead));
@@ -175,12 +168,7 @@ public class NotificationsFragment extends Fragment
() ->
SnackBar.error(
context,
requireActivity()
.findViewById(
android
.R
.id
.content),
fragmentRootView,
getString(
R
.string

View File

@@ -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 class RepoInfoFragment extends Fragment {
setRepoInfo(locale);
isAdmin = repository.getPermissions() != null && repository.getPermissions().isAdmin();
loadRepoTopics();
binding.addTopicChip.setOnClickListener(v -> showAddTopicDialog());
@@ -514,19 +517,37 @@ public class RepoInfoFragment extends Fragment {
if (isAdded()) {
switch (response.code()) {
case 200:
List<String> 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 class RepoInfoFragment extends Fragment {
@Override
public void onFailure(@NonNull Call<Topics> 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 @@ public class RepoInfoFragment extends Fragment {
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 @@ public class RepoInfoFragment extends Fragment {
getResources().getDimension(R.dimen.dimen8dp))
.build());
chip.setOnCloseIconClickListener(v -> deleteTopic(topic));
return chip;
}

View File

@@ -67,6 +67,27 @@ public class Notifications {
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;

View File

@@ -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);
}
}
}

View File

@@ -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<NotificationCount> call =
RetrofitClient.getApiInterface(
context,
account.getInstanceUrl(),
"token " + account.getToken(),
null)
.notifyNewAvailable();
Response<NotificationCount> 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;
}
}

View File

@@ -1,9 +1,14 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="40dp"
android:height="40dp" android:autoMirrored="true"
android:viewportWidth="40" android:viewportHeight="40">
<path android:fillAlpha="0.2" android:fillColor="@color/colorAccent"
android:pathData="M20.201,5.169c-8.254,0 -14.946,6.692 -14.946,14.946c0,8.255 6.692,14.946 14.946,14.946s14.946,-6.691 14.946,-14.946C35.146,11.861 28.455,5.169 20.201,5.169zM20.201,31.749c-6.425,0 -11.634,-5.208 -11.634,-11.634c0,-6.425 5.209,-11.634 11.634,-11.634c6.425,0 11.633,5.209 11.633,11.634C31.834,26.541 26.626,31.749 20.201,31.749z"
android:strokeAlpha="0.2"/>
<path android:fillColor="@color/colorAccent"
android:pathData="M26.013,10.047l1.654,-2.866c-2.198,-1.272 -4.743,-2.012 -7.466,-2.012h0v3.312h0C22.32,8.481 24.301,9.057 26.013,10.047z"/>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,1A11,11 0,1 0,23 12,11 11,0 0,0 12,1ZM12,20a8,8 0,1 1,8 -8A8,8 0,0 1,12 20Z"
android:strokeAlpha="0.25"
android:fillColor="?attr/iconsColor"
android:fillAlpha="0.25"/>
<path
android:pathData="M12,2.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"
android:fillColor="?attr/iconsColor"/>
</vector>

View File

@@ -169,6 +169,29 @@
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/gitignoreFrame"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/dimen8dp"
android:layout_marginBottom="@dimen/dimen8dp"
android:hint="@string/gitignore_template"
android:textColorHint="?attr/hintColor"
app:endIconTint="?attr/iconsColor"
app:hintTextColor="?attr/hintColor">
<AutoCompleteTextView
android:id="@+id/gitignoreTemplates"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="none"
android:labelFor="@+id/gitignoreTemplates"
android:textColor="?attr/inputTextColor"
android:textSize="@dimen/dimen16sp" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/licenseFrame"
style="@style/Widget.Material3.TextInputLayout.OutlinedBox.ExposedDropdownMenu"

View File

@@ -100,7 +100,6 @@
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone"
android:layout_marginBottom="@dimen/dimen16dp"
android:clipChildren="false"
android:clipToPadding="false">
@@ -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">

View File

@@ -130,184 +130,4 @@
<item>@string/pollingDelay45Minutes</item>
<item>@string/pollingDelay1Hour</item>
</string-array>
<string-array name="licenses">
<item>0BSD</item>
<item>AAL</item>
<item>ADSL</item>
<item>AFL-1.1</item>
<item>AFL-1.1</item>
<item>AFL-2.0</item>
<item>AFL-3.0</item>
<item>AGPL-1.0-only</item>
<item>AGPL-1.0-or-later</item>
<item>AGPL-3.0-only</item>
<item>AGPL-3.0-or-later</item>
<item>AML</item>
<item>APL-1.0</item>
<item>APSL-1.0</item>
<item>APSL-2.0</item>
<item>Adobe-2006</item>
<item>Aladdin</item>
<item>Apache-1.0</item>
<item>Apache-1.1</item>
<item>Apache-2.0</item>
<item>Artistic-1.0</item>
<item>Artistic-2.0</item>
<item>BSD-1-Clause</item>
<item>BSD-2-Clause</item>
<item>BSD-2-Clause-Patent</item>
<item>BSD-2-Clause-Views</item>
<item>BSD-3-Clause</item>
<item>BSD-3-Clause-Attribution</item>
<item>BSD-3-Clause-Clear</item>
<item>BSD-3-Clause-LBNL</item>
<item>BSD-3-Clause-Modification</item>
<item>BSD-3-Clause-No-Military-License</item>
<item>BSD-3-Clause-No-Nuclear-License</item>
<item>BSD-3-Clause-No-Nuclear-License-2014</item>
<item>BSD-3-Clause-No-Nuclear-Warranty</item>
<item>BSD-3-Clause-Open-MPI</item>
<item>BSD-3-Clause-Sun</item>
<item>BSD-4-Clause-Shortened</item>
<item>BSD-4-Clause-UC</item>
<item>BSD-4.3RENO</item>
<item>BSD-4.3TAHOE</item>
<item>BSD-Advertising-Acknowledgement</item>
<item>BSD-Attribution-HPND-disclaimer</item>
<item>BSD-Protection</item>
<item>BSD-Source-Code</item>
<item>BSD-Systemics</item>
<item>BitTorrent-1.0</item>
<item>BitTorrent-1.1</item>
<item>CC-BY-1.0</item>
<item>CC-BY-2.0</item>
<item>CC-BY-2.5</item>
<item>CC-BY-2.5-AU</item>
<item>CC-BY-3.0</item>
<item>CC-BY-3.0-AT</item>
<item>CC-BY-3.0-DE</item>
<item>CC-BY-4.0</item>
<item>CC-BY-NC-1.0</item>
<item>CC-BY-NC-2.0</item>
<item>CC-BY-NC-2.5</item>
<item>CC-BY-NC-3.0</item>
<item>CC-BY-NC-4.0</item>
<item>Community-Spec-1.0</item>
<item>Cube</item>
<item>D-FSL-1.0</item>
<item>DL-DE-BY-2.0</item>
<item>ECL-1.0</item>
<item>ECL-2.0</item>
<item>EFL-1.0</item>
<item>EFL-2.0</item>
<item>Elastic-2.0</item>
<item>FreeBSD-DOC</item>
<item>GD</item>
<item>GFDL-1.1-only</item>
<item>GFDL-1.1-or-later</item>
<item>GFDL-1.2-only</item>
<item>GFDL-1.2-or-later</item>
<item>GFDL-1.3-only</item>
<item>GFDL-1.3-or-later</item>
<item>GPL-1.0-only</item>
<item>GPL-1.0-or-later</item>
<item>GPL-2.0-only</item>
<item>GPL-2.0-or-later</item>
<item>GPL-3.0-interface-exception</item>
<item>GPL-3.0-only</item>
<item>GPL-3.0-or-later</item>
<item>GPL-CC-1.0</item>
<item>GStreamer-exception-2008</item>
<item>Glide</item>
<item>HP-1989</item>
<item>IBM-pibs</item>
<item>ICU</item>
<item>IPL-1.0</item>
<item>ImageMagick</item>
<item>Intel</item>
<item>Intel-ACPI</item>
<item>Interbase-1.0</item>
<item>JSON</item>
<item>LAL-1.3</item>
<item>LGPL-2.0-only</item>
<item>LGPL-2.0-or-later</item>
<item>LGPL-2.1-only</item>
<item>LGPL-2.1-or-later</item>
<item>LGPL-3.0-only</item>
<item>LGPL-3.0-or-later</item>
<item>LGPLLR</item>
<item>LLGPL</item>
<item>LPL-1.0</item>
<item>LPL-1.02</item>
<item>LPPL-1.0</item>
<item>LPPL-1.3a</item>
<item>LPPL-1.3c</item>
<item>Libpng</item>
<item>Linux-OpenIB</item>
<item>MIT</item>
<item>MIT-0</item>
<item>MIT-CMU</item>
<item>MIT-Festival</item>
<item>MIT-Modern-Variant</item>
<item>MIT-Wu</item>
<item>MIT-advertising</item>
<item>MIT-open-group</item>
<item>MPL-1.0</item>
<item>MPL-2.0</item>
<item>MirOS</item>
<item>NASA-1.3</item>
<item>NTP</item>
<item>Nokia</item>
<item>OCLC-2.0</item>
<item>OFL-1.0</item>
<item>OFL-1.1</item>
<item>OLDAP-1.1</item>
<item>OLDAP-2.0</item>
<item>OLDAP-2.8</item>
<item>OML</item>
<item>OSL-1.0</item>
<item>OSL-2.0</item>
<item>OSL-3.0</item>
<item>OpenSSL</item>
<item>PHP-3.0</item>
<item>PSF-2.0</item>
<item>PostgreSQL</item>
<item>Python-2.0</item>
<item>QPL-1.0</item>
<item>Qt-GPL-exception-1.0</item>
<item>Qt-GPL-exception-1.1</item>
<item>RPL-1.1</item>
<item>RPL-1.5</item>
<item>Ruby</item>
<item>SGI-B-1.0</item>
<item>SGI-B-2.0</item>
<item>SSH-OpenSSH</item>
<item>Sendmail</item>
<item>TCL</item>
<item>UnixCrypt</item>
<item>Unlicense</item>
<item>Vim</item>
<item>W3C</item>
<item>WTFPL</item>
<item>X11</item>
<item>XFree86-1.1</item>
<item>Xdebug-1.03</item>
<item>Xerox</item>
<item>YPL-1.0</item>
<item>ZPL-1.1</item>
<item>ZPL-2.0</item>
<item>Zed</item>
<item>Zimbra-1.4</item>
<item>Zlib</item>
<item>bzip2-1.0.6</item>
<item>copyleft-next-0.3.1</item>
<item>curl</item>
<item>gnuplot</item>
<item>libpng-2.0</item>
<item>libselinux-1.0</item>
<item>w3m</item>
<item>xpp</item>
<item>zlib-acknowledgement</item>
</string-array>
</resources>

View File

@@ -1073,4 +1073,8 @@
<string name="url_prompt_title">Show URL confirmation dialog</string>
<string name="url_prompt_description">Show a confirmation dialog before opening external links in Markdown rendered contents</string>
<string name="gitignore_template">Gitignore Template</string>
<string name="no_template">No template</string>
<string name="no_license">No license</string>
</resources>

View File

@@ -1,26 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<changelog>
<release version="12.0.0-dev" versioncode="1195">
<release version="13.0.0-dev" versioncode="1295">
<type name="🎉 Features 🎉">
<change>HTTP basic authentication (more in README.md)</change>
<change>Pinch to zoom in and out of images in issue/PR/comment popups</change>
<change>PIN/password as a fallback for biometric authentication</change>
<change>URL opening prompt popup (Settings → General)</change>
<change>Issue templates for new issues</change>
<change>DEV</change>
</type>
<type name="v🚀 Improvements 🚀">
<change>Zig language support in files and the languages bar</change>
<change>Added pagination to labels for the issues filter</change>
<change>Improved pinned issues scrolling</change>
<change>UI improvements across the app</change>
<change>DEV</change>
</type>
<type name="🐛 Bug Fixes 🐛">
<change>Fixed bottom navigation colors</change>
<change>Fixed releases and tags view</change>
<change>Fixed topics add button UI</change>
<change>Fixed showing 'no data found' for most visited repositories for a split second</change>
<change>Fixed comment content formatting in Activities</change>
<change>DEV</change>
</type>
</release>