├── .eslintrc.js
├── .gitattributes
├── .gitignore
├── .prettierrc.js
├── App.js
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── READMEv1.2.md
├── android
├── app
│ ├── _BUCK
│ ├── build.gradle
│ ├── build_defs.bzl
│ ├── debug.keystore
│ ├── proguard-rules.pro
│ └── src
│ │ ├── debug
│ │ └── AndroidManifest.xml
│ │ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── assets
│ │ └── index.android.bundle
│ │ ├── java
│ │ └── com
│ │ │ └── improvement_roll
│ │ │ ├── AppFeaturesModule.java
│ │ │ ├── AppFeaturesPackage.java
│ │ │ ├── MainActivity.java
│ │ │ ├── MainApplication.java
│ │ │ └── widget
│ │ │ ├── ImprovementRollWidget.java
│ │ │ ├── ImprovementRollWidgetConfigureActivity.java
│ │ │ └── RandomRollWorker.java
│ │ └── res
│ │ ├── drawable-hdpi
│ │ └── node_modules_reactnavigation_stack_src_views_assets_backicon.png
│ │ ├── drawable-mdpi
│ │ ├── node_modules_reactnavigation_stack_src_views_assets_backicon.png
│ │ └── node_modules_reactnavigation_stack_src_views_assets_backiconmask.png
│ │ ├── drawable-xhdpi
│ │ └── node_modules_reactnavigation_stack_src_views_assets_backicon.png
│ │ ├── drawable-xxhdpi
│ │ └── node_modules_reactnavigation_stack_src_views_assets_backicon.png
│ │ ├── drawable-xxxhdpi
│ │ └── node_modules_reactnavigation_stack_src_views_assets_backicon.png
│ │ ├── drawable
│ │ ├── example_appwidget_preview.xml
│ │ ├── notification_icon.xml
│ │ └── rn_edit_text_material.xml
│ │ ├── layout
│ │ ├── improvement_roll_widget.xml
│ │ └── improvement_roll_widget_configure.xml
│ │ ├── mipmap-hdpi
│ │ ├── ic_launcher.png
│ │ └── ic_launcher_round.png
│ │ ├── mipmap-mdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxhdpi
│ │ └── ic_launcher.png
│ │ ├── mipmap-xxxhdpi
│ │ └── ic_launcher.png
│ │ ├── values
│ │ ├── strings.xml
│ │ └── styles.xml
│ │ └── xml
│ │ └── improvement_roll_widget_info.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ ├── gradle-wrapper.jar
│ │ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle
├── app.json
├── babel.config.js
├── build-signed-apk.sh
├── categories
├── DefaultCategories.js
└── examples
│ ├── test.json
│ ├── test.toml
│ ├── test.yaml
│ ├── test.yml
│ └── test_multi.yaml
├── fastlane
└── metadata
│ └── android
│ └── en-US
│ ├── changelogs
│ ├── 1.txt
│ ├── 10.txt
│ ├── 11.txt
│ ├── 12.txt
│ ├── 2.txt
│ ├── 3.txt
│ ├── 4.txt
│ ├── 5..txt
│ ├── 6.txt
│ ├── 7.txt
│ ├── 8.txt
│ └── 9.txt
│ ├── full_description.txt
│ ├── images
│ ├── featureGraphic.png
│ ├── icon.png
│ └── phoneScreenshots
│ │ ├── 1.png
│ │ ├── 2.png
│ │ └── 3.png
│ ├── short_description.txt
│ └── title.txt
├── fdroid
└── com.improvement_roll.yml
├── flake.lock
├── flake.nix
├── index.js
├── metro.config.js
├── package-lock.json
├── package.json
├── pictures
├── bitcoin-btc-logo.svg
├── category.png
├── ethereum-eth-logo.svg
├── featureGraphic.png
├── home.png
├── homev1.3.png
├── icon.svg
├── import-comm-cat.png
├── monero-xmr-logo.svg
└── roll.png
├── screens
├── AddCategory.js
├── AdvSettings.js
├── Categories.js
├── CommunityCategories.js
├── ImportExport.js
├── Main.js
├── Options.js
└── Roll.js
└── utility_components
├── background-task-manager.js
├── icon.component.js
├── logging.component.js
├── mapping.json
├── navigation.component.js
├── roll-helper.js
├── styles.js
├── theme-context.js
├── ui-kitten.component.js
└── widget-manager.js
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | extends: '@react-native-community',
4 | };
5 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.pbxproj -text
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # OSX
2 | #
3 | .DS_Store
4 |
5 | # Xcode
6 | #
7 | build/
8 | *.pbxuser
9 | !default.pbxuser
10 | *.mode1v3
11 | !default.mode1v3
12 | *.mode2v3
13 | !default.mode2v3
14 | *.perspectivev3
15 | !default.perspectivev3
16 | xcuserdata
17 | *.xccheckout
18 | *.moved-aside
19 | DerivedData
20 | *.hmap
21 | *.ipa
22 | *.xcuserstate
23 |
24 | # Android/IntelliJ
25 | #
26 | build/
27 | .idea
28 | .gradle
29 | local.properties
30 | *.iml
31 |
32 | # node.js
33 | #
34 | node_modules/
35 | npm-debug.log
36 | yarn-error.log
37 |
38 | # BUCK
39 | buck-out/
40 | \.buckd/
41 | *.keystore
42 | !debug.keystore
43 |
44 | # fastlane
45 | #
46 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the
47 | # screenshots whenever they are needed.
48 | # For more information about the recommended setup visit:
49 | # https://docs.fastlane.tools/best-practices/source-control/
50 |
51 | */fastlane/report.xml
52 | */fastlane/Preview.html
53 | */fastlane/screenshots
54 |
55 | # Bundle artifact
56 | *.jsbundle
57 | android/app/src/main/assets/index.android.bundle
58 | android/app/src/main/assets/
59 |
60 | # CocoaPods
61 | /ios/Pods/
62 |
63 | ios
64 |
65 | *.hprof
66 | build-release.gradle
67 |
68 | android/app/src/main/assets.android-sdk/
69 | .android-sdk/
70 | android/app/keystore.properties
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | bracketSpacing: false,
3 | jsxBracketSameLine: true,
4 | singleQuote: true,
5 | trailingComma: 'all',
6 | };
7 |
--------------------------------------------------------------------------------
/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Alert, BackHandler } from 'react-native';
3 | import * as eva from '@eva-design/eva';
4 | import { ApplicationProvider, IconRegistry } from '@ui-kitten/components';
5 | import { EvaIconsPack } from '@ui-kitten/eva-icons';
6 | import { ThemeContext } from './utility_components/theme-context';
7 | import { AppNavigator } from './utility_components/navigation.component';
8 | import { default as mapping } from './utility_components/mapping.json'
9 | import {
10 | setJSExceptionHandler,
11 | setNativeExceptionHandler,
12 | } from 'react-native-exception-handler';
13 | import { checkMultiple, PERMISSIONS } from 'react-native-permissions';
14 | import * as logger from './utility_components/logging.component.js';
15 |
16 |
17 |
18 |
19 | const handleError = (e, isFatal) => {
20 | if (global.settings != undefined && global.settings.debugMode) {
21 |
22 | checkMultiple([PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE, PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE]).then((status) => {
23 | if (status[PERMISSIONS.ANDROID.READ_EXTERNAL_STORAGE] == 'granted' && status[PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE] == 'granted') {
24 | logger.logFatal(e.message);
25 | }
26 | })
27 | }
28 |
29 | if (isFatal) {
30 | Alert.alert(
31 | 'Unexpected error occurred',
32 | `
33 | Error: ${(isFatal) ? 'Fatal:' : ''} ${e.name} ${e.message}
34 | \nLogs have been saved to Downloads if FILE Permissions have been granted.
35 | \nPlease report this on the github!`,
36 | [{
37 | text: 'Close app and start again',
38 | onPress: () => {
39 | BackHandler.exitApp();
40 | }
41 | }]
42 | );
43 | } else {
44 | //console.log(e);
45 | logger.logDebug(e);
46 |
47 | }
48 | };
49 |
50 | setJSExceptionHandler((error, isFatal) => {
51 | handleError(error, isFatal);
52 | }, true);
53 |
54 | setNativeExceptionHandler((errorString) => {
55 |
56 | });
57 |
58 |
59 |
60 | export default () => {
61 |
62 | const [theme, setTheme] = React.useState('light');
63 | const [backgroundColor, setBackgroundColor] = React.useState('#ffffee');
64 |
65 | const toggleTheme = () => {
66 | const nextTheme = theme === 'light' ? 'dark' : 'light';
67 | const color = nextTheme === 'dark' ? '#222B45' : '#ffffee'
68 |
69 | setTheme(nextTheme);
70 | setBackgroundColor(color);
71 |
72 | };
73 |
74 | return (
75 | <>
76 |
77 |
78 |
79 |
80 |
81 |
82 | >
83 | );
84 | };
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing
2 |
3 | When contributing to this repository, if it is a major code change, please first discuss the change you wish to make via issue,
4 | email, or any other method, with the owners of this repository before making a change.
5 |
6 |
7 | ## Pull Request Process
8 |
9 | 1. Fork this repo
10 | 2. Make changes
11 | 3. Ensure any install or build dependencies are removed before the end of the layer when doing a build.
12 | 4. Update the README.md with details of changes to the interface, this includes new global variables or useful file locations.
13 | 5. Submit the pull request and await approval!
14 |
15 |
16 | ## Setting up for Development
17 |
18 | ### Linux
19 |
20 | - Follow react-native instructions for setting up the environment
21 | - Essentially you need the following and any relevant tools added to your path:
22 | - Java version 8 (I use 8 from openjdk)
23 | - Node/npm (I use nvm to install a specific LTS version, which is 14.17.4)
24 | - Android SDK Manager (You can install via android-studio or command-line tool)
25 | - Android SDK Platform 29
26 | - Intel x86 Atom_64 System Image
27 | - Android SDK Tools 29.0.2
28 | - An android VM or device connected to pc
29 |
30 | - `npm i` to install packages
31 | - `npm start` to start react native server
32 | - open another terminal
33 | - `npm run android` start the app
34 |
35 |
36 | ### Using Nix Flake
37 |
38 | A Nix flake is available for reproducible development environments:
39 |
40 | - Install Nix with flakes enabled: https://nixos.org/
41 | - Run `nix develop` in the project root to enter the dev environment
42 | - Run `npm i` to install packages
43 | - Run `npm start` to start the React Native server
44 | - Open another terminal and run `npm run android` to launch the app
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | ---
3 | A randomly selected to-do list
4 |
5 | [ ](https://f-droid.org/packages/com.improvement_roll/)
8 |
9 |
10 |
11 | ## What??
12 | Inspired from "rolling" threads on 4chan. You can create a category of tasks that you want to do (in no particular order).
13 | Then press the button, and it will randomly give you a task to do from said list.
14 |
15 | You can also sort tasks in a category by how long it would take to complete them and randomly select based on time.
16 |
17 | There is already a pre-generated category available called **General**
18 |
19 | ## Updating from version 1.2.x
20 |
21 | **First off, export all categories to ensure you wont lose anything!**
22 |
23 | With version 1.4 there is a change from `time` to `minutes`. Rolling is governed by that and the new `timeRange` variable set in advanced settings (which is default to 2).
24 | This makes category creation more flexible as you no longer need to adhear to the strict time ranges that were there before.
25 |
26 | However, if you have categories with tasks that are still using `time` there may be some unforseen effects from updating to v1.4+.
27 | In general this is what you can do to roll those older categories over:
28 |
29 |
30 | - In the app edit the category's tasks. Each task will have a blank input for minutes. Set the appropriate minutes for each task and save the category
31 | - You can also export your categories, edit the `time` attribute to `minutes`, set the appropriate minutes, Reset the data on the app (found in adv settings), and import them back into the app.
32 |
33 | Go [HERE](https://github.com/vukani-dev/improvement-roll/tree/main/READMEv1.2.md) for docs on the older version.
34 |
35 | ### Simple Example:
36 | Say you want to get in shape but are too lazy to commit to a program.
37 | You can create a Category called **Fitness**.
38 | And in the **Fitness** category you can add tasks like:
39 | - Do 50 push-ups
40 | - Squat for 5 minutes
41 | - Do 40 Jumping Jacks
42 | - *etc*
43 |
44 | Then whenever you have free time you can open the app and select the **Fitness** category and a random one of these tasks will be given to you.
45 | Now it's up to you to do them, but at least you didn't have to think about what to do :wink:
46 |
47 | ### Timed Example:
48 | I bet you're thinking:
49 | *What if the random task I've been given takes too much time to complete!* :worried:
50 |
51 | Whether you are or aren't I built this feature anyway!
52 |
53 | Let's say you are in the same situation as the simple example.
54 | You can create the same Category called **Fitness**
55 | During the category creation, toggle the "This category is split by time" box.
56 | Now you can enter in tasks and enter how many minutes it takes for you to complete them.
57 |
58 | #### New in v1.4+
59 | Once you select the **Fitness** category to roll this time, you will be asked how much time you have.
60 | You can enter exactly how many minutes you have, and the app will give you a task that's less than that amount up to a range of **2** minutes (this can be changed in advance settings).
61 | **For example**: If you enter **15** minutes and the time range is set to **2** you will be given a task that is between 13 and 15 minutes.
62 |
63 | Also included are a selection of quick time ranges you can select. These are generated based on the category you selected.
64 |
65 | This feature allows you to be more creative with how you design your categories and flexible when it comes to rolling.
66 |
67 | ## Community Categories
68 |
69 | ### Importing
70 | You can import Categories made by others via the `Community Categories` page.
71 | Its as simple as selecting the Category and pressing the Import Button in the top right hand corner:
72 |
73 |
74 |
75 | ### Search/Filter
76 | There is also a search field at the top of the page that can be revealed by pressing the magnifying glass.
77 | With this, you can filter the categories by:
78 |
79 | - **Name** ⇾ The name of the category
80 | - **Author** ⇾ The author of the category
81 | - **Tags** ⇾ Any included tags of the category
82 |
83 | ### Adding
84 | The categories come from an API that's generated by the following [Repo](https://github.com/vukani-dev/improvement-roll-service)
85 | More info can be found there on how to add a category of your own.
86 |
87 | ## Import/Export
88 |
89 | ### Importing
90 |
91 | If creating Categories on the app is too cumbersome, you can create Categories via text editor and import them into the app.
92 | This feature is located in the options page.
93 |
94 | Available formats:
95 |
96 | - **JSON**
97 | - **TOML**
98 | - **YAML**
99 |
100 | However, only with **JSON** and **YAML** can you import multiple Categories from one file.
101 | Examples can be found under [categories/examples](https://github.com/vukani-dev/improvement-roll/tree/main/categories/examples)
102 | The `minutes` variable under tasks is associated with the time it takes to usually complete it. This is only necessary for Categories that are timeSensitive (denoted by `timeSensitive` bool under the Category).
103 |
104 | ### Exporting
105 |
106 | Categories in the app can also be exported into any of the supported formats. They will automatically be exported to your Downloads folder. Once again, the *Export All* feature is only available if you are exporting to **JSON**.
107 |
108 | Both of these features require the app to need permission to read/save files on your phone. If you are not using this feature, the permissions are not needed.
109 |
110 | ## Advanced Settings
111 | On the Advanced Settings page (which can be found from the Options page) you will find a couple of tweaks:
112 |
113 | - **Time range settings** ⇾ allows you to set the time range used for exact rolls. This number is how many minutes below your entered minutes the rolling algorithm will pull from.
114 | - For example, with a time range of 0 when you roll and enter 15 as the amount of minutes you have, you will only be given tasks that take exactly 15 minutes. With a time range of 7 you will be given tasks that take from 8 to 15 minutes.
115 | - **Debug mode toggle** ⇾ This enables debug mode. You will see logs being written to a file called `imp-roll-logs.txt`. Not all events and functions are being logged at the moment, but this is a work in process :wink:. This is really helpful for debugging strange bugs users have.
116 | - **Reset option** ⇾ This will wipe the memory of the app back to its initial state.
117 |
118 | ## Notifications
119 |
120 | Never forget to work on your improvement tasks with random notifications:
121 |
122 | - **Random Task Reminders** ⇾ Enable notifications to receive random tasks from your selected category throughout the day.
123 | - **Customizable Frequency** ⇾ Set how often the app checks if it should send a notification (in hours).
124 | - **Probability Control** ⇾ Configure the chance (percentage) that a notification will be shown when the app checks.
125 | - **Active Hours** ⇾ Specify when notifications are allowed (for example, only between 9 AM and 10 PM).
126 | - **Direct Access** ⇾ Notifications include a "Roll Again" action to quickly get a new task.
127 |
128 | This feature helps you stay on track with your self-improvement goals by providing gentle reminders throughout your day.
129 |
130 | > **Note:** Enabling notifications requires the app to have notification permission. When you toggle on the notifications feature, the app will automatically request this permission if it hasn't been granted already.
131 |
132 | ## Widgets
133 |
134 | Add Improvement Roll to your home screen for quick access to tasks:
135 |
136 | - **Home Screen Integration** ⇾ Add widgets to your home screen for immediate access to random tasks.
137 | - **Category Selection** ⇾ Configure each widget to show tasks from a specific category.
138 | - **One-Tap Rolling** ⇾ Get a new random task with just a single tap on the widget.
139 | - **Quick App Access** ⇾ Open the main app directly from the widget when needed.
140 |
141 | Widgets make it even easier to incorporate improvement tasks into your daily routine by reducing friction - no need to open the app first!
142 |
143 | ## Development
144 |
145 | ### Linux
146 |
147 | - Follow react-native instructions for setting up the environment
148 | - Essentially you need the following and any relevant tools added to your path:
149 | - Java version 8 (I use 8 from openjdk)
150 | - Node/npm (I use nvm to install a specific LTS version, which is 14.17.4)
151 | - Android SDK Manager (You can install via android-studio or command-line tool)
152 | - Android SDK Platform 29
153 | - Intel x86 Atom_64 System Image
154 | - Android SDK Tools 29.0.2
155 | - An android VM or device connected to pc
156 |
157 | - `npm i` to install packages
158 | - `npm start` to start react native server
159 | - open another terminal
160 | - `npm run android` start the app
161 |
162 |
163 | ### Using Nix Flake
164 |
165 | A Nix flake is available for reproducible development environments:
166 |
167 | - Install Nix with flakes enabled: https://nixos.org/
168 | - Run `nix develop` in the project root to enter the dev environment
169 | - Run `npm i` to install packages
170 | - Run `npm start` to start the React Native server
171 | - Open another terminal and run `npm run android` to launch the app
172 |
173 | ## Screenshots
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 | ## What are these "rolling" threads
183 | On 4chan every post give you a corresponding ID. The ID generated from a post is what people would refer to as "rolling".
184 |
185 | *For example:*
186 | You could create a thread that says the last Number of your ID will determine how many push-ups you do in that instance. People would then post as a reply to that thread to find out how many push-ups they would do.
187 |
--------------------------------------------------------------------------------
/READMEv1.2.md:
--------------------------------------------------------------------------------
1 |
2 | ---
3 | A randomly selected todo list
4 |
5 | [ ](https://f-droid.org/packages/com.improvement_roll/)
8 |
9 |
10 |
11 | ## What??
12 | Inspired from "rolling" threads on 4chan. You can create category of tasks that you want to do (in no particular order).
13 | Then press the button and it will randomly give you a task to do from said list.
14 |
15 | You can also sort tasks in a category by how long it would take to complete them and randomly select based on time.
16 |
17 | There is already a pre-generated category available called **General**
18 |
19 | ### Simple Example:
20 | Say you want to get in shape but are too lazy to commit to a program.
21 | You can create a Category called **Fitness**.
22 | And in the **Fitness** category you can add tasks like:
23 | - Do 50 pushups
24 | - Squat for 5 minutes
25 | - Do 40 Jumping Jacks
26 | - *etc*
27 |
28 | Then whenever you have free time you can open the app and select the **Fitness** category and a random one of these tasks will be given to you.
29 | Now its up to you to do them but at least you didnt have to think about what to do :wink:
30 |
31 | ### Timed Example:
32 | I bet you're thinking:
33 | *What if the random task I've been given takes too much time to complete!* :worried:
34 |
35 | Whether you are or aren't I built this feature anyways!
36 |
37 | Lets say you are in the same situation as the simple example.
38 | You can create the same Category called **Fitness**
39 | During the category creation toggle the "This category is split by time" box.
40 | Now you can enter in tasks and select how much time it takes for you to complete them.
41 |
42 | Once you select the **Fitness** category to roll this time you will be asked how much time you have. And you will only be given tasks that are within that time range (If I only have 5 minutes id rather do pushups, But if I have an hour I can go for a run)
43 |
44 | ## Import/Export
45 |
46 | ### Importing
47 |
48 | If creating Categories on the app is too cumbersome, you can create Categories via text editor and import them into the app.
49 | This feature is located in the options page.
50 |
51 | Available formats:
52 |
53 | - **JSON**
54 | - **TOML**
55 | - **YAML**
56 |
57 | However only with **JSON** and **YAML** can you import multiple Categories from one file.
58 | Examples can be found under [categories/examples](https://github.com/vukani-dev/improvement-roll/tree/main/categories/examples)
59 | The `time` variable under tasks is associated with the time it takes to usually complete it. This is only necessary for Categories that are timeSensitive (denoted by `timeSensitive` bool under the Category).
60 |
61 | - 1 = 0 - 10 minutes
62 | - 2 = 10 - 20 minutes
63 | - 3 = 30 minutes - 1 hour
64 | - 4 = 1+ hours
65 |
66 | ### Exporting
67 |
68 | Categories in the app can also be exported into any of the supported formats. They will automatically be exported to your Downloads folder. Once again, the *Export All* feature is only available if you are exporting to **JSON**.
69 |
70 | Both of these features require the app to need permission to read/save files on your phone. If you are not using this feature the permissions are not needed.
71 |
72 | ## Development
73 |
74 | ### Linux
75 |
76 | - Follow react-native instructions for setting up the environment
77 | - Essentially you need the following and any relevant tools added to your path:
78 | - Java version 8 (I use 8 from openjdk)
79 | - Node/npm (I use nvm to install lts version which is 14.17.4)
80 | - yarn (Im using version 1.22.11)
81 | - Android SDK Manager (You can install via android-studio or command-line tool)
82 | - Android SDK Platform 29
83 | - Intel x86 Atom_64 System Image
84 | - Android SDK Tools 29.0.2
85 | - An android VM or device connected to pc
86 |
87 | - `yarn` to install packages
88 | - `yarn start` to start react native server
89 | - `yarn run android` start the app
90 |
91 | ## Screenshots
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 | ## What are these "rolling" threads
101 | On 4chan every post give you a corresponding ID. The ID generated from a post is what people would refer to as "rolling".
102 |
103 | *For example:*
104 | You could create a thread that says the last Number of your ID will determine how many pushups you do in that instance. People would then post as a reply to that thread to find out how many pushups they would do.
105 |
--------------------------------------------------------------------------------
/android/app/_BUCK:
--------------------------------------------------------------------------------
1 | # To learn about Buck see [Docs](https://buckbuild.com/).
2 | # To run your application with Buck:
3 | # - install Buck
4 | # - `npm start` - to start the packager
5 | # - `cd android`
6 | # - `keytool -genkey -v -keystore keystores/debug.keystore -storepass android -alias androiddebugkey -keypass android -dname "CN=Android Debug,O=Android,C=US"`
7 | # - `./gradlew :app:copyDownloadableDepsToLibs` - make all Gradle compile dependencies available to Buck
8 | # - `buck install -r android/app` - compile, install and run application
9 | #
10 |
11 | load(":build_defs.bzl", "create_aar_targets", "create_jar_targets")
12 |
13 | lib_deps = []
14 |
15 | create_aar_targets(glob(["libs/*.aar"]))
16 |
17 | create_jar_targets(glob(["libs/*.jar"]))
18 |
19 | android_library(
20 | name = "all-libs",
21 | exported_deps = lib_deps,
22 | )
23 |
24 | android_library(
25 | name = "app-code",
26 | srcs = glob([
27 | "src/main/java/**/*.java",
28 | ]),
29 | deps = [
30 | ":all-libs",
31 | ":build_config",
32 | ":res",
33 | ],
34 | )
35 |
36 | android_build_config(
37 | name = "build_config",
38 | package = "com.improvement_roll",
39 | )
40 |
41 | android_resource(
42 | name = "res",
43 | package = "com.improvement_roll",
44 | res = "src/main/res",
45 | )
46 |
47 | android_binary(
48 | name = "app",
49 | keystore = "//android/keystores:debug",
50 | manifest = "src/main/AndroidManifest.xml",
51 | package_type = "debug",
52 | deps = [
53 | ":app-code",
54 | ],
55 | )
56 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "com.android.application"
2 | apply plugin: "kotlin-android"
3 | apply plugin: "com.facebook.react"
4 |
5 | import com.android.build.OutputFile
6 |
7 | // Load keystore properties from keystore.properties file
8 | def keystorePropertiesFile = file("keystore.properties")
9 | def keystoreProperties = new Properties()
10 | if (keystorePropertiesFile.exists()) {
11 | keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
12 | }
13 |
14 | // Get properties from command line arguments if provided
15 | def getPropertyOrDefault(propertyName, defaultValue) {
16 | return project.hasProperty(propertyName) ? project.property(propertyName) : defaultValue
17 | }
18 |
19 | def outputFileName = getPropertyOrDefault('outputFile', "improvement-roll-1.4.0.apk")
20 |
21 | /**
22 | * This is the React Native configuration block
23 | */
24 | project.ext.react = [
25 | enableHermes: false, // clean and rebuild if changing
26 | nodeExecutableAndArgs: [System.getenv("NODE_BINARY") ?: "node"],
27 | cliPath: "../../node_modules/react-native/cli.js",
28 | bundleInRelease: true,
29 | bundleInDebug: false
30 | ]
31 |
32 | /**
33 | * Set this to true to create four separate APKs instead of one:
34 | * - An APK that only works on ARM devices
35 | * - An APK that only works on ARM64 devices
36 | * - An APK that only works on x86 devices
37 | * - An APK that only works on x86_64 devices
38 | * The advantage is the size of the APK is reduced by about 4MB.
39 | * Upload all the APKs to the Play Store and people will download
40 | * the correct one based on the CPU architecture of their device.
41 | */
42 | def enableSeparateBuildPerCPUArchitecture = false
43 |
44 | /**
45 | * Run Proguard to shrink the Java bytecode in release builds.
46 | */
47 | def enableProguardInReleaseBuilds = false
48 |
49 | /**
50 | * The preferred build flavor of JavaScriptCore.
51 | *
52 | * For example, to use the international variant, you can use:
53 | * `def jscFlavor = 'org.webkit:android-jsc-intl:+'`
54 | *
55 | * The international variant includes ICU i18n library and necessary data
56 | * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that
57 | * give correct results when using with locales other than en-US. Note that
58 | * this variant is about 6MiB larger per architecture than default.
59 | */
60 | def jscFlavor = 'org.webkit:android-jsc:+'
61 |
62 | /**
63 | * Whether to enable the Hermes VM.
64 | *
65 | * This should be set on project.ext.react and mirrored here. If it is not set
66 | * on project.ext.react, JavaScript will not be compiled to Hermes Bytecode
67 | * and the benefits of using Hermes will therefore be sharply reduced.
68 | */
69 | def enableHermes = project.ext.react.get("enableHermes", false);
70 |
71 | android {
72 | ndkVersion rootProject.ext.ndkVersion
73 |
74 | compileSdkVersion rootProject.ext.compileSdkVersion
75 |
76 | compileOptions {
77 | sourceCompatibility JavaVersion.VERSION_1_8
78 | targetCompatibility JavaVersion.VERSION_1_8
79 | }
80 |
81 | // Add resolution strategy to handle duplicate classes
82 | configurations.all {
83 | resolutionStrategy {
84 | force 'com.facebook.fbjni:fbjni:0.3.0'
85 | exclude group: 'com.facebook.fbjni', module: 'fbjni-java-only'
86 | }
87 | }
88 |
89 | // Handle duplicate native libraries
90 | packagingOptions {
91 | pickFirst '**/libc++_shared.so'
92 | pickFirst '**/libfbjni.so'
93 | pickFirst '**/libreactnativejni.so'
94 | }
95 |
96 | defaultConfig {
97 | applicationId "com.improvement_roll"
98 | minSdkVersion rootProject.ext.minSdkVersion
99 | targetSdkVersion rootProject.ext.targetSdkVersion
100 | versionCode 12
101 | versionName "1.4.0"
102 | }
103 | splits {
104 | abi {
105 | reset()
106 | enable enableSeparateBuildPerCPUArchitecture
107 | universalApk false // If true, also generate a universal APK
108 | include "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
109 | }
110 | }
111 | signingConfigs {
112 | debug {
113 | storeFile file('debug.keystore')
114 | storePassword 'android'
115 | keyAlias 'androiddebugkey'
116 | keyPassword 'android'
117 | }
118 | release {
119 | storeFile keystoreProperties.getProperty('storeFile') ? file(keystoreProperties.getProperty('storeFile')) : file('improll.keystore')
120 | storePassword keystoreProperties.getProperty('storePassword', 'android')
121 | keyAlias keystoreProperties.getProperty('keyAlias', 'androiddebugkey')
122 | keyPassword keystoreProperties.getProperty('keyPassword', 'android')
123 | }
124 | }
125 | buildTypes {
126 | debug {
127 | signingConfig signingConfigs.debug
128 | }
129 | release {
130 | // Caution! In production, you need to generate your own keystore file.
131 | // see https://reactnative.dev/docs/signed-apk-android.
132 | signingConfig signingConfigs.release
133 | // Re-enable minification with our robust ProGuard rules
134 | minifyEnabled true
135 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro"
136 |
137 | // Ensure JavaScript bundle works in release builds
138 | matchingFallbacks = ['release', 'debug']
139 | }
140 | }
141 |
142 | // applicationVariants are e.g. debug, release
143 | applicationVariants.all { variant ->
144 | variant.outputs.each { output ->
145 | // For each separate APK per architecture, set a unique version code as described here:
146 | // https://developer.android.com/studio/build/configure-apk-splits.html
147 | def versionCodes = ["armeabi-v7a": 1, "x86": 2, "arm64-v8a": 3, "x86_64": 4]
148 | def abi = output.getFilter(OutputFile.ABI)
149 | if (abi != null) { // null for the universal-debug, universal-release variants
150 | output.versionCodeOverride =
151 | versionCodes.get(abi) * 1048576 + defaultConfig.versionCode
152 | }
153 |
154 | // Set custom output filename if provided
155 | if (outputFileName != null && variant.buildType.name == "release") {
156 | output.outputFileName = outputFileName
157 | }
158 | }
159 | }
160 | }
161 |
162 | dependencies {
163 | implementation fileTree(dir: "libs", include: ["*.jar"])
164 |
165 | //noinspection GradleDynamicVersion
166 | implementation "com.facebook.react:react-native:+" // From node_modules
167 | implementation "com.facebook.fbjni:fbjni:0.3.0" // Add explicit FBJNI dependency
168 | implementation "com.facebook.fbjni:fbjni-java-only:0.2.2" // Add Java-only version as backup
169 |
170 | implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0"
171 |
172 | // Custom dependencies
173 | implementation "androidx.work:work-runtime:2.7.1"
174 | implementation(project(':react-native-device-info')) {
175 | exclude group: 'com.google.firebase'
176 | exclude group: 'com.google.android.gms'
177 | exclude group: 'com.android.installreferrer'
178 | }
179 | implementation project(':react-native-file-picker')
180 | implementation project(':react-native-fs')
181 |
182 | debugImplementation("com.facebook.flipper:flipper:${FLIPPER_VERSION}")
183 | debugImplementation("com.facebook.flipper:flipper-network-plugin:${FLIPPER_VERSION}") {
184 | exclude group:'com.facebook.flipper'
185 | }
186 | debugImplementation("com.facebook.flipper:flipper-fresco-plugin:${FLIPPER_VERSION}") {
187 | exclude group:'com.facebook.flipper'
188 | }
189 |
190 | if (enableHermes) {
191 | def hermesPath = "../../node_modules/hermes-engine/android/";
192 | debugImplementation files(hermesPath + "hermes-debug.aar")
193 | releaseImplementation files(hermesPath + "hermes-release.aar")
194 | } else {
195 | implementation jscFlavor
196 | }
197 | }
198 |
199 | // Run this once to be able to run the application with BUCK
200 | // puts all compile dependencies into folder libs for BUCK to use
201 | task copyDownloadableDepsToLibs(type: Copy) {
202 | from configurations.implementation
203 | into 'libs'
204 | }
205 |
206 | apply from: file("../../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesAppBuildGradle(project)
207 |
208 | // Explicitly ensure JS bundle is created for release builds
209 | project.afterEvaluate {
210 | if (project.hasProperty("bundleReleaseJsAndAssets")) {
211 | android.buildTypes.release.packageApplication.dependsOn(bundleReleaseJsAndAssets)
212 | }
213 | }
214 |
215 | // Force disable Codegen tasks that try to run Yarn commands
216 | tasks.whenTaskAdded { task ->
217 | if (task.name.contains('generateCodegen') || task.name.contains('Codegen')) {
218 | task.enabled = false
219 | task.dependsOn = []
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/android/app/build_defs.bzl:
--------------------------------------------------------------------------------
1 | """Helper definitions to glob .aar and .jar targets"""
2 |
3 | def create_aar_targets(aarfiles):
4 | for aarfile in aarfiles:
5 | name = "aars__" + aarfile[aarfile.rindex("/") + 1:aarfile.rindex(".aar")]
6 | lib_deps.append(":" + name)
7 | android_prebuilt_aar(
8 | name = name,
9 | aar = aarfile,
10 | )
11 |
12 | def create_jar_targets(jarfiles):
13 | for jarfile in jarfiles:
14 | name = "jars__" + jarfile[jarfile.rindex("/") + 1:jarfile.rindex(".jar")]
15 | lib_deps.append(":" + name)
16 | prebuilt_jar(
17 | name = name,
18 | binary_jar = jarfile,
19 | )
20 |
--------------------------------------------------------------------------------
/android/app/debug.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/android/app/debug.keystore
--------------------------------------------------------------------------------
/android/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in /usr/local/Cellar/android-sdk/24.3.3/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # React Native rules
13 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip
14 | -keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters
15 | -keep,allowobfuscation @interface com.facebook.common.internal.DoNotStrip
16 |
17 | # Do not strip any method/class that is annotated with @DoNotStrip
18 | -keep @com.facebook.proguard.annotations.DoNotStrip class *
19 | -keep @com.facebook.common.internal.DoNotStrip class *
20 | -keepclassmembers class * {
21 | @com.facebook.proguard.annotations.DoNotStrip *;
22 | @com.facebook.common.internal.DoNotStrip *;
23 | }
24 |
25 | -keepclassmembers @com.facebook.proguard.annotations.KeepGettersAndSetters class * {
26 | void set*(***);
27 | *** get*();
28 | }
29 |
30 | -keep class * extends com.facebook.react.bridge.JavaScriptModule { *; }
31 | -keep class * extends com.facebook.react.bridge.NativeModule { *; }
32 | -keepclassmembers,includedescriptorclasses class * { native ; }
33 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp ; }
34 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup ; }
35 |
36 | # Keep all the Facebook JNI code in release mode
37 | -keep class com.facebook.jni.** { *; }
38 | -keep class com.facebook.react.bridge.** { *; }
39 | -keep class com.facebook.react.turbomodule.** { *; }
40 | -keep class com.facebook.react.bridge.queue.** { *; }
41 | -keep class com.facebook.react.bridge.ReadableType { *; }
42 | -keep class com.facebook.react.bridge.WritableMap { *; }
43 | -keep class com.facebook.react.bridge.ReadableMap { *; }
44 | -keep class com.facebook.react.bridge.WritableArray { *; }
45 | -keep class com.facebook.react.jscexecutor.** { *; }
46 | -keep class * implements com.facebook.react.bridge.JavaScriptModule { *; }
47 | -keep class * implements com.facebook.react.bridge.NativeModule { *; }
48 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactProp ; }
49 | -keepclassmembers class * { @com.facebook.react.uimanager.annotations.ReactPropGroup ; }
50 | -keep class com.facebook.react.modules.** { *; }
51 |
52 | # Hermes
53 | -keep class com.facebook.hermes.unicode.** { *; }
54 | -keep class com.facebook.jni.** { *; }
55 |
56 | # Reanimated
57 | -keep class com.swmansion.reanimated.** { *; }
58 | -keep class com.facebook.react.turbomodule.** { *; }
59 |
60 | # Your custom native modules
61 | -keep class com.improvement_roll.** { *; }
62 | -keep class com.filepicker.** { *; }
63 | -keep class com.rnfs.** { *; }
64 |
65 | # Keep Android classes that might be accessed via reflection
66 | -keep public class com.android.installreferrer.** { *; }
67 |
68 | # React Native SVG
69 | -keep public class com.horcrux.svg.** { *; }
70 | -keep public interface com.horcrux.svg.** { *; }
71 |
72 | # OkHttp
73 | -keep class okhttp3.** { *; }
74 | -keep interface okhttp3.** { *; }
75 | -dontwarn okhttp3.**
76 | -dontwarn okio.**
77 |
78 | # Keep necessary attributes but allow optimization
79 | -keepattributes Signature
80 | -keepattributes *Annotation*
81 | -keepattributes SourceFile,LineNumberTable
82 | -keepattributes Exceptions
83 | -keepattributes InnerClasses
84 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
20 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
45 |
46 |
47 |
48 |
51 |
52 |
53 |
54 |
58 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/improvement_roll/AppFeaturesModule.java:
--------------------------------------------------------------------------------
1 | package com.improvement_roll;
2 |
3 | // React Native imports
4 | import com.facebook.react.bridge.NativeModule;
5 | import com.facebook.react.bridge.ReactApplicationContext;
6 | import com.facebook.react.bridge.ReactContextBaseJavaModule;
7 | import com.facebook.react.bridge.ReactMethod;
8 | import com.facebook.react.bridge.Promise;
9 | import com.facebook.react.bridge.Arguments;
10 | import com.facebook.react.bridge.WritableArray;
11 | import com.facebook.react.bridge.WritableMap;
12 | import com.facebook.react.modules.storage.AsyncLocalStorageUtil;
13 | import com.facebook.react.modules.storage.ReactDatabaseSupplier;
14 |
15 | // Android imports
16 | import android.content.Context;
17 | import android.content.Intent;
18 | import android.appwidget.AppWidgetManager;
19 | import android.content.ComponentName;
20 | import android.util.Log; // Import Log
21 | import androidx.work.Constraints;
22 | import androidx.work.NetworkType;
23 | import androidx.work.PeriodicWorkRequest;
24 | import androidx.work.WorkManager;
25 | import android.content.SharedPreferences;
26 |
27 | // Project specific imports
28 | import com.improvement_roll.widget.ImprovementRollWidget;
29 | import com.improvement_roll.widget.RandomRollWorker;
30 |
31 | // JSON and other imports
32 | import org.json.JSONArray;
33 | import org.json.JSONObject;
34 | import java.util.Random;
35 | import java.util.concurrent.TimeUnit;
36 |
37 | // Renamed from ImprovementRollWidgetModule
38 | public class AppFeaturesModule extends ReactContextBaseJavaModule {
39 | private static final String TAG = "AppFeaturesModule";
40 | private static final String RANDOM_ROLL_WORK_TAG = "randomRollWork";
41 | private final ReactApplicationContext reactContext;
42 |
43 | public AppFeaturesModule(ReactApplicationContext reactContext) {
44 | super(reactContext);
45 | this.reactContext = reactContext;
46 | }
47 |
48 | @Override
49 | public String getName() {
50 | // Changed module name
51 | return "AppFeatures";
52 | }
53 |
54 | // --- Widget Methods (Restored and Verified) ---
55 | @ReactMethod
56 | public void updateWidgets() {
57 | Context context = getReactApplicationContext();
58 | AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
59 | ComponentName thisWidget = new ComponentName(context, ImprovementRollWidget.class);
60 | int[] appWidgetIds = appWidgetManager.getAppWidgetIds(thisWidget);
61 |
62 | Intent intent = new Intent(context, ImprovementRollWidget.class);
63 | intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
64 | intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
65 | context.sendBroadcast(intent);
66 |
67 | Log.d(TAG, "Widget update broadcast sent");
68 | }
69 |
70 | @ReactMethod
71 | public void getCategories(Promise promise) {
72 | try {
73 | String categoriesJSON = AsyncLocalStorageUtil.getItemImpl(
74 | ReactDatabaseSupplier.getInstance(getReactApplicationContext()).get(),
75 | "categories"
76 | );
77 |
78 | Log.d(TAG, "Raw categories JSON from AsyncStorage: " + categoriesJSON);
79 |
80 | if (categoriesJSON != null && !categoriesJSON.isEmpty()) {
81 | JSONArray categories = new JSONArray(categoriesJSON);
82 | WritableArray resultArray = Arguments.createArray();
83 |
84 | for (int i = 0; i < categories.length(); i++) {
85 | JSONObject category = categories.getJSONObject(i);
86 | WritableMap resultMap = Arguments.createMap();
87 | resultMap.putString("name", category.getString("name"));
88 | resultMap.putString("description", category.optString("description", ""));
89 | resultArray.pushMap(resultMap);
90 | }
91 |
92 | promise.resolve(resultArray);
93 | } else {
94 | promise.resolve(Arguments.createArray());
95 | }
96 | } catch (Exception e) {
97 | Log.e(TAG, "Error getting categories", e);
98 | promise.reject("ERR_WIDGET", "Failed to get categories: " + e.getMessage());
99 | }
100 | }
101 |
102 | @ReactMethod
103 | public void rollFromCategory(String categoryName, Promise promise) {
104 | try {
105 | String categoriesJSON = AsyncLocalStorageUtil.getItemImpl(
106 | ReactDatabaseSupplier.getInstance(getReactApplicationContext()).get(),
107 | "categories"
108 | );
109 |
110 | Log.d(TAG, "Raw categories JSON from AsyncStorage: " + categoriesJSON);
111 |
112 | if (categoriesJSON != null && !categoriesJSON.isEmpty()) {
113 | JSONArray categories = new JSONArray(categoriesJSON);
114 |
115 | JSONObject targetCategory = null;
116 | for (int i = 0; i < categories.length(); i++) {
117 | JSONObject category = categories.getJSONObject(i);
118 | if (category.getString("name").equals(categoryName)) {
119 | targetCategory = category;
120 | break;
121 | }
122 | }
123 |
124 | if (targetCategory != null && targetCategory.has("tasks")) {
125 | JSONArray tasks = targetCategory.getJSONArray("tasks");
126 | if (tasks.length() > 0) {
127 | int randomIndex = new Random().nextInt(tasks.length());
128 | JSONObject task = tasks.getJSONObject(randomIndex);
129 |
130 | WritableMap resultMap = Arguments.createMap();
131 | resultMap.putString("name", task.getString("name"));
132 | resultMap.putString("desc", task.optString("desc", ""));
133 | resultMap.putInt("minutes", task.optInt("minutes", 0));
134 | resultMap.putString("categoryName", categoryName);
135 |
136 | promise.resolve(resultMap);
137 | return;
138 | }
139 | }
140 | }
141 |
142 | promise.resolve(null); // Resolve with null if category/task not found or error
143 | } catch (Exception e) {
144 | Log.e(TAG, "Error rolling task", e);
145 | promise.reject("ERR_WIDGET", "Failed to roll task: " + e.getMessage());
146 | }
147 | }
148 |
149 | @ReactMethod
150 | public void rollFromAnyCategory(Promise promise) {
151 | try {
152 | String categoriesJSON = AsyncLocalStorageUtil.getItemImpl(
153 | ReactDatabaseSupplier.getInstance(getReactApplicationContext()).get(),
154 | "categories"
155 | );
156 |
157 | Log.d(TAG, "Raw categories JSON from AsyncStorage: " + categoriesJSON);
158 |
159 | if (categoriesJSON != null && !categoriesJSON.isEmpty()) {
160 | JSONArray categories = new JSONArray(categoriesJSON);
161 |
162 | if (categories.length() > 0) {
163 | JSONObject category = categories.getJSONObject(0);
164 | String categoryName = category.getString("name");
165 |
166 | if (category.has("tasks")) {
167 | JSONArray tasks = category.getJSONArray("tasks");
168 | if (tasks.length() > 0) {
169 | int randomIndex = new Random().nextInt(tasks.length());
170 | JSONObject task = tasks.getJSONObject(randomIndex);
171 |
172 | WritableMap resultMap = Arguments.createMap();
173 | resultMap.putString("name", task.getString("name"));
174 | resultMap.putString("desc", task.optString("desc", ""));
175 | resultMap.putInt("minutes", task.optInt("minutes", 0));
176 | resultMap.putString("categoryName", categoryName);
177 |
178 | promise.resolve(resultMap);
179 | return;
180 | }
181 | }
182 | }
183 | }
184 |
185 | promise.resolve(null); // Resolve with null if no categories/tasks found or error
186 | } catch (Exception e) {
187 | Log.e(TAG, "Error rolling any task", e);
188 | promise.reject("ERR_WIDGET", "Failed to roll any task: " + e.getMessage());
189 | }
190 | }
191 |
192 | // --- New Background Task Methods ---
193 |
194 | @ReactMethod
195 | public void scheduleRandomNotifications(
196 | String categoryName,
197 | double frequencyHours,
198 | double probability,
199 | int activeHoursStart,
200 | int activeHoursEnd,
201 | Promise promise) {
202 | try {
203 | Log.d(TAG, String.format(
204 | "Scheduling random roll notifications for category '%s'. " +
205 | "Frequency: %.2f hours, Probability: %.0f%%, Active hours: %d-%d",
206 | categoryName, frequencyHours, probability * 100, activeHoursStart, activeHoursEnd));
207 |
208 | // Store these settings for the worker to access
209 | Context context = getReactApplicationContext();
210 | SharedPreferences prefs = context.getSharedPreferences("RandomRollSettings", Context.MODE_PRIVATE);
211 | SharedPreferences.Editor editor = prefs.edit();
212 | editor.putString("categoryName", categoryName);
213 | editor.putFloat("probability", (float) probability);
214 | editor.putInt("activeHoursStart", activeHoursStart);
215 | editor.putInt("activeHoursEnd", activeHoursEnd);
216 | editor.apply();
217 |
218 | // Convert hours to minutes for the work request
219 | long frequencyMinutes = (long) (frequencyHours * 60);
220 | // Use minimum frequency of 15 minutes to avoid excessive battery usage
221 | frequencyMinutes = Math.max(frequencyMinutes, 15);
222 |
223 | // Create a work request with the specified frequency
224 | PeriodicWorkRequest periodicWork =
225 | new PeriodicWorkRequest.Builder(RandomRollWorker.class, frequencyMinutes, TimeUnit.MINUTES)
226 | .addTag(RANDOM_ROLL_WORK_TAG)
227 | .build();
228 |
229 | WorkManager.getInstance(getReactApplicationContext())
230 | .enqueueUniquePeriodicWork(RANDOM_ROLL_WORK_TAG,
231 | androidx.work.ExistingPeriodicWorkPolicy.REPLACE,
232 | periodicWork);
233 |
234 | Log.i(TAG, "Random roll worker scheduled.");
235 | promise.resolve(true);
236 | } catch (Exception e) {
237 | Log.e(TAG, "Error scheduling worker", e);
238 | promise.reject("ERR_SCHEDULE", "Failed to schedule random notifications: " + e.getMessage());
239 | }
240 | }
241 |
242 | @ReactMethod
243 | public void cancelRandomNotifications(Promise promise) {
244 | try {
245 | Log.d(TAG, "Cancelling random roll notifications...");
246 | WorkManager.getInstance(getReactApplicationContext()).cancelUniqueWork(RANDOM_ROLL_WORK_TAG);
247 | Log.i(TAG, "Random roll worker cancelled.");
248 | promise.resolve(true);
249 | } catch (Exception e) {
250 | Log.e(TAG, "Error cancelling worker", e);
251 | promise.reject("ERR_CANCEL", "Failed to cancel random notifications: " + e.getMessage());
252 | }
253 | }
254 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/com/improvement_roll/AppFeaturesPackage.java:
--------------------------------------------------------------------------------
1 | package com.improvement_roll;
2 |
3 | import com.facebook.react.ReactPackage;
4 | import com.facebook.react.bridge.NativeModule;
5 | import com.facebook.react.bridge.ReactApplicationContext;
6 | import com.facebook.react.uimanager.ViewManager;
7 |
8 | import java.util.ArrayList;
9 | import java.util.Collections;
10 | import java.util.List;
11 |
12 | // Renamed from ImprovementRollWidgetPackage
13 | public class AppFeaturesPackage implements ReactPackage {
14 | @Override
15 | public List createViewManagers(ReactApplicationContext reactContext) {
16 | return Collections.emptyList();
17 | }
18 |
19 | @Override
20 | public List createNativeModules(ReactApplicationContext reactContext) {
21 | List modules = new ArrayList<>();
22 | // Add the renamed module
23 | modules.add(new AppFeaturesModule(reactContext));
24 | return modules;
25 | }
26 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/com/improvement_roll/MainActivity.java:
--------------------------------------------------------------------------------
1 | package com.improvement_roll;
2 |
3 | import android.content.Intent;
4 | import android.os.Bundle;
5 | import android.util.Log;
6 |
7 | import com.facebook.react.ReactActivity;
8 | import com.facebook.react.ReactActivityDelegate;
9 | import com.facebook.react.ReactRootView;
10 | import com.facebook.react.bridge.Arguments;
11 | import com.facebook.react.bridge.ReactApplicationContext;
12 | import com.facebook.react.bridge.ReactContext;
13 | import com.facebook.react.bridge.WritableMap;
14 | import com.facebook.react.modules.core.DeviceEventManagerModule;
15 | import com.improvement_roll.widget.RandomRollWorker;
16 |
17 | public class MainActivity extends ReactActivity {
18 | private static final String TAG = "MainActivity";
19 | private ReactActivityDelegate mDelegate;
20 |
21 | /**
22 | * Returns the name of the main component registered from JavaScript. This is used to schedule
23 | * rendering of the component.
24 | */
25 | @Override
26 | protected String getMainComponentName() {
27 | return "improvement_roll";
28 | }
29 |
30 | /**
31 | * Creates the React Activity Delegate
32 | */
33 | @Override
34 | protected ReactActivityDelegate createReactActivityDelegate() {
35 | mDelegate = new ReactActivityDelegate(this, getMainComponentName()) {
36 | @Override
37 | protected ReactRootView createRootView() {
38 | ReactRootView reactRootView = new ReactRootView(getContext());
39 | // If you opted-in for the New Architecture, we enable the Fabric Renderer.
40 | reactRootView.setIsFabric(false);
41 | return reactRootView;
42 | }
43 | };
44 | return mDelegate;
45 | }
46 |
47 | @Override
48 | protected void onCreate(Bundle savedInstanceState) {
49 | super.onCreate(savedInstanceState);
50 | handleIntent(getIntent());
51 | }
52 |
53 | @Override
54 | public void onNewIntent(Intent intent) {
55 | super.onNewIntent(intent);
56 | handleIntent(intent);
57 | }
58 |
59 | private void handleIntent(Intent intent) {
60 | if (intent == null || intent.getAction() == null) {
61 | return;
62 | }
63 |
64 | if ("com.improvement_roll.ROLL_AGAIN".equals(intent.getAction())) {
65 | String categoryName = intent.getStringExtra("categoryName");
66 | if (categoryName != null && !categoryName.isEmpty()) {
67 | Log.d(TAG, "Roll Again requested for category: " + categoryName);
68 |
69 | // Send an event to JS to handle this roll request
70 | // We'll need to wait for the JS context to be ready
71 | if (mDelegate != null) {
72 | ReactContext reactContext = mDelegate.getReactInstanceManager().getCurrentReactContext();
73 | if (reactContext != null) {
74 | sendRollAgainEvent(reactContext, categoryName);
75 | } else {
76 | // Context not ready, let's try again when the ReactActivity is resumed
77 | Log.d(TAG, "ReactContext not ready. Will try to send event when resumed.");
78 | }
79 | }
80 | }
81 | }
82 | }
83 |
84 | @Override
85 | protected void onResume() {
86 | super.onResume();
87 |
88 | // Check if we need to send pending events
89 | Intent intent = getIntent();
90 | if (intent != null && "com.improvement_roll.ROLL_AGAIN".equals(intent.getAction())) {
91 | String categoryName = intent.getStringExtra("categoryName");
92 | if (categoryName != null && !categoryName.isEmpty()) {
93 | if (mDelegate != null) {
94 | ReactContext reactContext = mDelegate.getReactInstanceManager().getCurrentReactContext();
95 | if (reactContext != null) {
96 | sendRollAgainEvent(reactContext, categoryName);
97 | setIntent(new Intent()); // Clear the intent to avoid repeating
98 | }
99 | }
100 | }
101 | }
102 | }
103 |
104 | private void sendRollAgainEvent(ReactContext reactContext, String categoryName) {
105 | WritableMap params = Arguments.createMap();
106 | params.putString("categoryName", categoryName);
107 | reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
108 | .emit("onRollAgainRequested", params);
109 | Log.d(TAG, "Sent roll again event to JS for category: " + categoryName);
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/improvement_roll/MainApplication.java:
--------------------------------------------------------------------------------
1 | package com.improvement_roll;
2 |
3 | import android.app.Application;
4 | import android.content.Context;
5 | import com.facebook.react.PackageList;
6 | import com.facebook.react.ReactApplication;
7 | import com.facebook.react.ReactInstanceManager;
8 | import com.facebook.react.ReactNativeHost;
9 | import com.facebook.react.ReactPackage;
10 | import com.facebook.soloader.SoLoader;
11 | import java.lang.reflect.InvocationTargetException;
12 | import java.util.List;
13 | import com.filepicker.FilePickerPackage;
14 | import com.rnfs.RNFSPackage;
15 | import com.improvement_roll.AppFeaturesPackage;
16 |
17 |
18 | public class MainApplication extends Application implements ReactApplication {
19 |
20 | private final ReactNativeHost mReactNativeHost =
21 | new ReactNativeHost(this) {
22 | @Override
23 | public boolean getUseDeveloperSupport() {
24 | return BuildConfig.DEBUG;
25 | }
26 |
27 | @Override
28 | protected List getPackages() {
29 | @SuppressWarnings("UnnecessaryLocalVariable")
30 | List packages = new PackageList(this).getPackages();
31 |
32 | // Packages that cannot be autolinked yet can be added manually here, for example:
33 | // packages.add(new MyReactNativePackage());
34 | // packages.add(new FilePickerPackage());
35 |
36 | // Use the renamed package
37 | packages.add(new AppFeaturesPackage());
38 |
39 | return packages;
40 | }
41 |
42 | @Override
43 | protected String getJSMainModuleName() {
44 | return "index";
45 | }
46 | };
47 |
48 | @Override
49 | public ReactNativeHost getReactNativeHost() {
50 | return mReactNativeHost;
51 | }
52 |
53 | @Override
54 | public void onCreate() {
55 | super.onCreate();
56 | SoLoader.init(this, /* native exopackage */ false);
57 | // initializeFlipper(this, getReactNativeHost().getReactInstanceManager()); // Comment out Flipper initialization
58 | }
59 |
60 | /**
61 | * Loads Flipper in React Native templates. Call this in the onCreate method with something like
62 | * initializeFlipper(this, getReactNativeHost().getReactInstanceManager());
63 | *
64 | * @param context
65 | * @param reactInstanceManager
66 | */
67 | private static void initializeFlipper(
68 | Context context, ReactInstanceManager reactInstanceManager) {
69 | if (BuildConfig.DEBUG) {
70 | try {
71 | /*
72 | We use reflection here to pick up the class that initializes Flipper,
73 | since Flipper library is not available in release mode
74 | */
75 | Class> aClass = Class.forName("com.improvement_roll.ReactNativeFlipper");
76 | aClass
77 | .getMethod("initializeFlipper", Context.class, ReactInstanceManager.class)
78 | .invoke(null, context, reactInstanceManager);
79 | } catch (ClassNotFoundException e) {
80 | e.printStackTrace();
81 | } catch (NoSuchMethodException e) {
82 | e.printStackTrace();
83 | } catch (IllegalAccessException e) {
84 | e.printStackTrace();
85 | } catch (InvocationTargetException e) {
86 | e.printStackTrace();
87 | }
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/android/app/src/main/java/com/improvement_roll/widget/ImprovementRollWidget.java:
--------------------------------------------------------------------------------
1 | package com.improvement_roll.widget;
2 |
3 | import android.app.PendingIntent;
4 | import android.appwidget.AppWidgetManager;
5 | import android.appwidget.AppWidgetProvider;
6 | import android.content.Context;
7 | import android.content.Intent;
8 | import android.content.SharedPreferences;
9 | import android.net.Uri;
10 | import android.util.Log;
11 | import android.widget.RemoteViews;
12 |
13 | import com.facebook.react.modules.storage.AsyncLocalStorageUtil;
14 | import com.facebook.react.modules.storage.ReactDatabaseSupplier;
15 | import com.improvement_roll.MainActivity;
16 | import com.improvement_roll.R;
17 |
18 | import org.json.JSONArray;
19 | import org.json.JSONObject;
20 |
21 | import java.util.Random;
22 |
23 | /**
24 | * Implementation of App Widget functionality.
25 | */
26 | public class ImprovementRollWidget extends AppWidgetProvider {
27 | private static final String TAG = "ImpRollWidget";
28 | private static final String PREFS_NAME = "com.improvement_roll.widget.ImprovementRollWidget";
29 | private static final String PREF_CATEGORY_KEY = "widget_category_";
30 | private static final String ACTION_ROLL_TASK = "com.improvement_roll.widget.ACTION_ROLL_TASK";
31 | private static final String ACTION_OPEN_APP = "com.improvement_roll.widget.ACTION_OPEN_APP";
32 |
33 | @Override
34 | public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
35 | // There may be multiple widgets active, so update all of them
36 | for (int appWidgetId : appWidgetIds) {
37 | updateAppWidget(context, appWidgetManager, appWidgetId);
38 | }
39 | }
40 |
41 | @Override
42 | public void onReceive(Context context, Intent intent) {
43 | super.onReceive(context, intent);
44 |
45 | if (ACTION_ROLL_TASK.equals(intent.getAction())) {
46 | int appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
47 | AppWidgetManager.INVALID_APPWIDGET_ID);
48 |
49 | if (appWidgetId != AppWidgetManager.INVALID_APPWIDGET_ID) {
50 | // Roll a new task and update widget
51 | rollTaskForWidget(context, appWidgetId);
52 | }
53 | } else if (ACTION_OPEN_APP.equals(intent.getAction())) {
54 | // Open the main app
55 | Intent launchIntent = new Intent(context, MainActivity.class);
56 | launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
57 | context.startActivity(launchIntent);
58 | }
59 | }
60 |
61 | @Override
62 | public void onDeleted(Context context, int[] appWidgetIds) {
63 | // When the user deletes the widget, delete the preference associated with it.
64 | SharedPreferences.Editor prefs = context.getSharedPreferences(PREFS_NAME, 0).edit();
65 | for (int appWidgetId : appWidgetIds) {
66 | prefs.remove(PREF_CATEGORY_KEY + appWidgetId);
67 | }
68 | prefs.apply();
69 | }
70 |
71 | static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId) {
72 | try {
73 | // Get the saved widget configuration (category)
74 | SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, 0);
75 | String categoryName = prefs.getString(PREF_CATEGORY_KEY + appWidgetId, "");
76 |
77 | // Load the last rolled task or roll a new one
78 | String taskName = prefs.getString("task_name_" + appWidgetId, "");
79 | String taskDesc = prefs.getString("task_desc_" + appWidgetId, "");
80 |
81 | if (taskName.isEmpty()) {
82 | // First time or no task available, try to roll
83 | if (!categoryName.isEmpty()) {
84 | rollTaskForWidget(context, appWidgetId);
85 | return; // This function will be called again with the new task
86 | } else {
87 | taskName = "Tap to roll";
88 | taskDesc = "Configure widget to select a category";
89 | }
90 | }
91 |
92 | // Create widget layout
93 | RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.improvement_roll_widget);
94 | views.setTextViewText(R.id.widget_task_name, taskName);
95 | views.setTextViewText(R.id.widget_task_desc, taskDesc);
96 |
97 | if (!categoryName.isEmpty()) {
98 | views.setTextViewText(R.id.widget_category_name, "Category: " + categoryName);
99 | } else {
100 | views.setTextViewText(R.id.widget_category_name, "Tap to configure");
101 | }
102 |
103 | // Set up intent for rolling a new task
104 | Intent rollIntent = new Intent(context, ImprovementRollWidget.class);
105 | rollIntent.setAction(ACTION_ROLL_TASK);
106 | rollIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
107 | PendingIntent rollPendingIntent = PendingIntent.getBroadcast(
108 | context, appWidgetId, rollIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
109 | views.setOnClickPendingIntent(R.id.widget_roll_button, rollPendingIntent);
110 |
111 | // Set up intent for opening the main app
112 | Intent openAppIntent = new Intent(context, ImprovementRollWidget.class);
113 | openAppIntent.setAction(ACTION_OPEN_APP);
114 | PendingIntent openAppPendingIntent = PendingIntent.getBroadcast(
115 | context, 0, openAppIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
116 | views.setOnClickPendingIntent(R.id.widget_open_app, openAppPendingIntent);
117 |
118 | // Set up intent for widget configuration if no category is selected
119 | if (categoryName.isEmpty()) {
120 | Intent configIntent = new Intent(context, ImprovementRollWidgetConfigureActivity.class);
121 | configIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
122 | PendingIntent configPendingIntent = PendingIntent.getActivity(
123 | context, appWidgetId, configIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
124 | views.setOnClickPendingIntent(R.id.widget_category_name, configPendingIntent);
125 | }
126 |
127 | // Update the widget
128 | appWidgetManager.updateAppWidget(appWidgetId, views);
129 | } catch (Exception e) {
130 | Log.e(TAG, "Error updating widget", e);
131 | }
132 | }
133 |
134 | private static void rollTaskForWidget(Context context, int appWidgetId) {
135 | try {
136 | SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, 0);
137 | String categoryName = prefs.getString(PREF_CATEGORY_KEY + appWidgetId, "");
138 |
139 | if (categoryName.isEmpty()) {
140 | return; // No category selected
141 | }
142 |
143 | // Get category data from AsyncStorage
144 | String categoriesJSON = AsyncLocalStorageUtil.getItemImpl(
145 | ReactDatabaseSupplier.getInstance(context).get(),
146 | "categories"
147 | );
148 |
149 | // Log the raw string for debugging
150 | Log.d(TAG, "Raw categories JSON from AsyncStorage: " + categoriesJSON);
151 |
152 | if (categoriesJSON != null && !categoriesJSON.isEmpty()) {
153 | JSONArray categories = new JSONArray(categoriesJSON);
154 |
155 | JSONObject targetCategory = null;
156 | // Find the requested category
157 | for (int i = 0; i < categories.length(); i++) {
158 | JSONObject category = categories.getJSONObject(i);
159 | if (category.getString("name").equals(categoryName)) {
160 | targetCategory = category;
161 | break;
162 | }
163 | }
164 |
165 | // If category found, roll a random task
166 | if (targetCategory != null && targetCategory.has("tasks")) {
167 | JSONArray tasks = targetCategory.getJSONArray("tasks");
168 | if (tasks.length() > 0) {
169 | int randomIndex = new Random().nextInt(tasks.length());
170 | JSONObject task = tasks.getJSONObject(randomIndex);
171 |
172 | // Save the task to SharedPreferences
173 | SharedPreferences.Editor editor = prefs.edit();
174 | editor.putString("task_name_" + appWidgetId, task.getString("name"));
175 | editor.putString("task_desc_" + appWidgetId, task.optString("desc", ""));
176 | editor.apply();
177 |
178 | // Update the widget with the new task
179 | AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
180 | updateAppWidget(context, appWidgetManager, appWidgetId);
181 | return; // Task rolled and widget updated
182 | } else {
183 | // Category found, but no tasks - Update widget with message
184 | Log.w(TAG, "Category '" + categoryName + "' has no tasks.");
185 | updateWidgetWithError(context, appWidgetId, categoryName, "No tasks in category");
186 | return;
187 | }
188 | } else if (targetCategory == null) {
189 | // Category not found - Update widget with message
190 | Log.w(TAG, "Configured category '" + categoryName + "' not found.");
191 | updateWidgetWithError(context, appWidgetId, "Category Not Found", "Please reconfigure widget");
192 | return;
193 | }
194 | }
195 | } catch (Exception e) {
196 | Log.e(TAG, "Error rolling task for widget", e);
197 | }
198 | }
199 |
200 | // Helper method to update widget with an error/status message
201 | private static void updateWidgetWithError(Context context, int appWidgetId, String title, String message) {
202 | AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
203 | RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.improvement_roll_widget);
204 |
205 | views.setTextViewText(R.id.widget_task_name, title);
206 | views.setTextViewText(R.id.widget_task_desc, message);
207 | // Keep category name if available, otherwise show title
208 | SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, 0);
209 | String categoryName = prefs.getString(PREF_CATEGORY_KEY + appWidgetId, "");
210 | views.setTextViewText(R.id.widget_category_name,
211 | categoryName.isEmpty() ? title : "Category: " + categoryName);
212 |
213 | // Ensure intents are still set up
214 | // Set up intent for rolling a new task
215 | Intent rollIntent = new Intent(context, ImprovementRollWidget.class);
216 | rollIntent.setAction(ACTION_ROLL_TASK);
217 | rollIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
218 | PendingIntent rollPendingIntent = PendingIntent.getBroadcast(
219 | context, appWidgetId, rollIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
220 | views.setOnClickPendingIntent(R.id.widget_roll_button, rollPendingIntent);
221 |
222 | // Set up intent for opening the main app
223 | Intent openAppIntent = new Intent(context, ImprovementRollWidget.class);
224 | openAppIntent.setAction(ACTION_OPEN_APP);
225 | PendingIntent openAppPendingIntent = PendingIntent.getBroadcast(
226 | context, 0, openAppIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
227 | views.setOnClickPendingIntent(R.id.widget_open_app, openAppPendingIntent);
228 |
229 | // Set up config intent if needed
230 | if (categoryName.isEmpty()) {
231 | Intent configIntent = new Intent(context, ImprovementRollWidgetConfigureActivity.class);
232 | configIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
233 | PendingIntent configPendingIntent = PendingIntent.getActivity(
234 | context, appWidgetId, configIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
235 | views.setOnClickPendingIntent(R.id.widget_category_name, configPendingIntent);
236 | }
237 |
238 | appWidgetManager.updateAppWidget(appWidgetId, views);
239 | }
240 |
241 | // Method to update configuration
242 | public static void saveCategoryPref(Context context, int appWidgetId, String categoryName) {
243 | SharedPreferences.Editor prefs = context.getSharedPreferences(PREFS_NAME, 0).edit();
244 | prefs.putString(PREF_CATEGORY_KEY + appWidgetId, categoryName);
245 | prefs.apply();
246 |
247 | // Clear any existing task
248 | prefs.remove("task_name_" + appWidgetId);
249 | prefs.remove("task_desc_" + appWidgetId);
250 | prefs.apply();
251 |
252 | // Roll a new task with the selected category
253 | rollTaskForWidget(context, appWidgetId);
254 | }
255 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/com/improvement_roll/widget/ImprovementRollWidgetConfigureActivity.java:
--------------------------------------------------------------------------------
1 | package com.improvement_roll.widget;
2 |
3 | import android.app.Activity;
4 | import android.appwidget.AppWidgetManager;
5 | import android.content.Context;
6 | import android.content.Intent;
7 | import android.os.Bundle;
8 | import android.util.Log;
9 | import android.view.View;
10 | import android.widget.AdapterView;
11 | import android.widget.ArrayAdapter;
12 | import android.widget.Button;
13 | import android.widget.ListView;
14 | import android.widget.TextView;
15 | import android.widget.Toast;
16 |
17 | import com.facebook.react.modules.storage.AsyncLocalStorageUtil;
18 | import com.facebook.react.modules.storage.ReactDatabaseSupplier;
19 | import com.improvement_roll.R;
20 |
21 | import org.json.JSONArray;
22 | import org.json.JSONObject;
23 |
24 | import java.util.ArrayList;
25 | import java.util.List;
26 |
27 | /**
28 | * The configuration screen for the ImprovementRollWidget AppWidget.
29 | */
30 | public class ImprovementRollWidgetConfigureActivity extends Activity {
31 | private static final String TAG = "ImpRollWidgetConfig";
32 |
33 | int mAppWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
34 | ListView categoryListView;
35 | Button cancelButton;
36 | TextView titleTextView;
37 |
38 | private final View.OnClickListener mOnCancelClickListener = new View.OnClickListener() {
39 | public void onClick(View v) {
40 | // The cancel button was clicked, close the activity without saving
41 | finish();
42 | }
43 | };
44 |
45 | @Override
46 | public void onCreate(Bundle icicle) {
47 | super.onCreate(icicle);
48 |
49 | // Set the result to CANCELED. This will cause the widget host to cancel
50 | // out of the widget placement if the user presses the back button.
51 | setResult(RESULT_CANCELED);
52 |
53 | setContentView(R.layout.improvement_roll_widget_configure);
54 |
55 | categoryListView = (ListView) findViewById(R.id.category_list);
56 | cancelButton = (Button) findViewById(R.id.cancel_button);
57 | titleTextView = (TextView) findViewById(R.id.title_text);
58 |
59 | // Find the widget id from the intent.
60 | Intent intent = getIntent();
61 | Bundle extras = intent.getExtras();
62 | if (extras != null) {
63 | mAppWidgetId = extras.getInt(
64 | AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID);
65 | }
66 |
67 | // If this activity was started with an intent without an app widget ID, finish with an error.
68 | if (mAppWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
69 | finish();
70 | return;
71 | }
72 |
73 | // Set up the cancel button
74 | cancelButton.setOnClickListener(mOnCancelClickListener);
75 |
76 | // Load available categories
77 | loadCategories();
78 | }
79 |
80 | private void loadCategories() {
81 | try {
82 | final List categoryNames = new ArrayList<>();
83 | final List categoryDescriptions = new ArrayList<>();
84 |
85 | // Get category data from AsyncStorage
86 | String categoriesJSON = AsyncLocalStorageUtil.getItemImpl(
87 | ReactDatabaseSupplier.getInstance(this).get(),
88 | "categories"
89 | );
90 |
91 | // Log the raw string for debugging
92 | Log.d(TAG, "Raw categories JSON from AsyncStorage: " + categoriesJSON);
93 |
94 | // Check if the string is not null or empty
95 | if (categoriesJSON != null && !categoriesJSON.isEmpty()) {
96 | // Removed incorrect substring call that assumed extra quotes
97 | JSONArray categories = new JSONArray(categoriesJSON);
98 |
99 | for (int i = 0; i < categories.length(); i++) {
100 | JSONObject category = categories.getJSONObject(i);
101 | categoryNames.add(category.getString("name"));
102 | categoryDescriptions.add(category.optString("description", ""));
103 | }
104 |
105 | // Create an adapter for the ListView
106 | ArrayAdapter adapter = new ArrayAdapter<>(this,
107 | android.R.layout.simple_list_item_1, categoryNames);
108 | categoryListView.setAdapter(adapter);
109 |
110 | // Set up item click listener
111 | categoryListView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
112 | @Override
113 | public void onItemClick(AdapterView> parent, View view, int position, long id) {
114 | final String categoryName = categoryNames.get(position);
115 |
116 | // Save the selected category for this widget
117 | ImprovementRollWidget.saveCategoryPref(ImprovementRollWidgetConfigureActivity.this,
118 | mAppWidgetId, categoryName);
119 |
120 | // Make sure we pass back the original appWidgetId
121 | Intent resultValue = new Intent();
122 | resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, mAppWidgetId);
123 | setResult(RESULT_OK, resultValue);
124 |
125 | Toast.makeText(ImprovementRollWidgetConfigureActivity.this,
126 | "Widget configured for " + categoryName, Toast.LENGTH_SHORT).show();
127 |
128 | finish();
129 | }
130 | });
131 |
132 | if (categoryNames.isEmpty()) {
133 | titleTextView.setText("No categories available");
134 | categoryListView.setVisibility(View.GONE);
135 | }
136 | } else {
137 | titleTextView.setText("No categories available");
138 | categoryListView.setVisibility(View.GONE);
139 | }
140 | } catch (Exception e) {
141 | Log.e(TAG, "Error loading categories", e);
142 | titleTextView.setText("Error loading categories");
143 | categoryListView.setVisibility(View.GONE);
144 | }
145 | }
146 | }
--------------------------------------------------------------------------------
/android/app/src/main/java/com/improvement_roll/widget/RandomRollWorker.java:
--------------------------------------------------------------------------------
1 | package com.improvement_roll.widget; // Using widget package for now, can be refactored
2 |
3 | import android.app.NotificationChannel;
4 | import android.app.NotificationManager;
5 | import android.app.PendingIntent;
6 | import android.content.Context;
7 | import android.content.Intent;
8 | import android.content.SharedPreferences;
9 | import android.os.Build;
10 | import android.util.Log;
11 |
12 | import androidx.annotation.NonNull;
13 | import androidx.core.app.NotificationCompat;
14 | import androidx.core.app.NotificationManagerCompat;
15 | import androidx.work.Worker;
16 | import androidx.work.WorkerParameters;
17 |
18 | import com.facebook.react.modules.storage.AsyncLocalStorageUtil;
19 | import com.facebook.react.modules.storage.ReactDatabaseSupplier;
20 | import com.improvement_roll.MainActivity;
21 | import com.improvement_roll.R;
22 |
23 | import org.json.JSONArray;
24 | import org.json.JSONObject;
25 |
26 | import java.util.Random;
27 |
28 | public class RandomRollWorker extends Worker {
29 | private static final String TAG = "RandomRollWorker";
30 | private static final String CHANNEL_ID = "ImprovementRollReminders";
31 | private static final int NOTIFICATION_ID = 1001;
32 |
33 | public RandomRollWorker(
34 | @NonNull Context context,
35 | @NonNull WorkerParameters params) {
36 | super(context, params);
37 | createNotificationChannel(context);
38 | }
39 |
40 | @NonNull
41 | @Override
42 | public Result doWork() {
43 | Log.d(TAG, "Worker running...");
44 | Context context = getApplicationContext();
45 |
46 | // Load settings
47 | SharedPreferences prefs = context.getSharedPreferences("RandomRollSettings", Context.MODE_PRIVATE);
48 | String categoryName = prefs.getString("categoryName", "General");
49 | float probability = prefs.getFloat("probability", 0.5f);
50 | int activeHoursStart = prefs.getInt("activeHoursStart", 9);
51 | int activeHoursEnd = prefs.getInt("activeHoursEnd", 22);
52 |
53 | // Check if current time is within active hours
54 | java.util.Calendar cal = java.util.Calendar.getInstance();
55 | int currentHour = cal.get(java.util.Calendar.HOUR_OF_DAY);
56 |
57 | // If current hour is outside active hours, skip
58 | if (currentHour < activeHoursStart || currentHour >= activeHoursEnd) {
59 | Log.d(TAG, String.format(
60 | "Current hour (%d) outside active hours (%d-%d). Skipping.",
61 | currentHour, activeHoursStart, activeHoursEnd));
62 | return Result.success();
63 | }
64 |
65 | // Randomly decide whether to show a notification based on configured probability
66 | if (Math.random() > probability) {
67 | Log.d(TAG, String.format(
68 | "Skipping notification based on probability setting (%.0f%%).",
69 | probability * 100));
70 | return Result.success(); // Still success, just didn't notify
71 | }
72 |
73 | try {
74 | String categoriesJSON = AsyncLocalStorageUtil.getItemImpl(
75 | ReactDatabaseSupplier.getInstance(context).get(),
76 | "categories"
77 | );
78 |
79 | if (categoriesJSON != null && !categoriesJSON.isEmpty()) {
80 | JSONArray categories = new JSONArray(categoriesJSON);
81 |
82 | if (categories.length() > 0) {
83 | // Find the requested category
84 | JSONObject targetCategory = null;
85 | for (int i = 0; i < categories.length(); i++) {
86 | JSONObject category = categories.getJSONObject(i);
87 | if (category.getString("name").equals(categoryName)) {
88 | targetCategory = category;
89 | break;
90 | }
91 | }
92 |
93 | // If category not found, fall back to first category
94 | if (targetCategory == null && categories.length() > 0) {
95 | targetCategory = categories.getJSONObject(0);
96 | categoryName = targetCategory.getString("name");
97 | Log.w(TAG, String.format(
98 | "Category '%s' not found. Falling back to '%s'.",
99 | categoryName, targetCategory.getString("name")));
100 | }
101 |
102 | if (targetCategory != null && targetCategory.has("tasks")) {
103 | JSONArray tasks = targetCategory.getJSONArray("tasks");
104 | if (tasks.length() > 0) {
105 | int randomIndex = new Random().nextInt(tasks.length());
106 | JSONObject task = tasks.getJSONObject(randomIndex);
107 | String taskName = task.getString("name");
108 | String taskDesc = task.optString("desc", "");
109 |
110 | Log.i(TAG, "Rolled task: " + taskName + " from category: " + categoryName);
111 | sendNotification(context, categoryName, taskName, taskDesc);
112 | return Result.success();
113 | } else {
114 | Log.w(TAG, "Category '" + categoryName + "' has no tasks.");
115 | }
116 | }
117 | } else {
118 | Log.w(TAG, "No categories found.");
119 | }
120 | } else {
121 | Log.w(TAG, "Categories JSON is null or empty.");
122 | }
123 |
124 | } catch (Exception e) {
125 | Log.e(TAG, "Error performing background roll", e);
126 | return Result.failure(); // Indicate work failed
127 | }
128 |
129 | // If we reach here, it means no task was rolled for some reason (no categories, empty category, etc.)
130 | return Result.success(); // Return success so worker doesn't get retried immediately for data issues
131 | }
132 |
133 | private void createNotificationChannel(Context context) {
134 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
135 | CharSequence name = "Productivity Reminders";
136 | String description = "Random reminders to perform a task";
137 | int importance = NotificationManager.IMPORTANCE_DEFAULT;
138 | NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
139 | channel.setDescription(description);
140 |
141 | NotificationManager notificationManager = context.getSystemService(NotificationManager.class);
142 | notificationManager.createNotificationChannel(channel);
143 | Log.d(TAG, "Notification channel created.");
144 | }
145 | }
146 |
147 | private void sendNotification(Context context, String categoryName, String taskName, String taskDesc) {
148 | Intent intent = new Intent(context, MainActivity.class);
149 | intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
150 | PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_IMMUTABLE);
151 |
152 | // Add "roll again" action
153 | Intent rollAgainIntent = new Intent(context, MainActivity.class);
154 | rollAgainIntent.setAction("com.improvement_roll.ROLL_AGAIN");
155 | rollAgainIntent.putExtra("categoryName", categoryName);
156 | PendingIntent rollAgainPendingIntent = PendingIntent.getActivity(
157 | context, 1, rollAgainIntent, PendingIntent.FLAG_IMMUTABLE);
158 |
159 | NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
160 | .setSmallIcon(R.drawable.notification_icon)
161 | .setContentTitle(categoryName)
162 | .setContentText(taskName)
163 | .setStyle(new NotificationCompat.BigTextStyle()
164 | .bigText(taskName + "\n" + taskDesc))
165 | .setPriority(NotificationCompat.PRIORITY_DEFAULT)
166 | .setContentIntent(pendingIntent)
167 | .addAction(android.R.drawable.ic_menu_rotate, "Roll Again", rollAgainPendingIntent)
168 | .setAutoCancel(true);
169 |
170 | NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
171 | notificationManager.notify(NOTIFICATION_ID, builder.build());
172 | Log.i(TAG, "Notification sent for task: " + taskName);
173 | }
174 | }
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-hdpi/node_modules_reactnavigation_stack_src_views_assets_backicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/android/app/src/main/res/drawable-hdpi/node_modules_reactnavigation_stack_src_views_assets_backicon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/node_modules_reactnavigation_stack_src_views_assets_backicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/android/app/src/main/res/drawable-mdpi/node_modules_reactnavigation_stack_src_views_assets_backicon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-mdpi/node_modules_reactnavigation_stack_src_views_assets_backiconmask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/android/app/src/main/res/drawable-mdpi/node_modules_reactnavigation_stack_src_views_assets_backiconmask.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xhdpi/node_modules_reactnavigation_stack_src_views_assets_backicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/android/app/src/main/res/drawable-xhdpi/node_modules_reactnavigation_stack_src_views_assets_backicon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnavigation_stack_src_views_assets_backicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/android/app/src/main/res/drawable-xxhdpi/node_modules_reactnavigation_stack_src_views_assets_backicon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-xxxhdpi/node_modules_reactnavigation_stack_src_views_assets_backicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/android/app/src/main/res/drawable-xxxhdpi/node_modules_reactnavigation_stack_src_views_assets_backicon.png
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/example_appwidget_preview.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/notification_icon.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/rn_edit_text_material.xml:
--------------------------------------------------------------------------------
1 |
2 |
16 |
21 |
22 |
23 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/android/app/src/main/res/layout/improvement_roll_widget.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
20 |
21 |
32 |
33 |
44 |
45 |
50 |
51 |
59 |
60 |
67 |
68 |
--------------------------------------------------------------------------------
/android/app/src/main/res/layout/improvement_roll_widget_configure.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
17 |
18 |
23 |
24 |
32 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Improvement Roll
3 |
4 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/android/app/src/main/res/xml/improvement_roll_widget_info.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules.
2 |
3 | buildscript {
4 | ext {
5 | buildToolsVersion = "31.0.0"
6 | minSdkVersion = 21
7 | compileSdkVersion = 31
8 | targetSdkVersion = 31
9 | kotlinVersion = "1.6.21"
10 | ndkVersion = "21.4.7075529"
11 | }
12 | repositories {
13 | google()
14 | mavenCentral()
15 | }
16 | dependencies {
17 | classpath("com.android.tools.build:gradle:7.1.2")
18 | classpath("com.facebook.react:react-native-gradle-plugin")
19 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
20 | classpath("de.undercouch:gradle-download-task:4.1.2")
21 | // NOTE: Do not place your application dependencies here; they belong
22 | // in the individual module build.gradle files
23 | }
24 | }
25 |
26 | allprojects {
27 | repositories {
28 | mavenLocal()
29 | maven {
30 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
31 | url("$rootDir/../node_modules/react-native/android")
32 | }
33 | maven {
34 | // Android JSC is installed from npm
35 | url("$rootDir/../node_modules/jsc-android/dist")
36 | }
37 | mavenCentral {
38 | // We don't want to fetch react-native from Maven Central as there are
39 | // older versions over there.
40 | content {
41 | excludeGroup "com.facebook.react"
42 | }
43 | }
44 | google()
45 | mavenCentral()
46 | maven { url 'https://www.jitpack.io' }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m
13 | org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
14 |
15 | # When configured, Gradle will run in incubating parallel mode.
16 | # This option should only be used with decoupled projects. More details, visit
17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
18 | # org.gradle.parallel=true
19 |
20 | # AndroidX package structure to make it clearer which packages are bundled with the
21 | # Android operating system, and which are packaged with your app's APK
22 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
23 | android.useAndroidX=true
24 | # Automatically convert third-party libraries to use AndroidX
25 | android.enableJetifier=true
26 |
27 | # Version of flipper SDK to use with React Native
28 | FLIPPER_VERSION=0.125.0
29 |
30 | # Use this property to specify which architecture you want to build.
31 | # You can also override it from the CLI using
32 | # ./gradlew -PreactNativeArchitectures=x86_64
33 | reactNativeArchitectures=armeabi-v7a,arm64-v8a,x86,x86_64
34 |
35 | # Use this property to enable support to the new architecture.
36 | # This will allow you to use TurboModules and the Fabric render in
37 | # your application. You should enable this flag either if you want
38 | # to write custom TurboModules/Fabric components OR use libraries that
39 | # are providing them.
40 | newArchEnabled=false
41 |
42 | # Explicitly disable Codegen to prevent yarn command execution
43 | react.disableCodegen=true
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/android/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionSha256Sum=b586e04868a22fd817c8971330fec37e298f3242eb85c374181b12d637f80302
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/android/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | #
21 | # Gradle start up script for POSIX generated by Gradle.
22 | #
23 | # Important for running:
24 | #
25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
26 | # noncompliant, but you have some other compliant shell such as ksh or
27 | # bash, then to run this script, type that shell name before the whole
28 | # command line, like:
29 | #
30 | # ksh Gradle
31 | #
32 | # Busybox and similar reduced shells will NOT work, because this script
33 | # requires all of these POSIX shell features:
34 | # * functions;
35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
37 | # * compound commands having a testable exit status, especially «case»;
38 | # * various built-in commands including «command», «set», and «ulimit».
39 | #
40 | # Important for patching:
41 | #
42 | # (2) This script targets any POSIX shell, so it avoids extensions provided
43 | # by Bash, Ksh, etc; in particular arrays are avoided.
44 | #
45 | # The "traditional" practice of packing multiple parameters into a
46 | # space-separated string is a well documented source of bugs and security
47 | # problems, so this is (mostly) avoided, by progressively accumulating
48 | # options in "$@", and eventually passing that to Java.
49 | #
50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
52 | # see the in-line comments for details.
53 | #
54 | # There are tweaks for specific operating systems such as AIX, CygWin,
55 | # Darwin, MinGW, and NonStop.
56 | #
57 | # (3) This script is generated from the Groovy template
58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
59 | # within the Gradle project.
60 | #
61 | # You can find Gradle at https://github.com/gradle/gradle/.
62 | #
63 | ##############################################################################
64 |
65 | # Attempt to set APP_HOME
66 |
67 | # Resolve links: $0 may be a link
68 | app_path=$0
69 |
70 | # Need this for daisy-chained symlinks.
71 | while
72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
73 | [ -h "$app_path" ]
74 | do
75 | ls=$( ls -ld "$app_path" )
76 | link=${ls#*' -> '}
77 | case $link in #(
78 | /*) app_path=$link ;; #(
79 | *) app_path=$APP_HOME$link ;;
80 | esac
81 | done
82 |
83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
84 |
85 | APP_NAME="Gradle"
86 | APP_BASE_NAME=${0##*/}
87 |
88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
137 |
138 | Please set the JAVA_HOME variable in your environment to match the
139 | location of your Java installation."
140 | fi
141 |
142 | # Increase the maximum file descriptors if we can.
143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
144 | case $MAX_FD in #(
145 | max*)
146 | MAX_FD=$( ulimit -H -n ) ||
147 | warn "Could not query maximum file descriptor limit"
148 | esac
149 | case $MAX_FD in #(
150 | '' | soft) :;; #(
151 | *)
152 | ulimit -n "$MAX_FD" ||
153 | warn "Could not set maximum file descriptor limit to $MAX_FD"
154 | esac
155 | fi
156 |
157 | # Collect all arguments for the java command, stacking in reverse order:
158 | # * args from the command line
159 | # * the main class name
160 | # * -classpath
161 | # * -D...appname settings
162 | # * --module-path (only if needed)
163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
164 |
165 | # For Cygwin or MSYS, switch paths to Windows format before running java
166 | if "$cygwin" || "$msys" ; then
167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
169 |
170 | JAVACMD=$( cygpath --unix "$JAVACMD" )
171 |
172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
173 | for arg do
174 | if
175 | case $arg in #(
176 | -*) false ;; # don't mess with options #(
177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
178 | [ -e "$t" ] ;; #(
179 | *) false ;;
180 | esac
181 | then
182 | arg=$( cygpath --path --ignore --mixed "$arg" )
183 | fi
184 | # Roll the args list around exactly as many times as the number of
185 | # args, so each arg winds up back in the position where it started, but
186 | # possibly modified.
187 | #
188 | # NB: a `for` loop captures its iteration list before it begins, so
189 | # changing the positional parameters here affects neither the number of
190 | # iterations, nor the values presented in `arg`.
191 | shift # remove old arg
192 | set -- "$@" "$arg" # push replacement arg
193 | done
194 | fi
195 |
196 | # Collect all arguments for the java command;
197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
198 | # shell script including quotes and variable substitutions, so put them in
199 | # double quotes to make sure that they get re-expanded; and
200 | # * put everything else in single quotes, so that it's not re-expanded.
201 |
202 | set -- \
203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
204 | -classpath "$CLASSPATH" \
205 | org.gradle.wrapper.GradleWrapperMain \
206 | "$@"
207 |
208 | # Use "xargs" to parse quoted args.
209 | #
210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
211 | #
212 | # In Bash we could simply go:
213 | #
214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
215 | # set -- "${ARGS[@]}" "$@"
216 | #
217 | # but POSIX shell has neither arrays nor command substitution, so instead we
218 | # post-process each arg (as a line of input to sed) to backslash-escape any
219 | # character that might be a shell metacharacter, then use eval to reverse
220 | # that process (while maintaining the separation between arguments), and wrap
221 | # the whole thing up as a single "set" statement.
222 | #
223 | # This will of course break if any of these variables contains a newline or
224 | # an unmatched quote.
225 | #
226 |
227 | eval "set -- $(
228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
229 | xargs -n1 |
230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
231 | tr '\n' ' '
232 | )" '"$@"'
233 |
234 | exec "$JAVACMD" "$@"
235 |
--------------------------------------------------------------------------------
/android/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto init
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto init
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :init
68 | @rem Get command-line arguments, handling Windows variants
69 |
70 | if not "%OS%" == "Windows_NT" goto win9xME_args
71 |
72 | :win9xME_args
73 | @rem Slurp the command line arguments.
74 | set CMD_LINE_ARGS=
75 | set _SKIP=2
76 |
77 | :win9xME_args_slurp
78 | if "x%~1" == "x" goto execute
79 |
80 | set CMD_LINE_ARGS=%*
81 |
82 | :execute
83 | @rem Setup the command line
84 |
85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
86 |
87 | @rem Execute Gradle
88 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
89 |
90 | :end
91 | @rem End local scope for the variables with windows NT shell
92 | if "%ERRORLEVEL%"=="0" goto mainEnd
93 |
94 | :fail
95 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
96 | rem the _cmd.exe /c_ return code!
97 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
98 | exit /b 1
99 |
100 | :mainEnd
101 | if "%OS%"=="Windows_NT" endlocal
102 |
103 | :omega
104 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'improvement_roll'
2 | apply from: file("../node_modules/@react-native-community/cli-platform-android/native_modules.gradle"); applyNativeModulesSettingsGradle(settings)
3 | include ':app'
4 |
5 | include ':react-native-file-picker'
6 | project(':react-native-file-picker').projectDir = new File(settingsDir, '../node_modules/react-native-file-picker/android')
7 |
8 | include ':react-native-fs'
9 | project(':react-native-fs').projectDir = new File(settingsDir, '../node_modules/react-native-fs/android')
10 |
11 | includeBuild('../node_modules/react-native-gradle-plugin')
12 |
13 | if (settings.hasProperty("newArchEnabled") && settings.newArchEnabled == "true") {
14 | include(":ReactAndroid")
15 | project(":ReactAndroid").projectDir = file('../node_modules/react-native/ReactAndroid')
16 | }
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "improvement_roll",
3 | "displayName": "improvement_roll"
4 | }
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ['module:metro-react-native-babel-preset'],
3 | };
4 |
--------------------------------------------------------------------------------
/build-signed-apk.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | # Exit on error
4 | set -e
5 |
6 | echo "Bundling JavaScript code..."
7 | mkdir -p android/app/src/main/assets
8 | npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/
9 |
10 | echo "Cleaning Android project..."
11 | cd android
12 | ./gradlew clean
13 |
14 | echo "Building signed release APK..."
15 | ./gradlew assembleRelease
16 |
17 |
18 | echo "Done! APK is available at:"
19 | echo "$(pwd)/app/build/outputs/apk/release/improvement-roll-1.4.0.apk"
20 |
21 | # Check if user wants to install the APK to a connected device
22 | if [ "$1" == "--install" ]; then
23 | echo "Installing APK to connected device..."
24 | adb devices
25 | adb install -r app/build/outputs/apk/release/improvement-roll-1.4.0.apk
26 | fi
27 |
28 | cd ..
--------------------------------------------------------------------------------
/categories/DefaultCategories.js:
--------------------------------------------------------------------------------
1 | const generalCategory = {
2 | name: "General",
3 | timeSensitive: true,
4 | description: "A mix of physical, creative, organizational and social tasks",
5 | key: Date.now(),
6 | tasks: [
7 | {
8 | name: "Meditate for 5 minutes",
9 | desc:
10 | "Find a quiet spot and set a timer for 5 minutes. Close your eyes and focus on your breath, try not to think about anything. Do this untill the timer runs out",
11 | minutes: 5,
12 | },
13 | {
14 | name: "Meditate for 10 minutes",
15 | desc:
16 | "Find a quiet spot and set a timer for 10 minutes. Close your eyes and focus on your breath, try not to think about anything. Do this untill the timer runs out",
17 | minutes: 10,
18 | },
19 | {
20 | name: "Do 25 pushups",
21 | desc: "Get down right now and do 25 pushups!",
22 | minutes: 1,
23 | },
24 | {
25 | name: "Do 50 pushups",
26 | desc: "Get down right now and do 50 pushups!",
27 | minutes: 3,
28 | },
29 | {
30 | name: "Do 25 situps",
31 | desc: "Get down right now and do 25 situps!",
32 | minutes: 2,
33 | },
34 | {
35 | name: "Plank for 3 minutes",
36 | desc:
37 | "Get down right now and plank for a total of 1 minute. If you cant do it in a row rest and continue untill the time requirement is met.",
38 | minutes: 3,
39 | },
40 | {
41 | name: "Go for a 10 minute walk",
42 | desc: "Lace up those shoes and go walk outside for 10 minutes",
43 | minutes: 10,
44 | },
45 | {
46 | name: "Journal for 10 minutes",
47 | desc:
48 | "Grab a notebook and write down your thoughts. This can be about an even or just how you're feeling today/this week",
49 | minutes: 10,
50 | },
51 |
52 | {
53 | name: "Clean the area around you",
54 | desc:
55 | "Wherever you are right now organize and clean the area. This includes taking out trash and sweeping/vacuuming ",
56 | minutes: 20,
57 | },
58 | {
59 | name: "Read a chapter of a book",
60 | desc:
61 | "Pick up a book you've been meaning to read or have been reading. Read a chapter or for 20 minutes, whichever comes first",
62 | minutes: 25,
63 | },
64 | {
65 | name: "Go for a 20 minute walk",
66 | desc: "Lace up those shoes and go walk outside for 20 minutes",
67 | minutes: 20,
68 | },
69 | {
70 | name: "Run a mile",
71 | desc:
72 | "Lace up those shoes and go run a mile. You must run the entire way. If you get tired stop and rest before continuing.",
73 | minutes: 25,
74 | },
75 | {
76 | name: "Practice a musical instrument for 30 minutes",
77 | desc:
78 | "Find an instrument you have and deliberately play on it for 30 minutes. Dont have an instrument? Practice singing.",
79 | minutes: 30,
80 | },
81 |
82 | {
83 | name: "Organize a messy folder on your computer",
84 | desc:
85 | "Find a messy folder on your computer or phone and organize it. Name things appropriately, delete things you dont need, and backup anything you want to save.",
86 | minutes: 23,
87 | },
88 | {
89 | name: "Do a calisthenics workout",
90 | desc: "Look up a 30 minute calistenic workout online and do it.",
91 | minutes: 30,
92 | },
93 | {
94 | name: "Go for a 50 minute walk",
95 | desc:
96 | "Lace up those shoes and go walk outside for 50 minutes, If you can invite a friend, neighbor or significant other for conversation.",
97 | minutes: 50,
98 | },
99 | {
100 | name: "Groom yourself",
101 | desc: "Shave, shower and follow a skin care regimine.",
102 | minutes: 40,
103 | },
104 | {
105 | name: "Go to the gym",
106 | desc:
107 | "Go to the gym and do a workout. If you dont have a gym then follow a training excersize program found at home.",
108 | minutes: 60,
109 | },
110 | {
111 | name: "Contact a friend or family member",
112 | desc:
113 | "Look for a friend or family member you havent spoken to in some time. Call, and or text them. If they dont respond find someone else, try to give this person at least 40 minutes of your attention if permitted.",
114 | minutes: 45,
115 | },
116 |
117 | {
118 | name: "Write a short story",
119 | desc:
120 | "Come up with a short standalone story. Write it down in a notebook and share it with someone.",
121 | minutes: 90,
122 | },
123 | {
124 | name: "Go on a hike",
125 | desc:
126 | "Go on a hike. If you are going alone you can bring a podcast or audiobook. If you are going with someone try to bring up something youve been thinking about lately.",
127 | minutes: 90,
128 | },
129 | {
130 | name: "Read a book for over an hour",
131 | desc:
132 | "Pick up a book youve been meaning to read or are currently reading. Sit down with some water and read for at least and hour and a half.",
133 | minutes: 65,
134 | },
135 | ],
136 | };
137 |
138 |
139 | export default generalCategory;
--------------------------------------------------------------------------------
/categories/examples/test.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "TestCat1",
4 | "timeSensitive": true,
5 | "description": "test desc1",
6 | "tasks": [
7 | {
8 | "name": "task1",
9 | "desc": "task desc 1",
10 | "minutes": 1
11 | },
12 | {
13 | "name": "task2",
14 | "desc": "task desc 2",
15 | "minutes": 2
16 | },
17 | {
18 | "name": "task3",
19 | "desc": "task desc 3",
20 | "minutes": 3
21 | }
22 | ]
23 | },
24 | {
25 | "name": "TestCat2",
26 | "timeSensitive": false,
27 | "description": "test desc2",
28 | "tasks": [
29 | {
30 | "name": "task1",
31 | "desc": "task desc 1"
32 | },
33 | {
34 | "name": "task2",
35 | "desc": "task desc 2"
36 | },
37 | {
38 | "name": "task3",
39 | "desc": "task desc 3"
40 | }
41 | ]
42 | }
43 | ]
--------------------------------------------------------------------------------
/categories/examples/test.toml:
--------------------------------------------------------------------------------
1 | name = "Test Cateogry TOML"
2 | time_sensitive = true
3 | description = "a test cat using toml"
4 |
5 | [[tasks]]
6 | name = "task1"
7 | desc = "desc for task1"
8 | minutes =1
9 |
10 | [[tasks]]
11 | name = "task2"
12 | desc = "desc for task2"
13 | minutes =1
14 |
15 | [[tasks]]
16 | name = "task3"
17 | desc = "desc for task3"
18 | minutes =1
19 |
20 | [[tasks]]
21 | name = "task4"
22 | desc = "desc for task4"
23 | minutes =2
24 |
25 | [[tasks]]
26 | name = "task5"
27 | desc = "desc for task5"
28 | minutes =2
29 |
30 | [[tasks]]
31 | name = "task6"
32 | desc = "desc for task6"
33 | minutes =2
34 |
35 | [[tasks]]
36 | name = "task7"
37 | desc = "desc for task7"
38 | minutes =3
39 |
40 | [[tasks]]
41 | name = "task8"
42 | desc = "desc for task8"
43 | minutes =3
44 |
45 | [[tasks]]
46 | name = "task9"
47 | desc = "desc for task9"
48 | minutes =3
--------------------------------------------------------------------------------
/categories/examples/test.yaml:
--------------------------------------------------------------------------------
1 | name: Test Yaml
2 | time_sensitive: true
3 | description: This is a test for yaml import
4 |
5 | tasks:
6 | - name: task1
7 | desc: desc for task1
8 | minutes: 1
9 | - name: task2
10 | desc: desc for task2
11 | minutes: 1
12 | - name: task3
13 | desc: desc for task3
14 | minutes: 1
15 | - name: task4
16 | desc: desc for task4
17 | minutes: 2
18 | - name: task5
19 | desc: desc for task5
20 | minutes: 2
21 | - name: task6
22 | desc: desc for task6
23 | minutes: 2
24 | - name: task7
25 | desc: desc for task7
26 | minutes: 3
27 | - name: task8
28 | desc: desc for task8
29 | minutes: 3
30 | - name: task9
31 | desc: desc for task9
32 | minutes: 3
--------------------------------------------------------------------------------
/categories/examples/test.yml:
--------------------------------------------------------------------------------
1 | name: Test Yaml
2 | time_sensitive: true
3 | description: This is a test for yaml import
4 |
5 | tasks:
6 | - name: task1
7 | desc: desc for task1
8 | minutes: 1
9 | - name: task2
10 | desc: desc for task2
11 | minutes: 1
12 | - name: task3
13 | desc: desc for task3
14 | minutes: 1
15 | - name: task4
16 | desc: desc for task4
17 | minutes: 2
18 | - name: task5
19 | desc: desc for task5
20 | minutes: 2
21 | - name: task6
22 | desc: desc for task6
23 | minutes: 2
24 | - name: task7
25 | desc: desc for task7
26 | minutes: 3
27 | - name: task8
28 | desc: desc for task8
29 | minutes: 3
30 | - name: task9
31 | desc: desc for task9
32 | minutes: 3
--------------------------------------------------------------------------------
/categories/examples/test_multi.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | - name: TestCat1
3 | time_sensitive: true
4 | description: This is a category for testing for multi-category yaml import
5 | tasks:
6 | - name: task1
7 | desc: desc for task1
8 | minutes: 1
9 | - name: task2
10 | desc: desc for task2
11 | minutes: 1
12 | - name: task3
13 | desc: desc for task3
14 | minutes: 1
15 | - name: task4
16 | desc: desc for task4
17 | minutes: 2
18 | - name: task5
19 | desc: desc for task5
20 | minutes: 2
21 | - name: task6
22 | desc: desc for task6
23 | minutes: 2
24 | - name: task7
25 | desc: desc for task7
26 | minutes: 3
27 | - name: task8
28 | desc: desc for task8
29 | minutes: 3
30 | - name: task9
31 | desc: desc for task9
32 | minutes: 3
33 |
34 |
35 | - name: TestCat2
36 | time_sensitive: false
37 | description: This is another category for testing multi-category yaml import
38 | tasks:
39 | - name: task1
40 | desc: desc for task1
41 | - name: task2
42 | desc: desc for task2
43 | - name: task3
44 | desc: desc for task3
45 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/1.txt:
--------------------------------------------------------------------------------
1 | Initial Release
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/10.txt:
--------------------------------------------------------------------------------
1 | Fixing strange cursor bug on category edit page
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/11.txt:
--------------------------------------------------------------------------------
1 | Major Update!
2 | Changing tasks from using "time" to using "minutes"
3 | Added Community Category sharing
4 | More details at the repo: https://github.com/vukani-dev/improvement-roll
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/12.txt:
--------------------------------------------------------------------------------
1 | # What's New in 1.4.0
2 |
3 | * Added Home Screen Widgets - Access your tasks directly from your home screen
4 | * Added Notification System - Get random task reminders throughout the day
5 | * Customizable notification settings - Set frequency, probability, and active hours
6 | * Performance improvements and bug fixes
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/2.txt:
--------------------------------------------------------------------------------
1 | Adding dark mode toggle.
2 | Restyling.
3 | Small bug fixes.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/3.txt:
--------------------------------------------------------------------------------
1 | Adds the ability to import and export categories.
2 | Supported files are JSON, YAML, and TOML.
3 | JSON supports import/export of multiple categories
4 |
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/4.txt:
--------------------------------------------------------------------------------
1 | Adding global crash reporter with accompanying logging.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/5..txt:
--------------------------------------------------------------------------------
1 | Fixing crash when importing a single Category.
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/6.txt:
--------------------------------------------------------------------------------
1 | Adding debugmode for logging
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/7.txt:
--------------------------------------------------------------------------------
1 | Styling changes on options and roll result screen
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/8.txt:
--------------------------------------------------------------------------------
1 | Code changes on edit category screen
2 | Possible fix for galaxy phones on Category inputs
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/changelogs/9.txt:
--------------------------------------------------------------------------------
1 | Add multiple category import from yaml files
2 | Fixing export bug
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/full_description.txt:
--------------------------------------------------------------------------------
1 | Inspired from "rolling" threads on 4chan. You can create category of tasks that you want to do (in no particular order).
2 | Then press the button and it will randomly give you a task to do from said list.
3 |
4 | You can also sort tasks in a category by how long it would take to complete them and randomly select based on time.
5 |
6 | More info at https://github.com/vukani-dev/improvement-roll/
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/fastlane/metadata/android/en-US/images/featureGraphic.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/fastlane/metadata/android/en-US/images/icon.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/short_description.txt:
--------------------------------------------------------------------------------
1 | Create a list of productive things to do and improve yourself one task at a time
--------------------------------------------------------------------------------
/fastlane/metadata/android/en-US/title.txt:
--------------------------------------------------------------------------------
1 | Improvement Roll
--------------------------------------------------------------------------------
/fdroid/com.improvement_roll.yml:
--------------------------------------------------------------------------------
1 | Categories:
2 | - Writing
3 | License: GPL-3.0-only
4 | AuthorName: Vukani
5 | AuthorEmail: vukani@posteo.net
6 | AuthorWebSite: https://vukani.io
7 | WebSite: https://github.com/vukani-dev/improvement-roll
8 | SourceCode: https://github.com/vukani-dev/improvement-roll
9 | IssueTracker: https://github.com/vukani-dev/improvement-roll/issues
10 | Bitcoin: 3JEbKevTtts3ZAdt4vKnN7sbqdAkcoDKqY
11 |
12 | AutoName: Improvement Roll
13 |
14 | RepoType: git
15 | Repo: https://github.com/vukani-dev/improvement-roll
16 |
17 | Builds:
18 | - versionName: '1.0'
19 | versionCode: 1
20 | commit: v1.0
21 | subdir: android/app
22 | sudo:
23 | - sysctl fs.inotify.max_user_watches=524288
24 | - curl -Lo node.tar.xz https://nodejs.org/dist/v10.18.1/node-v10.18.1-linux-x64.tar.xz
25 | - echo "8cc40f45c2c62529b15e83a6bbe0ac1febf57af3c5720df68067c96c0fddbbdf node.tar.xz"
26 | | sha256sum -c -
27 | - tar xJf node.tar.xz
28 | - cp -a node-v10.18.1-linux-x64/. /usr/local/
29 | - npm -g install yarn
30 | init: yarn install
31 | gradle:
32 | - yes
33 | scanignore:
34 | - android/build.gradle
35 | - node_modules/jsc-android
36 | - node_modules/react-native/android/com/facebook/react/react-native/*/
37 | - node_modules/react-native-reanimated/android/build.gradle
38 | - node_modules/react-native-safe-area-context/android/build.gradle
39 | - node_modules/react-native-screens/android/build.gradle
40 | - node_modules/react-native-vector-icons/android/build.gradle
41 | - node_modules/@react-native-async-storage/async-storage/android/build.gradle
42 | - node_modules/@react-native-community/masked-view/android/build.gradle
43 | - node_modules/react-native-device-info/android/build.gradle
44 | - node_modules/react-native-svg/android/build.gradle
45 | scandelete:
46 | - node_modules/
47 |
48 | AutoUpdateMode: Version v%v
49 | UpdateCheckMode: Tags
50 | CurrentVersion: 1.0
51 | CurrentVersionCode: 1
52 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-utils": {
4 | "inputs": {
5 | "systems": "systems"
6 | },
7 | "locked": {
8 | "lastModified": 1731533236,
9 | "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
10 | "owner": "numtide",
11 | "repo": "flake-utils",
12 | "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
13 | "type": "github"
14 | },
15 | "original": {
16 | "owner": "numtide",
17 | "repo": "flake-utils",
18 | "type": "github"
19 | }
20 | },
21 | "nixpkgs": {
22 | "locked": {
23 | "lastModified": 1743095683,
24 | "narHash": "sha256-gWd4urRoLRe8GLVC/3rYRae1h+xfQzt09xOfb0PaHSk=",
25 | "owner": "NixOS",
26 | "repo": "nixpkgs",
27 | "rev": "5e5402ecbcb27af32284d4a62553c019a3a49ea6",
28 | "type": "github"
29 | },
30 | "original": {
31 | "owner": "NixOS",
32 | "ref": "nixos-unstable",
33 | "repo": "nixpkgs",
34 | "type": "github"
35 | }
36 | },
37 | "nixpkgs-node14": {
38 | "flake": false,
39 | "locked": {
40 | "lastModified": 1659446231,
41 | "narHash": "sha256-hekabNdTdgR/iLsgce5TGWmfIDZ86qjPhxDg/8TlzhE=",
42 | "owner": "NixOS",
43 | "repo": "nixpkgs",
44 | "rev": "eabc38219184cc3e04a974fe31857d8e0eac098d",
45 | "type": "github"
46 | },
47 | "original": {
48 | "owner": "NixOS",
49 | "ref": "nixos-21.11",
50 | "repo": "nixpkgs",
51 | "type": "github"
52 | }
53 | },
54 | "root": {
55 | "inputs": {
56 | "flake-utils": "flake-utils",
57 | "nixpkgs": "nixpkgs",
58 | "nixpkgs-node14": "nixpkgs-node14"
59 | }
60 | },
61 | "systems": {
62 | "locked": {
63 | "lastModified": 1681028828,
64 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
65 | "owner": "nix-systems",
66 | "repo": "default",
67 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
68 | "type": "github"
69 | },
70 | "original": {
71 | "owner": "nix-systems",
72 | "repo": "default",
73 | "type": "github"
74 | }
75 | }
76 | },
77 | "root": "root",
78 | "version": 7
79 | }
80 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "Improvement Roll";
3 |
4 | inputs = {
5 | nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6 | nixpkgs-node14 = {
7 | url = "github:NixOS/nixpkgs/nixos-21.11";
8 | flake = false;
9 | };
10 | flake-utils.url = "github:numtide/flake-utils";
11 | };
12 |
13 | outputs = { self, nixpkgs, flake-utils, nixpkgs-node14 }:
14 | flake-utils.lib.eachDefaultSystem (system:
15 | let
16 | pkgs = import nixpkgs {
17 | inherit system;
18 | config.allowUnfree = true;
19 | config.android_sdk.accept_license = true;
20 | };
21 | pkgs-node14 = import nixpkgs-node14 { inherit system; };
22 | androidComposition = pkgs.androidenv.composeAndroidPackages {
23 | platformVersions = [ "31" ];
24 | buildToolsVersions = [ "30.0.3" "31.0.0" ];
25 | includeNDK = true;
26 | ndkVersions = [ "21.4.7075529" ];
27 | };
28 | in
29 | {
30 | devShells.default = pkgs.mkShell {
31 | buildInputs = with pkgs; [
32 | pkgs-node14.nodejs-14_x
33 | pkgs.yarn
34 |
35 | jdk17
36 | gradle
37 |
38 | watchman
39 |
40 | gnumake
41 | gcc
42 | pkg-config
43 | fdroidserver
44 | androidComposition.androidsdk
45 |
46 | ];
47 |
48 | shellHook = ''
49 | # ANDROID_SDK_ROOT is deprecated but set for compatibility
50 | export ANDROID_SDK_ROOT="${androidComposition.androidsdk}/libexec/android-sdk"
51 | # Correct ANDROID_HOME path
52 | export ANDROID_HOME="${androidComposition.androidsdk}/libexec/android-sdk"
53 | # Set NDK path
54 | export ANDROID_NDK_ROOT="$ANDROID_HOME/ndk-bundle"
55 | export PATH="$ANDROID_NDK_ROOT:$PATH"
56 | export JAVA_HOME="${pkgs.jdk17}"
57 | # Point Gradle to the correct aapt2 binary
58 | export GRADLE_OPTS="-Dorg.gradle.project.android.aapt2FromMavenOverride=$ANDROID_HOME/build-tools/31.0.0/aapt2"
59 | '';
60 |
61 | };
62 | }
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @format
3 | */
4 |
5 | import {AppRegistry} from 'react-native';
6 | import App from './App';
7 | import {name as appName} from './app.json';
8 |
9 |
10 | AppRegistry.registerComponent(appName, () => App);
11 |
--------------------------------------------------------------------------------
/metro.config.js:
--------------------------------------------------------------------------------
1 | const {getDefaultConfig} = require('metro-config');
2 |
3 | module.exports = (async () => {
4 | const {
5 | resolver: {sourceExts, assetExts},
6 | } = await getDefaultConfig();
7 |
8 | return {
9 | transformer: {
10 | babelTransformerPath: require.resolve('react-native-svg-transformer'),
11 | getTransformOptions: async () => ({
12 | transform: {
13 | experimentalImportSupport: false,
14 | inlineRequires: false,
15 | },
16 | }),
17 | },
18 | resolver: {
19 | assetExts: assetExts.filter((ext) => ext !== 'svg'),
20 | sourceExts: [...sourceExts, 'svg'],
21 | },
22 | };
23 | })();
24 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "improvement_roll",
3 | "version": "1.4.0",
4 | "private": true,
5 | "scripts": {
6 | "android": "react-native run-android",
7 | "start": "react-native start",
8 | "lint": "eslint .",
9 | "clean-android": "cd android && ./gradlew clean",
10 | "build-android-release": "cd android && ./gradlew assembleRelease --info --stacktrace",
11 | "install-android-release": "cd android && ./gradlew installRelease",
12 | "build-and-install-release": "npm run clean-android && npm run build-android-release && npm run install-android-release",
13 | "bundle-android": "mkdir -p android/app/src/main/assets && npx react-native bundle --platform android --dev false --entry-file index.js --bundle-output android/app/src/main/assets/index.android.bundle --assets-dest android/app/src/main/res/",
14 | },
15 | "dependencies": {
16 | "@eva-design/eva": "2.0.0",
17 | "@iarna/toml": "2.2.5",
18 | "@react-native-async-storage/async-storage": "1.13.2",
19 | "@react-native-community/masked-view": "0.1.10",
20 | "@react-navigation/native": "5.8.9",
21 | "@react-navigation/stack": "5.12.6",
22 | "@ui-kitten/components": "5.0.0",
23 | "@ui-kitten/eva-icons": "5.0.0",
24 | "js-yaml": "4.1.0",
25 | "react": "17.0.2",
26 | "react-native": "0.68.0",
27 | "react-native-device-info": "8.0.1",
28 | "react-native-exception-handler": "2.10.10",
29 | "react-native-file-picker": "0.0.21",
30 | "react-native-fs": "2.18.0",
31 | "react-native-gesture-handler": "2.4.2",
32 | "react-native-logs": "3.0.4",
33 | "react-native-modal": "13.0.0",
34 | "react-native-permissions": "3.0.0",
35 | "react-native-safe-area-context": "4.2.5",
36 | "react-native-screens": "^3.13.1",
37 | "react-native-simple-toast": "1.1.3",
38 | "react-native-svg": "12.1.1-0",
39 | "react-native-svg-transformer": "0.14.3",
40 | "react-native-vector-icons": "7.1.0"
41 | },
42 | "devDependencies": {
43 | "@babel/core": "7.8.4",
44 | "@babel/runtime": "7.8.4",
45 | "@react-native-community/eslint-config": "1.1.0",
46 | "babel-jest": "25.1.0",
47 | "eslint": "7.32.0",
48 | "jest": "25.1.0",
49 | "metro-react-native-babel-preset": "0.67.0",
50 | "react-test-renderer": "17.0.2"
51 | },
52 | "jest": {
53 | "preset": "react-native"
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/pictures/bitcoin-btc-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/pictures/category.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/pictures/category.png
--------------------------------------------------------------------------------
/pictures/ethereum-eth-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/pictures/featureGraphic.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/pictures/featureGraphic.png
--------------------------------------------------------------------------------
/pictures/home.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/pictures/home.png
--------------------------------------------------------------------------------
/pictures/homev1.3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/pictures/homev1.3.png
--------------------------------------------------------------------------------
/pictures/icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pictures/import-comm-cat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/pictures/import-comm-cat.png
--------------------------------------------------------------------------------
/pictures/monero-xmr-logo.svg:
--------------------------------------------------------------------------------
1 | monero
--------------------------------------------------------------------------------
/pictures/roll.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vukani-dev/improvement-roll/ddce8cf95b0b561931da96e07e047434abb1c985/pictures/roll.png
--------------------------------------------------------------------------------
/screens/Categories.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { View } from 'react-native';
3 | import AsyncStorage from '@react-native-async-storage/async-storage';
4 |
5 | import * as K from '../utility_components/ui-kitten.component.js';
6 | import Toast from 'react-native-simple-toast';
7 |
8 | import Modal from "react-native-modal";
9 | import { ThemeContext } from '../utility_components/theme-context';
10 | import StyleSheetFactory from '../utility_components/styles.js';
11 |
12 | import RNFS from 'react-native-fs';
13 | import { PermissionsAndroid } from 'react-native';
14 | import * as TOML from '@iarna/toml';
15 | import * as YAML from 'js-yaml';
16 |
17 | import * as logger from '../utility_components/logging.component.js';
18 |
19 | function CategoriesScreen({ route, navigation }) {
20 | const [allCategories, setAllCategories] = React.useState([]);
21 | const [timeRanges, setTimeRanges] = React.useState([]);
22 | const [minutes, setMinutes] = React.useState();
23 | const [modalVisible, setModalVisible] = React.useState(false);
24 | const [selectedCategory, setSelectedCategory] = React.useState({})
25 | const { action, type } = route.params;
26 |
27 | const themeContext = React.useContext(ThemeContext);
28 | const styleSheet = StyleSheetFactory.getSheet(themeContext.backgroundColor);
29 |
30 | const quickTimeRanges = React.useMemo(() => [
31 | {
32 | label: '0 - 10 min',
33 | tasks: [],
34 | min: 0,
35 | max: 10
36 | },
37 | {
38 | label: '11 - 30 min',
39 | tasks: [],
40 | min: 11,
41 | max: 30
42 | },
43 | {
44 | label: '31 min - 1 hour',
45 | tasks: [],
46 | min: 31,
47 | max: 60
48 | },
49 | {
50 | label: '1 hour +',
51 | tasks: [],
52 | min: 60,
53 | max: undefined
54 | },
55 | ], []);
56 |
57 | const BackAction = React.useCallback(() => (
58 |
59 | ), [navigation]);
60 |
61 | const AddIcon = React.useCallback((props) => , []);
62 | const ExportIcon = React.useCallback((props) => , []);
63 | const BackIcon = React.useCallback((props) => , []);
64 |
65 | React.useEffect(() => {
66 | let isMounted = true;
67 | AsyncStorage.getItem('categories')
68 | .then((value) => {
69 | if (isMounted) {
70 | const categories = value != null ? JSON.parse(value) : [];
71 | categories.sort((a, b) => new Date(b.key) - new Date(a.key));
72 | setAllCategories(categories);
73 | }
74 | })
75 | .catch(error => {
76 | logger.logWarning(`Error loading categories: ${error.message}`);
77 | });
78 |
79 | return () => {
80 | isMounted = false;
81 | };
82 | }, []);
83 |
84 | const _categorySelected = (category) => {
85 | switch (action) {
86 | case 'view':
87 | navigation.navigate('AddCategory', { category: category, mode: 'edit' });
88 | break;
89 | case 'roll':
90 | if (category.timeSensitive) {
91 | // Reset time ranges at the beginning
92 | quickTimeRanges.forEach(range => {
93 | range.tasks = [];
94 | });
95 |
96 | // Limit to 4 ranges with a Set for faster lookup
97 | const rangeSet = new Set();
98 |
99 | // Process all tasks in a single pass
100 | for (let i = 0; i < category.tasks.length; i++) {
101 | const task = category.tasks[i];
102 | const taskMinutes = task.minutes;
103 |
104 | // Early exit if we already have 4 ranges
105 | if (rangeSet.size >= 4) break;
106 |
107 | // Find the correct range for this task
108 | for (let j = 0; j < quickTimeRanges.length; j++) {
109 | const range = quickTimeRanges[j];
110 |
111 | if (taskMinutes >= range.min &&
112 | (taskMinutes <= range.max || range.max === undefined)) {
113 | // Add task to this range
114 | range.tasks.push(task);
115 |
116 | // Track unique ranges with Set
117 | if (!rangeSet.has(j)) {
118 | rangeSet.add(j);
119 | }
120 |
121 | // Each task can only belong to one range in this implementation
122 | // so we can break the inner loop early
123 | break;
124 | }
125 | }
126 | }
127 |
128 | // Convert the set of indices back to ranges
129 | const newTimeRange = Array.from(rangeSet).map(index => quickTimeRanges[index]);
130 |
131 | setTimeRanges(newTimeRange);
132 | setSelectedCategory(category);
133 | setModalVisible(true);
134 | } else {
135 | navigation.navigate('Roll', { tasks: category.tasks });
136 | }
137 | break;
138 | case 'export':
139 | _export([category]);
140 | break;
141 | }
142 | };
143 |
144 | const _export = (categories) => {
145 | PermissionsAndroid.request(
146 | PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE,
147 | {
148 | title: 'Storage Permissions',
149 | message: 'Your app needs permission.',
150 | buttonNeutral: 'Ask Me Later',
151 | buttonNegative: 'Cancel',
152 | buttonPositive: 'OK',
153 | },
154 | ).then((res) => {
155 | logger.logDebug(res);
156 | var filename =
157 | categories.length > 1
158 | ? `imp_roll_categories_${Date.now()}`
159 | : categories[0].name;
160 | var path2 = RNFS.DownloadDirectoryPath + `/${filename}.${type}`;
161 | var data = '';
162 |
163 | switch (type) {
164 | case 'json':
165 | data = JSON.stringify(categories);
166 | break;
167 | case 'toml':
168 | data = TOML.stringify(categories[0]);
169 | break;
170 | case 'yaml':
171 | data = YAML.dump(categories[0])
172 | break;
173 | }
174 |
175 | RNFS.writeFile(path2, data, 'utf8')
176 | .then((success) => {
177 | //console.log(success);
178 | logger.logDebug(success);
179 | navigation.navigate('ImportExport', { action: 'export', path: path2 });
180 | })
181 | .catch((err) => {
182 | logger.logWarning(err.message);
183 | //console.log('ERROR');
184 | //logger.logDebug('ERROR');
185 | //console.log(err);
186 | //logger.logDebug(err);
187 | });
188 | });
189 | };
190 |
191 | const _renderCategoryFooter = (item) => (
192 |
193 | {item.description}
194 |
195 | );
196 |
197 | const _renderCategory = (item) => {
198 | return (
199 | _categorySelected(item)}
201 | status="info"
202 | style={{ margin: 10 }}
203 | footer={() => _renderCategoryFooter(item)}>
204 |
207 | {item.name}
208 |
209 |
210 | );
211 | };
212 |
213 | const _timeSelected = (tasks) => {
214 | setModalVisible(false);
215 | navigation.navigate('Roll', { tasks: tasks });
216 | };
217 |
218 | const _exactRoll = () => {
219 | // Avoid unnecessary calculations if no minutes value is provided
220 | if (!minutes) {
221 | Toast.show('Please enter a time value');
222 | return;
223 | }
224 |
225 | const categoryTasks = selectedCategory.tasks;
226 | // Use default time range of 2 if setting is not loaded/available
227 | const timeRange = (global.settings && global.settings.timeRange !== undefined) ? global.settings.timeRange : 2;
228 | const lowerTimeLimit = Math.max(0, minutes - timeRange);
229 | const upperTimeLimit = minutes;
230 |
231 | logger.logDebug(`Lower Time range: ${lowerTimeLimit}`);
232 | logger.logDebug(`Higher time range: ${upperTimeLimit}`);
233 |
234 | // Use filter for cleaner code and potentially better performance
235 | const filteredTasks = categoryTasks.filter(task => {
236 | const taskMinutes = Number(task.minutes);
237 | return taskMinutes >= lowerTimeLimit && taskMinutes <= upperTimeLimit;
238 | });
239 |
240 | logger.logDebug(`Filtered list of tasks to roll from:`);
241 | logger.logDebug(filteredTasks);
242 |
243 | if (filteredTasks.length === 0) {
244 | Toast.show(`No tasks found within ${timeRange} minutes of ${minutes}`);
245 | return;
246 | }
247 |
248 | setModalVisible(false);
249 | navigation.navigate('Roll', { tasks: filteredTasks });
250 | };
251 |
252 | const _renderTimeModal = () => {
253 | // Memoize the time icon to prevent re-creation on every render
254 | const timeIcon = React.useCallback((props) =>
255 | , []);
256 |
257 | // Memoize the time range buttons to avoid unnecessary re-renders
258 | const timeRangeButtons = React.useMemo(() =>
259 | timeRanges.map((timeRange) => (
260 | _timeSelected(timeRange.tasks)}
264 | style={{ marginTop: 15 }}
265 | key={timeRange.label || `range-${timeRange.min}-${timeRange.max}`}>
266 | {timeRange.label}
267 |
268 | )), [timeRanges, timeIcon]);
269 |
270 | return (
271 | setModalVisible(false)}
274 | isVisible={modalVisible}
275 | avoidKeyboard={false}
276 | style={{
277 | marginLeft: 'auto',
278 | marginRight: 'auto',
279 | left: 0,
280 | right: 0,
281 | position: 'absolute'
282 | }}
283 | >
284 |
285 | How much time do you have?
286 |
287 | Exact:
288 |
289 |
290 | setMinutes(min)}
293 | keyboardType='number-pad'
294 | style={{
295 | flex: .5,
296 | marginRight: 50,
297 | }}
298 | />
299 | _exactRoll()}
301 | accessoryLeft={timeIcon}
302 | style={{
303 | flex: .5,
304 | }}
305 | >Roll!
306 |
307 |
308 | Quick Ranges:
309 | {timeRangeButtons}
310 |
311 |
312 | );
313 | };
314 |
315 | return (
316 |
317 |
323 | {_renderTimeModal()}
324 | _renderCategory(item)}>
333 |
334 | {action == 'view' ? (
335 | navigation.navigate('AddCategory')}>
340 | Create a new Category
341 |
342 | ) : (
343 |
344 | )}
345 | {action == 'export' && type == 'json' ? (
346 | _export(allCategories)}>
352 | Export all
353 |
354 | ) : (
355 |
356 | )}
357 |
358 |
359 | );
360 | }
361 |
362 | export default React.memo(CategoriesScreen);
363 |
--------------------------------------------------------------------------------
/screens/ImportExport.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import * as Kitten from '../utility_components/ui-kitten.component.js';
4 | import * as logger from '../utility_components/logging.component.js';
5 |
6 | import Toast from 'react-native-simple-toast';
7 | import { ThemeContext } from '../utility_components/theme-context';
8 | import StyleSheetFactory from '../utility_components/styles.js';
9 | import FilePickerManager from 'react-native-file-picker';
10 | import RNFS from 'react-native-fs';
11 | import AsyncStorage from '@react-native-async-storage/async-storage';
12 |
13 | import * as TOML from '@iarna/toml';
14 | import * as YAML from 'js-yaml';
15 | import widgetManager from '../utility_components/widget-manager';
16 |
17 | export default ({ navigation, route }) => {
18 | // Handle route params once on component mount
19 | React.useEffect(() => {
20 | if (route.params) {
21 | if (route.params.action === 'export') {
22 | Toast.show(`Saved to ${route.params.path}`);
23 | }
24 | // Clear params after handling
25 | navigation.setParams({ action: undefined, path: undefined });
26 | }
27 | }, [route.params]);
28 |
29 | const importTypes = ['json', 'toml', 'yaml'];
30 | const [selectedIndex, setSelectedIndex] = React.useState(new Kitten.IndexPath(0));
31 | const displayValue = importTypes[selectedIndex.row];
32 | const [errorModalVisible, setErrorModalVisible] = React.useState(false);
33 | const [errorText, setErrorText] = React.useState('');
34 | const [errorDetailText, setErrorDetailText] = React.useState('');
35 |
36 | const BackIcon = React.useCallback((props) =>
37 | , []);
38 |
39 | const BackAction = React.useCallback(() => (
40 |
41 | ), [BackIcon, navigation]);
42 |
43 | const themeContext = React.useContext(ThemeContext);
44 | const styleSheet = StyleSheetFactory.getSheet(themeContext.backgroundColor);
45 |
46 | const getFileType = React.useCallback((file) => {
47 | const filetype = file.path
48 | .substr(file.path.length - 4)
49 | .toLowerCase();
50 |
51 | if (filetype === '.yml') {
52 | return 'yaml';
53 | }
54 | return filetype;
55 | }, []);
56 |
57 | const parseFile = React.useCallback((file, type) => {
58 | logger.logDebug(`Parsing filepath: ${file.path}, type: ${type}`);
59 |
60 | RNFS.readFile(file.path).then((res) => {
61 | try {
62 | let parsedArray = [];
63 | let parsedFile;
64 |
65 | switch (type) {
66 | case 'json':
67 | parsedFile = JSON.parse(res);
68 | break;
69 | case 'yaml':
70 | parsedFile = YAML.load(res);
71 | break;
72 | case 'toml':
73 | parsedArray.push(TOML.parse(res));
74 | break;
75 | }
76 |
77 | if (Array.isArray(parsedFile)) {
78 | parsedArray = parsedFile;
79 | } else if (parsedFile) {
80 | parsedArray.push(parsedFile);
81 | }
82 |
83 | addCategories(parsedArray);
84 | } catch (err) {
85 | logger.logFatal(err.message);
86 | return showError('Error parsing category from file. Ensure the file is formatted correctly.', err.message);
87 | }
88 | }).catch(err => {
89 | logger.logFatal(`File read error: ${err.message}`);
90 | return showError('Error reading file', err.message);
91 | });
92 | }, []);
93 |
94 | const getUniqueName = React.useCallback((name, existingCategories) => {
95 | let newName = `${name}`;
96 | // Sort categories alphabetically
97 | existingCategories.sort((a, b) => a.name.localeCompare(b.name));
98 |
99 | let instance = 1;
100 | for (let i = 0; i < existingCategories.length; i++) {
101 | if (existingCategories[i].name === newName) {
102 | if (instance > 1) {
103 | newName = newName.substring(0, newName.lastIndexOf('_'));
104 | }
105 | newName += `_${instance.toString().padStart(2, '0')}`;
106 | instance++;
107 | }
108 | }
109 | return newName;
110 | }, []);
111 |
112 | const addCategories = React.useCallback(async (categoryArray) => {
113 | try {
114 | const value = await AsyncStorage.getItem('categories');
115 | const categories = value != null ? JSON.parse(value) : [];
116 |
117 | // Process each imported category for name uniqueness
118 | for (let i = 0; i < categoryArray.length; i++) {
119 | const category = categoryArray[i];
120 |
121 | // Make sure the category has a unique name
122 | category.name = getUniqueName(category.name, categories);
123 |
124 | // If the category doesn't have a key, add one
125 | if (!category.key) {
126 | category.key = Date.now() + i;
127 | }
128 |
129 | // Ensure timeSensitive is correctly set if it's using the old format
130 | if (category.time_sensitive !== undefined && category.timeSensitive === undefined) {
131 | category.timeSensitive = category.time_sensitive;
132 | delete category.time_sensitive;
133 | }
134 |
135 | // Add the category to the list
136 | categories.push(category);
137 | }
138 |
139 | const jsonValue = JSON.stringify(categories);
140 | await AsyncStorage.setItem('categories', jsonValue);
141 | logger.logDebug('Successfully imported category');
142 |
143 | // Update widgets after import
144 | widgetManager.updateWidgets();
145 |
146 | if (categoryArray.length > 1) {
147 | Toast.show(`Imported ${categoryArray.length} categories with unique names`, 20);
148 | } else if (categoryArray.length === 1) {
149 | Toast.show(`Imported category: ${categoryArray[0].name}`, 20);
150 | }
151 | } catch (error) {
152 | logger.logWarning(`Error importing categories: ${error.message}`);
153 | showError('Error importing categories', error.message);
154 | }
155 | }, [getUniqueName]);
156 |
157 | const importFile = React.useCallback(() => {
158 | FilePickerManager.showFilePicker(null, (response) => {
159 | if (response.didCancel) {
160 | logger.logDebug('User cancelled file picker');
161 | } else if (response.error) {
162 | logger.logFatal(`Error while selecting file ===> ${response.error}`);
163 | return showError('An error occurred while selecting your file. Check the logs for details');
164 | } else {
165 | const filetype = getFileType(response);
166 | if (!importTypes.includes(filetype)) {
167 | return showError('Only JSON, TOML, and YAML files are accepted');
168 | }
169 |
170 | parseFile(response, filetype);
171 | }
172 | });
173 | }, [getFileType, importTypes, parseFile]);
174 |
175 | const showError = React.useCallback((text, detailedText = null) => {
176 | setErrorText(text);
177 | setErrorDetailText(detailedText || '');
178 | setErrorModalVisible(true);
179 | }, []);
180 |
181 | const _errorModal = React.useMemo(() => {
182 | return (
183 | setErrorModalVisible(false)}>
187 |
188 | {errorText}
189 | {errorDetailText ? {errorDetailText} : null}
190 | setErrorModalVisible(false)}>DISMISS
191 |
192 |
193 | );
194 | }, [errorModalVisible, errorText, errorDetailText, styleSheet.modal_backdrop]);
195 |
196 | return (
197 |
198 |
204 |
205 | {_errorModal}
206 |
207 |
211 |
219 | Import
220 |
221 |
222 |
228 |
230 | navigation.navigate('Categories', {
231 | action: 'export',
232 | type: displayValue,
233 | })
234 | }>
235 | Export as...
236 |
237 | setSelectedIndex(index)}
241 | value={displayValue}>
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 | );
250 | };
251 |
--------------------------------------------------------------------------------
/screens/Main.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Toast from 'react-native-simple-toast';
3 | import generalCategory from '../categories/DefaultCategories';
4 | import { Button, Icon, Text, Layout } from '@ui-kitten/components';
5 | import { DeviceEventEmitter } from 'react-native';
6 | import { getCategories, rollFromCategory } from '../utility_components/roll-helper';
7 |
8 | import { ThemeContext } from '../utility_components/theme-context';
9 | import StyleSheetFactory from '../utility_components/styles.js';
10 | import AsyncStorage from '@react-native-async-storage/async-storage';
11 |
12 | import * as logger from '../utility_components/logging.component.js';
13 |
14 | export default ({ route, navigation }) => {
15 |
16 | const themeContext = React.useContext(ThemeContext);
17 | const styleSheet = StyleSheetFactory.getSheet(themeContext.backgroundColor);
18 |
19 | // check for route params to show relevant toasts
20 | if (route.params != undefined) {
21 | switch (route.params.action) {
22 | case 'reset':
23 | Toast.show('Reset complete');
24 | AsyncStorage.setItem('categories', JSON.stringify([generalCategory]));
25 | break;
26 | default:
27 | Toast.show(
28 | `Category "${route.params.categoryName}" ${route.params.action}.`,
29 | );
30 | break;
31 | }
32 | route.params = undefined;
33 | }
34 |
35 | React.useEffect(() => {
36 |
37 | AsyncStorage.getAllKeys().then((value) => {
38 | if (value.indexOf('categories') == -1) {
39 | AsyncStorage.setItem('categories', JSON.stringify([generalCategory]));
40 | }
41 |
42 | if (value.indexOf('theme') >= 0) {
43 | AsyncStorage.getItem('theme').then((val) => {
44 | if (val == 'dark') {
45 | themeContext.toggleTheme();
46 | }
47 | });
48 | }
49 |
50 | if (value.indexOf('settings') >= 0) {
51 | AsyncStorage.getItem('settings').then((val) => {
52 | //console.log(`Current settings:`)
53 | logger.logDebug(`Current settings:`)
54 | //console.log(val)
55 | logger.logDebug(val)
56 | global.settings = JSON.parse(val)
57 | });
58 | }
59 | else {
60 | var newSettings = { debugMode: false };
61 | AsyncStorage.setItem('settings', JSON.stringify(newSettings)).then((res) => {
62 | global.settings = newSettings;
63 | });
64 | }
65 | });
66 | }, []);
67 |
68 | // Listen for notification roll again events
69 | React.useEffect(() => {
70 | const handleRollAgain = async (event) => {
71 | try {
72 | const categoryName = event.categoryName;
73 | logger.logDebug(`Roll again requested for category: ${categoryName}`);
74 |
75 | // Find the category by name
76 | const categories = await getCategories();
77 | const category = categories.find(cat => cat.name === categoryName);
78 |
79 | if (category) {
80 | // Navigate to Roll screen with the category's tasks
81 | navigation.navigate('Roll', { tasks: category.tasks });
82 | Toast.show(`Rolling from ${categoryName}`);
83 | } else {
84 | logger.logWarning(`Category ${categoryName} not found for roll again action`);
85 | // Fall back to categories screen
86 | navigation.navigate('Categories', { action: 'roll' });
87 | }
88 | } catch (error) {
89 | logger.logWarning(`Error handling roll again event: ${error.message}`);
90 | // Fall back to categories screen
91 | navigation.navigate('Categories', { action: 'roll' });
92 | }
93 | };
94 |
95 | // Add event listener
96 | const subscription = DeviceEventEmitter.addListener('onRollAgainRequested', handleRollAgain);
97 |
98 | return () => {
99 | // Remove event listener on unmount
100 | subscription.remove();
101 | };
102 | }, [navigation]);
103 |
104 | const RollIcon = (props) => ;
105 | const ListIcon = (props) => ;
106 | const SettingsIcon = (props) => ;
107 | const GlobeIcon = (props) => ;
108 |
109 | return (
110 |
111 |
112 | Improvement
113 |
114 |
115 | Roll
116 |
117 |
118 | navigation.navigate('Categories', { action: 'roll' })}>
121 | Roll
122 |
123 | navigation.navigate('Categories', { action: 'view' })}>
127 | View Categories
128 |
129 | navigation.navigate('CommunityCategories', { action: 'view' })}>
133 | Community Categories
134 |
135 |
136 | navigation.navigate('Options')}>
139 | Options
140 |
141 |
142 | );
143 | }
144 |
--------------------------------------------------------------------------------
/screens/Options.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import {Linking, ActivityIndicator} from 'react-native';
3 | import * as Kitten from '../utility_components/ui-kitten.component.js';
4 | import * as Icons from '../utility_components/icon.component.js';
5 | import * as logger from '../utility_components/logging.component.js';
6 | import AsyncStorage from '@react-native-async-storage/async-storage';
7 |
8 | import {ThemeContext} from '../utility_components/theme-context';
9 | import StyleSheetFactory from '../utility_components/styles.js';
10 | import {getVersion} from 'react-native-device-info';
11 | import {check, PERMISSIONS, request, RESULTS} from 'react-native-permissions';
12 |
13 | import BTCIcon from '../pictures/bitcoin-btc-logo.svg';
14 | import XMRIcon from '../pictures/monero-xmr-logo.svg';
15 | import Toast from 'react-native-simple-toast';
16 |
17 |
18 | export default ({navigation}) => {
19 | const [resetModalVisible, setResetModalVisible] = React.useState(false);
20 | const [debugModalVisible, setDebugModalVisible] = React.useState(false);
21 | const [debugModeText, setDebugModeText] = React.useState(
22 | global.settings.debugMode ? 'Disable Debug Mode' : 'Enable Debug Mode',
23 | );
24 | const [loading, setLoading] = React.useState(false);
25 | const themeContext = React.useContext(ThemeContext);
26 | const styleSheet = StyleSheetFactory.getSheet(themeContext.backgroundColor);
27 |
28 | const BackAction = () => (
29 |
30 | );
31 | const ImportIcon = (props) => (
32 |
33 | );
34 | const ExportIcon = (props) => (
35 |
36 | );
37 | const OctoIcon = (props) => ;
38 |
39 | const makeVersionString = () => {
40 | return `Version ${getVersion()}`;
41 | };
42 |
43 |
44 | const checkPermissions = (value) => {
45 | if (value) {
46 | check(PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE).then((status) => {
47 | if (status != RESULTS.GRANTED) {
48 | setLoading(true);
49 | request(PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE).then((result) => {
50 | if (result != RESULTS.GRANTED) {
51 | Toast.show(
52 | 'You must grant write access to use this feature.',
53 | 5000,
54 | );
55 | setDebugModalVisible(false);
56 | setLoading(false);
57 | return;
58 | }
59 | setDebugging(value);
60 | });
61 | } else {
62 | setDebugging(value);
63 | }
64 | });
65 | } else {
66 | setDebugging(value);
67 | }
68 | };
69 |
70 | const setDebugging = (value) => {
71 | global.settings.debugMode = value;
72 | AsyncStorage.setItem('settings', JSON.stringify(global.settings)).then(
73 | () => {
74 | setDebugModalVisible(false);
75 | setDebugModeText(value ? 'Disable Debug Mode' : 'Enable Debug Mode');
76 | Toast.show(`Debug mode ${value ? 'enabled.' : 'disabled.'}`);
77 | setLoading(false);
78 | },
79 | );
80 | };
81 |
82 | const toggleTheme = () => {
83 | themeContext.toggleTheme();
84 | AsyncStorage.setItem(
85 | 'theme',
86 | themeContext.theme == 'dark' ? 'light' : 'dark',
87 | );
88 | };
89 |
90 | const openGithub = () => {
91 | Linking.canOpenURL(
92 | 'https://github.com/vukani-dev/improvement-roll.git',
93 | ).then((supported) => {
94 | if (supported) {
95 | Linking.openURL('https://github.com/vukani-dev/improvement-roll.git');
96 | } else {
97 | // console.log(
98 | // "Don't know how to open URI: https://github.com/vukani-dev/improvement-roll.git",
99 | // );
100 | logger.logDebug("Don't know how to open URI: https://github.com/vukani-dev/improvement-roll.git",
101 | );
102 | }
103 | });
104 | };
105 |
106 | const _renderResetModal = () => {
107 | return (
108 | setResetModalVisible(false)}>
113 |
114 |
121 |
122 | This will clear all of your categories and re-add the "General"
123 | category.
124 |
125 | Are you sure you want to do this?
126 |
127 |
135 | setResetModalVisible(false)}>
136 | No
137 |
138 | clearData()}>Yes
139 |
140 |
141 |
142 | );
143 | };
144 |
145 | const _renderDebugModal = () => {
146 | return (
147 | <>
148 | {loading ? (
149 |
157 |
163 |
164 | ) : (
165 | setDebugModalVisible(false)}>
170 |
171 |
178 | {_debugModalText(global.settings.debugMode)}
179 | Are you sure you want to do this?
180 |
181 |
189 | setDebugModalVisible(false)}>
190 | No
191 |
192 | checkPermissions(!global.settings.debugMode)}>
194 | Yes
195 |
196 |
197 |
198 |
199 | )}
200 | >
201 | );
202 | };
203 |
204 | const _debugModalText = (debugMode) => (
205 |
206 | {debugMode ? (
207 |
208 | This will disable Debug Mode.
209 |
210 | ) : (
211 |
212 | This will enable logging on the app. Logs about crashes and warnings
213 | will be saved to the Downloads folder with the name
214 | "imp-roll-logs.txt"
215 |
216 | )}
217 |
218 | );
219 | return (
220 |
221 |
227 |
228 |
237 |
241 | navigation.navigate('ImportExport')}>
246 | Import / Export
247 |
248 | navigation.navigate('AdvSettings')}>
253 | Advanced Settings
254 |
255 |
256 |
257 |
261 |
265 | Dark Mode
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
283 |
288 | Code
289 |
290 | openGithub()} accessoryRight={OctoIcon}>
291 | View on github for instructions and code
292 |
293 | ALL feedback and suggestions are welcome!
294 |
295 |
296 |
297 |
306 | Donate
307 |
308 |
314 |
315 |
316 |
323 |
324 | 1LBQjRRWNCAMfX7s364N2HE11rG18d9Rxy
325 |
326 |
327 |
333 |
334 |
335 |
341 |
342 | 867iNA9Tgr26KKDWyoK5qeLvk7HRNGHBBZ7kqJUDysygFb1A8L2M5925viKJh31YYSKRNyWzbXpCeXyisTRvBpKAK4Q9BWU
343 |
344 |
345 |
346 |
347 |
352 |
353 | );
354 | };
355 |
--------------------------------------------------------------------------------
/screens/Roll.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { ActivityIndicator } from 'react-native';
3 |
4 | import { Text, Button, Icon, Layout } from '@ui-kitten/components';
5 |
6 | import { ThemeContext } from '../utility_components/theme-context';
7 |
8 | const RollResultScreen = ({ route, navigation }) => {
9 | const tasks = route.params.tasks;
10 | const [lastRolledTask, setLastRolledTask] = React.useState({});
11 | const [loading, setLoading] = React.useState(true);
12 |
13 | const themeContext = React.useContext(ThemeContext);
14 |
15 | // Memoize the random selection function to avoid unnecessary recalculations
16 | const rollAndPickTask = React.useCallback((taskList) => {
17 | const randomIndex = Math.floor(Math.random() * taskList.length);
18 | return taskList[randomIndex];
19 | }, []);
20 |
21 | // Load initial task only once
22 | React.useEffect(() => {
23 | const timerId = setTimeout(() => {
24 | const selectedTask = rollAndPickTask(tasks);
25 | setLastRolledTask(selectedTask);
26 | setLoading(false);
27 | }, 2000);
28 |
29 | // Clean up timer to prevent memory leaks
30 | return () => clearTimeout(timerId);
31 | }, [rollAndPickTask, tasks]);
32 |
33 | // Optimize reRoll to prevent unnecessary calculations
34 | const reRoll = React.useCallback(() => {
35 | // Don't attempt to roll if there are no tasks
36 | if (tasks.length === 0) return;
37 |
38 | setLoading(true);
39 |
40 | const timerId = setTimeout(() => {
41 | if (tasks.length > 1) {
42 | // Pre-filter tasks outside of setState callback for better performance
43 | const filteredTasks = tasks.filter((task) => task.name !== lastRolledTask.name);
44 |
45 | if (filteredTasks.length > 0) {
46 | setLastRolledTask(rollAndPickTask(filteredTasks));
47 | } else {
48 | setLastRolledTask(rollAndPickTask(tasks));
49 | }
50 | } else if (tasks.length === 1) {
51 | setLastRolledTask(tasks[0]);
52 | }
53 |
54 | setLoading(false);
55 | }, 1000);
56 |
57 | // Clean up timer to prevent memory leaks
58 | return () => clearTimeout(timerId);
59 | }, [lastRolledTask.name, rollAndPickTask, tasks]);
60 |
61 | // Memoize icon components to prevent unnecessary re-renders
62 | const renderRerollIcon = React.useCallback((props) =>
63 | , []);
64 |
65 | const homeIcon = React.useCallback((props) =>
66 | , []);
67 |
68 | // Memoize the loading view to prevent unnecessary re-renders
69 | const loadingView = React.useMemo(() => (
70 |
78 |
84 |
85 | ), [loading, themeContext.backgroundColor]);
86 |
87 | // Memoize the content view to prevent unnecessary re-renders
88 | const contentView = React.useMemo(() => (
89 | <>
90 |
96 |
97 |
103 |
109 | {lastRolledTask.name}
110 |
111 |
112 |
113 |
119 |
125 | {lastRolledTask.desc}
126 |
127 |
128 |
129 |
135 |
139 | Re-roll
140 |
141 |
142 |
143 |
144 |
150 |
155 | Home
156 |
157 |
158 | >
159 | ), [lastRolledTask, themeContext.backgroundColor, renderRerollIcon, reRoll, homeIcon, navigation.popToTop]);
160 |
161 | return (
162 | <>
163 | {loading ? loadingView : contentView}
164 | >
165 | );
166 | };
167 |
168 | export default React.memo(RollResultScreen);
169 |
--------------------------------------------------------------------------------
/utility_components/background-task-manager.js:
--------------------------------------------------------------------------------
1 | import { NativeModules, Platform } from 'react-native';
2 |
3 | const { AppFeatures } = NativeModules;
4 |
5 | /**
6 | * Background Task Manager
7 | */
8 | class BackgroundTaskManager {
9 | /**
10 | * Check if module is available (Android only)
11 | */
12 | isAvailable() {
13 | return Platform.OS === 'android' && AppFeatures !== undefined;
14 | }
15 |
16 | /**
17 | * Schedule random roll notifications with advanced settings
18 | * @param {string} categoryName Category to roll from
19 | * @param {number} frequencyHours Hours between checks
20 | * @param {number} probability Probability of showing (0-1)
21 | * @param {number} activeHoursStart Start hour (0-23)
22 | * @param {number} activeHoursEnd End hour (0-23)
23 | * @returns {Promise}
24 | */
25 | async scheduleRandomNotifications(
26 | categoryName = 'General',
27 | frequencyHours = 6,
28 | probability = 0.5,
29 | activeHoursStart = 9,
30 | activeHoursEnd = 22
31 | ) {
32 | if (this.isAvailable()) {
33 | try {
34 | return await AppFeatures.scheduleRandomNotifications(
35 | categoryName,
36 | frequencyHours,
37 | probability,
38 | activeHoursStart,
39 | activeHoursEnd
40 | );
41 | } catch (e) {
42 | console.error('Failed to schedule notifications', e);
43 | return false;
44 | }
45 | }
46 | console.warn('Background task scheduling not available on this platform.');
47 | return false;
48 | }
49 |
50 | /**
51 | * Cancel random roll notifications
52 | * @returns {Promise}
53 | */
54 | async cancelRandomNotifications() {
55 | if (this.isAvailable()) {
56 | try {
57 | return await AppFeatures.cancelRandomNotifications();
58 | } catch (e) {
59 | console.error('Failed to cancel notifications', e);
60 | return false;
61 | }
62 | }
63 | console.warn('Background task scheduling not available on this platform.');
64 | return false;
65 | }
66 | }
67 |
68 | export default new BackgroundTaskManager();
--------------------------------------------------------------------------------
/utility_components/icon.component.js:
--------------------------------------------------------------------------------
1 |
2 | import * as K from '../utility_components/ui-kitten.component.js';
3 | import * as React from 'react';
4 |
5 | const BackIcon = (props) => ;
6 | const CautionIcon = (props) => (
7 |
8 | );
9 | const ImportIcon = (props) => (
10 |
11 | );
12 | const ExportIcon = (props) => (
13 |
14 | );
15 | const OctoIcon = (props) => ;
16 | const DebugIcon = (props) => ;
17 |
18 |
19 | const RollIcon = (props) => ;
20 | const ListIcon = (props) => ;
21 | const SettingsIcon = (props) => ;
22 | const GlobeIcon = (props) => ;
23 |
24 | const SaveIcon = (props) => ;
25 | const TrashIcon = (props) => ;
26 | const AddIcon = (props) => (
27 |
28 | );
29 |
30 | export {
31 | BackIcon, CautionIcon, ImportIcon, ExportIcon,
32 | OctoIcon, DebugIcon, RollIcon, ListIcon, SettingsIcon, GlobeIcon,
33 | SaveIcon, TrashIcon, AddIcon
34 | }
--------------------------------------------------------------------------------
/utility_components/logging.component.js:
--------------------------------------------------------------------------------
1 |
2 | import { logger, fileAsyncTransport } from "react-native-logs";
3 | import RNFS from 'react-native-fs';
4 | import { check, PERMISSIONS, RESULTS } from 'react-native-permissions';
5 |
6 |
7 | const config = {
8 | transport: fileAsyncTransport,
9 | transportOptions: {
10 | FS: RNFS,
11 | fileName: `imp-roll-logs.txt`,
12 | filePath: RNFS.DownloadDirectoryPath
13 | },
14 | };
15 |
16 |
17 | var rnLogger = logger.createLogger(config);
18 |
19 | const logWarning = (log) => {
20 | logToFile(log, 'warn')
21 | }
22 |
23 | const logDebug = (log) => {
24 | logToFile(log, 'debug')
25 | }
26 |
27 | const logFatal = (log) => {
28 | logToFile(log, 'fatal')
29 | }
30 |
31 | const logToFile = (log, type) => {
32 | if(global.settings == undefined || !global.settings.debugMode)
33 | return;
34 |
35 | check(PERMISSIONS.ANDROID.WRITE_EXTERNAL_STORAGE).then((status) => {
36 | if (status == RESULTS.GRANTED) {
37 | switch (type) {
38 | case 'warn':
39 | rnLogger.warn(log)
40 | break;
41 | case 'debug':
42 | rnLogger.debug(log)
43 | break;
44 | case 'fatal':
45 | rnLogger.error(log)
46 | break;
47 | default:
48 | break;
49 | }
50 | }
51 | })
52 | }
53 |
54 | export { logWarning, logDebug, logFatal }
--------------------------------------------------------------------------------
/utility_components/mapping.json:
--------------------------------------------------------------------------------
1 | {
2 | "components": {
3 | "Button": {
4 | "meta": {},
5 | "appearances": {
6 | "filled": {
7 | "mapping": {},
8 | "variantGroups": {
9 | "status": {
10 | "primary": {
11 | "borderColor": "#800",
12 | "backgroundColor": "#800"
13 | }
14 | }
15 | }
16 | }
17 | }
18 | }
19 | }
20 | }
--------------------------------------------------------------------------------
/utility_components/navigation.component.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | NavigationContainer,
4 | DefaultTheme,
5 | DarkTheme,
6 | } from '@react-navigation/native';
7 | import {createStackNavigator} from '@react-navigation/stack';
8 | import MainScreen from '../screens/Main';
9 | import OptionsScreen from '../screens/Options';
10 | import CategoriesScreen from '../screens/Categories';
11 | import RollResultScreen from '../screens/Roll';
12 | import AddCategoryScreen from '../screens/AddCategory';
13 | import ImportExportScreen from '../screens/ImportExport';
14 | import CommunityCategories from '../screens/CommunityCategories';
15 | import AdvancedSettings from '../screens/AdvSettings';
16 |
17 | const {Navigator, Screen} = createStackNavigator();
18 |
19 | const HomeNavigator = () => (
20 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | );
34 |
35 | export const AppNavigator = () => (
36 |
37 |
38 |
39 | );
40 |
--------------------------------------------------------------------------------
/utility_components/roll-helper.js:
--------------------------------------------------------------------------------
1 | import AsyncStorage from '@react-native-async-storage/async-storage';
2 |
3 | /**
4 | * Gets all categories from storage
5 | * @returns {Promise} Categories array
6 | */
7 | export async function getCategories() {
8 | try {
9 | const value = await AsyncStorage.getItem('categories');
10 | return value != null ? JSON.parse(value) : [];
11 | } catch (error) {
12 | console.error('Error reading categories:', error);
13 | return [];
14 | }
15 | }
16 |
17 | /**
18 | * Gets a category by name
19 | * @param {string} categoryName Name of the category to find
20 | * @returns {Promise} Category object or null if not found
21 | */
22 | export async function getCategoryByName(categoryName) {
23 | try {
24 | const categories = await getCategories();
25 | return categories.find(cat => cat.name === categoryName) || null;
26 | } catch (error) {
27 | console.error('Error finding category:', error);
28 | return null;
29 | }
30 | }
31 |
32 | /**
33 | * Rolls a random task from a specific category
34 | * @param {string} categoryName Name of the category to roll from
35 | * @returns {Promise} Random task object or null if category not found
36 | */
37 | export async function rollFromCategory(categoryName) {
38 | try {
39 | const category = await getCategoryByName(categoryName);
40 | if (!category || !category.tasks || !category.tasks.length) {
41 | return null;
42 | }
43 |
44 | const randomIndex = Math.floor(Math.random() * category.tasks.length);
45 | return category.tasks[randomIndex];
46 | } catch (error) {
47 | console.error('Error rolling from category:', error);
48 | return null;
49 | }
50 | }
51 |
52 | /**
53 | * Gets the names of all available categories
54 | * @returns {Promise>} Array of category names
55 | */
56 | export async function getCategoryNames() {
57 | try {
58 | const categories = await getCategories();
59 | return categories.map(cat => cat.name);
60 | } catch (error) {
61 | console.error('Error getting category names:', error);
62 | return [];
63 | }
64 | }
65 |
66 | /**
67 | * Rolls a random task from the first available category
68 | * @returns {Promise} Random task object or null if no categories
69 | */
70 | export async function rollFromAnyCategory() {
71 | try {
72 | const categories = await getCategories();
73 | if (!categories.length) return null;
74 |
75 | // Default to first category (usually "General")
76 | const category = categories[0];
77 | if (!category.tasks || !category.tasks.length) return null;
78 |
79 | const randomIndex = Math.floor(Math.random() * category.tasks.length);
80 | return {
81 | task: category.tasks[randomIndex],
82 | categoryName: category.name
83 | };
84 | } catch (error) {
85 | console.error('Error rolling from any category:', error);
86 | return null;
87 | }
88 | }
--------------------------------------------------------------------------------
/utility_components/styles.js:
--------------------------------------------------------------------------------
1 | import { View, StyleSheet, SafeAreaView } from 'react-native';
2 | export default class StyleSheetFactory {
3 | static getSheet(themeBackgroundColor) {
4 | return StyleSheet.create({
5 | centered_container: {
6 | flex: 1,
7 | alignItems: 'center',
8 | justifyContent: 'flex-start',
9 | backgroundColor: themeBackgroundColor,
10 | },
11 | columned_container: {
12 | flex: 1,
13 | flexDirection: 'column',
14 | backgroundColor: themeBackgroundColor,
15 | },
16 | loading_container: {
17 | flexDirection: 'column',
18 | justifyContent: 'center',
19 | alignItems: 'center',
20 | flex: 1,
21 | backgroundColor: themeBackgroundColor,
22 | },
23 | modal_container: {
24 | margin: 20,
25 | marginBottom: 100,
26 | borderRadius: 20,
27 | padding: 25,
28 | shadowColor: '#000',
29 | shadowOffset: {
30 | width: 0,
31 | height: 2,
32 | },
33 | shadowOpacity: 0.25,
34 | shadowRadius: 3.84,
35 | elevation: 5,
36 | },
37 | search_modal: {
38 | flex:1,
39 | margin: 30,
40 | marginBottom: 100,
41 | borderRadius: 20,
42 | padding: 25,
43 | shadowColor: '#000',
44 | shadowOffset: {
45 | width: 0,
46 | height: 2,
47 | },
48 | shadowOpacity: 0.25,
49 | shadowRadius: 3.84,
50 | elevation: 5,
51 | },
52 | modal_backdrop: {
53 | backgroundColor: 'rgba(0, 0, 0, 0.5)',
54 | },
55 | options_container: {
56 | flex: 1,
57 | flexDirection: 'column',
58 | justifyContent: 'space-around',
59 | alignItems: 'stretch',
60 | alignContent: 'space-around',
61 | backgroundColor: themeBackgroundColor,
62 | },
63 | top_navigation: {
64 | backgroundColor: themeBackgroundColor,
65 | flex: 0.05
66 | }
67 | });
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/utility_components/theme-context.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | export const ThemeContext = React.createContext({
4 | theme: 'light',
5 | toggleTheme: () => {},
6 | backgroundColor: '#ffffee'
7 | });
--------------------------------------------------------------------------------
/utility_components/ui-kitten.component.js:
--------------------------------------------------------------------------------
1 | import {
2 | Layout,
3 | Text,
4 | TopNavigation,
5 | TopNavigationAction,
6 | Icon,
7 | Button,
8 | Select,
9 | SelectItem,
10 | IndexPath,
11 | Card,
12 | Toggle,
13 | Divider,
14 | Input,
15 | RadioGroup,
16 | Radio,
17 | List,
18 | ListItem,
19 | CheckBox
20 | } from '@ui-kitten/components';
21 |
22 | import Modal from "react-native-modal";
23 | export {Layout, Text,
24 | TopNavigation,
25 | TopNavigationAction,
26 | Icon,
27 | Button,
28 | Select,
29 | SelectItem,
30 | IndexPath,
31 | Modal,
32 | Card,
33 | Toggle,
34 | Divider,
35 | Input,
36 | RadioGroup,
37 | Radio,
38 | List,
39 | ListItem,
40 | CheckBox
41 | }
--------------------------------------------------------------------------------
/utility_components/widget-manager.js:
--------------------------------------------------------------------------------
1 | import { NativeModules, Platform } from 'react-native';
2 | import { rollFromCategory, rollFromAnyCategory, getCategoryNames } from './roll-helper';
3 |
4 | const { AppFeatures } = NativeModules;
5 |
6 | /**
7 | * Widget manager to handle widget functionality
8 | */
9 | class WidgetManager {
10 | /**
11 | * Check if widget module is available (Android only)
12 | */
13 | isAvailable() {
14 | return Platform.OS === 'android' && AppFeatures !== undefined;
15 | }
16 |
17 | /**
18 | * Update all widgets
19 | */
20 | updateWidgets() {
21 | if (this.isAvailable()) {
22 | AppFeatures.updateWidgets();
23 | return true;
24 | }
25 | return false;
26 | }
27 |
28 | /**
29 | * Get all categories for widget configuration
30 | * @returns {Promise} Array of category objects
31 | */
32 | async getCategories() {
33 | if (this.isAvailable()) {
34 | return AppFeatures.getCategories();
35 | }
36 |
37 | // Fallback to JS implementation if native module not available
38 | const categories = await getCategoryNames();
39 | return categories.map(name => ({ name, description: '' }));
40 | }
41 |
42 | /**
43 | * Roll a task from a specific category
44 | * @param {string} categoryName The category to roll from
45 | * @returns {Promise} The rolled task
46 | */
47 | async rollFromCategory(categoryName) {
48 | if (this.isAvailable()) {
49 | return AppFeatures.rollFromCategory(categoryName);
50 | }
51 |
52 | // Fallback to JS implementation if native module not available
53 | return rollFromCategory(categoryName);
54 | }
55 |
56 | /**
57 | * Roll a task from any category
58 | * @returns {Promise} The rolled task with category information
59 | */
60 | async rollFromAnyCategory() {
61 | if (this.isAvailable()) {
62 | return AppFeatures.rollFromAnyCategory();
63 | }
64 |
65 | // Fallback to JS implementation if native module not available
66 | return rollFromAnyCategory();
67 | }
68 | }
69 |
70 | export default new WidgetManager();
--------------------------------------------------------------------------------