Merge remote-tracking branch 'origin/v3-vitepress'

This commit is contained in:
arkon 2023-09-10 17:57:47 -04:00
commit 437d408649
163 changed files with 12386 additions and 0 deletions

BIN
.github/assets/logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

24
.gitignore vendored Normal file
View File

@ -0,0 +1,24 @@
!.env.example
*.code-workspace
*.log
*.tgz
.cache
.DS_Store
.env
.env.*
.idea
.temp
.vite_opt_cache
.vscode
/coverage
website/src/client/shared.ts
website/src/node/shared.ts
cache
dist
examples-temp
node_modules
pnpm-global
website/src/.temp
website/src/assets/style/sandbox.styl
temp
TODOs.md

76
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,76 @@
# Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at the Tachiyomi [Discord server](https://discord.gg/tachiyomi). All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html)
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
[https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq)

81
CONTRIBUTING.md Normal file
View File

@ -0,0 +1,81 @@
# Tachiyomi Website Contributing Guide
Before submitting your contribution, please make sure to take a moment and read through the following guidelines:
- [Code of Conduct](../CODE_OF_CONDUCT.md)
- [Development Setup](#development-setup)
- [Project Structure](#project-structure)
## Development Setup
You will need [Node.js](http://nodejs.org) **version 18+**, and [pnpm](https://pnpm.io/installation) **version 8+**.
After cloning the repo and entering the the directory, go to the `/website` folder and run:
``` bash
# Installs any dependencies needed.
$ pnpm install
```
To run the project now, run:
``` bash
# This command start a local server you can access and edit live.
$ pnpm dev
```
### Commonly used PNPM scripts
``` bash
# This command will generate a static site inside a dist directory in your project.
$ pnpm build
# Run this command to preview the built files in a local server.
$ pnpm preview
```
**Please make sure to have `pnpm test` pass successfully before submitting a PR.** Although the same tests will be run against your PR on the CI server, it is better to have it working locally.
It is also recommended you lint your files before the PR.
## Project Structure
- **`website`**: contains all the website related files.
- **`src`**: contains all the markdown files used for the website.
- **`.vitepress`**:
- **`theme`**: contains custom theme files.
- `config.ts`: main configuration file for VitePress.
- **`public`**: files to be exposed publicly without any processing.
- **[`dist`](https://vitepress.dev/guide/deploy)**: contains built files for distribution.
Note this directory is only updated when a release happens or when you run the build command.
Changes to this folder will not carry over with Git.
- `package.json`: contains information about which plugins are installed in the project.
## Images and Videos guidelines
### Common
- Use the Android Emulator
- Use the default white theme
- Preferably use local source or a self-hosted extension if your media contains series
- Resize to have a width of 648px
### Images
- Use `.webp`
### Videos
- Use `.webm` format
- Encode it with our [HandBreak profile](../.github/tachiyomi-handbrake-profile.json)
- Remove audio track
## Credits
Thank you to all the people who have already contributed!
[![List of Contributors](https://contrib.rocks/image?repo=tachiyomiorg/website 'List of Contributors')](https://github.com/tachiyomiorg/website/graphs/contributors)

373
LICENSE Normal file
View File

@ -0,0 +1,373 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

64
README.md Normal file
View File

@ -0,0 +1,64 @@
<p align="center">
<br>
<a href="https://tachiyomi.org">
<img src="./.github/assets/logo.png" width="90"/>
</a>
</p>
<h1 align="center">Tachiyomi <a href="#">Website</a></h1>
<h3 align="center">Full-featured reader</h3>
<p align="center">Discover and read manga, webtoons, comics, and more easier than ever on your Android device.</p>
<p align="center">
<a title="Discord server" href="https://discord.gg/tachiyomi">
<img src="https://img.shields.io/discord/349436576037732353.svg?label=&labelColor=6A7EC2&color=7389D8&logo=discord&logoColor=FFFFFF">
</a>
<a title="GitHub downloads" href="https://github.com/tachiyomiorg/tachiyomi/releases">
<img src="https://img.shields.io/github/downloads/tachiyomiorg/tachiyomi/total?label=downloads&labelColor=27303D&color=0D1117&logo=github&logoColor=FFFFFF&style=flat">
</a>
<br>
<a title="Netlify deployment" href="https://app.netlify.com/sites/tachiyomi-v3-vitepress/deploys">
<img src="https://api.netlify.com/api/v1/badges/a2fcfa92-2d32-463c-9051-909f97e06f46/deploy-status">
</a>
<br>
<br>
</p>
<h3 align="center">Contributing</h3>
<p align="center">
<a href="./CODE_OF_CONDUCT.md">Code of conduct</a>
·
<a href="./CONTRIBUTING.md">Contributing guide</a>
·
<a href="https://tachiyomi.org/sandbox/style-guide/">Project style guide</a>
</p>
<p align="center">Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.</p>
<p align="center">If you got any questions, join our <a target="_blank" href="https://discord.gg/tachiyomi">Discord server</a>.</p>
<h3 align="center">Repositories</h3>
<div>
<p align="center">
<a href="https://github.com/tachiyomiorg/tachiyomi/">
<img src="https://github-readme-stats.vercel.app/api/pin/?username=tachiyomiorg&repo=tachiyomi&bg_color=161B22&text_color=c9d1d9&title_color=818CF8&icon_color=818CF8&border_radius=8&hide_border=true" alt="Android App">
</a>
<a href="https://github.com/tachiyomiorg/tachiyomi-extensions/">
<img src="https://github-readme-stats.vercel.app/api/pin/?username=tachiyomiorg&repo=tachiyomi-extensions&bg_color=161B22&text_color=c9d1d9&title_color=818CF8&icon_color=818CF8&border_radius=8&hide_border=true" alt="App Extensions">
</a>
</p>
</div>
<h3 align="center">License</h3>
<pre align="center">Copyright © 2023 The Tachiyomi Open Source Project<br><br>This Source Code Form is subject to the terms of the Mozilla Public<br>License, v. 2.0. If a copy of the MPL was not distributed with this<br>file, You can obtain one at http://mozilla.org/MPL/2.0/.</pre>
<h3 align="center">Credits</h3>
<p align="center">Thank you to all the people who have already contributed!</p>
<p align="right">
<a href="https://github.com/tachiyomiorg/tachiyomi/graphs/contributors">
<img src="https://contrib.rocks/image?repo=tachiyomiorg/website" width="800"/>
</a>
</p>

12
netlify.toml Normal file
View File

@ -0,0 +1,12 @@
[build.environment]
NODE_VERSION = "18"
[build]
base = "website"
publish = "dist"
[context.production]
command = "VITE_HOSTNAME=$URL pnpm build"
[context.deploy-preview]
command = "VITE_HOSTNAME=$DEPLOY_PRIME_URL pnpm build"

13
website/.editorconfig Normal file
View File

@ -0,0 +1,13 @@
root = true
[*]
end_of_line = lf
indent_style = tab
indent_size = 4
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.yml]
indent_style = space
indent_size = 2

12
website/.eslintignore Normal file
View File

@ -0,0 +1,12 @@
*.sh
*.md
*.woff
*.ttf
.vscode
.idea
.husky
.local
dist
node_modules
!docs/.vitepress
docs/.vitepress/cache

39
website/.eslintrc.cjs Normal file
View File

@ -0,0 +1,39 @@
module.exports = {
root: true,
extends: ["@antfu"],
rules: {
"comma-dangle": ["error", "only-multiline"],
"quotes": "off",
"no-tabs": "off",
"arrow-parens": ["error", "always"],
"@typescript-eslint/quotes": ["error", "double", { avoidEscape: true }],
"indent": "off",
"semi": ["error", "never"],
"@typescript-eslint/indent": ["error", "tab"],
"@typescript-eslint/brace-style": ["error", "1tbs"],
"@typescript-eslint/semi": ["error", "never"],
"vue/no-extra-parens": "off",
"vue/html-indent": ["error", "tab"],
"curly": ["error", "all"],
"brace-style": ["error", "1tbs"],
"no-console": "off",
"no-debugger": "off",
"vue/multi-word-component-names": "off",
"vue/comment-directive": "off",
"no-unused-vars": "off",
"vue/no-parsing-error": [
2,
{
"x-invalid-end-tag": false,
"missing-semicolon-after-character-reference": false,
},
],
/* --ECMAScript 6 ES6-- */
"no-useless-escape": "off",
"no-unused-expressions": [
"error",
{ allowShortCircuit: true, allowTernary: true },
],
},
}

View File

@ -0,0 +1,3 @@
{
"extends": "markdownlint/style/prettier"
}

View File

@ -0,0 +1,2 @@
node_modules
README.md

3
website/.npmrc Normal file
View File

@ -0,0 +1,3 @@
public-hoist-pattern[]=*eslint-plugin-*
public-hoist-pattern[]=@typescript-eslint/eslint-plugin
public-hoist-pattern[]=@antfu/*

25
website/.stylelintrc.cjs Normal file
View File

@ -0,0 +1,25 @@
module.exports = {
plugins: ["stylelint-stylus"],
rules: {
"stylus/pythonic": "never",
"stylus/declaration-colon": "always",
"stylus/semicolon": "never",
"stylus/single-line-comment-double-slash-space-after": "always",
"stylus/property-no-unknown": null,
"stylus/selector-type-no-unknown": null,
"stylus/selector-list-comma": "always",
"stylus/indentation": [
"tab",
{
indentInsideParens: "twice",
},
],
"rule-empty-line-before": [
"always",
{
except: ["first-nested"],
},
],
},
extends: ["stylelint-stylus/standard"],
}

47
website/.stylintrc Normal file
View File

@ -0,0 +1,47 @@
{
"blocks": false,
"brackets": "always",
"colons": "always",
"colors": "always",
"commaSpace": "always",
"commentSpace": "always",
"cssLiteral": "never",
"customProperties": [],
"depthLimit": false,
"duplicates": true,
"efficient": "always",
"exclude": [],
"extendPref": "@extends",
"globalDupe": true,
"groupOutputByFile": true,
"indentPref": false,
"leadingZero": "always",
"maxErrors": false,
"maxWarnings": false,
"mixed": true,
"mixins": [],
"namingConvention": false,
"namingConventionStrict": false,
"none": "always",
"noImportant": false,
"parenSpace": "never",
"placeholders": "always",
"prefixVarsWithDollar": "always",
"quotePref": "double",
"reporterOptions": {
"columns": ["lineData", "severity", "description", "rule"],
"columnSplitter": " ",
"showHeaders": false,
"truncate": true
},
"semicolons": "never",
"sortOrder": ["grouped", "alphabetical"],
"stackedProperties": "never",
"trailingWhitespace": "never",
"universal": false,
"valid": true,
"zeroUnits": "never",
"zIndexNormalize": false,
"stylusSupremacy.selectorSeparator": ",\n",
"stylusSupremacy.insertNewLineAroundBlocks": true
}

87
website/package.json Normal file
View File

@ -0,0 +1,87 @@
{
"name": "tachiyomi-website",
"version": "3.0.0",
"description": "Official website for the Tachiyomi app.",
"license": "MPL-2.0",
"private": true,
"type": "module",
"engines": {
"node": ">=18",
"pnpm": ">=8"
},
"repository": {
"type": "git",
"url": "git+https://github.com/tachiyomiorg/website.git"
},
"bugs": {
"url": "https://github.com/tachiyomiorg/website/issues"
},
"scripts": {
"preinstall": "npx only-allow pnpm",
"test": "pnpm lint && pnpm build && pnpm preview",
"dev": "vitepress dev src",
"build": "vitepress build src",
"preview": "vitepress preview src",
"lint": "pnpm lint:es && pnpm lint:mdl && pnpm lint:style",
"lint:fix": "pnpm lint:es:fix && pnpm lint:style:fix",
"lint:es": "eslint . --ext .vue,.js,.ts,.cjs,.mjs,.jsx,.tsx",
"lint:es:fix": "eslint . --ext .vue,.js,.ts,.cjs,.mjs,.jsx,.tsx --fix",
"lint:mdl": "markdownlint \"**/*.md\" \".github/**/*.md\" --enable sentences-per-line --disable MD025 MD033",
"lint:style": "stylelint \"**/*.{styl,vue}\" \"src/.vitepress/**/*.{styl,vue}\"",
"lint:style:fix": "stylelint --fix \"**/*.{styl,vue}\" \"src/.vitepress/**/*.{styl,vue}\""
},
"dependencies": {
"@iconify-prerendered/vue-mdi": "0.23.1689058119",
"@octokit/rest": "20.0.1",
"@octokit/types": "11.1.0",
"@tanstack/vue-query": "4.35.2",
"@vueuse/core": "10.4.1",
"axios": "1.5.0",
"element-plus": "2.3.12",
"lodash.groupby": "4.6.0",
"markdown-it": "13.0.1",
"markdown-it-shortcode-tag": "1.1.0",
"moment": "2.29.4"
},
"devDependencies": {
"@antfu/eslint-config": "0.41.0",
"@mdit/plugin-attrs": "0.4.8",
"@mdit/plugin-figure": "0.4.8",
"@mdit/plugin-img-lazyload": "0.4.8",
"@mdit/plugin-img-mark": "0.4.8",
"@mdit/plugin-img-size": "0.4.8",
"@mdit/plugin-include": "0.4.8",
"@resvg/resvg-js": "2.4.1",
"@types/gtag.js": "0.0.13",
"@types/lodash.groupby": "4.6.7",
"@types/markdown-it": "13.0.1",
"@types/node": "20.6.0",
"@typescript-eslint/eslint-plugin": "6.6.0",
"@typescript-eslint/parser": "6.6.0",
"eslint": "8.49.0",
"eslint-config-standard": "17.1.0",
"eslint-plugin-vue": "9.17.0",
"feed": "4.2.2",
"lint-staged": "14.0.1",
"markdownlint": "0.31.0",
"markdownlint-cli": "0.36.0",
"sentences-per-line": "0.2.1",
"stylelint": "15.10.3",
"stylelint-stylus": "0.18.0",
"stylus": "0.60.0",
"unplugin-element-plus": "0.8.0",
"vite-plugin-eslint": "1.8.1",
"vitepress": "1.0.0-rc.11",
"vitepress-plugin-tabs": "0.3.0",
"vue": "3.3.4",
"vue-eslint-parser": "9.3.1",
"x-satori": "0.1.5"
},
"simple-git-hooks": {
"pre-commit": "pnpm lint-staged"
},
"lint-staged": {
"*.{styl,vue}": "stylelint --fix",
"*.{html,json}": "prettier --write"
}
}

5730
website/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,72 @@
import process from "node:process"
import { URL, fileURLToPath } from "node:url"
import { defineConfig, loadEnv } from "vitepress"
import ElementPlus from "unplugin-element-plus/vite"
import markdownConfig from "./config/markdownConfig"
// For use with loading Markdown plugins
import themeConfig from "./config/themeConfig"
// Theme related config
import headConfig from "./config/headConfig" // Provides how to generate Meta head tags
import generateMeta from "./config/hooks/generateMeta"
// Enhanced meta generation
import generateFeed from "./config/hooks/generateFeed" // Allows generation of RSS feed
import generateOgImages from "./config/hooks/generateOgImages"
const title = "Tachiyomi"
const description = "Discover and read manga, webtoons, comics, and more easier than ever on your Android device."
const env = loadEnv("", process.cwd())
const hostname: string = env.VITE_HOSTNAME || "http://localhost:4173"
export default defineConfig({
outDir: "../dist",
lastUpdated: true,
cleanUrls: true,
title,
description,
sitemap: {
hostname,
},
head: headConfig,
markdown: markdownConfig,
themeConfig,
transformHead: async (context) => generateMeta(context, hostname),
buildEnd: async (context) => {
generateFeed(context, hostname)
generateOgImages(context)
},
vite: {
resolve: {
alias: [
{
// Used to show the release version on navbar.
find: /^.*\/VPNavBarMenu\.vue$/,
replacement: fileURLToPath(
new URL("./theme/components/CustomNavBarMenu.vue", import.meta.url),
),
},
{
find: /^.*VPNavScreenMenu\.vue$/,
replacement: fileURLToPath(
new URL("./theme/components/CustomNavScreenMenu.vue", import.meta.url),
),
},
{
find: /^.*VPSwitchAppearance\.vue$/,
replacement: fileURLToPath(
new URL("./theme/components/CustomSwitchAppearance.vue", import.meta.url),
),
},
],
},
plugins: [ElementPlus({})],
ssr: {
noExternal: ["element-plus"],
},
},
})

View File

@ -0,0 +1,6 @@
export const GITHUB_EXTENSION_JSON
= "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/index.json"
export const GITHUB_STABLE_API = "https://api.github.com/repos/tachiyomiorg/tachiyomi/releases/latest"
export const GITHUB_STABLE_RELEASE = "https://github.com/tachiyomiorg/tachiyomi/releases/latest"
export const GITHUB_PREVIEW_API = "https://api.github.com/repos/tachiyomiorg/android-app-preview/releases/latest"
export const GITHUB_PREVIEW_RELEASE = "https://github.com/tachiyomiorg/android-app-preview/releases/latest"

View File

@ -0,0 +1,59 @@
import type { HeadConfig } from "vitepress"
const headConfig: HeadConfig[] = [
["meta", { name: "darkreader-lock" }],
["meta", { name: "theme-color", content: "#818CF8" }],
["meta", { name: "msapplication-TileColor", content: "#818CF8" }],
["meta", { name: "viewport", content: "width=device-width, initial-scale=1.0" }],
["meta", { name: "referrer", content: "no-referrer-when-downgrade" }],
["link", { rel: "icon", type: "image/x-icon", href: "/favicon.ico" }],
[
"link",
{
rel: "icon",
type: "image/png",
sizes: "32x32",
href: "/favicon-32x32.png",
},
],
[
"link",
{
rel: "icon",
type: "image/png",
sizes: "16x16",
href: "/favicon-16x16.png",
},
],
["link", { rel: "manifest", href: "/site.webmanifest" }],
["link", { rel: "mask-icon", href: "/safari-pinned-tab.svg", color: "#818CF8" }],
[
"link",
{
rel: "apple-touch-icon",
type: "image/x-icon",
sizes: "180x180",
href: "/favicon.ico",
},
],
["meta", { name: "twitter:card", content: "summary" }],
["meta", { name: "twitter:site", content: "@tachiyomiorg" }],
["meta", { name: "twitter:creator", content: "@tachiyomiorg" }],
["meta", { property: "og:site_name", content: "Tachiyomi" }],
[
"meta",
{
property: "og:description",
content: "Discover and read manga, webtoons, comics, and more easier than ever on your Android device.",
},
],
["meta", { property: "og:locale", content: "en_US" }],
["meta", { property: "og:type", content: "website" }],
]
export default headConfig

View File

@ -0,0 +1,61 @@
import path from "node:path"
import { writeFileSync } from "node:fs"
import { Feed, type Item } from "feed"
import { type SiteConfig, createContentLoader } from "vitepress"
async function generateFeed(config: SiteConfig, hostname: string) {
const feed = new Feed({
title: config.site.title,
description: config.site.description,
id: hostname,
link: hostname,
language: "en",
image: `${hostname}/img/logo.png`,
favicon: `${hostname}/favicon.ico`,
copyright: `Copyright © 2015 - ${new Date().getFullYear()} Javier Tomás`,
})
const json: Item[] = []
const posts = await createContentLoader("news/*.md", {
excerpt: true,
render: true,
includeSrc: true,
}).load()
// Filter everything that"s not of type `article` (e.g. index.md)
const filteredPosts = posts.filter((post) => post.frontmatter.type === "article")
filteredPosts.sort((a, b) => +new Date(b.frontmatter.date as string) - +new Date(a.frontmatter.date as string))
for (const { url, frontmatter, html, src } of filteredPosts) {
const fullUrl = `${hostname}${url}`
// Strip `&ZeroWidthSpace;` from `html` string
const content = (html ?? "")
.replace(/&ZeroWidthSpace;/g, "")
.replace(/<a href="(\/.*?)">/g, `<a href="${hostname}$1">`)
const markdown = (src ?? "")
.replace(/^---.*---/s, "")
.replace(/]\((\/.*?)\)/g, `](${hostname}$1)`)
.replace(/^# .*$/m, "")
.trim()
const post = {
title: frontmatter.title,
id: fullUrl,
link: fullUrl,
description: frontmatter.description,
content,
date: frontmatter.date,
} satisfies Item
feed.addItem(post)
json.push({ ...post, content: markdown })
}
writeFileSync(path.join(config.outDir, "feed.rss"), feed.rss2())
writeFileSync(path.join(config.outDir, "news.json"), JSON.stringify(json))
}
export default generateFeed

View File

@ -0,0 +1,131 @@
import type { HeadConfig, TransformContext } from "vitepress"
function generateMeta(context: TransformContext, hostname: string) {
const head: HeadConfig[] = []
const { pageData } = context
const url = `${hostname}/${pageData.relativePath.replace(/((^|\/)index)?\.md$/, "$2")}`
head.push(["link", { rel: "canonical", href: url }])
head.push(["meta", { property: "og:url", content: url }])
head.push(["meta", { name: "twitter:url", content: url }])
head.push(["meta", { name: "twitter:card", content: "summary_large_image" }])
if (pageData.frontmatter.theme) {
head.push(["meta", { name: "theme-color", content: pageData.frontmatter.theme }])
}
if (pageData.frontmatter.type) {
head.push(["meta", { property: "og:type", content: pageData.frontmatter.type }])
}
if (pageData.frontmatter.customMetaTitle) {
head.push([
"meta",
{
property: "og:title",
content: pageData.frontmatter.customMetaTitle,
},
])
head.push([
"meta",
{
name: "twitter:title",
content: pageData.frontmatter.customMetaTitle,
},
])
head.push(["meta", { property: "og:site_name", content: "" }])
} else {
head.push(["meta", { property: "og:title", content: pageData.frontmatter.title }])
head.push(["meta", { name: "twitter:title", content: pageData.frontmatter.title }])
}
if (pageData.frontmatter.description) {
head.push([
"meta",
{
property: "og:description",
content: pageData.frontmatter.description,
},
])
head.push([
"meta",
{
name: "twitter:description",
content: pageData.frontmatter.description,
},
])
}
if (pageData.frontmatter.image) {
head.push([
"meta",
{
property: "og:image",
content: `${hostname}/${pageData.frontmatter.image.replace(/^\//, "")}`,
},
])
head.push([
"meta",
{
name: "twitter:image",
content: `${hostname}/${pageData.frontmatter.image.replace(/^\//, "")}`,
},
])
} else {
const url = pageData.filePath.replace("index.md", "").replace(".md", "")
const imageUrl = `${url}/__og_image__/og.png`.replace(/\/\//g, "/").replace(/^\//, "")
head.push(["meta", { property: "og:image", content: `${hostname}/${imageUrl}` }])
head.push(["meta", { property: "og:image:width", content: "1200" }])
head.push(["meta", { property: "og:image:height", content: "628" }])
head.push(["meta", { property: "og:image:type", content: "image/png" }])
head.push(["meta", { property: "og:image:alt", content: pageData.frontmatter.title }])
head.push(["meta", { name: "twitter:image", content: `${hostname}/${imageUrl}` }])
head.push(["meta", { name: "twitter:image:width", content: "1200" }])
head.push(["meta", { name: "twitter:image:height", content: "628" }])
head.push(["meta", { name: "twitter:image:alt", content: pageData.frontmatter.title }])
}
if (pageData.frontmatter.tag) {
head.push(["meta", { property: "article:tag", content: pageData.frontmatter.tag }])
}
if (pageData.frontmatter.date) {
head.push([
"meta",
{
property: "article:published_time",
content: pageData.frontmatter.date,
},
])
}
if (pageData.lastUpdated && pageData.frontmatter.lastUpdated !== false) {
head.push([
"meta",
{
property: "article:modified_time",
content: new Date(pageData.lastUpdated).toISOString(),
},
])
}
if (pageData.filePath === "news/index.md") {
head.push([
"link",
{
rel: "alternate",
type: "application/rss+xml",
title: "RSS feed for the news archive",
href: `${hostname}/feed.rss`,
},
])
head.push([
"link",
{
rel: "alternate",
type: "application/json",
title: "JSON of the news archive",
href: `${hostname}/news.json`,
},
])
}
return head
}
export default generateMeta

View File

@ -0,0 +1,111 @@
import { mkdir, readFile, writeFile } from "node:fs/promises"
import { dirname, resolve } from "node:path"
import { fileURLToPath } from "node:url"
import { createContentLoader } from "vitepress"
import type { ContentData, SiteConfig } from "vitepress"
import { type SatoriOptions, satoriVue } from "x-satori/vue"
import { renderAsync } from "@resvg/resvg-js"
const __dirname = dirname(fileURLToPath(import.meta.url))
const __fonts = resolve(__dirname, "../../fonts")
async function generateOgImages(config: SiteConfig) {
const pages = await createContentLoader("**/*.md", { excerpt: true }).load()
const template = await readFile(resolve(__dirname, "../../theme/components/OgImageTemplate.vue"), "utf-8")
const fonts: SatoriOptions["fonts"] = [
{
name: "Inter",
data: await readFile(resolve(__fonts, "Inter-Regular.otf")),
weight: 400,
style: "normal",
},
{
name: "Inter",
data: await readFile(resolve(__fonts, "Inter-Medium.otf")),
weight: 500,
style: "normal",
},
{
name: "Inter",
data: await readFile(resolve(__fonts, "Inter-SemiBold.otf")),
weight: 600,
style: "normal",
},
{
name: "Inter",
data: await readFile(resolve(__fonts, "Inter-Bold.otf")),
weight: 700,
style: "normal",
},
]
const filteredPages = pages.filter((p) => p.frontmatter.image === undefined)
for (const page of filteredPages) {
await generateImage({
page,
template,
outDir: config.outDir,
fonts,
})
}
}
export default generateOgImages
interface GenerateImagesOptions {
page: ContentData
template: string
outDir: string
fonts: SatoriOptions["fonts"]
}
function getDir(url: string) {
if (url.startsWith("/docs/faq/")) {
return "FAQ"
} else if (url.startsWith("/docs/guides/")) {
return "Guide"
} else if (url.startsWith("/news/") && url !== "/news/") {
return "News"
}
return undefined
}
async function generateImage({ page, template, outDir, fonts }: GenerateImagesOptions) {
const { frontmatter, url } = page
const options: SatoriOptions = {
width: 1200,
height: 628,
fonts,
props: {
title:
frontmatter.layout === "home"
? frontmatter.hero.name ?? frontmatter.title
: frontmatter.customMetaTitle ?? frontmatter.title,
description:
frontmatter.layout === "home"
? frontmatter.hero.tagline ?? frontmatter.description
: frontmatter.description,
dir: getDir(url),
},
}
const svg = await satoriVue(options, template)
const render = await renderAsync(svg, {
fitTo: {
mode: "width",
value: 1200,
},
})
const outputFolder = resolve(outDir, url.substring(1), "__og_image__")
const outputFile = resolve(outputFolder, "og.png")
await mkdir(outputFolder, { recursive: true })
return await writeFile(outputFile, render.asPng())
}

View File

@ -0,0 +1,29 @@
import type { MarkdownOptions } from "vitepress"
import { attrs } from "@mdit/plugin-attrs"
import { figure } from "@mdit/plugin-figure"
import { imgLazyload } from "@mdit/plugin-img-lazyload"
import { imgMark } from "@mdit/plugin-img-mark"
import { imgSize } from "@mdit/plugin-img-size"
import { include } from "@mdit/plugin-include"
import { tabsMarkdownPlugin } from "vitepress-plugin-tabs"
import shortcode_plugin from "markdown-it-shortcode-tag"
import shortcodes from "./shortcodes"
const markdownConfig: MarkdownOptions = {
config: (md) => {
md
.use(attrs)
.use(figure)
.use(imgLazyload)
.use(imgMark)
.use(imgSize)
.use(include, {
currentPath: (env) => env.filePath,
})
.use(tabsMarkdownPlugin)
.use(shortcode_plugin, shortcodes)
},
}
export default markdownConfig

View File

@ -0,0 +1,30 @@
import type { DefaultTheme } from "vitepress"
const nav: DefaultTheme.NavItem[] = [
{
text: "Get v{app_version}",
activeMatch: "^/*?(download|changelogs)/*?$",
items: [
{
text: "Download",
link: "/download/",
},
{
text: "Changelogs",
link: "/changelogs/",
},
],
},
{
text: "Docs",
link: "/docs/guides/getting-started",
activeMatch: "/docs/",
},
{
text: "News",
link: "/news/",
activeMatch: "/news/",
},
]
export default nav

View File

@ -0,0 +1,130 @@
import type { DefaultTheme } from "vitepress"
const sidebar: DefaultTheme.SidebarMulti = {
"/download/": defaultSidebar(),
"/extensions/": defaultSidebar(),
"/docs/": defaultSidebar(),
"/forks/": defaultSidebar(),
"/changelogs/": defaultSidebar(),
"/news/": defaultSidebar(),
"/sandbox/": defaultSidebar(),
}
function defaultSidebar(): DefaultTheme.SidebarItem[] {
return [
{
items: [
{
text: "Download",
link: "/download/",
},
{
text: "Extensions",
link: "/extensions/",
},
{
text: "Changelogs",
link: "/changelogs/",
},
{
text: "Forks",
link: "/forks/",
},
{
text: "Contribute",
link: "/docs/contribute",
},
],
},
{
text: "Frequently Asked Questions",
items: [
{ text: "General", link: "/docs/faq/general" },
{
text: "Library",
link: "/docs/faq/library",
},
{
text: "Browse",
link: "/docs/faq/browse/",
collapsed: true,
items: [
{ text: "Extensions", link: "/docs/faq/browse/extensions" },
{
text: "Local source",
link: "/docs/faq/browse/local-source",
},
],
},
{
text: "Downloads",
link: "/docs/faq/downloads",
},
{
text: "Reader",
link: "/docs/faq/reader",
},
{
text: "Settings",
link: "/docs/faq/settings",
},
{
text: "Android 11+",
link: "/docs/faq/android-11+",
},
],
},
{
text: "Guides",
items: [
{
text: "Getting started",
link: "/docs/guides/getting-started",
},
{
text: "Troubleshooting",
link: "/docs/guides/troubleshooting/",
collapsed: true,
items: [
{
text: "Common issues",
link: "/docs/guides/troubleshooting/common-issues",
},
{
text: "Diagnosis",
link: "/docs/guides/troubleshooting/diagnosis",
},
],
},
{
text: "Source migration",
link: "/docs/guides/source-migration",
},
{ text: "Backups", link: "/docs/guides/backups" },
{ text: "Tracking", link: "/docs/guides/tracking" },
{ text: "Categories", link: "/docs/guides/categories" },
{
text: "Local source",
link: "/docs/guides/local-source/",
collapsed: true,
items: [
{
text: "Advanced editing",
link: "/docs/guides/local-source/advanced",
},
],
},
{
text: "Reader settings",
link: "/docs/guides/reader-settings",
},
{
text: "Shizuku",
link: "/docs/guides/shizuku",
},
],
},
]
}
export default sidebar

View File

@ -0,0 +1,21 @@
export function simpleLangName(code: string) {
if (code === "all") {
return "All"
}
const namesInEnglish = new Intl.DisplayNames(["en"], { type: "language" })
return capitalize(namesInEnglish.of(code)!, "en")
}
export function langName(code: string) {
if (code === "all") {
return "All"
}
const namesInEnglish = new Intl.DisplayNames(["en"], { type: "language" })
const namesInNative = new Intl.DisplayNames([code], { type: "language" })
return `${capitalize(namesInEnglish.of(code)!, "en")} - ${capitalize(namesInNative.of(code)!, code)}`
}
function capitalize(string: string, locale: string) {
return string.charAt(0).toLocaleUpperCase(locale) + string.substring(1)
}

View File

@ -0,0 +1,91 @@
const iconMappings = {
alertDecagramOutline: '<svg originalIcon="alertDecagramOutline" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M23,12L20.56,14.78L20.9,18.46L17.29,19.28L15.4,22.46L12,21L8.6,22.47L6.71,19.29L3.1,18.47L3.44,14.78L1,12L3.44,9.21L3.1,5.53L6.71,4.72L8.6,1.54L12,3L15.4,1.54L17.29,4.72L20.9,5.54L20.56,9.22L23,12M20.33,12L18.5,9.89L18.74,7.1L16,6.5L14.58,4.07L12,5.18L9.42,4.07L8,6.5L5.26,7.09L5.5,9.88L3.67,12L5.5,14.1L5.26,16.9L8,17.5L9.42,19.93L12,18.81L14.58,19.92L16,17.5L18.74,16.89L18.5,14.1L20.33,12M11,15H13V17H11V15M11,7H13V13H11V7" /></svg>',
backupRestore: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,3A9,9 0 0,0 3,12H0L4,16L8,12H5A7,7 0 0,1 12,5A7,7 0 0,1 19,12A7,7 0 0,1 12,19C10.5,19 9.09,18.5 7.94,17.7L6.5,19.14C8.04,20.3 9.94,21 12,21A9,9 0 0,0 21,12A9,9 0 0,0 12,3M14,12A2,2 0 0,0 12,10A2,2 0 0,0 10,12A2,2 0 0,0 12,14A2,2 0 0,0 14,12Z" /></svg>',
bookmarkBoxOutline: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 20H18V22H4C2.9 22 2 21.1 2 20V6H4V20M22 4V16C22 17.1 21.1 18 20 18H8C6.9 18 6 17.1 6 16V4C6 2.9 6.9 2 8 2H20C21.1 2 22 2.9 22 4M20 4H8V16H20V4M18 6H13V13L15.5 11.5L18 13V6Z" /></svg>',
bookOpenOutline: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21,4H3A2,2 0 0,0 1,6V19A2,2 0 0,0 3,21H21A2,2 0 0,0 23,19V6A2,2 0 0,0 21,4M3,19V6H11V19H3M21,19H13V6H21V19M14,9.5H20V11H14V9.5M14,12H20V13.5H14V12M14,14.5H20V16H14V14.5Z" /></svg>',
cloudOffOutline: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19.8 22.6L17.15 20H6.5Q4.2 20 2.6 18.4T1 14.5Q1 12.58 2.19 11.08 3.38 9.57 5.25 9.15 5.33 8.95 5.4 8.76 5.5 8.57 5.55 8.35L1.4 4.2L2.8 2.8L21.2 21.2M6.5 18H15.15L7.1 9.95Q7.05 10.23 7.03 10.5 7 10.73 7 11H6.5Q5.05 11 4.03 12.03 3 13.05 3 14.5 3 15.95 4.03 17 5.05 18 6.5 18M11.13 14M21.6 18.75L20.15 17.35Q20.58 17 20.79 16.54 21 16.08 21 15.5 21 14.45 20.27 13.73 19.55 13 18.5 13H17V11Q17 8.93 15.54 7.46 14.08 6 12 6 11.33 6 10.7 6.16 10.07 6.33 9.5 6.68L8.05 5.23Q8.93 4.63 9.91 4.31 10.9 4 12 4 14.93 4 16.96 6.04 19 8.07 19 11 20.73 11.2 21.86 12.5 23 13.78 23 15.5 23 16.5 22.63 17.31 22.25 18.15 21.6 18.75M14.83 12.03Z" /></svg>',
codeTags: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14.6,16.6L19.2,12L14.6,7.4L16,6L22,12L16,18L14.6,16.6M9.4,16.6L4.8,12L9.4,7.4L8,6L2,12L8,18L9.4,16.6Z" /></svg>',
cog: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,15.5A3.5,3.5 0 0,1 8.5,12A3.5,3.5 0 0,1 12,8.5A3.5,3.5 0 0,1 15.5,12A3.5,3.5 0 0,1 12,15.5M19.43,12.97C19.47,12.65 19.5,12.33 19.5,12C19.5,11.67 19.47,11.34 19.43,11L21.54,9.37C21.73,9.22 21.78,8.95 21.66,8.73L19.66,5.27C19.54,5.05 19.27,4.96 19.05,5.05L16.56,6.05C16.04,5.66 15.5,5.32 14.87,5.07L14.5,2.42C14.46,2.18 14.25,2 14,2H10C9.75,2 9.54,2.18 9.5,2.42L9.13,5.07C8.5,5.32 7.96,5.66 7.44,6.05L4.95,5.05C4.73,4.96 4.46,5.05 4.34,5.27L2.34,8.73C2.21,8.95 2.27,9.22 2.46,9.37L4.57,11C4.53,11.34 4.5,11.67 4.5,12C4.5,12.33 4.53,12.65 4.57,12.97L2.46,14.63C2.27,14.78 2.21,15.05 2.34,15.27L4.34,18.73C4.46,18.95 4.73,19.03 4.95,18.95L7.44,17.94C7.96,18.34 8.5,18.68 9.13,18.93L9.5,21.58C9.54,21.82 9.75,22 10,22H14C14.25,22 14.46,21.82 14.5,21.58L14.87,18.93C15.5,18.67 16.04,18.34 16.56,17.94L19.05,18.95C19.27,19.03 19.54,18.95 19.66,18.73L21.66,15.27C21.78,15.05 21.73,14.78 21.54,14.63L19.43,12.97Z" /></svg>',
compassOutline: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7,17L10.2,10.2L17,7L13.8,13.8L7,17M12,11.1A0.9,0.9 0 0,0 11.1,12A0.9,0.9 0 0,0 12,12.9A0.9,0.9 0 0,0 12.9,12A0.9,0.9 0 0,0 12,11.1M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z" /></svg>',
dotsHorizontal: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16,12A2,2 0 0,1 18,10A2,2 0 0,1 20,12A2,2 0 0,1 18,14A2,2 0 0,1 16,12M10,12A2,2 0 0,1 12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12M4,12A2,2 0 0,1 6,10A2,2 0 0,1 8,12A2,2 0 0,1 6,14A2,2 0 0,1 4,12Z" /></svg>',
downloadOutline: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13,5V11H14.17L12,13.17L9.83,11H11V5H13M15,3H9V9H5L12,16L19,9H15V3M19,18H5V20H19V18Z" /></svg>',
glasses: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3,10C2.76,10 2.55,10.09 2.41,10.25C2.27,10.4 2.21,10.62 2.24,10.86L2.74,13.85C2.82,14.5 3.4,15 4,15H7C7.64,15 8.36,14.44 8.5,13.82L9.56,10.63C9.6,10.5 9.57,10.31 9.5,10.19C9.39,10.07 9.22,10 9,10H3M7,17H4C2.38,17 0.96,15.74 0.76,14.14L0.26,11.15C0.15,10.3 0.39,9.5 0.91,8.92C1.43,8.34 2.19,8 3,8H9C9.83,8 10.58,8.35 11.06,8.96C11.17,9.11 11.27,9.27 11.35,9.45C11.78,9.36 12.22,9.36 12.64,9.45C12.72,9.27 12.82,9.11 12.94,8.96C13.41,8.35 14.16,8 15,8H21C21.81,8 22.57,8.34 23.09,8.92C23.6,9.5 23.84,10.3 23.74,11.11L23.23,14.18C23.04,15.74 21.61,17 20,17H17C15.44,17 13.92,15.81 13.54,14.3L12.64,11.59C12.26,11.31 11.73,11.31 11.35,11.59L10.43,14.37C10.07,15.82 8.56,17 7,17M15,10C14.78,10 14.61,10.07 14.5,10.19C14.42,10.31 14.4,10.5 14.45,10.7L15.46,13.75C15.64,14.44 16.36,15 17,15H20C20.59,15 21.18,14.5 21.25,13.89L21.76,10.82C21.79,10.62 21.73,10.4 21.59,10.25C21.45,10.09 21.24,10 21,10H15Z" /></svg>',
helpCircleOutline: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11,18H13V16H11V18M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,6A4,4 0 0,0 8,10H10A2,2 0 0,1 12,8A2,2 0 0,1 14,10C14,12 11,11.75 11,15H13C13,12.75 16,12.5 16,10A4,4 0 0,0 12,6Z" /></svg>',
history: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13.5,8H12V13L16.28,15.54L17,14.33L13.5,12.25V8M13,3A9,9 0 0,0 4,12H1L4.96,16.03L9,12H6A7,7 0 0,1 13,5A7,7 0 0,1 20,12A7,7 0 0,1 13,19C11.07,19 9.32,18.21 8.06,16.94L6.64,18.36C8.27,20 10.5,21 13,21A9,9 0 0,0 22,12A9,9 0 0,0 13,3" /></svg>',
informationOutline: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11,9H13V7H11M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M11,17H13V11H11V17Z" /></svg>',
labelOutline: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16,17H5V7H16L19.55,12M17.63,5.84C17.27,5.33 16.67,5 16,5H5A2,2 0 0,0 3,7V17A2,2 0 0,0 5,19H16C16.67,19 17.27,18.66 17.63,18.15L22,12L17.63,5.84Z" /></svg>',
paletteOutline: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2C17.5,2 22,6 22,11A6,6 0 0,1 16,17H14.2C13.9,17 13.7,17.2 13.7,17.5C13.7,17.6 13.8,17.7 13.8,17.8C14.2,18.3 14.4,18.9 14.4,19.5C14.5,20.9 13.4,22 12,22M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C12.3,20 12.5,19.8 12.5,19.5C12.5,19.3 12.4,19.2 12.4,19.1C12,18.6 11.8,18.1 11.8,17.5C11.8,16.1 12.9,15 14.3,15H16A4,4 0 0,0 20,11C20,7.1 16.4,4 12,4M6.5,10C7.3,10 8,10.7 8,11.5C8,12.3 7.3,13 6.5,13C5.7,13 5,12.3 5,11.5C5,10.7 5.7,10 6.5,10M9.5,6C10.3,6 11,6.7 11,7.5C11,8.3 10.3,9 9.5,9C8.7,9 8,8.3 8,7.5C8,6.7 8.7,6 9.5,6M14.5,6C15.3,6 16,6.7 16,7.5C16,8.3 15.3,9 14.5,9C13.7,9 13,8.3 13,7.5C13,6.7 13.7,6 14.5,6M17.5,10C18.3,10 19,10.7 19,11.5C19,12.3 18.3,13 17.5,13C16.7,13 16,12.3 16,11.5C16,10.7 16.7,10 17.5,10Z" /></svg>',
queryStats: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960"><path d="m105-233-65-47 200-320 120 140 160-260 109 163q-23 1-43.5 5.5T545-539l-22-33-152 247-121-141-145 233ZM863-40 738-165q-20 14-44.5 21t-50.5 7q-75 0-127.5-52.5T463-317q0-75 52.5-127.5T643-497q75 0 127.5 52.5T823-317q0 26-7 50.5T795-221L920-97l-57 57ZM643-217q42 0 71-29t29-71q0-42-29-71t-71-29q-42 0-71 29t-29 71q0 42 29 71t71 29Zm89-320q-19-8-39.5-13t-42.5-6l205-324 65 47-188 296Z"/></svg>',
security: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,12H19C18.47,16.11 15.72,19.78 12,20.92V12H5V6.3L12,3.19M12,1L3,5V11C3,16.55 6.84,21.73 12,23C17.16,21.73 21,16.55 21,11V5L12,1Z" /></svg>',
sync: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12,18A6,6 0 0,1 6,12C6,11 6.25,10.03 6.7,9.2L5.24,7.74C4.46,8.97 4,10.43 4,12A8,8 0 0,0 12,20V23L16,19L12,15M12,4V1L8,5L12,9V6A6,6 0 0,1 18,12C18,13 17.75,13.97 17.3,14.8L18.76,16.26C19.54,15.03 20,13.57 20,12A8,8 0 0,0 12,4Z" /></svg>',
}
interface Navigation {
name: string
icon?: string
dependsOn?: string
}
const navigationMappings: Record<string, Navigation> = {
// Main menus
"main_library": { name: "Library", icon: iconMappings.bookmarkBoxOutline },
"main_updates": { name: "Updates", icon: iconMappings.alertDecagramOutline },
"main_history": { name: "History", icon: iconMappings.history },
"main_browse": { name: "Browse", icon: iconMappings.compassOutline },
"main_more": { name: "More", icon: iconMappings.dotsHorizontal },
// Browse menu
"sources": { name: "Sources", dependsOn: "main_browse" },
"extensions": { name: "Extensions", dependsOn: "main_browse" },
"migrate": { name: "Migrate", dependsOn: "main_browse" },
// More menu
"downloaded-only": { name: "Downloaded only", icon: iconMappings.cloudOffOutline, dependsOn: "main_more" },
"incognito-mode": { name: "Incognito mode", icon: iconMappings.glasses, dependsOn: "main_more" },
"download-queue": { name: "Download queue", icon: iconMappings.downloadOutline, dependsOn: "main_more" },
"categories": { name: "Categories", icon: iconMappings.labelOutline, dependsOn: "main_more" },
"statistics": { name: "Statistics", icon: iconMappings.queryStats, dependsOn: "main_more" },
"backup-and-restore": { name: "Backup and restore", icon: iconMappings.backupRestore, dependsOn: "main_more" },
"settings": { name: "Settings", icon: iconMappings.cog, dependsOn: "main_more" },
"about": { name: "About", icon: iconMappings.informationOutline, dependsOn: "main_more" },
"help": { name: "Help", icon: iconMappings.helpCircleOutline, dependsOn: "main_more" },
// Settings submenu
"appearance": { name: "Appearance", icon: iconMappings.paletteOutline, dependsOn: "settings" },
"library": { name: "Library", icon: iconMappings.bookmarkBoxOutline, dependsOn: "settings" },
"downloads": { name: "Downloads", icon: iconMappings.downloadOutline, dependsOn: "settings" },
"tracking": { name: "Tracking", icon: iconMappings.sync, dependsOn: "settings" },
"browse": { name: "Browse", icon: iconMappings.compassOutline, dependsOn: "settings" },
"security-and-privacy": { name: "Security and privacy", icon: iconMappings.security, dependsOn: "settings" },
"reader": { name: "Reader", icon: iconMappings.bookOpenOutline, dependsOn: "settings" },
"advanced": { name: "Advanced", icon: iconMappings.codeTags, dependsOn: "settings" },
}
function generateNavigationHtml(navKey: string) {
const navData = navigationMappings[navKey]
if (!navData) {
return "<strong style='color:var(--vp-c-danger-1)'>Unsupported Navigation!</strong>"
}
const { name, icon, dependsOn } = navData
const iconHtml = icon ?? ""
let html = `<span class='shortcode navigation ${navKey}'>${iconHtml}<span class="name">${name}</span></span>`
if (dependsOn) {
html = `${generateNavigationHtml(dependsOn)} -> ${html}`
}
return html
}
const shortcodes = {
nav: {
render({ to }) {
return generateNavigationHtml(to)
},
},
}
export default shortcodes

View File

@ -0,0 +1,72 @@
import type { DefaultTheme } from "vitepress"
import nav from "./navigation/navbar"
import sidebar from "./navigation/sidebar"
const themeConfig: DefaultTheme.Config = {
logo: {
src: "/img/logo-128px.png",
width: 24,
height: 24,
},
nav,
sidebar,
outline: [2, 3],
socialLinks: [
{
icon: "github",
link: "https://github.com/tachiyomiorg/tachiyomi",
ariaLabel: "Project GitHub",
},
{
icon: "discord",
link: "https://discord.gg/tachiyomi",
ariaLabel: "Discord Server",
},
{
icon: "twitter",
link: "https://twitter.com/tachiyomiorg",
ariaLabel: "Twitter Page",
},
{
icon: "facebook",
link: "https://facebook.com/tachiyomiorg",
ariaLabel: "Facebook Page",
},
{
icon: {
svg: '<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0zm5.01 4.744c.688 0 1.25.561 1.25 1.249a1.25 1.25 0 0 1-2.498.056l-2.597-.547-.8 3.747c1.824.07 3.48.632 4.674 1.488.308-.309.73-.491 1.207-.491.968 0 1.754.786 1.754 1.754 0 .716-.435 1.333-1.01 1.614a3.111 3.111 0 0 1 .042.52c0 2.694-3.13 4.87-7.004 4.87-3.874 0-7.004-2.176-7.004-4.87 0-.183.015-.366.043-.534A1.748 1.748 0 0 1 4.028 12c0-.968.786-1.754 1.754-1.754.463 0 .898.196 1.207.49 1.207-.883 2.878-1.43 4.744-1.487l.885-4.182a.342.342 0 0 1 .14-.197.35.35 0 0 1 .238-.042l2.906.617a1.214 1.214 0 0 1 1.108-.701zM9.25 12C8.561 12 8 12.562 8 13.25c0 .687.561 1.248 1.25 1.248.687 0 1.248-.561 1.248-1.249 0-.688-.561-1.249-1.249-1.249zm5.5 0c-.687 0-1.248.561-1.248 1.25 0 .687.561 1.248 1.249 1.248.688 0 1.249-.561 1.249-1.249 0-.687-.562-1.249-1.25-1.249zm-5.466 3.99a.327.327 0 0 0-.231.094.33.33 0 0 0 0 .463c.842.842 2.484.913 2.961.913.477 0 2.105-.056 2.961-.913a.361.361 0 0 0 .029-.463.33.33 0 0 0-.464 0c-.547.533-1.684.73-2.512.73-.828 0-1.979-.196-2.512-.73a.326.326 0 0 0-.232-.095z"/></svg>',
},
link: "https://reddit.com/r/Tachiyomi",
ariaLabel: "Support subreddit",
},
// { icon: "instagram", link: "https://instagram.com/tachiyomiorg", ariaLabel: "Instagram Page" },
],
footer: {
message: "<a href=\"https://www.apache.org/licenses/LICENSE-2.0\" target=\"_blank\">Open-source Apache Licensed</a> <span class=\"divider\">|</span> <a href=\"/privacy/\">Privacy policy</a> <span class=\"divider\">|</span> Powered by <a target=\"_blank\" href=\"https://www.netlify.com/\">Netlify <img src=\"/img/logo-netlify.svg\" alt=\"Netlify Logo\" height=\"12px\" width=\"12px\" style=\"display: inline-block\"></a>",
copyright: `Copyright © 2015 - ${new Date().getFullYear()} Javier Tomás`,
},
editLink: {
pattern: "https://github.com/xhenos/kodo/edit/v3-vitepress/website/src/:path",
text: "Help us improve this page",
},
lastUpdated: {
text: "Last updated",
formatOptions: {
dateStyle: "short",
timeStyle: "short",
},
},
search: {
provider: "local",
},
}
export default themeConfig

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,64 @@
<script setup lang="ts">
import { useData } from "vitepress"
import DefaultTheme from "vitepress/theme"
import { nextTick, provide } from "vue"
const { isDark } = useData()
function shouldEnableTransitions() {
return "startViewTransition" in document
&& window.matchMedia("(prefers-reduced-motion: no-preference)").matches
}
provide("toggle-appearance", async ({ clientX: x, clientY: y }: MouseEvent) => {
if (!shouldEnableTransitions()) {
isDark.value = !isDark.value
return
}
const clipPath = [
`circle(0px at ${x}px ${y}px)`,
`circle(${Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y),
)}px at ${x}px ${y}px)`,
]
// @ts-expect-error Missing types as its experimental
await document.startViewTransition(async () => {
isDark.value = !isDark.value
await nextTick()
}).ready
document.documentElement.animate(
{ clipPath: isDark.value ? clipPath.reverse() : clipPath },
{
duration: 300,
easing: "ease-in",
pseudoElement: `::view-transition-${isDark.value ? "old" : "new"}(root)`,
},
)
})
</script>
<template>
<DefaultTheme.Layout />
</template>
<style lang="stylus">
::view-transition-old(root),
::view-transition-new(root) {
animation: none
mix-blend-mode: normal
}
::view-transition-old(root),
.dark::view-transition-new(root) {
z-index: 1
}
::view-transition-new(root),
.dark::view-transition-old(root) {
z-index: 9999
}
</style>

View File

@ -0,0 +1,116 @@
<script setup lang="ts">
import { computed, toRefs } from "vue"
import MarkdownIt from "markdown-it"
import { type AppRelease, data as release } from "../data/release.data"
import Contributors from "./Contributors.vue"
const props = defineProps<{ type: keyof AppRelease }>()
const { type } = toRefs(props)
const md = new MarkdownIt()
const changelog = computed(() => {
const flavoredString = (release[type.value].body ?? "")
.replace(/(?<=\(|(, ))@(.*?)(?=\)|(, ))/g, "[@$2](https://github.com/$2)")
.replace("https://github.com/tachiyomiorg/tachiyomi/releases", "/changelogs/")
return md.render(flavoredString)
})
</script>
<template>
<div class="changelog">
<header>
<IconNewspaperVariant />
<h2>Changelog</h2>
</header>
<div v-html="changelog" />
<Contributors
:body="release[type].body!"
:author="release[type].author.login"
:tag="release[type].tag_name"
/>
</div>
<div class="fullChangelog">
<p>
View the full release
<a href="https://github.com/tachiyomiorg/tachiyomi/releases/latest" target="_blank" rel="noopener">
here
</a>
</p>
</div>
</template>
<style lang="stylus">
.changelog {
display: block
border: 1px solid var(--vp-c-bg-soft)
border-radius: 12px
background-color: var(--vp-c-bg-soft)
transition: border-color 0.25s, background-color 0.25s
padding: 24px
height: 100%
margin: 1.5em auto 0.5em
header {
display: flex
justify-content: center
align-items: baseline
margin: 0 0 1rem
}
svg {
font-size: 1.2em
margin-right: 0.5rem
vertical-align: middle
}
h2 {
font-size: 1.5rem
margin: 0
padding: 0
color: var(--vp-c-text-1)
border: none
}
div > p {
margin: 0 0 1rem
color: var(--vp-c-text-2)
font-size: 0.9rem
}
table {
border-radius: 8px
border-collapse: collapse
border: 1px solid var(--vp-c-divider)
tr,
th,
td {
border: none
width: 100%
}
tbody tr {
border-top: 1px solid var(--vp-c-divider)
}
tr > td {
&:first-child {
color: var(--vp-c-text-2)
}
&:last-child {
font-family: var(--vp-font-family-mono)
font-size: var(--vp-code-font-size)
}
}
}
}
.fullChangelog {
margin: 0 0 1rem
color: var(--vp-c-text-2)
font-size: 0.9rem
}
</style>

View File

@ -0,0 +1,69 @@
<script setup lang="ts">
import MarkdownIt from "markdown-it"
import { data as changelogs } from "../data/changelogs.data"
import Contributors from "./Contributors.vue"
const md = new MarkdownIt()
function renderMarkdown(string: string | null | undefined) {
const body = string ?? "No changelog provided."
const flavoredString = body
.split(/---\r\n\r\n### Checksums|---\r\n\r\nMD5/)[0]
.replace(/(?<=\(|(, ))@(.*?)(?=\)|(, ))/g, "[@$2](https://github.com/$2)")
.replace(/#(\d+)/g, "[#$1](https://github.com/tachiyomiorg/tachiyomi/issues/$1)")
.replace(/^Check out the .*past release notes.* if you're.*$/m, "")
.replace(/https\:\/\/github.com\/tachiyomiorg\/tachiyomi\/releases\/tag\/(.*?)/g, "#$1")
.trim()
return md.render(flavoredString)
}
const dateFormatter = new Intl.DateTimeFormat("en", {
dateStyle: "medium",
})
</script>
<template>
<div
v-for="(release, index) of changelogs"
:key="release.tag_name"
>
<h2 :id="index === 0 ? 'latest' : release.tag_name">
<a
:href="release.html_url"
target="_blank"
>
{{ release.tag_name.substring(1) }}
</a>
<Badge v-if="index === 0" type="tip" text="Latest" />
<a
class="header-anchor"
:href="index === 0 ? '#latest' : `#${release.tag_name}`"
:aria-label="`Permalink to &quot;${release.tag_name}&quot;`"
/>
</h2>
<time :datetime="release.published_at!">
{{ dateFormatter.format(new Date(release.published_at!)) }}
</time>
<div v-html="renderMarkdown(release.body)" />
<Contributors
:body="release.body!"
:author="release.author.login"
:tag="release.tag_name"
/>
</div>
</template>
<style lang="stylus" scoped>
h2 {
margin-bottom: 0
display: flex
align-items: center
gap: 0.5rem
}
time {
font-size: 0.875rem
color: var(--vp-c-text-2)
}
</style>

View File

@ -0,0 +1,108 @@
<script setup lang="ts">
import { computed, ref, toRefs } from "vue"
const props = defineProps<{ body: string; author: string; tag: string }>()
const { body, author, tag } = toRefs(props)
function isHigherThan(tagName: string, reference: string) {
return reference.localeCompare(tagName, undefined, { numeric: true, sensitivity: "base" }) >= 0
}
const notMentioned = computed(() => {
return isHigherThan("v0.8.5", tag.value) ? ["arkon"] : []
})
const nonExistent = ref<string[]>([])
const contributors = computed(() => {
const list = [...body.value.matchAll(/(?<=\(|(, ))@(.*?)(?=\)|(, ))/g)]
.map((match) => match[2])
const uncredited = author.value.includes("[bot]")
? notMentioned.value
: [author.value, ...notMentioned.value]
return [...new Set([...uncredited, ...list])].filter((user) => !nonExistent.value.includes(user))
})
const listFormatter = new Intl.ListFormat("en", {
style: "long",
type: "conjunction",
})
const contributorsText = computed(() => {
if (contributors.value.length <= 3) {
return listFormatter.format(contributors.value)
}
return listFormatter.format([
...contributors.value.slice(0, 2),
`${contributors.value.length - 2} other contributors`,
])
})
function addToNonExistent(user: string) {
if (!nonExistent.value.includes(user)) {
nonExistent.value.push(user)
}
}
</script>
<template>
<div v-if="contributors.length > 0" class="contributors">
<h3>Contributors</h3>
<ul>
<li
v-for="contributor of contributors"
:key="contributor"
>
<a
:href="`https://github.com/${contributor}`"
target="_blank"
:title="`${contributor} profile on GitHub`"
:aria-label="`${contributor} profile on GitHub`"
>
<img
:src="`https://github.com/${contributor}.png?size=32`"
:alt="`@${contributor} profile picture`"
loading="lazy"
class="avatar"
@error="addToNonExistent(contributor)"
>
</a>
</li>
</ul>
<div class="names">
{{ contributorsText }}
</div>
</div>
</template>
<style lang="stylus" scoped>
.contributors {
ul {
display: flex
align-items: center
flex-wrap: wrap
gap: 0.5rem
list-style-type: none
padding-left: 0
li + li {
margin-top: 0
}
}
.avatar {
width: 32px
height: 32px
border-radius: 50%
box-shadow: var(--vp-shadow-1)
border: 1px solid var(--vp-c-divider)
}
.names {
font-size: 0.875rem
color: var(--vp-c-text-2)
}
}
</style>

View File

@ -0,0 +1,64 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue"
import { useData } from "vitepress"
import type { DefaultTheme } from "vitepress/theme"
import VPNavBarMenuLink from "vitepress/dist/client/theme-default/components/VPNavBarMenuLink.vue"
import VPNavBarMenuGroup from "vitepress/dist/client/theme-default/components/VPNavBarMenuGroup.vue"
import { data as release } from "../data/release.data"
const { theme } = useData<DefaultTheme.Config>()
// Used to avoid hydration issues.
const replace = ref(false)
onMounted(() => {
replace.value = true
})
/**
* Workaround to use the release data directly while the sidebar
* and navbar doesn't support using the VitePress data loading.
*/
const nav = computed(() => {
if (!replace.value) {
return theme.value.nav
}
return theme.value.nav?.map((item) => {
if (!item.text.includes("{app_version}")) {
return item
}
const appVersion = release.stable.tag_name.substring(1)
return {
...item,
text: item.text.replace("{app_version}", appVersion),
} satisfies DefaultTheme.NavItem
})
})
</script>
<template>
<nav v-if="nav" aria-labelledby="main-nav-aria-label" class="VPNavBarMenu">
<span id="main-nav-aria-label" class="visually-hidden">Main navigation</span>
<template v-for="item in nav" :key="item.text">
<VPNavBarMenuLink v-if="'link' in item" :item="item" />
<VPNavBarMenuGroup v-else :item="item" />
</template>
</nav>
</template>
<style lang="stylus" scoped>
.VPNavBarMenu {
display: none
}
@media (min-width 768px) {
.VPNavBarMenu {
display: flex
}
}
</style>

View File

@ -0,0 +1,57 @@
<script setup lang="ts">
import { computed, onMounted, ref } from "vue"
import { type DefaultTheme, useData } from "vitepress"
import VPNavScreenMenuLink from "vitepress/dist/client/theme-default/components/VPNavScreenMenuLink.vue"
import VPNavScreenMenuGroup from "vitepress/dist/client/theme-default/components/VPNavScreenMenuGroup.vue"
import { data as release } from "../data/release.data"
const { theme } = useData<DefaultTheme.Config>()
// Used to avoid hydration issues.
const replace = ref(false)
onMounted(() => {
replace.value = true
})
/**
* Workaround to use the release data directly while the sidebar
* and navbar doesn't support using the VitePress data loading.
*/
const nav = computed(() => {
if (!replace.value) {
return theme.value.nav
}
return theme.value.nav?.map((item) => {
if (!item.text.includes("{app_version}")) {
return item
}
const appVersion = release.stable.tag_name.substring(1)
return {
...item,
text: item.text.replace("{app_version}", appVersion),
} satisfies DefaultTheme.NavItem
})
})
</script>
<template>
<nav v-if="nav" class="VPNavScreenMenu">
<template v-for="item in nav" :key="item.text">
<VPNavScreenMenuLink
v-if="'link' in item"
:item="item"
/>
<VPNavScreenMenuGroup
v-else
:text="item.text || ''"
:items="item.items"
/>
</template>
</nav>
</template>

View File

@ -0,0 +1,74 @@
<script setup lang="ts">
import { inject, onMounted, ref } from "vue"
import { useData } from "vitepress"
import VPIconMoon from "vitepress/dist/client/theme-default/components/icons/VPIconMoon.vue"
import VPIconSun from "vitepress/dist/client/theme-default/components/icons/VPIconSun.vue"
const { isDark } = useData()
const toggleAppearance = inject("toggle-appearance", () => {
isDark.value = !isDark.value
})
const supportsViewTransition = ref(false)
onMounted(() => {
supportsViewTransition.value = "startViewTransition" in document
&& window.matchMedia("(prefers-reduced-motion: no-preference)").matches
})
</script>
<template>
<button
type="button"
role="switch"
title="Toggle dark mode"
class="CustomSwitchAppearance"
:aria-checked="isDark"
:data-view-transition="supportsViewTransition"
@click="toggleAppearance"
>
<ClientOnly>
<Transition name="fade" mode="out-in">
<VPIconSun v-if="!isDark" class="sun" />
<VPIconMoon v-else class="moon" />
</Transition>
</ClientOnly>
</button>
</template>
<style lang="stylus" scoped>
.CustomSwitchAppearance {
display: flex
justify-content: center
align-items: center
width: 36px
height: 36px
color: var(--vp-c-text-2)
transition: color 0.5s
&:hover {
color: var(--vp-c-text-1)
transition: color 0.25s
}
& > :deep(svg) {
width: 20px
height: 20px
fill: currentColor
}
&[data-view-transition="false"] {
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.1s ease
}
.fade-enter-from,
.fade-leave-to {
opacity: 0
}
}
}
</style>

View File

@ -0,0 +1,174 @@
<script setup lang="ts">
/// <reference types="@types/gtag.js" />
import { computed, onMounted, ref } from "vue"
import { data as release } from "../data/release.data"
const downloadInformation = computed(() => ({
preview: {
tagName: release.preview.tag_name ?? "r0000",
asset: (release.preview.assets ?? [])
.find((a) => /^tachiyomi-r\d{4,}.apk/.test(a.name)),
},
stable: {
tagName: release.stable.tag_name ?? "v0.00.0",
asset: (release.stable.assets ?? [])
.find((a) => /^tachiyomi-v\d+\.\d+\.\d+.apk/.test(a.name)),
},
}))
const isAndroid = ref(true)
onMounted(() => {
isAndroid.value = !!navigator.userAgent.match(/android/i)
})
function handleAnalytics(type: "preview" | "stable") {
window.gtag?.("event", "Download", {
event_category: "App",
event_label: type === "stable" ? "Stable" : "Preview",
version: type === "stable"
? release.stable.tag_name
: release.preview.tag_name,
})
}
</script>
<template>
<div>
<div v-if="!isAndroid" class="custom-block danger">
<p class="custom-block-title">
Unsupported operating system
</p>
<p>
<strong>Tachiyomi</strong> is an <strong>Android app</strong> only.
Use an <strong>Android device</strong> to download and install the app.
</p>
</div>
<div v-if="!isAndroid" class="custom-block warning">
<p class="custom-block-title">
Caution
</p>
<p>
Any app for any operating systems other than Android called
<strong>Tachiyomi</strong> is not affiliated with this project.
</p>
<blockquote>
For more information, read the
<a href="/docs/faq/general">General FAQ</a>.
</blockquote>
</div>
<div class="download-buttons">
<a
class="download-button primary"
:download="downloadInformation.stable.asset?.name"
:href="downloadInformation.stable.asset?.browser_download_url"
@click="handleAnalytics('stable')"
>
<IconDownload />
<span class="text">Stable</span>
<span class="version">{{ downloadInformation.stable.tagName }}</span>
</a>
<a
class="download-button secondary"
:download="downloadInformation.preview.asset?.name"
:href="downloadInformation.preview.asset?.browser_download_url"
@click="handleAnalytics('preview')"
>
<IconBugReport />
<span class="text">Preview</span>
<span class="version">{{ downloadInformation.preview.tagName }}</span>
</a>
</div>
<span class="version-disclaimer">
Requires <strong>Android 6.0</strong> or higher.
</span>
</div>
</template>
<style lang="stylus">
.download-buttons {
display: flex
gap: 0.75em
justify-content: center
align-items: center
margin: 0.75em auto
}
.download-button {
display: inline-block
border: 1px solid transparent
text-align: center
font-weight: 600
white-space: nowrap
transition: color 0.25s, border-color 0.25s, background-color 0.25s
cursor: pointer
transition: all 0.3s ease
border-radius: 20px
padding: 0 20px
line-height: 38px
font-size: 14px
&:hover {
text-decoration: none !important
}
&.primary {
border-color: var(--vp-button-brand-border)
color: var(--vp-button-brand-text)
background-color: var(--vp-button-brand-bg)
&:hover {
border-color: var(--vp-button-brand-hover-border)
color: var(--vp-button-brand-hover-text)
background-color: var(--vp-button-brand-hover-bg)
}
&:active {
border-color: var(--vp-button-brand-active-border)
color: var(--vp-button-brand-active-text)
background-color: var(--vp-button-brand-active-bg)
}
}
&.secondary {
border-color: var(--vp-button-alt-border)
color: var(--vp-button-alt-text)
background-color: var(--vp-button-alt-bg)
&:hover {
border-color: var(--vp-button-alt-hover-border)
color: var(--vp-button-alt-hover-text)
background-color: var(--vp-button-alt-hover-bg)
}
&:active {
border-color: var(--vp-button-alt-active-border)
color: var(--vp-button-alt-active-text)
background-color: var(--vp-button-alt-active-bg)
}
}
svg {
display: inline-block
vertical-align: middle
margin-right: 0.5em
font-size: 1.25em
}
.text {
margin-right: 10px
}
.version {
font-size: 0.8em
}
}
.version-disclaimer {
display: block
text-align: center
margin: 0.75em auto
font-size: 0.75rem
}
</style>

View File

@ -0,0 +1,104 @@
<script setup lang="ts">
import { computed, toRefs } from "vue"
import { useMediaQuery } from "@vueuse/core"
import {
ElForm,
ElFormItem,
ElInput,
ElOption,
ElRadio,
ElRadioGroup,
ElSelect,
} from "element-plus"
import { langName, simpleLangName } from "../../../config/scripts/languages"
import type { Extension } from "../../queries/useExtensionsRepositoryQuery"
export type Nsfw = "Show all" | "NSFW" | "SFW"
export type Sort = "Ascending" | "Descending"
const props = defineProps<{
extensions: Extension[][]
search: string
lang: string[]
nsfw: Nsfw
sort: Sort
}>()
defineEmits<{
(e: "update:search", search: string): void
(e: "update:lang", lang: string[]): void
(e: "update:nsfw", nsfw: Nsfw): void
(e: "update:sort", sort: Sort): void
}>()
const { extensions } = toRefs(props)
const isSmallScreen = useMediaQuery("(max-width: 767px)")
const labelPosition = computed(() => isSmallScreen.value ? "top" : "right")
</script>
<template>
<ClientOnly>
<div class="filters-list">
<ElForm label-width="120px" :label-position="labelPosition">
<ElFormItem label="Search:">
<ElInput
:model-value="search"
placeholder="Search extensions by name or ID..."
clearable
@update:model-value="$emit('update:search', $event)"
/>
</ElFormItem>
<ElFormItem label="Languages:">
<ElSelect
:model-value="lang"
placeholder="Show specific languages..."
multiple
clearable
@update:model-value="$emit('update:lang', $event)"
>
<ElOption
v-for="[group] in extensions"
:key="group.lang"
:label="group.lang === 'en' ? simpleLangName(group.lang) : langName(group.lang)"
:value="group.lang"
/>
</ElSelect>
</ElFormItem>
<ElFormItem label="Sort by:">
<ElRadioGroup
:model-value="sort"
@update:model-value="$emit('update:sort', $event)"
>
<ElRadio label="Ascending" />
<ElRadio label="Descending" />
</ElRadioGroup>
</ElFormItem>
<ElFormItem label="Display mode:">
<ElRadioGroup
:model-value="nsfw"
@update:model-value="$emit('update:nsfw', $event)"
>
<ElRadio label="NSFW" />
<ElRadio label="SFW" />
<ElRadio label="Show all" />
</ElRadioGroup>
</ElFormItem>
</ElForm>
</div>
</ClientOnly>
</template>
<style lang="stylus">
.filters-list {
display: flex
flex-direction: column
row-gap: 1rem
}
.el-select {
width: 100%
}
</style>

View File

@ -0,0 +1,51 @@
<script setup lang="ts">
import { computed, toRefs } from "vue"
import { langName, simpleLangName } from "../../../config/scripts/languages"
import type { Extension } from "../../queries/useExtensionsRepositoryQuery"
import ExtensionItem from "./ExtensionItem.vue"
const props = defineProps<{ list: Extension[]; totalCount: number }>()
const { list } = toRefs(props)
const groupName = computed(() => {
const firstItem = list.value[0]
return firstItem.lang === "en"
? simpleLangName(firstItem.lang)
: langName(firstItem.lang)
})
</script>
<template>
<div class="extension-group">
<h2>
<span>{{ groupName }}</span>
<span class="extensions-total">
Total:
<span class="extensions-total-sum">
{{ totalCount }}
</span>
</span>
</h2>
<ExtensionItem
v-for="extension in list"
:id="extension.pkg.replace('eu.kanade.tachiyomi.extension.', '')"
:key="extension.apk"
:item="extension"
/>
</div>
</template>
<style lang="stylus">
.extension-group h2 {
display: flex
align-items: center
justify-content: space-between
.extensions-total-sum {
color: var(--vp-c-brand)
}
}
</style>

View File

@ -0,0 +1,172 @@
<script setup lang="ts">
/// <reference types="@types/gtag.js" />
import { computed, toRefs } from "vue"
import type { Extension } from "../../queries/useExtensionsRepositoryQuery"
const props = defineProps<{ item: Extension }>()
const { item } = toRefs(props)
const pkgId = computed(() => {
return item.value.pkg.replace("eu.kanade.tachiyomi.extension.", "")
})
const pkgName = computed(() => item.value.name.split(": ")[1])
const pkgIsNsfw = computed(() => item.value.nsfw === 1)
const iconUrl = computed(() => {
return `https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/icon/${item.value.pkg}.png`
})
const apkUrl = computed(() => {
return `https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/apk/${item.value.apk}`
})
function handleAnalytics() {
window.gtag?.("event", "Download", {
event_category: "Extension",
event_label: pkgName.value,
version: item.value.version,
})
}
</script>
<template>
<div class="extension">
<a :href="`#${pkgId}`" class="anchor" aria-hidden="true" @click.stop>#</a>
<img class="extension-icon" :src="iconUrl" loading="lazy" width="42" height="42">
<div class="extension-text">
<div class="upper">
{{ pkgName }}
</div>
<div class="lower">
{{ pkgId }}
</div>
</div>
<Badge v-if="pkgIsNsfw" type="danger" :text="item.version" title="This extension contains NSFW entries." />
<Badge v-else type="info" :text="item.version" title="This extension is free from NSFW entries." />
<a
:href="apkUrl"
class="extension-download"
title="Download APK"
:download="item.apk"
@click="handleAnalytics"
>
</a>
</div>
</template>
<style lang="stylus">
.extension {
position: relative
align-items: center
display: flex
width: calc(100% + 1em)
padding: 0.5em
margin: 0.8em -0.5em
border-radius: 8px
gap: 0.675rem
&:hover {
background-color: var(--vp-c-bg-soft)
}
&:target {
background-color: var(--vp-c-brand-soft)
border-radius: 8px
transition: 500ms background-color
}
.anchor {
position: absolute
left: 0
margin-left: -1em
font-size: 1.4em
opacity: 0
}
&:hover .anchor {
opacity: 1
}
.extension-icon {
flex-shrink: 0
margin-left: -4px
}
.extension-text {
flex-grow: 1
min-width: 0
.upper {
font-weight: 600
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
.badge {
font-weight: 400
}
}
.lower {
margin-top: 0.25rem
color: #6c757d
font-family: var(--vp-font-family-mono)
font-size: 0.9rem
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
line-height: 16px
}
}
.extension-download {
padding: 0.4em
font-weight: 700
font-size: 1.4em
border-radius: 4px
flex-shrink: 0
margin-right: -0.4em
.material-icons {
color: white
max-width: 18px
}
&:hover {
.VPBadge {
background-color: var(--vp-c-brand-darker)
text-decoration: none
}
}
}
@media (max-width 767px) {
padding: 0.4em 0
}
}
@media (max-width 767px) {
.anchor {
display: none
}
.extension {
border: 1px solid var(--vp-c-divider)
border-radius: 8px
padding: 0.5em
margin: 0.8em 0
width: 100%
.extension-icon {
margin-left: 0
}
.extension-download {
margin-right: 0
}
}
}
</style>

View File

@ -0,0 +1,36 @@
<script setup lang="ts">
import { computed, toRefs } from "vue"
import type { Extension } from "../../queries/useExtensionsRepositoryQuery"
import ExtensionGroup from "./ExtensionGroup.vue"
const props = defineProps<{ extensions: Extension[][] }>()
const { extensions } = toRefs(props)
const totalCount = computed(() => {
return extensions.value.reduce((sum, item) => sum + item.length, 0)
})
</script>
<template>
<div class="extension-list">
<ExtensionGroup
v-for="group in extensions"
:key="group[0].lang"
:list="group"
:class="group[0].lang"
:total-count="totalCount"
/>
</div>
</template>
<style lang="stylus">
.extension-list {
> div {
&:not(:first-of-type) {
.extensions-total {
display: none
}
}
}
}
</style>

View File

@ -0,0 +1,125 @@
<script setup lang="ts">
import groupBy from "lodash.groupby"
import { ElLoading } from "element-plus"
import { computed, nextTick, onMounted, reactive, ref, watch } from "vue"
import { simpleLangName } from "../../../config/scripts/languages"
import useExtensionsRepositoryQuery from "../../queries/useExtensionsRepositoryQuery"
import type { Extension } from "../../queries/useExtensionsRepositoryQuery"
import ExtensionFilters from "./ExtensionFilters.vue"
import ExtensionList from "./ExtensionList.vue"
import type { Nsfw, Sort } from "./ExtensionFilters.vue"
const { data: extensions, isLoading } = useExtensionsRepositoryQuery({
select: (response) => {
const values: Extension[][] = Object.values(groupBy(response, "lang"))
values.sort(languageComparator)
return values
},
})
const filters = reactive({
search: "",
lang: [] as string[],
nsfw: "Show all" as Nsfw,
sort: "Ascending" as Sort,
})
function languageComparator(a: Extension[], b: Extension[]) {
const langA = simpleLangName(a[0].lang)
const langB = simpleLangName(b[0].lang)
if (langA === "All" && langB === "English") {
return -1
}
if (langA === "English" && langB === "All") {
return 1
}
if (langA === "English") {
return -1
}
if (langB === "English") {
return 1
}
if (langA < langB) {
return -1
}
if (langA > langB) {
return 1
}
return 0
}
const filteredExtensions = computed(() => {
const filtered: Extension[][] = []
for (const group of (extensions.value ?? [])) {
let filteredGroup = filters.lang.length
? (filters.lang.includes(group[0].lang) ? group : [])
: group
if (filters.search) {
filteredGroup = filteredGroup.filter(
(ext) =>
ext.name.toLowerCase().includes(filters.search.toLowerCase())
|| ext.sources.some((source) => source.id.includes(filters.search)),
)
}
filteredGroup = filteredGroup.filter((ext) =>
filters.nsfw === "Show all" ? true : ext.nsfw === (filters.nsfw === "NSFW" ? 1 : 0),
)
if (filters.sort && filters.sort === "Descending") {
filteredGroup = filteredGroup.reverse()
}
if (filteredGroup.length) {
filtered.push(filteredGroup)
}
}
return filtered
})
const loadingInstance = ref<ReturnType<typeof ElLoading["service"]>>()
onMounted(() => {
loadingInstance.value = ElLoading.service({
target: ".extensions",
fullscreen: false,
background: "transparent",
})
})
watch(extensions, async () => {
if (window.location.hash) {
await nextTick()
document.getElementById(window.location.hash.substring(1))
?.scrollIntoView({ behavior: "smooth" })
}
})
watch([isLoading, loadingInstance], async ([newIsLoading]) => {
if (!newIsLoading) {
loadingInstance.value?.close()
}
})
</script>
<template>
<ExtensionFilters
v-model:search="filters.search"
v-model:lang="filters.lang"
v-model:nsfw="filters.nsfw"
v-model:sort="filters.sort"
:extensions="extensions ?? []"
/>
<div class="extensions">
<ExtensionList v-if="!isLoading" :extensions="filteredExtensions" />
</div>
</template>
<style lang="stylus" scoped>
.extensions {
min-height: 200px
}
</style>

View File

@ -0,0 +1,152 @@
<script setup lang="ts">
import { IconChevronRight } from "@iconify-prerendered/vue-mdi"
import { data as newsList } from "../data/news.data"
const dateFormatter = new Intl.DateTimeFormat("en", {
dateStyle: "medium",
timeZone: "UTC",
})
</script>
<template>
<article
v-for="news of newsList"
:key="news.url"
class="news"
>
<div>
<h3>
<a :href="news.url">
<span class="hover" />
<span class="title">{{ news.title }}</span>
</a>
<div class="background" />
</h3>
<time :datetime="news.date">
{{ dateFormatter.format(new Date(news.date)) }}
</time>
</div>
<p>{{ news.description }}</p>
<div class="readPrompt" aria-hidden="true">
<span>Read article</span>
<IconChevronRight />
</div>
</article>
</template>
<style lang="stylus" scoped>
.news {
display: flex
flex-direction: column
gap: 0.5rem
position: relative
&:first-of-type {
margin-top: 3rem
}
& + .news {
margin-top: 3rem
}
time {
font-size: 0.875rem
line-height: 1.25rem
color: var(--vp-c-text-2)
z-index: 10
}
h3,
p {
margin: 0
}
h3 {
position: unset
font-size: 1.125rem
line-height: 1.75rem
letter-spacing: -0.025em
}
h3 a {
font-weight: 600
color: var(--vp-c-text-1)
&:hover {
color: var(--vp-c-text-1)
text-decoration: none
}
&:focus {
outline: none
}
}
.title,
.readPrompt,
p,
time {
position: relative
}
.title {
z-index: 10
}
.readPrompt {
font-weight: 500
font-size: 0.875rem
line-height: 1.25rem
color: var(--vp-c-brand-1)
display: flex
align-items: center
margin-top: 0.25rem
svg {
margin-bottom: -2px
margin-left: 0.25rem
width: 1.25rem
height: 1.25rem
}
}
.hover,
.background {
position: absolute
z-index: 20
bottom: -1rem
top: -1rem
left: -1rem
right: -1rem
border-radius: 12px
}
.background {
background-color: var(--vp-c-bg-soft)
transform: scale(0.95)
opacity: 0
z-index: 0
transition: opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1), transform 0.15s cubic-bezier(0.4, 0, 0.2, 1)
}
&:hover .background,
&:focus-within .background {
opacity: 1
transform: scale(1)
}
h3: a
:focus-visible + .background {
outline: 2px solid var(--vp-c-brand-2)
}
:focus-visible + .background {
outline: 2px solid var(--vp-c-brand-2)
}
:focus-visible + .background {
outline: 2px solid var(--vp-c-brand-2)
}
}
</style>

View File

@ -0,0 +1,32 @@
<script setup lang="ts">
defineProps<{ title: string; description?: string; dir?: string }>()
</script>
<template>
<div
tw="w-full h-full bg-white flex flex-col"
style="background:linear-gradient(0deg, rgba(255, 255, 255, 0.92), rgba(231,230,255,0.86)), url(https://tachiyomi.org/img/open-graph-background.png)"
>
<div tw="p-10 w-full min-h-0 grow flex flex-col items-center justify-between">
<div tw="w-full flex justify-between items-center text-4xl font-medium">
<div tw="flex items-center">
<svg width="64" height="64" viewBox="0 0 288 288" preserveAspectRatio="xMidYMid meet" fill="#8995FF"><path d="M141.2 36.7l-18.3.3.8 9.8c.4 5.3.8 11.9.8 14.7v5h-41-41l.3 17.9.3 17.9 6.7-.7c15.2-1.4 101.5-1.8 146.2-.7l48 1.6c1.3.4 1.5-2 1.5-17.8V66.5l-38.3.4c-21 .2-39.8 0-41.8-.3l-3.5-.7.3-15c.3-12.8.1-14.9-1.2-14.7-.8.1-9.7.4-19.8.5zm-56.7 76.9c-12.8 5-16 6.7-15.8 8.1.1 1 2.2 7.3 4.7 14 5.8 15.3 12.4 38.4 16.6 57.8l3.5 15.3c.2.2 8.3-2.5 18.2-6l17.9-6.3-1.2-6c-3.5-16.8-24.8-83.6-26.7-83.4-.7.1-8.4 3-17.2 6.5zm93.6 10.1c-10.4 41.7-22.9 83.2-27.1 90.1l-1.9 3.1-17.3.3c-20 .3-91.3-.9-94.3-1.6-1.9-.5-2 0-2 18v18.6l5-.6c2.8-.3 51.8-.9 109-1.2l104-.7v-17.6-17.6l-8.5.7c-4.7.3-19.5.7-32.9.7-19.1.1-24.2-.2-23.8-1.2.3-.6 2.2-5 4.2-9.7 6.5-15.2 29.6-86.3 28.4-87.4-.4-.4-32.6-9.4-36.5-10.3-2-.4-2.4.7-6.3 16.4z" /></svg>
<div tw="text-slate-900 ml-2 mt-1 font-semibold">
Tachiyomi
</div>
</div>
<div v-if="dir" tw="flex items-center text-slate-600">
<div v-if="dir" tw="text-4xl font-semibold mr-2" v-html="dir" />
<svg v-if="dir === 'FAQ'" width="48" height="48" viewBox="0 -960 960 960" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M120-55.5q-15.5 0-26.5-11T82.5-93q0-15.5 11-26.5t26.5-11h720q15.5 0 26.5 11t11 26.5q0 15.5-11 26.5t-26.5 11H120ZM207-170q-15.5 0-26.5-11t-11-26.5v-193q-32.5-54-50.75-114.25T100.5-639.5q0-59 15.25-117.5t36.75-116q7.5-20 24.5-32.25t38-12.25q26.5 0 44.75 18.5T276-854.5l-5.5 92q-3 48.5 11 93t41.75 78Q351-558 391-537.75t89 20.25q59.5 0 116.5 11.5T697-471.5q43.5 23 68.25 58.5T790-326v118.5q0 15.5-11 26.5t-26.5 11h-355v-32q0-35 24-60.25T480-287.5h120q15.5 0 26.5-11t11-26.5q0-15.5-11-26.5t-26.5-11H480q-66 0-111.75 47T322.5-202v32H207Zm273-392.5q-65.5 0-111.5-46t-46-111.5q0-65.5 46-111.5t111.5-46q65.5 0 111.5 46t46 111.5q0 65.5-46 111.5t-111.5 46Z" /></svg>
<svg v-else-if="dir === 'Guide'" height="48" width="48" viewBox="0 -960 960 960" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M831-327.5V-558L516-386.5q-17 9-36 9t-36-9l-329.5-180q-10-5.5-14.5-14.25t-4.5-18.75q0-10 4.5-18.75t14.5-14.25l330-179.5q8.5-5 17.25-7t18.25-2q9.5 0 18.25 2t17.25 7l371 202q9 5 14.25 13.75T906-577v249.5q0 15.5-11 26.5t-26.5 11q-15.5 0-26.5-11t-11-26.5Zm-387 173-197-107q-18.5-10-28.75-27.75T208-328v-149.5L444-349q17 9 36 9t36-9l236-128.5V-328q0 21-10.25 38.75T713-261.5l-197 107q-8.5 5-17.5 7t-18.5 2q-9.5 0-18.5-2t-17.5-7Z" /></svg>
<svg v-else-if="dir === 'News'" height="48" width="48" viewBox="0 -960 960 960" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M157.5-122.5q-31 0-53-22t-22-53v-614q0-6.5 5.5-9t10.5 2.5l35.5 35.5q5.5 5.5 13 5.25t13-5.75l40-40q5.5-5.5 13-5.75t13 5.25l41 41q5.5 5.5 13 5.5t13-5.5l41-41q5.5-5.5 13-5.25t13 5.75l40 40q5.5 5.5 13 5.75t13-5.25l41-41q5.5-5.5 13-5.5t13 5.5l41 41q5.5 5.5 13 5.25t13-5.75l40-40q5.5-5.5 13-5.75t13 5.25l41 41q5.5 5.5 13 5.5t13-5.5l41-41q5.5-5.5 13-5.25t13 5.75l40 40q5.5 5.5 13 5.75t13-5.25l35.5-35.5q5-5 10.5-2.5t5.5 9v614q0 31-22 53t-53 22h-645Zm0-75h285v-245h-285v245Zm360 0h285v-85h-285v85Zm0-160h285v-85h-285v85Zm-360-160h645v-125h-645v125Z" /></svg>
</div>
</div>
<div tw="w-full pr-56 flex flex-col items-start justify-end">
<div tw="text-6xl font-bold text-slate-900" v-html="title" />
<div v-if="description" tw="mt-2 text-4xl text-slate-600" v-html="description" />
</div>
</div>
<div tw="shrink-0 h-2 w-full flex" style="background-color: #8995ff;" />
</div>
</template>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import { computed, onMounted, ref, toRefs } from "vue"
import moment from "moment"
import { type AppRelease, data as release } from "../data/release.data"
const props = defineProps<{ type: keyof AppRelease }>()
const { type } = toRefs(props)
const momentInfo = computed(() => ({
relative: moment(release[type.value].published_at).fromNow(),
exact: moment(release[type.value].published_at).format("dddd, MMMM Do YYYY [at] HH:mm"),
iso: release[type.value].published_at ?? undefined,
}))
// Mimic the <ClientOnly /> behavior to show custom text while rendering.
const show = ref(false)
onMounted(() => {
show.value = true
})
</script>
<template>
<time v-if="show" :datetime="momentInfo.iso" :title="momentInfo.exact">
{{ momentInfo.relative }}
</time>
<time v-else :datetime="momentInfo.iso">
{{ momentInfo.exact }}
</time>
</template>

View File

@ -0,0 +1,27 @@
<script setup lang="ts">
import { IconRssBox } from "@iconify-prerendered/vue-mdi"
</script>
<template>
<a href="/feed.rss" class="rss" title="RSS feed for the news archive">
<IconRssBox />
<span>RSS feed</span>
</a>
</template>
<style lang="stylus" scoped>
.rss {
& > * {
vertical-align: middle
position: relative
bottom: 1px
}
svg {
width: 1em
height: 1em
display: inline-block
margin-right: 4px
}
}
</style>

View File

@ -0,0 +1,22 @@
import { defineLoader } from "vitepress"
import { Octokit } from "@octokit/rest"
import type { GetResponseDataTypeFromEndpointMethod } from "@octokit/types"
const octokit = new Octokit()
type GitHubReleaseList = GetResponseDataTypeFromEndpointMethod<typeof octokit.repos.listReleases>
declare const data: GitHubReleaseList
export { data }
export default defineLoader({
async load(): Promise<GitHubReleaseList> {
const releases = await octokit.paginate(octokit.repos.listReleases, {
owner: "tachiyomiorg",
repo: "tachiyomi",
per_page: 100,
})
return releases
},
})

View File

@ -0,0 +1,29 @@
import { createContentLoader } from "vitepress"
export interface News {
title: string
description: string
date: string
url: string
}
declare const data: News[]
export { data }
export default createContentLoader("news/*.md", {
excerpt: true,
transform(articles) {
return articles
.filter(({ url }) => url !== "/news/")
.map(
({ frontmatter, url }) =>
<News>{
title: frontmatter.title,
description: frontmatter.description,
date: frontmatter.date,
url,
},
)
.sort((a, b) => b.date.localeCompare(a.date))
},
})

View File

@ -0,0 +1,31 @@
import { defineLoader } from "vitepress"
import { Octokit } from "@octokit/rest"
import type { GetResponseDataTypeFromEndpointMethod } from "@octokit/types"
const octokit = new Octokit()
type GitHubRelease = GetResponseDataTypeFromEndpointMethod<typeof octokit.repos.getLatestRelease>
export interface AppRelease {
stable: GitHubRelease
preview: GitHubRelease
}
declare const data: AppRelease
export { data }
export default defineLoader({
async load(): Promise<AppRelease> {
const { data: stable } = await octokit.repos.getLatestRelease({
owner: "tachiyomiorg",
repo: "tachiyomi",
})
const { data: preview } = await octokit.repos.getLatestRelease({
owner: "tachiyomiorg",
repo: "tachiyomi-preview",
})
return { stable, preview }
},
})

View File

@ -0,0 +1,31 @@
// https://vitepress.dev/guide/custom-theme
import DefaultTheme from "vitepress/theme"
// Import Stylus files
import "./styles/base.styl"
// Import Global plugins
import "element-plus/theme-chalk/dark/css-vars.css"
import { VueQueryPlugin } from "@tanstack/vue-query"
import { enhanceAppWithTabs } from "vitepress-plugin-tabs/client"
// Import icon components
import { IconBugReport, IconDownload, IconNewspaperVariant } from "@iconify-prerendered/vue-mdi"
import analytics from "./plugin/analytics"
import Layout from "./Layout.vue"
export default {
extends: DefaultTheme,
enhanceApp({ app }) {
app.use(VueQueryPlugin)
enhanceAppWithTabs(app)
app.component("IconDownload", IconDownload)
app.component("IconNewspaperVariant", IconNewspaperVariant)
app.component("IconBugReport", IconBugReport)
analytics({ id: "G-2CBXXM1Y86" })
},
Layout,
}

View File

@ -0,0 +1,37 @@
// Code based on vitepress-plugin-google-analytics.
// Customized as the plugin did not consider the script loading time.
// https://github.com/ZhongxuYang/vitepress-plugin-google-analytics
function mountGoogleAnalytics(id: string) {
if (("dataLayer" in window && window.gtag) || window.location.hostname === "localhost") {
return
}
const analyticsScript = document.createElement("script")
analyticsScript.addEventListener("load", () => {
// @ts-expect-error Missing types
window.dataLayer = window.dataLayer || []
function gtag(..._args: any[]) {
// @ts-expect-error Missing types
// eslint-disable-next-line prefer-rest-params
window.dataLayer.push(arguments)
}
gtag("js", new Date())
gtag("config", id)
window.gtag = gtag
})
analyticsScript.src = `https://www.googletagmanager.com/gtag/js?id=${id}`
document.body.appendChild(analyticsScript)
}
export default function ({ id }: { id: string }) {
// eslint-disable-next-line n/prefer-global/process
if (process.env.NODE_ENV === "production" && id && typeof window !== "undefined") {
mountGoogleAnalytics(id)
}
}

View File

@ -0,0 +1,45 @@
import type { UseQueryOptions } from "@tanstack/vue-query"
import { useQuery } from "@tanstack/vue-query"
import axios from "axios"
import { GITHUB_EXTENSION_JSON } from "../../config/constants"
export type ReleaseType = "stable" | "preview"
export interface Extension {
name: string
pkg: string
apk: string
lang: string
code: number
version: string
nsfw: number
hasReadme: number
hasChangelog: number
sources: Source[]
}
export interface Source {
name: string
lang: string
id: string
baseUrl: string
versionId: number
hasCloudflare: string
}
type UseExtensionsRepositoryQueryOptions<S = Extension[]> =
UseQueryOptions<Extension[], Error, S>
export default function useExtensionsRepositoryQuery<S = Extension[]>(options: UseExtensionsRepositoryQueryOptions<S> = {}) {
return useQuery<Extension[], Error, S>({
queryKey: ["extensions"],
queryFn: async () => {
const { data } = await axios.get<Extension[]>(GITHUB_EXTENSION_JSON)
return data
},
initialData: () => [],
refetchOnWindowFocus: false,
...options,
})
}

View File

@ -0,0 +1,278 @@
/**
* Customize default theme styling by overriding CSS variables:
* https://github.com/vuejs/vitepress/blob/main/src/client/theme-default/styles/vars.css
*/
/**
* Colors
* -------------------------------------------------------------------------- */
// Assign theme color
$themeColor = #818cf8
:root {
--vp-c-brand: $themeColor
--vp-c-brand-light: tint($themeColor, 20%)
--vp-c-brand-lighter: tint($themeColor, 40%)
--vp-c-brand-lightest: tint($themeColor, 60%)
--vp-c-brand-dark: shade($themeColor, 25%)
--vp-c-brand-darker: shade($themeColor, 50%)
--vp-c-brand-darkest: shade($themeColor, 75%)
--vp-c-brand-dimm: alpha($themeColor, 0.08)
}
/**
* Dark/Light Theme Overrides
* -------------------------------------------------------------------------- */
html:not(.dark) {
img[data-mode="darkmode-only"] {
display: none !important
& + figcaption {
display: none !important
}
}
}
.dark {
img[data-mode="lightmode-only"] {
display: none !important
& + figcaption {
display: none !important
}
}
}
/**
* Component: Button
* -------------------------------------------------------------------------- */
:root {
--vp-button-brand-bg: var(--vp-c-brand-darker)
}
/**
* Component: Home
* -------------------------------------------------------------------------- */
:root {
--vp-home-hero-name-color: transparent
--vp-home-hero-name-background: -webkit-linear-gradient(120deg, var(--vp-c-brand) 30%, var(--vp-c-brand-dark))
--vp-home-hero-image-background-image: linear-gradient(-45deg, var(--vp-c-brand-light) 50%, var(--vp-c-brand-lighter) 50%)
--vp-home-hero-image-filter: blur(40px)
}
.dark {
--vp-home-hero-image-background-image: linear-gradient(-45deg, var(--vp-c-brand-darker) 25%, var(--vp-c-brand-darkest) 25%)
}
@media (min-width 640px) {
:root {
--vp-home-hero-image-filter: blur(56px)
}
}
@media (min-width 960px) {
:root {
--vp-home-hero-image-filter: blur(72px)
}
}
.VPFooter {
.divider {
color: var(--vp-c-divider)
}
}
/**
* Component: Custom Block
* -------------------------------------------------------------------------- */
:root {
--vp-custom-block-tip-border: var(--vp-c-brand)
--vp-custom-block-tip-text: var(--vp-c-brand-darker)
--vp-custom-block-tip-bg: var(--vp-c-brand-dimm)
}
.dark {
--vp-custom-block-tip-border: var(--vp-c-brand)
--vp-custom-block-tip-text: var(--vp-c-brand-lightest)
--vp-custom-block-tip-bg: var(--vp-c-brand-dimm)
--vp-custom-block-info-bg: #212127
}
.plugin-tabs {
&--content {
padding: 2rem !important
}
}
/**
* Component: Algolia
* -------------------------------------------------------------------------- */
.DocSearch {
--docsearch-primary-color: var(--vp-c-brand) !important
}
.VPImage.image-src {
border-radius: 8px
}
/**
* Component: LocalSearch
*/
.VPLocalSearchBox {
--vp-local-search-highlight-bg: var(--vp-c-brand-soft)
--vp-local-search-highlight-text: var(--vp-c-brand-dark)
}
.dark .VPLocalSearchBox {
--vp-local-search-highlight-text: var(--vp-c-brand-lightest)
}
/**
* Component: Image Figure
* -------------------------------------------------------------------------- */
// Shitty method to give elevation to image - TO BE REPLACED
main figure {
margin: 2rem 0
transition: transform var(--vp-tt)
img {
margin: 0 auto
border-radius: 12px
box-shadow: 2px 2px 8px 4px var(--vp-c-bg-alt)
}
figcaption {
text-align: center
margin-top: 1.25rem
font-size: 0.875rem
color: var(--vp-c-text-2)
}
& > a .external-link-icon {
display: none
}
}
main :where(h1, h2, h3, h4, h5, h6) + figure {
margin-top: 1.5rem
}
.custom-block {
figure figcaption {
color: inherit
}
&.tip figure img {
--vp-c-bg-alt: var(--vp-custom-block-tip-bg)
}
}
/**
* Component: Links
* -------------------------------------------------------------------------- */
@media print {
figure:has(img)>a[href^="http://"]:after,
figure:has(img)>a[href^="https://"]:after {
content: ""
}
}
.vp-doc a {
text-decoration: none
&:hover {
text-decoration: underline
text-underline-offset: 2px
}
}
/**
* Component: Shortcodes
* -------------------------------------------------------------------------- */
.navigation {
color: var(--vp-c-green-2)
font-weight: 600
&:hover {
color: var(--vp-c-green-1)
cursor: default
}
svg,
span.name {
vertical-align: middle // Align both SVG and <span> vertically
position: relative
bottom: 1px
}
svg {
fill: currentColor
height: 1em
width: 1em
display: inline-block
margin-right: 4px
}
}
/**
* Component: Element Plus
*/
body {
--el-color-primary: var(--vp-c-brand-1)
}
/**
* Component: Appearance Switch
*/
.menu + .appearance::before {
margin-right: 8px !important
}
.appearance + .social-links::before {
margin-left: 8px !important
}
.VPMenu .CustomSwitchAppearance {
margin-right: -8px
}
/**
* Component: Forks page
*/
.forks .VPFeatures {
padding: 2rem 0 !important
.VPLink:hover {
text-decoration: none
}
.title {
line-height: 24px
font-size: 16px
font-weight: 600
margin: 0
border-top: none
padding-top: 0
letter-spacing: 0
color: var(--vp-c-text-1)
}
}
@media (min-width 640px) {
.forks .item.grid-4 {
width: 50% !important
}
}

View File

@ -0,0 +1,9 @@
.extension-list {
> div {
&:not(:first-of-type) {
.extensions-total {
display: none
}
}
}
}

View File

@ -0,0 +1,60 @@
// Assign theme color
$themeColor = #ff6884
.page-neko {
.VPHero {
h1 {
.clip {
--vp-home-hero-name-background: -webkit-linear-gradient(120deg, var(--vp-c-brand) 30%, var(--vp-c-brand-dark))
}
}
}
.VPButton {
&.brand {
border-color: var(--vp-button-brand-border)
color: var(--vp-button-brand-text)
background-color: var(--vp-button-brand-bg)
}
}
.image-bg {
display: none
}
::selection {
background: alpha($themeColor, 0.2)
}
}
/**
* Colors
* -------------------------------------------------------------------------- */
:root .page-neko {
--vp-c-brand: $themeColor
--vp-c-brand-light: tint($themeColor, 20%)
--vp-c-brand-lighter: tint($themeColor, 40%)
--vp-c-brand-lightest: tint($themeColor, 60%)
--vp-c-brand-dark: shade($themeColor, 25%)
--vp-c-brand-darker: shade($themeColor, 50%)
--vp-c-brand-darkest: shade($themeColor, 75%)
--vp-c-brand-dimm: alpha($themeColor, 0.08)
}
/**
* Component: Button
* -------------------------------------------------------------------------- */
:root .page-neko {
--vp-button-brand-border: var(--vp-c-brand-light)
--vp-button-brand-text: var(--vp-c-black)
--vp-button-brand-bg: var(--vp-c-brand)
--vp-button-brand-hover-border: var(--vp-c-brand-light)
--vp-button-brand-hover-text: var(--vp-c-black)
--vp-button-brand-hover-bg: var(--vp-c-brand-light)
--vp-button-brand-active-border: var(--vp-c-brand-light)
--vp-button-brand-active-text: var(--vp-c-black)
--vp-button-brand-active-bg: var(--vp-button-brand-bg)
}

View File

@ -0,0 +1,203 @@
// Assign theme color
$themeColor = #ffcc4d
.page-tachiyomi-az {
.VPHero {
h1 {
.clip {
--vp-home-hero-name-background: -webkit-linear-gradient(120deg, var(--vp-c-brand) 30%, var(--vp-c-brand-dark))
}
}
}
.VPButton {
&.brand {
border-color: var(--vp-button-brand-border)
color: var(--vp-button-brand-text)
background-color: var(--vp-button-brand-bg)
}
}
.image-bg {
display: none
}
::selection {
background: alpha($themeColor, 0.2)
}
}
/**
* Colors
* -------------------------------------------------------------------------- */
:root .page-tachiyomi-az {
--vp-c-brand: $themeColor
--vp-c-brand-light: tint($themeColor, 20%)
--vp-c-brand-lighter: tint($themeColor, 40%)
--vp-c-brand-lightest: tint($themeColor, 60%)
--vp-c-brand-dark: shade($themeColor, 25%)
--vp-c-brand-darker: shade($themeColor, 50%)
--vp-c-brand-darkest: shade($themeColor, 75%)
--vp-c-brand-dimm: alpha($themeColor, 0.08)
}
/**
* Component: Button
* -------------------------------------------------------------------------- */
:root .page-tachiyomi-az {
--vp-button-brand-border: var(--vp-c-brand-light)
--vp-button-brand-text: var(--vp-c-black)
--vp-button-brand-bg: var(--vp-c-brand)
--vp-button-brand-hover-border: var(--vp-c-brand-light)
--vp-button-brand-hover-text: var(--vp-c-black)
--vp-button-brand-hover-bg: var(--vp-c-brand-light)
--vp-button-brand-active-border: var(--vp-c-brand-light)
--vp-button-brand-active-text: var(--vp-c-black)
--vp-button-brand-active-bg: var(--vp-button-brand-bg)
}
/**
* Component: Dumb
* -------------------------------------------------------------------------- */
@font-face {
font-family: 'Comic Sans MS'
src: url('//db.onlinewebfonts.com/t/7cc6719bd5f0310be3150ba33418e72e.eot')
src: url('//db.onlinewebfonts.com/t/7cc6719bd5f0310be3150ba33418e72e.eot?#iefix') format('embedded-opentype'), url('//db.onlinewebfonts.com/t/7cc6719bd5f0310be3150ba33418e72e.woff2') format('woff2'), url('//db.onlinewebfonts.com/t/7cc6719bd5f0310be3150ba33418e72e.woff') format('woff'), url('//db.onlinewebfonts.com/t/7cc6719bd5f0310be3150ba33418e72e.ttf') format('truetype'), url('//db.onlinewebfonts.com/t/7cc6719bd5f0310be3150ba33418e72e.svg#Comic Sans MS') format('svg')
}
.azContainer {
width: 100%
overflow: hidden
.azMarquee {
display: inline-block
overflow: hidden
white-space: nowrap
animation: marquee 10s linear infinite
padding-left: 100%
.azWiggleText {
padding: 2em
width: fit-content
animation: wiggleAnimation 1s ease-out infinite
&:hover {
animation: barrelRollAnimation 0.6s
}
.azText {
font-family: 'Comic Sans MS', 'Comic Sans', cursive
font-size: 2em
display: inline-block
-webkit-text-stroke: 1px black
text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000
animation: rainbowTextColorAnimation 0.5s linear infinite, scaleXTextAnimation 2s infinite
}
}
}
}
@keyframes marquee {
0% {
transform: translate(0, 0)
}
100% {
transform: translate(-100%, 0)
}
}
@keyframes barrelRollAnimation {
from {
transform: rotate(0)
}
to {
transform: rotate(360deg)
}
}
@keyframes wiggleAnimation {
0% {
transform: rotate(0)
}
25% {
transform: rotate(-15deg)
}
50% {
transform: rotate(0)
}
75% {
transform: rotate(15deg)
}
100% {
transform: rotate(0)
}
}
@keyframes scaleXTextAnimation {
0% {
transform: scaleX(0.5) scaleY(0.5)
}
50% {
transform: scaleX(1) scaleY(1)
}
100% {
transform: scaleX(0.5) scaleY(0.5)
}
}
@keyframes rainbowTextColorAnimation {
0% {
color: hsl(0, 100%, 50%)
}
10% {
color: hsl(36, 100%, 50%)
}
20% {
color: hsl(72, 100%, 50%)
}
30% {
color: hsl(108, 100%, 50%)
}
40% {
color: hsl(144, 100%, 50%)
}
50% {
color: hsl(180, 100%, 50%)
}
60% {
color: hsl(216, 100%, 50%)
}
70% {
color: hsl(252, 100%, 50%)
}
80% {
color: hsl(288, 100%, 50%)
}
90% {
color: hsl(324, 100%, 50%)
}
100% {
color: hsl(360, 100%, 50%)
}
}

View File

@ -0,0 +1,60 @@
// Assign theme color
$themeColor = #0952af
.page-tachiyomi-j2k {
.VPHero {
h1 {
.clip {
--vp-home-hero-name-background: -webkit-linear-gradient(120deg, var(--vp-c-brand) 30%, var(--vp-c-brand-dark))
}
}
}
.VPButton {
&.brand {
border-color: var(--vp-button-brand-border)
color: var(--vp-button-brand-text)
background-color: var(--vp-button-brand-bg)
}
}
.image-bg {
display: none
}
::selection {
background: alpha($themeColor, 0.2)
}
}
/**
* Colors
* -------------------------------------------------------------------------- */
:root .page-tachiyomi-j2k {
--vp-c-brand: $themeColor
--vp-c-brand-light: tint($themeColor, 20%)
--vp-c-brand-lighter: tint($themeColor, 40%)
--vp-c-brand-lightest: tint($themeColor, 60%)
--vp-c-brand-dark: shade($themeColor, 25%)
--vp-c-brand-darker: shade($themeColor, 50%)
--vp-c-brand-darkest: shade($themeColor, 75%)
--vp-c-brand-dimm: alpha($themeColor, 0.08)
}
/**
* Component: Button
* -------------------------------------------------------------------------- */
:root .page-tachiyomi-j2k {
--vp-button-brand-border: var(--vp-c-brand-light)
--vp-button-brand-text: var(--vp-c-white)
--vp-button-brand-bg: var(--vp-c-brand)
--vp-button-brand-hover-border: var(--vp-c-brand-light)
--vp-button-brand-hover-text: var(--vp-c-white)
--vp-button-brand-hover-bg: var(--vp-c-brand-light)
--vp-button-brand-active-border: var(--vp-c-brand-light)
--vp-button-brand-active-text: var(--vp-c-white)
--vp-button-brand-active-bg: var(--vp-button-brand-bg)
}

View File

@ -0,0 +1,60 @@
// Assign theme color
$themeColor = #ce2828
.page-tachiyomi-sy {
.VPHero {
h1 {
.clip {
--vp-home-hero-name-background: -webkit-linear-gradient(120deg, var(--vp-c-brand) 30%, var(--vp-c-brand-dark))
}
}
}
.VPButton {
&.brand {
border-color: var(--vp-button-brand-border)
color: var(--vp-button-brand-text)
background-color: var(--vp-button-brand-bg)
}
}
.image-bg {
display: none
}
::selection {
background: alpha($themeColor, 0.2)
}
}
/**
* Colors
* -------------------------------------------------------------------------- */
:root .page-tachiyomi-sy {
--vp-c-brand: $themeColor
--vp-c-brand-light: tint($themeColor, 20%)
--vp-c-brand-lighter: tint($themeColor, 40%)
--vp-c-brand-lightest: tint($themeColor, 60%)
--vp-c-brand-dark: shade($themeColor, 25%)
--vp-c-brand-darker: shade($themeColor, 50%)
--vp-c-brand-darkest: shade($themeColor, 75%)
--vp-c-brand-dimm: alpha($themeColor, 0.08)
}
/**
* Component: Button
* -------------------------------------------------------------------------- */
:root .page-tachiyomi-sy {
--vp-button-brand-border: var(--vp-c-brand-light)
--vp-button-brand-text: var(--vp-c-white)
--vp-button-brand-bg: var(--vp-c-brand)
--vp-button-brand-hover-border: var(--vp-c-brand-light)
--vp-button-brand-hover-text: var(--vp-c-white)
--vp-button-brand-hover-bg: var(--vp-c-brand-light)
--vp-button-brand-active-border: var(--vp-c-brand-light)
--vp-button-brand-active-text: var(--vp-c-white)
--vp-button-brand-active-bg: var(--vp-button-brand-bg)
}

View File

@ -0,0 +1,99 @@
.tree {
border-radius: 8px
margin: 16px
padding: 16px
color: var(--vp-c-text-1)
// background-color: var(--vp-code-block-bg)
font-family: var(--vp-font-family-mono)
font-size: 0.85rem
line-height: 1.5
& > ul {
margin: 0
}
li {
& + li {
margin-top: 0
}
}
span {
&.folder {
&.root,
&.main {
color: var(--vp-c-brand-darker)
}
}
&.file.zip {
color: var(--vp-c-brand-darker) !important
}
&.file {
.file-extension {
color: var(--vp-c-text-2)
}
}
}
ul {
padding-left: 5px
list-style: none
li {
position: relative
padding-left: 15px
-webkit-box-sizing: border-box
-moz-box-sizing: border-box
box-sizing: border-box
&:before {
top: 15px
left: 0
width: 10px
height: 1px
margin: auto
}
&:after {
top: 0
bottom: 0
left: 0
width: 1px
height: 100%
}
&:before,
&:after {
position: absolute
content: ''
background-color: var(--vp-c-text-3)
}
&:last-child {
&:after {
height: 15px
}
}
}
}
&-icon {
height: 12px
width: 12px
margin-right: 6px
display: inline-block
}
}
.dark {
.tree {
span {
&.folder,
&.file.zip {
color: var(--vp-c-brand-lighter) !important
}
}
}
}

5
website/src/.vitepress/vue-shim.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
declare module "*.vue" {
import { Component } from "vue";
const _default: Component;
export default _default;
}

View File

@ -0,0 +1,18 @@
---
title: Changelogs
description: Changelogs of all Tachiyomi stable releases.
lastUpdated: false
editLink: false
prev: false
next: false
---
<script setup>
import ChangelogsList from "@theme/components/ChangelogsList.vue";
</script>
# Changelogs
Changelogs of all Tachiyomi stable releases, which are also available [on GitHub](https://github.com/tachiyomiorg/tachiyomi/releases). Preview releases can be seen [on GitHub](https://github.com/tachiyomiorg/tachiyomi-preview/releases).
<ChangelogsList />

View File

@ -0,0 +1,30 @@
---
title: Contribute
description: Find out how to help translate or build the app and extensions.
---
# Contribute
Find out how to help translate or build the app and extensions.
## Code
Know how to code and want to improve something or you generally want to support the creation of the app?
[![tachiyomiorg/tachiyomi - GitHub](https://gh-card.dev/repos/tachiyomiorg/tachiyomi.svg)](https://github.com/tachiyomiorg/tachiyomi)
[![tachiyomiorg/tachiyomi-extensions - GitHub](https://gh-card.dev/repos/tachiyomiorg/tachiyomi-extensions.svg)](https://github.com/tachiyomiorg/tachiyomi-extensions)
[![tachiyomiorg/website - GitHub](https://gh-card.dev/repos/tachiyomiorg/website.svg)](https://github.com/tachiyomiorg/website)
## Translation
![Graph of Weblate Translations](https://hosted.weblate.org/widgets/tachiyomi/-/strings/open-graph.png)
Want to help translate the app to your language? You can easily help by utilizing a service we use called **Weblate**.
### Helpful links
* [Translators guide](https://docs.weblate.org/en/latest/user/translating.html)
* [Secondary languages](https://docs.weblate.org/en/latest/user/profile.html#secondary-languages)
* [Subscriptions](https://docs.weblate.org/en/latest/user/profile.html#subscriptions)
* [Glossary](https://docs.weblate.org/en/latest/user/translating.html#glossary)
## Donation
If you're unable to contribute code nor translations but still wish to help, then you can choose to contribute directly to the projects founder [Inorichi](https://github.com/inorichi/) using [Ko-fi](https://ko-fi.com/inorichi).

View File

@ -0,0 +1,33 @@
---
title: Android 11+
titleTemplate: Frequently Asked Questions
description: Understanding Android 11 Changes.
---
# Android 11+
Understanding **Android 11** Changes.
## Android 11 modifications
**Android 11** introduced changes related to [Scoped Storage](https://developer.android.com/about/versions/11/privacy/storage), which was initially introduced in **Android 5.0 Lollipop**.
However, the enforcement of **Scoped Storage** was initiated by **Google** with **Android 11**.
Different smartphone manufacturers implement **Scoped Storage** with varying levels of success, resulting in varying user experiences.
Some encounter minimal issues, while others using different phone brands face numerous challenges.
## Implications for Tachiyomi
**Scoped Storage**'s introduction affects various storage-related functions in **Tachiyomi**.
These functions may become slower due to **Scoped Storage**'s inherent latency, as discussed in detail [here](https://www.xda-developers.com/android-q-storage-access-framework-scoped-storage/).
This impact encompasses tasks like deleting chapters, library loading times, accessing folders outside data directories for downloads and reading, and more.
## Enhancing performance
For potential performance improvements, consider utilizing the following ADB command if you are comfortable with it.
```bash
adb shell cmd appops set eu.kanade.tachiyomi android:legacy_storage allow
```
This command grants general storage access to the app, enabling **Tachiyomi** to use the conventional storage access interface.
If using **Tachiyomi Preview**, or a fork, replace `eu.kanade.tachiyomi` with the corresponding package name.

View File

@ -0,0 +1,72 @@
---
title: Extensions
titleTemplate: Browse - Frequently Asked Questions
description: Frequently Asked Questions about Extensions.
---
# Extensions
Frequently Asked Questions about Extensions.
## What are some recommended extensions and sources?
**Tachiyomi** does not endorse or recommend any source, and there is no best extension.
Instead, we encourage users to spend some time trying out a few sources themselves and discover what sources work best for them.
What works well for somebody else might not work well for you.
::: info Disclaimer
**Tachiyomi** isn't responsible for slow, down, missing chapters, or subpar image quality of sources as it doesn't host content.
:::
## What is a scanlator source?
Non-officially licensed series are translated by scanlators, often found on their websites or [MangaDex](https://mangadex.org/).
Compare with official sources like [MANGA Plus By SHUEISHA](https://mangaplus.shueisha.co.jp) or [VIZ Shonen Jump](https://www.viz.com/shonenjump).
Learn more in this [Wikipedia article](https://en.wikipedia.org/wiki/Scanlation).
## How do I request new extensions?
[Open an issue](https://github.com/tachiyomiorg/tachiyomi-extensions/issues) on **GitHub** if not already there.
Check the removed [extensions list](https://github.com/tachiyomiorg/tachiyomi-extensions/blob/master/REMOVED_SOURCES.md) first.
You can find the list of extensions to download [here](/extensions/)
## Enabling third-party installations
::::tabs
== Android 8.0 and higher
When prompted while installing your first extension, allow unknown apps installation from that source. For newer Androids, enable per-app in **Install unknown apps**.
::: details Video guide - Recorded on Android 10
<video controls muted preload="metadata">
<source src="/docs/faq/browse/extensions/unknown-sources-A10.light.webm" type="video/webm">
</video>
:::
::: tip Still got questions?
If you need more help regarding this, read [this post](https://nerdschalk.com/how-to-allow-apps-installation-from-unknown-sources-on-android-9-pie/ "nerdschalk.com | How to allow apps installation from unknown sources on Android 9 Pie").
:::
== Android 7.1 and lower
When prompted while installing your first extension, allow unknown apps installation from that source. For older Androids, enable globally in **Unknown sources**.
::: details Video guide - Recorded on Android 7
<video controls muted preload="metadata">
<source src="/docs/faq/browse/extensions/unknown-sources-A7.light.webm">
</video>
:::
::::
## How do I uninstall an extension?
Uninstall extensions like regular apps: through device settings or in **Tachiyomi**.
::: tip Uninstalling an extension
In **Tachiyomi**, uninstall an extension via <nav to="extensions">, then tap **Uninstall** on the chosen extension.
:::
## Why was an extension removed?
Extensions can be removed due to several reasons:
* Frequent website changes.
* Image scrambling.
* Scanlator removal request.
* Paywall implementation.
* Reverse engineering needs.
* Site shutdown.
Find the list of removed extensions [here](https://github.com/tachiyomiorg/tachiyomi-extensions/blob/master/REMOVED_SOURCES.md), excluding offline sites.

View File

@ -0,0 +1,37 @@
---
title: Browse
titleTemplate: Frequently Asked Questions
description: Frequently Asked Questions about Browse.
---
# Browse
Frequently Asked Questions about Browse.
## Why can't I see installed sources?
### If the extension language differs from your device's primary language
Enable the source's language at <nav to="sources">, tap on **Filter**, then turn on the language of the desired source.
### If it's an NSFW extension
Navigate to <nav to="browse"> and check the **Show in sources and extensions list** option.
## How can I locate a specific series?
At times, locating a source containing the series you want to read can be challenging.
Here are strategies to help you find it:
1. Search for the series on [Google](https://google.com/) or a database like [MangaUpdates](https://www.mangaupdates.com/).
1. Consider alternate spellings or title variations for the series.
- Retry searching your sources using the alternate titles you discovered.
1. If you still can't find the series on any source:
- Search for an extension for the website you found in *step 1* within the [extensions list](/extensions/).
### If you find the series on a specific scanlator or aggregator with an extension
Download the relevant extension, locate it under <nav to="sources">, and proceed to search for the series there.
### If you find the series on a specific scanlator or aggregator without an extension
Check if a request has been made to add the site as an extension [here](https://github.com/tachiyomiorg/tachiyomi-extensions/issues) and confirm it's not on the [list of extensions that won't be added back](https://github.com/tachiyomiorg/tachiyomi-extensions/blob/master/REMOVED_SOURCES.md).
If it's not requested and not on the exclusion list, you can [submit a request here](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
For scanlators or aggregators without an online reader, you can download the series from their website and set it up as a local series.

View File

@ -0,0 +1,29 @@
---
title: Local source
titleTemplate: Browse - Frequently Asked Questions
description: Frequently Asked Questions about the Local source.
---
# Local source
Frequently Asked Questions about the Local source.
## How can I import my downloaded series?
For a step-by-step process, we recommend referring to [this guide](/docs/guides/local-source/).
## What should I do if I can't find the Tachiyomi folder?
In case the **Tachiyomi** folder is not visible, create one using a file manager.
## How can I resolve empty/blank covers?
Occasionally, cover images for local series might not appear.
1. Confirm that you've organized the folder structure correctly.
* To verify, access the series with the missing cover and see if you can read chapters within the app.
* If not, follow the [provided guide](/docs/guides/local-source/#folder-structure) first.
1. Capture a screenshot of the read chapters, then remove the series from your library.
1. Navigate to <nav to="advanced"> and tap **Clear database**.
* This action will only affect series not in your library.
2. Return to <nav to="sources">, go to **Local source** and locate the series.
* The cover issue should now be resolved.
* Re-add the series to your library, mark your read chapters, and re-enable tracking if necessary.
Related GitHub Issue: [#932](https://github.com/tachiyomiorg/tachiyomi/issues/932)

View File

@ -0,0 +1,44 @@
---
title: Downloads
titleTemplate: Frequently Asked Questions
description: Frequently Asked Question about Downloads.
---
# Downloads
Frequently Asked Question about Downloads.
## How do I download multiple chapters or series at the same time?
The app does not support parallel downloads from a single source to prevent potential IP bans due to excessive requests.
While this might impact speed, it's preferable over rendering a source inaccessible.
Note that Tachiyomi will download from up to five different sources in parallel.
## Why did my downloads stop midway?
Downloads stopping midway may be related to network connection issues or source problems.
**Tachiyomi** will provide notifications regarding encountered errors during download attempts.
## Why can't I see my downloads?
Downloads might not be detected due to multiple factors:
* Inaccessibility of the download location.
> Ensure the SD card is properly detected if in use.
* Source name changes.
> Rename the source's folder to match the new name.
* Series title modified by the source.
> Adjust the folder title to the updated name.
## How do I manage what's downloading?
Navigate to <nav to="download-queue"> to interact with queued downloads.
Cancel all items by clicking the **Overflow** button beside a series chapter or the top right corner.
To reorder the queue, long-press and drag the `=` icon next to a queue item.
## Can I use both internal storage and external SD card storage?
No, you must choose a single location. Internal storage performs better than external SD cards.
## Why does my device photo gallery contain series pages?
**Tachiyomi** typically prevents series pages in downloads from appearing in your device's photo gallery by default through a `.nomedia` file.
However, in some cases, this might not function as intended.
A quick solution is to create the `.nomedia` file yourself, name it as such, and place it in your downloads folder. If the issue pertains to local source, put the `.nomedia` file in the respective local folder.

View File

@ -0,0 +1,75 @@
---
title: General
titleTemplate: Frequently Asked Questions
description: Frequently Asked Questions
---
# General
Frequently Asked Questions
## Why isn't Tachiyomi on the Google Play Store?
**Tachiyomi** won't be on the **Google Play Store**.
APK-based extensions conflict with [Google Play's content policy](https://play.google.com/about/developer-content-policy/).
**Google** might take down the app due to certain content, which the developers wishes to avoid.
To report **Tachiyomi** copycats on the **Google Play Store**, fill out [this form](https://support.google.com/googleplay/android-developer/contact/takedown) following the steps below.
:::details Steps to report Tachiyomi copycats
1. Go to the page -> three dots menu -> Flag as inappropriate -> Other objection.
1. After choosing "Other objection", you may choose to put down any or all of the following:
The app infringes on the Google Play Developer Policy by
Encouraging Infringement of Copyright
> We dont allow apps that induce or encourage copyright infringement. Before you publish your app, look for ways your app may be encouraging copyright infringement and get legal advice if necessary.
Trademark Infringement
> We dont allow apps that infringe on others trademarks. A trademark is a word, symbol, or combination that identifies the source of a good or service. Once acquired, a trademark gives the owner exclusive rights to the trademark usage with respect to certain goods or services.
>
> Trademark infringement is an improper or unauthorized use of an identical or similar trademark in a way that is likely to cause confusion as to the source of that product. If your app uses another partys trademarks in a way that is likely to cause confusion, your app may be suspended.
Misleading Claims, e.g. "MangaKa - Best Manga Reader"
> We dont allow apps that contain false or misleading information or claims, including in the description, title, icon, and screenshots. Here are some examples of common violations: Apps that misrepresent or do not accurately and clearly describe their functionality:
> - Developer or app names that misrepresent their current status or performance on Play. (E.g. “Editors Choice,” “Number 1 App,” “Top Paid”)...
> - Apps that are improperly categorized.
:::
## Is Tachiyomi available for iOS/iPadOS?
There is no iOS or iPadOS version and neither are there plans for one.
Porting is difficult due to the separate codebases of iOS and Android apps.
Any app proclaiming to be "**Tachiyomi for iOS**" is not by us and should be treated as a scam.
## What is Tachiyomi Preview?
**Tachiyomi Preview** is a regularly updated beta version of the app.
It showcases potential upcoming features, but it's more prone to bugs and crashes.
Ideal for users seeking the latest **Tachiyomi** experience, it's essential to [enable auto-backup](/docs/guides/backups#enabling-automatic-backups) to prevent library loss due to issues.
## How do I update from the F-Droid builds?
**Tachiyomi** on **F-Droid** lacks official support, updates aren't guaranteed.
To migrate to official builds:
1. Create a backup.
1. Uninstall the **F-Droid** version (discard app data if possible).
1. Download/install **Tachiyomi**.
1. Restore your backup.
Hosting an **F-Droid** repo isn't planned, as **Tachiyomi** manages updates independently, rendering **F-Droid** usage redundant.
Refer to [this GitHub comment](https://github.com/tachiyomiorg/tachiyomi/issues/6736#issuecomment-1059608058) for details.
## Can I read light novels?
**Tachiyomi** can't read light novels; it's an image parser, not a text parser.
## Can I stream anime?
**Tachiyomi** isn't designed for anime streaming.
Projects using the **Tachiyomi** name for anime streaming aren't affiliated with the main project.
## What's a fork?
Forks are alternate **Tachiyomi** versions with distinct features. Get more details [here](/forks/).

View File

@ -0,0 +1,75 @@
---
title: Library
titleTemplate: Frequently Asked Questions
description: Frequently Asked Questions about the Library.
---
# Library
Frequently Asked Questions about the Library.
## Why is Global Update skipping entries?
The app's default behavior is to skip updates for entries that meet the following criteria:
* Have unread chapters
* Haven't been initiated
* Carry a "**Completed**" status
This strategy prevents unnecessary load which may lead to sources implementing measures against **Tachiyomi**.
To manage entries with infrequent or no updates, consider using categories and excluding them from updates.
We recommend using the default settings and prioritizing unread chapters for reading.
If you wish to disable the notification about skipped items, you can do so at <nav to="advanced"> and then **Manage notifications** (doing so requires Android 8 or above).
## Why am I warned about large bulk updates and downloads?
Excessive server load may trigger anti-**Tachiyomi** measures from sources. See the previous question for more context. Long-running update checks and downloads may also impact your device's battery life.
To mitigate these concerns:
* Use categories to segment your library ("Reading", "Plan to Read", "Completed", etc.).
* Update only the Reading category by navigating to <nav to="library">, then tap **Categories** under **Global update**.
* If the warning persists, create a new category for infrequently updated entries (like monthly series or those on hiatus) and set global updates to target the more frequently updated reading category.
## Why aren't library updates working?
Some Android skins (e.g., **MIUI**) aggressively save battery, potentially shutting down apps in the background.
Whitelist **Tachiyomi** from your battery saver by going to <nav to="advanced"> and tapping **Disable battery optimization**.
If unsuccessful, refer to [Don't Kill My App](https://dontkillmyapp.com/) for how to disable specific battery-saving options for your device.
## How can I see the number of downloaded chapters?
Badges can be enabled by navigating to <nav to="main_library">, then going to **Filter** and clicking the **Display** tab.
Then, at the bottom, select **Download badges**.
## Can I sync between multiple devices?
**Tachiyomi** can't sync between devices.
Use its [backup and restore](/docs/guides/backups) features, including [auto backups](/docs/guides/backups#enabling-automatic-backups), for series database and content migration to another device.
## How can I ignore duplicate chapters?
Dealing with series translated by multiple groups that result in duplicate chapter releases?
Bookmark or mark as read the undesired chapters, then open the **Filter** menu, ensure you're on the **Filter** tab, then double-tap **Bookmarked** or single-tap **Unread**.
This hides bookmarked or read chapters, enabling you to skip them as you read.
Ensure [Skip filtered chapters](/docs/guides/reader-settings#skip-filtered-chapters) is enabled at <nav to="reader"> under the section **Reading**.
Alternatively, migrate to a source without duplicates.
Refer to the [migration guide](/docs/guides/source-migration) for detailed instructions.
## Why are some cover thumbnails corrupted or blank?
If cover thumbnails appear corrupted, blank, or broken, it's likely due to an incomplete download. Fix this by refreshing the covers in settings.
Refresh your covers at <nav to="advanced"> then tap **Refresh library covers**.
## Why have some chapters been marked as unread?
If certain series chapters are marked as unread without your interaction, it could be due to changed URLs.
**Tachiyomi** detects these changes and interprets the chapters as new.
## How do I pause reading history?
Disable **Incognito Mode** through <nav to="incognito-mode">.
## How do I only read downloaded chapters?
Enable **Download only** via <nav to="downloaded-only">.
## Why can't I disable the Downloaded filter?
Disable **Download only** via <nav to="downloaded-only">.

View File

@ -0,0 +1,18 @@
---
title: Reader
titleTemplate: Frequently Asked Questions
description: Frequently Asked Questions about the Reader.
---
# Reader
Frequently Asked Questions about the Reader.
## Why didn't the page load?
Besides network-related problems, **Tachiyomi** may occasionally fail to recognize certain images.
To address this, simply exit and re-enter the reader, often resolving the issue.
## Can I see two pages at once?
Not currently. Creating an effective dual-page reader that accommodates scanlator page inconsistencies and other complexities poses challenges. This feature may be added in the future.
## What do all the settings do?
For detailed instructions, please consult the guides section on the website [here](/docs/guides/reader-settings).

View File

@ -0,0 +1,24 @@
---
title: Settings
titleTemplate: Frequently Asked Questions
description: Frequently Asked Questions about various app settings.
---
# Settings
Frequently Asked Questions about various app settings.
## Why is taking screenshots blocked?
Turn off **Secure Screen** in <nav to="security-and-privacy">.
## What is DNS over HTTPS?
**DNS over HTTPS (DoH)** (in <nav to="advanced">) offers secure DNS resolution through HTTPS, preventing attacks.
Learn more [here](https://www.cloudflare.com/learning/dns/dns-over-tls/). This may help bypass some basic website blocking.
## What are the different installers?
Three installer options are available (in <nav to="advanced">):
* **Legacy**: A fallback installer if the standard **PackageInstaller** doesn't work.
> This is the default for **MIUI** (i.e. Xiaomi devices).
* **PackageInstaller**: The primary installer option with additional features dependent on the Android version.
> For instance, it can bypass user prompts when updating extensions on **Android 12**.
* **Shizuku**: Refer to the [Shizuku guide](/docs/guides/shizuku) for details on **Shizuku**'s functionality.

View File

@ -0,0 +1,112 @@
---
title: Backups
titleTemplate: Guides
description: Backups helps you prevent losing your library if something happens.
---
# Backups
Backups in **Tachiyomi** are compatible between different versions of the app.
::: tip How to create a backup
1. Go to <nav to="backup-and-restore">.
1. Select **Create backup** and choose a location to save it.
![Backup and Restore](/docs/guides/backups/backup.light.webp#light =414x215)
![Backup and Restore](/docs/guides/backups/backup.dark.webp#dark =414x215)
:::
## General backup details
### What's included in a backup?
Backups store the following information:
- **Titles**
- **Categories**
- **Read chapters** for titles in Library
- **Tracking settings**
- **Reading history**
- **Series information**
- Author, Artist, Date Added to Library, Selected Viewer, Read Duration, etc.
- **Extensions** used
### What's NOT included in a backup?
Backups do NOT store:
- **Reading history** of titles NOT in library
- **Settings** including app settings and extension-specific settings
- **Downloaded** chapters including [local source](/docs/guides/local-source/) chapters
## Restoring a backup
Restoring a backup can be done through the Backup and Restore settings.
To ensure a smooth restoration process, remember to:
1. Log into the Tracking services you previously used.
1. Download any extensions you've used in your backup.
Before starting to import the selected backup, the app will remind the user of these.
### Transferring downloads to a new installation
You can transfer downloaded series chapters from one version of **Tachiyomi** to another
by correctly specifying the Download Location.
## Suggestions for backups
### Enabling automatic backups
It is highly recommended to enable automatic backups to ensure you can recover in case of any issues.
::: tip How to enable automatic backups
1. Go to <nav to="backup-and-restore">.
1. Set a **backup frequency** to schedule automatic backups.
This way, you can recover from catastrophic failures.
![Automatic Backups](/docs/guides/backups/automatic_backups.light.webp#light =414x402)
![Automatic Backups](/docs/guides/backups/automatic_backups.dark.webp#dark =414x402)
:::
### Syncing backups with external cloud services
Cross device sync in **Tachiyomi** is not planned in the future, but users can use
[Autosync for Google Drive](https://play.google.com/store/apps/details?id=com.ttxapps.drivesync)
in order to sync backup files to Drive automatically with the following steps:
1. Install the app from the link above
2. Enable [Automatic Backups](/docs/guides/backups#enabling-automatic-backups) and set it to your desired frequency and storage location.
3. Download the latest backup from Google Drive and restore to whatever device you have
Users who are familiar with [FolderSync](https://play.google.com/store/apps/details?id=dk.tacit.android.foldersync.lite)
or [Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm) can setup auto sync of their backups similarly
## Additional information for forks
::: warning
This section explores some extra details regarding [forks](/forks/).
:::
All [Endorsed Tachiyomi forks](/forks/) support the `.proto.gz` format to backup/restore your library
In addition, some forks have specific limitations regarding backup restoration:
- [Neko](/forks/Neko/) can only restore [MangaDex](/extensions/#all.mangadex) entries in a backup.
> Entries from other sources will not transfer.
> Migrate everything to MangaDex if you require to.
- All forks have fork-specific settings and changes that might be saved in Backups.
Such settings are not restored in original Tachiyomi and will get lost when creating a new backup.
> For Example: [TachiyomiSY](/forks/TachiyomiSY/) has the option to backup/restore saved searches.
These will **NOT** be restored to original Tachiyomi or its forks.
- Only [TachiyomiAZ](/forks/TachiyomiAZ/) supports creating/restoring legacy `.json` backups and current `.proto.gz` backups.
> Users are recommended to update their `.json` backups to use the improved and efficient `.proto.gz` backups.
Be aware of these limitations when dealing with backups in different **Tachiyomi** forks.

View File

@ -0,0 +1,44 @@
---
title: Categories
titleTemplate: Guides
description: Organize your favorite series effortlessly with categories that declutter and structure your library.
---
# Categories
Organize your favorite series effortlessly with categories that declutter and structure your library.
To manage your categories, navigate to <nav to="categories">.
- You can name and sort categories as you prefer (e.g., by `Genre`, `Reading Status`).
- Add series to multiple categories and control update options through Library settings, even auto-download chapters from chosen categories.
> If you've enabled **Download new chapters** in the Downloads settings.
## Content
Categories would be useless without any content in them.
Below are some tips for using them.
:::: tabs
== Add entries
### Add series to a category
1. Long press the series you want to add.
1. Press the **Set categories** button.
1. Select which category or categories you want it in and press **OK**.
::: tip
You can also add multiple series to a category by selecting them when you see the **Set categories** button.
:::
== Remove entries
### Remove series from a category
1. Long press series that you want to remove.
1. Press the **Set categories** button.
1. Deselect the category or categories you want to remove it from and press **OK**.
::: tip
You can also remove multiple series from a category by selecting them when you see the **Set categories** button.
:::
::::

View File

@ -0,0 +1,78 @@
---
title: Getting started
titleTemplate: Guides
description: Essential information to help you get set up with Tachiyomi.
---
<script setup lang="ts">
import { data as release } from "@theme/data/release.data"
</script>
# Getting started
Essential information to help you get set up with Tachiyomi.
## Installation guide
### 1. Downloading Tachiyomi
1. Visit our [download](/download/) page to get the latest version of **Tachiyomi**.
1. After the download is complete, open the `tachiyomi-{{ release.stable.tag_name }}.apk` file.
1. Proceed with the installation process.
### 2. Adding sources
Once **Tachiyomi** is installed on your device, you can install extensions to access a wide range of sources.
1. Open the **Tachiyomi** app.
1. Navigate to the "**Browse**" section.
1. Tap on the "**Extensions**" tab.
1. Look for the extension you want to use and press the "**INSTALL**" button next to it.
1. Proceed with the installation process.
![Installing extensions](/docs/guides/getting-started/installing-extensions.light.webp#light =414x245)
![Installing extensions](/docs/guides/getting-started/installing-extensions.dark.webp#dark =414x245)
::: tip INSTALL PERMISSION
Depending on your device settings, you might need to grant **Tachiyomi** permission to install unknown apps.
![Security warning](/docs/guides/getting-started/security-warning.webp =546x165)
> Relevant guide: [Enabling Third-Party Installations](/docs/faq/browse/extensions#enabling-third-party-installations)
:::
### 3. Adding series to your library
After installing the desired extension, you'll find it in the **Sources** tab.
Here's how you can add series to your library:
1. Select the source you'd like to browse.
1. You can use the **Popular**/**Latest** listings to browse, or you can search for the series name.
1. Once you've found the series that you want to add, tap on it for more details.
1. Press the "**Add to library**" button, and the series will be added to your Library, ready to be read!
## Additional setup
### Series search options
If you want to search for series across all your sources, you can use the Global Search feature.
Follow these steps:
1. Go to the "**Browse**" section.
1. Ensure you're on the "**Sources**" tab located at the top-right corner.
1. Use the Search icon in the toolbar to find series from all available sources.
### Trouble finding a specific series?
If you encounter difficulties while searching for a specific series, consider the following points:
* Double-check your spelling and try again, as some sources might use **Japanese romanized** titles instead of **English** ones.
> Example: **Boku no Hero Academia** instead of **My Hero Academia**.
* Some sources may use different spellings or wordings for titles.
> Example: **Bungo Stray Dogs** instead of **Bungou Stray Dogs**
> Example: **3-gatsu no Lion** instead of **Sangatsu no Lion**.

View File

@ -0,0 +1,49 @@
---
title: Advanced editing
titleTemplate: Local source - Guides
description: Advanced local series metadata editing for enhanced library organization.
---
# Advanced editing
Advanced local series metadata editing for enhanced library organization.
## Editing local series details
It is possible to add details to local series.
Like series from other sources, you add information about the series such as the `author`, `artist`, `description`, and `genre` tags.
To import details along with your local series, you have to create a JSON file.
It can be named anything but it must be placed within the **Series** folder.
A standard file name is `details.json`.
This file will contain the extended details about the series in the `JSON` format.
You can see the example below on how to build the file.
Once the file is there, the app should load the data when you first open the series or you can pull down to refresh the details.
You can copy the following example and edit the details as needed:
```json
{
"title": "Example Title",
"author": "Example Author",
"artist": "Example Artist",
"description": "Example Description",
"genre": ["genre 1", "genre 2", "etc"],
"status": "0",
"_status values": ["0 = Unknown", "1 = Ongoing", "2 = Completed", "3 = Licensed", "4 = Publishing finished", "5 = Cancelled", "6 = On hiatus"]
}
```
::: tip
If you don't want to manually create the `details.json` file, you can alternatively use [this tool.](https://tachi-local.netlify.app/?utm\_source=tachi-website\&utm\_medium=referral\&utm\_campaign=tachi-website)
:::
## Using a custom cover image
It is also possible to use a custom image as a cover for each local series.
To do this, you only need to place the image file, that needs to be named `cover.jpg`, in the root of the series folder.
The app will then use your custom image in the local source listing.
<style scoped>
@import "../../../.vitepress/theme/styles/tree.styl"
</style>

View File

@ -0,0 +1,204 @@
---
title: Local source
titleTemplate: Guides
description: For users who would like to download and organize their own media.
---
# Local source
If you like to download and organize your media, then you want to know how to manage your own series in Tachiyomi.
::: warning
This page explores some advanced features.
:::
## Creating local series
1. Create a folder named `local` in the `/Tachiyomi/` folder.
> The `/Tachiyomi/` folder is located in the root of phone's **internal storage** or **external SD card** and it's **not related** to the `eu.kanade.tachiyomi/` folder or the download location in the settings.
1. Place correctly structured series inside `/Tachiyomi/local/`.
> If adding series in folders it is recommended to add a file named `.nomedia` to the local folder so images do not show up in the gallery.
1. You should now be able to access the series in <nav to="sources"> under **Local source**.
If you add more chapters then you'll have to manually refresh the chapter list (by pulling down the list).
Supported chapter formats are folders with pictures inside (such as `.jpg`, `.png`, etc), `ZIP`/`CBZ`, `RAR`/`CBR` and `EPUB`.
But expect better performance with directories and `ZIP`/`CBZ`.
Remember to give the app storage permissions on **Android 6** and newer.
### Folder structure
Tachiyomi requires a specific folder structure for local series to be correctly processed.
Local series will be read from the `Tachiyomi/local` folder.
Each series must have a `Series` folder and a `Chapter` folder.
Images will then go into the chapter folder.
See below for more information on archive files.
You can refer to the following example:
#### Example {#example-storages}
:::tabs
== Device Storage
<div class="tree">
<ul>
<img src="/img/folder.svg" class="tree-icon icon-folder">
<span class="folder root">/sdcard/Tachiyomi/local</span>
<li>
<img src="/img/folder.svg" class="tree-icon icon-folder">
<span class="folder main">[the series title]</span>
<ul>
<li>
<img src="/img/jpeg.svg" class="tree-icon icon-jpeg">
<span class="file jpg">cover<span class="file-extension">.jpg</span></span>
</li>
<li>
<img src="/img/folder.svg" class="tree-icon icon-folder">
<span class="folder">chapter_1</span>
<ul>
<li><span class="file">image_1<span class="file-extension">.ext</span></span></li>
<li><span class="file">image_n<span class="file-extension">.ext</span></span></li>
</ul>
</li>
<li>
<img src="/img/folder.svg" class="tree-icon icon-folder">
<span class="folder">chapter_2</span>
<ul>
<li><span class="file">image_1<span class="file-extension">.ext</span></span></li>
<li><span class="file">image_n<span class="file-extension">.ext</span></span></li>
</ul>
</li>
<li>
<img src="/img/folder.svg" class="tree-icon icon-folder">
<span class="folder">chapter_n</span>
<ul>
<li><span class="file">image_1<span class="file-extension">.ext</span></span></li>
<li><span class="file">image_n<span class="file-extension">.ext</span></span></li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
== SD Card
<div class="tree">
<ul>
<img src="/img/folder.svg" class="tree-icon icon-folder">
<span class="folder root">/storage/18F5-2C11/Tachiyomi/local</span>
<li>
<img src="/img/folder.svg" class="tree-icon icon-folder">
<span class="folder main">[the series title]</span>
<ul>
<li>
<img src="/img/jpeg.svg" class="tree-icon icon-jpeg">
<span class="file jpg">cover<span class="file-extension">.jpg</span></span>
</li>
<li>
<img src="/img/folder.svg" class="tree-icon icon-folder">
<span class="folder">chapter_1</span>
<ul>
<li><span class="file">image_1<span class="file-extension">.ext</span></span></li>
<li><span class="file">image_n<span class="file-extension">.ext</span></span></li>
</ul>
</li>
<li>
<img src="/img/folder.svg" class="tree-icon icon-folder">
<span class="folder">chapter_2</span>
<ul>
<li><span class="file">image_1<span class="file-extension">.ext</span></span></li>
<li><span class="file">image_n<span class="file-extension">.ext</span></span></li>
</ul>
</li>
<li>
<img src="/img/folder.svg" class="tree-icon icon-folder">
<span class="folder">chapter_n</span>
<ul>
<li><span class="file">image_1<span class="file-extension">.ext</span></span></li>
<li><span class="file">image_n<span class="file-extension">.ext</span></span></li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
:::
Tachiyomi will see four chapters in a single series.
The path to the folder with images must contain both the series title and the chapter name (as seen above).
### Archive files
Archive files such as `ZIP`/`CBZ` are supported but the folder structure inside is not.
Any folders inside the archive file are ignored.
You must place the archive inside the `Series` folder where the name will become the `Chapter` title.
All images inside the archive regardless of folder structure will become pages for that chapter.
#### Example {#example-archives}
:::tabs
== .ZIP
<div class="tree">
<ul>
<img src="/img/folder.svg" class="tree-icon icon-folder">
<span class="folder root">/sdcard/Tachiyomi/local</span>
<li>
<img src="/img/folder.svg" class="tree-icon icon-folder">
<span class="folder main">[the series title]</span>
<ul>
<li>
<img src="/img/jpeg.svg" class="tree-icon icon-jpeg">
<span class="file jpg">cover<span class="file-extension">.jpg</span></span>
</li>
<li>
<img src="/img/zip.svg" class="tree-icon icon-zip">
<span class="file zip">chapter_1<span class="file-extension">.zip</span></span>
<ul>
<li>
<img src="/img/jpeg.svg" class="tree-icon icon-jpeg">
<span class="file jpg">image_1<span class="file-extension">.jpg</span></span>
</li>
<li>
<img src="/img/jpeg.svg" class="tree-icon icon-jpeg">
<span class="file jpg">image_n<span class="file-extension">.jpg</span></span>
</li>
</ul>
</li>
<li>
<img src="/img/zip.svg" class="tree-icon icon-zip">
<span class="file zip">chapter_2<span class="file-extension">.zip</span></span>
<ul>
<li>
<img src="/img/jpeg.svg" class="tree-icon icon-jpeg">
<span class="file jpg">image_1<span class="file-extension">.jpg</span></span>
</li>
<li>
<img src="/img/jpeg.svg" class="tree-icon icon-jpeg">
<span class="file jpg">image_n<span class="file-extension">.jpg</span></span>
</li>
</ul>
</li>
<li>
<img src="/img/zip.svg" class="tree-icon icon-zip">
<span class="file zip">chapter_n<span class="file-extension">.zip</span></span>
<ul>
<li>
<img src="/img/jpeg.svg" class="tree-icon icon-jpeg">
<span class="file jpg">image_1<span class="file-extension">.jpg</span></span>
</li>
<li>
<img src="/img/jpeg.svg" class="tree-icon icon-jpeg">
<span class="file jpg">image_n<span class="file-extension">.jpg</span></span>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
:::
<style scoped>
@import "../../../.vitepress/theme/styles/tree.styl"
</style>

View File

@ -0,0 +1,296 @@
---
title: Reader settings
titleTemplate: Guides
description: This section relates to the reading experience in the app and navigating the reader.
---
# Reader settings
This section relates to the reading experience in the app and navigating the reader.
## Options
### Default reading mode <Badge type="info" text="Paged (right to left)" />
This setting sets the reader's default direction when you open a series.
::: tabs
== Paged (RTL)
Right-to-left, the default way of reading manga.
- Swipe right for next page.
- Swipe left for previous page.
== Paged (LTR)
Left-to-right, the default way of reading comics.
- Swipe left for next page.
- Swipe right for previous page.
== Paged (Vertical)
- Swipe up for next page.
- Swipe down for previous.
== Long strip
Default way of reading webtoons.
== Long strip with gaps
Long strip but with a little space between pages.
:::
::: tip
You can change viewer for different series by going to the series, opening a chapter, tapping the middle of the screen, pressing the gear icon, and selecting a different viewer in **Viewer for this series**.
:::
### Double tap animation speed <Badge type="info" text="Normal" />
Double tap animation speed changes the speed in which the zoom happens when double tapping.
::: tabs
== No animation
TBA
== Normal
TBA
== Fast
TBA
:::
### Show reading mode <Badge type="info" text="On" />
Briefly show current mode when reader is opened
### Show tap zones overlay <Badge type="info" text="Off" />
TBA
### 32-bit color <Badge type="info" text="Off" />
This setting decodes images in `ARGB888` format to allow the reader to display more colors.
::: warning
This setting will only show up on devices running **Android 8.0** or higher.
:::
### Animate page transitions <Badge type="info" text="On" />
This setting applies a smooth transition when tapping to change page.
## Display
### Default rotation type <Badge type="info" text="Free" />
This allows you to control how the screen is going to be oriented.
::: tabs
== Free
TBA
== Portrait
TBA
== Landscape
TBA
== Locked portrait
TBA
== Locked landscape
TBA
== Reverse portrait
TBA
:::
### Background color <Badge type="info" text="Black" />
::: tabs
== Black
![Black](/docs/guides/reader-settings/background-color_black.webp =512x788)
== Gray
![Gray](/docs/guides/reader-settings/background-color_gray.webp =512x788)
== White
![White](/docs/guides/reader-settings/background-color_white.webp =512x788)
== Automatic
Automatically sets the color based on the content of your page.
:::
### Fullscreen <Badge type="info" text="On" />
TBA
### Show content in cutout area <Badge type="info" text="On" />
TBA
### Keep screen on <Badge type="info" text="On" />
TBA
### Show page number <Badge type="info" text="On" />
TBA
## Reading
### Skip chapters marked read <Badge type="info" text="Off" />
TBA
### Skip filtered chapters <Badge type="info" text="On" />
TBA
### Skip duplicate chapters <Badge type="info" text="Off" />
TBA
### Always show chapter transition <Badge type="info" text="On" />
TBA
## Pages
### Tap zones <Badge type="info" text="Default" /> {#tap-zones-pages}
::: tabs
== Default
![Right and Left](/docs/guides/reader-settings/tap-zones_right-and-left.webp =247x600)
== L shaped
![L shaped](/docs/guides/reader-settings/tap-zones_l-shaped.webp =247x600)
== Kindle-ish
![Kindle-ish](/docs/guides/reader-settings/tap-zones_kindle-ish.webp =247x600)
== Edge
![Edge](/docs/guides/reader-settings/tap-zones_edge.webp =247x600)
== Right and Left
![Right and Left](/docs/guides/reader-settings/tap-zones_right-and-left.webp =247x600)
== Disabled
No tap zones to assist with navigation will be active.
:::
### Invert tap zones <Badge type="info" text="None" /> {#invert-tap-zones-pages}
::: tabs
== None
Keeps the default zap zones.
== Horizontal
Changes so that the tap zones are flipped horizontally.
== Vertical
Changes so that the tap zones are flipped vertically.
== Both
Changes so that the tap zones are flipped horizontally and vertically.
:::
### Scale type <Badge type="info" text="Fit screen" />
::: tabs
== Fit screen
![Fit screen](/docs/guides/reader-settings/scale-type_fit-screen.webp =512x788)
== Stretch
![Stretch](/docs/guides/reader-settings/scale-type_stretch.webp =512x788)
== Fit width
![Fit width](/docs/guides/reader-settings/scale-type_fit-width.webp =512x788)
== Fit height
![Fit height](/docs/guides/reader-settings/scale-type_fit-height.webp =512x788)
== Original size
![Original size](/docs/guides/reader-settings/scale-type_original-size.webp =512x788)
== Smart fit
![Smart fit](/docs/guides/reader-settings/scale-type_smart-fit.webp =512x788)
:::
### Zoom start position <Badge type="info" text="Automatic" />
::: tabs
== Automatic
![Center (TBA Default Image)](/docs/guides/reader-settings/zoom-start-position_center.webp =512x788)
== Left
![Left](/docs/guides/reader-settings/zoom-start-position_left.webp =512x788)
== Right
![Right](/docs/guides/reader-settings/zoom-start-position_right.webp =512x788)
== Center
![Center](/docs/guides/reader-settings/zoom-start-position_center.webp =512x788)
:::
### Crop borders <Badge type="info" text="Off" /> {#crop-borders-pages}
:::tabs
== Off
![Not cropping borders](/docs/guides/reader-settings/crop-borders_off.webp =512x788)
== On
![Cropping borders](/docs/guides/reader-settings/crop-borders_on.webp =512x788)
:::
### Automatically zoom into wide images <Badge type="info" text="On" />
TBA
### Pan wide images <Badge type="info" text="On" />
TBA
### Split wide pages <Badge type="info" text="Off" /> {#split-wide-pages}
TBA
### Rotate wide pages to fit <Badge type="info" text="Off" />
TBA
## Long strip
### Tap zones <Badge type="info" text="Default" /> {#tap-zones-longstrip}
::: tabs
== Default
![Right and Left](/docs/guides/reader-settings/tap-zones_right-and-left.webp =247x600)
== L shaped
![L shaped](/docs/guides/reader-settings/tap-zones_l-shaped.webp =247x600)
== Kindle-ish
![Kindle-ish](/docs/guides/reader-settings/tap-zones_kindle-ish.webp =247x600)
== Edge
![Edge](/docs/guides/reader-settings/tap-zones_edge.webp =247x600)
== Right and Left
![Right and Left](/docs/guides/reader-settings/tap-zones_right-and-left.webp =247x600)
== Disabled
No tap zones to assist with navigation will be active.
:::
### Invert tap zones <Badge type="info" text="None" /> {#invert-tap-zones-longstrip}
::: tabs
== None
TBA
== Horizontal
TBA
== Vertical
TBA
== Both
TBA
:::
### Side padding <Badge type="info" text="None" />
::: tabs
== None
TBA
== 5%
TBA
== 10%
TBA
== 15%
TBA
== 20%
TBA
== 25%
TBA
:::
### Sensitivity for hiding menu on scroll <Badge type="info" text="Low" />
::: tabs
== Highest
TBA
== High
TBA
== Low
TBA
== Lowest
TBA
:::
### Crop borders <Badge type="info" text="Off" /> {#crop-borders-longstrip}
TBA
### Split wide pages <Badge type="info" text="Off" /> {#split-wide-longstrip}
TBA
### Double tap to zoom <Badge type="info" text="On" />
TBA
## Navigation
TBA
### Volume keys <Badge type="info" text="Off" />
TBA
## Actions
### Show on long tap <Badge type="info" text="On" />
TBA
### Save pages into seperate folders <Badge type="info" text="Off" />
TBA

View File

@ -0,0 +1,17 @@
---
title: Shizuku
titleTemplate: Guides
description: Using Shizuku with Tachiyomi.
---
# Shizuku
**Shizuku** is an alternative method for installing and updating extensions in **Tachiyomi**.
It taps into system APIs to directly install packages without requiring user interaction.
To get a detailed understanding of **Shizuku**, its purpose, and how it operates, refer to the information provided [here](https://shizuku.rikka.app/introduction/).
## Setting up Shizuku
To set up **Shizuku**, follow the instructions [here](https://shizuku.rikka.app/guide/setup/).
After configuring it, enable **Shizuku** within **Tachiyomi** by navigating to <nav to="advanced"> then changing the **Installer** setting.

View File

@ -0,0 +1,100 @@
---
title: Source migration
titleTemplate: Guides
description: Migration is the process of moving series between sources without losing progress.
---
# Source migration
Migration is the process of moving series between sources without losing progress. This is most often used when a source is no longer accessible or another source is more up-to-date.
::: warning
Always make sure to have a backup in case anything unexpected occurs.
:::
::: danger
Downloaded chapter(s) do not transfer with migrations.
Migrations with downloaded chapter(s) may leave the download behind.
You will need to remove these manually with a file manager.
:::
## Migration guide
::::tabs
==Tachiyomi
### Migrating from Series
1. Tap into the **Series** you would like to migrate.
1. Go to Overflow and tap Migrate.
> **Tachiyomi** will do a global search of all the sources you have installed and enabled.
1. Select the **Source** that you'd like to migrate _to_ by tapping the **Series** thumbnail.
1. Choose which data you want to transfer over, and you're done.
### Migrating from Source
1. Tap into Browse on the bottom navbar.
1. Press the Migrate tab at the top next to Extensions.
1. Select the **Source** that you'd like to migrate _from_.
1. Tap the **Series** you'd like to migrate _from_ the **Source**.
> **Tachiyomi** will do a global search of all the sources you have installed and enabled.
1. Select the **Source** you'd like to migrate _to_ by tapping the **Series** thumbnail.
1. Choose which data you want to transfer over, and you're done.
==TachiyomiJ2K
### Migrating multiple Series
1. Tap **Settings** -> **Sources** -> **Source migration**.
1. Select the **Source** you'd like to migrate _from_ and select **All**.
1. Select the **Sources** that you'd like to migrate _to_ and search by and tap the arrow at the bottom right.
1. Choose which data you want to transfer over.
1. Wait until all your **Series** is found and hit the done at the top and you're done.
> If a series is not found, or is wrong you can manually search it by pressing Overflow -> **Search manually**.
### Migrating a single Series
1. Tap into a **Series** in your Library.
1. Tap **Overflow** -> **Migrate**.
1. Select the **Sources** you'd like to search and migrate _to_ and hit the arrow at the bottom right.
1. Wait until it is found and select _done_ in the top right and you're done.
> If a series is not found, or is wrong you can manually search it by pressing Overflow -> **Search manually**
==TachiyomiSY
### Migrating from Library
1. Tap into **Library**.
1. Tap **Overflow** -> **Source migration**.
1. Select the **Source** you'd like to migrate _from_ and select **All**.
1. Select the **Sources** that you'd like to migrate _to_ and search by and tap the arrow labeled Migrate at the bottom right.
1. Choose which data you want to transfer over.
1. Wait until all your **Series** is found and hit the done at the top and you're done.
> If a series is not found, or is wrong you can manually search it by pressing **Overflow** -> **Search manually**.
### Migrating from Source {#migrating-from-source-sy}
1. Tap into Browse on the bottom navbar.
1. Press the Migrate tab at the top next to Extensions.
1. Select the **Source** that you'd like to migrate _from_.
1. Select the **Source** you'd like to migrate _from_ and select **All**.
1. Select the **Sources** that you'd like to migrate _to_ and search by and tap the arrow labeled Migrate at the bottom right.
1. Choose which data you want to transfer over.
1. Wait until all your **Series** is found and hit the done at the top and you're done.
> If a series is not found, or is wrong you can manually search it by pressing **Overflow** -> **Search manually**.
==TachiyomiAZ
### Instructions
1. Tap into **Library**.
1. Tap **Overflow** -> **Source migration**.
1. Select the **Source** you'd like to migrate _from_ and select **All**.
1. Select the **Sources** that you'd like to migrate _to_ and search by and tap the arrow at the bottom right.
1. Choose which data you want to transfer over.
1. Wait until all your **Series** is found and hit the done at the top and you're done.
> If a series is not found, or is wrong you can manually search it by pressing **Overflow** -> **Search manually**.
::::

View File

@ -0,0 +1,73 @@
---
title: Tracking
titleTemplate: Guides
description: Tracking helps track your library with different online services.
---
# Tracking
Tracking helps you automatically send read chapters to supported trackers, so you can keep track of what and when you read it online.
## Services
[MyAnimeList](https://myanimelist.net/), [AniList](https://anilist.co/), [MangaUpdates](https://www.mangaupdates.com/), [Shikimori](https://shikimori.one/), [Bangumi](https://bangumi.tv/), and [Kitsu](https://kitsu.io/).
- Tracking is manual for each entry.
- Reading the last page of a chapter marks it as read.
- Set chapters by tapping or dragging the ticker.
- Offline progress syncs when online.
- Start date is auto-set.
- Completion status auto-updates.
- Tracking is one-way: **Tachiyomi -> Tracker**.
## Enhanced services
::::tabs
==Komga
- No separate login required.
- Automatic tracking.
- Works with **Komga** sources only.
- Two-way sync for local chapters.
- Manually read chapter syncs with delay.
- Auto-track on library add: <nav to="tracking">.
> Learn how to set it up on the [Komga](https://komga.org/) website.
==Kavita
TBA
> Learn how to set it up on the [Kavita](https://www.kavitareader.com/) website.
==Suwayomi
TBA
> Learn how to set it up on the [Suwayomi](https://suwayomi.org/) website.
::::
## General questions
### How do I login into trackers?
1. Go to <nav to="tracking">.
1. Tap the desired tracker to begin login.
### How do I set up tracking for each series?
1. Open the series.
1. Tap **Tracking**.
1. Tap **Add tracking** for the desired service.
::: tip
You can also change the search query if there is no match.
:::
### How do I log in with Kitsu?
To log in with Kitsu, you need to use your email address as your username.
### Can't find a series on MyAnimeList?
If you cannot find a series by name, you can look it up on MyAnimeList and then search for it in **Tachiyomi** using the following format: `id:<id from series URL>`.
You can also search for a series on your MAL profile list by searching in the following format: `my:<series name>`.
::: warning For your information
Related GitHub issue: [#65](https://github.com/tachiyomiorg/tachiyomi/issues/65)
:::
### Finding tracked/intracked series in your library
Go to <nav to="main_library">, then **Filter (top right) -> Filter tab** then **Toggle Tracked**.
If you are logged into more than one tracker, toggle the tracker you want to include or exclude.

View File

@ -0,0 +1,110 @@
---
title: Common issues
titleTemplate: Troubleshooting - Guides
description: Facing issues with a source or the app? Here's how to tackle common challenges.
---
# Common issues
Facing issues with a source or the app?
Here's how to tackle common challenges.
## Basic issues
### `Cannot Access SD Card`
* Temporarily switch download location from SD card, then revert and restart the app.
* Long filenames trigger this. Android file manager doesn't support **>255** characters.
* If known, shorten the file/folder name via computer when SD card is connected.
* Else, delete **Tachiyomi** downloads folder on SD card.
### Storage issues with Android 11+
See [this](/docs/faq/android-11+) section of the FAQ to learn how Scoped Storage affects **Tachiyomi** in **Android 11+** and how to fix it.
### Slow loading
Sources being slow could stem from site slowness, your internet, or source-imposed rate limits/IP bans.
### Reading is laggy
* Caused by oversized images in chapters.
* For **32-bit color** users, try disabling in <nav to="reader">.
* Free up RAM.
* Use sources with smaller images.
### App not installed
Refer to "[Unable to install the app or extensions](/docs/guides/troubleshooting/#app-or-extension-installation-issues)" section.
## Advanced errors
### `Java.lang Exception: Failed to bypass Cloudflare`
This error indicates the selected source is protected by **Cloudflare**.
Consult the [Cloudflare guide](/docs/guides/troubleshooting/#cloudflare) for solutions.
If issues persist, the source might have high **Cloudflare** protection.
### `Unable to resolve host` / `Connection failed` / `Failed to connect to` / `timeout` / `connection reset`
These errors indicate connection issues. Possible causes include:
* Weak internet connection.
* App lacks internet access.
* Your ISP has blocked the site.
* The site is down.
Try these solutions:
* Enable **DNS over HTTPS** under <nav to="advanced">.
* Change network (Wi-Fi, mobile data, VPN).
* Reboot router.
### `java.security.cert.CertPathValidatorException` / `Chain validation failed`
Validation issue with source's certificate.
Try these solutions:
* Check expired certificate, use SSL checker.
* Set correct device date and time.
* In <nav to="advanced">, try **Clear cache** and **Clear cookies**.
* Change network (Wi-Fi, mobile data, VPN).
* Reboot device.
### `Attempt to invoke virtual method 'com.hippo.unifile...'`
Storage-related error causes:
* Storage full, check device/SD Card.
* Grant **Tachiyomi** SD card access in Android settings.
* Download folder access issues, validate paths.
* Corrupted or inaccessible writing drive, verify using a file manager.
## HTTP errors
Encountering HTTP errors? Here's what they mean and how to address them.
### `HTTP Error: 403` - Forbidden
Possible reasons for this error:
* The selected source has Cloudflare protection. Check the [Cloudflare guide](/docs/guides/troubleshooting/#cloudflare) for solutions.
* The source might be down, removed the series, or banned your IP.
> Open WebView to confirm.
### `HTTP Error: 404` - Not Found
This error likely indicates a down source or removed series.
* Use **WebView** to verify.
> Consider switching to a different source for the series.
### `HTTP Error: 429` - Too Many Requests
This error suggests the source temporarily banned your IP due to fast downloads/reads.
[Report the issue](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose) to add rate limits and prevent future IP bans.
### `HTTP Error: 5xx`
Errors like `500`, `502`, etc., indicate server-side issues on the source's end.
[Check the source in WebView](/docs/guides/troubleshooting/#accessing-websites-via-webview) to confirm if it's down.
### `HTTP Error: 1006`
This error means a temporary IP ban by the source.
### `HTTP Error: 1020`
This error points to violating a firewall rule set by the site owner.
The owner might raise Cloudflare protection or block IPs from outside their country.
::: warning
For unlisted errors or if instructions don't help, refer to [Diagnosis](/docs/guides/troubleshooting/diagnosis).
:::

View File

@ -0,0 +1,65 @@
---
title: Diagnosis
titleTemplate: Troubleshooting - Guides
description: Facing issues with a source or the app? Follow these steps to troubleshoot and find solutions.
---
# Diagnosis
Facing issues with a source or the app?
Follow these steps to troubleshoot and find solutions.
## Primary diagnosis
1. **Update Extensions**: Check <nav to="extensions"> for updates, no pending updates should be present.
1. **Update App**: Go to <nav to="about"> and tap **Check for updates**.
1. **Manual Series Refresh**: Drag down to manually refresh problematic series.
1. **Test Other Series**: Try different series from the same source.
1. **Update WebView**: Ensure your WebView is current.
1. **Public WebView**: Attempt opening series in public WebView. Wait for CAPTCHA or Cloudflare protection if needed.
1. **Change Connection**: Switch networks (Wi-Fi, mobile data, VPN) and confirm IP change.
1. **Collaborative Check**: Get others to replicate the error.
1. **Source Status**: Verify the source's status in a browser.
1. **Retry Button**: Look for a retry button on the series page.
1. **Advanced Settings**: Under <nav to="advanced">, try these options:
- Clear Cache
- Clear Cookies
- Clear Database
- DNS over HTTPS
1. **Download Issues**: Delete the queue and retry downloads.
1. **Restart Tachiyomi**: Force close and reopen the app.
If any of these solutions help, go to [Personalized Issue](#personalized-issue).
If it is not just you, go to [Widespread Issue](#widespread-issue).
If none of these solutions help, try asking in our [Discord server](https://discord.gg/tachiyomi).
Check [#status-updates](https://discord.com/channels/349436576037732353/738862409284059239) first to see if your issue is known.
State your app version and the source, series, and chapter with the problem if it is not listed.
::: tip An extension update may fix your issue
Wait or check for an extension update if you have not already.
There are no ETAs for updates.
:::
## Personalized issue
If you're the only one facing a problem, you might be encountering [Cloudflare](/docs/guides/troubleshooting/#cloudflare) protection, an IP ban, or other countermeasures set by website owners against programs like **Tachiyomi**.
**To minimize future issues:**
- Avoid using downloads with the source.
- Reduce the number of series in your library from that source.
::: warning
These are general guidelines as each site has its specific undisclosed limits and triggers.
:::
## Widespread issue
When everyone experiences a problem, it could be with the extension or app:
1. Check open issues [for the app](https://github.com/tachiyomiorg/tachiyomi/issues) and/or [**extensions**](https://github.com/tachiyomiorg/tachiyomi-extensions/issues).
1. Check closed issues ([app](https://github.com/tachiyomiorg/tachiyomi/issues?q=is%3Aissue+is%3Aclosed)/[extensions](https://github.com/tachiyomiorg/tachiyomi-extensions/issues?q=is%3Aissue+is%3Aclosed)) in case it's resolved but not yet released.
1. If not found, create a new issue.
::: warning
If the site itself is problematic, patience is the only solution until it becomes functional again.
:::

View File

@ -0,0 +1,123 @@
---
title: Troubleshooting
titleTemplate: Guides
description: Facing source or app issues? Here's how to troubleshoot.
---
# Troubleshooting
Facing source or app issues?
Here's how to troubleshoot.
## WebView
### Accessing websites via WebView
::: tabs
== From Browse
1. Open **Browse** from the bottom navbar.
1. Tap the desired source.
1. Tap the **WebView** icon in the top toolbar.
1. Complete a **CAPTCHA** if one is shown.
1. Close by tapping `X` at the top-left.
== From a Series
1. Open a series.
1. Tap the **WebView** icon button.
1. Complete a **CAPTCHA** if one is shown.
1. Close by tapping `X` at the top-left.
:::
Repeat if needed.
Alternatively, try opening the website in your browser using the **Overflow** icon in the WebView screen and solve any **CAPTCHA** there.
![Open WebView](/docs/guides/troubleshooting/open-webview.dark.webp =1079x520)
### Clearing cookies and WebView data
This resets your WebView to a clean state, including any login states.
1. Navigate to <nav to="advanced">.
1. Tap **Clear cookies**.
1. Tap **Clear WebView data**.
### WebView update
To update WebView, you need to find what WebView implementation is used on your device.
Typical default implementation depends on the Android version as follows:
::: tabs
== Android 10 and above
[Android System WebView](https://play.google.com/store/apps/details?id=com.google.android.webview)
== Android 7 - 9
[Google Chrome](https://play.google.com/store/apps/details?id=com.android.chrome)
== Android 6 and below
[Android System WebView](https://play.google.com/store/apps/details?id=com.google.android.webview)
:::
::: tip **Android 7** and above
Newer Android users can check/change WebView in [Developer Options](https://developer.android.com/studio/debug/dev-options).
:::
::: warning Caution with Non-Standard WebView
Using non-standard WebView (like Firefox) might cause **Tachiyomi** to malfunction or crash.
It's best to use the standard [Android System WebView](https://play.google.com/store/apps/details?id=com.google.android.webview) or [Google Chrome](https://play.google.com/store/apps/details?id=com.android.chrome).
:::
## Cloudflare
**Cloudflare**, an anti-bot mechanism, is used by some sources.
Some sources intentionally have higher **Cloudflare** protection to deter apps like **Tachiyomi**.
### Dealing with Cloudflare looping
Certain sources may employ more advanced **Cloudflare** protection, leading to WebView continuously reloading when bypassing using the above solution.
If this occurs, try [Accessing the Website via WebView](#accessing-websites-via-webview).
### Changing your user agent
A user agent string shares requester information with websites, potentially affecting **Cloudflare**'s bot detection.
While some sources have specific user agent strings, most rely on the app's default.
::: info Changing your user agent
1. Navigate to <nav to="advanced">.
1. Modify **Default user agent string** to another value. You may need to experiment to find one that works.
> [Here's a reference](https://www.whatismybrowser.com/guides/the-latest-user-agent/).
1. Restart the app and retry source access.
:::
::: tip Did these methods not work?
Wait for the source to lower its protection or switch to different sources.
:::
## General
### Obtaining crash/error logs
For crash investigations, navigate to <nav to="advanced"> and tap **Dump crash logs**.
![Dump crashlogs](/docs/guides/troubleshooting/dump-crash-logs.dark.webp =512x386)
### Obtaining more logs
To diagnose abnormal app behavior, record device logs using a [Logcat Reader](https://play.google.com/store/apps/details?id=com.dp.logcatapp).
### App or extension installation issues
Encountering problems while trying to install app or extension `.apk` files?
Follow these steps:
1. Install [Split APK Installer](https://play.google.com/store/apps/details?id=com.aefyr.sai) from the Google Play Store.
1. Try installing your `.apk` from Split APK Installer.
**Split APK Installer** helps show better error messages or may even successfully install your `.apk` without issue.
Common errors include:
::: details `INSTALL_FAILED_UPDATE_INCOMPATIBLE: Package eu.kanade.tachiyomi signatures do not match the previously installed version; ignoring!`
Seeing this error while installing the `.apk` over an existing **F-Droid** build indicates a mismatch in signatures.
Data backup, uninstall, and fresh installation are required.
:::
::: details `DISPLAY_NAME column is null`
Seeing this error points to a corrupted `.apk`.
Try redownloading the `.apk`.
:::
::: details `INSTALL_FAILED_NO_MATCHING_ABIS`
Seeing this error suggests the `.apk` is incompatible with your CPU architecture.
Obtain the appropriate version or a universal `.apk` (i.e. the option with largest file size on GitHub).
:::

View File

@ -0,0 +1,24 @@
---
title: Download
description: Download page that allows users to access and install the latest version of the app.
lastUpdated: false
editLink: false
prev: false
next: false
---
<script setup>
import DownloadButtons from "@theme/components/DownloadButtons.vue";
import ReleaseDate from "@theme/components/ReleaseDate.vue";
import Changelog from "@theme/components/Changelog.vue";
</script>
# Download
The latest stable version of **Tachiyomi** was released **<ReleaseDate type="stable" />** and the latest preview version was released **<ReleaseDate type="preview" />**.
Preview releases are intended for testing upcoming changes and may not be a stable experience.
<DownloadButtons />
<Changelog type="stable" />

View File

@ -0,0 +1,18 @@
---
title: Extensions
description: Browse and download sources for Tachiyomi.
lastUpdated: false
editLink: false
prev: false
next: false
---
# Extensions
Extensions can also be installed directly from the app.
<ExtensionsWrapper/>
<script setup>
import ExtensionsWrapper from '@theme/components/Extensions/ExtensionsWrapper.vue'
</script>

View File

@ -0,0 +1,64 @@
---
title: Neko
titleTemplate: false
description: Features specific to MangaDex with the featureset of TachiyomiJ2K
layout: home
pageClass: page-neko
hero:
name: Neko
text: For MangaDex
tagline: Features specific to MangaDex with the featureset of TachiyomiJ2K
image: /forks/logo-neko.webp
actions:
- theme: brand
text: Download
link: https://github.com/CarlosEsco/Neko/releases/latest
- theme: alt
text: GitHub
link: https://github.com/CarlosEsco/Neko
customMetaTitle: Neko
features:
- title: MangaDex
details: Native login with support for MDList as a tracker, and syncing the MangaDex follows list.
icon: <img src="/img/logo-mangadex.svg" alt="MangaDex Logo" height="32" width="32">
- title: Similiar Series
details: Recommendation system
icon: 📚
- title: Merging missing chapters
details: If anything is missing on MangaDex you can attempt to merge them with another source.
icon: ❔
theme: "#FD6684"
image: /forks/logo-neko.webp
imageSize: small
---
<br><VPTeamMembers size="small" :members="members" />
<script setup>
import "@theme/styles/forks/neko.styl"
import { VPTeamMembers } from "vitepress/theme"
const members = [
{
avatar: "https://www.github.com/CarlosEsco.png",
name: "CarlosEsco",
title: "Creator",
links: [
{ icon: "github", link: "https://github.com/CarlosEsco" }
]
},
{
avatar: "https://www.github.com/Jays2Kings.png",
name: "Jays2Kings",
title: "Fork base",
links: [
{ icon: "github", link: "https://github.com/Jays2Kings" }
]
}
]
</script>

View File

@ -0,0 +1,82 @@
---
title: TachiyomiAZ
titleTemplate: false
description: Hentai-focused with legacy features
layout: home
pageClass: page-tachiyomi-az
hero:
name: TachiyomiAZ
text: Hentai-focused
tagline: Recommendation system, a hamburger menu, and loads of hentai
image: /forks/logo-az.webp
actions:
- theme: brand
text: Download
link: https://github.com/az4521/TachiyomiAZ/releases/latest
- theme: alt
text: GitHub
link: https://github.com/az4521/tachiyomiAZ
customMetaTitle: TachiyomiAZ
features:
- title: Hentai-focused
details: Adds several features to enhance your Hentai experience.
icon: 🔞
- title: Series recommendations
details: Get recommendations from MyAnimeList and AniDB.
icon: 📚
- title: Legacy design
details: Keeps the old design of Tachiyomi with hamburger menu.
icon: 👵
theme: "#FFCC4D"
image: /forks/logo-az.webp
imageSize: small
---
<br><VPTeamMembers size="small" :members="members" />
<script setup>
import "@theme/styles/forks/tachiyomi-az.styl"
import { VPTeamMembers } from "vitepress/theme"
const members = [
{
avatar: "https://www.github.com/az4521.png",
name: "az4521",
title: "Creator",
links: [
{ icon: "github", link: "https://github.com/az4521" }
]
},
{
avatar: "https://www.github.com/jobobby04.png",
name: "jobobby04",
title: "Former Maintainer",
links: [
{ icon: "github", link: "https://github.com/jobobby04" }
]
},
{
avatar: "https://www.github.com/NerdNumber9.png",
name: "NerdNumber9",
title: "Original EH Fork",
links: [
{ icon: "github", link: "https://github.com/NerdNumber9" }
]
}
]
</script>
<br>
<div class="azContainer">
<div class="azMarquee">
<div class="azWiggleText">
<span class="azText"><i>"The BEST fork"</i> - az4521</span>
</div>
</div>
</div>

View File

@ -0,0 +1,56 @@
---
title: TachiyomiJ2K
titleTemplate: false
description: New design approach along with several other enhancements
layout: home
pageClass: page-tachiyomi-j2k
hero:
name: TachiyomiJ2K
text: Redesigned
tagline: New design approach along with several other enhancements
image: /forks/logo-j2k.webp
actions:
- theme: brand
text: Download
link: https://github.com/Jays2Kings/tachiyomiJ2K/releases/latest
- theme: alt
text: GitHub
link: https://github.com/Jays2Kings/tachiyomiJ2K
customMetaTitle: TachiyomiJ2K
features:
- title: Redesigned UI
details: Entirely different design from regular Tachiyomi, with exciting new features.
icon: 👑
- title: Double-page for Tablets
details: Combine 2 pages while reading into a single one for a better tablet experience.
icon: 📖
- title: Dynamic categories
details: Group your library automatically by the tags, tracking status, source, and more.
icon: 🔖
theme: "#0952AF"
image: /forks/logo-j2k.webp
imageSize: small
---
<br><VPTeamMembers size="small" :members="members" />
<script setup>
import "@theme/styles/forks/tachiyomi-j2k.styl"
import { VPTeamMembers } from "vitepress/theme"
const members = [
{
avatar: "https://www.github.com/Jays2Kings.png",
name: "Jays2Kings",
title: "Creator",
links: [
{ icon: "github", link: "https://github.com/Jays2Kings" }
]
}
]
</script>

View File

@ -0,0 +1,72 @@
---
title: TachiyomiSY
titleTemplate: false
description: Keeping up-to-date with Tachiyomi while also adding exclusive features
layout: home
pageClass: page-tachiyomi-sy
hero:
name: TachiyomiSY
text: Hentai-focused
tagline: Keeping up-to-date with Tachiyomi while also adding exclusive features
image: /forks/logo-sy.webp
actions:
- theme: brand
text: Download
link: https://github.com/jobobby04/TachiyomiSY/releases/latest
- theme: alt
text: GitHub
link: https://github.com/jobobby04/TachiyomiSY
customMetaTitle: TachiyomiSY
features:
- title: Hentai-focused
details: Adds several features to enhance your Hentai experience.
icon: 🔞
- title: Series recommendations
details: Get recommendations from MyAnimeList and AniDB.
icon: 📚
- title: Autoscroll
details: Allows you to read without any extra interaction.
icon: 📜
theme: "#CE2828"
image: /forks/logo-sy.webp
imageSize: small
---
<br><VPTeamMembers size="small" :members="members" />
<script setup>
import "@theme/styles/forks/tachiyomi-sy.styl"
import { VPTeamMembers } from "vitepress/theme"
const members = [
{
avatar: "https://www.github.com/jobobby04.png",
name: "jobobby04",
title: "Creator",
links: [
{ icon: "github", link: "https://github.com/jobobby04" }
]
},
{
avatar: "https://www.github.com/she11sh0cked.png",
name: "she11sh0cked",
title: "Extra tracking, filter, recommendations, and more",
links: [
{ icon: "github", link: "https://github.com/she11sh0cked" }
]
},
{
avatar: "https://www.github.com/az4521.png",
name: "az4521",
title: "Base recommendations and AZ",
links: [
{ icon: "github", link: "https://github.com/az4521" }
]
}
]
</script>

View File

@ -0,0 +1,37 @@
---
title: Endorsed Forks
description: Forks are alternative versions of Tachiyomi with exclusive features.
lastUpdated: false
editLink: false
prev: false
next: false
pageClass: forks
features:
- title: Neko
details: Features specific to MangaDex with the feature set of TachiyomiJ2K
icon: <img src="/forks/logo-neko.webp" alt="Neko Logo" height="32" width="32">
link: /forks/Neko/
- title: TachiyomiJ2K
details: New design approach along with several other enhancements
icon: <img src="/forks/logo-j2k.webp" alt="TachiyomiJ2K Logo" height="32" width="32">
link: /forks/TachiyomiJ2K/
- title: TachiyomiSY
details: Keeping up-to-date with Tachiyomi while also adding exclusive features
icon: <img src="/forks/logo-sy.webp" alt="TachiyomiSY Logo" height="32" width="32">
link: /forks/TachiyomiSY/
- title: TachiyomiAZ
details: Keeps the old design of Tachiyomi with hamburger menu.
icon: <img src="/forks/logo-az.webp" alt="TachiyomiAZ Logo" height="32" width="32">
link: /forks/TachiyomiAZ/
---
<script setup>
import { VPHomeFeatures } from "vitepress/theme"
</script>
# Endorsed Forks
Forks are alternative versions of Tachiyomi with exclusive features.
<VPHomeFeatures />

37
website/src/index.md Normal file
View File

@ -0,0 +1,37 @@
---
title: Home
layout: home
hero:
name: Tachiyomi
text: Full-featured reader
tagline: Discover and read manga, webtoons, comics, and more easier than ever on your Android device.
image:
alt: Tachiyomi Library Tab Dark
light: /home/phone.light.webp
dark: /home/phone.dark.webp
actions:
- theme: brand
text: Get Started
link: /docs/guides/getting-started
- theme: alt
text: Download
link: /download/
customMetaTitle: Tachiyomi
features:
- title: Tracking
details: Automatically keep track of your series with MyAnimeList, AniList, Kitsu, and more.
icon: <svg height="24" width="24" viewBox="0 -960 960 960" fill="var(--vp-c-green-2)" xmlns="http://www.w3.org/2000/svg"><path d="M160-160v-80h110l-16-14q-52-46-73-105t-21-119q0-111 66.5-197.5T400-790v84q-72 26-116 88.5T240-478q0 45 17 87.5t53 78.5l10 10v-98h80v240H160Zm400-10v-84q72-26 116-88.5T720-482q0-45-17-87.5T650-648l-10-10v98h-80v-240h240v80H690l16 14q49 49 71.5 106.5T800-482q0 111-66.5 197.5T560-170Z"/></svg>
link: /docs/guides/tracking
linkText: Setup tracking
- title: Extensions
details: Online and offline reading from over a thousand sources.
icon: <svg height="24" width="24" viewBox="0 -960 960 960" fill="var(--vp-c-yellow-2)" xmlns="http://www.w3.org/2000/svg"><path d="M352-120H200q-33 0-56.5-23.5T120-200v-152q48 0 84-30.5t36-77.5q0-47-36-77.5T120-568v-152q0-33 23.5-56.5T200-800h160q0-42 29-71t71-29q42 0 71 29t29 71h160q33 0 56.5 23.5T800-720v160q42 0 71 29t29 71q0 42-29 71t-71 29v160q0 33-23.5 56.5T720-120H568q0-50-31.5-85T460-240q-45 0-76.5 35T352-120Zm-152-80h85q24-66 77-93t98-27q45 0 98 27t77 93h85v-240h80q8 0 14-6t6-14q0-8-6-14t-14-6h-80v-240H480v-80q0-8-6-14t-14-6q-8 0-14 6t-6 14v80H200v88q54 20 87 67t33 105q0 57-33 104t-87 68v88Zm310-310Z"/></svg>
link: /extensions/
linkText: Find extensions
- title: Customization
details: Make it yours with multiple reading modes, custom color filters, and many other settings.
icon: <svg height="24" width="24" viewBox="0 -960 960 960" fill="var(--vp-c-indigo-2)" xmlns="http://www.w3.org/2000/svg"><path d="M440-120v-240h80v80h320v80H520v80h-80Zm-320-80v-80h240v80H120Zm160-160v-80H120v-80h160v-80h80v240h-80Zm160-80v-80h400v80H440Zm160-160v-240h80v80h160v80H680v80h-80Zm-480-80v-80h400v80H120Z"/></svg>
---

View File

@ -0,0 +1,26 @@
---
type: article
title: Updated website
description: We've got a fresh new website we hope will be even easier to use
date: 2023-09-09
---
# Updated website
We've launched a fully rewritten website with improved usability for both users
and contributors. Some improvements include the [download](/download/) page,
which now doesn't require any new additional requests, and a new [changelogs](/changelogs/)
page to easily see historical app release notes.
This page is part of the new [news](/news/) section, which will be used in occasions
like this to keep you informed of any interesting updates.
Under the hood, we're now using [VitePress](https://vitepress.dev/) with
[Vue.js](https://vuejs.org/) v3, replacing our previous usage of
[VuePress](https://vuepress.vuejs.org/).
As we're still in the process of rewritting and updating part of the
documentation and guides, there may still be some errors or outdated content.
If you find any, please report by opening an issue in the [repository for this website](https://github.com/tachiyomiorg/website/issues/new/choose) on GitHub.
*The Tachiyomi Team*.

21
website/src/news/index.md Normal file
View File

@ -0,0 +1,21 @@
---
title: News
description: Collection of news and announcements about Tachiyomi.
lastUpdated: false
editLink: false
prev: false
next: false
---
<script setup>
import News from "@theme/components/News.vue";
import RssLink from "@theme/components/RssLink.vue";
</script>
# News
Collection of news and announcements about Tachiyomi.
Also available as <RssLink />.
<News />

View File

@ -0,0 +1,60 @@
---
title: Privacy policy
description: Privacy Policy that explains how Tachiyomi collects, uses, and protects users' personal information.
lastUpdated: false
editLink: false
---
# Privacy policy
Tachiyomi is an Open Source app.
This SERVICE is provided at no cost and is intended for use as is.
This page details our policies with the collection, use, and disclosure of Personal Information if anyone decided to use our Service.
If you choose to use our Service, then you agree to the collection and use of information in relation to this policy.
The Personal Information that we collect is used for providing and improving the Service.
We will not use or share your information with anyone except as described in this Privacy Policy.
## Information Collection and Use
For a better experience, while using our Service, we may require you to provide us with certain personally identifiable information.
The information that we request will be retained by us and used as described in this privacy policy.
Links to the privacy policy of third-party service providers used by the app:
* [Google Analytics for Firebase](https://firebase.google.com/policies/analytics)
* [Sentry](https://sentry.io/privacy/)
### Log Data
In a case of an error in the app, the Service automatically collects data and information called Log Data.
This Log Data may include information including your device name, operating system version, the configuration of the app when utilizing our Service, the time and date of your use of the Service, and other statistics.
This can be disabled within the app.
### Analytics Data
The Service is integrated with [Firebase](https://firebase.google.com/) to collect anonymized analytics data about Service usage.
For more information, you can refer to [How Google uses data when you use our partners' sites or apps](https://google.com/policies/privacy/partners/).
## External Links
This Service contains links to other sites.
If you click on a third-party link, you will be directed to that site.
Note that these external sites are not operated by us.
Therefore, we strongly advise you to review the Privacy Policy of these websites.
We have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services.
This includes the use of external tracking services (e.g. MyAnimeList).
## Changes to This Privacy Policy
We may periodically update our Privacy Policy.
Thus, you are advised to review this page periodically for any changes.
The current iteration of this policy is effective as of December 15, 2021.
## Contact Us
If you have any questions or suggestions about this Privacy Policy, do not hesitate to reach out to us on [our Discord server](https://discord.gg/tachiyomi).

View File

@ -0,0 +1,8 @@
# Cache headers for media files for the Google recommended of 6 months minimum.
*.webp
*.webm
*.png
*.jpg
*.svg
*.ico
Cache-Control: public, max-age=15552000

View File

@ -0,0 +1,12 @@
# Requested URL -> Redirected URL -> HTTP Code
/help/ / 301
/help/guides/getting-started/ /docs/guides/getting-started 301
/help/guides/troubleshooting/ /docs/guides/troubleshooting/ 301
/help/guides/source-migration/ /docs/guides/source-migration 301
/help/guides/backups/ /docs/guides/backups 301
/help/guides/tracking/ /docs/guides/tracking 301
/help/guides/categories/ /docs/guides/categories 301
/help/guides/local-manga/ /docs/guides/local-source/ 301
/help/guides/reader-settings/ /docs/guides/reader-settings 301
/help/contribution/ /docs/contribute 301
/help/faq/ /docs/faq/general 301

Some files were not shown because too many files have changed in this diff Show More