commit 47097a5efa735f6890c58cffbab722c555b21692 Author: tobiichi3227 Date: Sun Mar 9 08:12:23 2025 +0000 deploy: cf9254c60ccf92d53b2ab263a5a00c487d28f919 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb97dbf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +scripts/res.txt diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 0000000..e69de29 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..a1c4588 --- /dev/null +++ b/.pre-commit-config.yaml @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..733eb4d --- /dev/null +++ b/CONTRIBUTING.md @@ -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`. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + 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. + + + Copyright (C) + + 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 . + +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: + + Copyright (C) + 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 +. + + 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 +. diff --git a/LIST_OF_GENERATORS.md b/LIST_OF_GENERATORS.md new file mode 100644 index 0000000..dc4e3df --- /dev/null +++ b/LIST_OF_GENERATORS.md @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..9a0e375 --- /dev/null +++ b/README.md @@ -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. diff --git a/docs/bad_fortune_statistics.png b/docs/bad_fortune_statistics.png new file mode 100644 index 0000000..4ab6f42 Binary files /dev/null and b/docs/bad_fortune_statistics.png differ diff --git a/docs/fortune_statistics.md b/docs/fortune_statistics.md new file mode 100644 index 0000000..4f64ea9 --- /dev/null +++ b/docs/fortune_statistics.md @@ -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) diff --git a/docs/good_fortune_statistics.png b/docs/good_fortune_statistics.png new file mode 100644 index 0000000..0784d84 Binary files /dev/null and b/docs/good_fortune_statistics.png differ diff --git a/folders.json b/folders.json new file mode 100644 index 0000000..c01dcdb --- /dev/null +++ b/folders.json @@ -0,0 +1,6 @@ +{ + "folder_paths": [ + "fortune_generator", + "quote_generator" + ] +} diff --git a/fortune_generator/css/styles.css b/fortune_generator/css/styles.css new file mode 100644 index 0000000..d0474a6 --- /dev/null +++ b/fortune_generator/css/styles.css @@ -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 */ +} diff --git a/fortune_generator/index.html b/fortune_generator/index.html new file mode 100644 index 0000000..5f0faee --- /dev/null +++ b/fortune_generator/index.html @@ -0,0 +1,182 @@ + + + + + + Daily Fortune Generator + + + + + + + + + + + + + +
+
+

+
+ +
+
+
+

+
+
+

+
+
+

+
+
+
+
+

+
+
+
+

+
+
+

+
+
+ + +
+
+

+
+
+
+
+

+
+
+

+
+
+
+
+

+
+
+

+
+
+
+
+
+
+

+
+
+

+
+
+
+
+

+
+
+

+
+
+
+
+ + +
+ + + + +
+
+ + + + + + + + + + + + + + diff --git a/fortune_generator/js/fortune.js b/fortune_generator/js/fortune.js new file mode 100644 index 0000000..b74af1b --- /dev/null +++ b/fortune_generator/js/fortune.js @@ -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 = + `今日運勢`; +const allGood = + `萬事皆宜`; +const allBad = + `諸事不宜`; + +// 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 = + `${ + chineseMonth[month - 1] + "月" + }`; + const showDate = `${ + ("0" + date).slice(-2) + }`; + const showDay = + `${ + "星期" + week[day] + }`; + + $("#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 = + `距離${ + special_events[eventIndexList[eventIndex]].event + }還剩${days}`; + $(`#upcoming-event-${eventIndex + 1}`).html(upcoming_event); + } + } + + // show special event if today is a special day + if (special) { + const special_event_today = + `今日是${ + special_events[special_events_index].event + }`; + $("#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) => + `宜: ${event}`; +const bad_span = (event) => + `忌: ${event}`; +const desc_span = (desc) => + `${desc}`; + +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 = `§ ${fortuneStatus[status_index]} §`; + + if (special) { + status_index = special_events[special_events_index].status_index; + const special_status = `§ ${ + fortuneStatus[status_index] + } §`; + 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(); diff --git a/fortune_generator/js/matrix.js b/fortune_generator/js/matrix.js new file mode 100644 index 0000000..0ee77a9 --- /dev/null +++ b/fortune_generator/js/matrix.js @@ -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(); + } +} diff --git a/fortune_generator/js/scripts.js b/fortune_generator/js/scripts.js new file mode 100644 index 0000000..55fa293 --- /dev/null +++ b/fortune_generator/js/scripts.js @@ -0,0 +1,47 @@ +function copyResultImageToClipboard() { + try { + const $title = $("#title").clone().wrap('
'); + $("#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 = $("
", { + 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); +} diff --git a/fortune_generator/js/service-worker.js b/fortune_generator/js/service-worker.js new file mode 100644 index 0000000..1702ced --- /dev/null +++ b/fortune_generator/js/service-worker.js @@ -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); + }), + ); + } +}); diff --git a/fortune_generator/js/theme.js b/fortune_generator/js/theme.js new file mode 100644 index 0000000..74d3a10 --- /dev/null +++ b/fortune_generator/js/theme.js @@ -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(); +}); diff --git a/fortune_generator/json/commit_hash.json b/fortune_generator/json/commit_hash.json new file mode 100644 index 0000000..a4e6b66 --- /dev/null +++ b/fortune_generator/json/commit_hash.json @@ -0,0 +1 @@ +{ "commit_hash": "cf9254c60ccf92d53b2ab263a5a00c487d28f919" } diff --git a/fortune_generator/json/custom_special.json b/fortune_generator/json/custom_special.json new file mode 100644 index 0000000..2d07d60 --- /dev/null +++ b/fortune_generator/json/custom_special.json @@ -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": "" + } + } + ] +} diff --git a/fortune_generator/json/cyclical_special.json b/fortune_generator/json/cyclical_special.json new file mode 100644 index 0000000..4d801f6 --- /dev/null +++ b/fortune_generator/json/cyclical_special.json @@ -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": "" + } + } + ] +} diff --git a/fortune_generator/json/fortune.json b/fortune_generator/json/fortune.json new file mode 100644 index 0000000..0f1a0d4 --- /dev/null +++ b/fortune_generator/json/fortune.json @@ -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": [ + "缺少食材,口味不佳", + "小心燙傷" + ] + } + ] +} diff --git a/fortune_generator/json/static_special.json b/fortune_generator/json/static_special.json new file mode 100644 index 0000000..05b4658 --- /dev/null +++ b/fortune_generator/json/static_special.json @@ -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": "" + } + } + ] +} diff --git a/fortune_generator/json/themes.json b/fortune_generator/json/themes.json new file mode 100644 index 0000000..ab39ff6 --- /dev/null +++ b/fortune_generator/json/themes.json @@ -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" + } + } + ] +} diff --git a/fortune_generator/manifest.json b/fortune_generator/manifest.json new file mode 100644 index 0000000..9f82ca8 --- /dev/null +++ b/fortune_generator/manifest.json @@ -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" +} diff --git a/images/fortune_generator_example.png b/images/fortune_generator_example.png new file mode 100644 index 0000000..337f3db Binary files /dev/null and b/images/fortune_generator_example.png differ diff --git a/images/lifeadventurer-180x180.png b/images/lifeadventurer-180x180.png new file mode 100644 index 0000000..d97340e Binary files /dev/null and b/images/lifeadventurer-180x180.png differ diff --git a/images/lifeadventurer-192x192.png b/images/lifeadventurer-192x192.png new file mode 100644 index 0000000..cce9ff4 Binary files /dev/null and b/images/lifeadventurer-192x192.png differ diff --git a/images/lifeadventurer-270x270.png b/images/lifeadventurer-270x270.png new file mode 100644 index 0000000..b2dc9d1 Binary files /dev/null and b/images/lifeadventurer-270x270.png differ diff --git a/images/lifeadventurer-512x512.png b/images/lifeadventurer-512x512.png new file mode 100644 index 0000000..b24d70e Binary files /dev/null and b/images/lifeadventurer-512x512.png differ diff --git a/images/lifeadventurer.jpg b/images/lifeadventurer.jpg new file mode 100644 index 0000000..09afa85 Binary files /dev/null and b/images/lifeadventurer.jpg differ diff --git a/images/lifeadventurer.png b/images/lifeadventurer.png new file mode 100644 index 0000000..b11c086 Binary files /dev/null and b/images/lifeadventurer.png differ diff --git a/images/lifeadventurer_rounded_logo.png b/images/lifeadventurer_rounded_logo.png new file mode 100644 index 0000000..6ffb041 Binary files /dev/null and b/images/lifeadventurer_rounded_logo.png differ diff --git a/images/quote_generator_example.png b/images/quote_generator_example.png new file mode 100644 index 0000000..ec10a47 Binary files /dev/null and b/images/quote_generator_example.png differ diff --git a/images/quote_generator_example_(2).png b/images/quote_generator_example_(2).png new file mode 100644 index 0000000..8390c55 Binary files /dev/null and b/images/quote_generator_example_(2).png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..ccb5628 --- /dev/null +++ b/index.html @@ -0,0 +1,99 @@ + + + + + + Generators + + + + + + + + + + +
+
+

+ Generators Gallery +

+
+
+
+
+
+
+
+
+
+ fortune generator example +
+

Fortune Generator

+

+ Get your daily fortune with just a click. +

+ Check this out +
+ +
+
+
+
+ quote generator example +
+

Quote Generator

+

+ Generate inspiring and thought-provoking quotes effortlessly. +

+ Check this out +
+ +
+
+
+
+
+
+ +
+ + + diff --git a/quote_generator/backgrounds/dark/.gitkeep b/quote_generator/backgrounds/dark/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/quote_generator/backgrounds/light/.gitkeep b/quote_generator/backgrounds/light/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/quote_generator/css/styles.css b/quote_generator/css/styles.css new file mode 100644 index 0000000..cc4a71b --- /dev/null +++ b/quote_generator/css/styles.css @@ -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%; +} diff --git a/quote_generator/index.html b/quote_generator/index.html new file mode 100644 index 0000000..a8affa5 --- /dev/null +++ b/quote_generator/index.html @@ -0,0 +1,41 @@ + + + + + + Quote Generator + + + + + + + + + +
+

Today's quote

+
+

+

+
+
+ +
+
+
+ + + + + + + diff --git a/quote_generator/js/matrix.js b/quote_generator/js/matrix.js new file mode 100644 index 0000000..d746c78 --- /dev/null +++ b/quote_generator/js/matrix.js @@ -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(); + } +} diff --git a/quote_generator/js/quote.js b/quote_generator/js/quote.js new file mode 100644 index 0000000..35e885c --- /dev/null +++ b/quote_generator/js/quote.js @@ -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 = `"${quote}"`; + 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(); +} diff --git a/quote_generator/js/scripts.js b/quote_generator/js/scripts.js new file mode 100644 index 0000000..e12cda8 --- /dev/null +++ b/quote_generator/js/scripts.js @@ -0,0 +1,6 @@ +const darkModeIcon = document.querySelector("#dark-mode-icon"); + +darkModeIcon.onclick = () => { + darkModeIcon.classList.toggle("bx-sun"); + document.body.classList.toggle("dark-mode"); +}; diff --git a/quote_generator/json/quotes.json b/quote_generator/json/quotes.json new file mode 100644 index 0000000..40aed0d --- /dev/null +++ b/quote_generator/json/quotes.json @@ -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": "Don’t 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 + } + ] +} diff --git a/scripts.js b/scripts.js new file mode 100644 index 0000000..fbd0d42 --- /dev/null +++ b/scripts.js @@ -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"); +}; diff --git a/scripts/check-events.py b/scripts/check-events.py new file mode 100644 index 0000000..9ea20d2 --- /dev/null +++ b/scripts/check-events.py @@ -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) diff --git a/scripts/check-fortune.py b/scripts/check-fortune.py new file mode 100644 index 0000000..f689720 --- /dev/null +++ b/scripts/check-fortune.py @@ -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) diff --git a/scripts/check-theme.py b/scripts/check-theme.py new file mode 100644 index 0000000..eafba31 --- /dev/null +++ b/scripts/check-theme.py @@ -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) diff --git a/scripts/main.js b/scripts/main.js new file mode 100644 index 0000000..77625ba --- /dev/null +++ b/scripts/main.js @@ -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 +} diff --git a/scripts/plot_gen.py b/scripts/plot_gen.py new file mode 100644 index 0000000..dc10b77 --- /dev/null +++ b/scripts/plot_gen.py @@ -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() diff --git a/styles.css b/styles.css new file mode 100644 index 0000000..75999c2 --- /dev/null +++ b/styles.css @@ -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%; +}