Replacing highlightJS with custom view due to performance issues and high file sizes. (#837)

Fixing crash and minor improvements.

Add ".properties" and ".ini" formats.

Increase surface area of files list item.

Fixing issue where some source code is not visible.

Replacing highlightJS with custom view due to performance issues and high file sizes.

Reviewed-on: https://codeberg.org/gitnex/GitNex/pulls/837
Reviewed-by: M M Arif <mmarif@noreply.codeberg.org>
Reviewed-by: 6543 <6543@noreply.codeberg.org>
Co-Authored-By: opyale <opyale@noreply.codeberg.org>
Co-Committed-By: opyale <opyale@noreply.codeberg.org>
This commit is contained in:
opyale 2021-03-21 19:26:28 +01:00 committed by 6543
parent 98cf1a1976
commit dc29a31e63
26 changed files with 372 additions and 1222 deletions

View File

@ -83,7 +83,6 @@ Thanks to all the open source libraries, contributors and donators.
- [Pes8/android-material-color-picker-dialog](https://github.com/Pes8/android-material-color-picker-dialog)
- [HamidrezaAmz/BreadcrumbsView](https://github.com/HamidrezaAmz/BreadcrumbsView)
- [Baseflow/PhotoView](https://github.com/Baseflow/PhotoView)
- [PDDStudio/highlightjs-android](https://github.com/PDDStudio/highlightjs-android)
- [apache/commons](https://github.com/apache/commons-io)
- [barteksc/AndroidPdfViewer](https://github.com/barteksc/AndroidPdfViewer)
- [ge0rg/MemorizingTrustManager](https://github.com/ge0rg/MemorizingTrustManager)

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
!function(e){"use strict";function t(){"complete"===document.readyState?n():e.addEventListener("DOMContentLoaded",n)}function n(){try{var e=document.querySelectorAll("code.hljs");for(var t in e)e.hasOwnProperty(t)&&r(e[t])}catch(n){console.error("LineNumbers error: ",n)}}function r(e){if("object"==typeof e){var t=e.parentNode,n=o(t.textContent);if(n>1){for(var r="",c=0;n>c;c++)r+=c+1+"\n";var l=document.createElement("code");l.className="hljs hljs-line-numbers",l.style["float"]="left",l.textContent=r,t.insertBefore(l,e)}}}function o(e){if(0===e.length)return 0;var t=/\r\n|\r|\n/g,n=e.match(t);return n=n?n.length:0,e[e.length-1].match(t)||(n+=1),n}"undefined"==typeof e.hljs?console.error("highlight.js not detected!"):(e.hljs.initLineNumbersOnLoad=t,e.hljs.lineNumbersBlock=r)}(window);

View File

@ -1,66 +0,0 @@
/*
Date: 24 Fev 2015
Author: Pedro Oliveira <kanytu@gmail . com>
*/
.hljs {
color: #a9b7c6;
background: #282b2e;
display: block;
overflow-x: auto;
padding: 0.5em;
}
.hljs-number,
.hljs-literal,
.hljs-symbol,
.hljs-bullet {
color: #6897BB;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-deletion {
color: #cc7832;
}
.hljs-variable,
.hljs-template-variable,
.hljs-link {
color: #629755;
}
.hljs-comment,
.hljs-quote {
color: #808080;
}
.hljs-meta {
color: #bbb529;
}
.hljs-string,
.hljs-attribute,
.hljs-addition {
color: #6A8759;
}
.hljs-section,
.hljs-title,
.hljs-type {
color: #ffc66d;
}
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #e8bf6a;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

View File

@ -1,87 +0,0 @@
/*
Arduino® Light Theme - Stefania Mellai <s.mellai@arduino.cc>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #FFFFFF;
}
.hljs,
.hljs-subst {
color: #434f54;
}
.hljs-keyword,
.hljs-attribute,
.hljs-selector-tag,
.hljs-doctag,
.hljs-name {
color: #00979D;
}
.hljs-built_in,
.hljs-literal,
.hljs-bullet,
.hljs-code,
.hljs-addition {
color: #D35400;
}
.hljs-regexp,
.hljs-symbol,
.hljs-variable,
.hljs-template-variable,
.hljs-link,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #00979D;
}
.hljs-type,
.hljs-string,
.hljs-selector-id,
.hljs-selector-class,
.hljs-quote,
.hljs-template-tag,
.hljs-deletion {
color: #005C5F;
}
.hljs-title,
.hljs-section {
color: #880000;
font-weight: bold;
}
.hljs-comment {
color: rgba(149,165,166,.8);
}
.hljs-meta-keyword {
color: #728E00;
}
.hljs-meta {
color: #434f54;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}
.hljs-function {
color: #728E00;
}
.hljs-number {
color: #8A7B52;
}

View File

@ -1,71 +0,0 @@
/*
FAR Style (c) MajestiC <majestic2k@gmail.com>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #000080;
}
.hljs,
.hljs-subst {
color: #0ff;
}
.hljs-string,
.hljs-attribute,
.hljs-symbol,
.hljs-bullet,
.hljs-built_in,
.hljs-builtin-name,
.hljs-template-tag,
.hljs-template-variable,
.hljs-addition {
color: #ff0;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-section,
.hljs-type,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class,
.hljs-variable {
color: #fff;
}
.hljs-comment,
.hljs-quote,
.hljs-doctag,
.hljs-deletion {
color: #888;
}
.hljs-number,
.hljs-regexp,
.hljs-literal,
.hljs-link {
color: #0f0;
}
.hljs-meta {
color: #008080;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-title,
.hljs-section,
.hljs-name,
.hljs-strong {
font-weight: bold;
}
.hljs-emphasis {
font-style: italic;
}

View File

@ -1,99 +0,0 @@
/*
github.com style (c) Vasily Polovnyov <vast@whiteants.net>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
color: #333;
background: #f8f8f8;
}
.hljs-comment,
.hljs-quote {
color: #998;
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-subst {
color: #333;
font-weight: bold;
}
.hljs-number,
.hljs-literal,
.hljs-variable,
.hljs-template-variable,
.hljs-tag .hljs-attr {
color: #008080;
}
.hljs-string,
.hljs-doctag {
color: #d14;
}
.hljs-title,
.hljs-section,
.hljs-selector-id {
color: #900;
font-weight: bold;
}
.hljs-subst {
font-weight: normal;
}
.hljs-type,
.hljs-class .hljs-title {
color: #458;
font-weight: bold;
}
.hljs-tag,
.hljs-name,
.hljs-attribute {
color: #000080;
font-weight: normal;
}
.hljs-regexp,
.hljs-link {
color: #009926;
}
.hljs-symbol,
.hljs-bullet {
color: #990073;
}
.hljs-built_in,
.hljs-builtin-name {
color: #0086b3;
}
.hljs-meta {
color: #999;
font-weight: bold;
}
.hljs-deletion {
background: #fdd;
}
.hljs-addition {
background: #dfd;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

View File

@ -1,73 +0,0 @@
/*
IR_Black style (c) Vasily Mikhailitchenko <vaskas@programica.ru>
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #000;
color: #f8f8f8;
}
.hljs-comment,
.hljs-quote,
.hljs-meta {
color: #7c7c7c;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-tag,
.hljs-name {
color: #96cbfe;
}
.hljs-attribute,
.hljs-selector-id {
color: #ffffb6;
}
.hljs-string,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-addition {
color: #a8ff60;
}
.hljs-subst {
color: #daefa3;
}
.hljs-regexp,
.hljs-link {
color: #e9c062;
}
.hljs-title,
.hljs-section,
.hljs-type,
.hljs-doctag {
color: #ffffb6;
}
.hljs-symbol,
.hljs-bullet,
.hljs-variable,
.hljs-template-variable,
.hljs-literal {
color: #c6c5fe;
}
.hljs-number,
.hljs-deletion {
color:#ff73fd;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: bold;
}

View File

@ -1,83 +0,0 @@
/*
Monokai Sublime style. Derived from Monokai by noformnocontent http://nn.mit-license.org/
*/
.hljs {
display: block;
overflow-x: auto;
padding: 0.5em;
background: #23241f;
}
.hljs,
.hljs-tag,
.hljs-subst {
color: #f8f8f2;
}
.hljs-strong,
.hljs-emphasis {
color: #a8a8a2;
}
.hljs-bullet,
.hljs-quote,
.hljs-number,
.hljs-regexp,
.hljs-literal,
.hljs-link {
color: #ae81ff;
}
.hljs-code,
.hljs-title,
.hljs-section,
.hljs-selector-class {
color: #a6e22e;
}
.hljs-strong {
font-weight: bold;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-name,
.hljs-attr {
color: #f92672;
}
.hljs-symbol,
.hljs-attribute {
color: #66d9ef;
}
.hljs-params,
.hljs-class .hljs-title {
color: #f8f8f2;
}
.hljs-string,
.hljs-type,
.hljs-built_in,
.hljs-builtin-name,
.hljs-selector-id,
.hljs-selector-attr,
.hljs-selector-pseudo,
.hljs-addition,
.hljs-variable,
.hljs-template-variable {
color: #e6db74;
}
.hljs-comment,
.hljs-deletion,
.hljs-meta {
color: #75715e;
}

View File

@ -30,7 +30,6 @@ import org.mian.gitnex.helpers.Constants;
import org.mian.gitnex.helpers.Images;
import org.mian.gitnex.helpers.Markdown;
import org.mian.gitnex.helpers.Toasty;
import org.mian.gitnex.helpers.highlightjs.models.Theme;
import org.mian.gitnex.notifications.Notifications;
import java.io.IOException;
import java.io.OutputStream;
@ -163,20 +162,8 @@ public class FileViewActivity extends BaseActivity implements BottomSheetFileVie
binding.markdownFrame.setVisibility(View.GONE);
binding.pdfViewFrame.setVisibility(View.GONE);
switch(tinyDB.getInt("fileviewerSourceCodeThemeId")) {
case 1: binding.contents.setTheme(Theme.ARDUINO_LIGHT); break;
case 2: binding.contents.setTheme(Theme.GITHUB); break;
case 3: binding.contents.setTheme(Theme.FAR); break;
case 4: binding.contents.setTheme(Theme.IR_BLACK); break;
case 5: binding.contents.setTheme(Theme.ANDROID_STUDIO); break;
default: binding.contents.setTheme(Theme.MONOKAI_SUBLIME);
}
binding.contents.setVisibility(View.VISIBLE);
binding.contents.setContent(text);
binding.contents.setContent(text, fileExtension);
});
break;

View File

@ -1,73 +0,0 @@
package org.mian.gitnex.activities;
import android.annotation.SuppressLint;
import android.os.Bundle;
import androidx.appcompat.app.AlertDialog;
import org.mian.gitnex.R;
import org.mian.gitnex.databinding.ActivitySettingsFileviewerBinding;
import org.mian.gitnex.helpers.Toasty;
/**
* Author M M Arif
*/
public class SettingsFileViewerActivity extends BaseActivity {
private static final String[] fileViewerSourceCodeThemesList = {"Sublime", "Arduino Light", "Github", "Far ", "Ir Black", "Android Studio"};
private static int fileViewerSourceCodeThemesSelectedChoice = 0;
@SuppressLint("DefaultLocale")
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivitySettingsFileviewerBinding binding = ActivitySettingsFileviewerBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
binding.close.setOnClickListener(view -> finish());
if(fileViewerSourceCodeThemesSelectedChoice == 0) {
fileViewerSourceCodeThemesSelectedChoice = tinyDB.getInt("fileviewerThemeId");
}
binding.sourceCodeThemeSelected.setText(tinyDB.getString("fileviewerSourceCodeThemeStr", fileViewerSourceCodeThemesList[0]));
binding.switchPdfMode.setChecked(tinyDB.getBoolean("enablePdfMode"));
// fileviewer source code theme selection dialog
binding.sourceCodeThemeFrame.setOnClickListener(view -> {
AlertDialog.Builder fvtsBuilder = new AlertDialog.Builder(SettingsFileViewerActivity.this);
fvtsBuilder.setTitle(R.string.fileViewerSourceCodeThemeSelectorDialogTitle);
fvtsBuilder.setCancelable(fileViewerSourceCodeThemesSelectedChoice != -1);
fvtsBuilder.setSingleChoiceItems(fileViewerSourceCodeThemesList, fileViewerSourceCodeThemesSelectedChoice, (dialogInterfaceTheme, i) -> {
fileViewerSourceCodeThemesSelectedChoice = i;
binding.sourceCodeThemeSelected.setText(fileViewerSourceCodeThemesList[i]);
tinyDB.putString("fileviewerSourceCodeThemeStr", fileViewerSourceCodeThemesList[i]);
tinyDB.putInt("fileviewerSourceCodeThemeId", i);
dialogInterfaceTheme.dismiss();
Toasty.success(appCtx, getResources().getString(R.string.settingsSave));
});
AlertDialog alertDialog = fvtsBuilder.create();
alertDialog.show();
});
// pdf night mode switcher
binding.switchPdfMode.setOnCheckedChangeListener((buttonView, isChecked) -> {
tinyDB.putBoolean("enablePdfMode", isChecked);
tinyDB.putString("enablePdfModeInit", "yes");
Toasty.success(appCtx, getResources().getString(R.string.settingsSave));
});
}
}

View File

@ -7,6 +7,7 @@ import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.Filterable;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
@ -39,6 +40,7 @@ public class FilesAdapter extends RecyclerView.Adapter<FilesAdapter.FilesViewHol
private Files file;
private final LinearLayout fileFrame;
private final ImageView fileTypeIs;
private final TextView fileName;
private final TextView fileInfo;
@ -47,15 +49,15 @@ public class FilesAdapter extends RecyclerView.Adapter<FilesAdapter.FilesViewHol
super(itemView);
fileFrame = itemView.findViewById(R.id.fileFrame);
fileName = itemView.findViewById(R.id.fileName);
fileTypeIs = itemView.findViewById(R.id.fileTypeIs);
fileInfo = itemView.findViewById(R.id.fileInfo);
fileFrame.setOnClickListener(v -> filesListener.onClickFile(file));
//ImageView filesDropdownMenu = itemView.findViewById(R.id.filesDropdownMenu);
fileName.setOnClickListener(v -> filesListener.onClickFile(file));
/*filesDropdownMenu.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {

View File

@ -0,0 +1,140 @@
package org.mian.gitnex.core;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Set;
import io.noties.prism4j.GrammarLocator;
import io.noties.prism4j.Prism4j;
import io.noties.prism4j.annotations.PrismBundle;
/**
* @author opyale
*/
@PrismBundle(
includeAll = true,
grammarLocatorClassName = ".DefaultGrammarLocator"
)
public class MainGrammarLocator implements GrammarLocator {
public static final String DEFAULT_FALLBACK_LANGUAGE = "clike";
private static final DefaultGrammarLocator defaultGrammarLocator = new DefaultGrammarLocator();
private static volatile MainGrammarLocator instance;
private MainGrammarLocator() {}
public String fromExtension(String extension) {
switch(extension.toLowerCase()) {
case "b":
case "bf":
return "brainfuck";
case "c":
case "h":
case "hdl":
return "c";
case "clj":
case "cljs":
case "cljc":
case "edn":
return "clojure";
case "cc":
case "cpp":
case "cxx":
case "c++":
case "hh":
case "hpp":
case "hxx":
case "h++":
return "cpp";
case "cs":
case "csx":
return "csharp";
case "groovy":
case "gradle":
case "gvy":
case "gy":
case "gsh":
return "groovy";
case "js":
case "cjs":
case "mjs":
return "javascript";
case "kt":
case "kts":
case "ktm":
return "kotlin";
case "md":
return "markdown";
case "xml":
case "html":
case "htm":
case "mathml":
case "svg":
return "markup";
case "py":
case "pyi":
case "pyc":
case "pyd":
case "pyo":
case "pyw":
case "pyz":
return "python";
case "scala":
case "sc":
return "scala";
case "yaml":
case "yml":
case "properties": // This extension doesn't correspond to YAML, but it's the next best option
case "ini": // This extension doesn't correspond to YAML, but it's the next best option
return "yaml";
}
return extension;
}
@Nullable
@Override
public Prism4j.Grammar grammar(@NotNull Prism4j prism4j, @NotNull String language) {
return defaultGrammarLocator.grammar(prism4j, language);
}
@NotNull
@Override
public Set<String> languages() {
return defaultGrammarLocator.languages();
}
public static MainGrammarLocator getInstance() {
if(instance == null) {
synchronized(MainGrammarLocator.class) {
if(instance == null) {
instance = new MainGrammarLocator();
}
}
}
return instance;
}
}

View File

@ -15,7 +15,6 @@ import org.mian.gitnex.R;
import org.mian.gitnex.activities.MainActivity;
import org.mian.gitnex.activities.SettingsAppearanceActivity;
import org.mian.gitnex.activities.SettingsDraftsActivity;
import org.mian.gitnex.activities.SettingsFileViewerActivity;
import org.mian.gitnex.activities.SettingsGeneralActivity;
import org.mian.gitnex.activities.SettingsNotificationsActivity;
import org.mian.gitnex.activities.SettingsReportsActivity;
@ -54,8 +53,6 @@ public class SettingsFragment extends Fragment {
fragmentSettingsBinding.appearanceFrame.setOnClickListener(v1 -> startActivity(new Intent(ctx, SettingsAppearanceActivity.class)));
fragmentSettingsBinding.fileViewerFrame.setOnClickListener(v1 -> startActivity(new Intent(ctx, SettingsFileViewerActivity.class)));
fragmentSettingsBinding.draftsFrame.setOnClickListener(v1 -> startActivity(new Intent(ctx, SettingsDraftsActivity.class)));
fragmentSettingsBinding.securityFrame.setOnClickListener(v1 -> startActivity(new Intent(ctx, SettingsSecurityActivity.class)));

View File

@ -25,6 +25,8 @@ import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Author M M Arif
@ -295,6 +297,20 @@ public class AppUtil {
return (int) (context.getResources().getDisplayMetrics().scaledDensity * sp);
}
public static int getLineCount(String s) {
int lines = 0;
Pattern pattern = Pattern.compile("(\r\n|\r|\n)");
Matcher matcher = pattern.matcher(s);
while(matcher.find())
lines++;
return lines;
}
public static void copyToClipboard(Context ctx, CharSequence data, String message) {
ClipboardManager clipboard = (ClipboardManager) Objects.requireNonNull(ctx).getSystemService(Context.CLIPBOARD_SERVICE);

View File

@ -8,6 +8,7 @@ import androidx.annotation.NonNull;
import androidx.core.content.res.ResourcesCompat;
import org.mian.gitnex.R;
import org.mian.gitnex.clients.PicassoService;
import org.mian.gitnex.core.MainGrammarLocator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import io.noties.markwon.AbstractMarkwonPlugin;
@ -25,16 +26,11 @@ import io.noties.markwon.syntax.Prism4jThemeDarkula;
import io.noties.markwon.syntax.Prism4jThemeDefault;
import io.noties.markwon.syntax.SyntaxHighlightPlugin;
import io.noties.prism4j.Prism4j;
import io.noties.prism4j.annotations.PrismBundle;
/**
* @author opyale
*/
@PrismBundle(
includeAll = true,
grammarLocatorClassName = ".CustomGrammarLocator"
)
public class Markdown {
private static final ExecutorService executorService = Executors.newCachedThreadPool();
@ -65,12 +61,12 @@ public class Markdown {
Markwon.Builder builder = Markwon.builder(context)
.usePlugin(CorePlugin.create())
.usePlugin(HtmlPlugin.create())
.usePlugin(LinkifyPlugin.create())
.usePlugin(LinkifyPlugin.create(true))
.usePlugin(TablePlugin.create(context))
.usePlugin(TaskListPlugin.create(context))
.usePlugin(StrikethroughPlugin.create())
.usePlugin(PicassoImagesPlugin.create(PicassoService.getInstance(context).get()))
.usePlugin(SyntaxHighlightPlugin.create(new Prism4j(new CustomGrammarLocator()), prism4jTheme))
.usePlugin(SyntaxHighlightPlugin.create(new Prism4j(MainGrammarLocator.getInstance()), prism4jTheme, MainGrammarLocator.DEFAULT_FALLBACK_LANGUAGE))
.usePlugin(new AbstractMarkwonPlugin() {
@Override

View File

@ -0,0 +1,201 @@
package org.mian.gitnex.helpers;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.widget.HorizontalScrollView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.mian.gitnex.core.MainGrammarLocator;
import io.noties.markwon.syntax.Prism4jSyntaxHighlight;
import io.noties.markwon.syntax.Prism4jTheme;
import io.noties.markwon.syntax.Prism4jThemeDarkula;
import io.noties.markwon.syntax.Prism4jThemeDefault;
import io.noties.prism4j.Prism4j;
/**
* @author opyale
*/
public class SyntaxHighlightedArea extends LinearLayout {
private Prism4jTheme prism4jTheme;
private TextView sourceView;
private LinesView linesView;
public SyntaxHighlightedArea(@NonNull Context context) {
super(context);
setup();
}
public SyntaxHighlightedArea(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
setup();
}
public SyntaxHighlightedArea(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setup();
}
public void setup() {
prism4jTheme = TinyDB.getInstance(getContext()).getString("currentTheme").equals("dark") ?
Prism4jThemeDarkula.create() :
Prism4jThemeDefault.create();
sourceView = new TextView(getContext());
sourceView.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
sourceView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14);
sourceView.setTypeface(Typeface.createFromAsset(getContext().getAssets(), "fonts/sourcecodeproregular.ttf"));
sourceView.setTextColor(prism4jTheme.textColor());
int padding = AppUtil.getPixelsFromDensity(getContext(), 5);
sourceView.setPadding(padding, 0, padding, 0);
HorizontalScrollView horizontalScrollView = new HorizontalScrollView(getContext());
horizontalScrollView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
horizontalScrollView.addView(sourceView);
linesView = new LinesView(getContext());
linesView.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT));
linesView.setPadding(
AppUtil.getPixelsFromDensity(getContext(), 3), 0,
AppUtil.getPixelsFromDensity(getContext(), 6), 0);
linesView.getPaint().setTypeface(sourceView.getTypeface());
linesView.getPaint().setTextSize(sourceView.getTextSize());
linesView.setBackgroundColor(prism4jTheme.background());
linesView.setTextColor(prism4jTheme.textColor());
linesView.setLineColor(prism4jTheme.textColor());
setOrientation(HORIZONTAL);
setBackgroundColor(prism4jTheme.background());
addView(linesView);
addView(horizontalScrollView);
}
public void setContent(@NonNull String source, @NonNull String extension) {
if(source.length() > 0) {
linesView.setLineCount(AppUtil.getLineCount(source));
MainGrammarLocator mainGrammarLocator = MainGrammarLocator.getInstance();
Prism4jSyntaxHighlight prism4jSyntaxHighlight = Prism4jSyntaxHighlight.create(new Prism4j(mainGrammarLocator), prism4jTheme, MainGrammarLocator.DEFAULT_FALLBACK_LANGUAGE);
CharSequence highlightedSource = prism4jSyntaxHighlight.highlight(mainGrammarLocator.fromExtension(extension), source);
if(highlightedSource.charAt(highlightedSource.length() - 1) == '\n') {
// Removes a line break which is probably added by Prism4j but not actually present in the source.
// This line should be altered in case this gets fixed.
sourceView.setText(highlightedSource.subSequence(0, highlightedSource.length() - 1));
}
else {
sourceView.setText(highlightedSource);
}
}
}
public String getContent() {
return sourceView.getText().toString();
}
private static class LinesView extends View {
private final Paint paint = new Paint();
private final Rect textBounds = new Rect();
@ColorInt private int backgroundColor;
@ColorInt private int textColor;
@ColorInt private int lineColor;
private int lineCount;
public LinesView(Context context) {
super(context);
}
public void setLineCount(int lineCount) {
this.lineCount = lineCount;
}
@Override
public void setBackgroundColor(@ColorInt int backgroundColor) {
this.backgroundColor = backgroundColor;
}
public void setTextColor(@ColorInt int textColor) {
this.textColor = textColor;
}
public void setLineColor(@ColorInt int lineColor) {
this.lineColor = lineColor;
}
public Paint getPaint() {
return paint;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
String highestLineNumber = String.valueOf(lineCount);
paint.getTextBounds(highestLineNumber, 0, highestLineNumber.length(), textBounds);
setMeasuredDimension(getPaddingLeft() + textBounds.width() + getPaddingRight(), MeasureSpec.getSize(heightMeasureSpec));
}
@Override
protected void onDraw(Canvas canvas) {
paint.setColor(backgroundColor);
canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
float marginTopBottom = (float) (getHeight() - (textBounds.height() / 2)) / lineCount;
paint.setColor(textColor);
canvas.save();
canvas.translate(getPaddingLeft(), marginTopBottom);
for(int currentLine = 1; currentLine <= lineCount; currentLine++) {
canvas.drawText(String.valueOf(currentLine), 0, 0, paint);
canvas.translate(0, marginTopBottom);
}
paint.setColor(lineColor);
int dividerX = getWidth() - 1;
int dividerY = getHeight();
canvas.restore();
canvas.drawLine(dividerX,0, dividerX, dividerY, paint);
}
}
}

View File

@ -1,117 +0,0 @@
package org.mian.gitnex.helpers.highlightjs;
import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.webkit.WebSettings;
import android.webkit.WebView;
import org.mian.gitnex.helpers.highlightjs.models.Language;
import org.mian.gitnex.helpers.highlightjs.models.Theme;
import org.mian.gitnex.helpers.highlightjs.utils.SourceUtils;
/**
* This Class was created by Patrick J
* on 09.06.16. (modified by opyale)
*/
public class HighlightJsView extends WebView {
private Language language = Language.AUTO_DETECT;
private Theme theme = Theme.DEFAULT;
private boolean zoomSupport = false;
private boolean showLineNumbers = true;
private TextWrap textWrap = TextWrap.NO_WRAP;
private String source = "";
public HighlightJsView(Context context) {
super(context);
setup();
}
public HighlightJsView(Context context, AttributeSet attrs) {
super(context, attrs);
setup();
}
public HighlightJsView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setup();
}
@SuppressLint("SetJavaScriptEnabled")
private void setup() {
WebSettings settings = getSettings();
settings.setJavaScriptEnabled(true);
settings.setBuiltInZoomControls(true);
settings.setSupportZoom(zoomSupport);
settings.setDisplayZoomControls(false);
setScrollBarStyle(View.SCROLLBARS_INSIDE_OVERLAY);
}
private void changeZoomSettings(boolean enable) {
this.zoomSupport = enable;
getSettings().setSupportZoom(enable);
}
public String getContent() {
return source;
}
public void setContent(String source) {
this.source = (source == null) ? "" : source;
String html_content = SourceUtils.generateContent(this.source, theme.getName(), language.getName(), zoomSupport, showLineNumbers, textWrap);
loadDataWithBaseURL("file:///android_asset/", html_content, "text/html", "utf-8", null);
}
public void refresh() {
super.reload();
}
public void setHighlightLanguage(Language language) {
this.language = language;
}
public void setTheme(Theme theme) {
this.theme = theme;
}
public void setTextWrap(TextWrap textWrap) {
this.textWrap = textWrap;
}
public Language getHighlightLanguage() {
return language;
}
public Theme getTheme() {
return theme;
}
public void setZoomSupportEnabled(boolean supportZoom) {
changeZoomSettings(supportZoom);
}
public void setShowLineNumbers(boolean showLineNumbers) {
this.showLineNumbers = showLineNumbers;
}
public enum TextWrap {
NO_WRAP, WORD_WRAP, BREAK_ALL
}
}

View File

@ -1,181 +0,0 @@
package org.mian.gitnex.helpers.highlightjs.models;
/**
* This Class was created by Patrick J
* on 09.06.16.
*/
public enum Language {
AUTO_DETECT(null),
DISABLE_HIGHLIGHT("nohighlight"),
_1C("1c"),
ABNF("abnf"),
ACCESS_LOGS("accesslog"),
ADA("ada"),
ARM_ASSEMBLER("arm"),
AVR_ASSEMBLER("avrasm"),
ACTION_SCRIPT("actionscript"),
APACHE("apache"),
APPLE_SCRIPT("applescript"),
ASCII_DOC("asciidoc"),
ASPECT_J("aspectj"),
AUTO_HOTKEY("autohotkey"),
AUTO_IT("autoit"),
AXAPTA("axapta"),
AWK("awk"),
BASH("bash"),
SHELL("sh"),
ZSH("zsh"),
BASIC("basic"),
BNF("bnf"),
BRAINFUCK("brainfuck"),
C("c"),
C_SHARP("csharp"),
C_PLUS_PLUS("cpp"),
CACHE_OBJECT_SCRIPT("cos"),
C_MAKE("cmake"),
COQ("coq"),
CSP("csp"),
CSS("css"),
CAPTAIN_PROTO("capnproto"),
CLEAN("clean"),
CLOJURE("clojure"),
COFFEE_SCRIPT("coffeescript"),
CRMSH("crmsh"),
CRYSTAL("crystal"),
D("d"),
DNS_ZONE_FILE("dns"),
DOS("dos"),
BATCH("bat"),
DART("dart"),
DELPHI("delphi"),
DIFF("diff"),
DJANGO("django"),
DOCKER_FILE("dockerfile"),
DSCONFIG("dsconfig"),
DTS("dts"),
DUST("dust"),
EBNF("ebnf"),
ELIXIR("elixir"),
ELM("elm"),
ERLANG("erlang"),
EXCEL("excel"),
F_SHARP("fsharp"),
FIX("fix"),
FLIX("flix"),
FORTRAN("fortran"),
G_CODE("gcode"),
GAMS("gams"),
GAUSS("gauss"),
GHERKIN("gherkin"),
GO("go"),
GOLO("golo"),
GRADLE("gradle"),
GROOVY("groovy"),
HTML("html"),
XML("xml"),
HTTP("http"),
HAML("haml"),
HANDLEBARS("hbs"),
HASKELL("hs"),
HAXE("hx"),
HY("hy"),
INI("ini"),
INFORM7("i7"),
IRPF90("irpf90"),
JSON("json"),
JAVA("java"),
JAVA_SCRIPT("javascript"),
LASSO("lasso"),
LEAF("leaf"),
LESS("less"),
LDIF("ldif"),
LISP("lisp"),
LIVE_CODE_SERVER("livecodeserver"),
LIVE_SCRIPT("livescript"),
LLVM("llvm"),
LUA("lua"),
MAKEFILE("makefile"),
MARKDOWN("md"),
MATHEMATICA("mma"),
MATLAB("matlab"),
MAXIMA("maxima"),
MAYA_EMBEDDED_LANGUAGE("mel"),
MERCURY("mercury"),
MIZAR("mizar"),
MOJOLICIOUS("mojolicious"),
MONKEY("monkey"),
MOONSCRIPT("moonscript"),
N1QL("n1ql"),
NSIS("nsis"),
NGINX("nginx"),
NIMROD("nimrod"),
NIX("nix"),
O_CAML("ocaml"),
OBJECTIVE_C("objectivec"),
OPENGL_SHADING_LANGUAGE("glsl"),
OPEN_SCAD("scad"),
ORACLE_RULES_LANGUAGE("ruleslanguage"),
OXYGENE("oxygene"),
PF("pf"),
PHP("php"),
PARSER3("parser3"),
PERL("perl"),
PONY("pony"),
POWER_SHELL("ps"),
PROCESSING("processing"),
PROLOG("prolog"),
PROTOCOL_BUFFERS("protobuf"),
PUPPET("pp"),
PYTHON("python"),
PYTHON_PROFILER_RESULTS("profile"),
Q("k"),
QML("qml"),
R("r"),
RENDER_MAN_RIB("rib"),
RENDER_MAN_RSL("rsl"),
ROBOCONF("roboconf"),
RUBY("ruby"),
RUST("rust"),
SCSS("scss"),
SQL("sql"),
STEP_PART_21("p21"),
SCALA("scala"),
SCHEME("scheme"),
SCILAB("sci"),
SMALI("smali"),
SMALLTALK("smalltalk"),
STAN("stan"),
STATA("stata"),
STYLUS("stylus"),
SUB_UNIT("subunit"),
SWIFT("swift"),
TEST_ANYTHING_PROTOCOL("tap"),
TCL("tcl"),
TEX("tex"),
THRIFT("thrift"),
TP("tp"),
TWIG("twig"),
TYPE_SCRIPT("typescript"),
VB_NET("vbnet"),
VB_SCRIPT("vbscript"),
VHDL("vhdl"),
VALA("vala"),
VERILOG("v"),
VIM("vim"),
X86_ASSEMBLY("x86asm"),
XL("xl"),
X_QUERY("xq"),
ZEPHIR("zep");
private final String className;
Language(String name) {
this.className = name;
}
public String getName() {
return className;
}
}

View File

@ -1,96 +0,0 @@
package org.mian.gitnex.helpers.highlightjs.models;
/**
* This Class was created by Patrick J
* on 09.06.16.
*/
public enum Theme {
AGATE("agate"),
ANDROID_STUDIO("androidstudio"),
ARDUINO_LIGHT("arduino-light"),
ARTA("arta"),
ASCETIC("ascetic"),
ATELIER_CAVE_DARK("atelier-cave-dark"),
ATELIER_CAVE_LIGHT("atelier-cave-light"),
ATELIER_DUNE_DARK("atelier-dune-dark"),
ATELIER_DUNE_LIGHT("atelier-dune-light"),
ATELIER_ESTUARY_DARK("atelier-estuary-dark"),
ATELIER_ESTUARY_LIGHT("atelier-estuary-light"),
ATELIER_FOREST_DARK("atelier-forest-dark"),
ATELIER_FOREST_LIGHT("atelier-forest-light"),
ATELIER_HEATH_DARK("atelier-heath-dark"),
ATELIER_HEATH_LIGHT("atelier-heath-light"),
ATELIER_LAKESIDE_DARK("atelier-lakeside-dark"),
ATELIER_LAKESIDE_LIGHT("atelier-lakeside-light"),
ATELIER_PLATEAU_DARK("atelier-plateau-dark"),
ATELIER_PLATEAU_LIGHT("atelier-plateau-light"),
ATELIER_SAVANNA_DARK("atelier-savanna-dark"),
ATELIER_SAVANNA_LIGHT("atelier-savanna-light"),
ATELIER_SEASIDE_DARK("atelier-seaside-dark"),
ATELIER_SEASIDE_LIGHT("atelier-seaside-light"),
ATELIER_SULPHURPOOL_DARK("atelier-sulphurpool-dark"),
ATELIER_SULPHURPOOL_LIGHT("atelier-sulphurpool-light"),
ATOM_ONE_DARK("atom-one-dark"),
ATOM_ONE_LIGHT("atom-one-light"),
BROWN_PAPER("brown-paper"),
CODEPEN_EMBED("codepen-embed"),
COLOR_BREWER("color-brewer"),
DARK("dark"),
DARKULA("darkula"),
DEFAULT("default"),
DOCCO("docco"),
DRAKULA("drakula"),
FAR("far"),
FOUNDATION("foundaiton"),
GITHUB("github"),
GITHUB_GIST("github-gist"),
GOOGLECODE("googlecode"),
GRAYSCALE("grayscale"),
GRUVBOX_DARK("gruvbox-dark"),
GRUVBOX_LIGHT("gruvbox-light"),
HOPSCOTCH("hopscotch"),
HYBRID("hybrid"),
IDEA("idea"),
IR_BLACK("ir-black"),
KIMBIE_DARK("kimbie.dark"),
KIMBIE_LIGHT("kimbie.light"),
MAGULA("magula"),
MONO_BLUE("mono-blue"),
MONOKAI("monokai"),
MONOKAI_SUBLIME("monokai-sublime"),
OBSIDIAN("obsidian"),
OCEAN("ocean"),
PARAISO_DARK("paraiso-dark"),
PARAISO_LIGHT("paraiso-light"),
POJOAQUE("pojoaque"),
PURE_BASIC("purebasic"),
QT_CREATOR_DARK("qtcreator_dark"),
QT_CREATOR_LIGHT("qtcreator_light"),
RAILSCASTS("railscasts"),
RAINBOX("rainbow"),
SCHOOL_BOOK("school-book"),
SOLARIZED_DARK("solarized-dark"),
SOLARIZED_LIGHT("solarized-light"),
SUNBURST("sunburst"),
TOMORROW("tomorrow"),
TOMORROW_NIGHT_BLUE("tomorrow-night-blue"),
TOMORROW_NIGHT_BRIGHT("tomorrow-night-bright"),
TOMORROW_NIGHT("tomorrow-night"),
TOMORROW_NIGHT_EIGHTIES("tomorrow-night-eighties"),
VS("vs"),
X_CODE("xcode"),
XT256("xt256"),
ZENBURN("zenburn");
private final String themeName;
Theme(String themeName) {
this.themeName = themeName;
}
public String getName() {
return themeName;
}
}

View File

@ -1,98 +0,0 @@
package org.mian.gitnex.helpers.highlightjs.utils;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.mian.gitnex.helpers.highlightjs.HighlightJsView;
/**
* This Class was created by Patrick J
* on 09.06.16. (modified by opyale)
*/
public class SourceUtils {
public static String generateContent(String source, @NonNull String theme, @Nullable String language, boolean supportZoom, boolean showLineNumbers,
HighlightJsView.TextWrap textWrap) {
return getStylePageHeader(supportZoom, textWrap) +
getSourceForTheme(theme) +
(showLineNumbers ? getLineNumberStyling() : "") +
getScriptPageHeader(showLineNumbers) +
getSourceForLanguage(source, language) +
getTemplateFooter();
}
private static String getStylePageHeader(boolean enableZoom, HighlightJsView.TextWrap textWrap) {
String preTag;
switch (textWrap) {
case WORD_WRAP:
preTag = "pre { white-space: pre-wrap; word-wrap: break-word; }";
break;
case BREAK_ALL:
preTag = "pre { white-space: pre-wrap; word-break: break-all; }";
break;
default:
preTag = "pre { }";
break;
}
return "<!DOCTYPE html>" +
"<html>" +
"<head>" +
"<meta charset=\"utf-8\">" +
(enableZoom ? "" : "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0\">") +
"<style>" +
"html, body { width:100%; height: 100%; margin: 0px; padding: 0px; }" +
preTag +
"</style>";
}
private static String getScriptPageHeader(boolean showLineNumbers) {
return "<script src=\"./highlightjs/highlight.pack.js\"></script>" +
(showLineNumbers ? "<script src=\"./highlightjs/highlightjs-line-numbers.min.js\"></script>" : "") +
"<script>hljs.initHighlightingOnLoad();</script>" +
(showLineNumbers ? "<script>hljs.initLineNumbersOnLoad();</script>" : "") +
"</head><body style=\"margin: 0; padding: 0\" class=\"hljs\">";
}
private static String getLineNumberStyling() {
return "<style>" +
".hljs-line-numbers { text-align: right; border-right: 1px solid #ccc; color: #999; user-select: none; }" +
"</style>";
}
private static String getTemplateFooter() {
return "</body></html>";
}
private static String escapeCode(String code) {
return code.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;");
}
private static String getSourceForTheme(String theme) {
return String.format("<link rel=\"stylesheet\" href=\"./highlightjs/themes/%s.css\">", theme);
}
private static String getSourceForLanguage(String source, String language) {
if (language != null) {
return String.format("<pre><code class=\"%s\">%s</code></pre>", language, escapeCode(source));
} else {
return String.format("<pre><code>%s</code></pre>", escapeCode(source));
}
}
}

View File

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
@ -98,12 +99,12 @@
</LinearLayout>
<org.mian.gitnex.helpers.highlightjs.HighlightJsView
<org.mian.gitnex.helpers.SyntaxHighlightedArea
android:id="@+id/contents"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="14sp"
android:visibility="gone" />
android:visibility="gone"
tools:visibility="visible" />
<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/photo_view"

View File

@ -1,107 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/layoutFileView"
android:orientation="vertical"
android:background="?attr/primaryBackgroundColor">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Widget.AppCompat.SearchView">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/primaryBackgroundColor"
android:theme="@style/AppTheme.AppBarOverlay">
<ImageView
android:id="@+id/close"
android:layout_width="@dimen/close_button_size"
android:layout_height="@dimen/close_button_size"
android:layout_marginRight="15dp"
android:layout_marginLeft="15dp"
android:gravity="center_vertical"
android:contentDescription="@string/close"
android:src="@drawable/ic_close" />
<TextView
android:id="@+id/toolbar_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:text="@string/fileViewerHeader"
android:textColor="?attr/primaryTextColor"
android:maxLines="1"
android:textSize="20sp" />
</com.google.android.material.appbar.MaterialToolbar>
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:id="@+id/sourceCodeThemeFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="vertical">
<TextView
android:id="@+id/sourceCodeThemeHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:layout_marginTop="10dp"
android:layout_marginStart="44dp"
android:layout_marginEnd="24dp"
android:text="@string/settingsFileViewerSourceCodeHeaderText"
android:textColor="?attr/primaryTextColor"/>
<TextView
android:id="@+id/sourceCodeThemeSelected"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:layout_marginStart="44dp"
android:layout_marginEnd="24dp"
android:textColor="?attr/selectedTextColor"/>
</LinearLayout>
<RelativeLayout
android:id="@+id/pdfMode"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="25dp"
android:orientation="horizontal">
<TextView
android:id="@+id/pdfModeHeader"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:layout_marginStart="44dp"
android:layout_marginEnd="24dp"
android:text="@string/settingsPdfModeHeaderText"
android:textColor="?attr/primaryTextColor"/>
<com.google.android.material.switchmaterial.SwitchMaterial
android:id="@+id/switchPdfMode"
android:layout_toEndOf="@+id/pdfModeHeader"
android:layout_width="wrap_content"
android:layout_height="24dp"
android:switchMinWidth="56dp"
android:paddingStart="0dp"
android:paddingEnd="25dp"
android:layout_alignParentEnd="true"
android:gravity="end"
android:layout_gravity="end" />
</RelativeLayout>
</LinearLayout>

View File

@ -81,38 +81,6 @@
</LinearLayout>
<LinearLayout
android:id="@+id/fileViewerFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="25dp">
<TextView
android:id="@+id/fileViewer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="24dp"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:text="@string/fileViewerHeader"
android:textColor="?attr/primaryTextColor"
android:textSize="16sp"
app:drawableStartCompat="@drawable/ic_file" />
<TextView
android:id="@+id/fileViewerHintText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingStart="60dp"
android:paddingEnd="12dp"
android:text="@string/fileViewerHintText"
android:textColor="?attr/primaryTextColor"
android:textSize="12sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/draftsFrame"
android:layout_width="match_parent"

View File

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fileFrame"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"

View File

@ -639,7 +639,7 @@
<string name="appearanceHintText">Themes, fonts, badges</string>
<string name="fileViewerHintText">PDF mode, source code theme</string>
<string name="securityHintText">SSL certificates, cache</string>
<string name="securityHintText">Biometric authentication, SSL certificates, cache</string>
<string name="languagesHintText">Languages</string>
<string name="reportsHintText">Crash reports</string>
<string name="rateAppHintText">If you like GitNex you can give it a thumbs up</string>