This commit is contained in:
tobiichi3227
2025-03-09 08:12:23 +00:00
commit 47097a5efa
51 changed files with 5761 additions and 0 deletions

1
.gitignore vendored Normal file
View File

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

0
.nojekyll Normal file
View File

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

163
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,163 @@
# Contributing
## Fortune Generator
### 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`.

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>.

15
LIST_OF_GENERATORS.md Normal file
View File

@@ -0,0 +1,15 @@
# 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)
### [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.
- remind future special events

29
README.md Normal file
View File

@@ -0,0 +1,29 @@
# Generators
## 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.
## 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)
## 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

6
folders.json Normal file
View File

@@ -0,0 +1,6 @@
{
"folder_paths": [
"fortune_generator",
"quote_generator"
]
}

View File

@@ -0,0 +1,175 @@
: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 */
}

View File

@@ -0,0 +1,182 @@
<!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_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>
<div class="container">
<div class="row">
<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="row">
<div class="col">
<p id="special-day"></p>
</div>
</div>
<div class="row">
<p id="upcoming-event-1"></p>
</div>
<div class="row">
<p id="upcoming-event-2"></p>
</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>
</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">
<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="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>
<!-- 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/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,618 @@
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(commit_hash) {
let prefix = "";
if (commit_hash) {
prefix = `https://raw.githubusercontent.com/LifeAdventurer/generators/${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);
}
}
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];
// 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);
}
}
// 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);
}
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;
Update();
}
}
}
}
// 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 @@
{ "commit_hash": "cf9254c60ccf92d53b2ab263a5a00c487d28f919" }

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,864 @@
{
"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": "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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 802 KiB

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.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 906 KiB

99
index.html Normal file
View File

@@ -0,0 +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_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">
<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 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>
</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

@@ -0,0 +1,82 @@
: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-size: 24px;
}
body {
margin: 0;
padding: 0;
height: 100%;
display: flex;
flex: 1;
align-items: center;
justify-content: center;
}
#Matrix {
z-index: 0;
}
.container {
position: absolute;
z-index: 1;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
max-width: 800px;
margin: 0 auto;
text-align: center;
padding: 70px;
color: var(--text-color);
background-color: var(--bg-color);
border-radius: 30px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.4);
}
.quote-container {
margin: 10px;
}
button {
background-color: var(--button-color);
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;
}
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%;
}

View File

@@ -0,0 +1,41 @@
<!DOCTYPE html>
<html lang="en">
<head>
<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" id="imageContainer">
<h1>Today's quote</h1>
<div class="quote-container">
<p id="quote"></p>
<p id="author"></p>
</div>
<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="./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
}
]
}

62
scripts.js Normal file
View File

@@ -0,0 +1,62 @@
// fetch all folder paths of the generators from `folders.json`
let 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();
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
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;
$(`#last-update-${folderIndex}`).html(
`Last updated ${format_time_difference(timeDifference)} ago`,
);
});
// .catch(error => console.error('Error fetching data:', error));
}
}
// determine whether it is seconds, minutes, hours, or days ago
function format_time_difference(seconds) {
const minutes = Math.floor(seconds / 60);
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" : ""}`;
}
}
get_generator_card_footer();
const darkModeIcon = document.querySelector("#dark-mode-icon");
darkModeIcon.onclick = () => {
darkModeIcon.classList.toggle("bx-sun");
document.body.classList.toggle("dark-mode");
};

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()

91
styles.css Normal file
View File

@@ -0,0 +1,91 @@
: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: var(--bg-color);
}
h1 {
align-items: center;
text-align: center;
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 {
margin-top: 30px;
}
.card-footer {
background-color: var(--card-footer-color);
color: var(--card-footer-text-color);
}
#footer-author {
text-align: right;
}
#footer-author-icon {
width: 4%;
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%;
}