170 Commits
v1.0.0 ... main

Author SHA1 Message Date
tobiichi3227
79965ea3a8 Fix(fortune): unable to display result if already drawn for the day (#73) 2025-05-05 14:50:01 +08:00
tobiichi3227
7a372a7003 Feat: Add a script to generate Generator from a template (#72) 2025-03-28 22:32:47 +08:00
lifeadventurer
afda24a38a Fix(home-button): Adjust href to match both localhost and github pages 2025-03-20 20:22:49 +08:00
Moon
14a3912f4c Feat: Add fixed home button (#70) 2025-03-20 17:57:12 +08:00
tobiichi3227
4eb9212a23 fix: align generator item (#69)
* fix: align generator item

Close #26

* Chore: remove hack
2025-03-20 16:36:55 +08:00
Moon
ea18eb8057 Feat(fortune): Add Pi Day (#68) 2025-03-13 23:59:46 +08:00
tobiichi3227
c80c879af1 Fix(fortune): special_events_index is undefined when there are no special events today (#67) 2025-03-09 17:00:44 +08:00
tobiichi3227
f9f0f9c1a4 Fix(fortune): Copy preview url without commit hash (#66) 2025-03-09 16:43:39 +08:00
Moon
cf9254c60c CI: Fix permission issue in static.yml (#65) 2025-03-09 16:12:07 +08:00
tobiichi3227
e1e17bd2bc Feat(Fortune): Preview result url (#62)
* Feat(Fortune): Support preview result depends on url params

* Feat(Fortune): Specify commit hash in preview result to ensure event consistency

* Feat(Fortune): Add copy preview url button

* Feat(Fortune): Support theme color for copy preview button

* Impr(Fortune): Make UI looks better

* Docs(CONTRIBUTING): Add missing theme field
2025-03-09 15:52:26 +08:00
ChenKaiLiuG
1471b5950d Feat(fortune): Add new fortune events (#63)
* Update fortune.json

* Fix Update fortune.json

* Fix update fortune.json
2025-03-08 23:55:14 +08:00
tobiichi3227
6c5928b5c2 Fix(Fortune): Missing fortune and themes check on CI (#64)
* Fix(Fortune): Missing fortune and themes check on ci

* Update check-fortune-generator-json.yml
2025-03-08 15:59:00 +08:00
tobiichi3227
1d969400e5 Feat(Fortune): Support multiple event in same day (#61)
* Feat(Fortune): Support multiple event in same day

* Fix(Fortune): f-string typo

* Docs(Fortune): Multiple events supporting
2025-03-06 14:23:02 +08:00
Moon
237f0e1551 Feat(fortune): Trigger matrix background animation everytime (#60) 2025-03-02 23:02:44 +08:00
tobiichi3227
be8b5d30d7 Add tobiichi3227 to CODEOWNERS (#59)
* Add tobiichi3227 to CODEOWNERS

* Update CODEOWNERS
2025-02-28 22:54:07 +08:00
lifeadventurer
36502dd705 Fix: Horrible bad fortune distribution 2025-02-26 23:47:00 +08:00
tobiichi3227
fca41fb842 Feat(Fortune): Probability analyzer (#58) 2025-02-26 18:22:59 +08:00
tobiichi3227
8bd2345db7 Feat(Fortune): More json file checker (#57)
* Fix(Fortune): We should check if event is a dict first

* Feat(Fortune): Add fortune checker

* Feat(Fortune): Add theme checker
2025-02-26 00:23:13 +08:00
lifeadventurer
4531962dbb Chore: Update copyright to 2023-2025 2025-02-22 23:17:24 +08:00
lifeadventurer
16b7c8dde1 Feat: Add seedMagic 2025-02-22 01:30:08 +08:00
lifeadventurer
f7d450895f Refactor: Update service worker paths 2025-02-22 00:38:30 +08:00
tobiichi3227
52d09db764 Chore(Fortune): add pre-commit to check special events (#55)
* Chore(Fortune): add pre-commit to check special events

* Chore(Fortune): Update pre-commit hook version

* Chore(Fortune): replace shell script with Python for better compatibility
2025-02-21 01:00:49 +08:00
tobiichi3227
32e3156d30 Fix(Fortune): properly display Unicode characters (#54) 2025-02-17 07:44:41 +08:00
tobiichi3227
4391176199 Chore: Update service-worker special file cache (#53) 2025-02-16 14:50:49 +08:00
lifeadventurer
704032580b Chore: Remove redundant file 2025-02-13 23:14:54 +08:00
tobiichi3227
57f1307847 Impr(special): Add new special events for January and February (#48)
Co-authored-by: ChenKaiLiuG <141424456+ChenKaiLiuG@users.noreply.github.com>
2025-02-06 21:08:52 +08:00
Moon
c82e29a565 Merge pull request #51 from LifeAdventurer/LifeAdventurer-patch-1
CI: Update GitHub actions to latest
2025-02-06 17:41:09 +08:00
Moon
b03665545f CI: Update GitHub actions to latest
From https://github.com/LifeAdventurer/generators/actions/runs/13175670602
2025-02-06 17:40:23 +08:00
tobiichi3227
b82b3f366e Feat(fortune): ci add special event checker (#50)
* Feat(fortune): ci add special event checker

* Chore(Fortune): Move all scripts from `dev` to `scripts` and remove outdated scripts
2025-02-06 17:25:23 +08:00
tobiichi3227
caa59577dd Fix(Fortune): Categorize events (#49) 2025-01-24 13:28:33 +08:00
Moon
4b2f30bd7f Merge pull request #46 from tobiichi3227/fix/event-diff-cal
Fix(Fortune): Incorrect time difference calculation for recent events
2024-12-25 19:07:38 +08:00
54168328c2 Fix(Fortune): Categorize events by trigger date type 2024-12-25 11:39:06 +08:00
e80f077ae3 Fix(Fortune): Calculate the time difference for events that have already occurred based on the next year
For example, if today is January 2nd, then the New Year's Day on January 1st should be calculated using January 1st of the next year.
2024-12-25 11:35:26 +08:00
tobiichi3227
7852572f26 Feat: Add support for cyclical dates in special events to avoid yearly updates (#45)
* Feat(fortune): add static date and cyclical date support

* Chore(Fortune): convert static event date to cyclical date

* Docs: Update CONTRIBUTING.md

* Chore(Fortune): Remove old special events

* Fix(Fortune): Correctly set the trigger date for Thanksgiving

* Impr(fortune): Make the code more readable by reducing duplication and lengthy validation blocks
Additionally, strengthen the handling of edge cases (e.g., 31 in February).

* Impr(fortune): Separate the default and user-defined events into different files for easier identification and management

* Docs: Update CONTRIBUTING.md

* Impr(fortune): Simplify events file categorization

* Docs: Update CONTRIBUTING.md

* Docs: Update CONTRIBUTING.md
2024-12-12 22:48:21 +08:00
Moon
a4d457d9c7 Merge pull request #44 from tobiichi3227/fix-event-typo
Fix(fortune): Event description typo
2024-11-12 15:38:26 +08:00
ae01f41a84 Fix(fortune): event description typo 2024-11-12 15:28:22 +08:00
lifeadventurer
03a768090b Impr(theme): Add new theme - Cyberwave 2024-11-07 10:07:48 +08:00
lifeadventurer
6b45d59ceb Impr(theme): Add new theme - Aurora Borealis 2024-11-07 10:06:29 +08:00
lifeadventurer
fd9fe8b460 Impr(theme): Add new theme - Zen Garden 2024-11-07 09:51:38 +08:00
lifeadventurer
708bcdd342 Impr(theme): Add new theme - Abstract Art 2024-11-07 09:49:05 +08:00
lifeadventurer
eacd73f929 Impr(theme): Add new theme - Tropical Paradise 2024-11-07 09:49:00 +08:00
lifeadventurer
68bab668b1 Impr(theme): Add new theme - Metallic Shine 2024-11-07 09:48:54 +08:00
lifeadventurer
a10be0ef61 Feat(fortune): Add custom scrollbar styling for theme modal selection 2024-11-06 22:46:32 +08:00
lifeadventurer
5874d347a0 Feat(fortune): Set dynamic max-height and overflow for theme modal body with scrollable theme items 2024-11-06 22:18:07 +08:00
lifeadventurer
ccbea6777c Impr(theme): Add new theme - Vintage Sepia 2024-11-06 22:09:52 +08:00
lifeadventurer
15a435cc77 Impr(theme): Add new theme - Mystic Forest 2024-11-06 22:07:07 +08:00
lifeadventurer
2d2e9b5c78 Impr(theme): Refactored theme - Star Wars to Galactic Glow 2024-11-06 22:05:18 +08:00
lifeadventurer
6c657ad6ab Impr(theme): Add new theme - Lunar Eclipse 2024-11-06 21:54:37 +08:00
lifeadventurer
9344253ebb Impr(theme): Add new theme - Moonlit Night 2024-11-06 21:39:03 +08:00
lifeadventurer
72a3513a70 Impr(theme): Add new theme - Sunny Vibes 2024-11-06 21:29:04 +08:00
lifeadventurer
1464de0ff3 Impr(theme): Add new theme - Spring Blossom 2024-11-06 18:39:54 +08:00
lifeadventurer
97e8a340da Impr(theme): Refactored theme - Winter to Winter Wonderland 2024-11-06 15:47:48 +08:00
lifeadventurer
3fec297ad8 Impr(theme): Add new theme - Autumn Glow 2024-11-06 15:32:46 +08:00
lifeadventurer
99d2a23f3b Impr(theme): Add new theme - Tokyo Night 2024-11-06 00:19:44 +08:00
lifeadventurer
85c294b170 Chore(theme): Rename theme names 2024-11-06 00:16:22 +08:00
lifeadventurer
81808b7b57 Impr(theme): Add new theme - Catppuccin Dark 2024-11-06 00:13:29 +08:00
lifeadventurer
03a72dd1d1 Style: Use lowercases in hex colors 2024-11-06 00:13:29 +08:00
lifeadventurer
031364c279 Impr(theme): Add new theme - Star Wars 2024-11-06 00:13:17 +08:00
lifeadventurer
80df03f337 Docs(CONTRIBUTING): Add guidelines for contributing new themes 2024-11-02 16:49:10 +08:00
lifeadventurer
b93727e5b8 Refactor: Update theme winter 2024-11-02 16:38:25 +08:00
lifeadventurer
8d1e04090c Feat(theme): Save selected theme to localStorage for persistence 2024-11-01 23:35:40 +08:00
lifeadventurer
6d8017fe51 Chore(theme): Rearrange the order of the properties 2024-11-01 23:23:30 +08:00
lifeadventurer
ee5b9e8ffa Impr(theme): Add new theme - winter 2024-11-01 23:20:49 +08:00
lifeadventurer
f9c74860dd Style(fortune): Remove margin on the left for the first dot in preview container 2024-11-01 22:37:59 +08:00
lifeadventurer
9b8fbe8809 Feat(fortune): Add colorPreviewContainer to encapsulate dots for improved clarity 2024-11-01 22:36:35 +08:00
lifeadventurer
a345a61f25 Feat(fortune): Add color dots for visual preview in theme item 2024-11-01 22:02:03 +08:00
lifeadventurer
15d3cfb35a Feat(fortune): Implement theme switcher (Closes #42) 2024-11-01 21:25:42 +08:00
lifeadventurer
32b7abf7fc Impr(fortune): Add new special events for November 2024-11-01 18:49:08 +08:00
lifeadventurer
778cb316c0 Style: Apply deno lint and deno fmt across codebase 2024-10-31 19:56:47 +08:00
lifeadventurer
df772b0bde Refactor: Update service worker paths for custom domain 2024-10-31 17:56:49 +08:00
lifeadventurer
5ca0f68b5f Fix: Special event bug 2024-10-15 00:28:00 +08:00
lifeadventurer
32e3b5b9a3 Refactor: Use const instead of let for immutable variables (Make LSP happy) 2024-10-12 00:27:15 +08:00
lifeadventurer
bd00a3b72d Feat(quote generator): Add method for adding backgrounds (Closes #13) 2024-10-12 00:17:40 +08:00
lifeadventurer
41191438b9 Docs: Add License section in README.md 2024-10-10 22:34:43 +08:00
lifeadventurer
a48f7cc3ff Style: Lint markdown files 2024-10-10 22:34:26 +08:00
lifeadventurer
31752fda2a Impr(special): Add new special events for October 2024-10-01 01:03:34 +08:00
lifeadventurer
f983fd66ff Impr(quotes): Add 9 new quotes 2024-09-30 23:51:23 +08:00
lifeadventurer
eac16d697d CI: Remove stale GitHub Actions workflow 2024-09-25 16:50:11 +08:00
lifeadventurer
312e003ad2 Impr(special): Add new special events for September 2024-09-17 23:19:21 +08:00
lifeadventurer
d6f08a9a10 Docs: Clarify fortune type guidelines in CONTRIBUTING.md 2024-09-02 00:10:28 +08:00
lifeadventurer
1f160dd5de Docs: Update CONTRIBUTING.md 2024-09-01 01:25:30 +08:00
lifeadventurer
42dfbdc624 Impr(special): Add new special events for August 2024-08-23 00:26:15 +08:00
lifeadventurer
5acbf014f2 Impr(special): Add new special events for July 2024-08-22 19:29:12 +08:00
lifeadventurer
e9383ad17c Chore: Fix wrong path in docs/fortune_statistics.md 2024-08-22 14:33:30 +08:00
Moon
7773c94819 Create LICENSE 2024-07-01 23:33:26 +08:00
lifeadventurer
2261033611 Refactor: Replace substr with slice as substr is deprecated 2024-06-21 16:54:06 +08:00
lifeadventurer
a9668039ae Fix(fortune): Fix description showing undefined caused by negative seeds 2024-06-21 16:07:43 +08:00
lifeadventurer
4d097891e0 Fix(fortune): typo in scripts.js 2024-06-19 20:25:50 +08:00
lifeadventurer
b4d112693d Chore(fortune): Move function copyResultImageToClipboard and showCopiedNotice from fortune.js to scripts.js 2024-06-19 19:14:35 +08:00
lifeadventurer
8ac28e1d5c Feat(fortune): Chance to get different descriptions on the same event 2024-06-19 17:27:20 +08:00
lifeadventurer
29d910d06e Feat: Copied to clipboard notice and close #37 2024-06-17 22:05:47 +08:00
lifeadventurer
fa35f114be Fix: Incorrect special event date 2024-05-13 00:10:32 +08:00
lifeadventurer
eac44dc8db Impr(special): Add new special events for July 2024 2024-05-04 18:40:30 +08:00
lifeadventurer
950cdb531a Impr(special): Add new special events for June 2024 2024-05-04 17:55:58 +08:00
lifeadventurer
d7b46243b4 Impr(special): Add new special events for May 2024 2024-05-04 17:01:27 +08:00
lifeadventurer
d7c12a045b Fix: PWA cache not reloaded 2024-04-11 23:32:07 +08:00
lifeadventurer
3a9a17c319 Feat(fortune): Add new fortune events 2024-04-11 23:18:47 +08:00
lifeadventurer
116c505ba0 Feat: Show the copy button after clicking the getLuck button 2024-04-11 22:59:36 +08:00
lifeadventurer
b19f073a0f Create CODEOWNERS 2024-04-07 23:57:26 +08:00
lifeadventurer
74cf8dae86 Feat: Share fortune results as image and close #15
Co-authored-by: tobiichi3227 <cz1346219@gmail.com>
2024-04-07 23:52:23 +08:00
lifeadventurer
aa6f2e3166 Fix: Function daysDiff handling date bug 2024-03-31 00:07:17 +08:00
lifeadventurer
fad0f9d032 Fix: Gitroll scanned code smells 2024-03-21 22:30:02 +08:00
lifeadventurer
037beb020f Fix: Gitroll scanned bugs 2024-03-21 21:30:46 +08:00
lifeadventurer
3b7e205925 Fix: Special event feature is not functioning properly on IOS platforms and close #35 2024-03-21 15:03:26 +08:00
lifeadventurer
97a65aacce Impr(special): Add new special events for April 2024 2024-03-21 01:54:57 +08:00
lifeadventurer
a767fde4e6 impr: update fortune.json
avoid badFortunes events length equal to a power of two
2024-03-11 23:45:57 +08:00
lifeadventurer
8928828519 impr(special): add new special events for March 2024 2024-03-11 20:46:46 +08:00
lifeadventurer
ce66dd7228 fix: PWA manifest.json typo 2024-03-08 17:32:30 +08:00
tobiichi3227
ba0597f137 fix: pwa cache not reload (#34) 2024-03-08 17:27:00 +08:00
tobiichi3227
ace95c61f0 feat: change logo icon to rounded (#33)
Close #32
2024-03-07 23:03:08 +08:00
lifeadventurer
909eca1c9f chore: remove unnecessary Date() create
Co-authored-by: tobiichi3227 <cz1346219@gmail.com>
2024-03-07 22:51:31 +08:00
lifeadventurer
8b2a3c6e01 chore: organize docs/ and add dev/plot_gen.py 2024-03-07 22:45:13 +08:00
lifeadventurer
561fb6ef39 impr: replace previous statistics graph with a new one generated by plot_gen.py 2024-03-07 22:36:17 +08:00
lifeadventurer
98c3a8ee18 stlye: lint markdown 2024-03-07 22:04:10 +08:00
lifeadventurer
00519b53f2 chore: add .gitignore 2024-03-07 18:14:30 +08:00
tobiichi3227
92cd79a70f docs: add distribution statistics about fortune events (#29)
* docs: add docs about distribution of fortune item

* remove unused loop

* docs: add description of statistics method

* style: add comment and format code

* refactor: fortune_status_statistics.md

Use tables to better display statistical results

---------

Co-authored-by: Moon <108756201+LifeAdventurer@users.noreply.github.com>
2024-03-06 23:51:24 +08:00
lifeadventurer
9a402bde78 fix: pwa support install fail 2024-03-04 00:12:44 +08:00
lifeadventurer
7f610cb774 chore: update copyright to 2023-2024 2024-03-04 00:05:53 +08:00
tobiichi3227
ae666dfd50 docs: add docs about distribution of fortune (#28) 2024-03-01 22:43:58 +08:00
lifeadventurer
47a201e1d7 impr(quotes): add 7 new quotes 2024-02-20 21:09:35 +08:00
lifeadventurer
8e1c38e180 chore: add stale PR workflow 2024-02-19 14:07:34 +08:00
lifeadventurer
3adf03073b impr(special): add new special event 2024-01-25 23:53:32 +08:00
lifeadventurer
3347054a84 impr(quotes): add 5 new English quotes 2024-01-25 23:47:55 +08:00
lifeadventurer
f77441d37d refactor: special event sections 2023-12-31 21:44:44 +08:00
lifeadventurer
2127f28ab7 fix: daysDiff function calculate error 2023-12-31 21:04:08 +08:00
lifeadventurer
72e2d6d1b5 rename script.js to scripts.js 2023-12-31 20:58:22 +08:00
lifeadventurer
32c9f1b961 stlye: if-else statements 2023-12-30 22:26:30 +08:00
lifeadventurer
ad5a78a672 chore: put json files in folder and update fetch path 2023-12-28 22:31:39 +08:00
lifeadventurer
f95511c480 fix: fetch json file path error 2023-12-28 22:21:36 +08:00
lifeadventurer
83ef37a5d0 fix: styles.css path error in quote generator 2023-12-28 22:16:30 +08:00
lifeadventurer
a99fd4d831 chore: put json files under quote generator in folder 2023-12-28 22:12:27 +08:00
lifeadventurer
b7c910ae23 chore: delete .gitignore in quote generator 2023-12-28 22:11:07 +08:00
lifeadventurer
722215b86a impr(quotes): add 7 new quotes 2023-12-28 22:08:52 +08:00
lifeadventurer
93d0c3f3bf impr(special): add 3 special events 2023-12-28 00:17:54 +08:00
lifeadventurer
d777014ea7 feat: show the latest results nowadays
close #16

Co-authored-by: tobiichi3227 <cz1346219@gmail.com>
2023-12-25 21:14:17 +08:00
lifeadventurer
bd09fc1e19 feat: dark mode for generators gallery 2023-12-24 20:22:00 +08:00
lifeadventurer
98fa3db674 refactor: put all colors to :root 2023-12-24 17:04:21 +08:00
lifeadventurer
5c3148ac27 refactor: all files in folders 2023-12-24 16:08:41 +08:00
lifeadventurer
b16b4749bf chore: update file path 2023-12-24 15:55:22 +08:00
lifeadventurer
4d4d34392c refactor: organize files in folders 2023-12-24 15:51:55 +08:00
lifeadventurer
55b04e429a chore: remove useless file 2023-12-24 15:49:58 +08:00
lifeadventurer
3a11c5e320 chore: update web title 2023-12-23 23:44:27 +08:00
lifeadventurer
baf16ec368 impr: increase button font-size 2023-12-23 23:42:39 +08:00
lifeadventurer
eb02b8dbbf feat: add dark mode 2023-12-23 23:37:08 +08:00
lifeadventurer
00c38cc608 chore: remove unused color in styles.css 2023-12-23 23:28:57 +08:00
lifeadventurer
b4c97f5370 fix: button can't display well in small screen 2023-12-23 23:11:50 +08:00
lifeadventurer
4eb05b8e46 feat: add dark mode and close #10 2023-12-23 22:57:59 +08:00
lifeadventurer
ad1466bcb1 feat: replace color variables by class
including good fortune, bad-fortune, middle-fortune, desc, date-color
2023-12-23 17:04:45 +08:00
lifeadventurer
ddcede0143 feat: add colors to :root 2023-12-23 15:47:25 +08:00
lifeadventurer
c818817a3f feat: add dark mode for fortune generator 2023-12-23 14:00:47 +08:00
lifeadventurer
e32ed07a61 change all logos to rounded including web icon 2023-12-22 00:52:39 +08:00
lifeadventurer
678913a399 impr(quotes): add 9 new English quotes 2023-12-17 20:38:19 +08:00
lifeadventurer
c644573a7f Merge branch 'main' of https://github.com/LifeAdventurer/generators 2023-12-12 17:22:14 +08:00
lifeadventurer
e29c917625 fix: missing period in quotes.json 2023-12-10 23:34:02 +08:00
lifeadventurer
bd9789bbdd impr(quotes): add 10 new quotes 2023-12-10 23:26:13 +08:00
Moon
608448a88d Merge pull request #9 from tobiichi3227/main
fix: static file not pre cache
2023-12-07 23:33:59 +08:00
tobiichi3227
9aedcd7de3 Merge branch 'LifeAdventurer:main' into main 2023-12-07 23:30:09 +08:00
b8ba0f8206 fix: not pre cache static files
Co-authored-by: LifeAdventurer <life0adventurer@gmail.com>
2023-12-07 23:29:23 +08:00
Moon
f0ee4d8e85 Merge pull request #7 from tobiichi3227/main
fix offline cache failed
Co-authored-by: tobiichi3227 <cz1346219@gmail.com>
2023-12-07 23:20:39 +08:00
Moon
187b8ca54f docs: organize the comments
Co-authored-by: tobiichi3227 <cz1346219@gmail.com>
2023-12-07 23:13:16 +08:00
tobiichi3227
ecc21a63de Merge branch 'LifeAdventurer:main' into main 2023-12-07 22:56:23 +08:00
f76d2bf880 fix: offline cache failed
Co-authored-by: LifeAdventurer <life0adventurer@gmail.com>
2023-12-07 22:55:54 +08:00
Moon
7003d9c5b9 Merge pull request #6 from tobiichi3227/main
add offline mode support

Co-authored-by: tobiichi3227 <cz1346219@gmail.com>
2023-12-07 21:15:04 +08:00
tobiichi3227
eecc330409 Merge branch 'LifeAdventurer:main' into main 2023-12-07 21:00:19 +08:00
d15d9b30d3 feat: add offline status support 2023-12-07 20:59:34 +08:00
d6017207c6 pref: remove unused js library
Co-authored-by: LifeAdventurer <life0adventurer@gmail.com>
2023-12-07 19:35:49 +08:00
Moon
3b1ce611bd Merge pull request #4 from tobiichi3227/main
fix: cannot create files in Windows
Co-authored-by: tobiichi3227 <cz1346219@gmail.com>
2023-12-06 22:05:35 +08:00
4934ca5119 fix: cannot create files in windows
sorry moon
2023-12-06 22:02:13 +08:00
Moon
7e2e4cbb52 Merge pull request #3 from tobiichi3227/main
feat: add pwa support
2023-12-06 21:34:56 +08:00
a13092817d feat: add pwa support
Co-authored-by: LifeAdventurer <life0adventurer@gmail.com>
2023-12-06 21:32:54 +08:00
73 changed files with 6475 additions and 1203 deletions

1
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1 @@
* @LifeAdventurer @tobiichi3227

View File

@@ -0,0 +1,47 @@
name: Check fortune generator JSON files
on:
pull_request:
paths:
- 'fortune_generator/json/custom_special.json'
- 'fortune_generator/json/static_special.json'
- 'fortune_generator/json/cyclical_special.json'
- 'fortune_generator/json/fortune.json'
- 'fortune_generator/json/themes.json'
push:
paths:
- 'fortune_generator/json/custom_special.json'
- 'fortune_generator/json/static_special.json'
- 'fortune_generator/json/cyclical_special.json'
- 'fortune_generator/json/fortune.json'
- 'fortune_generator/json/themes.json'
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
- name: Check Custom Special Events
run: |
python3 scripts/check-events.py fortune_generator/json/custom_special.json custom
- name: Check Static Special Events
run: |
python3 scripts/check-events.py fortune_generator/json/static_special.json static
- name: Check Cyclical Special Events
run: |
python3 scripts/check-events.py fortune_generator/json/cyclical_special.json cyclical
- name: Check Fortune
run: |
python3 scripts/check-fortune.py fortune_generator/json/fortune.json
- name: Check Color Theme
run: |
python3 scripts/check-theme.py fortune_generator/json/themes.json

View File

@@ -11,7 +11,7 @@ on:
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
contents: write
pages: write
id-token: write
@@ -30,14 +30,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Upload artifact
uses: actions/upload-pages-artifact@v2
uses: actions/checkout@v4
- name: Write Commit Hash
run: |
cat << EOF | tee fortune_generator/json/commit_hash.json > /dev/null
{ "commit_hash": "$(git rev-parse HEAD)" }
EOF
- name: Deploy
uses: peaceiris/actions-gh-pages@v4
if: github.ref == 'refs/heads/main'
with:
# Upload entire repository
path: '.'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v2
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./
publish_branch: gh-pages

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
scripts/res.txt

52
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,52 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: local
hooks:
- id: check-cyclical-event
name: check-cyclical-event
entry: python3 scripts/check-events.py fortune_generator/json/cyclical_special.json cyclical
language: python
files: fortune_generator/json/cyclical_special.json
types: [json]
pass_filenames: false
- id: check-custom-event
name: check-custom-event
entry: python3 scripts/check-events.py fortune_generator/json/custom_special.json custom
language: python
files: fortune_generator/json/custom_special.json
types: [json]
pass_filenames: false
- id: check-static-event
name: check-static-event
entry: python3 scripts/check-events.py fortune_generator/json/static_special.json static
language: python
files: fortune_generator/json/static_special.json
types: [json]
pass_filenames: false
- id: check-fortune
name: check-fortune
entry: python3 scripts/check-fortune.py fortune_generator/json/fortune.json
language: python
files: fortune_generator/json/fortune.json
types: [json]
pass_filenames: false
- id: check-theme
name: check-theme
entry: python3 scripts/check-theme.py fortune_generator/json/themes.json
language: python
files: fortune_generator/json/themes.json
types: [json]
pass_filenames: false

View File

@@ -1,8 +1,163 @@
# Contributing
### Quote
## Fortune Generator
- Exclude content that includes any unlawful, defamatory, abusive, threatening or obscene text.
- Verify that your contribution meets JSON standards, specifically avoiding trailing comma at the end of a list.
### Fortune Events and Descriptions
1. Fortune Type:
- Good fortunes
- These should be added under the `"goodFortunes"` section in the JSON
file.
- Represent positive or beneficial events.
- Bad fortunes
- These should be added under the `"badFortunes"` section in the JSON file.
- Represent challenging or less favorable events.
2. Unique Content:
- Ensure your event and descriptions are original and not repeated in
existing entries.
3. Event Structure - Each fortune event should be added as new JSON object with
the following structure:
```json
{
"event": "Event Name",
"description": [
"Description 1",
"Description 2",
"Description 3",
"Description 4"
]
}
```
4. Maintain a positive and encouraging tone.
### Special Events
#### Date Structure
1. With year, month and date
```json
"triggerDate": {
"year": "Year",
"month": "Month",
"date": "Date"
}
```
We should place events of this type in the `fortune_generator/json/custom_special.json`.
For one-time or irregular events, or events with complex date calculations (like the Moon Festival in the lunar calendar).
**NOTE: Any special event that does not fit into either**
- Static events (fixed date every year)
- Cyclical events (recurring on a pattern like "fourth Thursday")
2. With only month and day
```json
"triggerDate": {
"month": "Month",
"date": "Date"
}
```
We should place events of this type in the `fortune_generator/json/static_special.json`.
For events with fixed dates.
3. With only month, week, weekday (like Mother's Day)
```json
"triggerDate": {
"month": "Month",
"week": "Week",
"weekday": "Weekday"
}
```
We should place events of this type in the `fortune_generator/json/cyclical_special.json`.
For recurring events (e.g., holidays like Thanksgiving and Mother's Day).
#### Event Structure
Special events require a more detailed structure.
1. Structure:
```json
{
"event": "Event Name",
"triggerDate": {}, // Please refer to explaination above
"status_index": "Status Index",
"goodFortunes": {
"l_1_event": "Good Fortune 1",
"l_1_desc": "Description 1",
"l_2_event": "Good Fortune 2",
"l_2_desc": "Description 2"
},
"badFortunes": {
"r_1_event": "Bad Fortune 1",
"r_1_desc": "Description 1",
"r_2_event": "Bad Fortune 2",
"r_2_desc": "Description 2"
}
}
```
2. Empty Fields: If there are no fortunes to add, leave the corresponding fields
as empty strings (`""`).
3. We support adding multiple special events on the same day,
and the hash function will determine which event will be shown for that day.
### Adding New Themes
#### JSON Theme Structure
When adding a new theme to `fortune_generator/json/themes.json`, follow this
structure:
```json
{
"name": "theme_name",
"properties": {
"bg-color": "#hexcode",
"good-fortune-color": "#hexcode",
"bad-fortune-color": "#hexcode",
"middle-fortune-color": "#hexcode",
"title-color": "#hexcode",
"desc-color": "#hexcode",
"button-color": "#hexcode",
"button-hover-color": "#hexcode",
"toggle-theme-button-color": "#hexcode",
"copy-result-button-color": "#hexcode",
"copy-preview-result-url-button-color": "#hexcode",
"date-color": "#hexcode",
"special-event-color": "#hexcode"
}
}
```
#### Guidelines for Adding Themes
1. Naming: Choose a unique and descriptive name for the theme.
2. Properties:
- Ensure that all property values are in valid hexadecimal format (`#rrggbb`
or `#rrggbbaa` for transparency).
- Hex Format: Use lowercase for all hex color codes for consistency.
- Make sure the colors have sufficient contrast for readability.
3. Consistency: Maintain a visually coherent set of colors.
4. Testing: Preview your theme in the app to confirm that colors display as
expected and are user-friendly.
5. Pull Request Naming:
- Use a clear PR name like `Impr(theme): Add {theme_name} theme`.
## Quote Generator
### Quotes
- Exclude content that includes any unlawful, defamatory, abusive, threatening
or obscene text.
- Verify that your contribution meets JSON standards, specifically avoiding
trailing comma at the end of a list.
- Ensure that the added quotes are not duplicates of any existing ones.
- Remember to name your pull request properly. For example, if you are adding new quotes, your pull request should be named `impr(quotes): add {count} new quotes`.
- Remember to name your pull request properly. For example, if you are adding
new quotes, your pull request should be named
`Impr(quotes): Add {count} new quotes`.

674
LICENSE Normal file
View File

@@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View File

@@ -1,11 +1,14 @@
# List of Generators
### [Quote_Generator](https://lifeadventurer.github.io/generators/quote_generator)
- Generate your daily quote with a button.
- Background with matrix animation when generating.
- If you want to contribute quotes, check the quote section in [CONTRIBUTING.md](./CONTRIBUTING.md#quote)
- If you want to contribute quotes, check the quote section in
[CONTRIBUTING.md](./CONTRIBUTING.md#quote)
### [Daily_Fortune_Generator](https://lifeadventurer.github.io/generators/fortune_generator)
- Generate your daily fortune with a generate button.
- Background with matrix animation when generating.
- Testing some features for an online judge.

View File

@@ -1,27 +1,29 @@
# Generators
<!-- ### **Table of Contents**
- [Generators](#generators)
- [**Table of Contents**](#table-of-contents)
- [Generators Gallery](#generators-gallery)
- [List of Generators](#list-of-generators)
- [Contribute](#contribute) -->
## Generators Gallery
Visit the [Generators Gallery](https://lifeadventurer.github.io/generators) to explore a collection of generators, each accompanied by a concise description, and with links to generators.
Visit the [Generators Gallery](https://lifeadventurer.github.io/generators) to
explore a collection of generators, each accompanied by a concise description,
and with links to generators.
## List of Generators
|Generators |Brief Description |
| ----------------------------------------------- | ------------------------------------------------------------ |
|**[Quote Generator][Quote Generator]** |Generate inspiring and thought-provoking quotes effortlessly. |
|**[Daily Fortune Generator][Fortune Generator]** |Get your daily fortune with just a click. |
For more in-depth information about each generator, refer to [LIST_OF_GENERATORS.md](./LIST_OF_GENERATORS.md)
| Generators | Brief Description |
| ------------------------------------------------ | ------------------------------------------------------------- |
| **[Quote Generator][Quote Generator]** | Generate inspiring and thought-provoking quotes effortlessly. |
| **[Daily Fortune Generator][Fortune Generator]** | Get your daily fortune with just a click. |
# Contribute
For more in-depth information about each generator, refer to
[LIST_OF_GENERATORS.md](./LIST_OF_GENERATORS.md)
## Contribute
Please refer to [CONTRIBUTING.md](./CONTRIBUTING.md)
[Quote Generator]: https://lifeadventurer.github.io/generators/quote_generator
[Fortune Generator]: https://lifeadventurer.github.io/generators/fortune_generator
## License
This project is licensed under the GNU General Public License v3.0 (GPL-3.0).
See the [LICENSE](./LICENSE) file for more details.

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1,29 @@
# Fortune Statistics
## Distribution of fortune
### 2000 IPs for 365 days, two groups in total
| Fortune Status | Percentage (1st time) | Percentage (2nd time) |
| -------------- | --------------------- | --------------------- |
| 大吉 | 20.33% | 20.30% |
| 中吉 | 14.37% | 14.34% |
| 小吉 | 10.57% | 10.59% |
| 吉 | 14.49% | 14.44% |
| 末吉 | 10.14% | 10.24% |
| 中平 | 14.29% | 14.40% |
| 凶 | 8.64% | 8.60% |
| 大凶 | 7.17% | 7.09% |
## Distribution statistics of daily fortune events
Statistical method: The sum of the number of fortune events that occurred for
2,000 random IPs on the same day.
The x-axis is the index value and the y-axis is the number of times.
| 宜 (Good Fortune) | 忌 (Bad Fortune) |
| ---------------------------------------------- | -------------------------------------------- |
| ![Good Fortune](./good_fortune_statistics.png) | ![Bad Fortune](./bad_fortune_statistics.png) |
[Statistics code](../dev/main.js)

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

View File

@@ -0,0 +1,184 @@
:root {
--button-color: #73a3eb;
--button-hover-color: #459aef;
--toggle-theme-button-color: #000000;
--copy-result-button-color: #000000;
--copy-preview-result-url-button-color: #000000;
--bg-color: #ffffff;
--good-fortune-color: #e74c3c;
--bad-fortune-color: #000000bf;
--middle-fortune-color: #5eb95e;
--desc-color: #7f7f7f;
--date-color: #096e1bc9;
--special-event-color: #3e4fbb;
--title-color: #000000cc;
}
* {
overflow: hidden;
text-align: center;
white-space: nowrap;
}
body {
margin: 0;
padding: 0;
height: 100%;
align-items: center;
justify-content: center;
}
.container {
top: 50%;
left: 50%;
width: 80%;
max-width: 800px;
position: absolute;
z-index: 1;
text-align: center;
transform: translate(-50%, -50%);
background-color: var(--bg-color);
border-radius: 40px;
padding: 10px;
}
.good-fortune {
color: var(--good-fortune-color) !important;
}
.bad-fortune {
color: var(--bad-fortune-color) !important;
}
.middle-fortune {
color: var(--middle-fortune-color) !important;
}
.desc {
color: var(--desc-color);
}
.date-color {
color: var(--date-color);
}
.title {
color: var(--title-color);
}
.special-event {
color: var(--special-event-color);
}
button {
background-color: var(--button-color);
color: var(--bg-color);
z-index: 2;
font-size: 20px;
border: none;
padding: 20px 20px;
border-radius: 30px;
cursor: pointer;
transition: all 0.3s ease-in-out;
}
button:hover {
background-color: var(--button-hover-color);
}
#Matrix {
z-index: 0;
}
#toggle-theme-button {
margin-top: 15px;
font-size: 2.4rem;
color: var(--toggle-theme-button-color);
cursor: pointer;
opacity: 85%;
}
#copy-result-button {
margin-top: 20px;
font-size: 2.2rem;
color: var(--copy-result-button-color);
}
#copy-preview-result-url-button {
margin-top: 20px;
font-size: 2.2rem;
color: var(--copy-preview-result-url-button-color);
}
#themeModal {
.modal-content {
background-color: var(--bg-color) !important;
color: var(--title-color) !important;
}
.modal-header,
.modal-footer {
background-color: var(--bg-color) !important;
color: var(--bg-color) !important;
}
.modal-title,
.btn-close {
color: var(--title-color) !important;
}
}
#themeItem {
background-color: var(--bg-color);
color: var(--title-color);
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-color);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: var(--button-color);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--button-hover-color);
}
.color-preview-container {
display: flex;
align-items: center;
padding: 3px;
border-radius: 25px;
}
.color-preview {
display: flex; /* Use flex to align dots in a row */
}
.color-dot {
display: inline-block;
width: 12px; /* Dot size */
height: 12px; /* Dot size */
border-radius: 50%; /* Circular shape */
margin-left: 5px; /* Spacing between dots */
}
.color-preview .color-dot:first-child {
margin-left: 0; /* No margin on the left for the first dot */
}
.home-button {
position: fixed;
top: 8px;
left: 8px;
z-index: 1000;
opacity: 0.8; /* Slightly transparent */
transition: opacity 0.3s;
}

View File

@@ -1,256 +0,0 @@
let ip;
$.getJSON("https://api.ipify.org?format=json", function(data) {
ip = data.ip;
})
let goodFortunes = [];
let badFortunes = [];
let special_events = [];
// using async and await to prevent fetching the data too late...
async function fetch_data(){
await fetch("fortune.json")
.then(response => response.json())
.then(data => {
goodFortunes = data.goodFortunes;
badFortunes = data.badFortunes;
})
await fetch("special.json")
.then(response => response.json())
.then(data => {
special_events = data.special_events;
})
}
// color adjust
const goodColor = "#e74c3c";
const badColor = "#000000bf";
const middleColor = "#5eb95e";
const descColor = "#7f7f7f";
const dateColor = "#096e1bC9";
const specialEventColor = "#3e4fbb";
const daystoSpecialEvent = "#485ccd";
const textColor = [goodColor, goodColor, goodColor, goodColor, goodColor, middleColor, badColor, badColor];
const fortuneStatus = ["大吉", "中吉", "小吉", "吉", "末吉", "中平", "凶", "大凶"];
const chineseMonth = ["一", "二", "三", "四", "五", "六", "七", "八", "九", "十", "十一", "十二"];
const week = ['日', '一', '二', '三', '四', '五', '六'];
const title = `<span style='font-size:8vmin; color:#000000CC;'><b>今日運勢</b></span>`;
const allGood = `<span style='font-size:6vmin; color:${badColor};'><b>萬事皆宜</b></span>`;
const allBad = `<span style='font-size:6vmin; color:${goodColor};'><b>諸事不宜</b></span>`;
// date
const d = new Date();
const date = d.getDate();
const day = d.getDay();
const month = d.getMonth() + 1;
const year = d.getFullYear();
function daysDiff(eventIndex){
// define the date right now and the special event date
const startDate = new Date(`${year}-${month}-${date}`);
const endDate = new Date(`${special_events[eventIndex].year}-${special_events[eventIndex].month}-${special_events[eventIndex].date}`);
// calculate the difference in milliseconds and convert it to days
const timeDiff = Math.floor((endDate - startDate) / (1000 * 60 * 60 * 24));
return timeDiff;
}
// pre-search jquery - save to a variable to improve performance
const J_l_1_event = $('#l-1-event');
const J_l_1_desc = $('#l-1-desc');
const J_l_2_event= $('#l-2-event');
const J_l_2_desc = $('#l-2-desc');
const J_r_1_event = $('#r-1-event');
const J_r_1_desc = $('#r-1-desc');
const J_r_2_event= $('#r-2-event');
const J_r_2_desc = $('#r-2-desc');
const J_ip_to_fortune = $('#ip-to-fortune');
let special = false;
let special_events_index = 0;
// init page
async function init_page(){
// fetch fortune.json and special.json
await fetch_data();
// hide the elements of show fortune page
$('#result-page').hide();
// show date before button pressed
const showMonth = `<span style='font-size:10vmin; color:${dateColor}; -webkit-writing-mode:vertical-lr;'><b>${chineseMonth[month - 1] + "月"}</b></span>`;
const showDate = `<span style='font-size:25vmin; color:${dateColor};'><b>${("0" + date).substr(-2)}</b></span>`;
const showDay = `<span style='font-size:10vmin; color:${dateColor}; -webkit-writing-mode:vertical-lr; margin-right:10%;'><b>${"星期" + week[day]}</b></span>`;
$('#month').html(showMonth);
$('#date').html(showDate);
$('#weekday').html(showDay);
let eventIndex_1 = -1, eventIndex_2 = -1;
// check if there is special event today
for(let i = 0; i < special_events.length; i++){
if(daysDiff(i) > 0){
if(eventIndex_1 == -1) eventIndex_1 = i;
else if(eventIndex_2 == -1) eventIndex_2 = i;
}
else if(daysDiff(i) == 0){
special = true;
special_events_index = i;
}
}
// if there is upcoming event then show
if(eventIndex_1 != -1){
let days = daysDiff(eventIndex_1);
let upcoming_event_1 = `<span style='font-size:5vmin; color:${descColor};'>距離<b style='color:${specialEventColor}'>${special_events[eventIndex_1].event}</b>還剩<b style='color:${daystoSpecialEvent}'>${days}</b>天</span>`;
$('#upcoming-event-1').html(upcoming_event_1);
}
if(eventIndex_2 != -1){
let days = daysDiff(eventIndex_2);
let upcoming_event_2 = `<span style='font-size:5vmin; color:${descColor};'>距離<b style='color:${specialEventColor}'>${special_events[eventIndex_2].event}</b>還剩<b style='color:${daystoSpecialEvent}'>${days}</b>天</span>`;
$('#upcoming-event-2').html(upcoming_event_2);
}
// show special event if today is a special day
if(special){
let special_event_today = `<span style='font-size:9vmin; color:${descColor};'>今日是<b style='color:${goodColor};'>${special_events[special_events_index].event}</b></span>`;
$('#special-day').html(special_event_today);
}
}
// event bar
const good_span = event => `<span style='font-size:5.6vmin; color:${goodColor};'><b>宜: </b>${event}</span>`;
const bad_span = event => `<span style='font-size:5.6vmin; color:${badColor};'><b>忌: </b>${event}</span>`;
const desc_span = desc => `<span style='font-size:3.5vmin; color:${descColor};'>${desc}</span>`;
function Appear() {
$('#title').html(title);
$('#btn').html('打卡成功');
// disable the btn
$('#btn').attr("disabled", "disabled");
//change page
$('#init-page').hide();
$('#result-page').show();
// transform ip to four numbers
let num = ip.split(".").map(num => parseInt(num));
// some lengths
const goodLen = goodFortunes.length;
const badLen = badFortunes.length;
const statusLen = fortuneStatus.length;
// TODO: improve the hash process
let hashDate = Math.round(Math.log10(year * ((month << (Math.log10(num[3]) + day - 1)) * (date << Math.log10(num[2] << day)))));
let seed1 = (num[0] >> hashDate) * (num[1] >> Math.min(hashDate, 2)) + (num[2] << 1) * (num[3] >> 3) + (date << 3) * (month << hashDate) + (year * day) >> 2;
let seed2 = (num[0] << (hashDate + 2)) * (num[1] << hashDate) + (num[2] << 1) * (num[3] << 3) + (date << (hashDate - 1)) * (month << 4) + year >> hashDate + (date * day) >> 1;
// decide the status
let status_index = ((seed1 + seed2) % statusLen + statusLen) % statusLen;
let status = `<span style='font-size:12vmin; color:${textColor[status_index]};'><b>§ ${fortuneStatus[status_index]} §</b></span>`;
if(special){
status_index = special_events[special_events_index].status_index;
let special_status = `<span style='font-size:12vmin; color:${textColor[status_index]};'><b>§ ${fortuneStatus[status_index]} §</b></span>`;
J_ip_to_fortune.html(special_status);
}
else{
J_ip_to_fortune.html(status);
}
// make sure the events won't collide
let set = new Set();
let l1 = (seed1 % goodLen + goodLen) % goodLen;
set.add(goodFortunes[l1].event);
let l2 = (((seed1 << 1) + date) % goodLen + goodLen) % goodLen;
while(set.has(goodFortunes[l2].event)){
l2 = (l2 + 1) % goodLen;
}
set.add(goodFortunes[l2].event);
let r1 = (((seed1 >> 1) + (d.getMonth() << 3)) % badLen + badLen) % badLen;
while(set.has(badFortunes[r1].event)){
r1 = (r1 + 2) % badLen;
}
set.add(badFortunes[r1].event);
let r2 = ((((((seed1 << 3) + (d.getFullYear() >> 5) * (date << 2)) % badLen) * seed2) >> 6) % badLen + badLen) % badLen;
while(set.has(badFortunes[r2].event)){
r2 = (r2 + 1) % badLen;
}
// organize the stuffs below this line...
let l_1_event = good_span(goodFortunes[l1].event);
let l_1_desc = desc_span(goodFortunes[l1].description);
let l_2_event = good_span(goodFortunes[l2].event);
let l_2_desc = desc_span(goodFortunes[l2].description);
let r_1_event = bad_span(badFortunes[r1].event);
let r_1_desc = desc_span(badFortunes[r1].description);
let r_2_event = bad_span(badFortunes[r2].event);
let r_2_desc = desc_span(badFortunes[r2].description);
if(special){
// instead clear variable name, use short variable name for here... cuz it's too repetitive
let Data = special_events[special_events_index];
if(status_index == 0){
J_r_1_event.html(allGood);
}
else{
J_r_1_event.html(bad_span(Data.badFortunes.r_1_event));
J_r_1_desc.html(desc_span(Data.badFortunes.r_1_desc));
J_r_2_event.html(bad_span(Data.badFortunes.r_2_event));
J_r_2_desc.html(desc_span(Data.badFortunes.r_2_desc));
if(Data.badFortunes.r_1_event.length == 0){
J_r_1_event.html(r_1_event);
J_r_1_desc.html(r_1_desc);
}
if(Data.badFortunes.r_2_event.length == 0){
J_r_2_event.html(r_2_event);
J_r_2_desc.html(r_2_desc);
}
}
if(status_index == statusLen - 1){
J_l_1_event.html(allBad);
}
else{
J_l_1_event.html(good_span(Data.goodFortunes.l_1_event));
J_l_1_desc.html(desc_span(Data.goodFortunes.l_1_desc));
J_l_2_event.html(good_span(Data.goodFortunes.l_2_event));
J_l_2_desc.html(desc_span(Data.goodFortunes.l_2_desc));
if(Data.goodFortunes.l_1_event.length == 0){
J_l_1_event.html(l_1_event);
J_l_1_desc.html(l_1_desc);
}
if(Data.goodFortunes.l_2_event.length == 0){
J_l_2_event.html(l_2_event);
J_l_2_desc.html(l_2_desc);
}
}
}
else{
if(status_index == 0){
J_r_1_event.html(allGood);
}
else{
J_r_1_event.html(r_1_event);
J_r_1_desc.html(r_1_desc);
J_r_2_event.html(r_2_event);
J_r_2_desc.html(r_2_desc);
}
if(status_index == statusLen - 1){
J_l_1_event.html(allBad);
}
else{
J_l_1_event.html(l_1_event);
J_l_1_desc.html(l_1_desc);
J_l_2_event.html(l_2_event);
J_l_2_desc.html(l_2_desc);
}
}
}
function getLuck() {
Update();
}
init_page();

View File

@@ -1,162 +0,0 @@
{
"goodFortunes" : [
{
"event": "睡覺",
"description": "品質良好,精神煥發"
},
{
"event": "做家務",
"description": "整潔使人心情愉悅"
},
{
"event": "冥想",
"description": "平靜心靈,緩解焦慮"
},
{
"event": "攝影",
"description": "捕捉到美好瞬間"
},
{
"event": "喝咖啡",
"description": "精力充沛燃燒脂肪"
},
{
"event": "朋友聚會",
"description": "充滿歡笑和美好回憶"
},
{
"event": "體育鍛鍊",
"description": "能量滿滿,效果顯著"
},
{
"event": "出遊" ,
"description": "好天氣,好心情"
},
{
"event": "吃大餐",
"description": "聯絡感情"
},
{
"event": "逛書店",
"description": "新書上架,打折推銷"
},
{
"event": "學新技能",
"description": "快速上手"
},
{
"event": "唱歌",
"description": "被星探發掘"
},
{
"event": "上課",
"description": "整天不累100% 消化"
},
{
"event": "洗澡",
"description": "重獲能量"
},
{
"event": "請教問題",
"description": "問題皆獲高人指點"
},
{
"event": "網購",
"description": "心儀商品皆促銷"
},
{
"event": "放假",
"description": "休息充電,明日再戰"
},
{
"event": "早睡",
"description": "好夢連連"
},
{
"event": "早起",
"description": "朝氣蓬勃,神采飛揚"
},
{
"event": "發文章",
"description": "瀏覽數暴增"
},
{
"event": "點外賣",
"description": "準時到達,新鮮好吃"
},
{
"event": "做善事",
"description": "積善成福"
},
{
"event": "散步",
"description": "空氣良好,放鬆身心"
}
],
"badFortunes" : [
{
"event": "體育鍛鍊",
"description": "不慎受傷"
},
{
"event": "攝影",
"description": "照片全消失"
},
{
"event": "出遊",
"description": "天氣不晴朗"
},
{
"event": "吃大餐",
"description": "被要求請客"
},
{
"event": "學新技能",
"description": "屢試不爽,始終不懂"
},
{
"event": "唱歌",
"description": "嗓子發炎"
},
{
"event": "洗澡",
"description": "水溫不穩"
},
{
"event": "請教問題",
"description": "疑難雜症,均無解答"
},
{
"event": "網購",
"description": "錯過促銷"
},
{
"event": "放假",
"description": "隔日工作量倍增"
},
{
"event": "晚睡",
"description": "失眠,明日精神渙散"
},
{
"event": "晚起",
"description": "整天都不順"
},
{
"event": "發文章",
"description": "搜索枯腸,不知所云"
},
{
"event": "點外賣",
"description": "路況壅塞,餐點冷掉"
},
{
"event": "喝咖啡",
"description": "晚上失眠"
},
{
"event": "散步",
"description": "被害蟲咬傷"
}
]
}

View File

@@ -1,93 +1,187 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Daily Fortune Generator</title>
<link rel="icon" href="../images/lifeadventurer.jpg">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<script src="https://unpkg.com/vue@3.3.8/dist/vue.global.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<link rel="stylesheet" href="./styles.css">
</head>
<body>
<div class="container">
<div class="row">
<p id="title"></p>
</div>
<!-- init page start -->
<div id="init-page">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Daily Fortune Generator</title>
<link rel="icon" href="../images/lifeadventurer_rounded_logo.png" />
<link rel="manifest" href="./manifest.json" />
<!-- bootstrap -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous"
/>
<script
src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"
></script>
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"
rel="stylesheet"
/>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"
integrity="sha512-7tWCgq9tTYS/QkGVyKrtLpqAoMV9XIUxoou+sPUypsaZx56cYR/qio84fPK9EvJJtKvJEwt7vkn6je5UVzGevw=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<link rel="stylesheet" href="./css/styles.css" />
<script>
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("./js/service-worker.js");
}
</script>
</head>
<body>
<!-- Home button at the top -->
<a href="../" class="btn border-0 home-button">
<i class="fas fa-home text-white"></i>
</a>
<div class="container">
<div class="row">
<div class="col-3">
<p id="month"></p>
<p id="title"></p>
</div>
<!-- init page start -->
<div id="init-page">
<div class="row">
<div class="col-3">
<p id="month"></p>
</div>
<div class="col-6">
<p id="date"></p>
</div>
<div class="col-3">
<p id="weekday"></p>
</div>
</div>
<div class="col-6">
<p id="date"></p>
<div class="row">
<div class="col">
<p id="special-day"></p>
</div>
</div>
<div class="col-3">
<p id="weekday"></p>
<div class="row">
<p id="upcoming-event-1"></p>
</div>
<div class="row">
<p id="upcoming-event-2"></p>
</div>
</div>
<div class="row">
<div class="col">
<p id="special-day"></p>
<!-- init page end -->
<!-- page after button clicked start -->
<div id="result-page">
<div class="row">
<p id="ip-to-fortune"></p>
</div>
<div class="row">
<div class="col">
<div class="row">
<p id="l-1-event"></p>
</div>
<div class="row">
<p id="l-1-desc"></p>
</div>
</div>
<div class="col">
<div class="row">
<p id="r-1-event"></p>
</div>
<div class="row">
<p id="r-1-desc"></p>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="row">
<p id="l-2-event"></p>
</div>
<div class="row">
<p id="l-2-desc"></p>
</div>
</div>
<div class="col">
<div class="row">
<p id="r-2-event"></p>
</div>
<div class="row">
<p id="r-2-desc"></p>
</div>
</div>
</div>
</div>
<!-- page after button click end -->
<div class="row">
<p id="upcoming-event-1"></p>
</div>
<div class="row">
<p id="upcoming-event-2"></p>
<i
class="col-2 fas fa-palette"
id="toggle-theme-button"
data-bs-toggle="modal"
data-bs-target="#themeModal"
></i>
<button class="col-4 offset-2" id="btn" onclick="getLuck()">
點擊打卡
</button>
<i
class="offset-md-1 col-md-1 col-2 fas fa-link d-none"
id="copy-preview-result-url-button"
onclick="copyPreviewResultUrlToClipboard()"
></i>
<i
class="col-2 fas fa-clone d-none"
id="copy-result-button"
onclick="copyResultImageToClipboard()"
></i>
</div>
</div>
<!-- init page end -->
<!-- page after button clicked start -->
<div id="result-page">
<div class="row">
<p id="ip-to-fortune"></p>
</div>
<div class="row">
<div class="col">
<div class="row">
<p id="l-1-event"></p>
<!-- Theme Modal -->
<div
class="modal fade"
id="themeModal"
tabindex="-1"
aria-labelledby="themeModalLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="themeModalLabel">Choose Theme</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
>
</button>
</div>
<div class="row">
<p id="l-1-desc"></p>
</div>
</div>
<div class="col">
<div class="row">
<p id="r-1-event"></p>
</div>
<div class="row">
<p id="r-1-desc"></p>
</div>
</div>
</div>
<div class="row">
<div class="col">
<div class="row">
<p id="l-2-event"></p>
</div>
<div class="row">
<p id="l-2-desc"></p>
</div>
</div>
<div class="col">
<div class="row">
<p id="r-2-event"></p>
</div>
<div class="row">
<p id="r-2-desc"></p>
<div class="modal-body" style="max-height: 70vh; overflow-y: auto">
<ul class="list-group" id="themeList">
<!-- Theme items will be dynamically populated here -->
</ul>
</div>
</div>
</div>
</div>
<!-- page after button click end -->
<button id="btn" onclick="getLuck()">點擊打卡</button>
</div>
<canvas id="Matrix"></canvas>
<script src="./fortune.js"></script>
<script src="./matrix.js"></script>
</body>
<canvas id="Matrix"></canvas>
<script src="./js/scripts.js"></script>
<script src="./js/fortune.js"></script>
<script src="./js/matrix.js"></script>
<script src="./js/theme.js"></script>
<script
src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"
integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"
crossorigin="anonymous"
></script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"
integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy"
crossorigin="anonymous"
></script>
</body>
</html>

View File

@@ -0,0 +1,627 @@
let ip = null;
fetch("https://api.ipify.org?format=json").then((response) => {
if (response.ok) {
return response.json();
}
throw new Error("Network response was not ok.");
}).then((res) => {
ip = res.ip;
}).catch((_error) => {
if ("caches" in window) {
caches.match("https://api.ipify.org?format=json").then((response) => {
if (response) {
return response.json();
}
}).then((data) => {
if (ip === null && data !== undefined) {
ip = JSON.parse(data).ip;
}
});
}
});
let goodFortunes = [];
let badFortunes = [];
let special_events = [];
let commit_hash = "";
// using async and await to prevent fetching the data too late...
async function fetch_data(requied_commit_hash) {
let prefix = "";
if (requied_commit_hash) {
prefix = `https://raw.githubusercontent.com/LifeAdventurer/generators/${requied_commit_hash}/fortune_generator/`;
}
await fetch(`${prefix}./json/fortune.json`)
.then((response) => response.json())
.then((data) => {
goodFortunes = data.goodFortunes;
badFortunes = data.badFortunes;
});
await fetch('./json/commit_hash.json')
.then((response) => response.json())
.then((data) => {
commit_hash = data.commit_hash;
});
async function fetch_events(path) {
await fetch(path)
.then((response) => response.json())
.then((data) => {
special_events.push(...data.special_events);
});
}
await fetch_events(`${prefix}./json/custom_special.json`);
await fetch_events(`${prefix}./json/static_special.json`);
await fetch_events(`${prefix}./json/cyclical_special.json`);
}
const textColorClass = [
"good-fortune",
"good-fortune",
"good-fortune",
"good-fortune",
"good-fortune",
"middle-fortune",
"bad-fortune",
"bad-fortune",
];
const fortuneStatus = [
"大吉",
"中吉",
"小吉",
"吉",
"末吉",
"中平",
"凶",
"大凶",
];
const chineseMonth = [
"一",
"二",
"三",
"四",
"五",
"六",
"七",
"八",
"九",
"十",
"十一",
"十二",
];
const week = ["日", "一", "二", "三", "四", "五", "六"];
const title =
`<span class="title" style="font-size:8vmin;"><b>今日運勢</b></span>`;
const allGood =
`<span class="bad-fortune" style="font-size:6vmin;"><b>萬事皆宜</b></span>`;
const allBad =
`<span class="good-fortune" style="font-size:6vmin;"><b>諸事不宜</b></span>`;
// date
const d = new Date();
const date = d.getDate();
const day = d.getDay();
const month = d.getMonth() + 1;
const year = d.getFullYear();
function validateNumber(value, min, max, fieldName, event) {
value = parseInt(value);
if (isNaN(value) || value < min || value > max) {
console.warn(
`illegal event: ${fieldName} should be between ${min} and ${max}`,
event,
);
return null;
}
return value;
}
function isLeapYear(year) {
if (year % 400 === 0) return true;
if (year % 100 === 0) return false;
if (year % 4 === 0) return true;
return false;
}
const daysPerMonth = [
0,
31,
28,
31,
30,
31,
30,
31,
31,
30,
31,
30,
31,
];
const maxDate = new Date(8640000000000000);
function daysDiff(eventIndex) {
// define the date right now and the special event date
const event = special_events[eventIndex];
const startDate = new Date(year, month - 1, date);
let eventYear = -1, eventMonth = -1, eventDate = -1;
if (!("triggerDate" in event)) {
console.warn("illegal event: missing `triggerDate` field", event);
return -1;
} else if (
Object.prototype.toString.call(event.triggerDate) !== "[object Object]"
) {
console.warn(
"illegal event: `triggerDate` field should be a json object",
event,
);
return -1;
}
const triggerDate = event.triggerDate;
let isCustomEvent = false;
eventYear = year;
if ("year" in triggerDate) {
eventYear = validateNumber(
triggerDate.year,
1,
maxDate.getFullYear(),
"triggerDate.year",
event,
);
if (eventYear === null) {
return -1;
}
isCustomEvent = true;
}
if (!("month" in triggerDate)) {
console.warn("illegal event: `triggerDate` missing `month` field", event);
return -1;
}
eventMonth = validateNumber(
triggerDate.month,
1,
12,
"triggerDate.Month",
event,
);
if (eventMonth === null) {
return -1;
}
if (
!("date" in triggerDate) &&
(!("week" in triggerDate) || !("weekday" in triggerDate))
) {
console.warn(
"illegal event: `triggerDate` require (`week` and `weekday`) or `date` field",
event,
);
return -1;
}
if ("date" in triggerDate) {
let days = daysPerMonth[eventMonth];
if (isLeapYear(eventYear) && eventMonth == 2) days += 1;
eventDate = validateNumber(
triggerDate.date,
1,
days,
"triggerDate.date",
event,
);
if (eventDate === null) {
return -1;
}
} else {
triggerDate.week = validateNumber(
triggerDate.week,
1,
5,
"triggerDate.week",
event,
);
triggerDate.weekday = validateNumber(
triggerDate.weekday,
1,
7,
"triggerDate.weekday",
event,
);
if (triggerDate.week === null || triggerDate.weekday === null) {
return -1;
}
const firstDayOfMonth = new Date(eventYear, eventMonth - 1, 1);
const firstDayWeekday = firstDayOfMonth.getDay();
// Sunday -> 7
const adjustedFirstDayWeekday = firstDayWeekday === 0 ? 7 : firstDayWeekday;
const firstTargetDay = 1 +
(triggerDate.weekday - adjustedFirstDayWeekday + 7) % 7;
eventDate = firstTargetDay + (triggerDate.week - 1) * 7;
}
if (
!isCustomEvent &&
(month > eventMonth || (month == eventMonth && date > eventDate))
) {
eventYear += 1;
}
const endDate = new Date(
eventYear,
eventMonth - 1,
eventDate,
);
// calculate the difference in milliseconds and convert it to days
const timeDiff = Math.ceil((endDate - startDate) / (1000 * 60 * 60 * 24));
return timeDiff;
}
// pre-search jquery - save to a variable to improve performance
const J_l_1_event = $("#l-1-event");
const J_l_1_desc = $("#l-1-desc");
const J_l_2_event = $("#l-2-event");
const J_l_2_desc = $("#l-2-desc");
const J_r_1_event = $("#r-1-event");
const J_r_1_desc = $("#r-1-desc");
const J_r_2_event = $("#r-2-event");
const J_r_2_desc = $("#r-2-desc");
const J_ip_to_fortune = $("#ip-to-fortune");
let special = false;
let special_events_index = -1;
let l1 = -1, l2 = -1, r1 = -1, r2 = -1;
let status_index = -1;
let seed1 = -1, seed2 = -1;
let fortune_generated = false;
let preview_result = false;
let current_day_special_events = [];
// init page
async function init_page() {
let urlParams = new URLSearchParams(window.location.search);
let commit_hash = null;
if (urlParams.has('fi') && urlParams.has('si') && urlParams.has('ei'), urlParams.has('ch')) { // fortune_index, status_index, event_index, commit_hash
status_index = parseInt(urlParams.get('si'));
special_events_index = parseInt(urlParams.get('ei'));
[l1, l2, r1, r2] = urlParams.get('fi').split(':').map(num => parseInt(num));
commit_hash = urlParams.get('ch');
if (isNaN(status_index) || isNaN(special_events_index) || isNaN(l1) || isNaN(l2) || isNaN(r1) || isNaN(r2)) {
special_events_index = -1;
l1 = -1, l2 = -1, r1 = -1, r2 = -1;
status_index = -1;
commit_hash = null;
} else {
preview_result = true;
if (special_events_index != -1) special = true;
}
}
// fetch data from `fortune.json`
await fetch_data(commit_hash);
// hide the elements of show fortune page
$("#result-page").hide();
// show date before button pressed
const showMonth =
`<span class="date-color" style="font-size:10vmin; -webkit-writing-mode:vertical-lr;"><b>${
chineseMonth[month - 1] + "月"
}</b></span>`;
const showDate = `<span class="date-color" style="font-size:25vmin;"><b>${
("0" + date).slice(-2)
}</b></span>`;
const showDay =
`<span class="date-color" style="font-size:10vmin; -webkit-writing-mode:vertical-lr; margin-right:10%;"><b>${
"星期" + week[day]
}</b></span>`;
$("#month").html(showMonth);
$("#date").html(showDate);
$("#weekday").html(showDay);
if (preview_result) Appear();
if (!preview_result) {
const showSpecialEventCount = 2;
let eventIndexList = Array(showSpecialEventCount).fill(-1);
let eventDiffDaysIndexList = Array(showSpecialEventCount).fill(
Number.MAX_SAFE_INTEGER,
);
// check if there is special event today
for (let i = 0; i < special_events.length; i++) {
let diffCount = daysDiff(i);
if (diffCount > 0) {
let j = 0;
for (; j < showSpecialEventCount; j++) {
if (diffCount < eventDiffDaysIndexList[j]) {
break;
}
}
eventDiffDaysIndexList[j] = diffCount;
eventIndexList[j] = i;
} else if (diffCount === 0) {
special = true;
current_day_special_events.push(i);
}
}
// if there is upcoming event then show
for (let eventIndex = 0; eventIndex < showSpecialEventCount; eventIndex++) {
if (eventIndexList[eventIndex] != -1) {
const days = daysDiff(eventIndexList[eventIndex]);
const upcoming_event =
`<span class="desc" style="font-size:5vmin;">距離<b class="special-event">${
special_events[eventIndexList[eventIndex]].event
}</b>還剩<b class="special-event">${days}</b>天</span>`;
$(`#upcoming-event-${eventIndex + 1}`).html(upcoming_event);
}
}
const last_date_str = localStorage.getItem("last_date");
if (last_date_str !== null && last_date_str !== undefined) {
const now_date = new Date();
const last_date = new Date(last_date_str);
if (
now_date.getFullYear() === last_date.getFullYear() &&
now_date.getMonth() === last_date.getMonth() &&
now_date.getDate() === last_date.getDate()
) {
fortune_generated = true;
}
}
if (fortune_generated) {
Update();
if (special) {
special_events_index = parseInt(localStorage.getItem("last_special_index"));
}
} else {
if (current_day_special_events.length) {
special_events_index = ip.split(".").map(num => parseInt(num)).reduce((acc, cur) => acc + cur);
special_events_index %= current_day_special_events.length;
special_events_index = current_day_special_events[special_events_index];
localStorage.setItem("last_special_index", special_events_index);
}
}
// show special event if today is a special day
if (special) {
const special_event_today =
`<span class="desc" style="font-size:9vmin;">今日是<b class="good-fortune">${
special_events[special_events_index].event
}</b></span>`;
$("#special-day").html(special_event_today);
}
}
}
// event bar
const good_span = (event) =>
`<span class="good-fortune" style="font-size:5.6vmin;"><b>宜: </b>${event}</span>`;
const bad_span = (event) =>
`<span class="bad-fortune" style="font-size:5.6vmin;"><b>忌: </b>${event}</span>`;
const desc_span = (desc) =>
`<span class="desc" style="font-size:3.5vmin;">${desc}</span>`;
function Appear() {
$("#title").html(title);
$("#btn").html("打卡成功");
// disable the btn
$("#btn").attr("disabled", "disabled");
//change page
$("#init-page").hide();
$("#result-page").show();
// some lengths
const goodLen = goodFortunes.length;
const badLen = badFortunes.length;
const statusLen = fortuneStatus.length;
if (!fortune_generated && !preview_result) {
// transform ip to four numbers
const num = ip.split(".").map((num) => parseInt(num));
// TODO: improve the hash process
const hashDate = Math.round(
Math.log10(
year *
((month << (Math.log10(num[3]) + day - 1)) *
(date << Math.log10(num[2] << day))),
),
);
seed1 = (num[0] >> hashDate) * (num[1] >> Math.min(hashDate, 2)) +
(num[2] << 1) * (num[3] >> 3) + (date << 3) * (month << hashDate) +
(year * day) >> 2;
seed2 = (num[0] << (hashDate + 2)) * (num[1] << hashDate) +
(num[2] << 1) * (num[3] << 2) +
(date << (hashDate - 1)) * (month << 4) + year >>
hashDate + (date * day) >> 1;
// decide the status
let seedMagic = 0;
if (seed1 > seed2) {
seedMagic = (seed1 ^ seed2) +
parseInt(seed1.toString().split("").reverse().join(""));
} else if (seed1 < seed2) {
let collatzLen = 0;
let temp = Math.abs(seed1 - seed2);
while (temp !== 1) {
temp = temp % 2 === 0 ? temp / 2 : 3 * temp + 1;
collatzLen++;
}
seedMagic = collatzLen + seed2.toString(2).replace(/0/g, "").length;
} else {
seedMagic = seed1 + seed2;
}
status_index = (seedMagic % statusLen + statusLen) % statusLen;
// update last record
localStorage.setItem("last_date", d.toISOString());
localStorage.setItem("last_status_index", status_index.toString());
localStorage.setItem("last_seed1", seed1.toString());
localStorage.setItem("last_seed2", seed2.toString());
} else if (!preview_result) {
status_index = parseInt(localStorage.getItem("last_status_index"));
seed1 = parseInt(localStorage.getItem("last_seed1"));
seed2 = parseInt(localStorage.getItem("last_seed2"));
}
const status = `<span class=${
textColorClass[status_index]
} style="font-size:12vmin;"><b>§ ${fortuneStatus[status_index]} §</b></span>`;
if (special) {
status_index = special_events[special_events_index].status_index;
const special_status = `<span class=${
textColorClass[status_index]
} style="font-size:12vmin;"><b>§ ${
fortuneStatus[status_index]
} §</b></span>`;
J_ip_to_fortune.html(special_status);
} else {
J_ip_to_fortune.html(status);
}
// make sure the events won't collide
if (!preview_result) {
const set = new Set();
l1 = (seed1 % goodLen + goodLen) % goodLen;
set.add(goodFortunes[l1].event);
l2 = (((seed1 << 1) + date) % goodLen + goodLen) % goodLen;
while (set.has(goodFortunes[l2].event)) {
l2 = (l2 + 1) % goodLen;
}
set.add(goodFortunes[l2].event);
r1 =
(((seed1 >> 2) + ((month * 42 + year) << 3 + 3) + 19) % badLen + badLen) %
badLen;
if (
r1 == 0 &&
(Math.abs(seed1) % 2 === Math.abs(seed2) % 2 || seed1 % 2 === 0 ||
seed2 % 3 === 1)
) {
r1 = (r1 + (Math.abs(seed1 - seed2) % 100) >> 4) % badLen;
}
while (set.has(badFortunes[r1].event)) {
r1 = (r1 + 7) % badLen;
}
set.add(badFortunes[r1].event);
r2 = (((((seed1 << 3 + 7) + (year >> 5) * (date << 2 + 3)) *
seed2) >> 4 + seed2 % 42) % badLen + badLen) % badLen;
if (
r2 == 0 &&
(Math.abs(seed1) % 3 % 2 === Math.abs(seed2) % 3 % 2 ||
seed1 % 3 === seed2 % 2 || (month % 3 === 1 && year % 2 === 1) ||
month % 4 === 3 || date % 7 === 2)
) {
r2 = ((r2 - (Math.abs(seed1 + seed2) % 10) >> 1) % badLen + badLen) %
badLen;
}
while (set.has(badFortunes[r2].event)) {
r2 = (r2 + 17) % badLen;
}
}
// organize the stuffs below this line...
const l1_desc_list = goodFortunes[l1].description;
const l2_desc_list = goodFortunes[l2].description;
const r1_desc_list = badFortunes[r1].description;
const r2_desc_list = badFortunes[r2].description;
const l_1_event = good_span(goodFortunes[l1].event);
const l_1_desc = desc_span(
l1_desc_list[Math.abs(seed1) % l1_desc_list.length],
);
const l_2_event = good_span(goodFortunes[l2].event);
const l_2_desc = desc_span(
l2_desc_list[Math.abs(seed2) % l2_desc_list.length],
);
const r_1_event = bad_span(badFortunes[r1].event);
const r_1_desc = desc_span(
r1_desc_list[Math.abs(seed1) % r1_desc_list.length],
);
const r_2_event = bad_span(badFortunes[r2].event);
const r_2_desc = desc_span(
r2_desc_list[Math.abs(seed2) % r2_desc_list.length],
);
if (special) {
// instead clear variable name, use short variable name for here... cuz it's too repetitive
const Data = special_events[special_events_index];
if (status_index == 0) {
J_r_1_event.html(allGood);
} else {
J_r_1_event.html(bad_span(Data.badFortunes.r_1_event));
J_r_1_desc.html(desc_span(Data.badFortunes.r_1_desc));
J_r_2_event.html(bad_span(Data.badFortunes.r_2_event));
J_r_2_desc.html(desc_span(Data.badFortunes.r_2_desc));
if (Data.badFortunes.r_1_event.length == 0) {
J_r_1_event.html(r_1_event);
J_r_1_desc.html(r_1_desc);
}
if (Data.badFortunes.r_2_event.length == 0) {
J_r_2_event.html(r_2_event);
J_r_2_desc.html(r_2_desc);
}
}
if (status_index == statusLen - 1) {
J_l_1_event.html(allBad);
} else {
J_l_1_event.html(good_span(Data.goodFortunes.l_1_event));
J_l_1_desc.html(desc_span(Data.goodFortunes.l_1_desc));
J_l_2_event.html(good_span(Data.goodFortunes.l_2_event));
J_l_2_desc.html(desc_span(Data.goodFortunes.l_2_desc));
if (Data.goodFortunes.l_1_event.length == 0) {
J_l_1_event.html(l_1_event);
J_l_1_desc.html(l_1_desc);
}
if (Data.goodFortunes.l_2_event.length == 0) {
J_l_2_event.html(l_2_event);
J_l_2_desc.html(l_2_desc);
}
}
} else {
if (status_index == 0) {
J_r_1_event.html(allGood);
} else {
J_r_1_event.html(r_1_event);
J_r_1_desc.html(r_1_desc);
J_r_2_event.html(r_2_event);
J_r_2_desc.html(r_2_desc);
}
if (status_index == statusLen - 1) {
J_l_1_event.html(allBad);
} else {
J_l_1_event.html(l_1_event);
J_l_1_desc.html(l_1_desc);
J_l_2_event.html(l_2_event);
J_l_2_desc.html(l_2_desc);
}
}
$("#copy-result-button").removeClass("d-none");
$("#copy-preview-result-url-button").removeClass("d-none");
}
function copyPreviewResultUrlToClipboard() {
let baseUrl = location.href.split("?")[0];
let url = `${baseUrl}?si=${status_index}&ei=${special_events_index}&fi=${[l1,l2,r1,r2].join(":")}&ch=${commit_hash.substr(0, 7)}`;
navigator.clipboard.writeText(url);
showCopiedNotice();
}
function getLuck() {
Update();
}
init_page();

View File

@@ -0,0 +1,51 @@
const canvas = document.getElementById("Matrix");
const context = canvas.getContext("2d");
canvas.height = globalThis.innerHeight + 100;
canvas.width = globalThis.innerWidth + 5;
const chars =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./*-+#$%^@!~?><:;[]{}=_αβΓγΔδεζηΘθικΛλμΞξΠπρΣσςτυΦφχΨψΩω×≦≧≠∞≒≡∩∠∟⊿∫∮∵∴¥〒¢£℃€℉╩◢ⅨⅧⅦⅥⅣⅢⅡあいうえおがぎぐげござじずぜぞだぢつでづどにぬのばひぴぶへぺぼみゃょァゐゎè";
const fontSize = 16;
const columns = canvas.width / fontSize;
const charArr = [];
for (let i = 0; i < columns; i++) {
charArr[i] = 1;
}
let frame = 0;
let str;
context.fillStyle = "rgba(0, 0, 0, 1)";
context.fillRect(0, 0, canvas.width, canvas.height);
function Update() {
context.fillStyle = "rgba(0, 0, 0, 0.05)";
context.fillRect(0, 0, canvas.width, canvas.height);
if (frame == 0) {
const a = parseInt(Math.random() * 255);
str = `rgba(${a}, ${Math.abs(a - 127)}, ${Math.abs(a - 255)}, 0.9)`;
}
context.fillStyle = str;
context.font = fontSize + "px monospace";
for (let i = 0; i < columns; i++) {
const text = chars[Math.floor(Math.random() * chars.length)];
context.fillText(text, i * fontSize, charArr[i] * fontSize);
if (charArr[i] * fontSize > canvas.height && Math.random() > 0.90) {
charArr[i] = 0;
}
charArr[i]++;
}
frame++;
if (frame <= 40 * (Math.floor(Math.random() * 10) + 3)) {
requestAnimationFrame(Update); // 40 frames a cycle
} else {
frame = 0;
Appear();
}
}

View File

@@ -0,0 +1,47 @@
function copyResultImageToClipboard() {
try {
const $title = $("#title").clone().wrap('<div class="row"></div>');
$("#result-page").prepend($title.parent());
const backgroundColor =
getComputedStyle($(".container")[0]).backgroundColor;
htmlToImage.toBlob($("#result-page")[0], {
skipFonts: true,
preferredFontFormat: "woff2",
backgroundColor: backgroundColor, // Set background color dynamically
}).then((blob) => {
navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
showCopiedNotice();
$title.parent().remove();
}).catch((error) => {
console.error("Error converting result page to image:", error);
$title.parent().remove();
});
} catch (error) {
console.error("Error copying result image to clipboard:", error);
}
}
function showCopiedNotice() {
const notice = $("<div>", {
text: "Copied to clipboard!",
css: {
position: "fixed",
bottom: "20px",
right: "20px",
padding: "10px 20px",
backgroundColor: "rgba(0, 0, 0, 0.7)",
color: "#fff",
borderRadius: "5px",
zIndex: 1000,
},
});
$("body").append(notice);
setTimeout(() => {
notice.fadeOut(300, () => {
notice.remove();
});
}, 3000);
}

View File

@@ -0,0 +1,106 @@
const pre_cache_file_version = "pre-v1.1.0";
const auto_cache_file_version = "auto-v1.1.0";
const ASSETS = [
"/generators/images/lifeadventurer-192x192.png",
"/generators/images/lifeadventurer-512x512.png",
"/generators/images/lifeadventurer-180x180.png",
"/generators/images/lifeadventurer-270x270.png",
"/generators/images/lifeadventurer.jpg",
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css",
"https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js",
];
const NEED_UPDATE = [
"/generators/fortune_generator/",
"/generators/fortune_generator/index.html",
"/generators/fortune_generator/css/styles.css",
"/generators/fortune_generator/js/fortune.js",
"/generators/fortune_generator/js/matrix.js",
"/generators/fortune_generator/json/custom_special.json",
"/generators/fortune_generator/json/cyclical_special.json",
"/generators/fortune_generator/json/static_special.json",
"/generators/fortune_generator/json/fortune.json",
"/generators/fortune_generator/json/manifest.json",
"https://api.ipify.org/?format=json",
];
const limit_cache_size = (name, size) => {
caches.open(name).then((cache) => {
cache.keys().then((keys) => {
if (keys.length > size) {
cache.delete(keys[0]).then(() => {
limit_cache_size(name, size);
});
}
});
});
};
const is_in_array = (str, array) => {
let path = "";
// Check the request's domain is the same as the current domain.
if (str.indexOf(self.origin) === 0) {
path = str.substring(self.origin.length); // Remove https://lifeadventurer.github.io
} else {
path = str; // outside request
}
return array.indexOf(path) > -1;
};
// install
self.addEventListener("install", (event) => {
self.skipWaiting();
//pre-cache files
event.waitUntil(
caches.open(pre_cache_file_version).then((cache) => {
cache.addAll(ASSETS);
}),
);
});
// activate
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((keys) => {
return Promise.all(keys.map((key) => {
if (
pre_cache_file_version.indexOf(key) === -1 &&
auto_cache_file_version.indexOf(key) === -1
) {
return caches.delete(key);
}
}));
}),
);
});
// fetch event
self.addEventListener("fetch", (event) => {
if (is_in_array(event.request.url, ASSETS)) {
// cache only strategy
event.respondWith(
caches.match(event.request.url),
);
} else if (is_in_array(event.request.url, NEED_UPDATE)) {
event.respondWith(
fetch(event.request.url).then(async (response) => {
if (response.ok) {
const cache = await caches.open(auto_cache_file_version);
cache.put(event.request.url, response.clone());
return response;
}
throw new Error("Network response was not ok.");
}).catch(async (_error) => {
const cache = await caches.open(auto_cache_file_version);
return cache.match(event.request.url);
}),
);
}
});

View File

@@ -0,0 +1,91 @@
document.addEventListener("DOMContentLoaded", () => {
const themeListContainer = document.querySelector("#themeList");
const root = document.documentElement;
// Apply the saved theme if it exists
applySavedTheme();
async function fetchThemes() {
try {
const response = await fetch("./json/themes.json");
const themes = await response.json();
populateThemeList(themes["themes"]);
} catch (error) {
console.error("Error fetching themes:", error);
}
}
// Populate theme list in modal
function populateThemeList(themes) {
themeListContainer.innerHTML = "";
themes.forEach((theme) => {
const themeItem = document.createElement("div");
themeItem.className =
"theme-item list-group-item d-flex justify-content-between align-items-center";
themeItem.style.cursor = "pointer";
themeItem.id = "themeItem";
// Add theme name
const themeName = document.createElement("span");
themeName.textContent = theme.name;
themeItem.appendChild(themeName);
const colorPreivewContainer = document.createElement("div");
colorPreivewContainer.className = "color-preview-container";
const propertyKeys = Object.keys(theme.properties);
colorPreivewContainer.style.backgroundColor =
theme.properties[propertyKeys[5]];
// Add color dots for visual preview
const colorPreview = document.createElement("div");
colorPreview.className = "color-preview";
Object.values(theme.properties).slice(0, 3).forEach((color) => {
const colorDot = document.createElement("span");
colorDot.style.backgroundColor = color;
colorDot.className = "color-dot";
colorPreview.appendChild(colorDot);
});
colorPreivewContainer.appendChild(colorPreview);
themeItem.appendChild(colorPreivewContainer);
// Apply theme on click
themeItem.addEventListener("click", () => {
applyTheme(theme.properties);
saveThemeToLocalStorage(theme.name);
});
themeListContainer.appendChild(themeItem);
});
}
// Apply theme by setting CSS variables
function applyTheme(properties) {
Object.entries(properties).forEach(([key, value]) => {
root.style.setProperty(`--${key}`, value);
});
}
function saveThemeToLocalStorage(themeName) {
localStorage.setItem("selectedTheme", themeName);
}
function applySavedTheme() {
const savedThemeName = localStorage.getItem("selectedTheme");
if (savedThemeName) {
fetch("./json/themes.json")
.then((response) => response.json())
.then((themes) => {
const theme = themes.themes.find((t) => t.name === savedThemeName);
if (theme) {
applyTheme(theme.properties);
}
})
.catch((error) => console.error("Error fetching themes:", error));
}
}
fetchThemes();
});

View File

@@ -0,0 +1,67 @@
{
"special_events": [
{
"event": "夏至",
"triggerDate": {
"year": "2025",
"month": "6",
"date": "21"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "觀賞日出和日落",
"l_1_desc": "享受一年最長的白天",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "中秋節",
"triggerDate": {
"year": "2025",
"month": "10",
"date": "6"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "賞月",
"l_1_desc": "與家人一同賞月,增進感情",
"l_2_event": "吃月餅",
"l_2_desc": "與家人朋友分享月餅的美味"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "冬至",
"triggerDate": {
"year": "2025",
"month": "12",
"date": "21"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "吃湯圓",
"l_1_desc": "團團圓圓",
"l_2_event": "保暖",
"l_2_desc": "冬至到了"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
}
]
}

View File

@@ -0,0 +1,46 @@
{
"special_events": [
{
"event": "母親節",
"triggerDate": {
"month": "5",
"week": "2",
"weekday": "7"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "家庭聚餐",
"l_1_desc": "表達對媽媽的感恩之心",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "感恩節",
"triggerDate": {
"month": "11",
"week": "4",
"weekday": "4"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "家人團聚",
"l_1_desc": "分享寶貴時光",
"l_2_event": "吃火雞大餐",
"l_2_desc": "Happy Thanksgiving!"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
}
]
}

View File

@@ -0,0 +1,364 @@
{
"goodFortunes": [
{
"event": "做家務",
"description": [
"整潔使人心情愉悅",
"增加運動量",
"培養責任感",
"增加成就感"
]
},
{
"event": "冥想",
"description": [
"平靜心靈,緩解焦慮",
"調節情緒",
"改善睡眠",
"提高專注",
"減輕壓力"
]
},
{
"event": "攝影",
"description": [
"捕捉到美好瞬間",
"激發想像力"
]
},
{
"event": "喝咖啡",
"description": [
"精力充沛",
"燃燒脂肪"
]
},
{
"event": "朋友聚會",
"description": [
"充滿歡笑和美好回憶",
"提升情感連結",
"緩解壓力"
]
},
{
"event": "體育鍛鍊",
"description": [
"能量滿滿,效果顯著",
"塑造身材",
"增強心肺功能"
]
},
{
"event": "出遊",
"description": [
"好天氣,好心情"
]
},
{
"event": "吃大餐",
"description": [
"聯絡感情"
]
},
{
"event": "逛書店",
"description": [
"新書上架,打折推銷"
]
},
{
"event": "學新技能",
"description": [
"快速上手"
]
},
{
"event": "唱歌",
"description": [
"被星探發掘"
]
},
{
"event": "上課",
"description": [
"整天不累100% 消化"
]
},
{
"event": "洗澡",
"description": [
"重獲能量",
"身心舒暢"
]
},
{
"event": "請教問題",
"description": [
"問題皆獲高人指點"
]
},
{
"event": "網購",
"description": [
"心儀商品皆促銷"
]
},
{
"event": "放假",
"description": [
"休息充電,明日再戰",
"減輕壓力",
"探索新興趣"
]
},
{
"event": "早睡",
"description": [
"好夢連連",
"調整生物鐘",
"減少壓力",
"提高免疫力",
"改善皮膚",
"提升工作效率"
]
},
{
"event": "早起",
"description": [
"朝氣蓬勃,神采飛揚"
]
},
{
"event": "發文章",
"description": [
"瀏覽數暴增",
"增加影響力",
"促進交流"
]
},
{
"event": "點外賣",
"description": [
"準時到達",
"新鮮好吃",
"減少清理"
]
},
{
"event": "做善事",
"description": [
"積善成福",
"助人為樂",
"培養同理心",
"心靈充實",
"增加幸福感"
]
},
{
"event": "散步",
"description": [
"空氣良好",
"放鬆身心"
]
},
{
"event": "觀星",
"description": [
"欣賞星空",
"享受寧靜"
]
},
{
"event": "野餐",
"description": [
"在大自然中享受美食"
]
},
{
"event": "釣魚",
"description": [
"收穫滿滿"
]
},
{
"event": "烹飪",
"description": [
"陶冶情操",
"廚藝提升",
"養成飲食習慣"
]
},
{
"event": "爬山",
"description": [
"挑戰自我",
"促進健康"
]
},
{
"event": "逛街",
"description": [
"買到心儀的物品",
"發現新奇事物",
"心情愉快"
]
},
{
"event": "看電影",
"description": [
"增加話題",
"與朋友同樂",
"放鬆心情"
]
},
{
"event": "聽音樂會",
"description": [
"增加藝術氣息",
"放鬆身心, 享受音樂"
]
}
],
"badFortunes": [
{
"event": "體育鍛鍊",
"description": [
"不慎受傷"
]
},
{
"event": "攝影",
"description": [
"照片全消失"
]
},
{
"event": "出遊",
"description": [
"天氣不晴朗"
]
},
{
"event": "吃大餐",
"description": [
"被要求請客"
]
},
{
"event": "學新技能",
"description": [
"屢試不爽,始終不懂"
]
},
{
"event": "唱歌",
"description": [
"嗓子發炎"
]
},
{
"event": "洗澡",
"description": [
"水溫不穩"
]
},
{
"event": "請教問題",
"description": [
"疑難雜症,均無解答"
]
},
{
"event": "網購",
"description": [
"錯過促銷"
]
},
{
"event": "放假",
"description": [
"隔日工作量倍增"
]
},
{
"event": "晚睡",
"description": [
"失眠,明日精神渙散"
]
},
{
"event": "晚起",
"description": [
"整天都不順"
]
},
{
"event": "發文章",
"description": [
"搜索枯腸,不知所云"
]
},
{
"event": "點外賣",
"description": [
"路況壅塞,餐點冷掉"
]
},
{
"event": "喝咖啡",
"description": [
"晚上失眠"
]
},
{
"event": "散步",
"description": [
"被害蟲咬傷"
]
},
{
"event": "吃冰",
"description": [
"受寒感冒",
"咳嗽不止"
]
},
{
"event": "爬山",
"description": [
"遇到地震...",
"不幸受傷"
]
},
{
"event": "觀星",
"description": [
"光害嚴重",
"烏雲密布"
]
},
{
"event": "野餐",
"description": [
"被害蟲咬傷",
"天氣不晴朗"
]
},
{
"event": "看電影",
"description": [
"場場爆滿",
"被旁人打擾",
"劇情大失所望"
]
},
{
"event": "烹飪",
"description": [
"缺少食材,口味不佳",
"小心燙傷"
]
}
]
}

View File

@@ -0,0 +1,884 @@
{
"special_events": [
{
"event": "元旦",
"triggerDate": {
"month": "1",
"date": "1"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "早起",
"l_1_desc": "心情愉悅迎接新年",
"l_2_event": "大掃除",
"l_2_desc": "新年新氣象"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "世界邏輯日",
"triggerDate": {
"month": "1",
"date": "14"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "思維訓練",
"l_1_desc": "提高自身邏輯能力",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "陷入死胡同",
"r_1_desc": "記得適當休息",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "國際擁抱日",
"triggerDate": {
"month": "1",
"date": "21"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "擁抱親朋好友",
"l_1_desc": "讓愛流動,增進情感連結",
"l_2_event": "送上擁抱",
"l_2_desc": "透過擁抱表達支持與愛意"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "國際資料隱私日",
"triggerDate": {
"month": "1",
"date": "28"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "整理資料",
"l_1_desc": "注意在線資料安全",
"l_2_event": "注意隱私",
"l_2_desc": "謹慎上網"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "世界濕地日",
"triggerDate": {
"month": "2",
"date": "2"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "參與保護濕地活動",
"l_1_desc": "重視濕地,參與保護",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "世界癌症日",
"triggerDate": {
"month": "2",
"date": "4"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "認識癌症",
"l_1_desc": "知道癌症並不可怕",
"l_2_event": "宣導健康生活",
"l_2_desc": "健康飲食運動,預防癌症"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "世界和平日",
"triggerDate": {
"month": "2",
"date": "5"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "參加和平遊行",
"l_1_desc": "支持和平,傳遞非暴力",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "國際比薩日",
"triggerDate": {
"month": "2",
"date": "9"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "品嚐各式比薩",
"l_1_desc": "共享比薩,樂享時光",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "國際氣象節",
"triggerDate": {
"month": "2",
"date": "10"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "了解氣候變化",
"l_1_desc": "認識氣候變遷,保護地球家園",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "世界社會正義日",
"triggerDate": {
"month": "2",
"date": "20"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "提升社會意識",
"l_1_desc": "關注不平等,參與正義行動",
"l_2_event": "支持弱勢群體",
"l_2_desc": "投身於公益事業,支持弱勢族群"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "國際母語日",
"triggerDate": {
"month": "2",
"date": "21"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "傳播母語",
"l_1_desc": "延續母語的使用,保護文化傳承",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "和平紀念日",
"triggerDate": {
"month": "2",
"date": "28"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "參與和平紀念活動",
"l_1_desc": "緬懷歷史,尊重和平的價值",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "植樹節",
"triggerDate": {
"month": "3",
"date": "12"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "植樹造林",
"l_1_desc": "保護生態、美化環境",
"l_2_event": "節能減碳",
"l_2_desc": "延長資源壽命"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "白色情人節",
"triggerDate": {
"month": "3",
"date": "14"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "送禮物",
"l_1_desc": "表達愛意和感激之情",
"l_2_event": "觀星",
"l_2_desc": "仰望星空,共描明月"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "國際數學日",
"triggerDate": {
"month": "3",
"date": "14"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "",
"l_1_desc": "",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "世界森林日",
"triggerDate": {
"month": "3",
"date": "21"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "環境教育",
"l_1_desc": "提升對自然的敬重",
"l_2_event": "節約用水",
"l_2_desc": "保護生態系統穩定"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "愚人節",
"triggerDate": {
"month": "4",
"date": "1"
},
"status_index": "3",
"goodFortunes": {
"l_1_event": "喜笑顏開",
"l_1_desc": "與親朋好友分享快樂",
"l_2_event": "開派對",
"l_2_desc": "組織有趣的活動和遊戲"
},
"badFortunes": {
"r_1_event": "冒犯他人",
"r_1_desc": "避免製造觸怒人的笑話",
"r_2_event": "惡作劇",
"r_2_desc": "注意避免不必要的麻煩"
}
},
{
"event": "兒童節",
"triggerDate": {
"month": "4",
"date": "4"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "喜笑顏開",
"l_1_desc": "與親朋好友分享快樂",
"l_2_event": "開派對",
"l_2_desc": "組織有趣的活動和遊戲"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "世界健康日",
"triggerDate": {
"month": "4",
"date": "7"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "健康飲食",
"l_1_desc": "多攝取水果、蔬菜和全穀食品",
"l_2_event": "運動鍛煉",
"l_2_desc": "保持身體健康和活力"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "世界地球日",
"triggerDate": {
"month": "4",
"date": "22"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "環保行動",
"l_1_desc": "參與植樹造林或垃圾回收等環保行動",
"l_2_event": "節能減排",
"l_2_desc": "選擇環保型交通工具"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "世界閱讀日",
"triggerDate": {
"month": "4",
"date": "23"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "推廣閱讀",
"l_1_desc": "激發對知識的渴望",
"l_2_event": "書籍分享",
"l_2_desc": "與他人分享你的書單"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "世界智慧財產權日",
"triggerDate": {
"month": "4",
"date": "26"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "保護創意",
"l_1_desc": "尊重他人的創意和智慧財產權,共同維護創作人的權益",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "星際大戰日",
"triggerDate": {
"month": "5",
"date": "04"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "電影馬拉松",
"l_1_desc": "播放所有星際大戰電影",
"l_2_event": "感受原力",
"l_2_desc": "May the force be with you, always."
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "世界微笑日",
"triggerDate": {
"month": "5",
"date": "08"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "微笑",
"l_1_desc": "用微笑向世界問好",
"l_2_event": "放慢腳步",
"l_2_desc": "觀察四周的美好事物"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "世界環境日",
"triggerDate": {
"month": "6",
"date": "05"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "少用塑膠",
"l_1_desc": "選擇可重複使用的替代品",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "世界獻血者日",
"triggerDate": {
"month": "6",
"date": "14"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "捐血",
"l_1_desc": "捐出血液和血漿,分享生命要時常",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "巧克力日",
"triggerDate": {
"month": "7",
"date": "07"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "送巧克力",
"l_1_desc": "共享巧克力盛宴",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "宅宅日",
"triggerDate": {
"month": "7",
"date": "13"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "觀影",
"l_1_desc": "看心愛的電影或影集",
"l_2_event": "閱讀",
"l_2_desc": "享受片刻的寧靜"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "國際冷笑話日",
"triggerDate": {
"month": "7",
"date": "24"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "講冷笑話",
"l_1_desc": "一起嘻嘻哈哈",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "國際友誼日",
"triggerDate": {
"month": "7",
"date": "30"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "與朋友聯絡",
"l_1_desc": "回憶美好時光",
"l_2_event": "一起出遊",
"l_2_desc": "增進彼此的感情"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "國際左撇子日",
"triggerDate": {
"month": "8",
"date": "13"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "挑戰新事物",
"l_1_desc": "嘗試用左手完成任務",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "世界攝影日",
"triggerDate": {
"month": "8",
"date": "19"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "拍攝照片",
"l_1_desc": "捕捉生活中的美好瞬間",
"l_2_event": "分享作品",
"l_2_desc": "展示您的攝影技巧"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "國際狗狗日",
"triggerDate": {
"month": "8",
"date": "26"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "陪伴狗狗",
"l_1_desc": "帶狗狗散步或遊玩",
"l_2_event": "分享作品",
"l_2_desc": "展示您的攝影技巧"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "國際慈善日",
"triggerDate": {
"month": "9",
"date": "5"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "捐贈物資",
"l_1_desc": "捐贈物資或金錢,幫助有需要的人",
"l_2_event": "參與志願活動",
"l_2_desc": "參加社區慈善活動,提升社會貢獻"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "國際和平日",
"triggerDate": {
"month": "9",
"date": "21"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "分享愛心",
"l_1_desc": "與他人分享關懷與愛心,促進和平",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "教師節",
"triggerDate": {
"month": "9",
"date": "28"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "感謝老師",
"l_1_desc": "向老師表達感謝,增進師生情誼",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "世界糧食日",
"triggerDate": {
"month": "10",
"date": "16"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "節約糧食",
"l_1_desc": "支持可持續的食物系統",
"l_2_event": "捐贈食品",
"l_2_desc": "捐贈食物給有需要的人,傳遞愛心"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "聯合國日",
"triggerDate": {
"month": "10",
"date": "24"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "支持和平",
"l_1_desc": "參與促進世界和平的活動",
"l_2_event": "了解國際事務",
"l_2_desc": "增強全球視野"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "萬聖節",
"triggerDate": {
"month": "10",
"date": "31"
},
"status_index": "4",
"goodFortunes": {
"l_1_event": "扮演角色",
"l_1_desc": "穿上喜愛的角色服裝,享受萬聖節的氛圍",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "忽略安全",
"r_2_desc": "活動時忽視安全措施可能帶來風險"
}
},
{
"event": "世界善心日",
"triggerDate": {
"month": "11",
"date": "13"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "善待他人",
"l_1_desc": "在生活中多一些善意與寬容",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "棉花糖日",
"triggerDate": {
"month": "12",
"date": "07"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "吃棉花糖",
"l_1_desc": "慶祝棉花糖日",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "貓奴日",
"triggerDate": {
"month": "12",
"date": "15"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "嚕貓",
"l_1_desc": "撫平傷心的心情",
"l_2_event": "喝咖啡",
"l_2_desc": "到貓咪咖啡店去喝咖啡"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "平安夜",
"triggerDate": {
"month": "12",
"date": "24"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "除舊佈新",
"l_1_desc": "平安祥和",
"l_2_event": "交換禮物",
"l_2_desc": "獲得真心的祝福"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "聖誕節",
"triggerDate": {
"month": "12",
"date": "25"
},
"status_index": "0",
"goodFortunes": {
"l_1_event": "家庭聚會",
"l_1_desc": "一起團圓吃火雞大餐",
"l_2_event": "注意保暖",
"l_2_desc": "冬至到了"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
}
]
}

View File

@@ -0,0 +1,346 @@
{
"themes": [
{
"name": "Classic Light",
"properties": {
"bg-color": "#ffffff",
"good-fortune-color": "#e74c3c",
"bad-fortune-color": "#000000bf",
"middle-fortune-color": "#5eb95e",
"title-color": "#000000cc",
"desc-color": "#7f7f7f",
"button-color": "#73a3eb",
"button-hover-color": "#459aef",
"toggle-theme-button-color": "#000000",
"copy-result-button-color": "#000000",
"copy-preview-result-url-button-color": "#000000",
"date-color": "#096e1bc9",
"special-event-color": "#3e4fbb"
}
},
{
"name": "Classic Dark",
"properties": {
"bg-color": "#1e1d24",
"good-fortune-color": "#e74c3c",
"bad-fortune-color": "#d4d4d4d9",
"middle-fortune-color": "#57c857",
"title-color": "#cdcdcd",
"desc-color": "#838282",
"button-color": "#5d99f4",
"button-hover-color": "#9ac6f1",
"toggle-theme-button-color": "#ffffff",
"copy-result-button-color": "#ffffff",
"copy-preview-result-url-button-color": "#ffffff",
"date-color": "#0ed64aed",
"special-event-color": "#6477f3"
}
},
{
"name": "Catppuccin Dark",
"properties": {
"bg-color": "#1e1e2e",
"good-fortune-color": "#94e2d5",
"bad-fortune-color": "#f38ba8",
"middle-fortune-color": "#f9e2af",
"title-color": "#f5c2e7",
"desc-color": "#cdd6f4",
"button-color": "#b9fbc0",
"button-hover-color": "#a6e3a1",
"toggle-theme-button-color": "#f9e2af",
"copy-result-button-color": "#f38ba8",
"copy-preview-result-url-button-color": "#f38ba8",
"date-color": "#f5c2e7",
"special-event-color": "#fab387"
}
},
{
"name": "Tokyo Night",
"properties": {
"bg-color": "#1a1b26",
"good-fortune-color": "#7dcfff",
"bad-fortune-color": "#ff5c8d",
"middle-fortune-color": "#e0af68",
"title-color": "#bb9af7",
"desc-color": "#c0caf5",
"button-color": "#6e6f8c",
"button-hover-color": "#5a5b7f",
"toggle-theme-button-color": "#7dcfff",
"copy-result-button-color": "#ff5c8d",
"copy-preview-result-url-button-color": "#ff5c8d",
"date-color": "#ffbb93",
"special-event-color": "#f7768e"
}
},
{
"name": "Spring Blossom",
"properties": {
"bg-color": "#f7f5f2",
"good-fortune-color": "#ff6f61",
"bad-fortune-color": "#6d597a",
"middle-fortune-color": "#86a77a",
"title-color": "#ff9f80",
"desc-color": "#5a5a5a",
"button-color": "#ffd166",
"button-hover-color": "#ffb347",
"toggle-theme-button-color": "#ff6f61",
"copy-result-button-color": "#6d597a",
"copy-preview-result-url-button-color": "#6d597a",
"date-color": "#ff9f80",
"special-event-color": "#ffb6b9"
}
},
{
"name": "Sunny Vibes",
"properties": {
"bg-color": "#fffbeb",
"good-fortune-color": "#ff7e67",
"bad-fortune-color": "#ffcc29",
"middle-fortune-color": "#1fab89",
"title-color": "#ff8c42",
"desc-color": "#4a4a4a",
"button-color": "#ffa41b",
"button-hover-color": "#ff8500",
"toggle-theme-button-color": "#34ace0",
"copy-result-button-color": "#ffcc29",
"copy-preview-result-url-button-color": "#ffcc29",
"date-color": "#ffd32a",
"special-event-color": "#f7b731"
}
},
{
"name": "Autumn Glow",
"properties": {
"bg-color": "#f4ede4",
"good-fortune-color": "#e27d60",
"bad-fortune-color": "#a23b2b",
"middle-fortune-color": "#c7a17a",
"title-color": "#5a3d31",
"desc-color": "#7a6e61",
"button-color": "#c7a17a",
"button-hover-color": "#b9896e",
"toggle-theme-button-color": "#a74c3c",
"copy-result-button-color": "#7a6e61",
"copy-preview-result-url-button-color": "#7a6e61",
"date-color": "#e6b89c",
"special-event-color": "#e8a87c"
}
},
{
"name": "Winter Wonderland",
"properties": {
"bg-color": "#e3f2fd",
"good-fortune-color": "#74b9ff",
"bad-fortune-color": "#6c5ce7",
"middle-fortune-color": "#81ecec",
"title-color": "#0984e3",
"desc-color": "#636e72",
"button-color": "#74b9ff",
"button-hover-color": "#a29bfe",
"toggle-theme-button-color": "#00b894",
"copy-result-button-color": "#0984e3",
"copy-preview-result-url-button-color": "#0984e3",
"date-color": "#74b9ff",
"special-event-color": "#fdcb6e"
}
},
{
"name": "Moonlit Night",
"properties": {
"bg-color": "#1a1a2e",
"good-fortune-color": "#7ed6df",
"bad-fortune-color": "#ffeaa7",
"middle-fortune-color": "#8c94a6",
"title-color": "#ffffff",
"desc-color": "#a4b0be",
"button-color": "#30336b",
"button-hover-color": "#535c88",
"toggle-theme-button-color": "#7ed6df",
"copy-result-button-color": "#ffeaa7",
"copy-preview-result-url-button-color": "#ffeaa7",
"date-color": "#dcdde1",
"special-event-color": "#ff9ff3"
}
},
{
"name": "Lunar Eclipse",
"properties": {
"bg-color": "#0d0e1a",
"good-fortune-color": "#7289da",
"bad-fortune-color": "#b56576",
"middle-fortune-color": "#a0a8c1",
"title-color": "#e1e1e6",
"desc-color": "#8b8b97",
"button-color": "#494e6b",
"button-hover-color": "#646b8a",
"toggle-theme-button-color": "#7289da",
"copy-result-button-color": "#b56576",
"copy-preview-result-url-button-color": "#b56576",
"date-color": "#d4d4dc",
"special-event-color": "#ffb86c"
}
},
{
"name": "Galactic Glow",
"properties": {
"bg-color": "#1b1d2a",
"good-fortune-color": "#00eaff",
"bad-fortune-color": "#ff5555",
"middle-fortune-color": "#ffe347",
"title-color": "#ffe81f",
"desc-color": "#c4c7d1",
"button-color": "#3b3f58",
"button-hover-color": "#52577a",
"toggle-theme-button-color": "#00eaff",
"copy-result-button-color": "#ff5555",
"copy-preview-result-url-button-color": "#ff5555",
"date-color": "#9aedfe",
"special-event-color": "#ffa07a"
}
},
{
"name": "Mystic Forest",
"properties": {
"bg-color": "#1c3b24",
"good-fortune-color": "#a1e887",
"bad-fortune-color": "#d94e3b",
"middle-fortune-color": "#83c5a3",
"title-color": "#e4f9e0",
"desc-color": "#b5c9b4",
"button-color": "#4a7a58",
"button-hover-color": "#6a9a76",
"toggle-theme-button-color": "#a1e887",
"copy-result-button-color": "#d94e3b",
"copy-preview-result-url-button-color": "#d94e3b",
"date-color": "#e4f9e0",
"special-event-color": "#9fd9b7"
}
},
{
"name": "Vintage Sepia",
"properties": {
"bg-color": "#f5e9da",
"good-fortune-color": "#d4a373",
"bad-fortune-color": "#8b5e3c",
"middle-fortune-color": "#c3a593",
"title-color": "#3f312b",
"desc-color": "#736357",
"button-color": "#a67a5b",
"button-hover-color": "#b98b6f",
"toggle-theme-button-color": "#8b5e3c",
"copy-result-button-color": "#d4a373",
"copy-preview-result-url-button-color": "#d4a373",
"date-color": "#7f6a5d",
"special-event-color": "#c7ab93"
}
},
{
"name": "Metallic Shine",
"properties": {
"bg-color": "#2c2f33",
"good-fortune-color": "#4e8c47",
"bad-fortune-color": "#d9534f",
"middle-fortune-color": "#f1c40f",
"title-color": "#bdc3c7",
"desc-color": "#95a5a6",
"button-color": "#3498db",
"button-hover-color": "#2980b9",
"toggle-theme-button-color": "#4e8c47",
"copy-result-button-color": "#d9534f",
"copy-preview-result-url-button-color": "#d9534f",
"date-color": "#bdc3c7",
"special-event-color": "#f39c12"
}
},
{
"name": "Tropical Paradise",
"properties": {
"bg-color": "#ffdf80",
"good-fortune-color": "#00bfae",
"bad-fortune-color": "#ff6347",
"middle-fortune-color": "#3eb489",
"title-color": "#2e8b57",
"desc-color": "#708090",
"button-color": "#ff4500",
"button-hover-color": "#ff6347",
"toggle-theme-button-color": "#00bfae",
"copy-result-button-color": "#ff6347",
"copy-preview-result-url-button-color": "#ff6347",
"date-color": "#2e8b57",
"special-event-color": "#ff8c00"
}
},
{
"name": "Abstract Art",
"properties": {
"bg-color": "#f4e1d2",
"good-fortune-color": "#e1b1e3",
"bad-fortune-color": "#c93f36",
"middle-fortune-color": "#bde7e0",
"title-color": "#cf63a1",
"desc-color": "#7b6362",
"button-color": "#fc7b6d",
"button-hover-color": "#fc4f48",
"toggle-theme-button-color": "#e1b1e3",
"copy-result-button-color": "#c93f36",
"copy-preview-result-url-button-color": "#c93f36",
"date-color": "#cf63a1",
"special-event-color": "#f6c6d4"
}
},
{
"name": "Zen Garden",
"properties": {
"bg-color": "#f4f1e1",
"good-fortune-color": "#8c9f6f",
"bad-fortune-color": "#e18e8b",
"middle-fortune-color": "#b7c7b5",
"title-color": "#4f5049",
"desc-color": "#78756f",
"button-color": "#c1c0b2",
"button-hover-color": "#b0b098",
"toggle-theme-button-color": "#8c9f6f",
"copy-result-button-color": "#e18e8b",
"copy-preview-result-url-button-color": "#e18e8b",
"date-color": "#4f5049",
"special-event-color": "#b7c7b5"
}
},
{
"name": "Aurora Borealis",
"properties": {
"bg-color": "#0b0c1d",
"good-fortune-color": "#00d084",
"bad-fortune-color": "#455a64",
"middle-fortune-color": "#9c7ae0",
"title-color": "#d9e9f0",
"desc-color": "#95a5b3",
"button-color": "#608fcf",
"button-hover-color": "#5072b3",
"toggle-theme-button-color": "#00d084",
"copy-result-button-color": "#455a64",
"copy-preview-result-url-button-color": "#455a64",
"date-color": "#cfd8dc",
"special-event-color": "#a1d6ff"
}
},
{
"name": "Cyberwave",
"properties": {
"bg-color": "#1a1a2e",
"good-fortune-color": "#00f5d4",
"bad-fortune-color": "#293462",
"middle-fortune-color": "#adff2f",
"title-color": "#f8f8ff",
"desc-color": "#adb5bd",
"button-color": "#0077b6",
"button-hover-color": "#005f87",
"toggle-theme-button-color": "#00f5d4",
"copy-result-button-color": "#293462",
"copy-preview-result-url-button-color": "#293462",
"date-color": "#72efdd",
"special-event-color": "#72ddf7"
}
}
]
}

View File

@@ -0,0 +1,32 @@
{
"short_name": "Fortune Generator",
"name": "Fortune Generator",
"description": "Get your daily fortune with just a click.",
"background_color": "#1a1b1e",
"theme_color": "#1a1b1e",
"icons": [
{
"src": "../images/lifeadventurer-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "../images/lifeadventurer-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "../images/lifeadventurer-180x180.png",
"sizes": "180x180",
"type": "image/png"
},
{
"src": "../images/lifeadventurer-270x270.png",
"sizes": "270x270",
"type": "image/png"
}
],
"start_url": "/generators/fortune_generator/index.html",
"display": "standalone",
"orientation": "portrait"
}

View File

@@ -1,47 +0,0 @@
const canvas = document.getElementById("Matrix")
const context = canvas.getContext("2d")
canvas.height = window.innerHeight + 100;
canvas.width = window.innerWidth + 5;
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./*-+#$%^@!~?><:;[]{}\」=_αβΓγΔδεζηΘθικΛλμΞξΠπρΣσςτυΦφχΨψΩω×≦≧≠∞≒≡∩∠∟⊿∫∮∵∴¥〒¢£℃€℉╩◢ⅨⅧⅦⅥⅣⅢⅡあいうえおがぎぐげござじずぜぞだぢつでづどにぬのばひぴぶへぺぼみゃょァゐゎè";
const fontSize = 16;
const columns = canvas.width / fontSize;
const charArr = [];
for(let i = 0; i < columns; ++i) {
charArr[i] = 1;
}
let frame = 0;
let str;
context.fillStyle = "rgba(0, 0, 0, 1)";
context.fillRect(0, 0, canvas.width, canvas.height);
function Update() {
context.fillStyle = "rgba(0, 0, 0, 0.05)";
context.fillRect(0, 0, canvas.width, canvas.height);
if(frame == 0){
let a = parseInt(Math.random() * 255);
str = `rgba(${a}, ${Math.abs(a - 127)}, ${Math.abs(a - 255)}, 0.9)`;
}
context.fillStyle = str;
context.font = fontSize + "px monospace";
for(let i = 0; i < columns; ++i){
const text = chars[Math.floor(Math.random() * chars.length)];
context.fillText(text, i * fontSize, charArr[i] * fontSize);
if(charArr[i] * fontSize > canvas.height && Math.random() > 0.90){
charArr[i] = 0;
}
charArr[i]++;
}
frame++;
if(frame <= 40 * (Math.floor(Math.random() * 10) + 3)) requestAnimationFrame(Update); // 40 frames a cycle
else{
frame = 0;
Appear();
}
}

View File

@@ -1,137 +0,0 @@
{
"special_events": [
{
"event": "感恩節",
"year": "2023",
"month": "11",
"date": "23",
"status_index": "0",
"goodFortunes": {
"l_1_event": "家人團聚",
"l_1_desc": "分享寶貴時光",
"l_2_event": "吃火雞大餐",
"l_2_desc": "Happy Thanksgiving!"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "棉花糖日",
"year": "2023",
"month": "12",
"date": "07",
"status_index": "0",
"goodFortunes": {
"l_1_event": "吃棉花糖",
"l_1_desc": "慶祝棉花糖日",
"l_2_event": "",
"l_2_desc": ""
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "貓奴日",
"year": "2023",
"month": "12",
"date": "15",
"status_index": "0",
"goodFortunes": {
"l_1_event": "嚕貓",
"l_1_desc": "撫平傷心的心情",
"l_2_event": "喝咖啡",
"l_2_desc": "到貓咪咖啡店去喝咖啡"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "冬至",
"year": "2023",
"month": "12",
"date": "22",
"status_index": "0",
"goodFortunes": {
"l_1_event": "吃湯圓",
"l_1_desc": "團團圓圓",
"l_2_event": "保暖",
"l_2_desc": "冬至到了"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "平安夜",
"year": "2023",
"month": "12",
"date": "24",
"status_index": "0",
"goodFortunes": {
"l_1_event": "除舊佈新",
"l_1_desc": "平安祥和",
"l_2_event": "交換禮物",
"l_2_desc": "獲得真心的祝福"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "聖誕節",
"year": "2023",
"month": "12",
"date": "25",
"status_index": "0",
"goodFortunes": {
"l_1_event": "家庭聚會",
"l_1_desc": "一起團圓吃火雞大餐",
"l_2_event": "注意保暖",
"l_2_desc": "冬至到了"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
},
{
"event": "元旦",
"year": "2024",
"month": "1",
"date": "1",
"status_index": "0",
"goodFortunes": {
"l_1_event": "早起",
"l_1_desc": "心情愉悅迎接新年",
"l_2_event": "大掃除",
"l_2_desc": "新年新氣象"
},
"badFortunes": {
"r_1_event": "",
"r_1_desc": "",
"r_2_event": "",
"r_2_desc": ""
}
}
]
}

View File

@@ -1,61 +0,0 @@
:root {
--button-color: #79abf7;
--button-hover-color: #4590dc;
--white: #FFFFFF;
}
* {
overflow: hidden;
text-align: center;
white-space: nowrap;
}
body {
margin: 0;
padding: 0;
height: 100%;
align-items: center;
justify-content: center;
}
.container {
top: 50%;
left: 50%;
width: 80%;
max-width: 800px;
position: absolute;
z-index: 1;
text-align: center;
transform: translate(-50%, -50%);
background-color: var(--white);
border-radius: 40px;
padding: 10px;
}
.left-result {
color: #e74c3c !important;
}
.right-result {
color: #000000bf !important;
}
button {
background-color: var(--button-color);
color: var(--white);
z-index: 2;
font-size: 20px;
border: none;
padding: 20px 20px;
border-radius: 30px;
cursor: pointer;
transition: all 0.3s ease-in-out;
}
button:hover {
background-color: var(--button-hover-color);
}
#Matrix {
z-index: 0;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

BIN
images/lifeadventurer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

View File

@@ -1,64 +1,99 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Generators</title>
<link rel="icon" href="./images/lifeadventurer.jpg">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN" crossorigin="anonymous">
<script src="https://unpkg.com/vue@3.3.8/dist/vue.global.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<link rel="stylesheet" href="./styles.css">
</head>
<body>
<header>
<!-- <hgroup> -->
<h1>Generators Gallery</h1>
<!-- </hgroup> -->
</header>
<section>
<div class="container">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Generators</title>
<link rel="icon" href="./images/lifeadventurer_rounded_logo.png" />
<!-- bootstrap 5.3.2 -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
crossorigin="anonymous"
/>
<!-- jquery 3.7.1 -->
<script
src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"
></script>
<!-- box icons -->
<link
href="https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css"
rel="stylesheet"
/>
<link rel="stylesheet" href="./styles.css" />
</head>
<body>
<header>
<div class="row">
<div class="col-md-6">
<div class="card mb-3 bg-dark text-white border-0">
<img class="card-img-top" src="./images/fortune_generator_example.png" alt="fortune generator example">
<!-- <video src="#" autoplay></video> -->
<div class="card-body">
<h4 class="card-title">Fortune Generator</h4>
<p class="card-text">Get your daily fortune with just a click.</p>
<a href="./fortune_generator/" class="btn btn-secondary">Check this out</a>
</div>
<div class="card-footer">
<div id="last-update-1"></div>
<h1 class="col-md-4 col-sm-6 offset-md-4 offset-sm-3">
Generators Gallery
</h1>
<div
class="col-md-1 col-sm-1 offset-md-3 offset-sm-2 bx bx-moon"
id="dark-mode-icon"
>
</div>
</div>
</header>
<section>
<div class="container">
<div class="row">
<div class="col-md-6">
<div class="card mb-3 border-0">
<img
class="card-img-top"
src="./images/fortune_generator_example.png"
alt="fortune generator example"
/>
<div class="card-body">
<h4 class="card-title">Fortune Generator</h4>
<p class="card-text">
Get your daily fortune with just a click.
</p>
<a class="btn" href="./fortune_generator/">Check this out</a>
</div>
<div class="card-footer">
<div id="last-update-1"></div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card mb-3 bg-dark text-white border-0">
<img class="card-img-top" src="./images/quote_generator_example_(2).png" alt="quote generator example">
<!-- <video src="#" autoplay></video> -->
<div class="card-body">
<h4 class="card-title">Quote Generator</h4>
<p class="card-text">Generate inspiring and thought-provoking quotes effortlessly.</p>
<a href="./quote_generator/" class="btn btn-secondary">Check this out</a>
</div>
<div class="card-footer">
<div id="last-update-2"></div>
<div class="col-md-6">
<div class="card mb-3 border-0">
<img
class="card-img-top"
src="./images/quote_generator_example_(2).png"
alt="quote generator example"
/>
<div class="card-body">
<h4 class="card-title">Quote Generator</h4>
<p class="card-text">
Generate inspiring and thought-provoking quotes effortlessly.
</p>
<a class="btn" href="./quote_generator/">Check this out</a>
</div>
<div class="card-footer">
<div id="last-update-2"></div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<footer>
<div class="row text-muted py-3 me-3 float-end" id="footer-author">
<h5> Copyright © 2023 LifeAdventurer | All Rights Reserved.
<a href="https://github.com/LifeAdventurer">
<img id="footer-author-icon" src="./images/lifeadventurer.jpg" alt="footer image">
</a>
</h5>
</div>
</footer>
<script src="./scripts.js"></script>
</body>
</section>
<footer>
<div class="row text-muted py-3 me-3 float-end" id="footer-author">
<h5>
Copyright © 2023-2025 LifeAdventurer | All Rights Reserved.
<a href="https://github.com/LifeAdventurer">
<img
id="footer-author-icon"
src="./images/lifeadventurer_rounded_logo.png"
alt="footer image"
/>
</a>
</h5>
</div>
</footer>
<script src="./scripts.js"></script>
</body>
</html>

View File

@@ -1,2 +0,0 @@
quote_input.txt
quote_output.txt

View File

@@ -1,9 +1,24 @@
:root {
--button-color: #9dc4ff;
--button-hover-color: #5ca8f3;
--bg-color: #ffffffd7;
--text-color: #000000;
}
.dark-mode {
--button-color: #66a1fa;
--button-hover-color: #8ec1f4;
--bg-color: #1b1919d7;
--dark-mode-icon-color: #ffffff;
--text-color: #ffffff;
}
html {
background: #282828;
height: 100%;
text-align: center;
overflow: hidden;
font-family: Georgia, 'Times New Roman', Times, serif;
font-family: Georgia, "Times New Roman", Times, serif;
font-size: 24px;
}
@@ -21,12 +36,6 @@ body {
z-index: 0;
}
:root {
--button-color: #9DC4FF;
--button-hover-color: #5ca8f3;
--white: #FFFFFF;
}
.container {
position: absolute;
z-index: 1;
@@ -38,7 +47,8 @@ body {
margin: 0 auto;
text-align: center;
padding: 70px;
background-color: var(--white);
color: var(--text-color);
background-color: var(--bg-color);
border-radius: 30px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4);
}
@@ -49,16 +59,33 @@ body {
button {
background-color: var(--button-color);
color: var(--white);
font-size: 16px;
color: var(--bg-color);
font-size: 25px;
border: none;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4);
padding: 17px 20px;
border-radius: 30px;
cursor: pointer;
transition: all 0.3s ease-in-out;
transition: all 0.3s ease-in-out;
}
button:hover {
background-color: var(--button-hover-color);
}
#dark-mode-icon {
margin-top: 15px;
font-size: 2.4rem;
color: var(--dark-mode-icon-color);
cursor: pointer;
opacity: 85%;
}
.home-button {
position: fixed;
top: 8px;
left: 8px;
z-index: 1000;
opacity: 0.8; /* Slightly transparent */
transition: opacity 0.3s;
}

View File

@@ -1,22 +0,0 @@
#include <iostream>
#include <string>
using namespace std;
int main() {
ios_base::sync_with_stdio(false);
cin.tie(0);
string quote, author;
while(true){
getline(cin, quote);
if(quote == "EOF") break;
getline(cin, author);
cout << "{\n";
cout << " \"quote\": \"" << quote << "\",\n";
cout << " \"author\": \"" << author << "\"\n";
cout << "},\n";
}
return 0;
}

View File

@@ -1,26 +1,46 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> Moon's Project </title>
<link rel="icon" href="../images/lifeadventurer.jpg">
<link rel="stylesheet" href="./styles.css">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Quote Generator</title>
<link rel="icon" href="../images/lifeadventurer_rounded_logo.png" />
<!-- bootstrap -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"
crossorigin="anonymous"
/>
<!-- box icons -->
<link
href="https://unpkg.com/boxicons@2.1.4/css/boxicons.min.css"
rel="stylesheet"
/>
<link rel="stylesheet" href="./css/styles.css" />
</head>
<body>
<div class="container">
<!-- Home button at the top -->
<a href="../" class="btn border-0 home-button">
<i class="bx bx-home text-white"></i>
</a>
<div class="container" id="imageContainer">
<h1>Today's quote</h1>
<div class="quote-container">
<p id="quote"></p>
<p id="author"></p>
</div>
<button onclick="getQuote()">Generate </button>
<div class="row">
<button class="col-4 offset-4" onclick="getQuote()">Generate</button>
<div class="col-2 offset-2 bx bx-moon" id="dark-mode-icon"></div>
</div>
</div>
<canvas id="Matrix"> </canvas>
<script src="./matrix.js"> </script>
<script src="./quote.js"> </script>
<canvas id="Matrix"></canvas>
<script src="./js/scripts.js"></script>
<script src="./js/matrix.js"></script>
<script src="./js/quote.js"></script>
</body>
</html>

View File

@@ -0,0 +1,51 @@
const canvas = document.getElementById("Matrix");
const context = canvas.getContext("2d");
canvas.height = globalThis.innerHeight + 100;
canvas.width = globalThis.innerWidth + 5;
const chars =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./*-+#$%^@!~?><:;[]{}=_αβΓγΔδεζηΘθικΛλμΞξΠπρΣσςτυΦφχΨψΩω×≦≧≠∞≒≡∩∠∟⊿∫∮∵∴¥〒¢£℃€℉╩◢ⅨⅧⅦⅥⅣⅢⅡあいうえおがぎぐげござじずぜぞだぢつでづどにぬのばひぴぶへぺぼみゃょァゐゎè";
const fontSize = 16;
const columns = canvas.width / fontSize;
const charArr = [];
for (let i = 0; i < columns; i++) {
charArr[i] = 1;
}
let frame = 0;
let str;
context.fillStyle = "rgba(0, 0, 0, 1)";
context.fillRect(0, 0, canvas.width, canvas.height);
function Update() {
context.fillStyle = "rgba(0, 0, 0, 0.05)";
context.fillRect(0, 0, canvas.width, canvas.height);
if (frame == 0) {
let a = parseInt(Math.random() * 255);
str = `rgba(${a}, ${Math.abs(a - 127)}, ${Math.abs(a - 255)}, 0.9)`;
}
context.fillStyle = str;
context.font = fontSize + "px monospace";
for (let i = 0; i < columns; i++) {
const text = chars[Math.floor(Math.random() * chars.length)];
context.fillText(text, i * fontSize, charArr[i] * fontSize);
if (charArr[i] * fontSize > canvas.height && Math.random() > 0.90) {
charArr[i] = 0;
}
charArr[i]++;
}
frame++;
if (frame <= 40 * (Math.floor(Math.random() * 10) + 3)) {
requestAnimationFrame(Update); // 40 frames a cycle
} else {
frame = 0;
Appear();
}
}

View File

@@ -0,0 +1,47 @@
const quoteElement = document.getElementById("quote");
const authorElement = document.getElementById("author");
const buttonElement = document.querySelector("button");
let quotes = [];
fetch("./json/quotes.json")
.then((response) => response.json())
.then((data) => {
quotes = data.quotes;
});
function Appear() {
const index = Math.floor(Math.random() * quotes.length);
const { quote, author } = quotes[index];
quoteElement.innerHTML = `<b style='font-size:28px;'>"${quote}"</b>`;
authorElement.innerHTML = "- " + author;
const container = document.getElementById("imageContainer");
const folderPath = "./backgrounds/";
// TODO: Get number of images from a JSON file.
const numDarkImages = 0;
const numLightImages = 0;
if (numDarkImages && numLightImages) {
const isDark = Math.random() < 0.5;
let randomIndex, randomImage;
const darkModeIcon = document.querySelector("#dark-mode-icon");
console.log(isDark);
if (isDark) {
randomIndex = Math.floor(Math.random() * numDarkImages) + 1;
randomImage = folderPath + "dark/" + randomIndex + ".jpg";
darkModeIcon.onclick();
} else {
randomIndex = Math.floor(Math.random() * numLightImages) + 1;
randomImage = folderPath + "light/" + randomIndex + ".jpg";
}
container.style.backgroundImage = "url('" + randomImage + "')";
container.style.opacity = 0.85;
container.style.backgroundSize = "100% 100%";
}
}
function getQuote() {
Update();
}

View File

@@ -0,0 +1,6 @@
const darkModeIcon = document.querySelector("#dark-mode-icon");
darkModeIcon.onclick = () => {
darkModeIcon.classList.toggle("bx-sun");
document.body.classList.toggle("dark-mode");
};

View File

@@ -0,0 +1,379 @@
{
"quotes": [
{
"quote": "To AC is human. To AK divine.",
"author": "Moon",
"id": 1
},
{
"quote": "Life is like riding a bicycle. To keep your balance, you must keep moving.",
"author": "Albert Einstein",
"id": 2
},
{
"quote": "A dream is what makes people love life even when it is painful.",
"author": "Theodore Zeldin",
"id": 3
},
{
"quote": "Dont quit. Suffer now and live the rest of your life as a champion.",
"author": "Muhammad Ali",
"id": 4
},
{
"quote": "Though nobody can go back and make a new beginning… Anyone can start over and make a new ending.",
"author": "Chico Xavier",
"id": 5
},
{
"quote": "Be happy for this moment. This moment is your life.",
"author": "Omar Khayyam",
"id": 6
},
{
"quote": "Life is not a problem to be solved, but a reality to be experienced.",
"author": "Soren Kierkegaard",
"id": 7
},
{
"quote": "All the waters of the world find one another again, and very road leads us wanderers too back home.",
"author": "Hermann Hesse",
"id": 8
},
{
"quote": "Time does not pass, it continues.",
"author": "Marty Rubin",
"id": 9
},
{
"quote": "Be both soft and wild. Just like the Moon. Or the storm. Or the sea.",
"author": "Victoria Erickson",
"id": 10
},
{
"quote": "Pain is a part of growing up. It is how we learn...",
"author": "Dan Brown",
"id": 11
},
{
"quote": "Beneath the winter, you can feel the bone structure of the landscape, and the whole story doesn't show.",
"author": "Andrew Wyeth",
"id": 12
},
{
"quote": "I had been educated in the rhythms of the mountain, in which change was never fundamental, only cyclical.",
"author": "Tara Westover",
"id": 13
},
{
"quote": "When you look at the stars, if feels like you are not from any particular piece of land, but from the solar system.",
"author": "Kalpana Chawla",
"id": 14
},
{
"quote": "Not all those who wander are lost.",
"author": "J. R. R. Tolkien",
"id": 15
},
{
"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.",
"author": "Saint Augustine",
"id": 16
},
{
"quote": "Change your opinions, keep to your principles; change your leaves, keep intact your roots.",
"author": "Victor Hugo",
"id": 17
},
{
"quote": "Patience is not simply the ability to wait, it's how we behave while we're waiting.",
"author": "Joyce Meyer",
"id": 18
},
{
"quote": "A rolling stone gathers no moss, but it gains a certain polish.",
"author": "Oliver Herford",
"id": 19
},
{
"quote": "I decided to fly through the air, live in the sunlight and enjoy life as much as I could.",
"author": "Evel Knievel",
"id": 20
},
{
"quote": "Be a life long or short, its completeness depends on what it was lived for.",
"author": "David Starr Jordan",
"id": 21
},
{
"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.",
"author": "Albert Einstein",
"id": 22
},
{
"quote": "All human wisdom is summed up in two words; wait and hope.",
"author": "Alexandre Dumas",
"id": 23
},
{
"quote": "There is no happiness like this happiness: quiet mornings, light from the river, the weekend ahead.",
"author": "James Salter",
"id": 24
},
{
"quote": "A lead falls; something is flying by; Let whatever your eyes gaze upon be created, and the soul of the hearer remain shivering.",
"author": "Vicente Huidobro",
"id": 25
},
{
"quote": "Still round the corner there may wait, a new road or a secret gate.",
"author": "J. R. R. Tolkien",
"id": 26
},
{
"quote": "One of the advantages of being disorganized is that one is always having surprising discoveries.",
"author": "A. A. Milne",
"id": 27
},
{
"quote": "Talk is cheap. Show me the code.",
"author": "Linus Torvalds",
"id": 28
},
{
"quote": "Every day is a journey, and the journey itself is home.",
"author": "Matsuo Basho",
"id": 29
},
{
"quote": "Life is an ongoing process of choosing between safety and risk. Make the growth choice a dozen times a day.",
"author": "Abraham Maslow",
"id": 30
},
{
"quote": "A leaf fluttered in through the window, as if supported by the rays of the sun.",
"author": "Anais Nin",
"id": 31
},
{
"quote": "All that we see or seem is but a dream within a dream.",
"author": "Edgar Allan Poe",
"id": 32
},
{
"quote": "Let every dawn be to you as the beginning of life, and every setting sun be to you as its close.",
"author": "John Ruskin",
"id": 33
},
{
"quote": "The softer snow falls, the longer it dwells upon, and the deeper it sinks into the mind.",
"author": "Samuel Taylor Coleridge",
"id": 34
},
{
"quote": "Happiness does not lie in happiness, but in the achievement of it.",
"author": "Fyodor Dostoevsky",
"id": 35
},
{
"quote": "You and I are all as much continuous with the physical universe as a wave is continuous with the ocean.",
"author": "Alan Watts",
"id": 36
},
{
"quote": "No self is an island, each exists in a fabric of relations that is more complex and mobile than ever before.",
"author": "Jean-Francois Lyotard",
"id": 37
},
{
"quote": "Sit in reverie and watch the changing color of the waves that break upon the idle seashore of the mind.",
"author": "Henry Longfellow",
"id": 38
},
{
"quote": "Water, stories, the body, all the things we do, are mediums that hid and show what's hidden.",
"author": "Rumi",
"id": 39
},
{
"quote": "Focus on what lights a fire inside of you and use that passion to fill a white space.",
"author": "Kendra Scott",
"id": 40
},
{
"quote": "Unlike a drop of water lost its identity when joins the ocean, man keeps his being in which he lives.",
"author": "B. R. Ambedkar",
"id": 41
},
{
"quote": "Snow isn't just pretty. It also cleanses our world, our senses, and a kind of weary familiarity.",
"author": "John Burnside",
"id": 42
},
{
"quote": "Loss is nothing else but change, and change is nature's delight.",
"author": "Marcus Aurelius",
"id": 43
},
{
"quote": "If there is magic on this planet, it is contained in water.",
"author": "Loren Eiseley",
"id": 44
},
{
"quote": "If the world's a veil of tears, smile till rainbows span it.",
"author": "Lucy Larcom",
"id": 45
},
{
"quote": "Any landscape is a condition of the spirit.",
"author": "Henri Frederic Amiel",
"id": 46
},
{
"quote": "Life is in a land full of thorns and weeds, there always a space in which the good seed can grow.",
"author": "Jorge Mario Bergoglio",
"id": 47
},
{
"quote": "The whole universe appears as an infinite storm of beauty.",
"author": "John Muir",
"id": 48
},
{
"quote": "You traverse the world in search of happiness, which is within the reach of every man.",
"author": "Horace",
"id": 49
},
{
"quote": "We sail within a vast sphere, ever drifting in uncertainty, driven from end to end.",
"author": "Blaise Pascal",
"id": 50
},
{
"quote": "Nothing is poetical if the plain daylight is not poetical.",
"author": "Gilbert K. Chesterton",
"id": 51
},
{
"quote": "Rest is not idleness. To lie sometimes on the grass under trees, and listen to the murmur of the water.",
"author": "John Lubbock",
"id": 52
},
{
"quote": "As the sun makes ice melt, kindness causes misunderstanding, mistrust, and hostility to evaporate.",
"author": "Albert Schweitzer",
"id": 53
},
{
"quote": "In my search for you, I've made my new home in the eyes of a bird, staring at the passing wind.",
"author": "Kaili Blues",
"id": 54
},
{
"quote": "Your spark isn't your purpose. The last box fills in when you're ready to come live.",
"author": "Film, Soul",
"id": 55
},
{
"quote": "There's a point at which we make our lives, but we also take the path which si given to us.",
"author": "Ali Smith",
"id": 56
},
{
"quote": "Yesterday is but today's memory, and tomorrow is today's dream.",
"author": "Khalil Gibran",
"id": 57
},
{
"quote": "For the wise man looks into space and he know there is no limited dimensions.",
"author": "Zhuangzi",
"id": 58
},
{
"quote": "My life is bathed in golden sunlight, and the really wonderful thing is that I know it.",
"author": "Helen McCrory",
"id": 59
},
{
"quote": "You block your dream when you allow your feat to grow bigger than you faith.",
"author": "Mary Manin Morrissey",
"id": 60
},
{
"quote": "The future influences the present just as much as the past.",
"author": "Friedrich Nietzsche",
"id": 61
},
{
"quote": "You could cover the whole earth with asphalt, but sooner or later green grass would break through.",
"author": "Ilya Ehrenburg",
"id": 62
},
{
"quote": "Empathy is about finding echoes of another person in yourself.",
"author": "Mohsin Hamid",
"id": 63
},
{
"quote": "Life is like this one big process of letting go.",
"author": "Adrianne Lenker",
"id": 64
},
{
"quote": "Great results cannot be achieved at once; and we should be satisfied to advance in life, step by step.",
"author": "Samuel Smiles",
"id": 65
},
{
"quote": "To live means to be aware, joyously, drunkenly, serenely, divinely aware.",
"author": "Henry Miller",
"id": 66
},
{
"quote": "The only limit to our realization of tomorrow is our doubts of today.",
"author": "Franklin D. Roosevelt",
"id": 67
},
{
"quote": "The choices you make from this day forward will lead you, step by step, to the future you deserve.",
"author": "Chris Murray",
"id": 68
},
{
"quote": "I am in the right place at the right time, and everything happens at the exactly right moment.",
"author": "Charlie Chaplin",
"id": 69
},
{
"quote": "I will not be \"famous,\" \"great.\" I will go on adventuring, changing, opening my mind and my eye.",
"author": "Virginia Woolf",
"id": 70
},
{
"quote": "When I am well-rested and focused on things that truly interest me, time often ceases to be an issue.",
"author": "James Clear",
"id": 71
},
{
"quote": "When you're no longer thinking ahead, each footstep isn't just a means to an end but an unique event in itself.",
"author": "Robert M. Pirsig",
"id": 72
},
{
"quote": "As long as the night lasts, I shall dance in the sky With all the dying fireworks of the light.",
"author": "Carl Sandburg",
"id": 73
},
{
"quote": "Your vision will become clear only when you can look into your own heart.",
"author": "Carl Jung",
"id": 74
},
{
"quote": "The goldenrod is yellow, the corn is turning brown, the trees in apple orchards with fruit are bending down.",
"author": "Helen Hunt Jackson",
"id": 75
}
]
}

View File

@@ -1,47 +0,0 @@
const canvas = document.getElementById("Matrix")
const context = canvas.getContext("2d")
canvas.height = window.innerHeight + 100;
canvas.width = window.innerWidth + 5;
const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./*-+#$%^@!~?><:;[]{}\」=_αβΓγΔδεζηΘθικΛλμΞξΠπρΣσςτυΦφχΨψΩω×≦≧≠∞≒≡∩∠∟⊿∫∮∵∴¥〒¢£℃€℉╩◢ⅨⅧⅦⅥⅣⅢⅡあいうえおがぎぐげござじずぜぞだぢつでづどにぬのばひぴぶへぺぼみゃょァゐゎè";
const fontSize = 16;
const columns = canvas.width / fontSize;
const charArr = [];
for(let i = 0; i < columns; ++i) {
charArr[i] = 1;
}
let frame = 0;
let str;
context.fillStyle = "rgba(0, 0, 0, 1)";
context.fillRect(0, 0, canvas.width, canvas.height);
function Update() {
context.fillStyle = "rgba(0, 0, 0, 0.05)";
context.fillRect(0, 0, canvas.width, canvas.height);
if(frame == 0){
let a = parseInt(Math.random() * 255);
str = `rgba(${a}, ${Math.abs(a - 127)}, ${Math.abs(a - 255)}, 0.9)`;
}
context.fillStyle = str;
context.font = fontSize + "px monospace";
for(let i = 0; i < columns; ++i){
const text = chars[Math.floor(Math.random() * chars.length)];
context.fillText(text, i * fontSize, charArr[i] * fontSize);
if(charArr[i] * fontSize > canvas.height && Math.random() > 0.90){
charArr[i] = 0;
}
charArr[i]++;
}
frame++;
if(frame <= 40 * (Math.floor(Math.random() * 10) + 3)) requestAnimationFrame(Update); // 40 frames a cycle
else{
frame = 0;
Appear();
}
}

View File

@@ -1,24 +0,0 @@
const quoteElement = document.getElementById("quote");
const authorElement = document.getElementById("author");
const buttonElement = document.querySelector("button");
let quotes = [];
fetch("quotes.json")
.then(response => response.json())
.then(data => {
quotes = data.quotes;
});
function Appear() {
console.log(quotes);
const index = Math.floor(Math.random() * quotes.length);
const {quote, author} = quotes[index];
quoteElement.innerHTML = `<b style='font-size:28px;'>"</b>` + quote + `<b style='font-size:28px;'>"</b>` ;
authorElement.innerHTML = "- " + author;
}
function getQuote() {
Update();
}

View File

@@ -1,144 +0,0 @@
{
"quotes" : [
{
"quote": "To AC is human. To AK divine.",
"author": "Moon",
"id": 1
},
{
"quote": "Life is like riding a bicycle. To keep your balance, you must keep moving.",
"author": "Albert Einstein",
"id": 2
},
{
"quote": "A dream is what makes people love life even when it is painful.",
"author": "Theodore Zeldin",
"id": 3
},
{
"quote": "Dont quit. Suffer now and live the rest of your life as a champion.",
"author": "Muhammad Ali",
"id": 4
},
{
"quote": "Though nobody can go back and make a new beginning… Anyone can start over and make a new ending.",
"author": "Chico Xavier",
"id": 5
},
{
"quote": "Be happy for this moment. This moment is your life.",
"author": "Omar Khayyam",
"id": 6
},
{
"quote": "Life is not a problem to be solved, but a reality to be experienced.",
"author": "Soren Kierkegaard",
"id": 7
},
{
"quote": "All the waters of the world find one another again, and very road leads us wanderers too back home.",
"author": "Hermann Hesse",
"id": 8
},
{
"quote": "Time does not pass, it continues.",
"author": "Marty Rubin",
"id": 9
},
{
"quote": "Be both soft and wild. Just like the Moon. Or the storm. Or the sea.",
"author": "Victoria Erickson",
"id": 10
},
{
"quote": "Pain is a part of growing up. It is how we learn...",
"author": "Dan Brown",
"id": 11
},
{
"quote": "Beneath the winter, you can feel the bone structure of the landscape, and the whole story doesn't show.",
"author": "Andrew Wyeth",
"id": 12
},
{
"quote": "I had been educated in the rhythms of the mountain, in which change was never fundamental, only cyclical.",
"author": "Tara Westover",
"id": 13
},
{
"quote": "When you look at the stars, if feels like you are not from any particular piece of land, but from the solar system.",
"author": "Kalpana Chawla",
"id": 14
},
{
"quote": "Not all those who wander are lost.",
"author": "J. R. R. Tolkien",
"id": 15
},
{
"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.",
"author": "Saint Augustine",
"id": 16
},
{
"quote": "Change your opinions, keep to your principles; change your leaves, keep intact your roots.",
"author": "Victor Hugo",
"id": 17
},
{
"quote": "Patience is not simply the ability to wait, it's how we behave while we're waiting.",
"author": "Joyce Meyer",
"id": 18
},
{
"quote": "A rolling stone gathers no moss, but it gains a certain polish.",
"author": "Oliver Herford",
"id": 19
},
{
"quote": "I decided to fly through the air, live in the sunlight and enjoy life as much as I could.",
"author": "Evel Knievel",
"id": 20
},
{
"quote": "Be a life long or short, its completeness depends on what it was lived for.",
"author": "David Starr Jordan",
"id": 21
},
{
"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.",
"author": "Albert Einstein",
"id": 22
},
{
"quote": "All human wisdom is summed up in two words; wait and hope.",
"author": "Alexandre Dumas",
"id": 23
},
{
"quote": "There is no happiness like this happiness: quiet mornings, light from the river, the weekend ahead.",
"author": "James Salter",
"id": 24
},
{
"quote": "A lead falls; something is flying by; Let whatever your eyes gaze upon be created, and the soul of the hearer remain shivering.",
"author": "Vicente Huidobro",
"id": 25
},
{
"quote": "Still round the corner there may wait, a new road or a secret gate.",
"author": "J. R. R. Tolkien",
"id": 26
},
{
"quote": "One of the advantages of being disorganized is that one is always having surprising discoveries.",
"author": "A. A. Milne",
"id": 27
},
{
"quote": "Talk is cheap. Show me the code.",
"author": "Linus Torvalds",
"id": 28
}
]
}

View File

@@ -1,37 +1,36 @@
// fetch all folder paths of the generators from `folders.json`
let folderPaths = []
let folderPaths = [];
async function fetch_folders(){
await fetch('./folders.json')
.then(response => response.json())
.then(data => {
folderPaths = data.folder_paths;
// console.log(folderPaths);
})
async function fetch_folders() {
await fetch("./folders.json")
.then((response) => response.json())
.then((data) => {
folderPaths = data.folder_paths;
});
}
async function get_generator_card_footer(){
await fetch_folders()
// console.log(folderPaths);
const repoOwner = 'LifeAdventurer';
const repoName = 'generators';
for(let folderIndex = 1; folderIndex <= folderPaths.length; folderIndex++){
let folderPath = folderPaths[folderIndex - 1];
const apiUrl = `https://api.github.com/repos/${repoOwner}/${repoName}/commits?path=${folderPath}`;
console.log(apiUrl);
async function get_generator_card_footer() {
await fetch_folders();
const repoOwner = "LifeAdventurer";
const repoName = "generators";
for (let folderIndex = 1; folderIndex <= folderPaths.length; folderIndex++) {
const folderPath = folderPaths[folderIndex - 1];
const apiUrl =
`https://api.github.com/repos/${repoOwner}/${repoName}/commits?path=${folderPath}`;
fetch(apiUrl)
.then(response => response.json())
.then(data => {
// the latest commit will be at the top of the list
let lastCommit = data[0].commit.author.date;
const commitTimeStamp = new Date(lastCommit).getTime() / 1000;
const currentTimeStamp = Math.floor(new Date().getTime() / 1000);
const timeDifference = currentTimeStamp - commitTimeStamp;
.then((response) => response.json())
.then((data) => {
// the latest commit will be at the top of the list
const lastCommit = data[0].commit.author.date;
const commitTimeStamp = new Date(lastCommit).getTime() / 1000;
const currentTimeStamp = Math.floor(new Date().getTime() / 1000);
const timeDifference = currentTimeStamp - commitTimeStamp;
// console.log(timeSinceLastUpdate);
$(`#last-update-${folderIndex}`).html(`Last updated ${format_time_difference(timeDifference)} ago`)
})
$(`#last-update-${folderIndex}`).html(
`Last updated ${format_time_difference(timeDifference)} ago`,
);
});
// .catch(error => console.error('Error fetching data:', error));
}
}
@@ -42,18 +41,27 @@ function format_time_difference(seconds) {
const hours = Math.floor(minutes / 60);
const days = Math.floor(hours / 24);
if(days > 0){
return `${days} day${days > 1 ? 's' : ''}`;
}
else if(hours > 0){
return `${hours} hour${hours > 1 ? 's' : ''}`;
}
else if(minutes > 0){
return `${minutes} minute${minutes > 1 ? 's' : ''}`;
}
else{
return `${seconds} second${seconds > 1 ? 's' : ''}`;
if (days > 0) {
return `${days} day${days > 1 ? "s" : ""}`;
} else if (hours > 0) {
return `${hours} hour${hours > 1 ? "s" : ""}`;
} else if (minutes > 0) {
return `${minutes} minute${minutes > 1 ? "s" : ""}`;
} else {
return `${seconds} second${seconds > 1 ? "s" : ""}`;
}
}
get_generator_card_footer()
get_generator_card_footer();
const darkModeIcon = document.querySelector("#dark-mode-icon");
darkModeIcon.onclick = () => {
darkModeIcon.classList.toggle("bx-sun");
document.body.classList.toggle("dark-mode");
};
// temporary
let max_height = -1;
document.querySelectorAll('.card-body').forEach(el => max_height = Math.max(max_height, el.offsetHeight));
document.querySelectorAll('.card-body').forEach(el => el.style.height = `${max_height}px`);

396
scripts/check-events.py Normal file
View File

@@ -0,0 +1,396 @@
#!/bin/python3
import logging
import collections
import datetime
import argparse
import enum
import json
class DateType(enum.Enum):
CUSTOM = "custom"
STATIC = "static"
CYCLICAL = "cyclical"
def __str__(self):
return self.name.lower()
def __repr__(self):
return str(self)
@staticmethod
def argparse(s):
try:
return DateType[s.upper()]
except KeyError:
return s
args_parser = argparse.ArgumentParser(description="special events checker")
args_parser.add_argument("path", type=str, help="event json file path")
args_parser.add_argument(
"type",
type=DateType.argparse,
choices=[t for t in DateType],
help="event date type",
)
args = args_parser.parse_args()
special_events: dict[str, list[dict]] = {}
try:
with open(args.path) as f:
special_events = json.loads(f.read())
except json.JSONDecodeError:
print(f"`{args.path}` json syntax error.")
exit(-1)
except FileNotFoundError:
print(f"`{args.path}` not found.")
print("Please contact developer to solve this problem.")
exit(-1)
if not isinstance(special_events, dict):
print("`special_events` should be a dict")
exit(-1)
if "special_events" not in special_events:
print(f"`special_events` not found in `{args.path}`.")
exit(-1)
if not isinstance(special_events["special_events"], list):
print(f"`special_events` in `{args.path}` should be a list.")
exit(-1)
MIN_STATUS_INDEX = 0
MAX_STATUS_INDEX = 7
DAYSPERMONTH = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
errors: dict[int, list[str]] = collections.defaultdict(list)
def is_leap_year(year: int) -> bool:
"""Determines whether a given year is a leap year.
Args:
year (int): The year to check.
Returns:
bool: True if the year is a leap year, False otherwise.
"""
if year % 400 == 0:
return True
if year % 100 == 0:
return False
if year % 4 == 0:
return True
return False
def validate_number(event_idx: int, value, min: int, max: int, field_name: str) -> int | None:
"""Validates whether a given value is an integer within a specified range.
Args:
event_idx (int): The index of the event for associating validation errors.
value (Any): The value to validate.
min (int): The minimum acceptable value (inclusive).
max (int): The maximum acceptable value (inclusive).
field_name (str): The name of the field being validated, used in error messages.
Returns:
int | None: The validated integer value if it is within the range, otherwise None.
Raises:
ValueError: If `value` cannot be converted to an integer.
Validation Rules:
- If `value` cannot be converted to an integer, an error is recorded and None is returned.
- If `value` is outside the range defined by `min` and `max`, an error is recorded and None is returned.
"""
try:
value = int(value)
except ValueError:
errors[event_idx].append(f"`{field_name}` should be between {min} and {max}")
return None
if value < min or value > max:
errors[event_idx].append(f"`{field_name}` should be between {min} and {max}")
return None
return value
def require_field_check(
obj: dict, event_idx: int, fields: list[tuple[str, type]], required_field: str = ""
) -> bool:
"""
Validates the presence and types of required fields in a given object.
Args:
obj (dict): The object (dictionary) to validate.
event_idx (int): The index of the event for associating validation errors.
fields (list[tuple[str, type]]): A list of tuples where each tuple contains a field name and its expected type.
required_field (str, optional): An optional prefix for error messages to indicate a higher-level required field. Defaults to "".
Returns:
bool: True if all required fields are present and have the correct types, otherwise False.
Validation Rules:
- If a required field is missing, an error message is recorded.
- If a field is present but its type does not match the expected type, an error message is recorded.
- The `required_field` parameter, if provided, is prepended to error messages for context.
"""
error_found = False
for field_name, field_type in fields:
if field_name not in obj:
error_found = True
msg = ""
if required_field != "":
msg = f"`{required_field}` "
msg += f"missing `{field_name}`."
errors[event_idx].append(msg)
elif not isinstance(obj[field_name], field_type):
error_found = True
errors[event_idx].append(f"`{field_name}` should be a `{field_type}` type.")
if error_found:
return False
return True
event_names = set()
event_dates = set()
def check_structure(event, idx: int):
if not isinstance(event, dict):
errors[idx].append("should be a dict")
return False
if not require_field_check(
event,
idx,
[
("event", str),
("triggerDate", dict),
("status_index", str),
("goodFortunes", dict),
("badFortunes", dict),
],
):
return False
event_name: str = event["event"]
if event_name.strip() == "":
errors[idx].append("event name should not empty.")
return
if event_name in event_names:
errors[idx].append(f"event `{event_name}` already exists.")
validate_number(
idx, event["status_index"], MIN_STATUS_INDEX, MAX_STATUS_INDEX, "status_index"
)
if require_field_check(
event["goodFortunes"],
idx,
[
("l_1_event", str),
("l_1_desc", str),
("l_2_event", str),
("l_2_desc", str),
],
"goodFortunes"
):
if bool(event["goodFortunes"]["l_1_event"]) ^ bool(event["goodFortunes"]["l_1_desc"]):
# Check for inconsistency: XOR is used to ensure both l_1_event and l_1_desc
# are either both provided or both missing. If only one is provided, log an error.
errors[idx].append("First good fortune is incomplete.")
if bool(event["goodFortunes"]["l_2_event"]) ^ bool(event["goodFortunes"]["l_2_desc"]):
# Check for inconsistency: XOR is used to ensure both l_2_event and l_2_desc
# are either both provided or both missing. If only one is provided, log an error.
errors[idx].append("Second good fortune is incomplete.")
if require_field_check(
event["badFortunes"],
idx,
[
("r_1_event", str),
("r_1_desc", str),
("r_2_event", str),
("r_2_desc", str),
],
"badFortunes"
):
if bool(event["badFortunes"]["r_1_event"]) ^ bool(event["badFortunes"]["r_1_desc"]):
# Check for inconsistency: XOR is used to ensure both r_1_event and r_1_desc
# are either both provided or both missing. If only one is provided, log an error.
errors[idx].append("First bad fortune is incomplete.")
if bool(event["badFortunes"]["r_2_event"]) ^ bool(event["badFortunes"]["r_2_desc"]):
# Check for inconsistency: XOR is used to ensure both r_2_event and r_2_desc
# are either both provided or both missing. If only one is provided, log an error.
errors[idx].append("Second bad fortune is incomplete.")
event_names.add(event_name)
return True
def check_static_date(event: dict, idx: int):
trigger_date: dict = event["triggerDate"]
corrected = require_field_check(
trigger_date,
idx,
[
("month", str),
("date", str),
],
"triggerDate",
)
event_name: str = event["event"]
if "year" in trigger_date:
errors[idx].append(
f"this event `{event_name}` should be placed in `custom_special.json`."
)
if "week" in trigger_date or "weekday" in trigger_date:
errors[idx].append(
f"this event `{event_name}` should be placed in `cyclical_special.json`."
)
if not corrected:
return
month = validate_number(idx, trigger_date["month"], 1, 12, "triggerDate.month")
if month is not None:
validate_number(
idx, trigger_date["date"], 1, DAYSPERMONTH[month], "triggerDate.date"
)
key = f'{event_name}:{trigger_date["month"]}/{trigger_date["date"]}'
if key in event_dates:
errors[idx].append(f"The `{key}` is repeated.")
event_dates.add(key)
def check_cyclical_date(event: dict, idx: int):
trigger_date: dict = event["triggerDate"]
corrected = require_field_check(
trigger_date,
idx,
[
("month", str),
("week", str),
("weekday", str),
],
"triggerDate",
)
event_name: str = event["event"]
if "year" in trigger_date:
errors[idx].append(
f"this event `{event_name}` should be placed in `custom_special.json`."
)
elif "date" in trigger_date:
errors[idx].append(
f"this event `{event_name}` should be placed in `static_special.json`."
)
if not corrected:
return
validate_number(idx, trigger_date["month"], 1, 12, "triggerDate.month")
validate_number(idx, trigger_date["week"], 1, 5, "triggerDate.week")
validate_number(idx, trigger_date["weekday"], 1, 7, "triggerDate.weekday")
key = f'{event_name}:{trigger_date["month"]}/{trigger_date["week"]}/{trigger_date["weekday"]}'
if key in event_dates:
errors[idx].append(f"The `{key}` is repeated.")
event_dates.add(key)
def check_custom_date(event: dict, idx: int):
trigger_date: dict = event["triggerDate"]
corrected = require_field_check(
trigger_date,
idx,
[
("year", str),
("month", str),
("date", str),
],
"triggerDate",
)
event_name: str = event["event"]
if "week" in trigger_date or "weekday" in trigger_date:
errors[idx].append(
f"this event `{event_name}` should be placed in `cyclical_special.json`.",
)
elif "year" not in trigger_date:
errors[idx].append(
f"this event `{event_name}` should be placed in `static_special.json`."
)
if not corrected:
return
year = validate_number(
idx,
trigger_date["year"],
datetime.datetime.min.year,
datetime.datetime.max.year,
"triggerDate.year",
)
month = validate_number(idx, trigger_date["month"], 1, 12, "triggerDate.month")
if year is None or month is None:
return
days = DAYSPERMONTH[month]
if month == 2 and is_leap_year(year):
days += 1 # 29
date = validate_number(idx, trigger_date["date"], 1, days, "triggerDate.date")
if date is None:
return
key = f'{event_name}:{year}/{month}/{date}'
if key in event_dates:
errors[idx].append(f"The `{key}` is repeated.")
event_dates.add(key)
date_checker = {
DateType.CUSTOM: check_custom_date,
DateType.STATIC: check_static_date,
DateType.CYCLICAL: check_cyclical_date,
}
check_triggerdate = date_checker[args.type]
for idx, event in enumerate(special_events["special_events"]):
if check_structure(event, idx):
check_triggerdate(event, idx)
if errors:
logging.error(args.path)
for idx, error_msgs in errors.items():
logging.error(json.dumps(special_events["special_events"][idx], indent=4, ensure_ascii=False))
for msg in error_msgs:
logging.error(msg)
exit(-1)

169
scripts/check-fortune.py Normal file
View File

@@ -0,0 +1,169 @@
#!/bin/python3
import json
import logging
import argparse
import collections
args_parser = argparse.ArgumentParser(description="fortune checker")
args_parser.add_argument("path", type=str, help="event json file path")
args = args_parser.parse_args()
errors: dict[tuple[str, int], list[str]] = collections.defaultdict(list)
good_fortunes: list[dict] = None
bad_fortunes: list[dict] = None
all_fortunes = None
try:
with open(args.path) as f:
all_fortunes = json.loads(f.read())
except json.JSONDecodeError:
print(f"`{args.path}` json syntax error.")
exit(-1)
except FileNotFoundError:
print(f"`{args.path}` not found.")
print("Please contact developer to solve this problem.")
exit(-1)
if not isinstance(all_fortunes, dict):
print(f"`{args.path}` should contain a dict")
exit(-1)
try:
good_fortunes = all_fortunes["goodFortunes"]
except KeyError:
print(f"`{args.path}` should contain `goodFortunes`")
if not isinstance(good_fortunes, list):
print("`goodFortunes` should be a list.")
try:
bad_fortunes = all_fortunes["badFortunes"]
except KeyError:
print(f"`{args.path}` should contain `badFortunes`")
if not isinstance(bad_fortunes, list):
print("`badFortunes` should be a list.")
def require_field_check(
obj: dict,
fortune_idx: tuple[str, int],
fields: list[tuple[str, type]],
required_field: str = "",
) -> bool:
"""
Validates the presence and types of required fields in a given object.
Args:
obj (dict): The object (dictionary) to validate.
fortune_idx (tuple[str, int]): The index of the fortune for associating validation errors.
fields (list[tuple[str, type]]): A list of tuples where each tuple contains a field name and its expected type.
required_field (str, optional): An optional prefix for error messages to indicate a higher-level required field. Defaults to "".
Returns:
bool: True if all required fields are present and have the correct types, otherwise False.
Validation Rules:
- If a required field is missing, an error message is recorded.
- If a field is present but its type does not match the expected type, an error message is recorded.
- The `required_field` parameter, if provided, is prepended to error messages for context.
"""
error_found = False
for field_name, field_type in fields:
if field_name not in obj:
error_found = True
msg = ""
if required_field != "":
msg = f"`{required_field}` "
msg += f"missing `{field_name}`."
errors[fortune_idx].append(msg)
elif not isinstance(obj[field_name], field_type):
error_found = True
errors[fortune_idx].append(
f"`{field_name}` should be a `{field_type}` type."
)
if error_found:
return False
return True
fortune_names = set()
def check_fortune(fortune, idx: tuple[str, int]):
if not isinstance(fortune, dict):
errors[idx].append("fortune should be a dict.")
return False
if not require_field_check(fortune, idx, [
("event", str),
("description", list)
]):
return False
fortune_name = fortune["event"]
if fortune_name in fortune_names:
errors[idx].append(f"fortune `{fortune_name}` already exists.")
if not fortune_name:
errors[idx].append("fortune name should not be empty.")
if not fortune["description"]:
errors[idx].append("fortune description should not be empty.")
return False
descriptions = set()
for desc in fortune["description"]:
if not isinstance(desc, str):
errors[idx].append(f"fortune description {desc} should be a string.")
continue
if not desc:
errors[idx].append(f"fortune description {desc} should not be empty.")
continue
if desc in descriptions:
errors[idx].append(f"fortune description {desc} already exists.")
continue
else:
descriptions.add(desc)
fortune_names.add(fortune_name)
return True
if good_fortunes:
for idx, fortune in enumerate(good_fortunes):
check_fortune(fortune, ("goodFortunes", idx))
fortune_names.clear()
if bad_fortunes:
for idx, fortune in enumerate(bad_fortunes):
check_fortune(fortune, ("badFortunes", idx))
if errors:
logging.error(args.path)
for idx, error_msgs in errors.items():
fortunes = None
if idx[0] == "goodFortunes":
fortunes = good_fortunes
elif idx[0] == "badFortunes":
fortunes = bad_fortunes
if not fortunes:
continue
logging.error(
json.dumps(
fortunes[idx[1]], indent=4, ensure_ascii=False
)
)
for msg in error_msgs:
logging.error(msg)
exit(-1)

165
scripts/check-theme.py Normal file
View File

@@ -0,0 +1,165 @@
#!/bin/python3
import json
import logging
import argparse
import collections
args_parser = argparse.ArgumentParser(description="theme checker")
args_parser.add_argument("path", type=str, help="event json file path")
args = args_parser.parse_args()
errors: dict[int, list[str]] = collections.defaultdict(list)
themes: list[dict[str]] = None
j = None
try:
with open(args.path) as f:
j = json.loads(f.read())
except json.JSONDecodeError:
print(f"`{args.path}` json syntax error.")
exit(-1)
except FileNotFoundError:
print(f"`{args.path}` not found.")
print("Please contact developer to solve this problem.")
exit(-1)
if not isinstance(j, dict):
print(f"`{args.path}` should contain a dict")
exit(-1)
try:
themes = j["themes"]
except KeyError:
print(f"`{args.path}` should contain `themes`")
exit(-1)
if not isinstance(themes, list):
print("`themes` should be a list.")
exit(-1)
def require_field_check(
obj: dict,
theme_idx: int,
fields: list[tuple[str, type]],
required_field: str = "",
) -> bool:
"""
Validates the presence and types of required fields in a given object.
Args:
obj (dict): The object (dictionary) to validate.
theme_idx (int): The index of the fortune for associating validation errors.
fields (list[tuple[str, type]]): A list of tuples where each tuple contains a field name and its expected type.
required_field (str, optional): An optional prefix for error messages to indicate a higher-level required field. Defaults to "".
Returns:
bool: True if all required fields are present and have the correct types, otherwise False.
Validation Rules:
- If a required field is missing, an error message is recorded.
- If a field is present but its type does not match the expected type, an error message is recorded.
- The `required_field` parameter, if provided, is prepended to error messages for context.
"""
error_found = False
for field_name, field_type in fields:
if field_name not in obj:
error_found = True
msg = ""
if required_field != "":
msg = f"`{required_field}` "
msg += f"missing `{field_name}`."
errors[theme_idx].append(msg)
elif not isinstance(obj[field_name], field_type):
error_found = True
errors[theme_idx].append(
f"`{field_name}` should be a `{field_type}` type."
)
if error_found:
return False
return True
theme_names = set()
def check_theme(theme, idx: int):
if not isinstance(theme, dict):
errors[idx].append("theme should be a dict.")
return False
if not require_field_check(theme, idx, [
("name", str),
("properties", dict)
]):
return False
theme_name = theme["name"]
if theme_name in theme_names:
errors[idx].append(f"theme `{theme_name}` already exists.")
if not theme_name:
errors[idx].append("theme name should not be empty.")
properties = theme["properties"]
properties_field_required = [
("bg-color", str),
("good-fortune-color", str),
("bad-fortune-color", str),
("middle-fortune-color", str),
("title-color", str),
("desc-color", str),
("button-color", str),
("button-hover-color", str),
("toggle-theme-button-color", str),
("copy-result-button-color", str),
("copy-preview-result-url-button-color", str),
("date-color", str),
("special-event-color", str),
]
if not require_field_check(properties, idx, properties_field_required):
return False
for field_name in (v[0] for v in properties_field_required):
color: str = properties[field_name]
if color[0] != "#":
errors[idx].append(f"color {color} should starts with `#`.")
continue
color = color[1:]
if any(not ch.isdigit() and not ch.islower() for ch in color):
errors[idx].append(f"color {color} should be all lowercase.")
continue
hex = set("0123456789abcdef")
if any(ch not in hex for ch in color):
errors[idx].append(f"color {color} should be a hex value.")
continue
if len(color) != len("rrggbb") and len(color) != len("rrggbbaa"):
errors[idx].append(f"color {color} should be in `rrggbb` or `rrggbbaa` format.")
continue
theme_names.add(theme_name)
return True
for idx, theme in enumerate(themes):
check_theme(theme, idx)
if errors:
logging.error(args.path)
for idx, error_msgs in errors.items():
logging.error(
json.dumps(
themes[idx], indent=4, ensure_ascii=False
)
)
for msg in error_msgs:
logging.error(msg)
exit(-1)

146
scripts/main.js Normal file
View File

@@ -0,0 +1,146 @@
function check_ip_valid(n1, n2, n3, n4) {
if (n1 > 255 || n2 > 255 || n3 > 255 || n4 > 255) return false;
// private network
if (n1 === 10) return false;
// Carrier-grade NAT
if (n1 == 100 && n2 == 64) return false;
// localhost
if (n1 === 127 && n2 === 0 && n3 === 0) return false;
// link-local address
if (n1 == 169 && n2 == 254) return false;
// private network
if (n1 === 172) { if (n2 >= 16 && n2 <= 31) return false; }
if (n1 === 192) {
if (n2 === 168) return false; // private network
if (n2 === 0 && n3 === 0) return false; // IANA RFC 5735
if (n2 === 0 && n3 === 2) return false; // TEST-NET-1 RFC 5735
if (n2 === 88 && n3 === 99) return false; // 6to4
}
if (n1 == 198) {
if (n2 == 18) return false; // RFC 2544
if (n2 == 51 && n3 == 100) return false; // TEST-NET-2 RFC 5735
}
if (n1 == 203 && n3 == 113) return false; // TEST-NET-3 RFC 5735
// class D network
if (n1 == 224) return false;
// class E network
if (n1 == 255) return false;
return true;
}
let goodFortunes = -1;
let badFortunes = -1;
let badLen = -1;
let goodLen = -1;
let buckets = {};
const statusLen = 8;
const fs = require("fs");
fs.readFile('../fortune_generator/json/fortune.json', 'utf8', (err, content) => {
if (err) {
return;
}
let tmp = JSON.parse(content);
goodFortunes = tmp.goodFortunes;
goodLen = goodFortunes.length;
badFortunes = tmp.badFortunes;
badLen = badFortunes.length;
let num = null;
const dates = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 30];
let buckets = {};
let day = 0;
let run_cnt = 0;
let current_year = (new Date()).getFullYear();
while (run_cnt != 2000) {
let n1 = parseInt(Math.random() * 255 + 1);
let n2 = parseInt(Math.random() * 255 + 1);
let n3 = parseInt(Math.random() * 255 + 1);
let n4 = parseInt(Math.random() * 255 + 1);
if (!check_ip_valid(n1, n2, n3, n4)) continue;
let index = `${n1}.${n2}.${n3}.${n4}`;
if (buckets[index] != undefined) continue;
buckets[index] = [-1, -1, -1, -1, -1];
for (let i = 1; i <= 12; i++) {
for (let j = 1; j <= dates[i - 1]; j++) {
day %= 7;
run(current_year, i, j, day, [n1, n2, n3, n4], buckets);
day++;
}
}
run_cnt++;
}
fs.writeFile("./res.txt", JSON.stringify(buckets), (err) => {
console.log(err);
});
});
// calculate hash and write result
function run(year, month, date, day, ip, buckets) {
let num = ip;
// NOTE: hardcode
const hashDate = Math.round(
Math.log10(
year *
((month << (Math.log10(num[3]) + day - 1)) *
(date << Math.log10(num[2] << day))),
),
);
seed1 = (num[0] >> hashDate) * (num[1] >> Math.min(hashDate, 2)) +
(num[2] << 1) * (num[3] >> 3) + (date << 3) * (month << hashDate) +
(year * day) >> 2;
seed2 = (num[0] << (hashDate + 2)) * (num[1] << hashDate) +
(num[2] << 1) * (num[3] << 2) +
(date << (hashDate - 1)) * (month << 4) + year >>
hashDate + (date * day) >> 1;
// decide the status
let seedMagic = 0;
if (seed1 > seed2) {
seedMagic = (seed1 ^ seed2) + parseInt(seed1.toString().split('').reverse().join(''));
} else if (seed1 < seed2) {
let collatzLen = 0;
let temp = Math.abs(seed1 - seed2);
while (temp !== 1) {
temp = temp % 2 === 0 ? temp / 2 : 3 * temp + 1;
collatzLen++;
}
seedMagic = collatzLen + seed2.toString(2).replace(/0/g, '').length;
} else {
seedMagic = seed1 + seed2;
}
status_index = ((seedMagic) % statusLen + statusLen) % statusLen;
// make sure the events won't collide
const set = new Set();
const l1 = (seed1 % goodLen + goodLen) % goodLen;
set.add(goodFortunes[l1].event);
let l2 = (((seed1 << 1) + date) % goodLen + goodLen) % goodLen;
while (set.has(goodFortunes[l2].event)) {
l2 = (l2 + 1) % goodLen;
}
set.add(goodFortunes[l2].event);
let r1 = (((seed1 >> 1) + (month << 3)) % badLen + badLen) % badLen;
while (set.has(badFortunes[r1].event)) {
r1 = (r1 + 2) % badLen;
}
set.add(badFortunes[r1].event);
let r2 = ((((((seed1 << 3) + (year >> 5) * (date << 2)) % badLen) *
seed2) >> 6) % badLen + badLen) % badLen;
while (set.has(badFortunes[r2].event)) {
r2 = (r2 + 1) % badLen;
}
// NOTE: hardcode end
// write l1, l2, r1, r2
let index = `${ip[0]}.${ip[1]}.${ip[2]}.${ip[3]}`;
buckets[index][0] = l1;
buckets[index][1] = l2;
buckets[index][2] = r1;
buckets[index][3] = r2;
buckets[index][4] = status_index
}

68
scripts/plot_gen.py Normal file
View File

@@ -0,0 +1,68 @@
import json
import matplotlib.pyplot as plt
with open("./res.txt") as res_file:
res = json.load(res_file)
good_len = -1
bad_len = -1
with open("../fortune_generator/json/fortune.json") as fortune_file:
j = json.load(fortune_file)
good_len = len(j["goodFortunes"])
bad_len = len(j["badFortunes"])
status_bucket = [0] * 8
good_bucket = [0] * good_len
bad_bucket = [0] * bad_len
for ip, v in res.items():
assert all(val != -1 for val in v)
good_bucket[v[0]] += 1
good_bucket[v[1]] += 1
bad_bucket[v[2]] += 1
bad_bucket[v[3]] += 1
status_bucket[v[4]] += 1
groups = 1
fig, axs = plt.subplots(groups, 1, figsize=(8, 6))
axs.bar(
range(good_len),
good_bucket,
color="skyblue",
edgecolor="black",
)
axs.set_xlabel("Good Fortune Event Index")
axs.set_ylabel("Occurrences")
plt.tight_layout()
plt.show()
fig, axs = plt.subplots(groups, 1, figsize=(8, 6))
axs.bar(
range(bad_len),
bad_bucket,
color="skyblue",
edgecolor="black",
)
axs.set_xlabel("Bad Fortune Event Index")
axs.set_ylabel("Occurrences")
plt.tight_layout()
plt.show()
fig, axs = plt.subplots(groups, 1, figsize=(8, 6))
axs.bar(
range(len(status_bucket)),
status_bucket,
color="skyblue",
edgecolor="black",
)
axs.set_xlabel("Status Index")
axs.set_ylabel("Occurrences")
plt.show()

View File

@@ -0,0 +1,79 @@
#!/bin/python3
import os
import json
import shutil
import logging
import argparse
args_parser = argparse.ArgumentParser(description="Generator Template Generator")
args_parser.add_argument("config", type=str, help="config json path", nargs="?")
args = args_parser.parse_args()
if args.config:
config = None
with open(args.config, "r") as f:
config = json.loads(f.read())
name = config["name"]
desc = config["desc"]
title = config["title"]
repo_name = config["repo_name"]
else:
name = input("Generator name (like fortune, quote): ")
desc = input("Generator desc: ")
title = input("Generator title: ")
repo_name = input("Github repo name: ")
folder_path = f"{name}_generator"
if os.path.exists(folder_path):
logging.error(f"{folder_path} already exists. Please choose another name.")
exit(1)
os.mkdir(folder_path)
os.mkdir(f"{folder_path}/css")
os.mkdir(f"{folder_path}/js")
os.mkdir(f"{folder_path}/images")
os.mkdir(f"{folder_path}/json")
def write_file(src_path, dst_path, **kwargs):
content = None
with open(f"scripts/template/{src_path}", "r") as f:
content = f.read()
for key, val in kwargs.items():
assert (
content.find("{{ %s }}" % key) != -1
), f"The key '{key}' does not appear in scripts/template/{src_path}"
content = content.replace("{{ %s }}" % key, val)
with open(dst_path, "w") as f:
f.write(content)
write_file("css/styles.css", f"{folder_path}/css/styles.css")
write_file("js/main.js", f"{folder_path}/js/{name}.js", name=name)
write_file("js/matrix.js", f"{folder_path}/js/matrix.js")
write_file("js/scripts.js", f"{folder_path}/js/scripts.js")
write_file("js/theme.js", f"{folder_path}/js/theme.js")
write_file(
"js/service-worker.js",
f"{folder_path}/js/service-worker.js",
name=name,
repo_name=repo_name,
folder_path=folder_path,
)
write_file(
"manifest.json",
f"{folder_path}/manifest.json",
title=title,
desc=desc,
repo_name=repo_name,
folder_path=folder_path,
)
write_file("json/themes.json", f"{folder_path}/json/themes.json")
write_file("index.html", f"{folder_path}/index.html", name=name, desc=desc, title=title)
shutil.copytree("scripts/template/images", f"{folder_path}/images", dirs_exist_ok=True)

View File

@@ -0,0 +1,135 @@
:root {
--button-color: #73a3eb;
--button-hover-color: #459aef;
--toggle-theme-button-color: #000000;
--copy-result-button-color: #000000;
--bg-color: #ffffff;
--title-color: #000000cc;
}
* {
overflow: hidden;
text-align: center;
white-space: nowrap;
}
body {
margin: 0;
padding: 0;
height: 100%;
align-items: center;
justify-content: center;
}
.container {
top: 50%;
left: 50%;
width: 80%;
max-width: 800px;
position: absolute;
z-index: 1;
text-align: center;
transform: translate(-50%, -50%);
background-color: var(--bg-color);
border-radius: 40px;
padding: 10px;
}
button {
background-color: var(--button-color);
color: var(--bg-color);
z-index: 2;
font-size: 20px;
border: none;
padding: 20px 20px;
border-radius: 30px;
cursor: pointer;
transition: all 0.3s ease-in-out;
}
button:hover {
background-color: var(--button-hover-color);
}
#Matrix {
z-index: 0;
}
#toggle-theme-button {
margin-top: 15px;
font-size: 2.4rem;
color: var(--toggle-theme-button-color);
cursor: pointer;
opacity: 85%;
}
#copy-result-button {
margin-top: 20px;
font-size: 1.5rem;
color: var(--copy-result-button-color);
}
#themeModal {
.modal-content {
background-color: var(--bg-color) !important;
color: var(--title-color) !important;
}
.modal-header,
.modal-footer {
background-color: var(--bg-color) !important;
color: var(--bg-color) !important;
}
.modal-title,
.btn-close {
color: var(--title-color) !important;
}
}
#themeItem {
background-color: var(--bg-color);
color: var(--title-color);
}
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: var(--bg-color);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
background: var(--button-color);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--button-hover-color);
}
.color-preview-container {
display: flex;
align-items: center;
padding: 3px;
border-radius: 25px;
}
.color-preview {
display: flex; /* Use flex to align dots in a row */
}
.color-dot {
display: inline-block;
width: 12px; /* Dot size */
height: 12px; /* Dot size */
border-radius: 50%; /* Circular shape */
margin-left: 5px; /* Spacing between dots */
}
.color-preview .color-dot:first-child {
margin-left: 0; /* No margin on the left for the first dot */
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 KiB

111
scripts/template/index.html Normal file
View File

@@ -0,0 +1,111 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="{{ desc }}"/>
<title>{{ title }}</title>
<link rel="icon" href="./images/logo.png" />
<link rel="manifest" href="./manifest.json" />
<!-- bootstrap -->
<link
href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css"
rel="stylesheet"
integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH"
crossorigin="anonymous"
/>
<script
src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js"
></script>
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css"
rel="stylesheet"
/>
<script
src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"
integrity="sha512-7tWCgq9tTYS/QkGVyKrtLpqAoMV9XIUxoou+sPUypsaZx56cYR/qio84fPK9EvJJtKvJEwt7vkn6je5UVzGevw=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
></script>
<link rel="stylesheet" href="./css/styles.css" />
<script>
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("./js/service-worker.js");
}
</script>
</head>
<body>
<div class="container">
<div id="init-page">
</div>
<div id="result-page" style="display: none;">
</div>
<div class="row">
<i
class="col-2 fas fa-palette"
id="toggle-theme-button"
data-bs-toggle="modal"
data-bs-target="#themeModal"
></i>
<button class="col-4 offset-2 bi bi-files" id="btn" onclick="get{{ name }}()">
Generate
</button>
<i
class="col-2 offset-2 fas fa-clone d-none"
id="copy-result-button"
onclick="copyResultImageToClipboard()"
></i>
</div>
</div>
<!-- Theme Modal -->
<div
class="modal fade"
id="themeModal"
tabindex="-1"
aria-labelledby="themeModalLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="themeModalLabel">Choose Theme</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
>
</button>
</div>
<div class="modal-body" style="max-height: 70vh; overflow-y: auto">
<ul class="list-group" id="themeList">
<!-- Theme items will be dynamically populated here -->
</ul>
</div>
</div>
</div>
</div>
<canvas id="Matrix"></canvas>
<script src="./js/scripts.js"></script>
<script src="./js/{{ name }}.js"></script>
<script src="./js/matrix.js"></script>
<script src="./js/theme.js"></script>
<script
src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/dist/umd/popper.min.js"
integrity="sha384-I7E8VVD/ismYTF4hNIPjVp/Zjvgyol6VFvRkX/vR+Vc4jQkC+hVqc2pM8ODewa9r"
crossorigin="anonymous"
></script>
<script
src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.min.js"
integrity="sha384-0pUGZvbkm6XF6gxjEnlmuGrJXVbNuzT9qBBavbLwCsOGabYfZo0T0to5eqruptLy"
crossorigin="anonymous"
></script>
</body>
</html>

View File

@@ -0,0 +1,15 @@
function InitPage() {
$("#init-page").show();
$("#result-page").hide();
}
function Appear() {
$("#init-page").hide();
$("#result-page").show();
}
function get{{ name }}() {
Update();
}
InitPage()

View File

@@ -0,0 +1,51 @@
const canvas = document.getElementById("Matrix");
const context = canvas.getContext("2d");
canvas.height = globalThis.innerHeight + 100;
canvas.width = globalThis.innerWidth + 5;
const chars =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./*-+#$%^@!~?><:;[]{}=_αβΓγΔδεζηΘθικΛλμΞξΠπρΣσςτυΦφχΨψΩω×≦≧≠∞≒≡∩∠∟⊿∫∮∵∴¥〒¢£℃€℉╩◢ⅨⅧⅦⅥⅣⅢⅡあいうえおがぎぐげござじずぜぞだぢつでづどにぬのばひぴぶへぺぼみゃょァゐゎè";
const fontSize = 16;
const columns = canvas.width / fontSize;
const charArr = [];
for (let i = 0; i < columns; i++) {
charArr[i] = 1;
}
let frame = 0;
let str;
context.fillStyle = "rgba(0, 0, 0, 1)";
context.fillRect(0, 0, canvas.width, canvas.height);
function Update() {
context.fillStyle = "rgba(0, 0, 0, 0.05)";
context.fillRect(0, 0, canvas.width, canvas.height);
if (frame == 0) {
const a = parseInt(Math.random() * 255);
str = `rgba(${a}, ${Math.abs(a - 127)}, ${Math.abs(a - 255)}, 0.9)`;
}
context.fillStyle = str;
context.font = fontSize + "px monospace";
for (let i = 0; i < columns; i++) {
const text = chars[Math.floor(Math.random() * chars.length)];
context.fillText(text, i * fontSize, charArr[i] * fontSize);
if (charArr[i] * fontSize > canvas.height && Math.random() > 0.90) {
charArr[i] = 0;
}
charArr[i]++;
}
frame++;
if (frame <= 40 * (Math.floor(Math.random() * 10) + 3)) {
requestAnimationFrame(Update); // 40 frames a cycle
} else {
frame = 0;
Appear();
}
}

View File

@@ -0,0 +1,45 @@
function copyResultImageToClipboard() {
try {
const root = document.documentElement;
const backgroundColor = getComputedStyle(root).getPropertyValue('--bg-color');
htmlToImage.toBlob($("#result-page")[0], {
skipFonts: true,
preferredFontFormat: "woff2",
backgroundColor: backgroundColor, // Set background color dynamically
}).then((blob) => {
navigator.clipboard.write([new ClipboardItem({ [blob.type]: blob })]);
showCopiedNotice();
$title.parent().remove();
}).catch((error) => {
console.error("Error converting result page to image:", error);
$title.parent().remove();
});
} catch (error) {
console.error("Error copying result image to clipboard:", error);
}
}
function showCopiedNotice() {
const notice = $("<div>", {
text: "Copied to clipboard!",
css: {
position: "fixed",
bottom: "20px",
right: "20px",
padding: "10px 20px",
backgroundColor: "rgba(0, 0, 0, 0.7)",
color: "#fff",
borderRadius: "5px",
zIndex: 1000,
},
});
$("body").append(notice);
setTimeout(() => {
notice.fadeOut(300, () => {
notice.remove();
});
}, 3000);
}

View File

@@ -0,0 +1,102 @@
const pre_cache_file_version = "pre-v1.0.0";
const auto_cache_file_version = "auto-v1.0.0";
const ASSETS = [
"/{{ repo_name }}/{{ folder_path }}/images/logo-192x192.png",
"/{{ repo_name }}/{{ folder_path }}/images/logo-512x512.png",
"/{{ repo_name }}/{{ folder_path }}/images/logo-180x180.png",
"/{{ repo_name }}/{{ folder_path }}/images/logo-270x270.png",
"/{{ repo_name }}/{{ folder_path }}/images/logo.jpg",
"https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css",
"https://ajax.googleapis.com/ajax/libs/jquery/3.7.1/jquery.min.js",
];
const NEED_UPDATE = [
"/{{ repo_name }}/{{ folder_path }}/",
"/{{ repo_name }}/{{ folder_path }}/index.html",
"/{{ repo_name }}/{{ folder_path }}/css/styles.css",
"/{{ repo_name }}/{{ folder_path }}/js/{{ name }}.js",
"/{{ repo_name }}/{{ folder_path }}/js/matrix.js",
"/{{ repo_name }}/{{ folder_path }}/json/theme.json",
"/{{ repo_name }}/{{ folder_path }}/json/manifest.json",
];
const limit_cache_size = (name, size) => {
caches.open(name).then((cache) => {
cache.keys().then((keys) => {
if (keys.length > size) {
cache.delete(keys[0]).then(() => {
limit_cache_size(name, size);
});
}
});
});
};
const is_in_array = (str, array) => {
let path = "";
// Check the request's domain is the same as the current domain.
if (str.indexOf(self.origin) === 0) {
path = str.substring(self.origin.length); // Remove https://lifeadventurer.github.io
} else {
path = str; // outside request
}
return array.indexOf(path) > -1;
};
// install
self.addEventListener("install", (event) => {
self.skipWaiting();
//pre-cache files
event.waitUntil(
caches.open(pre_cache_file_version).then((cache) => {
cache.addAll(ASSETS);
}),
);
});
// activate
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((keys) => {
return Promise.all(keys.map((key) => {
if (
pre_cache_file_version.indexOf(key) === -1 &&
auto_cache_file_version.indexOf(key) === -1
) {
return caches.delete(key);
}
}));
}),
);
});
// fetch event
self.addEventListener("fetch", (event) => {
if (is_in_array(event.request.url, ASSETS)) {
// cache only strategy
event.respondWith(
caches.match(event.request.url),
);
} else if (is_in_array(event.request.url, NEED_UPDATE)) {
event.respondWith(
fetch(event.request.url).then(async (response) => {
if (response.ok) {
const cache = await caches.open(auto_cache_file_version);
cache.put(event.request.url, response.clone());
return response;
}
throw new Error("Network response was not ok.");
}).catch(async (_error) => {
const cache = await caches.open(auto_cache_file_version);
return cache.match(event.request.url);
}),
);
}
});

View File

@@ -0,0 +1,91 @@
document.addEventListener("DOMContentLoaded", () => {
const themeListContainer = document.querySelector("#themeList");
const root = document.documentElement;
// Apply the saved theme if it exists
applySavedTheme();
async function fetchThemes() {
try {
const response = await fetch("./json/themes.json");
const themes = await response.json();
populateThemeList(themes["themes"]);
} catch (error) {
console.error("Error fetching themes:", error);
}
}
// Populate theme list in modal
function populateThemeList(themes) {
themeListContainer.innerHTML = "";
themes.forEach((theme) => {
const themeItem = document.createElement("div");
themeItem.className =
"theme-item list-group-item d-flex justify-content-between align-items-center";
themeItem.style.cursor = "pointer";
themeItem.id = "themeItem";
// Add theme name
const themeName = document.createElement("span");
themeName.textContent = theme.name;
themeItem.appendChild(themeName);
const colorPreivewContainer = document.createElement("div");
colorPreivewContainer.className = "color-preview-container";
const propertyKeys = Object.keys(theme.properties);
colorPreivewContainer.style.backgroundColor =
theme.properties[propertyKeys[5]];
// Add color dots for visual preview
const colorPreview = document.createElement("div");
colorPreview.className = "color-preview";
Object.values(theme.properties).slice(0, 3).forEach((color) => {
const colorDot = document.createElement("span");
colorDot.style.backgroundColor = color;
colorDot.className = "color-dot";
colorPreview.appendChild(colorDot);
});
colorPreivewContainer.appendChild(colorPreview);
themeItem.appendChild(colorPreivewContainer);
// Apply theme on click
themeItem.addEventListener("click", () => {
applyTheme(theme.properties);
saveThemeToLocalStorage(theme.name);
});
themeListContainer.appendChild(themeItem);
});
}
// Apply theme by setting CSS variables
function applyTheme(properties) {
Object.entries(properties).forEach(([key, value]) => {
root.style.setProperty(`--${key}`, value);
});
}
function saveThemeToLocalStorage(themeName) {
localStorage.setItem("selectedTheme", themeName);
}
function applySavedTheme() {
const savedThemeName = localStorage.getItem("selectedTheme");
if (savedThemeName) {
fetch("./json/themes.json")
.then((response) => response.json())
.then((themes) => {
const theme = themes.themes.find((t) => t.name === savedThemeName);
if (theme) {
applyTheme(theme.properties);
}
})
.catch((error) => console.error("Error fetching themes:", error));
}
}
fetchThemes();
});

View File

@@ -0,0 +1,26 @@
{
"themes": [
{
"name": "Classic Light",
"properties": {
"title-color": "#000000cc",
"bg-color": "#ffffff",
"button-color": "#73a3eb",
"button-hover-color": "#459aef",
"toggle-theme-button-color": "#000000",
"copy-result-button-color": "#000000"
}
},
{
"name": "Classic Dark",
"properties": {
"title-color": "#cdcdcd",
"bg-color": "#1e1d24",
"button-color": "#5d99f4",
"button-hover-color": "#9ac6f1",
"toggle-theme-button-color": "#ffffff",
"copy-result-button-color": "#ffffff"
}
}
]
}

View File

@@ -0,0 +1,32 @@
{
"short_name": "{{ title }}",
"name": "{{ title }}",
"description": "{{ desc }}",
"background_color": "#1a1b1e",
"theme_color": "#1a1b1e",
"icons": [
{
"src": "./images/logo-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./images/logo-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "./images/logo-180x180.png",
"sizes": "180x180",
"type": "image/png"
},
{
"src": "./images/logo-270x270.png",
"sizes": "270x270",
"type": "image/png"
}
],
"start_url": "/{{ repo_name }}/{{ folder_path }}/index.html",
"display": "standalone",
"orientation": "portrait"
}

View File

@@ -1,11 +1,61 @@
:root {
--bg-color: #ffffff;
--title-color: #363636;
/* button */
--button-color: #6c757d;
--button-hover-color: #565e64;
--button-text-color: #ffffff;
/* card-footer */
--card-bg-color: #212529;
--card-title-color: #ffffff;
--card-footer-color: #343a40;
--card-footer-text-color: #adb5bd;
}
.dark-mode {
--bg-color: #000000dd;
--title-color: #ffffffd8;
--dark-mode-icon-color: #efefef;
/* button */
--button-color: #9c9fa2ec;
--button-hover-color: #797d7fec;
--button-text-color: #121212;
/* card-footer */
--card-bg-color: #f8f8f8;
--card-title-color: #3a3a3a;
--card-footer-color: #e1e1e1;
--card-footer-text-color: #4c4c4c;
}
.btn {
background-color: var(--button-color);
color: var(--button-text-color);
}
.btn:hover {
background-color: var(--button-hover-color);
}
body {
background-color: #ffffff;
background-color: var(--bg-color);
}
h1 {
align-items: center;
text-align: center;
color: #363636;
color: var(--title-color);
}
h5 {
color: var(--title-color);
}
.card {
background-color: var(--card-bg-color);
}
.card-title, .card-text {
color: var(--card-title-color);
}
.container {
@@ -13,8 +63,8 @@ h1 {
}
.card-footer {
background-color: #343a40;
color: #adb5bd;
background-color: var(--card-footer-color);
color: var(--card-footer-text-color);
}
#footer-author {
@@ -26,3 +76,16 @@ h1 {
border-radius: 50%;
overflow: hidden;
}
.row {
display: flex;
}
#dark-mode-icon {
margin-left: 25px;
margin-top: 15px;
font-size: 2.4rem;
color: var(--dark-mode-icon-color);
cursor: pointer;
opacity: 85%;
}