├── .github ├── CODEOWNERS └── workflows │ ├── check-fortune-generator-json.yml │ └── static.yml ├── .gitignore ├── .pre-commit-config.yaml ├── CONTRIBUTING.md ├── LICENSE ├── LIST_OF_GENERATORS.md ├── README.md ├── docs ├── bad_fortune_statistics.png ├── fortune_statistics.md └── good_fortune_statistics.png ├── folders.json ├── fortune_generator ├── css │ └── styles.css ├── index.html ├── js │ ├── fortune.js │ ├── matrix.js │ ├── scripts.js │ ├── service-worker.js │ └── theme.js ├── json │ ├── custom_special.json │ ├── cyclical_special.json │ ├── fortune.json │ ├── static_special.json │ └── themes.json └── manifest.json ├── images ├── fortune_generator_example.png ├── lifeadventurer-180x180.png ├── lifeadventurer-192x192.png ├── lifeadventurer-270x270.png ├── lifeadventurer-512x512.png ├── lifeadventurer.jpg ├── lifeadventurer.png ├── lifeadventurer_rounded_logo.png ├── quote_generator_example.png └── quote_generator_example_(2).png ├── index.html ├── quote_generator ├── backgrounds │ ├── dark │ │ └── .gitkeep │ └── light │ │ └── .gitkeep ├── css │ └── styles.css ├── index.html ├── js │ ├── matrix.js │ ├── quote.js │ └── scripts.js └── json │ └── quotes.json ├── scripts.js ├── scripts ├── check-events.py ├── check-fortune.py ├── check-theme.py ├── main.js ├── plot_gen.py ├── template-generator.py └── template │ ├── css │ └── styles.css │ ├── images │ ├── logo-180x180.png │ ├── logo-192x192.png │ ├── logo-270x270.png │ ├── logo-512x512.png │ └── logo.png │ ├── index.html │ ├── js │ ├── main.js │ ├── matrix.js │ ├── scripts.js │ ├── service-worker.js │ └── theme.js │ ├── json │ └── themes.json │ └── manifest.json └── styles.css /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @LifeAdventurer @tobiichi3227 2 | -------------------------------------------------------------------------------- /.github/workflows/check-fortune-generator-json.yml: -------------------------------------------------------------------------------- 1 | name: Check fortune generator JSON files 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'fortune_generator/json/custom_special.json' 7 | - 'fortune_generator/json/static_special.json' 8 | - 'fortune_generator/json/cyclical_special.json' 9 | - 'fortune_generator/json/fortune.json' 10 | - 'fortune_generator/json/themes.json' 11 | 12 | push: 13 | paths: 14 | - 'fortune_generator/json/custom_special.json' 15 | - 'fortune_generator/json/static_special.json' 16 | - 'fortune_generator/json/cyclical_special.json' 17 | - 'fortune_generator/json/fortune.json' 18 | - 'fortune_generator/json/themes.json' 19 | 20 | permissions: 21 | contents: read 22 | 23 | jobs: 24 | check: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: actions/setup-python@v5 29 | - name: Check Custom Special Events 30 | run: | 31 | python3 scripts/check-events.py fortune_generator/json/custom_special.json custom 32 | 33 | - name: Check Static Special Events 34 | run: | 35 | python3 scripts/check-events.py fortune_generator/json/static_special.json static 36 | 37 | - name: Check Cyclical Special Events 38 | run: | 39 | python3 scripts/check-events.py fortune_generator/json/cyclical_special.json cyclical 40 | 41 | - name: Check Fortune 42 | run: | 43 | python3 scripts/check-fortune.py fortune_generator/json/fortune.json 44 | 45 | - name: Check Color Theme 46 | run: | 47 | python3 scripts/check-theme.py fortune_generator/json/themes.json 48 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: write 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Single deploy job since we're just deploying 26 | deploy: 27 | environment: 28 | name: github-pages 29 | url: ${{ steps.deployment.outputs.page_url }} 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | 35 | - name: Write Commit Hash 36 | run: | 37 | cat << EOF | tee fortune_generator/json/commit_hash.json > /dev/null 38 | { "commit_hash": "$(git rev-parse HEAD)" } 39 | EOF 40 | 41 | - name: Deploy 42 | uses: peaceiris/actions-gh-pages@v4 43 | if: github.ref == 'refs/heads/main' 44 | with: 45 | github_token: ${{ secrets.GITHUB_TOKEN }} 46 | publish_dir: ./ 47 | publish_branch: gh-pages 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | scripts/res.txt 2 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v5.0.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | 12 | - repo: local 13 | hooks: 14 | - id: check-cyclical-event 15 | name: check-cyclical-event 16 | entry: python3 scripts/check-events.py fortune_generator/json/cyclical_special.json cyclical 17 | language: python 18 | files: fortune_generator/json/cyclical_special.json 19 | types: [json] 20 | pass_filenames: false 21 | 22 | - id: check-custom-event 23 | name: check-custom-event 24 | entry: python3 scripts/check-events.py fortune_generator/json/custom_special.json custom 25 | language: python 26 | files: fortune_generator/json/custom_special.json 27 | types: [json] 28 | pass_filenames: false 29 | 30 | - id: check-static-event 31 | name: check-static-event 32 | entry: python3 scripts/check-events.py fortune_generator/json/static_special.json static 33 | language: python 34 | files: fortune_generator/json/static_special.json 35 | types: [json] 36 | pass_filenames: false 37 | 38 | - id: check-fortune 39 | name: check-fortune 40 | entry: python3 scripts/check-fortune.py fortune_generator/json/fortune.json 41 | language: python 42 | files: fortune_generator/json/fortune.json 43 | types: [json] 44 | pass_filenames: false 45 | 46 | - id: check-theme 47 | name: check-theme 48 | entry: python3 scripts/check-theme.py fortune_generator/json/themes.json 49 | language: python 50 | files: fortune_generator/json/themes.json 51 | types: [json] 52 | pass_filenames: false 53 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Fortune Generator 4 | 5 | ### Fortune Events and Descriptions 6 | 7 | 1. Fortune Type: 8 | - Good fortunes 9 | - These should be added under the `"goodFortunes"` section in the JSON 10 | file. 11 | - Represent positive or beneficial events. 12 | - Bad fortunes 13 | - These should be added under the `"badFortunes"` section in the JSON file. 14 | - Represent challenging or less favorable events. 15 | 16 | 2. Unique Content: 17 | - Ensure your event and descriptions are original and not repeated in 18 | existing entries. 19 | 20 | 3. Event Structure - Each fortune event should be added as new JSON object with 21 | the following structure: 22 | ```json 23 | { 24 | "event": "Event Name", 25 | "description": [ 26 | "Description 1", 27 | "Description 2", 28 | "Description 3", 29 | "Description 4" 30 | ] 31 | } 32 | ``` 33 | 34 | 4. Maintain a positive and encouraging tone. 35 | 36 | ### Special Events 37 | #### Date Structure 38 | 1. With year, month and date 39 | ```json 40 | "triggerDate": { 41 | "year": "Year", 42 | "month": "Month", 43 | "date": "Date" 44 | } 45 | ``` 46 | 47 | We should place events of this type in the `fortune_generator/json/custom_special.json`. 48 | 49 | For one-time or irregular events, or events with complex date calculations (like the Moon Festival in the lunar calendar). 50 | 51 | **NOTE: Any special event that does not fit into either** 52 | - Static events (fixed date every year) 53 | - Cyclical events (recurring on a pattern like "fourth Thursday") 54 | 55 | 2. With only month and day 56 | ```json 57 | "triggerDate": { 58 | "month": "Month", 59 | "date": "Date" 60 | } 61 | ``` 62 | 63 | We should place events of this type in the `fortune_generator/json/static_special.json`. 64 | 65 | For events with fixed dates. 66 | 67 | 3. With only month, week, weekday (like Mother's Day) 68 | ```json 69 | "triggerDate": { 70 | "month": "Month", 71 | "week": "Week", 72 | "weekday": "Weekday" 73 | } 74 | ``` 75 | 76 | We should place events of this type in the `fortune_generator/json/cyclical_special.json`. 77 | 78 | For recurring events (e.g., holidays like Thanksgiving and Mother's Day). 79 | 80 | #### Event Structure 81 | Special events require a more detailed structure. 82 | 83 | 1. Structure: 84 | ```json 85 | { 86 | "event": "Event Name", 87 | "triggerDate": {}, // Please refer to explaination above 88 | "status_index": "Status Index", 89 | "goodFortunes": { 90 | "l_1_event": "Good Fortune 1", 91 | "l_1_desc": "Description 1", 92 | "l_2_event": "Good Fortune 2", 93 | "l_2_desc": "Description 2" 94 | }, 95 | "badFortunes": { 96 | "r_1_event": "Bad Fortune 1", 97 | "r_1_desc": "Description 1", 98 | "r_2_event": "Bad Fortune 2", 99 | "r_2_desc": "Description 2" 100 | } 101 | } 102 | ``` 103 | 104 | 2. Empty Fields: If there are no fortunes to add, leave the corresponding fields 105 | as empty strings (`""`). 106 | 107 | 3. We support adding multiple special events on the same day, 108 | and the hash function will determine which event will be shown for that day. 109 | 110 | ### Adding New Themes 111 | 112 | #### JSON Theme Structure 113 | 114 | When adding a new theme to `fortune_generator/json/themes.json`, follow this 115 | structure: 116 | 117 | ```json 118 | { 119 | "name": "theme_name", 120 | "properties": { 121 | "bg-color": "#hexcode", 122 | "good-fortune-color": "#hexcode", 123 | "bad-fortune-color": "#hexcode", 124 | "middle-fortune-color": "#hexcode", 125 | "title-color": "#hexcode", 126 | "desc-color": "#hexcode", 127 | "button-color": "#hexcode", 128 | "button-hover-color": "#hexcode", 129 | "toggle-theme-button-color": "#hexcode", 130 | "copy-result-button-color": "#hexcode", 131 | "copy-preview-result-url-button-color": "#hexcode", 132 | "date-color": "#hexcode", 133 | "special-event-color": "#hexcode" 134 | } 135 | } 136 | ``` 137 | 138 | #### Guidelines for Adding Themes 139 | 140 | 1. Naming: Choose a unique and descriptive name for the theme. 141 | 2. Properties: 142 | - Ensure that all property values are in valid hexadecimal format (`#rrggbb` 143 | or `#rrggbbaa` for transparency). 144 | - Hex Format: Use lowercase for all hex color codes for consistency. 145 | - Make sure the colors have sufficient contrast for readability. 146 | 3. Consistency: Maintain a visually coherent set of colors. 147 | 4. Testing: Preview your theme in the app to confirm that colors display as 148 | expected and are user-friendly. 149 | 5. Pull Request Naming: 150 | - Use a clear PR name like `Impr(theme): Add {theme_name} theme`. 151 | 152 | ## Quote Generator 153 | 154 | ### Quotes 155 | 156 | - Exclude content that includes any unlawful, defamatory, abusive, threatening 157 | or obscene text. 158 | - Verify that your contribution meets JSON standards, specifically avoiding 159 | trailing comma at the end of a list. 160 | - Ensure that the added quotes are not duplicates of any existing ones. 161 | - Remember to name your pull request properly. For example, if you are adding 162 | new quotes, your pull request should be named 163 | `Impr(quotes): Add {count} new quotes`. 164 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /LIST_OF_GENERATORS.md: -------------------------------------------------------------------------------- 1 | # List of Generators 2 | 3 | ### [Quote_Generator](https://lifeadventurer.github.io/generators/quote_generator) 4 | 5 | - Generate your daily quote with a button. 6 | - Background with matrix animation when generating. 7 | - If you want to contribute quotes, check the quote section in 8 | [CONTRIBUTING.md](./CONTRIBUTING.md#quote) 9 | 10 | ### [Daily_Fortune_Generator](https://lifeadventurer.github.io/generators/fortune_generator) 11 | 12 | - Generate your daily fortune with a generate button. 13 | - Background with matrix animation when generating. 14 | - Testing some features for an online judge. 15 | - remind future special events 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Generators 2 | 3 | ## Generators Gallery 4 | 5 | Visit the [Generators Gallery](https://lifeadventurer.github.io/generators) to 6 | explore a collection of generators, each accompanied by a concise description, 7 | and with links to generators. 8 | 9 | ## List of Generators 10 | 11 | | Generators | Brief Description | 12 | | ------------------------------------------------ | ------------------------------------------------------------- | 13 | | **[Quote Generator][Quote Generator]** | Generate inspiring and thought-provoking quotes effortlessly. | 14 | | **[Daily Fortune Generator][Fortune Generator]** | Get your daily fortune with just a click. | 15 | 16 | For more in-depth information about each generator, refer to 17 | [LIST_OF_GENERATORS.md](./LIST_OF_GENERATORS.md) 18 | 19 | ## Contribute 20 | 21 | Please refer to [CONTRIBUTING.md](./CONTRIBUTING.md) 22 | 23 | [Quote Generator]: https://lifeadventurer.github.io/generators/quote_generator 24 | [Fortune Generator]: https://lifeadventurer.github.io/generators/fortune_generator 25 | 26 | ## License 27 | 28 | This project is licensed under the GNU General Public License v3.0 (GPL-3.0). 29 | See the [LICENSE](./LICENSE) file for more details. 30 | -------------------------------------------------------------------------------- /docs/bad_fortune_statistics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LifeAdventurer/generators/79965ea3a8425e38e8ec0e88fa5d2798f471540c/docs/bad_fortune_statistics.png -------------------------------------------------------------------------------- /docs/fortune_statistics.md: -------------------------------------------------------------------------------- 1 | # Fortune Statistics 2 | 3 | ## Distribution of fortune 4 | 5 | ### 2000 IPs for 365 days, two groups in total 6 | 7 | | Fortune Status | Percentage (1st time) | Percentage (2nd time) | 8 | | -------------- | --------------------- | --------------------- | 9 | | 大吉 | 20.33% | 20.30% | 10 | | 中吉 | 14.37% | 14.34% | 11 | | 小吉 | 10.57% | 10.59% | 12 | | 吉 | 14.49% | 14.44% | 13 | | 末吉 | 10.14% | 10.24% | 14 | | 中平 | 14.29% | 14.40% | 15 | | 凶 | 8.64% | 8.60% | 16 | | 大凶 | 7.17% | 7.09% | 17 | 18 | ## Distribution statistics of daily fortune events 19 | 20 | Statistical method: The sum of the number of fortune events that occurred for 21 | 2,000 random IPs on the same day. 22 | 23 | The x-axis is the index value and the y-axis is the number of times. 24 | 25 | | 宜 (Good Fortune) | 忌 (Bad Fortune) | 26 | | ---------------------------------------------- | -------------------------------------------- | 27 | | ![Good Fortune](./good_fortune_statistics.png) | ![Bad Fortune](./bad_fortune_statistics.png) | 28 | 29 | [Statistics code](../dev/main.js) 30 | -------------------------------------------------------------------------------- /docs/good_fortune_statistics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LifeAdventurer/generators/79965ea3a8425e38e8ec0e88fa5d2798f471540c/docs/good_fortune_statistics.png -------------------------------------------------------------------------------- /folders.json: -------------------------------------------------------------------------------- 1 | { 2 | "folder_paths": [ 3 | "fortune_generator", 4 | "quote_generator" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /fortune_generator/css/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --button-color: #73a3eb; 3 | --button-hover-color: #459aef; 4 | --toggle-theme-button-color: #000000; 5 | --copy-result-button-color: #000000; 6 | --copy-preview-result-url-button-color: #000000; 7 | --bg-color: #ffffff; 8 | --good-fortune-color: #e74c3c; 9 | --bad-fortune-color: #000000bf; 10 | --middle-fortune-color: #5eb95e; 11 | --desc-color: #7f7f7f; 12 | --date-color: #096e1bc9; 13 | --special-event-color: #3e4fbb; 14 | --title-color: #000000cc; 15 | } 16 | 17 | * { 18 | overflow: hidden; 19 | text-align: center; 20 | white-space: nowrap; 21 | } 22 | 23 | body { 24 | margin: 0; 25 | padding: 0; 26 | height: 100%; 27 | align-items: center; 28 | justify-content: center; 29 | } 30 | 31 | .container { 32 | top: 50%; 33 | left: 50%; 34 | width: 80%; 35 | max-width: 800px; 36 | position: absolute; 37 | z-index: 1; 38 | text-align: center; 39 | transform: translate(-50%, -50%); 40 | background-color: var(--bg-color); 41 | border-radius: 40px; 42 | padding: 10px; 43 | } 44 | 45 | .good-fortune { 46 | color: var(--good-fortune-color) !important; 47 | } 48 | 49 | .bad-fortune { 50 | color: var(--bad-fortune-color) !important; 51 | } 52 | 53 | .middle-fortune { 54 | color: var(--middle-fortune-color) !important; 55 | } 56 | 57 | .desc { 58 | color: var(--desc-color); 59 | } 60 | 61 | .date-color { 62 | color: var(--date-color); 63 | } 64 | 65 | .title { 66 | color: var(--title-color); 67 | } 68 | 69 | .special-event { 70 | color: var(--special-event-color); 71 | } 72 | 73 | button { 74 | background-color: var(--button-color); 75 | color: var(--bg-color); 76 | z-index: 2; 77 | font-size: 20px; 78 | border: none; 79 | padding: 20px 20px; 80 | border-radius: 30px; 81 | cursor: pointer; 82 | transition: all 0.3s ease-in-out; 83 | } 84 | 85 | button:hover { 86 | background-color: var(--button-hover-color); 87 | } 88 | 89 | #Matrix { 90 | z-index: 0; 91 | } 92 | 93 | #toggle-theme-button { 94 | margin-top: 15px; 95 | font-size: 2.4rem; 96 | color: var(--toggle-theme-button-color); 97 | cursor: pointer; 98 | opacity: 85%; 99 | } 100 | 101 | #copy-result-button { 102 | margin-top: 20px; 103 | font-size: 2.2rem; 104 | color: var(--copy-result-button-color); 105 | } 106 | 107 | #copy-preview-result-url-button { 108 | margin-top: 20px; 109 | font-size: 2.2rem; 110 | color: var(--copy-preview-result-url-button-color); 111 | } 112 | 113 | #themeModal { 114 | .modal-content { 115 | background-color: var(--bg-color) !important; 116 | color: var(--title-color) !important; 117 | } 118 | 119 | .modal-header, 120 | .modal-footer { 121 | background-color: var(--bg-color) !important; 122 | color: var(--bg-color) !important; 123 | } 124 | 125 | .modal-title, 126 | .btn-close { 127 | color: var(--title-color) !important; 128 | } 129 | } 130 | 131 | #themeItem { 132 | background-color: var(--bg-color); 133 | color: var(--title-color); 134 | } 135 | 136 | ::-webkit-scrollbar { 137 | width: 8px; 138 | } 139 | 140 | ::-webkit-scrollbar-track { 141 | background: var(--bg-color); 142 | border-radius: 10px; 143 | } 144 | 145 | ::-webkit-scrollbar-thumb { 146 | background: var(--button-color); 147 | border-radius: 10px; 148 | } 149 | 150 | ::-webkit-scrollbar-thumb:hover { 151 | background: var(--button-hover-color); 152 | } 153 | 154 | .color-preview-container { 155 | display: flex; 156 | align-items: center; 157 | padding: 3px; 158 | border-radius: 25px; 159 | } 160 | 161 | .color-preview { 162 | display: flex; /* Use flex to align dots in a row */ 163 | } 164 | 165 | .color-dot { 166 | display: inline-block; 167 | width: 12px; /* Dot size */ 168 | height: 12px; /* Dot size */ 169 | border-radius: 50%; /* Circular shape */ 170 | margin-left: 5px; /* Spacing between dots */ 171 | } 172 | 173 | .color-preview .color-dot:first-child { 174 | margin-left: 0; /* No margin on the left for the first dot */ 175 | } 176 | 177 | .home-button { 178 | position: fixed; 179 | top: 8px; 180 | left: 8px; 181 | z-index: 1000; 182 | opacity: 0.8; /* Slightly transparent */ 183 | transition: opacity 0.3s; 184 | } 185 | -------------------------------------------------------------------------------- /fortune_generator/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Daily Fortune Generator 7 | 8 | 9 | 10 | 16 | 19 | 20 | 24 | 30 | 31 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 |
46 |

47 |
48 | 49 |
50 |
51 |
52 |

53 |
54 |
55 |

56 |
57 |
58 |

59 |
60 |
61 |
62 |
63 |

64 |
65 |
66 |
67 |

68 |
69 |
70 |

71 |
72 |
73 | 74 | 75 |
76 |
77 |

78 |
79 |
80 |
81 |
82 |

83 |
84 |
85 |

86 |
87 |
88 |
89 |
90 |

91 |
92 |
93 |

94 |
95 |
96 |
97 |
98 |
99 |
100 |

101 |
102 |
103 |

104 |
105 |
106 |
107 |
108 |

109 |
110 |
111 |

112 |
113 |
114 |
115 |
116 | 117 | 118 |
119 | 125 | 128 | 133 | 138 |
139 |
140 | 141 | 142 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 181 | 186 | 187 | 188 | -------------------------------------------------------------------------------- /fortune_generator/js/fortune.js: -------------------------------------------------------------------------------- 1 | let ip = null; 2 | fetch("https://api.ipify.org?format=json").then((response) => { 3 | if (response.ok) { 4 | return response.json(); 5 | } 6 | 7 | throw new Error("Network response was not ok."); 8 | }).then((res) => { 9 | ip = res.ip; 10 | }).catch((_error) => { 11 | if ("caches" in window) { 12 | caches.match("https://api.ipify.org?format=json").then((response) => { 13 | if (response) { 14 | return response.json(); 15 | } 16 | }).then((data) => { 17 | if (ip === null && data !== undefined) { 18 | ip = JSON.parse(data).ip; 19 | } 20 | }); 21 | } 22 | }); 23 | 24 | let goodFortunes = []; 25 | let badFortunes = []; 26 | let special_events = []; 27 | let commit_hash = ""; 28 | 29 | // using async and await to prevent fetching the data too late... 30 | async function fetch_data(requied_commit_hash) { 31 | let prefix = ""; 32 | if (requied_commit_hash) { 33 | prefix = `https://raw.githubusercontent.com/LifeAdventurer/generators/${requied_commit_hash}/fortune_generator/`; 34 | } 35 | await fetch(`${prefix}./json/fortune.json`) 36 | .then((response) => response.json()) 37 | .then((data) => { 38 | goodFortunes = data.goodFortunes; 39 | badFortunes = data.badFortunes; 40 | }); 41 | await fetch('./json/commit_hash.json') 42 | .then((response) => response.json()) 43 | .then((data) => { 44 | commit_hash = data.commit_hash; 45 | }); 46 | 47 | async function fetch_events(path) { 48 | await fetch(path) 49 | .then((response) => response.json()) 50 | .then((data) => { 51 | special_events.push(...data.special_events); 52 | }); 53 | } 54 | 55 | await fetch_events(`${prefix}./json/custom_special.json`); 56 | await fetch_events(`${prefix}./json/static_special.json`); 57 | await fetch_events(`${prefix}./json/cyclical_special.json`); 58 | } 59 | 60 | const textColorClass = [ 61 | "good-fortune", 62 | "good-fortune", 63 | "good-fortune", 64 | "good-fortune", 65 | "good-fortune", 66 | "middle-fortune", 67 | "bad-fortune", 68 | "bad-fortune", 69 | ]; 70 | const fortuneStatus = [ 71 | "大吉", 72 | "中吉", 73 | "小吉", 74 | "吉", 75 | "末吉", 76 | "中平", 77 | "凶", 78 | "大凶", 79 | ]; 80 | const chineseMonth = [ 81 | "一", 82 | "二", 83 | "三", 84 | "四", 85 | "五", 86 | "六", 87 | "七", 88 | "八", 89 | "九", 90 | "十", 91 | "十一", 92 | "十二", 93 | ]; 94 | const week = ["日", "一", "二", "三", "四", "五", "六"]; 95 | 96 | const title = 97 | `今日運勢`; 98 | const allGood = 99 | `萬事皆宜`; 100 | const allBad = 101 | `諸事不宜`; 102 | 103 | // date 104 | const d = new Date(); 105 | const date = d.getDate(); 106 | const day = d.getDay(); 107 | const month = d.getMonth() + 1; 108 | const year = d.getFullYear(); 109 | 110 | function validateNumber(value, min, max, fieldName, event) { 111 | value = parseInt(value); 112 | if (isNaN(value) || value < min || value > max) { 113 | console.warn( 114 | `illegal event: ${fieldName} should be between ${min} and ${max}`, 115 | event, 116 | ); 117 | return null; 118 | } 119 | return value; 120 | } 121 | 122 | function isLeapYear(year) { 123 | if (year % 400 === 0) return true; 124 | if (year % 100 === 0) return false; 125 | if (year % 4 === 0) return true; 126 | return false; 127 | } 128 | 129 | const daysPerMonth = [ 130 | 0, 131 | 31, 132 | 28, 133 | 31, 134 | 30, 135 | 31, 136 | 30, 137 | 31, 138 | 31, 139 | 30, 140 | 31, 141 | 30, 142 | 31, 143 | ]; 144 | const maxDate = new Date(8640000000000000); 145 | 146 | function daysDiff(eventIndex) { 147 | // define the date right now and the special event date 148 | const event = special_events[eventIndex]; 149 | const startDate = new Date(year, month - 1, date); 150 | let eventYear = -1, eventMonth = -1, eventDate = -1; 151 | if (!("triggerDate" in event)) { 152 | console.warn("illegal event: missing `triggerDate` field", event); 153 | return -1; 154 | } else if ( 155 | Object.prototype.toString.call(event.triggerDate) !== "[object Object]" 156 | ) { 157 | console.warn( 158 | "illegal event: `triggerDate` field should be a json object", 159 | event, 160 | ); 161 | return -1; 162 | } 163 | const triggerDate = event.triggerDate; 164 | 165 | let isCustomEvent = false; 166 | eventYear = year; 167 | if ("year" in triggerDate) { 168 | eventYear = validateNumber( 169 | triggerDate.year, 170 | 1, 171 | maxDate.getFullYear(), 172 | "triggerDate.year", 173 | event, 174 | ); 175 | if (eventYear === null) { 176 | return -1; 177 | } 178 | isCustomEvent = true; 179 | } 180 | 181 | if (!("month" in triggerDate)) { 182 | console.warn("illegal event: `triggerDate` missing `month` field", event); 183 | return -1; 184 | } 185 | eventMonth = validateNumber( 186 | triggerDate.month, 187 | 1, 188 | 12, 189 | "triggerDate.Month", 190 | event, 191 | ); 192 | if (eventMonth === null) { 193 | return -1; 194 | } 195 | 196 | if ( 197 | !("date" in triggerDate) && 198 | (!("week" in triggerDate) || !("weekday" in triggerDate)) 199 | ) { 200 | console.warn( 201 | "illegal event: `triggerDate` require (`week` and `weekday`) or `date` field", 202 | event, 203 | ); 204 | return -1; 205 | } 206 | 207 | if ("date" in triggerDate) { 208 | let days = daysPerMonth[eventMonth]; 209 | if (isLeapYear(eventYear) && eventMonth == 2) days += 1; 210 | eventDate = validateNumber( 211 | triggerDate.date, 212 | 1, 213 | days, 214 | "triggerDate.date", 215 | event, 216 | ); 217 | if (eventDate === null) { 218 | return -1; 219 | } 220 | } else { 221 | triggerDate.week = validateNumber( 222 | triggerDate.week, 223 | 1, 224 | 5, 225 | "triggerDate.week", 226 | event, 227 | ); 228 | triggerDate.weekday = validateNumber( 229 | triggerDate.weekday, 230 | 1, 231 | 7, 232 | "triggerDate.weekday", 233 | event, 234 | ); 235 | if (triggerDate.week === null || triggerDate.weekday === null) { 236 | return -1; 237 | } 238 | 239 | const firstDayOfMonth = new Date(eventYear, eventMonth - 1, 1); 240 | const firstDayWeekday = firstDayOfMonth.getDay(); 241 | 242 | // Sunday -> 7 243 | const adjustedFirstDayWeekday = firstDayWeekday === 0 ? 7 : firstDayWeekday; 244 | const firstTargetDay = 1 + 245 | (triggerDate.weekday - adjustedFirstDayWeekday + 7) % 7; 246 | eventDate = firstTargetDay + (triggerDate.week - 1) * 7; 247 | } 248 | 249 | if ( 250 | !isCustomEvent && 251 | (month > eventMonth || (month == eventMonth && date > eventDate)) 252 | ) { 253 | eventYear += 1; 254 | } 255 | 256 | const endDate = new Date( 257 | eventYear, 258 | eventMonth - 1, 259 | eventDate, 260 | ); 261 | 262 | // calculate the difference in milliseconds and convert it to days 263 | const timeDiff = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24)); 264 | return timeDiff; 265 | } 266 | 267 | // pre-search jquery - save to a variable to improve performance 268 | const J_l_1_event = $("#l-1-event"); 269 | const J_l_1_desc = $("#l-1-desc"); 270 | const J_l_2_event = $("#l-2-event"); 271 | const J_l_2_desc = $("#l-2-desc"); 272 | const J_r_1_event = $("#r-1-event"); 273 | const J_r_1_desc = $("#r-1-desc"); 274 | const J_r_2_event = $("#r-2-event"); 275 | const J_r_2_desc = $("#r-2-desc"); 276 | const J_ip_to_fortune = $("#ip-to-fortune"); 277 | 278 | let special = false; 279 | let special_events_index = -1; 280 | let l1 = -1, l2 = -1, r1 = -1, r2 = -1; 281 | let status_index = -1; 282 | let seed1 = -1, seed2 = -1; 283 | let fortune_generated = false; 284 | let preview_result = false; 285 | let current_day_special_events = []; 286 | 287 | // init page 288 | async function init_page() { 289 | let urlParams = new URLSearchParams(window.location.search); 290 | let commit_hash = null; 291 | if (urlParams.has('fi') && urlParams.has('si') && urlParams.has('ei'), urlParams.has('ch')) { // fortune_index, status_index, event_index, commit_hash 292 | status_index = parseInt(urlParams.get('si')); 293 | special_events_index = parseInt(urlParams.get('ei')); 294 | [l1, l2, r1, r2] = urlParams.get('fi').split(':').map(num => parseInt(num)); 295 | commit_hash = urlParams.get('ch'); 296 | if (isNaN(status_index) || isNaN(special_events_index) || isNaN(l1) || isNaN(l2) || isNaN(r1) || isNaN(r2)) { 297 | special_events_index = -1; 298 | l1 = -1, l2 = -1, r1 = -1, r2 = -1; 299 | status_index = -1; 300 | commit_hash = null; 301 | } else { 302 | preview_result = true; 303 | if (special_events_index != -1) special = true; 304 | } 305 | } 306 | // fetch data from `fortune.json` 307 | await fetch_data(commit_hash); 308 | 309 | // hide the elements of show fortune page 310 | $("#result-page").hide(); 311 | 312 | // show date before button pressed 313 | const showMonth = 314 | `${ 315 | chineseMonth[month - 1] + "月" 316 | }`; 317 | const showDate = `${ 318 | ("0" + date).slice(-2) 319 | }`; 320 | const showDay = 321 | `${ 322 | "星期" + week[day] 323 | }`; 324 | 325 | $("#month").html(showMonth); 326 | $("#date").html(showDate); 327 | $("#weekday").html(showDay); 328 | 329 | if (preview_result) Appear(); 330 | if (!preview_result) { 331 | const showSpecialEventCount = 2; 332 | let eventIndexList = Array(showSpecialEventCount).fill(-1); 333 | let eventDiffDaysIndexList = Array(showSpecialEventCount).fill( 334 | Number.MAX_SAFE_INTEGER, 335 | ); 336 | 337 | // check if there is special event today 338 | for (let i = 0; i < special_events.length; i++) { 339 | let diffCount = daysDiff(i); 340 | if (diffCount > 0) { 341 | let j = 0; 342 | for (; j < showSpecialEventCount; j++) { 343 | if (diffCount < eventDiffDaysIndexList[j]) { 344 | break; 345 | } 346 | } 347 | 348 | eventDiffDaysIndexList[j] = diffCount; 349 | eventIndexList[j] = i; 350 | } else if (diffCount === 0) { 351 | special = true; 352 | current_day_special_events.push(i); 353 | } 354 | } 355 | 356 | // if there is upcoming event then show 357 | for (let eventIndex = 0; eventIndex < showSpecialEventCount; eventIndex++) { 358 | if (eventIndexList[eventIndex] != -1) { 359 | const days = daysDiff(eventIndexList[eventIndex]); 360 | const upcoming_event = 361 | `距離${ 362 | special_events[eventIndexList[eventIndex]].event 363 | }還剩${days}`; 364 | $(`#upcoming-event-${eventIndex + 1}`).html(upcoming_event); 365 | } 366 | } 367 | 368 | const last_date_str = localStorage.getItem("last_date"); 369 | if (last_date_str !== null && last_date_str !== undefined) { 370 | const now_date = new Date(); 371 | const last_date = new Date(last_date_str); 372 | 373 | if ( 374 | now_date.getFullYear() === last_date.getFullYear() && 375 | now_date.getMonth() === last_date.getMonth() && 376 | now_date.getDate() === last_date.getDate() 377 | ) { 378 | fortune_generated = true; 379 | } 380 | } 381 | 382 | if (fortune_generated) { 383 | Update(); 384 | if (special) { 385 | special_events_index = parseInt(localStorage.getItem("last_special_index")); 386 | } 387 | } else { 388 | if (current_day_special_events.length) { 389 | special_events_index = ip.split(".").map(num => parseInt(num)).reduce((acc, cur) => acc + cur); 390 | special_events_index %= current_day_special_events.length; 391 | special_events_index = current_day_special_events[special_events_index]; 392 | localStorage.setItem("last_special_index", special_events_index); 393 | } 394 | } 395 | 396 | // show special event if today is a special day 397 | if (special) { 398 | const special_event_today = 399 | `今日是${ 400 | special_events[special_events_index].event 401 | }`; 402 | $("#special-day").html(special_event_today); 403 | } 404 | } 405 | } 406 | 407 | // event bar 408 | const good_span = (event) => 409 | `宜: ${event}`; 410 | const bad_span = (event) => 411 | `忌: ${event}`; 412 | const desc_span = (desc) => 413 | `${desc}`; 414 | 415 | function Appear() { 416 | $("#title").html(title); 417 | $("#btn").html("打卡成功"); 418 | // disable the btn 419 | $("#btn").attr("disabled", "disabled"); 420 | //change page 421 | $("#init-page").hide(); 422 | $("#result-page").show(); 423 | 424 | // some lengths 425 | const goodLen = goodFortunes.length; 426 | const badLen = badFortunes.length; 427 | const statusLen = fortuneStatus.length; 428 | 429 | if (!fortune_generated && !preview_result) { 430 | // transform ip to four numbers 431 | const num = ip.split(".").map((num) => parseInt(num)); 432 | 433 | // TODO: improve the hash process 434 | const hashDate = Math.round( 435 | Math.log10( 436 | year * 437 | ((month << (Math.log10(num[3]) + day - 1)) * 438 | (date << Math.log10(num[2] << day))), 439 | ), 440 | ); 441 | seed1 = (num[0] >> hashDate) * (num[1] >> Math.min(hashDate, 2)) + 442 | (num[2] << 1) * (num[3] >> 3) + (date << 3) * (month << hashDate) + 443 | (year * day) >> 2; 444 | seed2 = (num[0] << (hashDate + 2)) * (num[1] << hashDate) + 445 | (num[2] << 1) * (num[3] << 2) + 446 | (date << (hashDate - 1)) * (month << 4) + year >> 447 | hashDate + (date * day) >> 1; 448 | 449 | // decide the status 450 | let seedMagic = 0; 451 | if (seed1 > seed2) { 452 | seedMagic = (seed1 ^ seed2) + 453 | parseInt(seed1.toString().split("").reverse().join("")); 454 | } else if (seed1 < seed2) { 455 | let collatzLen = 0; 456 | let temp = Math.abs(seed1 - seed2); 457 | while (temp !== 1) { 458 | temp = temp % 2 === 0 ? temp / 2 : 3 * temp + 1; 459 | collatzLen++; 460 | } 461 | seedMagic = collatzLen + seed2.toString(2).replace(/0/g, "").length; 462 | } else { 463 | seedMagic = seed1 + seed2; 464 | } 465 | status_index = (seedMagic % statusLen + statusLen) % statusLen; 466 | 467 | // update last record 468 | localStorage.setItem("last_date", d.toISOString()); 469 | localStorage.setItem("last_status_index", status_index.toString()); 470 | localStorage.setItem("last_seed1", seed1.toString()); 471 | localStorage.setItem("last_seed2", seed2.toString()); 472 | } else if (!preview_result) { 473 | status_index = parseInt(localStorage.getItem("last_status_index")); 474 | seed1 = parseInt(localStorage.getItem("last_seed1")); 475 | seed2 = parseInt(localStorage.getItem("last_seed2")); 476 | } 477 | 478 | const status = `§ ${fortuneStatus[status_index]} §`; 481 | 482 | if (special) { 483 | status_index = special_events[special_events_index].status_index; 484 | const special_status = `§ ${ 487 | fortuneStatus[status_index] 488 | } §`; 489 | J_ip_to_fortune.html(special_status); 490 | } else { 491 | J_ip_to_fortune.html(status); 492 | } 493 | 494 | // make sure the events won't collide 495 | if (!preview_result) { 496 | const set = new Set(); 497 | l1 = (seed1 % goodLen + goodLen) % goodLen; 498 | set.add(goodFortunes[l1].event); 499 | l2 = (((seed1 << 1) + date) % goodLen + goodLen) % goodLen; 500 | while (set.has(goodFortunes[l2].event)) { 501 | l2 = (l2 + 1) % goodLen; 502 | } 503 | set.add(goodFortunes[l2].event); 504 | r1 = 505 | (((seed1 >> 2) + ((month * 42 + year) << 3 + 3) + 19) % badLen + badLen) % 506 | badLen; 507 | if ( 508 | r1 == 0 && 509 | (Math.abs(seed1) % 2 === Math.abs(seed2) % 2 || seed1 % 2 === 0 || 510 | seed2 % 3 === 1) 511 | ) { 512 | r1 = (r1 + (Math.abs(seed1 - seed2) % 100) >> 4) % badLen; 513 | } 514 | while (set.has(badFortunes[r1].event)) { 515 | r1 = (r1 + 7) % badLen; 516 | } 517 | set.add(badFortunes[r1].event); 518 | r2 = (((((seed1 << 3 + 7) + (year >> 5) * (date << 2 + 3)) * 519 | seed2) >> 4 + seed2 % 42) % badLen + badLen) % badLen; 520 | if ( 521 | r2 == 0 && 522 | (Math.abs(seed1) % 3 % 2 === Math.abs(seed2) % 3 % 2 || 523 | seed1 % 3 === seed2 % 2 || (month % 3 === 1 && year % 2 === 1) || 524 | month % 4 === 3 || date % 7 === 2) 525 | ) { 526 | r2 = ((r2 - (Math.abs(seed1 + seed2) % 10) >> 1) % badLen + badLen) % 527 | badLen; 528 | } 529 | while (set.has(badFortunes[r2].event)) { 530 | r2 = (r2 + 17) % badLen; 531 | } 532 | } 533 | 534 | // organize the stuffs below this line... 535 | const l1_desc_list = goodFortunes[l1].description; 536 | const l2_desc_list = goodFortunes[l2].description; 537 | const r1_desc_list = badFortunes[r1].description; 538 | const r2_desc_list = badFortunes[r2].description; 539 | const l_1_event = good_span(goodFortunes[l1].event); 540 | const l_1_desc = desc_span( 541 | l1_desc_list[Math.abs(seed1) % l1_desc_list.length], 542 | ); 543 | const l_2_event = good_span(goodFortunes[l2].event); 544 | const l_2_desc = desc_span( 545 | l2_desc_list[Math.abs(seed2) % l2_desc_list.length], 546 | ); 547 | const r_1_event = bad_span(badFortunes[r1].event); 548 | const r_1_desc = desc_span( 549 | r1_desc_list[Math.abs(seed1) % r1_desc_list.length], 550 | ); 551 | const r_2_event = bad_span(badFortunes[r2].event); 552 | const r_2_desc = desc_span( 553 | r2_desc_list[Math.abs(seed2) % r2_desc_list.length], 554 | ); 555 | 556 | if (special) { 557 | // instead clear variable name, use short variable name for here... cuz it's too repetitive 558 | const Data = special_events[special_events_index]; 559 | if (status_index == 0) { 560 | J_r_1_event.html(allGood); 561 | } else { 562 | J_r_1_event.html(bad_span(Data.badFortunes.r_1_event)); 563 | J_r_1_desc.html(desc_span(Data.badFortunes.r_1_desc)); 564 | J_r_2_event.html(bad_span(Data.badFortunes.r_2_event)); 565 | J_r_2_desc.html(desc_span(Data.badFortunes.r_2_desc)); 566 | 567 | if (Data.badFortunes.r_1_event.length == 0) { 568 | J_r_1_event.html(r_1_event); 569 | J_r_1_desc.html(r_1_desc); 570 | } 571 | if (Data.badFortunes.r_2_event.length == 0) { 572 | J_r_2_event.html(r_2_event); 573 | J_r_2_desc.html(r_2_desc); 574 | } 575 | } 576 | if (status_index == statusLen - 1) { 577 | J_l_1_event.html(allBad); 578 | } else { 579 | J_l_1_event.html(good_span(Data.goodFortunes.l_1_event)); 580 | J_l_1_desc.html(desc_span(Data.goodFortunes.l_1_desc)); 581 | J_l_2_event.html(good_span(Data.goodFortunes.l_2_event)); 582 | J_l_2_desc.html(desc_span(Data.goodFortunes.l_2_desc)); 583 | 584 | if (Data.goodFortunes.l_1_event.length == 0) { 585 | J_l_1_event.html(l_1_event); 586 | J_l_1_desc.html(l_1_desc); 587 | } 588 | if (Data.goodFortunes.l_2_event.length == 0) { 589 | J_l_2_event.html(l_2_event); 590 | J_l_2_desc.html(l_2_desc); 591 | } 592 | } 593 | } else { 594 | if (status_index == 0) { 595 | J_r_1_event.html(allGood); 596 | } else { 597 | J_r_1_event.html(r_1_event); 598 | J_r_1_desc.html(r_1_desc); 599 | J_r_2_event.html(r_2_event); 600 | J_r_2_desc.html(r_2_desc); 601 | } 602 | 603 | if (status_index == statusLen - 1) { 604 | J_l_1_event.html(allBad); 605 | } else { 606 | J_l_1_event.html(l_1_event); 607 | J_l_1_desc.html(l_1_desc); 608 | J_l_2_event.html(l_2_event); 609 | J_l_2_desc.html(l_2_desc); 610 | } 611 | } 612 | $("#copy-result-button").removeClass("d-none"); 613 | $("#copy-preview-result-url-button").removeClass("d-none"); 614 | } 615 | 616 | function copyPreviewResultUrlToClipboard() { 617 | let baseUrl = location.href.split("?")[0]; 618 | let url = `${baseUrl}?si=${status_index}&ei=${special_events_index}&fi=${[l1,l2,r1,r2].join(":")}&ch=${commit_hash.substr(0, 7)}`; 619 | navigator.clipboard.writeText(url); 620 | showCopiedNotice(); 621 | } 622 | 623 | function getLuck() { 624 | Update(); 625 | } 626 | 627 | init_page(); 628 | -------------------------------------------------------------------------------- /fortune_generator/js/matrix.js: -------------------------------------------------------------------------------- 1 | const canvas = document.getElementById("Matrix"); 2 | const context = canvas.getContext("2d"); 3 | 4 | canvas.height = globalThis.innerHeight + 100; 5 | canvas.width = globalThis.innerWidth + 5; 6 | 7 | const chars = 8 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./*-+#$%^@!~?><:;[]{}=_αβΓγΔδεζηΘθικΛλμΞξΠπρΣσςτυΦφχΨψΩω×≦≧≠∞≒≡~∩∠∪∟⊿∫∮∵∴$¥〒¢£℃€℉╩◢ⅩⅨⅧⅦⅥⅤⅣⅢⅡⅠあいうえおがぎぐげござじずぜぞだぢつでづどにぬのばひぴぶへぺぼみゃょァゐゎè"; 9 | 10 | const fontSize = 16; 11 | const columns = canvas.width / fontSize; 12 | 13 | const charArr = []; 14 | for (let i = 0; i < columns; i++) { 15 | charArr[i] = 1; 16 | } 17 | 18 | let frame = 0; 19 | let str; 20 | 21 | context.fillStyle = "rgba(0, 0, 0, 1)"; 22 | context.fillRect(0, 0, canvas.width, canvas.height); 23 | 24 | function Update() { 25 | context.fillStyle = "rgba(0, 0, 0, 0.05)"; 26 | context.fillRect(0, 0, canvas.width, canvas.height); 27 | 28 | if (frame == 0) { 29 | const a = parseInt(Math.random() * 255); 30 | str = `rgba(${a}, ${Math.abs(a - 127)}, ${Math.abs(a - 255)}, 0.9)`; 31 | } 32 | context.fillStyle = str; 33 | context.font = fontSize + "px monospace"; 34 | 35 | for (let i = 0; i < columns; i++) { 36 | const text = chars[Math.floor(Math.random() * chars.length)]; 37 | context.fillText(text, i * fontSize, charArr[i] * fontSize); 38 | if (charArr[i] * fontSize > canvas.height && Math.random() > 0.90) { 39 | charArr[i] = 0; 40 | } 41 | charArr[i]++; 42 | } 43 | 44 | frame++; 45 | if (frame <= 40 * (Math.floor(Math.random() * 10) + 3)) { 46 | requestAnimationFrame(Update); // 40 frames a cycle 47 | } else { 48 | frame = 0; 49 | Appear(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /fortune_generator/js/scripts.js: -------------------------------------------------------------------------------- 1 | function copyResultImageToClipboard() { 2 | try { 3 | const $title = $("#title").clone().wrap('
'); 4 | $("#result-page").prepend($title.parent()); 5 | 6 | const backgroundColor = 7 | getComputedStyle($(".container")[0]).backgroundColor; 8 | htmlToImage.toBlob($("#result-page")[0], { 9 | skipFonts: true, 10 | preferredFontFormat: "woff2", 11 | backgroundColor: backgroundColor, // Set background color dynamically 12 | }).then((blob) => { 13 | navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]); 14 | showCopiedNotice(); 15 | $title.parent().remove(); 16 | }).catch((error) => { 17 | console.error("Error converting result page to image:", error); 18 | $title.parent().remove(); 19 | }); 20 | } catch (error) { 21 | console.error("Error copying result image to clipboard:", error); 22 | } 23 | } 24 | 25 | function showCopiedNotice() { 26 | const notice = $("
", { 27 | text: "Copied to clipboard!", 28 | css: { 29 | position: "fixed", 30 | bottom: "20px", 31 | right: "20px", 32 | padding: "10px 20px", 33 | backgroundColor: "rgba(0, 0, 0, 0.7)", 34 | color: "#fff", 35 | borderRadius: "5px", 36 | zIndex: 1000, 37 | }, 38 | }); 39 | 40 | $("body").append(notice); 41 | 42 | setTimeout(() => { 43 | notice.fadeOut(300, () => { 44 | notice.remove(); 45 | }); 46 | }, 3000); 47 | } 48 | -------------------------------------------------------------------------------- /fortune_generator/js/service-worker.js: -------------------------------------------------------------------------------- 1 | const pre_cache_file_version = "pre-v1.1.0"; 2 | const auto_cache_file_version = "auto-v1.1.0"; 3 | 4 | const ASSETS = [ 5 | "/generators/images/lifeadventurer-192x192.png", 6 | "/generators/images/lifeadventurer-512x512.png", 7 | "/generators/images/lifeadventurer-180x180.png", 8 | "/generators/images/lifeadventurer-270x270.png", 9 | "/generators/images/lifeadventurer.jpg", 10 | 11 | "https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css", 12 | "https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js", 13 | ]; 14 | 15 | const NEED_UPDATE = [ 16 | "/generators/fortune_generator/", 17 | "/generators/fortune_generator/index.html", 18 | "/generators/fortune_generator/css/styles.css", 19 | "/generators/fortune_generator/js/fortune.js", 20 | "/generators/fortune_generator/js/matrix.js", 21 | "/generators/fortune_generator/json/custom_special.json", 22 | "/generators/fortune_generator/json/cyclical_special.json", 23 | "/generators/fortune_generator/json/static_special.json", 24 | "/generators/fortune_generator/json/fortune.json", 25 | "/generators/fortune_generator/json/manifest.json", 26 | "https://api.ipify.org/?format=json", 27 | ]; 28 | 29 | const limit_cache_size = (name, size) => { 30 | caches.open(name).then((cache) => { 31 | cache.keys().then((keys) => { 32 | if (keys.length > size) { 33 | cache.delete(keys[0]).then(() => { 34 | limit_cache_size(name, size); 35 | }); 36 | } 37 | }); 38 | }); 39 | }; 40 | 41 | const is_in_array = (str, array) => { 42 | let path = ""; 43 | 44 | // Check the request's domain is the same as the current domain. 45 | if (str.indexOf(self.origin) === 0) { 46 | path = str.substring(self.origin.length); // Remove https://lifeadventurer.github.io 47 | } else { 48 | path = str; // outside request 49 | } 50 | 51 | return array.indexOf(path) > -1; 52 | }; 53 | 54 | // install 55 | self.addEventListener("install", (event) => { 56 | self.skipWaiting(); 57 | 58 | //pre-cache files 59 | event.waitUntil( 60 | caches.open(pre_cache_file_version).then((cache) => { 61 | cache.addAll(ASSETS); 62 | }), 63 | ); 64 | }); 65 | 66 | // activate 67 | self.addEventListener("activate", (event) => { 68 | event.waitUntil( 69 | caches.keys().then((keys) => { 70 | return Promise.all(keys.map((key) => { 71 | if ( 72 | pre_cache_file_version.indexOf(key) === -1 && 73 | auto_cache_file_version.indexOf(key) === -1 74 | ) { 75 | return caches.delete(key); 76 | } 77 | })); 78 | }), 79 | ); 80 | }); 81 | 82 | // fetch event 83 | self.addEventListener("fetch", (event) => { 84 | if (is_in_array(event.request.url, ASSETS)) { 85 | // cache only strategy 86 | 87 | event.respondWith( 88 | caches.match(event.request.url), 89 | ); 90 | } else if (is_in_array(event.request.url, NEED_UPDATE)) { 91 | event.respondWith( 92 | fetch(event.request.url).then(async (response) => { 93 | if (response.ok) { 94 | const cache = await caches.open(auto_cache_file_version); 95 | cache.put(event.request.url, response.clone()); 96 | return response; 97 | } 98 | 99 | throw new Error("Network response was not ok."); 100 | }).catch(async (_error) => { 101 | const cache = await caches.open(auto_cache_file_version); 102 | return cache.match(event.request.url); 103 | }), 104 | ); 105 | } 106 | }); 107 | -------------------------------------------------------------------------------- /fortune_generator/js/theme.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", () => { 2 | const themeListContainer = document.querySelector("#themeList"); 3 | const root = document.documentElement; 4 | 5 | // Apply the saved theme if it exists 6 | applySavedTheme(); 7 | 8 | async function fetchThemes() { 9 | try { 10 | const response = await fetch("./json/themes.json"); 11 | const themes = await response.json(); 12 | populateThemeList(themes["themes"]); 13 | } catch (error) { 14 | console.error("Error fetching themes:", error); 15 | } 16 | } 17 | 18 | // Populate theme list in modal 19 | function populateThemeList(themes) { 20 | themeListContainer.innerHTML = ""; 21 | themes.forEach((theme) => { 22 | const themeItem = document.createElement("div"); 23 | themeItem.className = 24 | "theme-item list-group-item d-flex justify-content-between align-items-center"; 25 | themeItem.style.cursor = "pointer"; 26 | themeItem.id = "themeItem"; 27 | 28 | // Add theme name 29 | const themeName = document.createElement("span"); 30 | themeName.textContent = theme.name; 31 | themeItem.appendChild(themeName); 32 | 33 | const colorPreivewContainer = document.createElement("div"); 34 | colorPreivewContainer.className = "color-preview-container"; 35 | 36 | const propertyKeys = Object.keys(theme.properties); 37 | colorPreivewContainer.style.backgroundColor = 38 | theme.properties[propertyKeys[5]]; 39 | 40 | // Add color dots for visual preview 41 | const colorPreview = document.createElement("div"); 42 | colorPreview.className = "color-preview"; 43 | 44 | Object.values(theme.properties).slice(0, 3).forEach((color) => { 45 | const colorDot = document.createElement("span"); 46 | colorDot.style.backgroundColor = color; 47 | colorDot.className = "color-dot"; 48 | colorPreview.appendChild(colorDot); 49 | }); 50 | 51 | colorPreivewContainer.appendChild(colorPreview); 52 | themeItem.appendChild(colorPreivewContainer); 53 | 54 | // Apply theme on click 55 | themeItem.addEventListener("click", () => { 56 | applyTheme(theme.properties); 57 | saveThemeToLocalStorage(theme.name); 58 | }); 59 | 60 | themeListContainer.appendChild(themeItem); 61 | }); 62 | } 63 | 64 | // Apply theme by setting CSS variables 65 | function applyTheme(properties) { 66 | Object.entries(properties).forEach(([key, value]) => { 67 | root.style.setProperty(`--${key}`, value); 68 | }); 69 | } 70 | 71 | function saveThemeToLocalStorage(themeName) { 72 | localStorage.setItem("selectedTheme", themeName); 73 | } 74 | 75 | function applySavedTheme() { 76 | const savedThemeName = localStorage.getItem("selectedTheme"); 77 | if (savedThemeName) { 78 | fetch("./json/themes.json") 79 | .then((response) => response.json()) 80 | .then((themes) => { 81 | const theme = themes.themes.find((t) => t.name === savedThemeName); 82 | if (theme) { 83 | applyTheme(theme.properties); 84 | } 85 | }) 86 | .catch((error) => console.error("Error fetching themes:", error)); 87 | } 88 | } 89 | 90 | fetchThemes(); 91 | }); 92 | -------------------------------------------------------------------------------- /fortune_generator/json/custom_special.json: -------------------------------------------------------------------------------- 1 | { 2 | "special_events": [ 3 | { 4 | "event": "夏至", 5 | "triggerDate": { 6 | "year": "2025", 7 | "month": "6", 8 | "date": "21" 9 | }, 10 | "status_index": "0", 11 | "goodFortunes": { 12 | "l_1_event": "觀賞日出和日落", 13 | "l_1_desc": "享受一年最長的白天", 14 | "l_2_event": "", 15 | "l_2_desc": "" 16 | }, 17 | "badFortunes": { 18 | "r_1_event": "", 19 | "r_1_desc": "", 20 | "r_2_event": "", 21 | "r_2_desc": "" 22 | } 23 | }, 24 | { 25 | "event": "中秋節", 26 | "triggerDate": { 27 | "year": "2025", 28 | "month": "10", 29 | "date": "6" 30 | }, 31 | "status_index": "0", 32 | "goodFortunes": { 33 | "l_1_event": "賞月", 34 | "l_1_desc": "與家人一同賞月,增進感情", 35 | "l_2_event": "吃月餅", 36 | "l_2_desc": "與家人朋友分享月餅的美味" 37 | }, 38 | "badFortunes": { 39 | "r_1_event": "", 40 | "r_1_desc": "", 41 | "r_2_event": "", 42 | "r_2_desc": "" 43 | } 44 | }, 45 | { 46 | "event": "冬至", 47 | "triggerDate": { 48 | "year": "2025", 49 | "month": "12", 50 | "date": "21" 51 | }, 52 | "status_index": "0", 53 | "goodFortunes": { 54 | "l_1_event": "吃湯圓", 55 | "l_1_desc": "團團圓圓", 56 | "l_2_event": "保暖", 57 | "l_2_desc": "冬至到了" 58 | }, 59 | "badFortunes": { 60 | "r_1_event": "", 61 | "r_1_desc": "", 62 | "r_2_event": "", 63 | "r_2_desc": "" 64 | } 65 | } 66 | ] 67 | } 68 | -------------------------------------------------------------------------------- /fortune_generator/json/cyclical_special.json: -------------------------------------------------------------------------------- 1 | { 2 | "special_events": [ 3 | { 4 | "event": "母親節", 5 | "triggerDate": { 6 | "month": "5", 7 | "week": "2", 8 | "weekday": "7" 9 | }, 10 | "status_index": "0", 11 | "goodFortunes": { 12 | "l_1_event": "家庭聚餐", 13 | "l_1_desc": "表達對媽媽的感恩之心", 14 | "l_2_event": "", 15 | "l_2_desc": "" 16 | }, 17 | "badFortunes": { 18 | "r_1_event": "", 19 | "r_1_desc": "", 20 | "r_2_event": "", 21 | "r_2_desc": "" 22 | } 23 | }, 24 | { 25 | "event": "感恩節", 26 | "triggerDate": { 27 | "month": "11", 28 | "week": "4", 29 | "weekday": "4" 30 | }, 31 | "status_index": "0", 32 | "goodFortunes": { 33 | "l_1_event": "家人團聚", 34 | "l_1_desc": "分享寶貴時光", 35 | "l_2_event": "吃火雞大餐", 36 | "l_2_desc": "Happy Thanksgiving!" 37 | }, 38 | "badFortunes": { 39 | "r_1_event": "", 40 | "r_1_desc": "", 41 | "r_2_event": "", 42 | "r_2_desc": "" 43 | } 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /fortune_generator/json/fortune.json: -------------------------------------------------------------------------------- 1 | { 2 | "goodFortunes": [ 3 | { 4 | "event": "做家務", 5 | "description": [ 6 | "整潔使人心情愉悅", 7 | "增加運動量", 8 | "培養責任感", 9 | "增加成就感" 10 | ] 11 | }, 12 | { 13 | "event": "冥想", 14 | "description": [ 15 | "平靜心靈,緩解焦慮", 16 | "調節情緒", 17 | "改善睡眠", 18 | "提高專注", 19 | "減輕壓力" 20 | ] 21 | }, 22 | { 23 | "event": "攝影", 24 | "description": [ 25 | "捕捉到美好瞬間", 26 | "激發想像力" 27 | ] 28 | }, 29 | { 30 | "event": "喝咖啡", 31 | "description": [ 32 | "精力充沛", 33 | "燃燒脂肪" 34 | ] 35 | }, 36 | { 37 | "event": "朋友聚會", 38 | "description": [ 39 | "充滿歡笑和美好回憶", 40 | "提升情感連結", 41 | "緩解壓力" 42 | ] 43 | }, 44 | { 45 | "event": "體育鍛鍊", 46 | "description": [ 47 | "能量滿滿,效果顯著", 48 | "塑造身材", 49 | "增強心肺功能" 50 | ] 51 | }, 52 | { 53 | "event": "出遊", 54 | "description": [ 55 | "好天氣,好心情" 56 | ] 57 | }, 58 | { 59 | "event": "吃大餐", 60 | "description": [ 61 | "聯絡感情" 62 | ] 63 | }, 64 | { 65 | "event": "逛書店", 66 | "description": [ 67 | "新書上架,打折推銷" 68 | ] 69 | }, 70 | { 71 | "event": "學新技能", 72 | "description": [ 73 | "快速上手" 74 | ] 75 | }, 76 | { 77 | "event": "唱歌", 78 | "description": [ 79 | "被星探發掘" 80 | ] 81 | }, 82 | { 83 | "event": "上課", 84 | "description": [ 85 | "整天不累,100% 消化" 86 | ] 87 | }, 88 | { 89 | "event": "洗澡", 90 | "description": [ 91 | "重獲能量", 92 | "身心舒暢" 93 | ] 94 | }, 95 | { 96 | "event": "請教問題", 97 | "description": [ 98 | "問題皆獲高人指點" 99 | ] 100 | }, 101 | { 102 | "event": "網購", 103 | "description": [ 104 | "心儀商品皆促銷" 105 | ] 106 | }, 107 | { 108 | "event": "放假", 109 | "description": [ 110 | "休息充電,明日再戰", 111 | "減輕壓力", 112 | "探索新興趣" 113 | ] 114 | }, 115 | { 116 | "event": "早睡", 117 | "description": [ 118 | "好夢連連", 119 | "調整生物鐘", 120 | "減少壓力", 121 | "提高免疫力", 122 | "改善皮膚", 123 | "提升工作效率" 124 | ] 125 | }, 126 | { 127 | "event": "早起", 128 | "description": [ 129 | "朝氣蓬勃,神采飛揚" 130 | ] 131 | }, 132 | { 133 | "event": "發文章", 134 | "description": [ 135 | "瀏覽數暴增", 136 | "增加影響力", 137 | "促進交流" 138 | ] 139 | }, 140 | { 141 | "event": "點外賣", 142 | "description": [ 143 | "準時到達", 144 | "新鮮好吃", 145 | "減少清理" 146 | ] 147 | }, 148 | { 149 | "event": "做善事", 150 | "description": [ 151 | "積善成福", 152 | "助人為樂", 153 | "培養同理心", 154 | "心靈充實", 155 | "增加幸福感" 156 | ] 157 | }, 158 | { 159 | "event": "散步", 160 | "description": [ 161 | "空氣良好", 162 | "放鬆身心" 163 | ] 164 | }, 165 | { 166 | "event": "觀星", 167 | "description": [ 168 | "欣賞星空", 169 | "享受寧靜" 170 | ] 171 | }, 172 | { 173 | "event": "野餐", 174 | "description": [ 175 | "在大自然中享受美食" 176 | ] 177 | }, 178 | { 179 | "event": "釣魚", 180 | "description": [ 181 | "收穫滿滿" 182 | ] 183 | }, 184 | { 185 | "event": "烹飪", 186 | "description": [ 187 | "陶冶情操", 188 | "廚藝提升", 189 | "養成飲食習慣" 190 | ] 191 | }, 192 | { 193 | "event": "爬山", 194 | "description": [ 195 | "挑戰自我", 196 | "促進健康" 197 | ] 198 | }, 199 | { 200 | "event": "逛街", 201 | "description": [ 202 | "買到心儀的物品", 203 | "發現新奇事物", 204 | "心情愉快" 205 | ] 206 | }, 207 | { 208 | "event": "看電影", 209 | "description": [ 210 | "增加話題", 211 | "與朋友同樂", 212 | "放鬆心情" 213 | ] 214 | }, 215 | { 216 | "event": "聽音樂會", 217 | "description": [ 218 | "增加藝術氣息", 219 | "放鬆身心, 享受音樂" 220 | ] 221 | } 222 | ], 223 | "badFortunes": [ 224 | { 225 | "event": "體育鍛鍊", 226 | "description": [ 227 | "不慎受傷" 228 | ] 229 | }, 230 | { 231 | "event": "攝影", 232 | "description": [ 233 | "照片全消失" 234 | ] 235 | }, 236 | { 237 | "event": "出遊", 238 | "description": [ 239 | "天氣不晴朗" 240 | ] 241 | }, 242 | { 243 | "event": "吃大餐", 244 | "description": [ 245 | "被要求請客" 246 | ] 247 | }, 248 | { 249 | "event": "學新技能", 250 | "description": [ 251 | "屢試不爽,始終不懂" 252 | ] 253 | }, 254 | { 255 | "event": "唱歌", 256 | "description": [ 257 | "嗓子發炎" 258 | ] 259 | }, 260 | { 261 | "event": "洗澡", 262 | "description": [ 263 | "水溫不穩" 264 | ] 265 | }, 266 | { 267 | "event": "請教問題", 268 | "description": [ 269 | "疑難雜症,均無解答" 270 | ] 271 | }, 272 | { 273 | "event": "網購", 274 | "description": [ 275 | "錯過促銷" 276 | ] 277 | }, 278 | { 279 | "event": "放假", 280 | "description": [ 281 | "隔日工作量倍增" 282 | ] 283 | }, 284 | { 285 | "event": "晚睡", 286 | "description": [ 287 | "失眠,明日精神渙散" 288 | ] 289 | }, 290 | { 291 | "event": "晚起", 292 | "description": [ 293 | "整天都不順" 294 | ] 295 | }, 296 | { 297 | "event": "發文章", 298 | "description": [ 299 | "搜索枯腸,不知所云" 300 | ] 301 | }, 302 | { 303 | "event": "點外賣", 304 | "description": [ 305 | "路況壅塞,餐點冷掉" 306 | ] 307 | }, 308 | { 309 | "event": "喝咖啡", 310 | "description": [ 311 | "晚上失眠" 312 | ] 313 | }, 314 | { 315 | "event": "散步", 316 | "description": [ 317 | "被害蟲咬傷" 318 | ] 319 | }, 320 | { 321 | "event": "吃冰", 322 | "description": [ 323 | "受寒感冒", 324 | "咳嗽不止" 325 | ] 326 | }, 327 | { 328 | "event": "爬山", 329 | "description": [ 330 | "遇到地震...", 331 | "不幸受傷" 332 | ] 333 | }, 334 | { 335 | "event": "觀星", 336 | "description": [ 337 | "光害嚴重", 338 | "烏雲密布" 339 | ] 340 | }, 341 | { 342 | "event": "野餐", 343 | "description": [ 344 | "被害蟲咬傷", 345 | "天氣不晴朗" 346 | ] 347 | }, 348 | { 349 | "event": "看電影", 350 | "description": [ 351 | "場場爆滿", 352 | "被旁人打擾", 353 | "劇情大失所望" 354 | ] 355 | }, 356 | { 357 | "event": "烹飪", 358 | "description": [ 359 | "缺少食材,口味不佳", 360 | "小心燙傷" 361 | ] 362 | } 363 | ] 364 | } 365 | -------------------------------------------------------------------------------- /fortune_generator/json/static_special.json: -------------------------------------------------------------------------------- 1 | { 2 | "special_events": [ 3 | { 4 | "event": "元旦", 5 | "triggerDate": { 6 | "month": "1", 7 | "date": "1" 8 | }, 9 | "status_index": "0", 10 | "goodFortunes": { 11 | "l_1_event": "早起", 12 | "l_1_desc": "心情愉悅迎接新年", 13 | "l_2_event": "大掃除", 14 | "l_2_desc": "新年新氣象" 15 | }, 16 | "badFortunes": { 17 | "r_1_event": "", 18 | "r_1_desc": "", 19 | "r_2_event": "", 20 | "r_2_desc": "" 21 | } 22 | }, 23 | { 24 | "event": "世界邏輯日", 25 | "triggerDate": { 26 | "month": "1", 27 | "date": "14" 28 | }, 29 | "status_index": "0", 30 | "goodFortunes": { 31 | "l_1_event": "思維訓練", 32 | "l_1_desc": "提高自身邏輯能力", 33 | "l_2_event": "", 34 | "l_2_desc": "" 35 | }, 36 | "badFortunes": { 37 | "r_1_event": "陷入死胡同", 38 | "r_1_desc": "記得適當休息", 39 | "r_2_event": "", 40 | "r_2_desc": "" 41 | } 42 | }, 43 | { 44 | "event": "國際擁抱日", 45 | "triggerDate": { 46 | "month": "1", 47 | "date": "21" 48 | }, 49 | "status_index": "0", 50 | "goodFortunes": { 51 | "l_1_event": "擁抱親朋好友", 52 | "l_1_desc": "讓愛流動,增進情感連結", 53 | "l_2_event": "送上擁抱", 54 | "l_2_desc": "透過擁抱表達支持與愛意" 55 | }, 56 | "badFortunes": { 57 | "r_1_event": "", 58 | "r_1_desc": "", 59 | "r_2_event": "", 60 | "r_2_desc": "" 61 | } 62 | }, 63 | { 64 | "event": "國際資料隱私日", 65 | "triggerDate": { 66 | "month": "1", 67 | "date": "28" 68 | }, 69 | "status_index": "0", 70 | "goodFortunes": { 71 | "l_1_event": "整理資料", 72 | "l_1_desc": "注意在線資料安全", 73 | "l_2_event": "注意隱私", 74 | "l_2_desc": "謹慎上網" 75 | }, 76 | "badFortunes": { 77 | "r_1_event": "", 78 | "r_1_desc": "", 79 | "r_2_event": "", 80 | "r_2_desc": "" 81 | } 82 | }, 83 | { 84 | "event": "世界濕地日", 85 | "triggerDate": { 86 | "month": "2", 87 | "date": "2" 88 | }, 89 | "status_index": "0", 90 | "goodFortunes": { 91 | "l_1_event": "參與保護濕地活動", 92 | "l_1_desc": "重視濕地,參與保護", 93 | "l_2_event": "", 94 | "l_2_desc": "" 95 | }, 96 | "badFortunes": { 97 | "r_1_event": "", 98 | "r_1_desc": "", 99 | "r_2_event": "", 100 | "r_2_desc": "" 101 | } 102 | }, 103 | { 104 | "event": "世界癌症日", 105 | "triggerDate": { 106 | "month": "2", 107 | "date": "4" 108 | }, 109 | "status_index": "0", 110 | "goodFortunes": { 111 | "l_1_event": "認識癌症", 112 | "l_1_desc": "知道癌症並不可怕", 113 | "l_2_event": "宣導健康生活", 114 | "l_2_desc": "健康飲食運動,預防癌症" 115 | }, 116 | "badFortunes": { 117 | "r_1_event": "", 118 | "r_1_desc": "", 119 | "r_2_event": "", 120 | "r_2_desc": "" 121 | } 122 | }, 123 | { 124 | "event": "世界和平日", 125 | "triggerDate": { 126 | "month": "2", 127 | "date": "5" 128 | }, 129 | "status_index": "0", 130 | "goodFortunes": { 131 | "l_1_event": "參加和平遊行", 132 | "l_1_desc": "支持和平,傳遞非暴力", 133 | "l_2_event": "", 134 | "l_2_desc": "" 135 | }, 136 | "badFortunes": { 137 | "r_1_event": "", 138 | "r_1_desc": "", 139 | "r_2_event": "", 140 | "r_2_desc": "" 141 | } 142 | }, 143 | { 144 | "event": "國際比薩日", 145 | "triggerDate": { 146 | "month": "2", 147 | "date": "9" 148 | }, 149 | "status_index": "0", 150 | "goodFortunes": { 151 | "l_1_event": "品嚐各式比薩", 152 | "l_1_desc": "共享比薩,樂享時光", 153 | "l_2_event": "", 154 | "l_2_desc": "" 155 | }, 156 | "badFortunes": { 157 | "r_1_event": "", 158 | "r_1_desc": "", 159 | "r_2_event": "", 160 | "r_2_desc": "" 161 | } 162 | }, 163 | { 164 | "event": "國際氣象節", 165 | "triggerDate": { 166 | "month": "2", 167 | "date": "10" 168 | }, 169 | "status_index": "0", 170 | "goodFortunes": { 171 | "l_1_event": "了解氣候變化", 172 | "l_1_desc": "認識氣候變遷,保護地球家園", 173 | "l_2_event": "", 174 | "l_2_desc": "" 175 | }, 176 | "badFortunes": { 177 | "r_1_event": "", 178 | "r_1_desc": "", 179 | "r_2_event": "", 180 | "r_2_desc": "" 181 | } 182 | }, 183 | { 184 | "event": "世界社會正義日", 185 | "triggerDate": { 186 | "month": "2", 187 | "date": "20" 188 | }, 189 | "status_index": "0", 190 | "goodFortunes": { 191 | "l_1_event": "提升社會意識", 192 | "l_1_desc": "關注不平等,參與正義行動", 193 | "l_2_event": "支持弱勢群體", 194 | "l_2_desc": "投身於公益事業,支持弱勢族群" 195 | }, 196 | "badFortunes": { 197 | "r_1_event": "", 198 | "r_1_desc": "", 199 | "r_2_event": "", 200 | "r_2_desc": "" 201 | } 202 | }, 203 | { 204 | "event": "國際母語日", 205 | "triggerDate": { 206 | "month": "2", 207 | "date": "21" 208 | }, 209 | "status_index": "0", 210 | "goodFortunes": { 211 | "l_1_event": "傳播母語", 212 | "l_1_desc": "延續母語的使用,保護文化傳承", 213 | "l_2_event": "", 214 | "l_2_desc": "" 215 | }, 216 | "badFortunes": { 217 | "r_1_event": "", 218 | "r_1_desc": "", 219 | "r_2_event": "", 220 | "r_2_desc": "" 221 | } 222 | }, 223 | { 224 | "event": "和平紀念日", 225 | "triggerDate": { 226 | "month": "2", 227 | "date": "28" 228 | }, 229 | "status_index": "0", 230 | "goodFortunes": { 231 | "l_1_event": "參與和平紀念活動", 232 | "l_1_desc": "緬懷歷史,尊重和平的價值", 233 | "l_2_event": "", 234 | "l_2_desc": "" 235 | }, 236 | "badFortunes": { 237 | "r_1_event": "", 238 | "r_1_desc": "", 239 | "r_2_event": "", 240 | "r_2_desc": "" 241 | } 242 | }, 243 | { 244 | "event": "植樹節", 245 | "triggerDate": { 246 | "month": "3", 247 | "date": "12" 248 | }, 249 | "status_index": "0", 250 | "goodFortunes": { 251 | "l_1_event": "植樹造林", 252 | "l_1_desc": "保護生態、美化環境", 253 | "l_2_event": "節能減碳", 254 | "l_2_desc": "延長資源壽命" 255 | }, 256 | "badFortunes": { 257 | "r_1_event": "", 258 | "r_1_desc": "", 259 | "r_2_event": "", 260 | "r_2_desc": "" 261 | } 262 | }, 263 | { 264 | "event": "白色情人節", 265 | "triggerDate": { 266 | "month": "3", 267 | "date": "14" 268 | }, 269 | "status_index": "0", 270 | "goodFortunes": { 271 | "l_1_event": "送禮物", 272 | "l_1_desc": "表達愛意和感激之情", 273 | "l_2_event": "觀星", 274 | "l_2_desc": "仰望星空,共描明月" 275 | }, 276 | "badFortunes": { 277 | "r_1_event": "", 278 | "r_1_desc": "", 279 | "r_2_event": "", 280 | "r_2_desc": "" 281 | } 282 | }, 283 | { 284 | "event": "國際數學日", 285 | "triggerDate": { 286 | "month": "3", 287 | "date": "14" 288 | }, 289 | "status_index": "0", 290 | "goodFortunes": { 291 | "l_1_event": "", 292 | "l_1_desc": "", 293 | "l_2_event": "", 294 | "l_2_desc": "" 295 | }, 296 | "badFortunes": { 297 | "r_1_event": "", 298 | "r_1_desc": "", 299 | "r_2_event": "", 300 | "r_2_desc": "" 301 | } 302 | }, 303 | { 304 | "event": "世界森林日", 305 | "triggerDate": { 306 | "month": "3", 307 | "date": "21" 308 | }, 309 | "status_index": "0", 310 | "goodFortunes": { 311 | "l_1_event": "環境教育", 312 | "l_1_desc": "提升對自然的敬重", 313 | "l_2_event": "節約用水", 314 | "l_2_desc": "保護生態系統穩定" 315 | }, 316 | "badFortunes": { 317 | "r_1_event": "", 318 | "r_1_desc": "", 319 | "r_2_event": "", 320 | "r_2_desc": "" 321 | } 322 | }, 323 | { 324 | "event": "愚人節", 325 | "triggerDate": { 326 | "month": "4", 327 | "date": "1" 328 | }, 329 | "status_index": "3", 330 | "goodFortunes": { 331 | "l_1_event": "喜笑顏開", 332 | "l_1_desc": "與親朋好友分享快樂", 333 | "l_2_event": "開派對", 334 | "l_2_desc": "組織有趣的活動和遊戲" 335 | }, 336 | "badFortunes": { 337 | "r_1_event": "冒犯他人", 338 | "r_1_desc": "避免製造觸怒人的笑話", 339 | "r_2_event": "惡作劇", 340 | "r_2_desc": "注意避免不必要的麻煩" 341 | } 342 | }, 343 | { 344 | "event": "兒童節", 345 | "triggerDate": { 346 | "month": "4", 347 | "date": "4" 348 | }, 349 | "status_index": "0", 350 | "goodFortunes": { 351 | "l_1_event": "喜笑顏開", 352 | "l_1_desc": "與親朋好友分享快樂", 353 | "l_2_event": "開派對", 354 | "l_2_desc": "組織有趣的活動和遊戲" 355 | }, 356 | "badFortunes": { 357 | "r_1_event": "", 358 | "r_1_desc": "", 359 | "r_2_event": "", 360 | "r_2_desc": "" 361 | } 362 | }, 363 | { 364 | "event": "世界健康日", 365 | "triggerDate": { 366 | "month": "4", 367 | "date": "7" 368 | }, 369 | "status_index": "0", 370 | "goodFortunes": { 371 | "l_1_event": "健康飲食", 372 | "l_1_desc": "多攝取水果、蔬菜和全穀食品", 373 | "l_2_event": "運動鍛煉", 374 | "l_2_desc": "保持身體健康和活力" 375 | }, 376 | "badFortunes": { 377 | "r_1_event": "", 378 | "r_1_desc": "", 379 | "r_2_event": "", 380 | "r_2_desc": "" 381 | } 382 | }, 383 | { 384 | "event": "世界地球日", 385 | "triggerDate": { 386 | "month": "4", 387 | "date": "22" 388 | }, 389 | "status_index": "0", 390 | "goodFortunes": { 391 | "l_1_event": "環保行動", 392 | "l_1_desc": "參與植樹造林或垃圾回收等環保行動", 393 | "l_2_event": "節能減排", 394 | "l_2_desc": "選擇環保型交通工具" 395 | }, 396 | "badFortunes": { 397 | "r_1_event": "", 398 | "r_1_desc": "", 399 | "r_2_event": "", 400 | "r_2_desc": "" 401 | } 402 | }, 403 | { 404 | "event": "世界閱讀日", 405 | "triggerDate": { 406 | "month": "4", 407 | "date": "23" 408 | }, 409 | "status_index": "0", 410 | "goodFortunes": { 411 | "l_1_event": "推廣閱讀", 412 | "l_1_desc": "激發對知識的渴望", 413 | "l_2_event": "書籍分享", 414 | "l_2_desc": "與他人分享你的書單" 415 | }, 416 | "badFortunes": { 417 | "r_1_event": "", 418 | "r_1_desc": "", 419 | "r_2_event": "", 420 | "r_2_desc": "" 421 | } 422 | }, 423 | { 424 | "event": "世界智慧財產權日", 425 | "triggerDate": { 426 | "month": "4", 427 | "date": "26" 428 | }, 429 | "status_index": "0", 430 | "goodFortunes": { 431 | "l_1_event": "保護創意", 432 | "l_1_desc": "尊重他人的創意和智慧財產權,共同維護創作人的權益", 433 | "l_2_event": "", 434 | "l_2_desc": "" 435 | }, 436 | "badFortunes": { 437 | "r_1_event": "", 438 | "r_1_desc": "", 439 | "r_2_event": "", 440 | "r_2_desc": "" 441 | } 442 | }, 443 | { 444 | "event": "星際大戰日", 445 | "triggerDate": { 446 | "month": "5", 447 | "date": "04" 448 | }, 449 | "status_index": "0", 450 | "goodFortunes": { 451 | "l_1_event": "電影馬拉松", 452 | "l_1_desc": "播放所有星際大戰電影", 453 | "l_2_event": "感受原力", 454 | "l_2_desc": "May the force be with you, always." 455 | }, 456 | "badFortunes": { 457 | "r_1_event": "", 458 | "r_1_desc": "", 459 | "r_2_event": "", 460 | "r_2_desc": "" 461 | } 462 | }, 463 | { 464 | "event": "世界微笑日", 465 | "triggerDate": { 466 | "month": "5", 467 | "date": "08" 468 | }, 469 | "status_index": "0", 470 | "goodFortunes": { 471 | "l_1_event": "微笑", 472 | "l_1_desc": "用微笑向世界問好", 473 | "l_2_event": "放慢腳步", 474 | "l_2_desc": "觀察四周的美好事物" 475 | }, 476 | "badFortunes": { 477 | "r_1_event": "", 478 | "r_1_desc": "", 479 | "r_2_event": "", 480 | "r_2_desc": "" 481 | } 482 | }, 483 | { 484 | "event": "世界環境日", 485 | "triggerDate": { 486 | "month": "6", 487 | "date": "05" 488 | }, 489 | "status_index": "0", 490 | "goodFortunes": { 491 | "l_1_event": "少用塑膠", 492 | "l_1_desc": "選擇可重複使用的替代品", 493 | "l_2_event": "", 494 | "l_2_desc": "" 495 | }, 496 | "badFortunes": { 497 | "r_1_event": "", 498 | "r_1_desc": "", 499 | "r_2_event": "", 500 | "r_2_desc": "" 501 | } 502 | }, 503 | { 504 | "event": "世界獻血者日", 505 | "triggerDate": { 506 | "month": "6", 507 | "date": "14" 508 | }, 509 | "status_index": "0", 510 | "goodFortunes": { 511 | "l_1_event": "捐血", 512 | "l_1_desc": "捐出血液和血漿,分享生命要時常", 513 | "l_2_event": "", 514 | "l_2_desc": "" 515 | }, 516 | "badFortunes": { 517 | "r_1_event": "", 518 | "r_1_desc": "", 519 | "r_2_event": "", 520 | "r_2_desc": "" 521 | } 522 | }, 523 | { 524 | "event": "巧克力日", 525 | "triggerDate": { 526 | "month": "7", 527 | "date": "07" 528 | }, 529 | "status_index": "0", 530 | "goodFortunes": { 531 | "l_1_event": "送巧克力", 532 | "l_1_desc": "共享巧克力盛宴", 533 | "l_2_event": "", 534 | "l_2_desc": "" 535 | }, 536 | "badFortunes": { 537 | "r_1_event": "", 538 | "r_1_desc": "", 539 | "r_2_event": "", 540 | "r_2_desc": "" 541 | } 542 | }, 543 | { 544 | "event": "宅宅日", 545 | "triggerDate": { 546 | "month": "7", 547 | "date": "13" 548 | }, 549 | "status_index": "0", 550 | "goodFortunes": { 551 | "l_1_event": "觀影", 552 | "l_1_desc": "看心愛的電影或影集", 553 | "l_2_event": "閱讀", 554 | "l_2_desc": "享受片刻的寧靜" 555 | }, 556 | "badFortunes": { 557 | "r_1_event": "", 558 | "r_1_desc": "", 559 | "r_2_event": "", 560 | "r_2_desc": "" 561 | } 562 | }, 563 | { 564 | "event": "國際冷笑話日", 565 | "triggerDate": { 566 | "month": "7", 567 | "date": "24" 568 | }, 569 | "status_index": "0", 570 | "goodFortunes": { 571 | "l_1_event": "講冷笑話", 572 | "l_1_desc": "一起嘻嘻哈哈", 573 | "l_2_event": "", 574 | "l_2_desc": "" 575 | }, 576 | "badFortunes": { 577 | "r_1_event": "", 578 | "r_1_desc": "", 579 | "r_2_event": "", 580 | "r_2_desc": "" 581 | } 582 | }, 583 | { 584 | "event": "國際友誼日", 585 | "triggerDate": { 586 | "month": "7", 587 | "date": "30" 588 | }, 589 | "status_index": "0", 590 | "goodFortunes": { 591 | "l_1_event": "與朋友聯絡", 592 | "l_1_desc": "回憶美好時光", 593 | "l_2_event": "一起出遊", 594 | "l_2_desc": "增進彼此的感情" 595 | }, 596 | "badFortunes": { 597 | "r_1_event": "", 598 | "r_1_desc": "", 599 | "r_2_event": "", 600 | "r_2_desc": "" 601 | } 602 | }, 603 | { 604 | "event": "國際左撇子日", 605 | "triggerDate": { 606 | "month": "8", 607 | "date": "13" 608 | }, 609 | "status_index": "0", 610 | "goodFortunes": { 611 | "l_1_event": "挑戰新事物", 612 | "l_1_desc": "嘗試用左手完成任務", 613 | "l_2_event": "", 614 | "l_2_desc": "" 615 | }, 616 | "badFortunes": { 617 | "r_1_event": "", 618 | "r_1_desc": "", 619 | "r_2_event": "", 620 | "r_2_desc": "" 621 | } 622 | }, 623 | { 624 | "event": "世界攝影日", 625 | "triggerDate": { 626 | "month": "8", 627 | "date": "19" 628 | }, 629 | "status_index": "0", 630 | "goodFortunes": { 631 | "l_1_event": "拍攝照片", 632 | "l_1_desc": "捕捉生活中的美好瞬間", 633 | "l_2_event": "分享作品", 634 | "l_2_desc": "展示您的攝影技巧" 635 | }, 636 | "badFortunes": { 637 | "r_1_event": "", 638 | "r_1_desc": "", 639 | "r_2_event": "", 640 | "r_2_desc": "" 641 | } 642 | }, 643 | { 644 | "event": "國際狗狗日", 645 | "triggerDate": { 646 | "month": "8", 647 | "date": "26" 648 | }, 649 | "status_index": "0", 650 | "goodFortunes": { 651 | "l_1_event": "陪伴狗狗", 652 | "l_1_desc": "帶狗狗散步或遊玩", 653 | "l_2_event": "分享作品", 654 | "l_2_desc": "展示您的攝影技巧" 655 | }, 656 | "badFortunes": { 657 | "r_1_event": "", 658 | "r_1_desc": "", 659 | "r_2_event": "", 660 | "r_2_desc": "" 661 | } 662 | }, 663 | { 664 | "event": "國際慈善日", 665 | "triggerDate": { 666 | "month": "9", 667 | "date": "5" 668 | }, 669 | "status_index": "0", 670 | "goodFortunes": { 671 | "l_1_event": "捐贈物資", 672 | "l_1_desc": "捐贈物資或金錢,幫助有需要的人", 673 | "l_2_event": "參與志願活動", 674 | "l_2_desc": "參加社區慈善活動,提升社會貢獻" 675 | }, 676 | "badFortunes": { 677 | "r_1_event": "", 678 | "r_1_desc": "", 679 | "r_2_event": "", 680 | "r_2_desc": "" 681 | } 682 | }, 683 | { 684 | "event": "國際和平日", 685 | "triggerDate": { 686 | "month": "9", 687 | "date": "21" 688 | }, 689 | "status_index": "0", 690 | "goodFortunes": { 691 | "l_1_event": "分享愛心", 692 | "l_1_desc": "與他人分享關懷與愛心,促進和平", 693 | "l_2_event": "", 694 | "l_2_desc": "" 695 | }, 696 | "badFortunes": { 697 | "r_1_event": "", 698 | "r_1_desc": "", 699 | "r_2_event": "", 700 | "r_2_desc": "" 701 | } 702 | }, 703 | { 704 | "event": "教師節", 705 | "triggerDate": { 706 | "month": "9", 707 | "date": "28" 708 | }, 709 | "status_index": "0", 710 | "goodFortunes": { 711 | "l_1_event": "感謝老師", 712 | "l_1_desc": "向老師表達感謝,增進師生情誼", 713 | "l_2_event": "", 714 | "l_2_desc": "" 715 | }, 716 | "badFortunes": { 717 | "r_1_event": "", 718 | "r_1_desc": "", 719 | "r_2_event": "", 720 | "r_2_desc": "" 721 | } 722 | }, 723 | { 724 | "event": "世界糧食日", 725 | "triggerDate": { 726 | "month": "10", 727 | "date": "16" 728 | }, 729 | "status_index": "0", 730 | "goodFortunes": { 731 | "l_1_event": "節約糧食", 732 | "l_1_desc": "支持可持續的食物系統", 733 | "l_2_event": "捐贈食品", 734 | "l_2_desc": "捐贈食物給有需要的人,傳遞愛心" 735 | }, 736 | "badFortunes": { 737 | "r_1_event": "", 738 | "r_1_desc": "", 739 | "r_2_event": "", 740 | "r_2_desc": "" 741 | } 742 | }, 743 | { 744 | "event": "聯合國日", 745 | "triggerDate": { 746 | "month": "10", 747 | "date": "24" 748 | }, 749 | "status_index": "0", 750 | "goodFortunes": { 751 | "l_1_event": "支持和平", 752 | "l_1_desc": "參與促進世界和平的活動", 753 | "l_2_event": "了解國際事務", 754 | "l_2_desc": "增強全球視野" 755 | }, 756 | "badFortunes": { 757 | "r_1_event": "", 758 | "r_1_desc": "", 759 | "r_2_event": "", 760 | "r_2_desc": "" 761 | } 762 | }, 763 | { 764 | "event": "萬聖節", 765 | "triggerDate": { 766 | "month": "10", 767 | "date": "31" 768 | }, 769 | "status_index": "4", 770 | "goodFortunes": { 771 | "l_1_event": "扮演角色", 772 | "l_1_desc": "穿上喜愛的角色服裝,享受萬聖節的氛圍", 773 | "l_2_event": "", 774 | "l_2_desc": "" 775 | }, 776 | "badFortunes": { 777 | "r_1_event": "", 778 | "r_1_desc": "", 779 | "r_2_event": "忽略安全", 780 | "r_2_desc": "活動時忽視安全措施可能帶來風險" 781 | } 782 | }, 783 | { 784 | "event": "世界善心日", 785 | "triggerDate": { 786 | "month": "11", 787 | "date": "13" 788 | }, 789 | "status_index": "0", 790 | "goodFortunes": { 791 | "l_1_event": "善待他人", 792 | "l_1_desc": "在生活中多一些善意與寬容", 793 | "l_2_event": "", 794 | "l_2_desc": "" 795 | }, 796 | "badFortunes": { 797 | "r_1_event": "", 798 | "r_1_desc": "", 799 | "r_2_event": "", 800 | "r_2_desc": "" 801 | } 802 | }, 803 | { 804 | "event": "棉花糖日", 805 | "triggerDate": { 806 | "month": "12", 807 | "date": "07" 808 | }, 809 | "status_index": "0", 810 | "goodFortunes": { 811 | "l_1_event": "吃棉花糖", 812 | "l_1_desc": "慶祝棉花糖日", 813 | "l_2_event": "", 814 | "l_2_desc": "" 815 | }, 816 | "badFortunes": { 817 | "r_1_event": "", 818 | "r_1_desc": "", 819 | "r_2_event": "", 820 | "r_2_desc": "" 821 | } 822 | }, 823 | { 824 | "event": "貓奴日", 825 | "triggerDate": { 826 | "month": "12", 827 | "date": "15" 828 | }, 829 | "status_index": "0", 830 | "goodFortunes": { 831 | "l_1_event": "嚕貓", 832 | "l_1_desc": "撫平傷心的心情", 833 | "l_2_event": "喝咖啡", 834 | "l_2_desc": "到貓咪咖啡店去喝咖啡" 835 | }, 836 | "badFortunes": { 837 | "r_1_event": "", 838 | "r_1_desc": "", 839 | "r_2_event": "", 840 | "r_2_desc": "" 841 | } 842 | }, 843 | { 844 | "event": "平安夜", 845 | "triggerDate": { 846 | "month": "12", 847 | "date": "24" 848 | }, 849 | "status_index": "0", 850 | "goodFortunes": { 851 | "l_1_event": "除舊佈新", 852 | "l_1_desc": "平安祥和", 853 | "l_2_event": "交換禮物", 854 | "l_2_desc": "獲得真心的祝福" 855 | }, 856 | "badFortunes": { 857 | "r_1_event": "", 858 | "r_1_desc": "", 859 | "r_2_event": "", 860 | "r_2_desc": "" 861 | } 862 | }, 863 | { 864 | "event": "聖誕節", 865 | "triggerDate": { 866 | "month": "12", 867 | "date": "25" 868 | }, 869 | "status_index": "0", 870 | "goodFortunes": { 871 | "l_1_event": "家庭聚會", 872 | "l_1_desc": "一起團圓吃火雞大餐", 873 | "l_2_event": "注意保暖", 874 | "l_2_desc": "冬至到了" 875 | }, 876 | "badFortunes": { 877 | "r_1_event": "", 878 | "r_1_desc": "", 879 | "r_2_event": "", 880 | "r_2_desc": "" 881 | } 882 | } 883 | ] 884 | } 885 | -------------------------------------------------------------------------------- /fortune_generator/json/themes.json: -------------------------------------------------------------------------------- 1 | { 2 | "themes": [ 3 | { 4 | "name": "Classic Light", 5 | "properties": { 6 | "bg-color": "#ffffff", 7 | "good-fortune-color": "#e74c3c", 8 | "bad-fortune-color": "#000000bf", 9 | "middle-fortune-color": "#5eb95e", 10 | "title-color": "#000000cc", 11 | "desc-color": "#7f7f7f", 12 | "button-color": "#73a3eb", 13 | "button-hover-color": "#459aef", 14 | "toggle-theme-button-color": "#000000", 15 | "copy-result-button-color": "#000000", 16 | "copy-preview-result-url-button-color": "#000000", 17 | "date-color": "#096e1bc9", 18 | "special-event-color": "#3e4fbb" 19 | } 20 | }, 21 | { 22 | "name": "Classic Dark", 23 | "properties": { 24 | "bg-color": "#1e1d24", 25 | "good-fortune-color": "#e74c3c", 26 | "bad-fortune-color": "#d4d4d4d9", 27 | "middle-fortune-color": "#57c857", 28 | "title-color": "#cdcdcd", 29 | "desc-color": "#838282", 30 | "button-color": "#5d99f4", 31 | "button-hover-color": "#9ac6f1", 32 | "toggle-theme-button-color": "#ffffff", 33 | "copy-result-button-color": "#ffffff", 34 | "copy-preview-result-url-button-color": "#ffffff", 35 | "date-color": "#0ed64aed", 36 | "special-event-color": "#6477f3" 37 | } 38 | }, 39 | { 40 | "name": "Catppuccin Dark", 41 | "properties": { 42 | "bg-color": "#1e1e2e", 43 | "good-fortune-color": "#94e2d5", 44 | "bad-fortune-color": "#f38ba8", 45 | "middle-fortune-color": "#f9e2af", 46 | "title-color": "#f5c2e7", 47 | "desc-color": "#cdd6f4", 48 | "button-color": "#b9fbc0", 49 | "button-hover-color": "#a6e3a1", 50 | "toggle-theme-button-color": "#f9e2af", 51 | "copy-result-button-color": "#f38ba8", 52 | "copy-preview-result-url-button-color": "#f38ba8", 53 | "date-color": "#f5c2e7", 54 | "special-event-color": "#fab387" 55 | } 56 | }, 57 | { 58 | "name": "Tokyo Night", 59 | "properties": { 60 | "bg-color": "#1a1b26", 61 | "good-fortune-color": "#7dcfff", 62 | "bad-fortune-color": "#ff5c8d", 63 | "middle-fortune-color": "#e0af68", 64 | "title-color": "#bb9af7", 65 | "desc-color": "#c0caf5", 66 | "button-color": "#6e6f8c", 67 | "button-hover-color": "#5a5b7f", 68 | "toggle-theme-button-color": "#7dcfff", 69 | "copy-result-button-color": "#ff5c8d", 70 | "copy-preview-result-url-button-color": "#ff5c8d", 71 | "date-color": "#ffbb93", 72 | "special-event-color": "#f7768e" 73 | } 74 | }, 75 | { 76 | "name": "Spring Blossom", 77 | "properties": { 78 | "bg-color": "#f7f5f2", 79 | "good-fortune-color": "#ff6f61", 80 | "bad-fortune-color": "#6d597a", 81 | "middle-fortune-color": "#86a77a", 82 | "title-color": "#ff9f80", 83 | "desc-color": "#5a5a5a", 84 | "button-color": "#ffd166", 85 | "button-hover-color": "#ffb347", 86 | "toggle-theme-button-color": "#ff6f61", 87 | "copy-result-button-color": "#6d597a", 88 | "copy-preview-result-url-button-color": "#6d597a", 89 | "date-color": "#ff9f80", 90 | "special-event-color": "#ffb6b9" 91 | } 92 | }, 93 | { 94 | "name": "Sunny Vibes", 95 | "properties": { 96 | "bg-color": "#fffbeb", 97 | "good-fortune-color": "#ff7e67", 98 | "bad-fortune-color": "#ffcc29", 99 | "middle-fortune-color": "#1fab89", 100 | "title-color": "#ff8c42", 101 | "desc-color": "#4a4a4a", 102 | "button-color": "#ffa41b", 103 | "button-hover-color": "#ff8500", 104 | "toggle-theme-button-color": "#34ace0", 105 | "copy-result-button-color": "#ffcc29", 106 | "copy-preview-result-url-button-color": "#ffcc29", 107 | "date-color": "#ffd32a", 108 | "special-event-color": "#f7b731" 109 | } 110 | }, 111 | { 112 | "name": "Autumn Glow", 113 | "properties": { 114 | "bg-color": "#f4ede4", 115 | "good-fortune-color": "#e27d60", 116 | "bad-fortune-color": "#a23b2b", 117 | "middle-fortune-color": "#c7a17a", 118 | "title-color": "#5a3d31", 119 | "desc-color": "#7a6e61", 120 | "button-color": "#c7a17a", 121 | "button-hover-color": "#b9896e", 122 | "toggle-theme-button-color": "#a74c3c", 123 | "copy-result-button-color": "#7a6e61", 124 | "copy-preview-result-url-button-color": "#7a6e61", 125 | "date-color": "#e6b89c", 126 | "special-event-color": "#e8a87c" 127 | } 128 | }, 129 | { 130 | "name": "Winter Wonderland", 131 | "properties": { 132 | "bg-color": "#e3f2fd", 133 | "good-fortune-color": "#74b9ff", 134 | "bad-fortune-color": "#6c5ce7", 135 | "middle-fortune-color": "#81ecec", 136 | "title-color": "#0984e3", 137 | "desc-color": "#636e72", 138 | "button-color": "#74b9ff", 139 | "button-hover-color": "#a29bfe", 140 | "toggle-theme-button-color": "#00b894", 141 | "copy-result-button-color": "#0984e3", 142 | "copy-preview-result-url-button-color": "#0984e3", 143 | "date-color": "#74b9ff", 144 | "special-event-color": "#fdcb6e" 145 | } 146 | }, 147 | { 148 | "name": "Moonlit Night", 149 | "properties": { 150 | "bg-color": "#1a1a2e", 151 | "good-fortune-color": "#7ed6df", 152 | "bad-fortune-color": "#ffeaa7", 153 | "middle-fortune-color": "#8c94a6", 154 | "title-color": "#ffffff", 155 | "desc-color": "#a4b0be", 156 | "button-color": "#30336b", 157 | "button-hover-color": "#535c88", 158 | "toggle-theme-button-color": "#7ed6df", 159 | "copy-result-button-color": "#ffeaa7", 160 | "copy-preview-result-url-button-color": "#ffeaa7", 161 | "date-color": "#dcdde1", 162 | "special-event-color": "#ff9ff3" 163 | } 164 | }, 165 | { 166 | "name": "Lunar Eclipse", 167 | "properties": { 168 | "bg-color": "#0d0e1a", 169 | "good-fortune-color": "#7289da", 170 | "bad-fortune-color": "#b56576", 171 | "middle-fortune-color": "#a0a8c1", 172 | "title-color": "#e1e1e6", 173 | "desc-color": "#8b8b97", 174 | "button-color": "#494e6b", 175 | "button-hover-color": "#646b8a", 176 | "toggle-theme-button-color": "#7289da", 177 | "copy-result-button-color": "#b56576", 178 | "copy-preview-result-url-button-color": "#b56576", 179 | "date-color": "#d4d4dc", 180 | "special-event-color": "#ffb86c" 181 | } 182 | }, 183 | { 184 | "name": "Galactic Glow", 185 | "properties": { 186 | "bg-color": "#1b1d2a", 187 | "good-fortune-color": "#00eaff", 188 | "bad-fortune-color": "#ff5555", 189 | "middle-fortune-color": "#ffe347", 190 | "title-color": "#ffe81f", 191 | "desc-color": "#c4c7d1", 192 | "button-color": "#3b3f58", 193 | "button-hover-color": "#52577a", 194 | "toggle-theme-button-color": "#00eaff", 195 | "copy-result-button-color": "#ff5555", 196 | "copy-preview-result-url-button-color": "#ff5555", 197 | "date-color": "#9aedfe", 198 | "special-event-color": "#ffa07a" 199 | } 200 | }, 201 | { 202 | "name": "Mystic Forest", 203 | "properties": { 204 | "bg-color": "#1c3b24", 205 | "good-fortune-color": "#a1e887", 206 | "bad-fortune-color": "#d94e3b", 207 | "middle-fortune-color": "#83c5a3", 208 | "title-color": "#e4f9e0", 209 | "desc-color": "#b5c9b4", 210 | "button-color": "#4a7a58", 211 | "button-hover-color": "#6a9a76", 212 | "toggle-theme-button-color": "#a1e887", 213 | "copy-result-button-color": "#d94e3b", 214 | "copy-preview-result-url-button-color": "#d94e3b", 215 | "date-color": "#e4f9e0", 216 | "special-event-color": "#9fd9b7" 217 | } 218 | }, 219 | { 220 | "name": "Vintage Sepia", 221 | "properties": { 222 | "bg-color": "#f5e9da", 223 | "good-fortune-color": "#d4a373", 224 | "bad-fortune-color": "#8b5e3c", 225 | "middle-fortune-color": "#c3a593", 226 | "title-color": "#3f312b", 227 | "desc-color": "#736357", 228 | "button-color": "#a67a5b", 229 | "button-hover-color": "#b98b6f", 230 | "toggle-theme-button-color": "#8b5e3c", 231 | "copy-result-button-color": "#d4a373", 232 | "copy-preview-result-url-button-color": "#d4a373", 233 | "date-color": "#7f6a5d", 234 | "special-event-color": "#c7ab93" 235 | } 236 | }, 237 | { 238 | "name": "Metallic Shine", 239 | "properties": { 240 | "bg-color": "#2c2f33", 241 | "good-fortune-color": "#4e8c47", 242 | "bad-fortune-color": "#d9534f", 243 | "middle-fortune-color": "#f1c40f", 244 | "title-color": "#bdc3c7", 245 | "desc-color": "#95a5a6", 246 | "button-color": "#3498db", 247 | "button-hover-color": "#2980b9", 248 | "toggle-theme-button-color": "#4e8c47", 249 | "copy-result-button-color": "#d9534f", 250 | "copy-preview-result-url-button-color": "#d9534f", 251 | "date-color": "#bdc3c7", 252 | "special-event-color": "#f39c12" 253 | } 254 | }, 255 | { 256 | "name": "Tropical Paradise", 257 | "properties": { 258 | "bg-color": "#ffdf80", 259 | "good-fortune-color": "#00bfae", 260 | "bad-fortune-color": "#ff6347", 261 | "middle-fortune-color": "#3eb489", 262 | "title-color": "#2e8b57", 263 | "desc-color": "#708090", 264 | "button-color": "#ff4500", 265 | "button-hover-color": "#ff6347", 266 | "toggle-theme-button-color": "#00bfae", 267 | "copy-result-button-color": "#ff6347", 268 | "copy-preview-result-url-button-color": "#ff6347", 269 | "date-color": "#2e8b57", 270 | "special-event-color": "#ff8c00" 271 | } 272 | }, 273 | { 274 | "name": "Abstract Art", 275 | "properties": { 276 | "bg-color": "#f4e1d2", 277 | "good-fortune-color": "#e1b1e3", 278 | "bad-fortune-color": "#c93f36", 279 | "middle-fortune-color": "#bde7e0", 280 | "title-color": "#cf63a1", 281 | "desc-color": "#7b6362", 282 | "button-color": "#fc7b6d", 283 | "button-hover-color": "#fc4f48", 284 | "toggle-theme-button-color": "#e1b1e3", 285 | "copy-result-button-color": "#c93f36", 286 | "copy-preview-result-url-button-color": "#c93f36", 287 | "date-color": "#cf63a1", 288 | "special-event-color": "#f6c6d4" 289 | } 290 | }, 291 | { 292 | "name": "Zen Garden", 293 | "properties": { 294 | "bg-color": "#f4f1e1", 295 | "good-fortune-color": "#8c9f6f", 296 | "bad-fortune-color": "#e18e8b", 297 | "middle-fortune-color": "#b7c7b5", 298 | "title-color": "#4f5049", 299 | "desc-color": "#78756f", 300 | "button-color": "#c1c0b2", 301 | "button-hover-color": "#b0b098", 302 | "toggle-theme-button-color": "#8c9f6f", 303 | "copy-result-button-color": "#e18e8b", 304 | "copy-preview-result-url-button-color": "#e18e8b", 305 | "date-color": "#4f5049", 306 | "special-event-color": "#b7c7b5" 307 | } 308 | }, 309 | { 310 | "name": "Aurora Borealis", 311 | "properties": { 312 | "bg-color": "#0b0c1d", 313 | "good-fortune-color": "#00d084", 314 | "bad-fortune-color": "#455a64", 315 | "middle-fortune-color": "#9c7ae0", 316 | "title-color": "#d9e9f0", 317 | "desc-color": "#95a5b3", 318 | "button-color": "#608fcf", 319 | "button-hover-color": "#5072b3", 320 | "toggle-theme-button-color": "#00d084", 321 | "copy-result-button-color": "#455a64", 322 | "copy-preview-result-url-button-color": "#455a64", 323 | "date-color": "#cfd8dc", 324 | "special-event-color": "#a1d6ff" 325 | } 326 | }, 327 | { 328 | "name": "Cyberwave", 329 | "properties": { 330 | "bg-color": "#1a1a2e", 331 | "good-fortune-color": "#00f5d4", 332 | "bad-fortune-color": "#293462", 333 | "middle-fortune-color": "#adff2f", 334 | "title-color": "#f8f8ff", 335 | "desc-color": "#adb5bd", 336 | "button-color": "#0077b6", 337 | "button-hover-color": "#005f87", 338 | "toggle-theme-button-color": "#00f5d4", 339 | "copy-result-button-color": "#293462", 340 | "copy-preview-result-url-button-color": "#293462", 341 | "date-color": "#72efdd", 342 | "special-event-color": "#72ddf7" 343 | } 344 | } 345 | ] 346 | } 347 | -------------------------------------------------------------------------------- /fortune_generator/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Fortune Generator", 3 | "name": "Fortune Generator", 4 | "description": "Get your daily fortune with just a click.", 5 | "background_color": "#1a1b1e", 6 | "theme_color": "#1a1b1e", 7 | "icons": [ 8 | { 9 | "src": "../images/lifeadventurer-192x192.png", 10 | "sizes": "192x192", 11 | "type": "image/png" 12 | }, 13 | { 14 | "src": "../images/lifeadventurer-512x512.png", 15 | "sizes": "512x512", 16 | "type": "image/png" 17 | }, 18 | { 19 | "src": "../images/lifeadventurer-180x180.png", 20 | "sizes": "180x180", 21 | "type": "image/png" 22 | }, 23 | { 24 | "src": "../images/lifeadventurer-270x270.png", 25 | "sizes": "270x270", 26 | "type": "image/png" 27 | } 28 | ], 29 | "start_url": "/generators/fortune_generator/index.html", 30 | "display": "standalone", 31 | "orientation": "portrait" 32 | } 33 | -------------------------------------------------------------------------------- /images/fortune_generator_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LifeAdventurer/generators/79965ea3a8425e38e8ec0e88fa5d2798f471540c/images/fortune_generator_example.png -------------------------------------------------------------------------------- /images/lifeadventurer-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LifeAdventurer/generators/79965ea3a8425e38e8ec0e88fa5d2798f471540c/images/lifeadventurer-180x180.png -------------------------------------------------------------------------------- /images/lifeadventurer-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LifeAdventurer/generators/79965ea3a8425e38e8ec0e88fa5d2798f471540c/images/lifeadventurer-192x192.png -------------------------------------------------------------------------------- /images/lifeadventurer-270x270.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LifeAdventurer/generators/79965ea3a8425e38e8ec0e88fa5d2798f471540c/images/lifeadventurer-270x270.png -------------------------------------------------------------------------------- /images/lifeadventurer-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LifeAdventurer/generators/79965ea3a8425e38e8ec0e88fa5d2798f471540c/images/lifeadventurer-512x512.png -------------------------------------------------------------------------------- /images/lifeadventurer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LifeAdventurer/generators/79965ea3a8425e38e8ec0e88fa5d2798f471540c/images/lifeadventurer.jpg -------------------------------------------------------------------------------- /images/lifeadventurer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LifeAdventurer/generators/79965ea3a8425e38e8ec0e88fa5d2798f471540c/images/lifeadventurer.png -------------------------------------------------------------------------------- /images/lifeadventurer_rounded_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LifeAdventurer/generators/79965ea3a8425e38e8ec0e88fa5d2798f471540c/images/lifeadventurer_rounded_logo.png -------------------------------------------------------------------------------- /images/quote_generator_example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LifeAdventurer/generators/79965ea3a8425e38e8ec0e88fa5d2798f471540c/images/quote_generator_example.png -------------------------------------------------------------------------------- /images/quote_generator_example_(2).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LifeAdventurer/generators/79965ea3a8425e38e8ec0e88fa5d2798f471540c/images/quote_generator_example_(2).png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Generators 7 | 8 | 9 | 15 | 16 | 19 | 20 | 24 | 25 | 26 | 27 |
28 |
29 |

30 | Generators Gallery 31 |

32 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | fortune generator example 49 |
50 |

Fortune Generator

51 |

52 | Get your daily fortune with just a click. 53 |

54 | Check this out 55 |
56 | 59 |
60 |
61 |
62 |
63 | quote generator example 68 |
69 |

Quote Generator

70 |

71 | Generate inspiring and thought-provoking quotes effortlessly. 72 |

73 | Check this out 74 |
75 | 78 |
79 |
80 |
81 |
82 |
83 |
84 | 96 |
97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /quote_generator/backgrounds/dark/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LifeAdventurer/generators/79965ea3a8425e38e8ec0e88fa5d2798f471540c/quote_generator/backgrounds/dark/.gitkeep -------------------------------------------------------------------------------- /quote_generator/backgrounds/light/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LifeAdventurer/generators/79965ea3a8425e38e8ec0e88fa5d2798f471540c/quote_generator/backgrounds/light/.gitkeep -------------------------------------------------------------------------------- /quote_generator/css/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --button-color: #9dc4ff; 3 | --button-hover-color: #5ca8f3; 4 | --bg-color: #ffffffd7; 5 | --text-color: #000000; 6 | } 7 | 8 | .dark-mode { 9 | --button-color: #66a1fa; 10 | --button-hover-color: #8ec1f4; 11 | --bg-color: #1b1919d7; 12 | --dark-mode-icon-color: #ffffff; 13 | --text-color: #ffffff; 14 | } 15 | 16 | html { 17 | background: #282828; 18 | height: 100%; 19 | text-align: center; 20 | overflow: hidden; 21 | font-family: Georgia, "Times New Roman", Times, serif; 22 | font-size: 24px; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | padding: 0; 28 | height: 100%; 29 | display: flex; 30 | flex: 1; 31 | align-items: center; 32 | justify-content: center; 33 | } 34 | 35 | #Matrix { 36 | z-index: 0; 37 | } 38 | 39 | .container { 40 | position: absolute; 41 | z-index: 1; 42 | top: 50%; 43 | left: 50%; 44 | transform: translate(-50%, -50%); 45 | width: 80%; 46 | max-width: 800px; 47 | margin: 0 auto; 48 | text-align: center; 49 | padding: 70px; 50 | color: var(--text-color); 51 | background-color: var(--bg-color); 52 | border-radius: 30px; 53 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4); 54 | } 55 | 56 | .quote-container { 57 | margin: 10px; 58 | } 59 | 60 | button { 61 | background-color: var(--button-color); 62 | color: var(--bg-color); 63 | font-size: 25px; 64 | border: none; 65 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4); 66 | padding: 17px 20px; 67 | border-radius: 30px; 68 | cursor: pointer; 69 | transition: all 0.3s ease-in-out; 70 | } 71 | 72 | button:hover { 73 | background-color: var(--button-hover-color); 74 | } 75 | 76 | #dark-mode-icon { 77 | margin-top: 15px; 78 | font-size: 2.4rem; 79 | color: var(--dark-mode-icon-color); 80 | cursor: pointer; 81 | opacity: 85%; 82 | } 83 | 84 | .home-button { 85 | position: fixed; 86 | top: 8px; 87 | left: 8px; 88 | z-index: 1000; 89 | opacity: 0.8; /* Slightly transparent */ 90 | transition: opacity 0.3s; 91 | } 92 | -------------------------------------------------------------------------------- /quote_generator/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Quote Generator 7 | 8 | 9 | 15 | 16 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 |

Today's quote

31 |
32 |

33 |

34 |
35 |
36 | 37 |
38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /quote_generator/js/matrix.js: -------------------------------------------------------------------------------- 1 | const canvas = document.getElementById("Matrix"); 2 | const context = canvas.getContext("2d"); 3 | 4 | canvas.height = globalThis.innerHeight + 100; 5 | canvas.width = globalThis.innerWidth + 5; 6 | 7 | const chars = 8 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./*-+#$%^@!~?><:;[]{}=_αβΓγΔδεζηΘθικΛλμΞξΠπρΣσςτυΦφχΨψΩω×≦≧≠∞≒≡~∩∠∪∟⊿∫∮∵∴$¥〒¢£℃€℉╩◢ⅩⅨⅧⅦⅥⅤⅣⅢⅡⅠあいうえおがぎぐげござじずぜぞだぢつでづどにぬのばひぴぶへぺぼみゃょァゐゎè"; 9 | 10 | const fontSize = 16; 11 | const columns = canvas.width / fontSize; 12 | 13 | const charArr = []; 14 | for (let i = 0; i < columns; i++) { 15 | charArr[i] = 1; 16 | } 17 | 18 | let frame = 0; 19 | let str; 20 | 21 | context.fillStyle = "rgba(0, 0, 0, 1)"; 22 | context.fillRect(0, 0, canvas.width, canvas.height); 23 | 24 | function Update() { 25 | context.fillStyle = "rgba(0, 0, 0, 0.05)"; 26 | context.fillRect(0, 0, canvas.width, canvas.height); 27 | 28 | if (frame == 0) { 29 | let a = parseInt(Math.random() * 255); 30 | str = `rgba(${a}, ${Math.abs(a - 127)}, ${Math.abs(a - 255)}, 0.9)`; 31 | } 32 | context.fillStyle = str; 33 | context.font = fontSize + "px monospace"; 34 | 35 | for (let i = 0; i < columns; i++) { 36 | const text = chars[Math.floor(Math.random() * chars.length)]; 37 | context.fillText(text, i * fontSize, charArr[i] * fontSize); 38 | if (charArr[i] * fontSize > canvas.height && Math.random() > 0.90) { 39 | charArr[i] = 0; 40 | } 41 | charArr[i]++; 42 | } 43 | frame++; 44 | 45 | if (frame <= 40 * (Math.floor(Math.random() * 10) + 3)) { 46 | requestAnimationFrame(Update); // 40 frames a cycle 47 | } else { 48 | frame = 0; 49 | Appear(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /quote_generator/js/quote.js: -------------------------------------------------------------------------------- 1 | const quoteElement = document.getElementById("quote"); 2 | const authorElement = document.getElementById("author"); 3 | const buttonElement = document.querySelector("button"); 4 | 5 | let quotes = []; 6 | 7 | fetch("./json/quotes.json") 8 | .then((response) => response.json()) 9 | .then((data) => { 10 | quotes = data.quotes; 11 | }); 12 | 13 | function Appear() { 14 | const index = Math.floor(Math.random() * quotes.length); 15 | const { quote, author } = quotes[index]; 16 | 17 | quoteElement.innerHTML = `"${quote}"`; 18 | authorElement.innerHTML = "- " + author; 19 | 20 | const container = document.getElementById("imageContainer"); 21 | const folderPath = "./backgrounds/"; 22 | // TODO: Get number of images from a JSON file. 23 | const numDarkImages = 0; 24 | const numLightImages = 0; 25 | 26 | if (numDarkImages && numLightImages) { 27 | const isDark = Math.random() < 0.5; 28 | let randomIndex, randomImage; 29 | const darkModeIcon = document.querySelector("#dark-mode-icon"); 30 | console.log(isDark); 31 | if (isDark) { 32 | randomIndex = Math.floor(Math.random() * numDarkImages) + 1; 33 | randomImage = folderPath + "dark/" + randomIndex + ".jpg"; 34 | darkModeIcon.onclick(); 35 | } else { 36 | randomIndex = Math.floor(Math.random() * numLightImages) + 1; 37 | randomImage = folderPath + "light/" + randomIndex + ".jpg"; 38 | } 39 | container.style.backgroundImage = "url('" + randomImage + "')"; 40 | container.style.opacity = 0.85; 41 | container.style.backgroundSize = "100% 100%"; 42 | } 43 | } 44 | 45 | function getQuote() { 46 | Update(); 47 | } 48 | -------------------------------------------------------------------------------- /quote_generator/js/scripts.js: -------------------------------------------------------------------------------- 1 | const darkModeIcon = document.querySelector("#dark-mode-icon"); 2 | 3 | darkModeIcon.onclick = () => { 4 | darkModeIcon.classList.toggle("bx-sun"); 5 | document.body.classList.toggle("dark-mode"); 6 | }; 7 | -------------------------------------------------------------------------------- /quote_generator/json/quotes.json: -------------------------------------------------------------------------------- 1 | { 2 | "quotes": [ 3 | { 4 | "quote": "To AC is human. To AK divine.", 5 | "author": "Moon", 6 | "id": 1 7 | }, 8 | { 9 | "quote": "Life is like riding a bicycle. To keep your balance, you must keep moving.", 10 | "author": "Albert Einstein", 11 | "id": 2 12 | }, 13 | { 14 | "quote": "A dream is what makes people love life even when it is painful.", 15 | "author": "Theodore Zeldin", 16 | "id": 3 17 | }, 18 | { 19 | "quote": "Don’t quit. Suffer now and live the rest of your life as a champion.", 20 | "author": "Muhammad Ali", 21 | "id": 4 22 | }, 23 | { 24 | "quote": "Though nobody can go back and make a new beginning… Anyone can start over and make a new ending.", 25 | "author": "Chico Xavier", 26 | "id": 5 27 | }, 28 | { 29 | "quote": "Be happy for this moment. This moment is your life.", 30 | "author": "Omar Khayyam", 31 | "id": 6 32 | }, 33 | { 34 | "quote": "Life is not a problem to be solved, but a reality to be experienced.", 35 | "author": "Soren Kierkegaard", 36 | "id": 7 37 | }, 38 | { 39 | "quote": "All the waters of the world find one another again, and very road leads us wanderers too back home.", 40 | "author": "Hermann Hesse", 41 | "id": 8 42 | }, 43 | { 44 | "quote": "Time does not pass, it continues.", 45 | "author": "Marty Rubin", 46 | "id": 9 47 | }, 48 | { 49 | "quote": "Be both soft and wild. Just like the Moon. Or the storm. Or the sea.", 50 | "author": "Victoria Erickson", 51 | "id": 10 52 | }, 53 | { 54 | "quote": "Pain is a part of growing up. It is how we learn...", 55 | "author": "Dan Brown", 56 | "id": 11 57 | }, 58 | { 59 | "quote": "Beneath the winter, you can feel the bone structure of the landscape, and the whole story doesn't show.", 60 | "author": "Andrew Wyeth", 61 | "id": 12 62 | }, 63 | { 64 | "quote": "I had been educated in the rhythms of the mountain, in which change was never fundamental, only cyclical.", 65 | "author": "Tara Westover", 66 | "id": 13 67 | }, 68 | { 69 | "quote": "When you look at the stars, if feels like you are not from any particular piece of land, but from the solar system.", 70 | "author": "Kalpana Chawla", 71 | "id": 14 72 | }, 73 | { 74 | "quote": "Not all those who wander are lost.", 75 | "author": "J. R. R. Tolkien", 76 | "id": 15 77 | }, 78 | { 79 | "quote": "What then is time? If no one asks me, I know what it is. If I wish to explain it to him who asks, I do not know.", 80 | "author": "Saint Augustine", 81 | "id": 16 82 | }, 83 | { 84 | "quote": "Change your opinions, keep to your principles; change your leaves, keep intact your roots.", 85 | "author": "Victor Hugo", 86 | "id": 17 87 | }, 88 | { 89 | "quote": "Patience is not simply the ability to wait, it's how we behave while we're waiting.", 90 | "author": "Joyce Meyer", 91 | "id": 18 92 | }, 93 | { 94 | "quote": "A rolling stone gathers no moss, but it gains a certain polish.", 95 | "author": "Oliver Herford", 96 | "id": 19 97 | }, 98 | { 99 | "quote": "I decided to fly through the air, live in the sunlight and enjoy life as much as I could.", 100 | "author": "Evel Knievel", 101 | "id": 20 102 | }, 103 | { 104 | "quote": "Be a life long or short, its completeness depends on what it was lived for.", 105 | "author": "David Starr Jordan", 106 | "id": 21 107 | }, 108 | { 109 | "quote": "There are two ways to live: you can live as if nothing is a miracle; you can live as if everything is a miracle.", 110 | "author": "Albert Einstein", 111 | "id": 22 112 | }, 113 | { 114 | "quote": "All human wisdom is summed up in two words; wait and hope.", 115 | "author": "Alexandre Dumas", 116 | "id": 23 117 | }, 118 | { 119 | "quote": "There is no happiness like this happiness: quiet mornings, light from the river, the weekend ahead.", 120 | "author": "James Salter", 121 | "id": 24 122 | }, 123 | { 124 | "quote": "A lead falls; something is flying by; Let whatever your eyes gaze upon be created, and the soul of the hearer remain shivering.", 125 | "author": "Vicente Huidobro", 126 | "id": 25 127 | }, 128 | { 129 | "quote": "Still round the corner there may wait, a new road or a secret gate.", 130 | "author": "J. R. R. Tolkien", 131 | "id": 26 132 | }, 133 | { 134 | "quote": "One of the advantages of being disorganized is that one is always having surprising discoveries.", 135 | "author": "A. A. Milne", 136 | "id": 27 137 | }, 138 | { 139 | "quote": "Talk is cheap. Show me the code.", 140 | "author": "Linus Torvalds", 141 | "id": 28 142 | }, 143 | { 144 | "quote": "Every day is a journey, and the journey itself is home.", 145 | "author": "Matsuo Basho", 146 | "id": 29 147 | }, 148 | { 149 | "quote": "Life is an ongoing process of choosing between safety and risk. Make the growth choice a dozen times a day.", 150 | "author": "Abraham Maslow", 151 | "id": 30 152 | }, 153 | { 154 | "quote": "A leaf fluttered in through the window, as if supported by the rays of the sun.", 155 | "author": "Anais Nin", 156 | "id": 31 157 | }, 158 | { 159 | "quote": "All that we see or seem is but a dream within a dream.", 160 | "author": "Edgar Allan Poe", 161 | "id": 32 162 | }, 163 | { 164 | "quote": "Let every dawn be to you as the beginning of life, and every setting sun be to you as its close.", 165 | "author": "John Ruskin", 166 | "id": 33 167 | }, 168 | { 169 | "quote": "The softer snow falls, the longer it dwells upon, and the deeper it sinks into the mind.", 170 | "author": "Samuel Taylor Coleridge", 171 | "id": 34 172 | }, 173 | { 174 | "quote": "Happiness does not lie in happiness, but in the achievement of it.", 175 | "author": "Fyodor Dostoevsky", 176 | "id": 35 177 | }, 178 | { 179 | "quote": "You and I are all as much continuous with the physical universe as a wave is continuous with the ocean.", 180 | "author": "Alan Watts", 181 | "id": 36 182 | }, 183 | { 184 | "quote": "No self is an island, each exists in a fabric of relations that is more complex and mobile than ever before.", 185 | "author": "Jean-Francois Lyotard", 186 | "id": 37 187 | }, 188 | { 189 | "quote": "Sit in reverie and watch the changing color of the waves that break upon the idle seashore of the mind.", 190 | "author": "Henry Longfellow", 191 | "id": 38 192 | }, 193 | { 194 | "quote": "Water, stories, the body, all the things we do, are mediums that hid and show what's hidden.", 195 | "author": "Rumi", 196 | "id": 39 197 | }, 198 | { 199 | "quote": "Focus on what lights a fire inside of you and use that passion to fill a white space.", 200 | "author": "Kendra Scott", 201 | "id": 40 202 | }, 203 | { 204 | "quote": "Unlike a drop of water lost its identity when joins the ocean, man keeps his being in which he lives.", 205 | "author": "B. R. Ambedkar", 206 | "id": 41 207 | }, 208 | { 209 | "quote": "Snow isn't just pretty. It also cleanses our world, our senses, and a kind of weary familiarity.", 210 | "author": "John Burnside", 211 | "id": 42 212 | }, 213 | { 214 | "quote": "Loss is nothing else but change, and change is nature's delight.", 215 | "author": "Marcus Aurelius", 216 | "id": 43 217 | }, 218 | { 219 | "quote": "If there is magic on this planet, it is contained in water.", 220 | "author": "Loren Eiseley", 221 | "id": 44 222 | }, 223 | { 224 | "quote": "If the world's a veil of tears, smile till rainbows span it.", 225 | "author": "Lucy Larcom", 226 | "id": 45 227 | }, 228 | { 229 | "quote": "Any landscape is a condition of the spirit.", 230 | "author": "Henri Frederic Amiel", 231 | "id": 46 232 | }, 233 | { 234 | "quote": "Life is in a land full of thorns and weeds, there always a space in which the good seed can grow.", 235 | "author": "Jorge Mario Bergoglio", 236 | "id": 47 237 | }, 238 | { 239 | "quote": "The whole universe appears as an infinite storm of beauty.", 240 | "author": "John Muir", 241 | "id": 48 242 | }, 243 | { 244 | "quote": "You traverse the world in search of happiness, which is within the reach of every man.", 245 | "author": "Horace", 246 | "id": 49 247 | }, 248 | { 249 | "quote": "We sail within a vast sphere, ever drifting in uncertainty, driven from end to end.", 250 | "author": "Blaise Pascal", 251 | "id": 50 252 | }, 253 | { 254 | "quote": "Nothing is poetical if the plain daylight is not poetical.", 255 | "author": "Gilbert K. Chesterton", 256 | "id": 51 257 | }, 258 | { 259 | "quote": "Rest is not idleness. To lie sometimes on the grass under trees, and listen to the murmur of the water.", 260 | "author": "John Lubbock", 261 | "id": 52 262 | }, 263 | { 264 | "quote": "As the sun makes ice melt, kindness causes misunderstanding, mistrust, and hostility to evaporate.", 265 | "author": "Albert Schweitzer", 266 | "id": 53 267 | }, 268 | { 269 | "quote": "In my search for you, I've made my new home in the eyes of a bird, staring at the passing wind.", 270 | "author": "Kaili Blues", 271 | "id": 54 272 | }, 273 | { 274 | "quote": "Your spark isn't your purpose. The last box fills in when you're ready to come live.", 275 | "author": "Film, Soul", 276 | "id": 55 277 | }, 278 | { 279 | "quote": "There's a point at which we make our lives, but we also take the path which si given to us.", 280 | "author": "Ali Smith", 281 | "id": 56 282 | }, 283 | { 284 | "quote": "Yesterday is but today's memory, and tomorrow is today's dream.", 285 | "author": "Khalil Gibran", 286 | "id": 57 287 | }, 288 | { 289 | "quote": "For the wise man looks into space and he know there is no limited dimensions.", 290 | "author": "Zhuangzi", 291 | "id": 58 292 | }, 293 | { 294 | "quote": "My life is bathed in golden sunlight, and the really wonderful thing is that I know it.", 295 | "author": "Helen McCrory", 296 | "id": 59 297 | }, 298 | { 299 | "quote": "You block your dream when you allow your feat to grow bigger than you faith.", 300 | "author": "Mary Manin Morrissey", 301 | "id": 60 302 | }, 303 | { 304 | "quote": "The future influences the present just as much as the past.", 305 | "author": "Friedrich Nietzsche", 306 | "id": 61 307 | }, 308 | { 309 | "quote": "You could cover the whole earth with asphalt, but sooner or later green grass would break through.", 310 | "author": "Ilya Ehrenburg", 311 | "id": 62 312 | }, 313 | { 314 | "quote": "Empathy is about finding echoes of another person in yourself.", 315 | "author": "Mohsin Hamid", 316 | "id": 63 317 | }, 318 | { 319 | "quote": "Life is like this one big process of letting go.", 320 | "author": "Adrianne Lenker", 321 | "id": 64 322 | }, 323 | { 324 | "quote": "Great results cannot be achieved at once; and we should be satisfied to advance in life, step by step.", 325 | "author": "Samuel Smiles", 326 | "id": 65 327 | }, 328 | { 329 | "quote": "To live means to be aware, joyously, drunkenly, serenely, divinely aware.", 330 | "author": "Henry Miller", 331 | "id": 66 332 | }, 333 | { 334 | "quote": "The only limit to our realization of tomorrow is our doubts of today.", 335 | "author": "Franklin D. Roosevelt", 336 | "id": 67 337 | }, 338 | { 339 | "quote": "The choices you make from this day forward will lead you, step by step, to the future you deserve.", 340 | "author": "Chris Murray", 341 | "id": 68 342 | }, 343 | { 344 | "quote": "I am in the right place at the right time, and everything happens at the exactly right moment.", 345 | "author": "Charlie Chaplin", 346 | "id": 69 347 | }, 348 | { 349 | "quote": "I will not be \"famous,\" \"great.\" I will go on adventuring, changing, opening my mind and my eye.", 350 | "author": "Virginia Woolf", 351 | "id": 70 352 | }, 353 | { 354 | "quote": "When I am well-rested and focused on things that truly interest me, time often ceases to be an issue.", 355 | "author": "James Clear", 356 | "id": 71 357 | }, 358 | { 359 | "quote": "When you're no longer thinking ahead, each footstep isn't just a means to an end but an unique event in itself.", 360 | "author": "Robert M. Pirsig", 361 | "id": 72 362 | }, 363 | { 364 | "quote": "As long as the night lasts, I shall dance in the sky With all the dying fireworks of the light.", 365 | "author": "Carl Sandburg", 366 | "id": 73 367 | }, 368 | { 369 | "quote": "Your vision will become clear only when you can look into your own heart.", 370 | "author": "Carl Jung", 371 | "id": 74 372 | }, 373 | { 374 | "quote": "The goldenrod is yellow, the corn is turning brown, the trees in apple orchards with fruit are bending down.", 375 | "author": "Helen Hunt Jackson", 376 | "id": 75 377 | } 378 | ] 379 | } 380 | -------------------------------------------------------------------------------- /scripts.js: -------------------------------------------------------------------------------- 1 | // fetch all folder paths of the generators from `folders.json` 2 | let folderPaths = []; 3 | 4 | async function fetch_folders() { 5 | await fetch("./folders.json") 6 | .then((response) => response.json()) 7 | .then((data) => { 8 | folderPaths = data.folder_paths; 9 | }); 10 | } 11 | 12 | async function get_generator_card_footer() { 13 | await fetch_folders(); 14 | const repoOwner = "LifeAdventurer"; 15 | const repoName = "generators"; 16 | for (let folderIndex = 1; folderIndex <= folderPaths.length; folderIndex++) { 17 | const folderPath = folderPaths[folderIndex - 1]; 18 | const apiUrl = 19 | `https://api.github.com/repos/${repoOwner}/${repoName}/commits?path=${folderPath}`; 20 | 21 | fetch(apiUrl) 22 | .then((response) => response.json()) 23 | .then((data) => { 24 | // the latest commit will be at the top of the list 25 | const lastCommit = data[0].commit.author.date; 26 | const commitTimeStamp = new Date(lastCommit).getTime() / 1000; 27 | const currentTimeStamp = Math.floor(new Date().getTime() / 1000); 28 | const timeDifference = currentTimeStamp - commitTimeStamp; 29 | 30 | $(`#last-update-${folderIndex}`).html( 31 | `Last updated ${format_time_difference(timeDifference)} ago`, 32 | ); 33 | }); 34 | // .catch(error => console.error('Error fetching data:', error)); 35 | } 36 | } 37 | 38 | // determine whether it is seconds, minutes, hours, or days ago 39 | function format_time_difference(seconds) { 40 | const minutes = Math.floor(seconds / 60); 41 | const hours = Math.floor(minutes / 60); 42 | const days = Math.floor(hours / 24); 43 | 44 | if (days > 0) { 45 | return `${days} day${days > 1 ? "s" : ""}`; 46 | } else if (hours > 0) { 47 | return `${hours} hour${hours > 1 ? "s" : ""}`; 48 | } else if (minutes > 0) { 49 | return `${minutes} minute${minutes > 1 ? "s" : ""}`; 50 | } else { 51 | return `${seconds} second${seconds > 1 ? "s" : ""}`; 52 | } 53 | } 54 | 55 | get_generator_card_footer(); 56 | 57 | const darkModeIcon = document.querySelector("#dark-mode-icon"); 58 | 59 | darkModeIcon.onclick = () => { 60 | darkModeIcon.classList.toggle("bx-sun"); 61 | document.body.classList.toggle("dark-mode"); 62 | }; 63 | 64 | // temporary 65 | let max_height = -1; 66 | document.querySelectorAll('.card-body').forEach(el => max_height = Math.max(max_height, el.offsetHeight)); 67 | document.querySelectorAll('.card-body').forEach(el => el.style.height = `${max_height}px`); 68 | -------------------------------------------------------------------------------- /scripts/check-events.py: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | 3 | import logging 4 | import collections 5 | import datetime 6 | import argparse 7 | import enum 8 | import json 9 | 10 | class DateType(enum.Enum): 11 | CUSTOM = "custom" 12 | STATIC = "static" 13 | CYCLICAL = "cyclical" 14 | 15 | def __str__(self): 16 | return self.name.lower() 17 | 18 | def __repr__(self): 19 | return str(self) 20 | 21 | @staticmethod 22 | def argparse(s): 23 | try: 24 | return DateType[s.upper()] 25 | except KeyError: 26 | return s 27 | 28 | 29 | args_parser = argparse.ArgumentParser(description="special events checker") 30 | args_parser.add_argument("path", type=str, help="event json file path") 31 | args_parser.add_argument( 32 | "type", 33 | type=DateType.argparse, 34 | choices=[t for t in DateType], 35 | help="event date type", 36 | ) 37 | 38 | args = args_parser.parse_args() 39 | 40 | special_events: dict[str, list[dict]] = {} 41 | 42 | try: 43 | with open(args.path) as f: 44 | special_events = json.loads(f.read()) 45 | except json.JSONDecodeError: 46 | print(f"`{args.path}` json syntax error.") 47 | exit(-1) 48 | 49 | except FileNotFoundError: 50 | print(f"`{args.path}` not found.") 51 | print("Please contact developer to solve this problem.") 52 | exit(-1) 53 | 54 | if not isinstance(special_events, dict): 55 | print("`special_events` should be a dict") 56 | exit(-1) 57 | 58 | if "special_events" not in special_events: 59 | print(f"`special_events` not found in `{args.path}`.") 60 | exit(-1) 61 | 62 | if not isinstance(special_events["special_events"], list): 63 | print(f"`special_events` in `{args.path}` should be a list.") 64 | exit(-1) 65 | 66 | MIN_STATUS_INDEX = 0 67 | MAX_STATUS_INDEX = 7 68 | DAYSPERMONTH = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] 69 | 70 | errors: dict[int, list[str]] = collections.defaultdict(list) 71 | 72 | 73 | def is_leap_year(year: int) -> bool: 74 | """Determines whether a given year is a leap year. 75 | 76 | Args: 77 | year (int): The year to check. 78 | 79 | Returns: 80 | bool: True if the year is a leap year, False otherwise. 81 | """ 82 | 83 | if year % 400 == 0: 84 | return True 85 | if year % 100 == 0: 86 | return False 87 | if year % 4 == 0: 88 | return True 89 | 90 | return False 91 | 92 | 93 | def validate_number(event_idx: int, value, min: int, max: int, field_name: str) -> int | None: 94 | """Validates whether a given value is an integer within a specified range. 95 | 96 | Args: 97 | event_idx (int): The index of the event for associating validation errors. 98 | value (Any): The value to validate. 99 | min (int): The minimum acceptable value (inclusive). 100 | max (int): The maximum acceptable value (inclusive). 101 | field_name (str): The name of the field being validated, used in error messages. 102 | 103 | Returns: 104 | int | None: The validated integer value if it is within the range, otherwise None. 105 | 106 | Raises: 107 | ValueError: If `value` cannot be converted to an integer. 108 | 109 | Validation Rules: 110 | - If `value` cannot be converted to an integer, an error is recorded and None is returned. 111 | - If `value` is outside the range defined by `min` and `max`, an error is recorded and None is returned. 112 | """ 113 | 114 | try: 115 | value = int(value) 116 | except ValueError: 117 | errors[event_idx].append(f"`{field_name}` should be between {min} and {max}") 118 | return None 119 | 120 | if value < min or value > max: 121 | errors[event_idx].append(f"`{field_name}` should be between {min} and {max}") 122 | return None 123 | 124 | return value 125 | 126 | 127 | def require_field_check( 128 | obj: dict, event_idx: int, fields: list[tuple[str, type]], required_field: str = "" 129 | ) -> bool: 130 | """ 131 | Validates the presence and types of required fields in a given object. 132 | 133 | Args: 134 | obj (dict): The object (dictionary) to validate. 135 | event_idx (int): The index of the event for associating validation errors. 136 | fields (list[tuple[str, type]]): A list of tuples where each tuple contains a field name and its expected type. 137 | required_field (str, optional): An optional prefix for error messages to indicate a higher-level required field. Defaults to "". 138 | 139 | Returns: 140 | bool: True if all required fields are present and have the correct types, otherwise False. 141 | 142 | Validation Rules: 143 | - If a required field is missing, an error message is recorded. 144 | - If a field is present but its type does not match the expected type, an error message is recorded. 145 | - The `required_field` parameter, if provided, is prepended to error messages for context. 146 | """ 147 | 148 | error_found = False 149 | for field_name, field_type in fields: 150 | if field_name not in obj: 151 | error_found = True 152 | msg = "" 153 | if required_field != "": 154 | msg = f"`{required_field}` " 155 | 156 | msg += f"missing `{field_name}`." 157 | errors[event_idx].append(msg) 158 | 159 | elif not isinstance(obj[field_name], field_type): 160 | error_found = True 161 | errors[event_idx].append(f"`{field_name}` should be a `{field_type}` type.") 162 | 163 | if error_found: 164 | return False 165 | return True 166 | 167 | 168 | event_names = set() 169 | event_dates = set() 170 | 171 | 172 | def check_structure(event, idx: int): 173 | if not isinstance(event, dict): 174 | errors[idx].append("should be a dict") 175 | return False 176 | 177 | if not require_field_check( 178 | event, 179 | idx, 180 | [ 181 | ("event", str), 182 | ("triggerDate", dict), 183 | ("status_index", str), 184 | ("goodFortunes", dict), 185 | ("badFortunes", dict), 186 | ], 187 | ): 188 | return False 189 | 190 | event_name: str = event["event"] 191 | if event_name.strip() == "": 192 | errors[idx].append("event name should not empty.") 193 | return 194 | 195 | if event_name in event_names: 196 | errors[idx].append(f"event `{event_name}` already exists.") 197 | 198 | validate_number( 199 | idx, event["status_index"], MIN_STATUS_INDEX, MAX_STATUS_INDEX, "status_index" 200 | ) 201 | 202 | if require_field_check( 203 | event["goodFortunes"], 204 | idx, 205 | [ 206 | ("l_1_event", str), 207 | ("l_1_desc", str), 208 | ("l_2_event", str), 209 | ("l_2_desc", str), 210 | ], 211 | "goodFortunes" 212 | ): 213 | if bool(event["goodFortunes"]["l_1_event"]) ^ bool(event["goodFortunes"]["l_1_desc"]): 214 | # Check for inconsistency: XOR is used to ensure both l_1_event and l_1_desc 215 | # are either both provided or both missing. If only one is provided, log an error. 216 | errors[idx].append("First good fortune is incomplete.") 217 | 218 | if bool(event["goodFortunes"]["l_2_event"]) ^ bool(event["goodFortunes"]["l_2_desc"]): 219 | # Check for inconsistency: XOR is used to ensure both l_2_event and l_2_desc 220 | # are either both provided or both missing. If only one is provided, log an error. 221 | errors[idx].append("Second good fortune is incomplete.") 222 | 223 | if require_field_check( 224 | event["badFortunes"], 225 | idx, 226 | [ 227 | ("r_1_event", str), 228 | ("r_1_desc", str), 229 | ("r_2_event", str), 230 | ("r_2_desc", str), 231 | ], 232 | "badFortunes" 233 | ): 234 | if bool(event["badFortunes"]["r_1_event"]) ^ bool(event["badFortunes"]["r_1_desc"]): 235 | # Check for inconsistency: XOR is used to ensure both r_1_event and r_1_desc 236 | # are either both provided or both missing. If only one is provided, log an error. 237 | errors[idx].append("First bad fortune is incomplete.") 238 | 239 | if bool(event["badFortunes"]["r_2_event"]) ^ bool(event["badFortunes"]["r_2_desc"]): 240 | # Check for inconsistency: XOR is used to ensure both r_2_event and r_2_desc 241 | # are either both provided or both missing. If only one is provided, log an error. 242 | errors[idx].append("Second bad fortune is incomplete.") 243 | 244 | event_names.add(event_name) 245 | 246 | return True 247 | 248 | def check_static_date(event: dict, idx: int): 249 | trigger_date: dict = event["triggerDate"] 250 | corrected = require_field_check( 251 | trigger_date, 252 | idx, 253 | [ 254 | ("month", str), 255 | ("date", str), 256 | ], 257 | "triggerDate", 258 | ) 259 | 260 | event_name: str = event["event"] 261 | if "year" in trigger_date: 262 | errors[idx].append( 263 | f"this event `{event_name}` should be placed in `custom_special.json`." 264 | ) 265 | 266 | if "week" in trigger_date or "weekday" in trigger_date: 267 | errors[idx].append( 268 | f"this event `{event_name}` should be placed in `cyclical_special.json`." 269 | ) 270 | 271 | if not corrected: 272 | return 273 | 274 | month = validate_number(idx, trigger_date["month"], 1, 12, "triggerDate.month") 275 | if month is not None: 276 | validate_number( 277 | idx, trigger_date["date"], 1, DAYSPERMONTH[month], "triggerDate.date" 278 | ) 279 | 280 | key = f'{event_name}:{trigger_date["month"]}/{trigger_date["date"]}' 281 | if key in event_dates: 282 | errors[idx].append(f"The `{key}` is repeated.") 283 | 284 | event_dates.add(key) 285 | 286 | 287 | def check_cyclical_date(event: dict, idx: int): 288 | trigger_date: dict = event["triggerDate"] 289 | corrected = require_field_check( 290 | trigger_date, 291 | idx, 292 | [ 293 | ("month", str), 294 | ("week", str), 295 | ("weekday", str), 296 | ], 297 | "triggerDate", 298 | ) 299 | 300 | event_name: str = event["event"] 301 | if "year" in trigger_date: 302 | errors[idx].append( 303 | f"this event `{event_name}` should be placed in `custom_special.json`." 304 | ) 305 | 306 | elif "date" in trigger_date: 307 | errors[idx].append( 308 | f"this event `{event_name}` should be placed in `static_special.json`." 309 | ) 310 | 311 | if not corrected: 312 | return 313 | 314 | validate_number(idx, trigger_date["month"], 1, 12, "triggerDate.month") 315 | validate_number(idx, trigger_date["week"], 1, 5, "triggerDate.week") 316 | validate_number(idx, trigger_date["weekday"], 1, 7, "triggerDate.weekday") 317 | 318 | key = f'{event_name}:{trigger_date["month"]}/{trigger_date["week"]}/{trigger_date["weekday"]}' 319 | if key in event_dates: 320 | errors[idx].append(f"The `{key}` is repeated.") 321 | 322 | event_dates.add(key) 323 | 324 | 325 | def check_custom_date(event: dict, idx: int): 326 | trigger_date: dict = event["triggerDate"] 327 | corrected = require_field_check( 328 | trigger_date, 329 | idx, 330 | [ 331 | ("year", str), 332 | ("month", str), 333 | ("date", str), 334 | ], 335 | "triggerDate", 336 | ) 337 | 338 | event_name: str = event["event"] 339 | if "week" in trigger_date or "weekday" in trigger_date: 340 | errors[idx].append( 341 | f"this event `{event_name}` should be placed in `cyclical_special.json`.", 342 | ) 343 | 344 | elif "year" not in trigger_date: 345 | errors[idx].append( 346 | f"this event `{event_name}` should be placed in `static_special.json`." 347 | ) 348 | 349 | if not corrected: 350 | return 351 | 352 | year = validate_number( 353 | idx, 354 | trigger_date["year"], 355 | datetime.datetime.min.year, 356 | datetime.datetime.max.year, 357 | "triggerDate.year", 358 | ) 359 | month = validate_number(idx, trigger_date["month"], 1, 12, "triggerDate.month") 360 | 361 | if year is None or month is None: 362 | return 363 | 364 | days = DAYSPERMONTH[month] 365 | if month == 2 and is_leap_year(year): 366 | days += 1 # 29 367 | 368 | date = validate_number(idx, trigger_date["date"], 1, days, "triggerDate.date") 369 | if date is None: 370 | return 371 | 372 | key = f'{event_name}:{year}/{month}/{date}' 373 | if key in event_dates: 374 | errors[idx].append(f"The `{key}` is repeated.") 375 | 376 | event_dates.add(key) 377 | 378 | 379 | date_checker = { 380 | DateType.CUSTOM: check_custom_date, 381 | DateType.STATIC: check_static_date, 382 | DateType.CYCLICAL: check_cyclical_date, 383 | } 384 | check_triggerdate = date_checker[args.type] 385 | 386 | for idx, event in enumerate(special_events["special_events"]): 387 | if check_structure(event, idx): 388 | check_triggerdate(event, idx) 389 | 390 | if errors: 391 | logging.error(args.path) 392 | for idx, error_msgs in errors.items(): 393 | logging.error(json.dumps(special_events["special_events"][idx], indent=4, ensure_ascii=False)) 394 | for msg in error_msgs: 395 | logging.error(msg) 396 | exit(-1) 397 | -------------------------------------------------------------------------------- /scripts/check-fortune.py: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | 3 | import json 4 | import logging 5 | import argparse 6 | import collections 7 | 8 | args_parser = argparse.ArgumentParser(description="fortune checker") 9 | args_parser.add_argument("path", type=str, help="event json file path") 10 | 11 | args = args_parser.parse_args() 12 | errors: dict[tuple[str, int], list[str]] = collections.defaultdict(list) 13 | good_fortunes: list[dict] = None 14 | bad_fortunes: list[dict] = None 15 | all_fortunes = None 16 | 17 | try: 18 | with open(args.path) as f: 19 | all_fortunes = json.loads(f.read()) 20 | except json.JSONDecodeError: 21 | print(f"`{args.path}` json syntax error.") 22 | exit(-1) 23 | 24 | except FileNotFoundError: 25 | print(f"`{args.path}` not found.") 26 | print("Please contact developer to solve this problem.") 27 | exit(-1) 28 | 29 | if not isinstance(all_fortunes, dict): 30 | print(f"`{args.path}` should contain a dict") 31 | exit(-1) 32 | 33 | try: 34 | good_fortunes = all_fortunes["goodFortunes"] 35 | except KeyError: 36 | print(f"`{args.path}` should contain `goodFortunes`") 37 | 38 | if not isinstance(good_fortunes, list): 39 | print("`goodFortunes` should be a list.") 40 | 41 | try: 42 | bad_fortunes = all_fortunes["badFortunes"] 43 | except KeyError: 44 | print(f"`{args.path}` should contain `badFortunes`") 45 | 46 | if not isinstance(bad_fortunes, list): 47 | print("`badFortunes` should be a list.") 48 | 49 | 50 | def require_field_check( 51 | obj: dict, 52 | fortune_idx: tuple[str, int], 53 | fields: list[tuple[str, type]], 54 | required_field: str = "", 55 | ) -> bool: 56 | """ 57 | Validates the presence and types of required fields in a given object. 58 | 59 | Args: 60 | obj (dict): The object (dictionary) to validate. 61 | fortune_idx (tuple[str, int]): The index of the fortune for associating validation errors. 62 | fields (list[tuple[str, type]]): A list of tuples where each tuple contains a field name and its expected type. 63 | required_field (str, optional): An optional prefix for error messages to indicate a higher-level required field. Defaults to "". 64 | 65 | Returns: 66 | bool: True if all required fields are present and have the correct types, otherwise False. 67 | 68 | Validation Rules: 69 | - If a required field is missing, an error message is recorded. 70 | - If a field is present but its type does not match the expected type, an error message is recorded. 71 | - The `required_field` parameter, if provided, is prepended to error messages for context. 72 | """ 73 | 74 | error_found = False 75 | for field_name, field_type in fields: 76 | if field_name not in obj: 77 | error_found = True 78 | msg = "" 79 | if required_field != "": 80 | msg = f"`{required_field}` " 81 | 82 | msg += f"missing `{field_name}`." 83 | errors[fortune_idx].append(msg) 84 | 85 | elif not isinstance(obj[field_name], field_type): 86 | error_found = True 87 | errors[fortune_idx].append( 88 | f"`{field_name}` should be a `{field_type}` type." 89 | ) 90 | 91 | if error_found: 92 | return False 93 | return True 94 | 95 | 96 | fortune_names = set() 97 | 98 | def check_fortune(fortune, idx: tuple[str, int]): 99 | if not isinstance(fortune, dict): 100 | errors[idx].append("fortune should be a dict.") 101 | return False 102 | 103 | if not require_field_check(fortune, idx, [ 104 | ("event", str), 105 | ("description", list) 106 | ]): 107 | return False 108 | 109 | fortune_name = fortune["event"] 110 | if fortune_name in fortune_names: 111 | errors[idx].append(f"fortune `{fortune_name}` already exists.") 112 | 113 | if not fortune_name: 114 | errors[idx].append("fortune name should not be empty.") 115 | 116 | 117 | if not fortune["description"]: 118 | errors[idx].append("fortune description should not be empty.") 119 | return False 120 | 121 | descriptions = set() 122 | for desc in fortune["description"]: 123 | if not isinstance(desc, str): 124 | errors[idx].append(f"fortune description {desc} should be a string.") 125 | continue 126 | 127 | if not desc: 128 | errors[idx].append(f"fortune description {desc} should not be empty.") 129 | continue 130 | 131 | if desc in descriptions: 132 | errors[idx].append(f"fortune description {desc} already exists.") 133 | continue 134 | else: 135 | descriptions.add(desc) 136 | 137 | fortune_names.add(fortune_name) 138 | 139 | return True 140 | 141 | if good_fortunes: 142 | for idx, fortune in enumerate(good_fortunes): 143 | check_fortune(fortune, ("goodFortunes", idx)) 144 | 145 | fortune_names.clear() 146 | if bad_fortunes: 147 | for idx, fortune in enumerate(bad_fortunes): 148 | check_fortune(fortune, ("badFortunes", idx)) 149 | 150 | if errors: 151 | logging.error(args.path) 152 | for idx, error_msgs in errors.items(): 153 | fortunes = None 154 | if idx[0] == "goodFortunes": 155 | fortunes = good_fortunes 156 | elif idx[0] == "badFortunes": 157 | fortunes = bad_fortunes 158 | 159 | if not fortunes: 160 | continue 161 | 162 | logging.error( 163 | json.dumps( 164 | fortunes[idx[1]], indent=4, ensure_ascii=False 165 | ) 166 | ) 167 | for msg in error_msgs: 168 | logging.error(msg) 169 | exit(-1) 170 | -------------------------------------------------------------------------------- /scripts/check-theme.py: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | 3 | import json 4 | import logging 5 | import argparse 6 | import collections 7 | 8 | args_parser = argparse.ArgumentParser(description="theme checker") 9 | args_parser.add_argument("path", type=str, help="event json file path") 10 | 11 | args = args_parser.parse_args() 12 | errors: dict[int, list[str]] = collections.defaultdict(list) 13 | themes: list[dict[str]] = None 14 | j = None 15 | 16 | try: 17 | with open(args.path) as f: 18 | j = json.loads(f.read()) 19 | except json.JSONDecodeError: 20 | print(f"`{args.path}` json syntax error.") 21 | exit(-1) 22 | 23 | except FileNotFoundError: 24 | print(f"`{args.path}` not found.") 25 | print("Please contact developer to solve this problem.") 26 | exit(-1) 27 | 28 | if not isinstance(j, dict): 29 | print(f"`{args.path}` should contain a dict") 30 | exit(-1) 31 | 32 | try: 33 | themes = j["themes"] 34 | except KeyError: 35 | print(f"`{args.path}` should contain `themes`") 36 | exit(-1) 37 | 38 | if not isinstance(themes, list): 39 | print("`themes` should be a list.") 40 | exit(-1) 41 | 42 | def require_field_check( 43 | obj: dict, 44 | theme_idx: int, 45 | fields: list[tuple[str, type]], 46 | required_field: str = "", 47 | ) -> bool: 48 | """ 49 | Validates the presence and types of required fields in a given object. 50 | 51 | Args: 52 | obj (dict): The object (dictionary) to validate. 53 | theme_idx (int): The index of the fortune for associating validation errors. 54 | fields (list[tuple[str, type]]): A list of tuples where each tuple contains a field name and its expected type. 55 | required_field (str, optional): An optional prefix for error messages to indicate a higher-level required field. Defaults to "". 56 | 57 | Returns: 58 | bool: True if all required fields are present and have the correct types, otherwise False. 59 | 60 | Validation Rules: 61 | - If a required field is missing, an error message is recorded. 62 | - If a field is present but its type does not match the expected type, an error message is recorded. 63 | - The `required_field` parameter, if provided, is prepended to error messages for context. 64 | """ 65 | 66 | error_found = False 67 | for field_name, field_type in fields: 68 | if field_name not in obj: 69 | error_found = True 70 | msg = "" 71 | if required_field != "": 72 | msg = f"`{required_field}` " 73 | 74 | msg += f"missing `{field_name}`." 75 | errors[theme_idx].append(msg) 76 | 77 | elif not isinstance(obj[field_name], field_type): 78 | error_found = True 79 | errors[theme_idx].append( 80 | f"`{field_name}` should be a `{field_type}` type." 81 | ) 82 | 83 | if error_found: 84 | return False 85 | return True 86 | 87 | 88 | theme_names = set() 89 | 90 | def check_theme(theme, idx: int): 91 | if not isinstance(theme, dict): 92 | errors[idx].append("theme should be a dict.") 93 | return False 94 | 95 | if not require_field_check(theme, idx, [ 96 | ("name", str), 97 | ("properties", dict) 98 | ]): 99 | return False 100 | 101 | theme_name = theme["name"] 102 | if theme_name in theme_names: 103 | errors[idx].append(f"theme `{theme_name}` already exists.") 104 | 105 | if not theme_name: 106 | errors[idx].append("theme name should not be empty.") 107 | 108 | properties = theme["properties"] 109 | properties_field_required = [ 110 | ("bg-color", str), 111 | ("good-fortune-color", str), 112 | ("bad-fortune-color", str), 113 | ("middle-fortune-color", str), 114 | ("title-color", str), 115 | ("desc-color", str), 116 | ("button-color", str), 117 | ("button-hover-color", str), 118 | ("toggle-theme-button-color", str), 119 | ("copy-result-button-color", str), 120 | ("copy-preview-result-url-button-color", str), 121 | ("date-color", str), 122 | ("special-event-color", str), 123 | ] 124 | if not require_field_check(properties, idx, properties_field_required): 125 | return False 126 | 127 | for field_name in (v[0] for v in properties_field_required): 128 | color: str = properties[field_name] 129 | if color[0] != "#": 130 | errors[idx].append(f"color {color} should starts with `#`.") 131 | continue 132 | 133 | color = color[1:] 134 | if any(not ch.isdigit() and not ch.islower() for ch in color): 135 | errors[idx].append(f"color {color} should be all lowercase.") 136 | continue 137 | 138 | hex = set("0123456789abcdef") 139 | if any(ch not in hex for ch in color): 140 | errors[idx].append(f"color {color} should be a hex value.") 141 | continue 142 | 143 | if len(color) != len("rrggbb") and len(color) != len("rrggbbaa"): 144 | errors[idx].append(f"color {color} should be in `rrggbb` or `rrggbbaa` format.") 145 | continue 146 | 147 | 148 | theme_names.add(theme_name) 149 | 150 | return True 151 | 152 | for idx, theme in enumerate(themes): 153 | check_theme(theme, idx) 154 | 155 | if errors: 156 | logging.error(args.path) 157 | for idx, error_msgs in errors.items(): 158 | logging.error( 159 | json.dumps( 160 | themes[idx], indent=4, ensure_ascii=False 161 | ) 162 | ) 163 | for msg in error_msgs: 164 | logging.error(msg) 165 | exit(-1) 166 | -------------------------------------------------------------------------------- /scripts/main.js: -------------------------------------------------------------------------------- 1 | function check_ip_valid(n1, n2, n3, n4) { 2 | if (n1 > 255 || n2 > 255 || n3 > 255 || n4 > 255) return false; 3 | // private network 4 | if (n1 === 10) return false; 5 | // Carrier-grade NAT 6 | if (n1 == 100 && n2 == 64) return false; 7 | // localhost 8 | if (n1 === 127 && n2 === 0 && n3 === 0) return false; 9 | // link-local address 10 | if (n1 == 169 && n2 == 254) return false; 11 | // private network 12 | if (n1 === 172) { if (n2 >= 16 && n2 <= 31) return false; } 13 | if (n1 === 192) { 14 | if (n2 === 168) return false; // private network 15 | if (n2 === 0 && n3 === 0) return false; // IANA RFC 5735 16 | if (n2 === 0 && n3 === 2) return false; // TEST-NET-1 RFC 5735 17 | if (n2 === 88 && n3 === 99) return false; // 6to4 18 | } 19 | if (n1 == 198) { 20 | if (n2 == 18) return false; // RFC 2544 21 | if (n2 == 51 && n3 == 100) return false; // TEST-NET-2 RFC 5735 22 | } 23 | if (n1 == 203 && n3 == 113) return false; // TEST-NET-3 RFC 5735 24 | // class D network 25 | if (n1 == 224) return false; 26 | // class E network 27 | if (n1 == 255) return false; 28 | return true; 29 | } 30 | 31 | let goodFortunes = -1; 32 | let badFortunes = -1; 33 | let badLen = -1; 34 | let goodLen = -1; 35 | let buckets = {}; 36 | const statusLen = 8; 37 | 38 | const fs = require("fs"); 39 | fs.readFile('../fortune_generator/json/fortune.json', 'utf8', (err, content) => { 40 | if (err) { 41 | return; 42 | } 43 | 44 | let tmp = JSON.parse(content); 45 | 46 | goodFortunes = tmp.goodFortunes; 47 | goodLen = goodFortunes.length; 48 | badFortunes = tmp.badFortunes; 49 | badLen = badFortunes.length; 50 | 51 | let num = null; 52 | const dates = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 30]; 53 | let buckets = {}; 54 | let day = 0; 55 | let run_cnt = 0; 56 | let current_year = (new Date()).getFullYear(); 57 | while (run_cnt != 2000) { 58 | let n1 = parseInt(Math.random() * 255 + 1); 59 | let n2 = parseInt(Math.random() * 255 + 1); 60 | let n3 = parseInt(Math.random() * 255 + 1); 61 | let n4 = parseInt(Math.random() * 255 + 1); 62 | if (!check_ip_valid(n1, n2, n3, n4)) continue; 63 | let index = `${n1}.${n2}.${n3}.${n4}`; 64 | if (buckets[index] != undefined) continue; 65 | buckets[index] = [-1, -1, -1, -1, -1]; 66 | for (let i = 1; i <= 12; i++) { 67 | for (let j = 1; j <= dates[i - 1]; j++) { 68 | day %= 7; 69 | run(current_year, i, j, day, [n1, n2, n3, n4], buckets); 70 | day++; 71 | } 72 | } 73 | run_cnt++; 74 | } 75 | 76 | fs.writeFile("./res.txt", JSON.stringify(buckets), (err) => { 77 | console.log(err); 78 | }); 79 | }); 80 | 81 | // calculate hash and write result 82 | function run(year, month, date, day, ip, buckets) { 83 | let num = ip; 84 | 85 | // NOTE: hardcode 86 | const hashDate = Math.round( 87 | Math.log10( 88 | year * 89 | ((month << (Math.log10(num[3]) + day - 1)) * 90 | (date << Math.log10(num[2] << day))), 91 | ), 92 | ); 93 | seed1 = (num[0] >> hashDate) * (num[1] >> Math.min(hashDate, 2)) + 94 | (num[2] << 1) * (num[3] >> 3) + (date << 3) * (month << hashDate) + 95 | (year * day) >> 2; 96 | seed2 = (num[0] << (hashDate + 2)) * (num[1] << hashDate) + 97 | (num[2] << 1) * (num[3] << 2) + 98 | (date << (hashDate - 1)) * (month << 4) + year >> 99 | hashDate + (date * day) >> 1; 100 | 101 | // decide the status 102 | let seedMagic = 0; 103 | if (seed1 > seed2) { 104 | seedMagic = (seed1 ^ seed2) + parseInt(seed1.toString().split('').reverse().join('')); 105 | } else if (seed1 < seed2) { 106 | let collatzLen = 0; 107 | let temp = Math.abs(seed1 - seed2); 108 | while (temp !== 1) { 109 | temp = temp % 2 === 0 ? temp / 2 : 3 * temp + 1; 110 | collatzLen++; 111 | } 112 | seedMagic = collatzLen + seed2.toString(2).replace(/0/g, '').length; 113 | } else { 114 | seedMagic = seed1 + seed2; 115 | } 116 | status_index = ((seedMagic) % statusLen + statusLen) % statusLen; 117 | 118 | // make sure the events won't collide 119 | const set = new Set(); 120 | const l1 = (seed1 % goodLen + goodLen) % goodLen; 121 | set.add(goodFortunes[l1].event); 122 | let l2 = (((seed1 << 1) + date) % goodLen + goodLen) % goodLen; 123 | while (set.has(goodFortunes[l2].event)) { 124 | l2 = (l2 + 1) % goodLen; 125 | } 126 | set.add(goodFortunes[l2].event); 127 | let r1 = (((seed1 >> 1) + (month << 3)) % badLen + badLen) % badLen; 128 | while (set.has(badFortunes[r1].event)) { 129 | r1 = (r1 + 2) % badLen; 130 | } 131 | set.add(badFortunes[r1].event); 132 | let r2 = ((((((seed1 << 3) + (year >> 5) * (date << 2)) % badLen) * 133 | seed2) >> 6) % badLen + badLen) % badLen; 134 | while (set.has(badFortunes[r2].event)) { 135 | r2 = (r2 + 1) % badLen; 136 | } 137 | // NOTE: hardcode end 138 | 139 | // write l1, l2, r1, r2 140 | let index = `${ip[0]}.${ip[1]}.${ip[2]}.${ip[3]}`; 141 | buckets[index][0] = l1; 142 | buckets[index][1] = l2; 143 | buckets[index][2] = r1; 144 | buckets[index][3] = r2; 145 | buckets[index][4] = status_index 146 | } 147 | -------------------------------------------------------------------------------- /scripts/plot_gen.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import matplotlib.pyplot as plt 4 | 5 | with open("./res.txt") as res_file: 6 | res = json.load(res_file) 7 | 8 | good_len = -1 9 | bad_len = -1 10 | with open("../fortune_generator/json/fortune.json") as fortune_file: 11 | j = json.load(fortune_file) 12 | good_len = len(j["goodFortunes"]) 13 | bad_len = len(j["badFortunes"]) 14 | 15 | status_bucket = [0] * 8 16 | good_bucket = [0] * good_len 17 | bad_bucket = [0] * bad_len 18 | 19 | for ip, v in res.items(): 20 | assert all(val != -1 for val in v) 21 | good_bucket[v[0]] += 1 22 | good_bucket[v[1]] += 1 23 | bad_bucket[v[2]] += 1 24 | bad_bucket[v[3]] += 1 25 | status_bucket[v[4]] += 1 26 | 27 | 28 | groups = 1 29 | fig, axs = plt.subplots(groups, 1, figsize=(8, 6)) 30 | 31 | axs.bar( 32 | range(good_len), 33 | good_bucket, 34 | color="skyblue", 35 | edgecolor="black", 36 | ) 37 | axs.set_xlabel("Good Fortune Event Index") 38 | axs.set_ylabel("Occurrences") 39 | 40 | plt.tight_layout() 41 | plt.show() 42 | 43 | fig, axs = plt.subplots(groups, 1, figsize=(8, 6)) 44 | 45 | axs.bar( 46 | range(bad_len), 47 | bad_bucket, 48 | color="skyblue", 49 | edgecolor="black", 50 | ) 51 | axs.set_xlabel("Bad Fortune Event Index") 52 | axs.set_ylabel("Occurrences") 53 | 54 | plt.tight_layout() 55 | plt.show() 56 | 57 | fig, axs = plt.subplots(groups, 1, figsize=(8, 6)) 58 | 59 | axs.bar( 60 | range(len(status_bucket)), 61 | status_bucket, 62 | color="skyblue", 63 | edgecolor="black", 64 | ) 65 | axs.set_xlabel("Status Index") 66 | axs.set_ylabel("Occurrences") 67 | 68 | plt.show() 69 | -------------------------------------------------------------------------------- /scripts/template-generator.py: -------------------------------------------------------------------------------- 1 | #!/bin/python3 2 | 3 | import os 4 | import json 5 | import shutil 6 | import logging 7 | import argparse 8 | 9 | args_parser = argparse.ArgumentParser(description="Generator Template Generator") 10 | args_parser.add_argument("config", type=str, help="config json path", nargs="?") 11 | args = args_parser.parse_args() 12 | 13 | if args.config: 14 | config = None 15 | with open(args.config, "r") as f: 16 | config = json.loads(f.read()) 17 | 18 | name = config["name"] 19 | desc = config["desc"] 20 | title = config["title"] 21 | repo_name = config["repo_name"] 22 | 23 | else: 24 | name = input("Generator name (like fortune, quote): ") 25 | desc = input("Generator desc: ") 26 | title = input("Generator title: ") 27 | repo_name = input("Github repo name: ") 28 | 29 | folder_path = f"{name}_generator" 30 | 31 | if os.path.exists(folder_path): 32 | logging.error(f"{folder_path} already exists. Please choose another name.") 33 | exit(1) 34 | 35 | os.mkdir(folder_path) 36 | os.mkdir(f"{folder_path}/css") 37 | os.mkdir(f"{folder_path}/js") 38 | os.mkdir(f"{folder_path}/images") 39 | os.mkdir(f"{folder_path}/json") 40 | 41 | 42 | def write_file(src_path, dst_path, **kwargs): 43 | content = None 44 | with open(f"scripts/template/{src_path}", "r") as f: 45 | content = f.read() 46 | 47 | for key, val in kwargs.items(): 48 | assert ( 49 | content.find("{{ %s }}" % key) != -1 50 | ), f"The key '{key}' does not appear in scripts/template/{src_path}" 51 | content = content.replace("{{ %s }}" % key, val) 52 | 53 | with open(dst_path, "w") as f: 54 | f.write(content) 55 | 56 | 57 | write_file("css/styles.css", f"{folder_path}/css/styles.css") 58 | write_file("js/main.js", f"{folder_path}/js/{name}.js", name=name) 59 | write_file("js/matrix.js", f"{folder_path}/js/matrix.js") 60 | write_file("js/scripts.js", f"{folder_path}/js/scripts.js") 61 | write_file("js/theme.js", f"{folder_path}/js/theme.js") 62 | write_file( 63 | "js/service-worker.js", 64 | f"{folder_path}/js/service-worker.js", 65 | name=name, 66 | repo_name=repo_name, 67 | folder_path=folder_path, 68 | ) 69 | write_file( 70 | "manifest.json", 71 | f"{folder_path}/manifest.json", 72 | title=title, 73 | desc=desc, 74 | repo_name=repo_name, 75 | folder_path=folder_path, 76 | ) 77 | write_file("json/themes.json", f"{folder_path}/json/themes.json") 78 | write_file("index.html", f"{folder_path}/index.html", name=name, desc=desc, title=title) 79 | shutil.copytree("scripts/template/images", f"{folder_path}/images", dirs_exist_ok=True) 80 | -------------------------------------------------------------------------------- /scripts/template/css/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --button-color: #73a3eb; 3 | --button-hover-color: #459aef; 4 | --toggle-theme-button-color: #000000; 5 | --copy-result-button-color: #000000; 6 | --bg-color: #ffffff; 7 | --title-color: #000000cc; 8 | } 9 | 10 | * { 11 | overflow: hidden; 12 | text-align: center; 13 | white-space: nowrap; 14 | } 15 | 16 | body { 17 | margin: 0; 18 | padding: 0; 19 | height: 100%; 20 | align-items: center; 21 | justify-content: center; 22 | } 23 | 24 | .container { 25 | top: 50%; 26 | left: 50%; 27 | width: 80%; 28 | max-width: 800px; 29 | position: absolute; 30 | z-index: 1; 31 | text-align: center; 32 | transform: translate(-50%, -50%); 33 | background-color: var(--bg-color); 34 | border-radius: 40px; 35 | padding: 10px; 36 | } 37 | 38 | 39 | button { 40 | background-color: var(--button-color); 41 | color: var(--bg-color); 42 | z-index: 2; 43 | font-size: 20px; 44 | border: none; 45 | padding: 20px 20px; 46 | border-radius: 30px; 47 | cursor: pointer; 48 | transition: all 0.3s ease-in-out; 49 | } 50 | 51 | button:hover { 52 | background-color: var(--button-hover-color); 53 | } 54 | 55 | #Matrix { 56 | z-index: 0; 57 | } 58 | 59 | #toggle-theme-button { 60 | margin-top: 15px; 61 | font-size: 2.4rem; 62 | color: var(--toggle-theme-button-color); 63 | cursor: pointer; 64 | opacity: 85%; 65 | } 66 | 67 | #copy-result-button { 68 | margin-top: 20px; 69 | font-size: 1.5rem; 70 | color: var(--copy-result-button-color); 71 | } 72 | 73 | #themeModal { 74 | .modal-content { 75 | background-color: var(--bg-color) !important; 76 | color: var(--title-color) !important; 77 | } 78 | 79 | .modal-header, 80 | .modal-footer { 81 | background-color: var(--bg-color) !important; 82 | color: var(--bg-color) !important; 83 | } 84 | 85 | .modal-title, 86 | .btn-close { 87 | color: var(--title-color) !important; 88 | } 89 | } 90 | 91 | #themeItem { 92 | background-color: var(--bg-color); 93 | color: var(--title-color); 94 | } 95 | 96 | ::-webkit-scrollbar { 97 | width: 8px; 98 | } 99 | 100 | ::-webkit-scrollbar-track { 101 | background: var(--bg-color); 102 | border-radius: 10px; 103 | } 104 | 105 | ::-webkit-scrollbar-thumb { 106 | background: var(--button-color); 107 | border-radius: 10px; 108 | } 109 | 110 | ::-webkit-scrollbar-thumb:hover { 111 | background: var(--button-hover-color); 112 | } 113 | 114 | .color-preview-container { 115 | display: flex; 116 | align-items: center; 117 | padding: 3px; 118 | border-radius: 25px; 119 | } 120 | 121 | .color-preview { 122 | display: flex; /* Use flex to align dots in a row */ 123 | } 124 | 125 | .color-dot { 126 | display: inline-block; 127 | width: 12px; /* Dot size */ 128 | height: 12px; /* Dot size */ 129 | border-radius: 50%; /* Circular shape */ 130 | margin-left: 5px; /* Spacing between dots */ 131 | } 132 | 133 | .color-preview .color-dot:first-child { 134 | margin-left: 0; /* No margin on the left for the first dot */ 135 | } 136 | -------------------------------------------------------------------------------- /scripts/template/images/logo-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LifeAdventurer/generators/79965ea3a8425e38e8ec0e88fa5d2798f471540c/scripts/template/images/logo-180x180.png -------------------------------------------------------------------------------- /scripts/template/images/logo-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LifeAdventurer/generators/79965ea3a8425e38e8ec0e88fa5d2798f471540c/scripts/template/images/logo-192x192.png -------------------------------------------------------------------------------- /scripts/template/images/logo-270x270.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LifeAdventurer/generators/79965ea3a8425e38e8ec0e88fa5d2798f471540c/scripts/template/images/logo-270x270.png -------------------------------------------------------------------------------- /scripts/template/images/logo-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LifeAdventurer/generators/79965ea3a8425e38e8ec0e88fa5d2798f471540c/scripts/template/images/logo-512x512.png -------------------------------------------------------------------------------- /scripts/template/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/LifeAdventurer/generators/79965ea3a8425e38e8ec0e88fa5d2798f471540c/scripts/template/images/logo.png -------------------------------------------------------------------------------- /scripts/template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ title }} 8 | 9 | 10 | 11 | 17 | 20 | 21 | 25 | 31 | 32 | 37 | 38 | 39 | 40 |
41 |
42 |
43 | 44 | 46 | 47 |
48 | 54 | 57 | 62 |
63 |
64 | 65 | 66 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 105 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /scripts/template/js/main.js: -------------------------------------------------------------------------------- 1 | function InitPage() { 2 | $("#init-page").show(); 3 | $("#result-page").hide(); 4 | } 5 | 6 | function Appear() { 7 | $("#init-page").hide(); 8 | $("#result-page").show(); 9 | } 10 | 11 | function get{{ name }}() { 12 | Update(); 13 | } 14 | 15 | InitPage() 16 | -------------------------------------------------------------------------------- /scripts/template/js/matrix.js: -------------------------------------------------------------------------------- 1 | const canvas = document.getElementById("Matrix"); 2 | const context = canvas.getContext("2d"); 3 | 4 | canvas.height = globalThis.innerHeight + 100; 5 | canvas.width = globalThis.innerWidth + 5; 6 | 7 | const chars = 8 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./*-+#$%^@!~?><:;[]{}=_αβΓγΔδεζηΘθικΛλμΞξΠπρΣσςτυΦφχΨψΩω×≦≧≠∞≒≡~∩∠∪∟⊿∫∮∵∴$¥〒¢£℃€℉╩◢ⅩⅨⅧⅦⅥⅤⅣⅢⅡⅠあいうえおがぎぐげござじずぜぞだぢつでづどにぬのばひぴぶへぺぼみゃょァゐゎè"; 9 | 10 | const fontSize = 16; 11 | const columns = canvas.width / fontSize; 12 | 13 | const charArr = []; 14 | for (let i = 0; i < columns; i++) { 15 | charArr[i] = 1; 16 | } 17 | 18 | let frame = 0; 19 | let str; 20 | 21 | context.fillStyle = "rgba(0, 0, 0, 1)"; 22 | context.fillRect(0, 0, canvas.width, canvas.height); 23 | 24 | function Update() { 25 | context.fillStyle = "rgba(0, 0, 0, 0.05)"; 26 | context.fillRect(0, 0, canvas.width, canvas.height); 27 | 28 | if (frame == 0) { 29 | const a = parseInt(Math.random() * 255); 30 | str = `rgba(${a}, ${Math.abs(a - 127)}, ${Math.abs(a - 255)}, 0.9)`; 31 | } 32 | context.fillStyle = str; 33 | context.font = fontSize + "px monospace"; 34 | 35 | for (let i = 0; i < columns; i++) { 36 | const text = chars[Math.floor(Math.random() * chars.length)]; 37 | context.fillText(text, i * fontSize, charArr[i] * fontSize); 38 | if (charArr[i] * fontSize > canvas.height && Math.random() > 0.90) { 39 | charArr[i] = 0; 40 | } 41 | charArr[i]++; 42 | } 43 | frame++; 44 | 45 | if (frame <= 40 * (Math.floor(Math.random() * 10) + 3)) { 46 | requestAnimationFrame(Update); // 40 frames a cycle 47 | } else { 48 | frame = 0; 49 | Appear(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /scripts/template/js/scripts.js: -------------------------------------------------------------------------------- 1 | function copyResultImageToClipboard() { 2 | try { 3 | const root = document.documentElement; 4 | const backgroundColor = getComputedStyle(root).getPropertyValue('--bg-color'); 5 | 6 | htmlToImage.toBlob($("#result-page")[0], { 7 | skipFonts: true, 8 | preferredFontFormat: "woff2", 9 | backgroundColor: backgroundColor, // Set background color dynamically 10 | }).then((blob) => { 11 | navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]); 12 | showCopiedNotice(); 13 | $title.parent().remove(); 14 | }).catch((error) => { 15 | console.error("Error converting result page to image:", error); 16 | $title.parent().remove(); 17 | }); 18 | } catch (error) { 19 | console.error("Error copying result image to clipboard:", error); 20 | } 21 | } 22 | 23 | function showCopiedNotice() { 24 | const notice = $("
", { 25 | text: "Copied to clipboard!", 26 | css: { 27 | position: "fixed", 28 | bottom: "20px", 29 | right: "20px", 30 | padding: "10px 20px", 31 | backgroundColor: "rgba(0, 0, 0, 0.7)", 32 | color: "#fff", 33 | borderRadius: "5px", 34 | zIndex: 1000, 35 | }, 36 | }); 37 | 38 | $("body").append(notice); 39 | 40 | setTimeout(() => { 41 | notice.fadeOut(300, () => { 42 | notice.remove(); 43 | }); 44 | }, 3000); 45 | } 46 | -------------------------------------------------------------------------------- /scripts/template/js/service-worker.js: -------------------------------------------------------------------------------- 1 | const pre_cache_file_version = "pre-v1.0.0"; 2 | const auto_cache_file_version = "auto-v1.0.0"; 3 | 4 | const ASSETS = [ 5 | "/{{ repo_name }}/{{ folder_path }}/images/logo-192x192.png", 6 | "/{{ repo_name }}/{{ folder_path }}/images/logo-512x512.png", 7 | "/{{ repo_name }}/{{ folder_path }}/images/logo-180x180.png", 8 | "/{{ repo_name }}/{{ folder_path }}/images/logo-270x270.png", 9 | "/{{ repo_name }}/{{ folder_path }}/images/logo.jpg", 10 | 11 | "https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css", 12 | "https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js", 13 | ]; 14 | 15 | const NEED_UPDATE = [ 16 | "/{{ repo_name }}/{{ folder_path }}/", 17 | "/{{ repo_name }}/{{ folder_path }}/index.html", 18 | "/{{ repo_name }}/{{ folder_path }}/css/styles.css", 19 | "/{{ repo_name }}/{{ folder_path }}/js/{{ name }}.js", 20 | "/{{ repo_name }}/{{ folder_path }}/js/matrix.js", 21 | "/{{ repo_name }}/{{ folder_path }}/json/theme.json", 22 | "/{{ repo_name }}/{{ folder_path }}/json/manifest.json", 23 | ]; 24 | 25 | const limit_cache_size = (name, size) => { 26 | caches.open(name).then((cache) => { 27 | cache.keys().then((keys) => { 28 | if (keys.length > size) { 29 | cache.delete(keys[0]).then(() => { 30 | limit_cache_size(name, size); 31 | }); 32 | } 33 | }); 34 | }); 35 | }; 36 | 37 | const is_in_array = (str, array) => { 38 | let path = ""; 39 | 40 | // Check the request's domain is the same as the current domain. 41 | if (str.indexOf(self.origin) === 0) { 42 | path = str.substring(self.origin.length); // Remove https://lifeadventurer.github.io 43 | } else { 44 | path = str; // outside request 45 | } 46 | 47 | return array.indexOf(path) > -1; 48 | }; 49 | 50 | // install 51 | self.addEventListener("install", (event) => { 52 | self.skipWaiting(); 53 | 54 | //pre-cache files 55 | event.waitUntil( 56 | caches.open(pre_cache_file_version).then((cache) => { 57 | cache.addAll(ASSETS); 58 | }), 59 | ); 60 | }); 61 | 62 | // activate 63 | self.addEventListener("activate", (event) => { 64 | event.waitUntil( 65 | caches.keys().then((keys) => { 66 | return Promise.all(keys.map((key) => { 67 | if ( 68 | pre_cache_file_version.indexOf(key) === -1 && 69 | auto_cache_file_version.indexOf(key) === -1 70 | ) { 71 | return caches.delete(key); 72 | } 73 | })); 74 | }), 75 | ); 76 | }); 77 | 78 | // fetch event 79 | self.addEventListener("fetch", (event) => { 80 | if (is_in_array(event.request.url, ASSETS)) { 81 | // cache only strategy 82 | 83 | event.respondWith( 84 | caches.match(event.request.url), 85 | ); 86 | } else if (is_in_array(event.request.url, NEED_UPDATE)) { 87 | event.respondWith( 88 | fetch(event.request.url).then(async (response) => { 89 | if (response.ok) { 90 | const cache = await caches.open(auto_cache_file_version); 91 | cache.put(event.request.url, response.clone()); 92 | return response; 93 | } 94 | 95 | throw new Error("Network response was not ok."); 96 | }).catch(async (_error) => { 97 | const cache = await caches.open(auto_cache_file_version); 98 | return cache.match(event.request.url); 99 | }), 100 | ); 101 | } 102 | }); 103 | -------------------------------------------------------------------------------- /scripts/template/js/theme.js: -------------------------------------------------------------------------------- 1 | document.addEventListener("DOMContentLoaded", () => { 2 | const themeListContainer = document.querySelector("#themeList"); 3 | const root = document.documentElement; 4 | 5 | // Apply the saved theme if it exists 6 | applySavedTheme(); 7 | 8 | async function fetchThemes() { 9 | try { 10 | const response = await fetch("./json/themes.json"); 11 | const themes = await response.json(); 12 | populateThemeList(themes["themes"]); 13 | } catch (error) { 14 | console.error("Error fetching themes:", error); 15 | } 16 | } 17 | 18 | // Populate theme list in modal 19 | function populateThemeList(themes) { 20 | themeListContainer.innerHTML = ""; 21 | themes.forEach((theme) => { 22 | const themeItem = document.createElement("div"); 23 | themeItem.className = 24 | "theme-item list-group-item d-flex justify-content-between align-items-center"; 25 | themeItem.style.cursor = "pointer"; 26 | themeItem.id = "themeItem"; 27 | 28 | // Add theme name 29 | const themeName = document.createElement("span"); 30 | themeName.textContent = theme.name; 31 | themeItem.appendChild(themeName); 32 | 33 | const colorPreivewContainer = document.createElement("div"); 34 | colorPreivewContainer.className = "color-preview-container"; 35 | 36 | const propertyKeys = Object.keys(theme.properties); 37 | colorPreivewContainer.style.backgroundColor = 38 | theme.properties[propertyKeys[5]]; 39 | 40 | // Add color dots for visual preview 41 | const colorPreview = document.createElement("div"); 42 | colorPreview.className = "color-preview"; 43 | 44 | Object.values(theme.properties).slice(0, 3).forEach((color) => { 45 | const colorDot = document.createElement("span"); 46 | colorDot.style.backgroundColor = color; 47 | colorDot.className = "color-dot"; 48 | colorPreview.appendChild(colorDot); 49 | }); 50 | 51 | colorPreivewContainer.appendChild(colorPreview); 52 | themeItem.appendChild(colorPreivewContainer); 53 | 54 | // Apply theme on click 55 | themeItem.addEventListener("click", () => { 56 | applyTheme(theme.properties); 57 | saveThemeToLocalStorage(theme.name); 58 | }); 59 | 60 | themeListContainer.appendChild(themeItem); 61 | }); 62 | } 63 | 64 | // Apply theme by setting CSS variables 65 | function applyTheme(properties) { 66 | Object.entries(properties).forEach(([key, value]) => { 67 | root.style.setProperty(`--${key}`, value); 68 | }); 69 | } 70 | 71 | function saveThemeToLocalStorage(themeName) { 72 | localStorage.setItem("selectedTheme", themeName); 73 | } 74 | 75 | function applySavedTheme() { 76 | const savedThemeName = localStorage.getItem("selectedTheme"); 77 | if (savedThemeName) { 78 | fetch("./json/themes.json") 79 | .then((response) => response.json()) 80 | .then((themes) => { 81 | const theme = themes.themes.find((t) => t.name === savedThemeName); 82 | if (theme) { 83 | applyTheme(theme.properties); 84 | } 85 | }) 86 | .catch((error) => console.error("Error fetching themes:", error)); 87 | } 88 | } 89 | 90 | fetchThemes(); 91 | }); 92 | -------------------------------------------------------------------------------- /scripts/template/json/themes.json: -------------------------------------------------------------------------------- 1 | { 2 | "themes": [ 3 | { 4 | "name": "Classic Light", 5 | "properties": { 6 | "title-color": "#000000cc", 7 | "bg-color": "#ffffff", 8 | "button-color": "#73a3eb", 9 | "button-hover-color": "#459aef", 10 | "toggle-theme-button-color": "#000000", 11 | "copy-result-button-color": "#000000" 12 | } 13 | }, 14 | { 15 | "name": "Classic Dark", 16 | "properties": { 17 | "title-color": "#cdcdcd", 18 | "bg-color": "#1e1d24", 19 | "button-color": "#5d99f4", 20 | "button-hover-color": "#9ac6f1", 21 | "toggle-theme-button-color": "#ffffff", 22 | "copy-result-button-color": "#ffffff" 23 | } 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /scripts/template/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "{{ title }}", 3 | "name": "{{ title }}", 4 | "description": "{{ desc }}", 5 | "background_color": "#1a1b1e", 6 | "theme_color": "#1a1b1e", 7 | "icons": [ 8 | { 9 | "src": "./images/logo-192x192.png", 10 | "sizes": "192x192", 11 | "type": "image/png" 12 | }, 13 | { 14 | "src": "./images/logo-512x512.png", 15 | "sizes": "512x512", 16 | "type": "image/png" 17 | }, 18 | { 19 | "src": "./images/logo-180x180.png", 20 | "sizes": "180x180", 21 | "type": "image/png" 22 | }, 23 | { 24 | "src": "./images/logo-270x270.png", 25 | "sizes": "270x270", 26 | "type": "image/png" 27 | } 28 | ], 29 | "start_url": "/{{ repo_name }}/{{ folder_path }}/index.html", 30 | "display": "standalone", 31 | "orientation": "portrait" 32 | } 33 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --bg-color: #ffffff; 3 | --title-color: #363636; 4 | /* button */ 5 | --button-color: #6c757d; 6 | --button-hover-color: #565e64; 7 | --button-text-color: #ffffff; 8 | /* card-footer */ 9 | --card-bg-color: #212529; 10 | --card-title-color: #ffffff; 11 | --card-footer-color: #343a40; 12 | --card-footer-text-color: #adb5bd; 13 | } 14 | 15 | .dark-mode { 16 | --bg-color: #000000dd; 17 | --title-color: #ffffffd8; 18 | --dark-mode-icon-color: #efefef; 19 | /* button */ 20 | --button-color: #9c9fa2ec; 21 | --button-hover-color: #797d7fec; 22 | --button-text-color: #121212; 23 | /* card-footer */ 24 | --card-bg-color: #f8f8f8; 25 | --card-title-color: #3a3a3a; 26 | --card-footer-color: #e1e1e1; 27 | --card-footer-text-color: #4c4c4c; 28 | } 29 | 30 | .btn { 31 | background-color: var(--button-color); 32 | color: var(--button-text-color); 33 | } 34 | 35 | .btn:hover { 36 | background-color: var(--button-hover-color); 37 | } 38 | 39 | body { 40 | background-color: var(--bg-color); 41 | } 42 | 43 | h1 { 44 | align-items: center; 45 | text-align: center; 46 | color: var(--title-color); 47 | } 48 | 49 | h5 { 50 | color: var(--title-color); 51 | } 52 | 53 | .card { 54 | background-color: var(--card-bg-color); 55 | } 56 | 57 | .card-title, .card-text { 58 | color: var(--card-title-color); 59 | } 60 | 61 | .container { 62 | margin-top: 30px; 63 | } 64 | 65 | .card-footer { 66 | background-color: var(--card-footer-color); 67 | color: var(--card-footer-text-color); 68 | } 69 | 70 | #footer-author { 71 | text-align: right; 72 | } 73 | 74 | #footer-author-icon { 75 | width: 4%; 76 | border-radius: 50%; 77 | overflow: hidden; 78 | } 79 | 80 | .row { 81 | display: flex; 82 | } 83 | 84 | #dark-mode-icon { 85 | margin-left: 25px; 86 | margin-top: 15px; 87 | font-size: 2.4rem; 88 | color: var(--dark-mode-icon-color); 89 | cursor: pointer; 90 | opacity: 85%; 91 | } 92 | --------------------------------------------------------------------------------