12 | * Contains a list of all of the Articles that are linked to a certain Article through one
13 | * of the three types of links (Connection, Membership, or Residence).
14 | *
15 | */
16 | public class LinkedArticleList implements Serializable {
17 |
18 | private HashSet personNames = new HashSet<>();
19 | private HashSet groupNames = new HashSet<>();
20 | private HashSet placeNames = new HashSet<>();
21 | private HashSet itemNames = new HashSet<>();
22 | private HashSet conceptNames = new HashSet<>();
23 |
24 | /**
25 | * Adds an Article to this list.
26 | * @param category The {@link Category} of the Article.
27 | * @param name The name of the Article.
28 | */
29 | public void addArticle(Category category, String name) {
30 | switch (category) {
31 | case Person:
32 | personNames.add(name);
33 | break;
34 | case Group:
35 | groupNames.add(name);
36 | break;
37 | case Place:
38 | placeNames.add(name);
39 | break;
40 | case Item:
41 | itemNames.add(name);
42 | break;
43 | case Concept:
44 | default:
45 | conceptNames.add(name);
46 | break;
47 | }
48 | }
49 |
50 | /**
51 | * Gets all Article links within a certain Category.
52 | * @param category The {@link Category} of Articles to return.
53 | * @return A HashSet containing all Article links of the specified Category.
54 | */
55 | public HashSet getAllLinksInCategory(Category category) {
56 | switch (category) {
57 | case Person:
58 | return personNames;
59 | case Group:
60 | return groupNames;
61 | case Place:
62 | return placeNames;
63 | case Item:
64 | return itemNames;
65 | case Concept:
66 | default:
67 | return conceptNames;
68 | }
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/app/src/main/java/com/averi/worldscribe/utilities/AttributeGetter.java:
--------------------------------------------------------------------------------
1 | package com.averi.worldscribe.utilities;
2 |
3 | import android.content.Context;
4 | import android.content.res.Resources;
5 | import android.content.res.TypedArray;
6 | import android.graphics.Color;
7 | import android.util.TypedValue;
8 |
9 | /**
10 | * Created by mark on 24/06/16.
11 | */
12 | public class AttributeGetter {
13 |
14 | public static int getColorAttribute(Context context, int colorID) {
15 | TypedValue typedValue = new TypedValue();
16 | Resources.Theme theme = context.getTheme();
17 | theme.resolveAttribute(colorID, typedValue, true);
18 | return typedValue.data;
19 | }
20 |
21 | /**
22 | * Get a color attribute from a specific style.
23 | * @param context The Context calling this method.
24 | * @param styleID The ID of the style resource to retrieve the color from.
25 | * @param colorID The ID of the color attribute.
26 | * @return The resource ID of the color attribute as specified by the style.
27 | */
28 | public static int getColorAttribute(Context context, int styleID, int colorID) {
29 | TypedArray typedArray = context.obtainStyledAttributes(styleID, new int[]{colorID});
30 | int colorResource = typedArray.getColor(0, Color.BLACK);
31 | typedArray.recycle();
32 | return colorResource;
33 | }
34 |
35 | /**
36 | * Get the name of a style.
37 | * @param context The Context calling this method.
38 | * @param styleID The ID of the style resource to retrieve the name from.
39 | * @return The name of the specified style.
40 | */
41 | public static String getStyleName(Context context, int styleID) {
42 | TypedArray typedArray = context.obtainStyledAttributes(styleID,
43 | new int[]{android.R.attr.name});
44 | String styleName = typedArray.getString(0);
45 | typedArray.recycle();
46 | return styleName;
47 | }
48 |
49 | /**
50 | * Gets the style name of a Theme.
51 | * @param theme A Theme object.
52 | * @return The Theme's style name.
53 | */
54 | public static String getStyleName(Resources.Theme theme) {
55 | TypedValue attributeValue = new TypedValue();
56 | theme.resolveAttribute(android.R.attr.name, attributeValue, true);
57 | return (String) attributeValue.string;
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 | 14sp
6 | 18sp
7 | 14sp
8 | 20dp
9 | 10sp
10 | 10dp
11 | 48sp
12 | 3sp
13 | 3dp
14 | 8dp
15 | 10dp
16 | 3dp
17 | 10dp
18 | 8dp
19 | 55dp
20 | 15dp
21 | 22sp
22 | 40dp
23 | 22sp
24 | 50sp
25 | 50dp
26 | 5dp
27 | 15dp
28 | 3dp
29 | 2dp
30 |
31 | 192dp
32 | 192dp
33 |
34 | 22sp
35 |
36 | 5dp
37 | 16sp
38 | 22sp
39 | 22sp
40 | 5dp
41 | 22sp
42 | 10dp
43 | 5dp
44 | 10dp
45 | 3dp
46 | 70dp
47 | 13dp
48 | 8dp
49 |
50 | 100dp
51 |
52 |
--------------------------------------------------------------------------------
/app/src/main/java/com/averi/worldscribe/utilities/tasks/SaveConnectionTask.java:
--------------------------------------------------------------------------------
1 | package com.averi.worldscribe.utilities.tasks;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.documentfile.provider.DocumentFile;
6 |
7 | import com.averi.worldscribe.Connection;
8 | import com.averi.worldscribe.WorldScribeApplication;
9 | import com.averi.worldscribe.utilities.ExternalWriter;
10 |
11 | import java.io.IOException;
12 | import java.util.concurrent.Callable;
13 |
14 | public class SaveConnectionTask implements Callable {
15 | private final Connection connection;
16 |
17 | /**
18 | * Instantiates a SaveConnectionTask for saving a Connection to external storage.
19 | * @param connection The Connection to save
20 | */
21 | public SaveConnectionTask(Connection connection) {
22 | this.connection = connection;
23 | }
24 |
25 | @Override
26 | public Void call() throws IOException {
27 | Context context = WorldScribeApplication.getAppContext();
28 |
29 | String mainArticleConnectionFilepath = connection.worldName + "/"
30 | + connection.articleCategory.pluralName(context) + "/"
31 | + connection.articleName + "/Connections/"
32 | + connection.connectedArticleCategory.pluralName(context) + "/"
33 | + connection.connectedArticleName + ".txt";
34 | DocumentFile mainArticleConnectionFile = TaskUtils.getFile(mainArticleConnectionFilepath,
35 | "text/plain");
36 | if (!(ExternalWriter.writeStringToFile(context, mainArticleConnectionFile, connection.articleRelation))) {
37 | throw new IOException("Could not write to file 'WorldScribe/" + mainArticleConnectionFilepath + "'");
38 | }
39 |
40 | String connectedArticleConnectionFilepath = connection.worldName + "/"
41 | + connection.connectedArticleCategory.pluralName(context) + "/"
42 | + connection.connectedArticleName + "/Connections/"
43 | + connection.articleCategory.pluralName(context) + "/"
44 | + connection.articleName + ".txt";
45 | DocumentFile connectedArticleConnectionFile = TaskUtils.getFile(connectedArticleConnectionFilepath,
46 | "text/plain");
47 | if (!(ExternalWriter.writeStringToFile(context, connectedArticleConnectionFile, connection.connectedArticleRelation))) {
48 | throw new IOException("Could not write to file 'WorldScribe/" + connectedArticleConnectionFilepath + "'");
49 | }
50 |
51 | return null;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/Flipper/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/erasable_item_card.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
20 |
21 |
30 |
31 |
39 |
40 |
41 |
42 |
48 |
49 |
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/app/src/main/java/com/averi/worldscribe/activities/PrivacyPolicyActivity.java:
--------------------------------------------------------------------------------
1 | package com.averi.worldscribe.activities;
2 |
3 | import android.content.DialogInterface;
4 | import android.content.Intent;
5 | import android.content.SharedPreferences;
6 | import android.os.Bundle;
7 | import android.view.LayoutInflater;
8 | import android.view.View;
9 | import android.view.ViewGroup;
10 |
11 | import androidx.appcompat.app.AlertDialog;
12 |
13 | import com.averi.worldscribe.R;
14 | import com.averi.worldscribe.utilities.ActivityUtilities;
15 | import com.averi.worldscribe.utilities.AppPreferences;
16 |
17 | public class PrivacyPolicyActivity extends ThemedActivity {
18 |
19 | SharedPreferences preferences;
20 |
21 | @Override
22 | protected void onCreate(Bundle savedInstanceState) {
23 | super.onCreate(savedInstanceState);
24 |
25 | preferences = getSharedPreferences("com.averi.worldscribe", MODE_PRIVATE);
26 |
27 | AlertDialog.Builder builder = ActivityUtilities.getThemedDialogBuilder(this,
28 | nightModeIsEnabled());
29 | LayoutInflater inflater = this.getLayoutInflater();
30 | View content = inflater.inflate(R.layout.announcements_dialog, null);
31 | final AlertDialog dialog = builder.setView(content)
32 | .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
33 | @Override
34 | public void onClick(DialogInterface dialog, int id) {
35 | if (preferences.getBoolean(AppPreferences.HAS_AGREED_TO_PRIVACY_POLICY, false)) {
36 | goToPermissionsActivity();
37 | }
38 | }
39 | }).create();
40 | dialog.show();
41 | }
42 |
43 | @Override
44 | protected int getLayoutResourceID() {
45 | return R.layout.activity_privacy_policy;
46 | }
47 |
48 | @Override
49 | protected ViewGroup getRootLayout() {
50 | return findViewById(R.id.linearScreen);
51 | }
52 |
53 | public void handleTapAgreeButton(View agreeButton) {
54 | preferences.edit()
55 | .putBoolean(AppPreferences.HAS_AGREED_TO_PRIVACY_POLICY, true)
56 | .apply();
57 | goToPermissionsActivity();
58 | }
59 |
60 | public void handleTapDisagreeButton(View disagreeButton) {
61 | finish();
62 | }
63 |
64 | private void goToPermissionsActivity() {
65 | Intent goToPermissionsIntent = new Intent(this, PermissionActivity.class);
66 | startActivity(goToPermissionsIntent);
67 | finish();
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/app/src/main/java/com/averi/worldscribe/views/ArticleSectionCollapser.java:
--------------------------------------------------------------------------------
1 | package com.averi.worldscribe.views;
2 |
3 | import android.content.Context;
4 | import android.view.View;
5 | import android.view.ViewGroup;
6 | import android.widget.TextView;
7 |
8 | import com.averi.worldscribe.R;
9 |
10 | /**
11 | * Created by mark on 28/08/16.
12 | *
13 | * An onClickListener for Article section TextViews, allowing them to collapse and expand their
14 | * respective sections when clicked.
15 | */
16 | public class ArticleSectionCollapser implements View.OnClickListener {
17 |
18 | public static final int DEFAULT_SECTION_VISIBILITY = View.GONE;
19 |
20 | private Context context;
21 | private String sectionName;
22 | private TextView sectionHeader;
23 | private ViewGroup sectionLayout;
24 |
25 | /**
26 | * Instantiates a new ArticleSectionCollapser.
27 | * @param context The Context the Article section belongs to.
28 | * @param sectionHeader The TextView displaying the section's name.
29 | * @param sectionLayout The layout containing the actual content of the section.
30 | */
31 | public ArticleSectionCollapser(Context context, TextView sectionHeader,
32 | ViewGroup sectionLayout) {
33 | this.context = context;
34 | this.sectionName = sectionHeader.getText().toString();
35 | this.sectionHeader = sectionHeader;
36 | this.sectionLayout = sectionLayout;
37 |
38 | sectionLayout.setVisibility(DEFAULT_SECTION_VISIBILITY);
39 | updateCollapseIcon();
40 | }
41 |
42 | @Override
43 | public void onClick(View view) {
44 | toggleSectionLayoutVisiblity();
45 | updateCollapseIcon();
46 | }
47 |
48 | /**
49 | * Toggles the visibility of the section's layout.
50 | */
51 | private void toggleSectionLayoutVisiblity() {
52 | if (sectionLayout.getVisibility() == View.VISIBLE) {
53 | sectionLayout.setVisibility(View.GONE);
54 | } else {
55 | sectionLayout.setVisibility(View.VISIBLE);
56 | }
57 | }
58 |
59 | /**
60 | * Changes the icon beside the header to show whether the section is currently
61 | * collapsed or expanded.
62 | */
63 | private void updateCollapseIcon() {
64 | if (sectionLayout.getVisibility() == View.VISIBLE) {
65 | sectionHeader.setText(context.getString(R.string.expandedSectionHeader,
66 | sectionName));
67 | } else {
68 | sectionHeader.setText(context.getString(R.string.collapsedSectionHeader,
69 | sectionName));
70 | }
71 | }
72 |
73 | }
74 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/list_menu.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/app/src/main/java/com/averi/worldscribe/utilities/tasks/LoadExternalImageTask.java:
--------------------------------------------------------------------------------
1 | package com.averi.worldscribe.utilities.tasks;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 |
6 | import androidx.documentfile.provider.DocumentFile;
7 |
8 | import com.averi.worldscribe.WorldScribeApplication;
9 | import com.averi.worldscribe.utilities.ImageDecoder;
10 |
11 | import java.io.FileNotFoundException;
12 | import java.util.Arrays;
13 | import java.util.List;
14 | import java.util.concurrent.Callable;
15 |
16 | public class LoadExternalImageTask implements Callable {
17 | private final String imagePath;
18 | private final int imageWidth;
19 | private final int imageHeight;
20 |
21 | /**
22 | * Instantiates a LoadImageTask for retrieving an external image as a Bitmap.
23 | * If the given image cannot be found, the task returns null.
24 | * @param imagePath The filepath of the image whose contents will be
25 | * read. This filepath is relative to the app folder.
26 | * For example, "World1/Image.jpg" would retrieve the contents
27 | * of "/storage/emulated/0/WorldScribe/World1/Image.jpg".
28 | * @param imageWidth Width to resize the image to, in pixels.
29 | * @param imageHeight Height to resize the image to, in pixels.
30 | */
31 | public LoadExternalImageTask(String imagePath, int imageWidth, int imageHeight) {
32 | this.imagePath = imagePath;
33 | this.imageWidth = imageWidth;
34 | this.imageHeight = imageHeight;
35 | }
36 |
37 | @Override
38 | public Bitmap call() throws FileNotFoundException {
39 | DocumentFile imageFile = TaskUtils.getFile(imagePath, null);
40 |
41 | // Some external images might have a dot prepended to the file name.
42 | // See issue #8 to see why only some image files have a dot.
43 | if (imageFile == null) {
44 | List pathTokens = Arrays.asList(imagePath.split("/"));
45 | StringBuilder stringBuilder = new StringBuilder();
46 | for (String token : pathTokens.subList(0, pathTokens.size() - 1)) {
47 | stringBuilder.append(token);
48 | stringBuilder.append("/");
49 | }
50 | String dotFilename = "." + pathTokens.get(pathTokens.size() - 1);
51 | stringBuilder.append(dotFilename);
52 | String dotFilepath = stringBuilder.toString();
53 |
54 | imageFile = TaskUtils.getFile(dotFilepath, null);
55 | if (imageFile == null) {
56 | return null;
57 | }
58 | }
59 |
60 | Context context = WorldScribeApplication.getAppContext();
61 | return ImageDecoder.decodeBitmapFromFile(context, imageFile, imageWidth, imageHeight);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/src/main/java/com/averi/worldscribe/activities/NextcloudLoginActivity.java:
--------------------------------------------------------------------------------
1 | package com.averi.worldscribe.activities;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.os.Bundle;
6 | import android.view.View;
7 | import android.view.ViewGroup;
8 | import android.widget.Button;
9 | import android.widget.EditText;
10 | import android.widget.Toast;
11 |
12 | import androidx.appcompat.widget.Toolbar;
13 |
14 | import com.averi.worldscribe.R;
15 |
16 | public class NextcloudLoginActivity extends BackButtonActivity implements View.OnClickListener {
17 |
18 | public static final String SERVER = "server";
19 | public static final String USERNAME = "username";
20 | public static final String PASSWORD = "password";
21 |
22 | private EditText Username;
23 | private EditText Password;
24 | private EditText Server;
25 |
26 | @Override
27 | protected void onCreate(Bundle savedInstanceState) {
28 | super.onCreate(savedInstanceState);
29 |
30 | Button login = (Button)findViewById(R.id.loginButton);
31 | login.setOnClickListener(this);
32 |
33 | Username = (EditText)findViewById(R.id.username);
34 | Password = (EditText)findViewById(R.id.password);
35 | Server = (EditText)findViewById(R.id.serverAddr);
36 |
37 | Server.setText(getIntent().getStringExtra(SERVER));
38 | Username.setText(getIntent().getStringExtra(USERNAME));
39 | }
40 |
41 | @Override
42 | protected void setAppBar() {
43 | Toolbar myToolbar = (Toolbar) findViewById(R.id.my_toolbar);
44 | assert myToolbar != null;
45 | myToolbar.setTitle(R.string.settingsTitle);
46 | setSupportActionBar(myToolbar);
47 |
48 | super.setAppBar();
49 | }
50 |
51 | @Override
52 | protected int getLayoutResourceID() {
53 | return R.layout.activity_nextcloud_login;
54 | }
55 |
56 | @Override
57 | protected ViewGroup getRootLayout() {
58 | return (ViewGroup) findViewById(R.id.root);
59 | }
60 |
61 | @Override
62 | public void onClick(View v) {
63 |
64 | if(Username.getText().toString().isEmpty() ||
65 | Password.getText().toString().isEmpty() ||
66 | Server.getText().toString().isEmpty())
67 | {
68 | Toast.makeText(this, getString(R.string.emptyFields), Toast.LENGTH_SHORT).show();
69 | }
70 | else
71 | {
72 | Intent resultIntent = new Intent();
73 | resultIntent.putExtra(USERNAME, Username.getText().toString());
74 | resultIntent.putExtra(PASSWORD, Password.getText().toString());
75 | resultIntent.putExtra(SERVER, Server.getText().toString());
76 |
77 |
78 | setResult(Activity.RESULT_OK, resultIntent);
79 | finish();
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'com.android.application'
2 |
3 | // Create a variable called keystorePropertiesFile, and initialize it to your
4 | // keystore.properties file, in the rootProject folder.
5 | def keystorePropertiesFile = rootProject.file("keystore.properties")
6 |
7 | // Initialize a new Properties() object called keystoreProperties.
8 | def keystoreProperties = new Properties()
9 |
10 | // Load your keystore.properties file into the keystoreProperties object.
11 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
12 |
13 | android {
14 | compileSdkVersion 33
15 | compileOptions {
16 | sourceCompatibility JavaVersion.VERSION_1_8
17 | targetCompatibility JavaVersion.VERSION_1_8
18 | }
19 | signingConfigs {
20 | config {
21 | keyAlias keystoreProperties['keyAlias']
22 | keyPassword keystoreProperties['keyPassword']
23 | storeFile file(keystoreProperties['storeFile'])
24 | storePassword keystoreProperties['storePassword']
25 | }
26 | }
27 | defaultConfig {
28 | applicationId "com.averi.worldscribe"
29 | minSdkVersion 19
30 | targetSdkVersion 35
31 | versionCode 30
32 | versionName "1.9.0"
33 | multiDexEnabled true
34 | signingConfig signingConfigs.config
35 | }
36 | buildTypes {
37 | release {
38 | minifyEnabled false
39 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
40 | }
41 | }
42 | productFlavors {
43 | }
44 | }
45 |
46 | repositories {
47 | maven { url "https://jitpack.io" }
48 | }
49 |
50 | dependencies {
51 | implementation fileTree(include: ['*.jar'], dir: 'libs')
52 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
53 | testImplementation 'junit:junit:4.12'
54 | implementation 'androidx.appcompat:appcompat:1.0.0'
55 | implementation 'androidx.cardview:cardview:1.0.0'
56 | implementation 'androidx.recyclerview:recyclerview:1.0.0'
57 | implementation 'com.google.android.material:material:1.0.0'
58 | implementation 'com.android.support:multidex:1.0.3'
59 | implementation "commons-httpclient:commons-httpclient:3.1@jar"
60 | implementation project(':flipper')
61 | def lifecycle_version = "2.2.0"
62 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
63 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
64 | implementation("com.github.nextcloud:android-library:2.13.0") {
65 | exclude group: 'org.ogce', module: 'xpp3' // unused in Android and brings wrong Junit version
66 | }
67 | api 'com.theartofdev.edmodo:android-image-cropper:2.8.0'
68 | api 'com.google.guava:guava:28.0-jre'
69 | api 'com.dropbox.core:dropbox-core-sdk:3.1.5'
70 | api 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
71 | }
72 |
--------------------------------------------------------------------------------
/app/src/main/java/com/averi/worldscribe/viewmodels/PersonViewModel.java:
--------------------------------------------------------------------------------
1 | package com.averi.worldscribe.viewmodels;
2 |
3 | import android.util.Log;
4 |
5 | import androidx.lifecycle.MutableLiveData;
6 | import androidx.lifecycle.ViewModel;
7 |
8 | import com.averi.worldscribe.Category;
9 | import com.averi.worldscribe.Membership;
10 | import com.averi.worldscribe.Residence;
11 | import com.averi.worldscribe.utilities.TaskRunner;
12 | import com.averi.worldscribe.utilities.tasks.GetFilenamesInFolderTask;
13 | import com.averi.worldscribe.utilities.tasks.GetMembershipsTask;
14 |
15 | import java.util.ArrayList;
16 |
17 | public class PersonViewModel extends ViewModel {
18 | private final TaskRunner taskRunner = new TaskRunner();
19 | private final MutableLiveData> memberships = new MutableLiveData<>(null);
20 | private final MutableLiveData> residences = new MutableLiveData<>(null);
21 | private final MutableLiveData errorMessage = new MutableLiveData<>("");
22 |
23 | public MutableLiveData> getMemberships() { return memberships; }
24 |
25 | public MutableLiveData> getResidences() {
26 | return residences;
27 | }
28 |
29 | public void loadMemberships(String worldName, String groupName) {
30 | memberships.postValue(null);
31 | taskRunner.executeAsync(new GetMembershipsTask(worldName, Category.Person, groupName),
32 | memberships::postValue, this::handleLoadError);
33 | }
34 |
35 | public void loadResidences(String worldName, String personName) {
36 | String residencesFolderPath = worldName + "/People/" + personName + "/" + "Residences";
37 |
38 | residences.postValue(null);
39 | taskRunner.executeAsync(new GetFilenamesInFolderTask(residencesFolderPath, true),
40 | (residenceNames) -> onLoadResidenceNames(worldName, personName, residenceNames),
41 | this::handleLoadError);
42 | }
43 |
44 | private void onLoadResidenceNames(String worldName, String residentName, ArrayList residenceNames) {
45 | ArrayList updatedResidences = new ArrayList<>();
46 | for (String placeName : residenceNames) {
47 | Residence residence = new Residence();
48 | residence.worldName = worldName;
49 | residence.placeName = placeName;
50 | residence.residentName = residentName;
51 | updatedResidences.add(residence);
52 | }
53 | residences.postValue(updatedResidences);
54 | }
55 |
56 | public MutableLiveData getErrorMessage() {
57 | return errorMessage;
58 | }
59 |
60 | public void clearErrorMessage() {
61 | errorMessage.postValue("");
62 | }
63 |
64 | private void handleLoadError(Exception exception) {
65 | errorMessage.postValue(Log.getStackTraceString(exception));
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/app/src/main/java/com/averi/worldscribe/activities/LoadWorldActivity.java:
--------------------------------------------------------------------------------
1 | package com.averi.worldscribe.activities;
2 |
3 | import android.os.Bundle;
4 | import androidx.coordinatorlayout.widget.CoordinatorLayout;
5 | import androidx.recyclerview.widget.LinearLayoutManager;
6 | import androidx.recyclerview.widget.RecyclerView;
7 | import androidx.appcompat.widget.Toolbar;
8 | import android.view.View;
9 | import android.view.ViewGroup;
10 | import android.widget.TextView;
11 |
12 | import com.averi.worldscribe.R;
13 | import com.averi.worldscribe.adapters.StringListAdapter;
14 | import com.averi.worldscribe.adapters.StringListContext;
15 | import com.averi.worldscribe.utilities.ActivityUtilities;
16 | import com.averi.worldscribe.utilities.ExternalReader;
17 |
18 | import java.util.ArrayList;
19 |
20 | public class LoadWorldActivity extends BackButtonActivity implements StringListContext {
21 |
22 | private CoordinatorLayout coordinatorLayout;
23 | private RecyclerView recyclerView;
24 | private TextView textEmpty;
25 |
26 | @Override
27 | protected void onCreate(Bundle savedInstanceState) {
28 | super.onCreate(savedInstanceState);
29 |
30 | coordinatorLayout = (CoordinatorLayout) findViewById(R.id.coordinatorLayout);
31 | recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
32 | textEmpty = (TextView) findViewById(R.id.empty);
33 |
34 | setupRecyclerView();
35 | setAppBar();
36 | populateList();
37 | }
38 |
39 | private void setupRecyclerView() {
40 | recyclerView.setLayoutManager(new LinearLayoutManager(this));
41 | }
42 |
43 | @Override
44 | protected int getLayoutResourceID() {
45 | return R.layout.activity_load_world;
46 | }
47 |
48 | @Override
49 | protected ViewGroup getRootLayout() {
50 | return (ViewGroup) findViewById(R.id.coordinatorLayout);
51 | }
52 |
53 | @Override
54 | protected void setAppBar() {
55 | String title = this.getResources().getString(R.string.loadWorldTitle);
56 | setSupportActionBar((Toolbar) findViewById(R.id.my_toolbar));
57 |
58 | assert getSupportActionBar() != null;
59 | getSupportActionBar().setTitle(title);
60 |
61 | super.setAppBar();
62 | }
63 |
64 | private void populateList() {
65 | ArrayList worldNames = ExternalReader.getWorldList(this);
66 |
67 | StringListAdapter adapter = new StringListAdapter(this, worldNames);
68 | recyclerView.setAdapter(adapter);
69 |
70 | if (worldNames.isEmpty()) {
71 | textEmpty.setVisibility(View.VISIBLE);
72 | recyclerView.setVisibility(View.GONE);
73 | } else {
74 | textEmpty.setVisibility(View.GONE);
75 | recyclerView.setVisibility(View.VISIBLE);
76 | }
77 | }
78 |
79 | public void respondToListItemSelection(String itemName) {
80 | ActivityUtilities.goToWorld(this, itemName);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_article_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
20 |
21 |
25 |
35 |
36 |
37 |
46 |
47 |
58 |
59 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_select_article.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
21 |
22 |
26 |
36 |
37 |
38 |
47 |
48 |
59 |
60 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/snippet_card.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
20 |
21 |
30 |
31 |
39 |
40 |
41 |
42 |
48 |
49 |
58 |
59 |
68 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/app/src/main/java/com/averi/worldscribe/utilities/ImageDecoder.java:
--------------------------------------------------------------------------------
1 | package com.averi.worldscribe.utilities;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.graphics.BitmapFactory;
6 |
7 | import androidx.documentfile.provider.DocumentFile;
8 |
9 | import java.io.File;
10 | import java.io.FileNotFoundException;
11 |
12 | /**
13 | * Created by mark on 06/07/16.
14 | */
15 | public class ImageDecoder {
16 |
17 | /**
18 | * (Taken from https://developer.android.com/training/displaying-bitmaps/load-bitmap.html).
19 | * @param imageFile The file that will be decoded into a Bitmap.
20 | * @param reqWidth The minimum width; the Bitmap will be scaled so that its width is at least
21 | * this large.
22 | * @param reqHeight The minimum height; the Bitmap will be scaled so that its height is at least
23 | * this large.
24 | * @return A Bitmap for the image found in imageFile, scaled accordingly.
25 | */
26 | public static Bitmap decodeBitmapFromFile(Context context, DocumentFile imageFile, int reqWidth, int reqHeight)
27 | throws FileNotFoundException {
28 | // First decode with inJustDecodeBounds=true to check dimensions
29 | final BitmapFactory.Options options = new BitmapFactory.Options();
30 | options.inJustDecodeBounds = true;
31 | BitmapFactory.decodeStream(context.getContentResolver().openInputStream(imageFile.getUri()), null, options);
32 |
33 | // Calculate inSampleSize
34 | options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
35 |
36 | // Decode bitmap with inSampleSize set
37 | options.inJustDecodeBounds = false;
38 | return BitmapFactory.decodeStream(context.getContentResolver().openInputStream(imageFile.getUri()), null, options);
39 | }
40 |
41 | /**
42 | * Given a Bitmap's target dimensions, calculate a size value that is a power of two.
43 | * @param options The set of options used for decoding the Bitmap.
44 | * @param reqWidth The Bitmap's target width.
45 | * @param reqHeight The Bitmap's target height.
46 | * @return The appropriate size value to be passed to options.inSampleSize.
47 | */
48 | public static int calculateInSampleSize(
49 | BitmapFactory.Options options, int reqWidth, int reqHeight) {
50 | // Raw height and width of image
51 | final int height = options.outHeight;
52 | final int width = options.outWidth;
53 | int inSampleSize = 1;
54 |
55 | if (height > reqHeight || width > reqWidth) {
56 |
57 | final int halfHeight = height / 2;
58 | final int halfWidth = width / 2;
59 |
60 | // Calculate the largest inSampleSize value that is a power of 2 and keeps both
61 | // height and width larger than the requested height and width.
62 | while ((halfHeight / inSampleSize) > reqHeight
63 | && (halfWidth / inSampleSize) > reqWidth) {
64 | inSampleSize *= 2;
65 | }
66 | }
67 | return inSampleSize;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/main/java/com/averi/worldscribe/utilities/tasks/GetMembershipsTask.java:
--------------------------------------------------------------------------------
1 | package com.averi.worldscribe.utilities.tasks;
2 |
3 | import androidx.documentfile.provider.DocumentFile;
4 |
5 | import com.averi.worldscribe.Category;
6 | import com.averi.worldscribe.Membership;
7 |
8 | import java.io.FileNotFoundException;
9 | import java.io.IOException;
10 | import java.util.ArrayList;
11 | import java.util.concurrent.Callable;
12 |
13 | public class GetMembershipsTask implements Callable> {
14 | private final String worldName;
15 | private final String articleName;
16 | private final Category articleCategory;
17 |
18 | /**
19 | * Instantiates a {@link GetMembershipsTask} for retrieving the
20 | * {@link Membership}s associated with a Person or Group.
21 | * @param worldName The name of the World that the Person or Group belongs to.
22 | * @param articleName The name of the Person or Group.
23 | * @param articleCategory The Category of the Article whose Memberships will be retrieved.
24 | * Must be Category.Person or Category.Group.
25 | */
26 | public GetMembershipsTask(String worldName, Category articleCategory, String articleName) {
27 | this.worldName = worldName;
28 | this.articleName = articleName;
29 | this.articleCategory = articleCategory;
30 | }
31 |
32 | @Override
33 | public ArrayList call() throws IOException, IllegalArgumentException {
34 | ArrayList memberships = new ArrayList<>();
35 |
36 | String membershipsFolderPath;
37 | if (articleCategory == Category.Person) {
38 | membershipsFolderPath = worldName + "/People/" + articleName + "/Memberships";
39 | }
40 | else if (articleCategory == Category.Group) {
41 | membershipsFolderPath = worldName + "/Groups/" + articleName + "/Members";
42 | }
43 | else {
44 | throw new IllegalArgumentException("Attempted to load Memberships for an Article that was not a Person or a Group.");
45 | }
46 |
47 | DocumentFile membershipsFolder = TaskUtils.getFolder(membershipsFolderPath, true);
48 | if (membershipsFolder == null) {
49 | throw new FileNotFoundException("Could not access folder at 'WorldScribe/" + membershipsFolderPath + "'");
50 | }
51 |
52 | for (DocumentFile membershipFile : membershipsFolder.listFiles()) {
53 | Membership membership = new Membership();
54 | membership.worldName = worldName;
55 | membership.memberRole = TaskUtils.readFileContents(membershipFile);
56 | if (articleCategory == Category.Person) {
57 | membership.groupName = TaskUtils.stripFileExtension(membershipFile.getName());
58 | membership.memberName = articleName;
59 | }
60 | else {
61 | membership.groupName = articleName;
62 | membership.memberName = TaskUtils.stripFileExtension(membershipFile.getName());
63 | }
64 | memberships.add(membership);
65 | }
66 |
67 | return memberships;
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_permission.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
16 |
17 |
23 |
24 |
27 |
28 |
39 |
40 |
43 |
44 |
53 |
54 |
57 |
58 |
69 |
70 |
71 |
72 |
73 |
--------------------------------------------------------------------------------
/app/src/main/java/com/averi/worldscribe/utilities/WorldUtilities.java:
--------------------------------------------------------------------------------
1 | package com.averi.worldscribe.utilities;
2 |
3 | import android.content.Context;
4 | import android.content.DialogInterface;
5 | import android.content.Intent;
6 | import androidx.appcompat.app.AlertDialog;
7 | import android.widget.Toast;
8 |
9 | import com.averi.worldscribe.R;
10 | import com.averi.worldscribe.activities.CreateOrLoadWorldActivity;
11 | import com.averi.worldscribe.activities.CreateWorldActivity;
12 |
13 | /**
14 | * Created by mark on 12/08/19.
15 | */
16 | public class WorldUtilities {
17 |
18 | /**
19 | * Asks the user to confirm deletion of a certain World, then deletes the World once confirmed.
20 | * If an error occurs during the process, an error is displayed.
21 | * @param context The context calling this method.
22 | * @param worldName The name of the world to be deleted.
23 | */
24 | public static void deleteWorld(Context context, String worldName) {
25 | final Context listenerContext = context;
26 | final String listenerWorldName = worldName;
27 |
28 | new AlertDialog.Builder(context)
29 | .setTitle(context.getString(R.string.confirmWorldDeletionTitle, worldName))
30 | .setMessage(context.getString(R.string.confirmWorldDeletion, worldName))
31 | .setIcon(android.R.drawable.ic_dialog_alert)
32 | .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() {
33 |
34 | public void onClick(DialogInterface dialog, int whichButton) {
35 | boolean worldWasDeleted = ExternalDeleter.deleteWorld(context, listenerWorldName);
36 |
37 | if (worldWasDeleted) {
38 | goToNextActivityAfterWorldDeletion(listenerContext);
39 | } else {
40 | Toast.makeText(listenerContext,
41 | listenerContext.getString(R.string.deleteWorldError),
42 | Toast.LENGTH_SHORT).show();
43 | }
44 | }})
45 | .setNegativeButton(android.R.string.no, null).show();
46 | }
47 |
48 | /**
49 | * Goes to the appropriate Activity after deleting the World currently opened in the app.
50 | * If at least one other World exists in the app directory, the CreateOrLoadWorldActivity will
51 | * be opened.
52 | * If no Worlds exist after deleting the current one, CreateWorldActivity will be opened.
53 | * @param context The Context calling this method.
54 | */
55 | private static void goToNextActivityAfterWorldDeletion(Context context) {
56 | Intent nextActivityIntent;
57 |
58 | if (ExternalReader.worldListIsEmpty(context)) {
59 | nextActivityIntent = new Intent(context, CreateWorldActivity.class);
60 | } else {
61 | nextActivityIntent = new Intent(context, CreateOrLoadWorldActivity.class);
62 | }
63 |
64 | nextActivityIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
65 | context.startActivity(nextActivityIntent);
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/app/src/main/java/com/averi/worldscribe/Category.java:
--------------------------------------------------------------------------------
1 | package com.averi.worldscribe;
2 |
3 | import android.content.Context;
4 |
5 | import com.averi.worldscribe.exceptions.InvalidCategoryNameException;
6 |
7 | /**
8 | * Created by mark on 14/06/16.
9 | */
10 | public enum Category {
11 | Person,
12 | Group,
13 | Place,
14 | Item,
15 | Concept;
16 |
17 | /* We use hard-coded, non-localized English here because these functions are used for file operations.
18 | For consistency, all files and folders should have the same name on every decide. */
19 |
20 | public String name(Context context) {
21 | switch (this) {
22 | case Person:
23 | return "Person";
24 | case Group:
25 | return "Group";
26 | case Place:
27 | return "Place";
28 | case Item:
29 | return "Item";
30 | case Concept:
31 | default:
32 | return "Concept";
33 | }
34 | }
35 |
36 | /* Added for the menu titles of connections, because the text was a mixed of translation and english.
37 | This function is used in SelectArticleActivity@setAppBar*/
38 |
39 | public String translatedName(Context context) {
40 | switch (this) {
41 | case Person:
42 | return context.getResources().getString(R.string.personText);
43 | case Group:
44 | return context.getResources().getString(R.string.groupText);
45 | case Place:
46 | return context.getResources().getString(R.string.placeText);
47 | case Item:
48 | return context.getResources().getString(R.string.itemsText);
49 | case Concept:
50 | default:
51 | return context.getResources().getString(R.string.conceptText);
52 | }
53 | }
54 |
55 | public String pluralName(Context context) {
56 | switch (this) {
57 | case Person:
58 | return "People";
59 | case Group:
60 | return "Groups";
61 | case Place:
62 | return "Places";
63 | case Item:
64 | return "Items";
65 | case Concept:
66 | default:
67 | return "Concepts";
68 | }
69 | }
70 |
71 | public static Category getCategoryFromName(Context context, String categoryName) {
72 | if (categoryName.equals(context.getResources().getString(R.string.personText))) {
73 | return Person;
74 | } else if (categoryName.equals(context.getResources().getString(R.string.groupText))) {
75 | return Group;
76 | } else if (categoryName.equals(context.getResources().getString(R.string.placeText))) {
77 | return Place;
78 | } else if (categoryName.equals(context.getResources().getString(R.string.itemText))) {
79 | return Item;
80 | } else if (categoryName.equals(context.getResources().getString(R.string.conceptText))) {
81 | return Concept;
82 | } else {
83 | throw new InvalidCategoryNameException("An invalid Category name was given.");
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_create_snippet.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
12 |
21 |
22 |
23 |
31 |
32 |
41 |
42 |
52 |
53 |
56 |
57 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_create_world.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
12 |
21 |
22 |
23 |
31 |
32 |
42 |
43 |
53 |
54 |
57 |
58 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/app/src/main/java/com/averi/worldscribe/views/MarqueeToolbar.java:
--------------------------------------------------------------------------------
1 | package com.averi.worldscribe.views;
2 |
3 | import android.content.Context;
4 | import androidx.appcompat.widget.Toolbar;
5 | import android.text.TextUtils;
6 | import android.util.AttributeSet;
7 | import android.widget.TextView;
8 |
9 | import java.lang.reflect.Field;
10 |
11 | /**
12 | * A Marquee-able Android Toolbar.
13 | *
14 | *
15 | * Credit to InsanityOnABun. The Gist for this class can be found
16 | * here.
17 | *
18 | */
19 | public class MarqueeToolbar extends Toolbar {
20 |
21 | TextView title;
22 | boolean reflected = false;
23 |
24 | public MarqueeToolbar(Context context) {
25 | super(context);
26 | }
27 |
28 | public MarqueeToolbar(Context context, AttributeSet attrs) {
29 | super(context, attrs);
30 | }
31 |
32 | public MarqueeToolbar(Context context, AttributeSet attrs, int defStyleAttr) {
33 | super(context, attrs, defStyleAttr);
34 | }
35 |
36 | @Override
37 | public void setTitle(CharSequence title) {
38 | if (!reflected) {
39 | reflected = reflectTitle();
40 | }
41 | super.setTitle(title);
42 | // Due to postDelayed(), selectTitle() will cause an exception if it's called before
43 | // the title TextView has been created. In Activities, setSupportActionBar() might
44 | // call selectTitle() too early, thus causing an exception.
45 | // We can prevent this by checking reflected, which can only be set
46 | // to true if the title TextView exists.
47 | if (reflected) {
48 | selectTitle();
49 | }
50 | }
51 |
52 | @Override
53 | public void setTitle(int resId) {
54 | if (!reflected) {
55 | reflected = reflectTitle();
56 | }
57 | super.setTitle(resId);
58 | // Due to postDelayed(), selectTitle() will cause an exception if it's called before
59 | // the title TextView has been created. In Activities, setSupportActionBar() might
60 | // call selectTitle() too early, thus causing an exception.
61 | // We can prevent this by checking reflected, which can only be set
62 | // to true if the title TextView exists.
63 | if (reflected) {
64 | selectTitle();
65 | }
66 | }
67 |
68 | private boolean reflectTitle() {
69 | try {
70 | Field field = Toolbar.class.getDeclaredField("mTitleTextView");
71 | field.setAccessible(true);
72 | title = (TextView) field.get(this);
73 | title.setEllipsize(TextUtils.TruncateAt.MARQUEE);
74 | title.setMarqueeRepeatLimit(-1);
75 | return true;
76 | } catch (NoSuchFieldException e) {
77 | e.printStackTrace();
78 | return false;
79 | } catch (IllegalAccessException e) {
80 | e.printStackTrace();
81 | return false;
82 | } catch (NullPointerException e) {
83 | e.printStackTrace();
84 | return false;
85 | }
86 | }
87 |
88 | public void selectTitle() {
89 | title.postDelayed(new Runnable() {
90 | @Override
91 | public void run() {
92 | title.setSelected(true);
93 | }
94 | }, 1000);
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/membership_card.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
20 |
21 |
30 |
31 |
39 |
40 |
48 |
49 |
50 |
51 |
57 |
58 |
67 |
68 |
77 |
78 |
79 |
80 |
81 |
82 |
--------------------------------------------------------------------------------
/app/src/main/java/com/averi/worldscribe/utilities/LogErrorTask.java:
--------------------------------------------------------------------------------
1 | package com.averi.worldscribe.utilities;
2 |
3 | import android.content.Context;
4 | import android.os.AsyncTask;
5 | import androidx.annotation.Nullable;
6 | import androidx.documentfile.provider.DocumentFile;
7 |
8 | import android.util.Log;
9 |
10 | import com.balda.flipper.DocumentFileCompat;
11 |
12 | import java.io.IOException;
13 | import java.io.OutputStream;
14 | import java.io.PrintWriter;
15 | import java.text.SimpleDateFormat;
16 | import java.util.Date;
17 | import java.util.Locale;
18 |
19 | public class LogErrorTask extends AsyncTask {
20 | private static final String ERROR_LOG_FILE_NAME = "%s_applog.txt";
21 | private static final String EXCEPTION_LOG_MESSAGE = "Exception:\n%s\nStack Trace:\n";
22 |
23 | ErrorLoggingActivity activity;
24 | private String openingMessage;
25 | private Context context;
26 | private Exception exception;
27 |
28 | public LogErrorTask(ErrorLoggingActivity activity, String openingMessage, Context context,
29 | @Nullable Exception exception) {
30 | this.activity = activity;
31 | this.openingMessage = openingMessage;
32 | this.context = context;
33 | this.exception = exception;
34 | }
35 |
36 | @Override
37 | protected Object doInBackground(Object[] params) {
38 | DocumentFile errorLogFile = generateErrorLogFile();
39 |
40 | try {
41 | OutputStream outputStream = context.getContentResolver().openOutputStream(errorLogFile.getUri());
42 | PrintWriter errorLogPrintStream = new PrintWriter(outputStream);
43 |
44 | errorLogPrintStream.print(openingMessage + "\n");
45 |
46 | if (exception != null) {
47 | errorLogPrintStream.print("\n" + String.format(EXCEPTION_LOG_MESSAGE,
48 | exception.getMessage()));
49 | exception.printStackTrace(errorLogPrintStream);
50 | }
51 |
52 | errorLogPrintStream.close();
53 | activity.onErrorLoggingCompletion(openingMessage, errorLogFile);
54 | } catch (IOException exception) {
55 | //TODO: Handle this exception more elegantly
56 | Log.e("WorldScribe", exception.getMessage());
57 | }
58 |
59 | return errorLogFile;
60 | }
61 |
62 | /**
63 | * Generates an empty error log file for Dropbox error logging purposes.
64 | *
65 | * This new file will overwrite any existing error log files that were created on the
66 | * same day.
67 | *
68 | * @return An empty error log file whose file name is based on the current date
69 | */
70 | private DocumentFile generateErrorLogFile() {
71 | Date datum = new Date();
72 | SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
73 | String fullName = String.format(ERROR_LOG_FILE_NAME, df.format(datum));
74 |
75 | DocumentFile appDirectory = FileRetriever.getAppDirectory(context, false);
76 | DocumentFile errorLogFile = DocumentFileCompat.peekFile(appDirectory, fullName, null);
77 | if (errorLogFile != null) {
78 | errorLogFile.delete();
79 | }
80 | try {
81 | errorLogFile = appDirectory.createFile("text/plain", fullName);
82 | } catch (Exception e) {
83 | //TODO: Handle this exception more elegantly
84 | Log.e("WorldScribe", e.getMessage());
85 | }
86 |
87 | return errorLogFile;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/connection_card.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
20 |
21 |
30 |
31 |
41 |
42 |
50 |
51 |
52 |
53 |
59 |
60 |
69 |
70 |
79 |
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_snippet.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
21 |
22 |
26 |
35 |
36 |
37 |
40 |
41 |
45 |
46 |
63 |
64 |
65 |
72 |
73 |
74 |
75 |
76 |
--------------------------------------------------------------------------------
/app/src/main/res/directory_layout.txt~:
--------------------------------------------------------------------------------
1 | WorldScribe/
2 | World1/
3 | concepts/
4 | Concept1/
5 | connections/
6 | Concepts/
7 | article_name.txt
8 | Groups/
9 | article_name.txt
10 | Items/
11 | article_name.txt
12 | People/
13 | article_name.txt
14 | Places/
15 | article_name.txt
16 | snippets/
17 | snippet1.txt
18 | snippet2.txt
19 | description.txt
20 | image.jpg
21 | groups/
22 | Group1/
23 | connections/
24 | Concepts/
25 | article_name.txt
26 | Groups/
27 | article_name.txt
28 | Items/
29 | article_name.txt
30 | People/
31 | article_name.txt
32 | Places/
33 | article_name.txt
34 | snippets/
35 | snippet1.txt
36 | snippet2.txt
37 | image.jpg
38 | mandate.txt
39 | members.txt
40 | items/
41 | Item1/
42 | connections/
43 | Concepts/
44 | article_name.txt
45 | Groups/
46 | article_name.txt
47 | Items/
48 | article_name.txt
49 | People/
50 | article_name.txt
51 | Places/
52 | article_name.txt
53 | snippets/
54 | snippet1.txt
55 | snippet2.txt
56 | image.jpg
57 | history.txt
58 | properties.txt
59 | people/
60 | Person1/
61 | connections/
62 | Concepts/
63 | article_name.txt
64 | Groups/
65 | article_name.txt
66 | Items/
67 | article_name.txt
68 | People/
69 | article_name.txt
70 | Places/
71 | article_name.txt
72 | memberships/
73 | group1.txt
74 | group2.txt
75 | residences/
76 | place1.txt
77 | place2.txt
78 | snippets/
79 | snippet1.txt
80 | snippet2.txt
81 | aliases.txt
82 | bio.txt
83 | gender.txt
84 | image.jpg
85 | places/
86 | Place1/
87 | connections/
88 | Concepts/
89 | article_name.txt
90 | Groups/
91 | article_name.txt
92 | Items/
93 | article_name.txt
94 | People/
95 | article_name.txt
96 | Places/
97 | article_name.txt
98 | snippets/
99 | snippet1.txt
100 | snippet2.txt
101 | description.txt
102 | image.jpg
103 | inhabitants.txt
104 | history.txt
105 |
--------------------------------------------------------------------------------
/app/src/main/res/raw/announcements.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | [b]This is the final update for World Scribe for Android.[/b] It is with a
7 | heavy heart that we announce that further development of the official World Scribe
8 | Android app will be ceasing after this update. World Scribe began as a simple hobby
9 | project in 2016. At the time, we decided the app would use simple text files in external
10 | storage so that World data could be easily copied over to another device, or even edited
11 | in other apps. However, recent changes in Android 13 have severely limited access to
12 | external storage. As a result, World Scribe's core functionality is no longer compatible
13 | with Android. Making the app work with Android 13+ would require a rewrite of the app,
14 | and for a free project that we built in our spare time, this is not something that we
15 | can pursue at this time due to other life commitments. But that doesn't mean World
16 | Scribe is going away completely! We have also released [b][a href="https://worldscribe2.averistudios.com/"]World Scribe 2[/a][/b],
17 | a successor to World Scribe that you can use on your desktop PC. If you have an older
18 | Android device and would still like to use the Android app, you can download the APK
19 | installation files [b][a href="https://github.com/MarquisLP/World-Scribe/releases"]here[/a][/b].
20 | If you are an Android developer, feel free to fork the [b][a href="https://github.com/MarquisLP/World-Scribe"]source code repository[/a][/b]
21 | and develop your own version of World Scribe. [b]If your device is on Android 13 or
22 | later and the app is no longer working for you, you can recover your World data by
23 | searching for a "WorldScribe" folder in your device's file storage.[/b] Thank you so
24 | much for all of your support over the years, and we hope you continue to broaden the
25 | limits of imagination through world-building.
26 |
27 |
28 |
29 | [b]Performance issues on Android 10 and above have been fixed![/b] A large
30 | portion of the app was rewritten to make navigation faster and prevent the app from
31 | freezing. Thank you for your patience in waiting for this update. Because this is a large
32 | update, you may encounter errors. If you do, please let us know in an email, with
33 | screenshots if possible, at: support@averistudios.com
34 |
35 |
36 |
37 | [b]Please be aware that if you upgrade your device to Android 10 or Android
38 | 11, the app may perform significantly slower.[/b] This is due to changes by Google in
39 | the Android operating system that make file operations much slower. We are currently
40 | working to improve this situation, but we ask that you please be patient as this will
41 | require significant changes throughout the entire app. We deeply apologize to users who
42 | are already impacted by these performance issues.
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app/src/main/java/com/averi/worldscribe/adapters/StringListAdapter.java:
--------------------------------------------------------------------------------
1 | package com.averi.worldscribe.adapters;
2 |
3 | import androidx.recyclerview.widget.RecyclerView;
4 | import android.util.Log;
5 | import android.view.LayoutInflater;
6 | import android.view.View;
7 | import android.view.ViewGroup;
8 | import android.widget.TextView;
9 |
10 | import com.averi.worldscribe.R;
11 |
12 | import java.util.ArrayList;
13 | import java.util.Collections;
14 |
15 | /**
16 | * Created by mark on 15/06/16.
17 | */
18 | public class StringListAdapter extends RecyclerView.Adapter {
19 |
20 | private ArrayList strings;
21 | private ArrayList stringsCopy; // For adding back items during filtering
22 | private StringListContext context;
23 |
24 | public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
25 | public TextView textView;
26 | private StringListContext context;
27 |
28 | public ViewHolder(StringListContext context, TextView textView) {
29 | super(textView);
30 | this.textView = textView;
31 | this.context = context;
32 | textView.setOnClickListener(this);
33 | }
34 |
35 | @Override
36 | public void onClick(View view) {
37 | context.respondToListItemSelection(textView.getText().toString());
38 | }
39 | }
40 |
41 | public StringListAdapter(StringListContext context, ArrayList strings) {
42 | this.strings = new ArrayList<>(strings);
43 | this.stringsCopy = new ArrayList<>(strings);
44 | this.context = context;
45 | }
46 |
47 | @Override
48 | public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
49 | View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.list_item_text, parent,
50 | false);
51 | return new ViewHolder(context, (TextView) view);
52 | }
53 |
54 | @Override
55 | public void onBindViewHolder(ViewHolder holder, int position) {
56 | holder.textView.setText(strings.get(position));
57 | }
58 |
59 | @Override
60 | public int getItemCount() {
61 | return strings.size();
62 | }
63 |
64 | /**
65 | * Set a new list of strings as this Adapter's content.
66 | * @param strings The new contents held by this Adapter.
67 | */
68 | public void updateList(ArrayList strings) {
69 | this.strings.clear();
70 | this.stringsCopy.clear();
71 | this.strings.addAll(strings);
72 | this.stringsCopy.addAll(strings);
73 | Collections.sort(this.strings);
74 | Collections.sort(this.stringsCopy);
75 | }
76 |
77 | /**
78 | * Filters the items in this Adapter to include only items that contain the
79 | * specified query string.
80 | * @param query A string of text meant to match one or more items in this Adapter
81 | */
82 | public void filterQuery(String query) {
83 | strings.clear();
84 |
85 | if (query.isEmpty()) { // Empty query means user hasn't entered anything yet.
86 | Log.d("WorldScribe", String.valueOf(stringsCopy.size()));
87 | strings.addAll(stringsCopy);
88 | } else {
89 | query = query.toLowerCase(); // Searches are case-insensitive.
90 | for (String string : stringsCopy) {
91 | if (string.toLowerCase().contains(query)) {
92 | strings.add(string);
93 | }
94 | }
95 | }
96 |
97 | notifyDataSetChanged();
98 | }
99 |
100 | }
101 |
--------------------------------------------------------------------------------
/app/src/main/java/com/averi/worldscribe/activities/ReaderModeActivity.java:
--------------------------------------------------------------------------------
1 | package com.averi.worldscribe.activities;
2 |
3 | import android.os.Bundle;
4 | import android.view.Menu;
5 | import android.view.MenuItem;
6 | import android.view.ViewGroup;
7 |
8 | import com.averi.worldscribe.R;
9 | import com.averi.worldscribe.utilities.ActivityUtilities;
10 | import com.averi.worldscribe.utilities.ThemedSnackbar;
11 |
12 | /**
13 | *
14 | * Created by mark on 22/12/17.
15 | *
16 | *
17 | * An Activity that has Action Bar Buttons for toggling between Reader Mode and Editor Mode.
18 | *
19 | *
20 | * Subclasses MUST override {@link #onCreateOptionsMenu(Menu menu)} to inflate the appropriate
21 | * menu, and then return super.onCreateOptionsMenu(Menu menu).
22 | * The inflated menu MUST contain R.id.enableReaderModeItem and
23 | * R.id.enableEditorModeItem.
24 | *
25 | */
26 | public abstract class ReaderModeActivity extends BackButtonActivity {
27 |
28 | /**
29 | * Set to true if Reader Mode is currently enabled for this Activity.
30 | */
31 | private boolean readerModeIsEnabled = false;
32 | /**
33 | * Set to true if a Snackbar indicating the current mode should be shown the next time the
34 | * Options menu is created.
35 | */
36 | private boolean showCurrentModeSnackbar = false;
37 |
38 | @Override
39 | protected void onCreate(Bundle savedInstanceState) {
40 | super.onCreate(savedInstanceState);
41 | }
42 |
43 | @Override
44 | public boolean onOptionsItemSelected(MenuItem item) {
45 | switch (item.getItemId()) {
46 | case R.id.enableReaderModeItem: {
47 | ViewGroup rootLayout = this.getRootLayout();
48 | ActivityUtilities.toggleAllEditTexts(rootLayout, false);
49 | showCurrentModeSnackbar = true;
50 | readerModeIsEnabled = true;
51 | this.invalidateOptionsMenu(); // Reloads the items in the Action Bar
52 | return true;
53 | }
54 | case R.id.enableEditorModeItem: {
55 | ViewGroup rootLayout = this.getRootLayout();
56 | ActivityUtilities.toggleAllEditTexts(rootLayout, true);
57 | showCurrentModeSnackbar = true;
58 | readerModeIsEnabled = false;
59 | this.invalidateOptionsMenu(); // Reloads the items in the Action Bar
60 | return true;
61 | }
62 | default:
63 | return super.onOptionsItemSelected(item);
64 | }
65 | }
66 |
67 | @Override
68 | public boolean onCreateOptionsMenu(Menu menu) {
69 | // This is to prevent the Snackbar from showing up the first time the Activity is loaded,
70 | // or when the Activity is resumed.
71 | if (showCurrentModeSnackbar) {
72 | if (readerModeIsEnabled) {
73 | menu.findItem(R.id.enableReaderModeItem).setVisible(false);
74 | menu.findItem(R.id.enableEditorModeItem).setVisible(true);
75 | ThemedSnackbar.showSnackbarMessage(this, getRootLayout(),
76 | getString(R.string.readerModeEnabledMessage));
77 | showCurrentModeSnackbar = false;
78 | } else {
79 | menu.findItem(R.id.enableReaderModeItem).setVisible(true);
80 | menu.findItem(R.id.enableEditorModeItem).setVisible(false);
81 | ThemedSnackbar.showSnackbarMessage(this, getRootLayout(),
82 | getString(R.string.editorModeEnabledMessage));
83 | showCurrentModeSnackbar = false;
84 | }
85 | }
86 |
87 | return true;
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/Flipper/flipper/src/main/java/com/balda/flipper/FileDescription.java:
--------------------------------------------------------------------------------
1 | package com.balda.flipper;
2 |
3 | import android.content.Context;
4 | import android.net.Uri;
5 | import android.os.Parcel;
6 | import android.os.Parcelable;
7 | import android.webkit.MimeTypeMap;
8 |
9 | import java.io.File;
10 |
11 | import androidx.annotation.NonNull;
12 | import androidx.annotation.Nullable;
13 | import androidx.documentfile.provider.DocumentFile;
14 |
15 | @SuppressWarnings("unused")
16 | public class FileDescription implements Parcelable {
17 |
18 | private String name;
19 | private String mime;
20 | @Nullable
21 | private Uri uri;
22 |
23 | public FileDescription(@NonNull String name, @NonNull String mime) {
24 | this.name = name;
25 | this.mime = mime;
26 | }
27 |
28 | public FileDescription(@NonNull Context context, @NonNull Uri file) {
29 | //noinspection ConstantConditions
30 | this(DocumentFile.fromSingleUri(context, file));
31 | }
32 |
33 | public FileDescription(@NonNull DocumentFile file) {
34 | this.name = file.getName();
35 | this.mime = file.getType();
36 | this.uri = file.getUri();
37 | }
38 |
39 | public FileDescription(@NonNull File file) {
40 | this.name = file.getName();
41 | int len = name.indexOf(".");
42 | if (len == -1) {
43 | this.mime = "*/*";
44 | } else {
45 | this.mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(name.substring(len));
46 | }
47 | }
48 |
49 | protected FileDescription(Parcel in) {
50 | name = in.readString();
51 | mime = in.readString();
52 | if (in.readInt() == 1) {
53 | uri = in.readParcelable(Uri.class.getClassLoader());
54 | }
55 | }
56 |
57 | public static final Creator CREATOR = new Creator() {
58 | @Override
59 | public FileDescription createFromParcel(Parcel in) {
60 | return new FileDescription(in);
61 | }
62 |
63 | @Override
64 | public FileDescription[] newArray(int size) {
65 | return new FileDescription[size];
66 | }
67 | };
68 |
69 | public String getName() {
70 | return name;
71 | }
72 |
73 | public void setName(String name) {
74 | if (name != null)
75 | this.name = name;
76 | }
77 |
78 | public String getMime() {
79 | return mime;
80 | }
81 |
82 | public void setMime(String mime) {
83 | if (mime != null)
84 | this.mime = mime;
85 | }
86 |
87 | @Nullable
88 | public Uri getUri() {
89 | return uri;
90 | }
91 |
92 | public void setUri(@Nullable Uri uri) {
93 | this.uri = uri;
94 | }
95 |
96 | /**
97 | * It returns the name with extension according to mime type set
98 | *
99 | * @return The full name example myimage.png
100 | */
101 | public String getFullName() {
102 | if (name.contains("."))
103 | return name;
104 | String ext = MimeTypeMap.getSingleton().getExtensionFromMimeType(mime);
105 | if (ext != null)
106 | return name + "." + ext;
107 | return name;
108 | }
109 |
110 | @Override
111 | public int describeContents() {
112 | return 0;
113 | }
114 |
115 | @Override
116 | public void writeToParcel(Parcel parcel, int i) {
117 | parcel.writeString(name);
118 | parcel.writeString(mime);
119 | if (uri != null) {
120 | parcel.writeInt(1);
121 | parcel.writeParcelable(uri, i);
122 | } else
123 | parcel.writeInt(0);
124 | }
125 | }
126 |
--------------------------------------------------------------------------------