├── .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 | banner 2 | --- 3 | A randomly selected to-do list 4 | 5 | [Get it on F-Droid](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 | home 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 | home 177 | category 178 | roll 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 | banner 2 | --- 3 | A randomly selected todo list 4 | 5 | [Get it on F-Droid](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 | home 95 | category 96 | roll 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 | 123 | 129 | 135 | 136 | 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 | 141 | 142 | 143 | 144 | 150 | 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(); --------------------------------------------------------------------------------