├── .github ├── ISSUE_TEMPLATE │ └── Bug.yml ├── assets │ └── readme_logo.png ├── readme │ ├── README_en.md │ ├── README_ru.md │ └── README_uk.md └── workflows │ └── python-app.yml ├── .gitignore ├── LICENSE ├── NlightNuitka.bat ├── NlightPyinstaller.bat ├── README.md ├── data ├── i18n │ ├── en_US.qm │ ├── en_US.ts │ ├── ru_RU.qm │ ├── ru_RU.ts │ ├── uk_UA.qm │ └── uk_UA.ts ├── icons │ ├── buttons │ │ ├── svg_24dp_black │ │ │ └── actions │ │ │ │ └── shikimori.svg │ │ └── svg_24dp_white │ │ │ └── actions │ │ │ └── shikimori.svg │ ├── icon.png │ └── lang │ │ ├── gb.svg │ │ ├── jp.svg │ │ ├── ru.svg │ │ └── ua.svg ├── resource.py ├── resource.qrc └── ui │ ├── containers │ ├── image_area.py │ ├── image_area.ui │ ├── text_area.py │ └── text_area.ui │ ├── manga_item.py │ ├── manga_item.ui │ ├── widgets │ ├── facial.py │ ├── facial.ui │ ├── history.py │ ├── history.ui │ ├── info.py │ ├── info.ui │ ├── library.py │ ├── library.ui │ ├── reader.py │ ├── reader.ui │ ├── shikimori.py │ └── shikimori.ui │ └── windows │ ├── reader.py │ └── reader.ui ├── main.py ├── make_version_file.py ├── makepkg.sh ├── nlightreader ├── __init__.py ├── consts │ ├── __init__.py │ ├── app.py │ ├── colors.py │ ├── enums.py │ ├── files │ │ ├── __init__.py │ │ └── files.py │ ├── items │ │ ├── __init__.py │ │ ├── anilib_items.py │ │ ├── desu_items.py │ │ ├── lib_base_items.py │ │ ├── mangadex_items.py │ │ ├── mangalib_items.py │ │ ├── parser_items.py │ │ ├── preset_items.py │ │ ├── ranobehub_items.py │ │ ├── ranobelib_items.py │ │ ├── remanga_items.py │ │ ├── rulate_items.py │ │ └── shikimori_items.py │ ├── paths │ │ ├── __init__.py │ │ └── paths.py │ └── urls.py ├── controlers │ ├── __init__.py │ └── filters_controller.py ├── exceptions │ ├── __init__.py │ └── parser_content_exc.py ├── items │ ├── RequestForm.py │ ├── __init__.py │ └── other_items.py ├── models │ ├── __init__.py │ ├── base_model.py │ ├── chapter_model.py │ ├── character_model.py │ ├── image_model.py │ ├── manga_model.py │ └── sort_models.py ├── parsers │ ├── __init__.py │ ├── catalog.py │ ├── catalogs_base.py │ ├── combined │ │ ├── lib │ │ │ ├── __init__.py │ │ │ ├── lib_anilib.py │ │ │ ├── lib_base.py │ │ │ ├── lib_mangalib.py │ │ │ └── lib_ranobelib.py │ │ └── shikimori │ │ │ ├── __init__.py │ │ │ ├── shikimori_anime.py │ │ │ ├── shikimori_base.py │ │ │ ├── shikimori_lib.py │ │ │ ├── shikimori_manga.py │ │ │ └── shikimori_ranobe.py │ ├── hentai_manga │ │ ├── __init__.py │ │ ├── allhentai_hmanga.py │ │ └── nhentai_hmanga.py │ ├── local_library.py │ ├── manga │ │ ├── Lib.py │ │ ├── __init__.py │ │ ├── desu_manga.py │ │ ├── mangadex_manga.py │ │ └── remanga_manga.py │ ├── ranobe │ │ ├── __init__.py │ │ ├── ranobehub_ranobe.py │ │ └── rulate_ranobe.py │ └── service │ │ └── kodik.py ├── utils │ ├── catalog_manager.py │ ├── config.py │ ├── database.py │ ├── decorators.py │ ├── file_manager.py │ ├── html_video.py │ ├── kodik_server.py │ ├── text_formatter.py │ ├── threads.py │ ├── token.py │ ├── translator.py │ └── utils.py ├── widgets │ ├── containers │ │ ├── __init__.py │ │ ├── content_container.py │ │ ├── image_area.py │ │ ├── manga_area.py │ │ └── text_area.py │ ├── contexts │ │ ├── __init__.py │ │ ├── history_note_menu.py │ │ ├── library_manga_menu.py │ │ └── read_mark_menu.py │ ├── dialogs │ │ ├── __init__.py │ │ ├── auth_dialog.py │ │ ├── character_info_dialog.py │ │ ├── genres_dialog.py │ │ └── rate_dialog.py │ ├── items │ │ ├── __init__.py │ │ ├── manga_item.py │ │ └── title_tree_item.py │ └── pages │ │ ├── __init__.py │ │ ├── base_page.py │ │ ├── external_library_page.py │ │ ├── history_page.py │ │ ├── info_page.py │ │ ├── library_page.py │ │ ├── main_page.py │ │ └── settings_intefrace.py └── windows │ ├── __init__.py │ ├── parent_window.py │ └── reader_window.py ├── pkg_res ├── Nlight.desktop ├── Nlight.ico └── Nlight.svg ├── pyproject.toml ├── requirements.txt └── requirements ├── dev.txt ├── prod.txt └── test.txt /.github/ISSUE_TEMPLATE/Bug.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to fill out this bug report! 11 | - type: textarea 12 | id: what-happened 13 | attributes: 14 | label: What happened? 15 | description: Also tell us, what did you expect to happen? 16 | placeholder: Tell us what you see! 17 | validations: 18 | required: true 19 | - type: input 20 | id: OS 21 | attributes: 22 | label: Operation System 23 | description: Which operation system are you using? 24 | placeholder: ex. Windows11 23H2 25 | validations: 26 | required: true 27 | - type: input 28 | id: Nlight 29 | attributes: 30 | label: Nlight Version 31 | description: Which version of Nlight are you using? 32 | placeholder: ex. 1.10.0.5 33 | validations: 34 | required: true 35 | - type: textarea 36 | id: reproduce 37 | attributes: 38 | label: How to Reproduce? 39 | description: A clear and concise description of what the bug is. 40 | placeholder: Steps to reproduce the behavior. You can use gif to demonstrate the problem. 41 | validations: 42 | required: true 43 | -------------------------------------------------------------------------------- /.github/assets/readme_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brandonzorn/Nlight/8b7b8924c10b49fcca9281a0b2596ef3e485e27d/.github/assets/readme_logo.png -------------------------------------------------------------------------------- /.github/readme/README_en.md: -------------------------------------------------------------------------------- 1 | # Nlight 2 | 3 | 🌐 [Русский](README_ru.md) • [Українська](README_uk.md) 4 | 5 | ![Nlight Logo](../assets/readme_logo.png) 6 | 7 | **Nlight** is an application for reading manga and ranobe, with support for Shikimori and catalogs in Russian and English. 8 | 9 | --- 10 | 11 | ## 🚀 Features 12 | 13 | * 🔍 Browse, search, and read content 14 | * 📚 Filter by genre, type, and sort order 15 | * 🧩 Shikimori integration 16 | * 🌗 Light and dark theme support 17 | * 🌍 Available in 🇷🇺 Russian, 🇺🇦 Ukrainian, and 🇬🇧 English 18 | 19 | --- 20 | 21 | ## 📖 Supported Catalogs 22 | 23 | ### 📺 Anime 24 | 25 | * Shikimori (🇷🇺) 26 | * AniLib (🇷🇺) 27 | 28 | ### 📚 Manga 29 | 30 | * Desu (🇷🇺) 31 | * Shikimori (🇷🇺) 32 | * MangaDex (🇷🇺, 🇬🇧) 33 | * Remanga (🇷🇺) 34 | * MangaLib (🇷🇺) 35 | 36 | ### 📘 Ranobe 37 | 38 | * Rulate (🇷🇺) 39 | * Erolate (🇷🇺) 40 | * Ranobehub (🇷🇺) 41 | * RanobeLib (🇷🇺) 42 | 43 | ### 🔞 Hentai Manga 44 | 45 | * AllHentai (🇷🇺) 46 | * NHentai (🇬🇧) 47 | 48 | --- 49 | 50 | ## 🖼️ Screenshots 51 | 52 | | ![Screenshot 1](https://github.com/brandonzorn/Nlight/assets/68778953/f714165d-5df6-4b38-89a6-02d940172469) | ![Screenshot 2](https://github.com/brandonzorn/Nlight/assets/68778953/1da43e9a-52af-402d-8f30-189f31a31ba2) | ![Screenshot 3](https://github.com/brandonzorn/Nlight/assets/68778953/168f00a3-4174-41ba-8773-4548ef7ced9b) | 53 | | :---------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------: | 54 | 55 | --- 56 | 57 | ## 🧩 Contribution & Support 58 | 59 | You are welcome to: 60 | 61 | * Suggest improvements 62 | * Report issues or bugs 63 | * Help with translations 64 | 65 | --- 66 | 67 | ## 📄 License 68 | 69 | This project is licensed under the [MIT License](../../LICENSE). 70 | 71 | --- -------------------------------------------------------------------------------- /.github/readme/README_ru.md: -------------------------------------------------------------------------------- 1 | # Nlight 2 | 3 | 🌐 [Українська](README_uk.md) • [English](README_en.md) 4 | 5 | ![Nlight Logo](../assets/readme_logo.png) 6 | 7 | **Nlight** — это приложение для чтения манги и ранобэ, с поддержкой Shikimori, каталогов на русском и английском языках. 8 | 9 | --- 10 | 11 | ## 🚀 Возможности 12 | 13 | * 🔍 Просмотр, поиск и чтение контента 14 | * 📚 Фильтрация по жанрам, типу и сортировке 15 | * 🧩 Интеграция с Shikimori 16 | * 🌗 Светлая и тёмная темы оформления 17 | * 🌍 Поддержка 🇷🇺 Русского, 🇺🇦 Украинского и 🇬🇧 Английского языков 18 | 19 | --- 20 | 21 | ## 📖 Поддерживаемые каталоги 22 | 23 | ### 📺 Аниме 24 | 25 | * Shikimori (🇷🇺) 26 | * AniLib (🇷🇺) 27 | 28 | ### 📚 Манга 29 | 30 | * Desu (🇷🇺) 31 | * Shikimori (🇷🇺) 32 | * MangaDex (🇷🇺, 🇬🇧) 33 | * Remanga (🇷🇺) 34 | * MangaLib (🇷🇺) 35 | 36 | ### 📘 Ранобэ 37 | 38 | * Rulate (🇷🇺) 39 | * Erolate (🇷🇺) 40 | * Ranobehub (🇷🇺) 41 | * RanobeLib (🇷🇺) 42 | 43 | ### 🔞 Хентай-манга 44 | 45 | * AllHentai (🇷🇺) 46 | * NHentai (🇬🇧) 47 | 48 | --- 49 | 50 | ## 🖼️ Скриншоты 51 | 52 | | ![Screenshot 1](https://github.com/brandonzorn/Nlight/assets/68778953/f714165d-5df6-4b38-89a6-02d940172469) | ![Screenshot 2](https://github.com/brandonzorn/Nlight/assets/68778953/1da43e9a-52af-402d-8f30-189f31a31ba2) | ![Screenshot 3](https://github.com/brandonzorn/Nlight/assets/68778953/168f00a3-4174-41ba-8773-4548ef7ced9b) | 53 | | :---------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------: | 54 | 55 | --- 56 | 57 | ## 🧩 Поддержка и вклад 58 | 59 | Вы можете: 60 | 61 | * Предложить улучшения 62 | * Сообщить об ошибках 63 | * Помочь с переводами 64 | 65 | --- 66 | 67 | ## 📄 Лицензия 68 | 69 | Этот проект распространяется под лицензией [MIT License](../../LICENSE). 70 | 71 | --- -------------------------------------------------------------------------------- /.github/readme/README_uk.md: -------------------------------------------------------------------------------- 1 | # Nlight 2 | 3 | 🌐 [Русский](README_ru.md) • [English](README_en.md) 4 | 5 | ![Nlight Logo](../assets/readme_logo.png) 6 | 7 | **Nlight** — це додаток для читання манґи та ранобе з підтримкою Shikimori і каталогів українською, російською та англійською мовами. 8 | 9 | --- 10 | 11 | ## 🚀 Можливості 12 | 13 | * 🔍 Перегляд, пошук і читання контенту 14 | * 📚 Фільтрація за жанрами, типом і порядком сортування 15 | * 🧩 Інтеграція з Shikimori 16 | * 🌗 Світла й темна теми оформлення 17 | * 🌍 Підтримка 🇷🇺 російської, 🇺🇦 української та 🇬🇧 англійської мов 18 | 19 | --- 20 | 21 | ## 📖 Підтримувані каталоги 22 | 23 | ### 📺 Аніме 24 | 25 | * Shikimori (🇷🇺) 26 | * AniLib (🇷🇺) 27 | 28 | ### 📚 Манґа 29 | 30 | * Desu (🇷🇺) 31 | * Shikimori (🇷🇺) 32 | * MangaDex (🇷🇺, 🇬🇧) 33 | * Remanga (🇷🇺) 34 | * MangaLib (🇷🇺) 35 | 36 | ### 📘 Ранобе 37 | 38 | * Rulate (🇷🇺) 39 | * Erolate (🇷🇺) 40 | * Ranobehub (🇷🇺) 41 | * RanobeLib (🇷🇺) 42 | 43 | ### 🔞 Хентай-манґа 44 | 45 | * AllHentai (🇷🇺) 46 | * NHentai (🇬🇧) 47 | 48 | --- 49 | 50 | ## 🖼️ Скриншоти 51 | 52 | | ![Screenshot 1](https://github.com/brandonzorn/Nlight/assets/68778953/f714165d-5df6-4b38-89a6-02d940172469) | ![Screenshot 2](https://github.com/brandonzorn/Nlight/assets/68778953/1da43e9a-52af-402d-8f30-189f31a31ba2) | ![Screenshot 3](https://github.com/brandonzorn/Nlight/assets/68778953/168f00a3-4174-41ba-8773-4548ef7ced9b) | 53 | | :---------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------: | 54 | 55 | --- 56 | 57 | ## 🧩 Підтримка та внесок 58 | 59 | Ви можете: 60 | 61 | * Запропонувати покращення 62 | * Повідомити про помилки 63 | * Допомогти з перекладом 64 | 65 | --- 66 | 67 | ## 📄 Ліцензія 68 | 69 | Цей проєкт поширюється за [MIT License](../../LICENSE). 70 | 71 | --- -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | name: Python application 2 | 3 | on: 4 | push: 5 | branches: [ "master", "1.10", "1.9", "dev_1.10", "dev_1.9"] 6 | pull_request: 7 | branches: [ "master", "1.10", "1.9", "dev_1.10", "dev_1.9"] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | flake8-test: 14 | runs-on: ubuntu-latest 15 | container: python:3.11 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Install dependencies 19 | run: pip install -r requirements/test.txt 20 | - name: Check code formatting with flake8 21 | run: flake8 ./ 22 | 23 | black-test: 24 | runs-on: ubuntu-latest 25 | container: python:3.11 26 | steps: 27 | - uses: actions/checkout@v3 28 | - name: Install dependencies 29 | run: pip install black 30 | - name: Check code formatting with black 31 | run: black ./ --check --verbose --diff 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /venv 3 | /Nlight 4 | /pkg_res/version_info.txt 5 | /dist* 6 | /build* 7 | /Nlight.spec 8 | /keys.py 9 | /package 10 | *.pyc 11 | *.log 12 | .dmypy.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 brandonzorn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NlightNuitka.bat: -------------------------------------------------------------------------------- 1 | nuitka ^ 2 | --onefile ^ 3 | --windows-console-mode=disable ^ 4 | --follow-imports ^ 5 | --enable-plugin=pyside6 ^ 6 | --product-version="1.10.4.0" ^ 7 | --file-version="1.10.4.0" ^ 8 | --company-name="brandonzorn" ^ 9 | --product-name="Nlight" ^ 10 | --windows-icon-from-ico="pkg_res/Nlight.ico" ^ 11 | -o "Nlight" ^ 12 | --output-dir=build_nuitka/ ^ 13 | main.py 14 | -------------------------------------------------------------------------------- /NlightPyinstaller.bat: -------------------------------------------------------------------------------- 1 | pyinstaller ^ 2 | --noconfirm ^ 3 | --onefile ^ 4 | --windowed ^ 5 | --icon "pkg_res/Nlight.ico" ^ 6 | --name "Nlight" ^ 7 | --version-file "pkg_res/version_info.txt" ^ 8 | main.py 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nlight 2 | 3 | 🌐 [Русский](.github/readme/README_ru.md) • [Українська](.github/readme/README_uk.md) • [English](.github/readme/README_en.md) 4 | 5 | ![Nlight Logo](.github/assets/readme_logo.png) 6 | 7 | **Nlight** is an application for reading manga and ranobe, with support for Shikimori and catalogs in Russian and English. 8 | 9 | --- 10 | 11 | ## 🚀 Features 12 | 13 | * 🔍 Browse, search, and read content 14 | * 📚 Filter by genre, type, and sort order 15 | * 🧩 Shikimori integration 16 | * 🌗 Light and dark theme support 17 | * 🌍 Available in 🇷🇺 Russian, 🇺🇦 Ukrainian, and 🇬🇧 English 18 | 19 | --- 20 | 21 | ## 📖 Supported Catalogs 22 | 23 | ### 📺 Anime 24 | 25 | * Shikimori (🇷🇺) 26 | * AniLib (🇷🇺) 27 | 28 | ### 📚 Manga 29 | 30 | * Desu (🇷🇺) 31 | * Shikimori (🇷🇺) 32 | * MangaDex (🇷🇺, 🇬🇧) 33 | * Remanga (🇷🇺) 34 | * MangaLib (🇷🇺) 35 | 36 | ### 📘 Ranobe 37 | 38 | * Rulate (🇷🇺) 39 | * Erolate (🇷🇺) 40 | * Ranobehub (🇷🇺) 41 | * RanobeLib (🇷🇺) 42 | 43 | ### 🔞 Hentai Manga 44 | 45 | * AllHentai (🇷🇺) 46 | * NHentai (🇬🇧) 47 | 48 | --- 49 | 50 | ## 🖼️ Screenshots 51 | 52 | | ![Screenshot 1](https://github.com/brandonzorn/Nlight/assets/68778953/f714165d-5df6-4b38-89a6-02d940172469) | ![Screenshot 2](https://github.com/brandonzorn/Nlight/assets/68778953/1da43e9a-52af-402d-8f30-189f31a31ba2) | ![Screenshot 3](https://github.com/brandonzorn/Nlight/assets/68778953/168f00a3-4174-41ba-8773-4548ef7ced9b) | 53 | | :---------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------: | :---------------------------------------------------------------------------------------------------------: | 54 | 55 | --- 56 | 57 | ## 🧩 Contribution & Support 58 | 59 | You are welcome to: 60 | 61 | * Suggest improvements 62 | * Report issues or bugs 63 | * Help with translations 64 | 65 | --- 66 | 67 | ## 📄 License 68 | 69 | This project is licensed under the [MIT License](LICENSE). 70 | 71 | --- -------------------------------------------------------------------------------- /data/i18n/en_US.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brandonzorn/Nlight/8b7b8924c10b49fcca9281a0b2596ef3e485e27d/data/i18n/en_US.qm -------------------------------------------------------------------------------- /data/i18n/ru_RU.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brandonzorn/Nlight/8b7b8924c10b49fcca9281a0b2596ef3e485e27d/data/i18n/ru_RU.qm -------------------------------------------------------------------------------- /data/i18n/uk_UA.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brandonzorn/Nlight/8b7b8924c10b49fcca9281a0b2596ef3e485e27d/data/i18n/uk_UA.qm -------------------------------------------------------------------------------- /data/icons/buttons/svg_24dp_black/actions/shikimori.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /data/icons/buttons/svg_24dp_white/actions/shikimori.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /data/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brandonzorn/Nlight/8b7b8924c10b49fcca9281a0b2596ef3e485e27d/data/icons/icon.png -------------------------------------------------------------------------------- /data/icons/lang/gb.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/icons/lang/jp.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/icons/lang/ru.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/icons/lang/ua.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/resource.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | icons/icon.png 4 | 5 | 6 | icons/lang/gb.svg 7 | icons/lang/jp.svg 8 | icons/lang/ru.svg 9 | icons/lang/ua.svg 10 | 11 | 12 | icons/buttons/svg_24dp_white/actions/shikimori.svg 13 | 14 | 15 | icons/buttons/svg_24dp_black/actions/shikimori.svg 16 | 17 | 18 | i18n/en_US.qm 19 | i18n/ru_RU.qm 20 | i18n/uk_UA.qm 21 | 22 | 23 | -------------------------------------------------------------------------------- /data/ui/containers/image_area.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'image_area.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.9.1 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QApplication, QFrame, QHBoxLayout, QLabel, 19 | QSizePolicy, QVBoxLayout, QWidget) 20 | 21 | from qfluentwidgets import (ScrollArea, SimpleCardWidget) 22 | 23 | class Ui_Form(object): 24 | def setupUi(self, Form): 25 | if not Form.objectName(): 26 | Form.setObjectName(u"Form") 27 | Form.resize(674, 512) 28 | self.verticalLayout = QVBoxLayout(Form) 29 | self.verticalLayout.setObjectName(u"verticalLayout") 30 | self.verticalLayout.setContentsMargins(0, 0, 0, 0) 31 | self.frame = SimpleCardWidget(Form) 32 | self.frame.setObjectName(u"frame") 33 | self.frame.setFrameShape(QFrame.StyledPanel) 34 | self.frame.setFrameShadow(QFrame.Raised) 35 | self.horizontalLayout = QHBoxLayout(self.frame) 36 | self.horizontalLayout.setObjectName(u"horizontalLayout") 37 | self.horizontalLayout.setContentsMargins(9, 9, 9, 9) 38 | self.scrollArea = ScrollArea(self.frame) 39 | self.scrollArea.setObjectName(u"scrollArea") 40 | self.scrollArea.setFocusPolicy(Qt.NoFocus) 41 | self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 42 | self.scrollArea.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 43 | self.scrollArea.setWidgetResizable(True) 44 | self.scrollAreaWidgetContents = QWidget() 45 | self.scrollAreaWidgetContents.setObjectName(u"scrollAreaWidgetContents") 46 | self.scrollAreaWidgetContents.setGeometry(QRect(0, 0, 652, 490)) 47 | self.horizontalLayout_2 = QHBoxLayout(self.scrollAreaWidgetContents) 48 | self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") 49 | self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) 50 | self.img_lbl = QLabel(self.scrollAreaWidgetContents) 51 | self.img_lbl.setObjectName(u"img_lbl") 52 | self.img_lbl.setAlignment(Qt.AlignHCenter|Qt.AlignTop) 53 | self.img_lbl.setTextInteractionFlags(Qt.NoTextInteraction) 54 | 55 | self.horizontalLayout_2.addWidget(self.img_lbl) 56 | 57 | self.scrollArea.setWidget(self.scrollAreaWidgetContents) 58 | 59 | self.horizontalLayout.addWidget(self.scrollArea) 60 | 61 | 62 | self.verticalLayout.addWidget(self.frame) 63 | 64 | 65 | self.retranslateUi(Form) 66 | 67 | QMetaObject.connectSlotsByName(Form) 68 | # setupUi 69 | 70 | def retranslateUi(self, Form): 71 | Form.setWindowTitle(QCoreApplication.translate("Form", u"Form", None)) 72 | self.img_lbl.setText("") 73 | # retranslateUi 74 | 75 | -------------------------------------------------------------------------------- /data/ui/containers/image_area.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 674 10 | 512 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 31 | 32 | QFrame::StyledPanel 33 | 34 | 35 | QFrame::Raised 36 | 37 | 38 | 39 | 9 40 | 41 | 42 | 9 43 | 44 | 45 | 9 46 | 47 | 48 | 9 49 | 50 | 51 | 52 | 53 | Qt::NoFocus 54 | 55 | 56 | Qt::ScrollBarAlwaysOff 57 | 58 | 59 | Qt::ScrollBarAlwaysOff 60 | 61 | 62 | true 63 | 64 | 65 | 66 | 67 | 0 68 | 0 69 | 652 70 | 490 71 | 72 | 73 | 74 | 75 | 0 76 | 77 | 78 | 0 79 | 80 | 81 | 0 82 | 83 | 84 | 0 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | Qt::AlignHCenter|Qt::AlignTop 93 | 94 | 95 | Qt::NoTextInteraction 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | ScrollArea 111 | QScrollArea 112 |
qfluentwidgets
113 | 1 114 |
115 | 116 | SimpleCardWidget 117 | QFrame 118 |
qfluentwidgets
119 | 1 120 |
121 |
122 | 123 | 124 |
125 | -------------------------------------------------------------------------------- /data/ui/containers/text_area.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'text_area.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.9.1 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QApplication, QFrame, QHBoxLayout, QSizePolicy, 19 | QVBoxLayout, QWidget) 20 | 21 | from qfluentwidgets import (CardWidget, SimpleCardWidget, Slider, TextEdit) 22 | 23 | class Ui_Form(object): 24 | def setupUi(self, Form): 25 | if not Form.objectName(): 26 | Form.setObjectName(u"Form") 27 | Form.resize(607, 508) 28 | self.verticalLayout_2 = QVBoxLayout(Form) 29 | self.verticalLayout_2.setObjectName(u"verticalLayout_2") 30 | self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) 31 | self.size_frame = SimpleCardWidget(Form) 32 | self.size_frame.setObjectName(u"size_frame") 33 | self.horizontalLayout_2 = QHBoxLayout(self.size_frame) 34 | self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") 35 | self.size_slider = Slider(self.size_frame) 36 | self.size_slider.setObjectName(u"size_slider") 37 | self.size_slider.setFocusPolicy(Qt.NoFocus) 38 | self.size_slider.setMinimum(9) 39 | self.size_slider.setMaximum(25) 40 | self.size_slider.setOrientation(Qt.Horizontal) 41 | 42 | self.horizontalLayout_2.addWidget(self.size_slider) 43 | 44 | 45 | self.verticalLayout_2.addWidget(self.size_frame) 46 | 47 | self.frame = SimpleCardWidget(Form) 48 | self.frame.setObjectName(u"frame") 49 | sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) 50 | sizePolicy.setHorizontalStretch(0) 51 | sizePolicy.setVerticalStretch(0) 52 | sizePolicy.setHeightForWidth(self.frame.sizePolicy().hasHeightForWidth()) 53 | self.frame.setSizePolicy(sizePolicy) 54 | self.horizontalLayout = QHBoxLayout(self.frame) 55 | self.horizontalLayout.setObjectName(u"horizontalLayout") 56 | self.text_browser = TextEdit(self.frame) 57 | self.text_browser.setObjectName(u"text_browser") 58 | self.text_browser.setFocusPolicy(Qt.NoFocus) 59 | self.text_browser.setFrameShape(QFrame.NoFrame) 60 | self.text_browser.setTextInteractionFlags(Qt.NoTextInteraction) 61 | 62 | self.horizontalLayout.addWidget(self.text_browser) 63 | 64 | 65 | self.verticalLayout_2.addWidget(self.frame) 66 | 67 | 68 | self.retranslateUi(Form) 69 | 70 | QMetaObject.connectSlotsByName(Form) 71 | # setupUi 72 | 73 | def retranslateUi(self, Form): 74 | Form.setWindowTitle(QCoreApplication.translate("Form", u"Form", None)) 75 | # retranslateUi 76 | 77 | -------------------------------------------------------------------------------- /data/ui/containers/text_area.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 607 10 | 508 11 | 12 | 13 | 14 | Form 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | Qt::NoFocus 36 | 37 | 38 | 9 39 | 40 | 41 | 25 42 | 43 | 44 | Qt::Horizontal 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 0 56 | 0 57 | 58 | 59 | 60 | 61 | 62 | 63 | Qt::NoFocus 64 | 65 | 66 | QFrame::NoFrame 67 | 68 | 69 | Qt::NoTextInteraction 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | Slider 81 | QSlider 82 |
qfluentwidgets
83 |
84 | 85 | TextEdit 86 | QTextEdit 87 |
qfluentwidgets
88 |
89 | 90 | CardWidget 91 | QFrame 92 |
qfluentwidgets
93 | 1 94 |
95 | 96 | SimpleCardWidget 97 | CardWidget 98 |
qfluentwidgets
99 | 1 100 |
101 |
102 | 103 | 104 |
105 | -------------------------------------------------------------------------------- /data/ui/manga_item.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'manga_item.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.9.1 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QApplication, QLabel, QSizePolicy, QVBoxLayout, 19 | QWidget) 20 | 21 | from qfluentwidgets import (BodyLabel, CardWidget) 22 | 23 | class Ui_Form(object): 24 | def setupUi(self, Form): 25 | if not Form.objectName(): 26 | Form.setObjectName(u"Form") 27 | Form.resize(400, 380) 28 | Form.setContextMenuPolicy(Qt.CustomContextMenu) 29 | self.verticalLayout_2 = QVBoxLayout(Form) 30 | self.verticalLayout_2.setSpacing(3) 31 | self.verticalLayout_2.setObjectName(u"verticalLayout_2") 32 | self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) 33 | self.image_card = CardWidget(Form) 34 | self.image_card.setObjectName(u"image_card") 35 | sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) 36 | sizePolicy.setHorizontalStretch(0) 37 | sizePolicy.setVerticalStretch(0) 38 | sizePolicy.setHeightForWidth(self.image_card.sizePolicy().hasHeightForWidth()) 39 | self.image_card.setSizePolicy(sizePolicy) 40 | self.verticalLayout_3 = QVBoxLayout(self.image_card) 41 | self.verticalLayout_3.setSpacing(0) 42 | self.verticalLayout_3.setObjectName(u"verticalLayout_3") 43 | self.verticalLayout_3.setContentsMargins(0, 0, 0, 0) 44 | self.image = QLabel(self.image_card) 45 | self.image.setObjectName(u"image") 46 | self.image.setAlignment(Qt.AlignHCenter|Qt.AlignTop) 47 | 48 | self.verticalLayout_3.addWidget(self.image) 49 | 50 | 51 | self.verticalLayout_2.addWidget(self.image_card) 52 | 53 | self.name_lbl = BodyLabel(Form) 54 | self.name_lbl.setObjectName(u"name_lbl") 55 | 56 | self.verticalLayout_2.addWidget(self.name_lbl) 57 | 58 | 59 | self.retranslateUi(Form) 60 | 61 | QMetaObject.connectSlotsByName(Form) 62 | # setupUi 63 | 64 | def retranslateUi(self, Form): 65 | Form.setWindowTitle(QCoreApplication.translate("Form", u"Form", None)) 66 | # retranslateUi 67 | 68 | -------------------------------------------------------------------------------- /data/ui/manga_item.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 400 10 | 380 11 | 12 | 13 | 14 | Qt::CustomContextMenu 15 | 16 | 17 | Form 18 | 19 | 20 | 21 | 3 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 0 31 | 32 | 33 | 0 34 | 35 | 36 | 37 | 38 | 39 | 0 40 | 0 41 | 42 | 43 | 44 | 45 | 0 46 | 47 | 48 | 0 49 | 50 | 51 | 0 52 | 53 | 54 | 0 55 | 56 | 57 | 0 58 | 59 | 60 | 61 | 62 | Qt::AlignHCenter|Qt::AlignTop 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | BodyLabel 77 | QLabel 78 |
qfluentwidgets
79 |
80 | 81 | CardWidget 82 | QWidget 83 |
qfluentwidgets
84 | 1 85 |
86 |
87 | 88 | 89 |
90 | -------------------------------------------------------------------------------- /data/ui/widgets/history.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'history.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.9.1 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QApplication, QFrame, QHBoxLayout, QHeaderView, 19 | QSizePolicy, QSpacerItem, QTreeWidgetItem, QVBoxLayout, 20 | QWidget) 21 | 22 | from qfluentwidgets import (SimpleCardWidget, ToolButton, TreeWidget) 23 | 24 | class Ui_Form(object): 25 | def setupUi(self, Form): 26 | if not Form.objectName(): 27 | Form.setObjectName(u"Form") 28 | Form.resize(767, 391) 29 | Form.setLocale(QLocale(QLocale.English, QLocale.UnitedStates)) 30 | self.horizontalLayout = QHBoxLayout(Form) 31 | self.horizontalLayout.setObjectName(u"horizontalLayout") 32 | self.horizontalLayout.setContentsMargins(0, 0, 0, 0) 33 | self.items_frame = SimpleCardWidget(Form) 34 | self.items_frame.setObjectName(u"items_frame") 35 | self.items_frame.setFrameShape(QFrame.StyledPanel) 36 | self.items_frame.setFrameShadow(QFrame.Raised) 37 | self.verticalLayout_2 = QVBoxLayout(self.items_frame) 38 | self.verticalLayout_2.setObjectName(u"verticalLayout_2") 39 | self.items_tree = TreeWidget(self.items_frame) 40 | __qtreewidgetitem = QTreeWidgetItem() 41 | __qtreewidgetitem.setText(0, u"1"); 42 | self.items_tree.setHeaderItem(__qtreewidgetitem) 43 | self.items_tree.setObjectName(u"items_tree") 44 | self.items_tree.setContextMenuPolicy(Qt.CustomContextMenu) 45 | self.items_tree.header().setVisible(False) 46 | 47 | self.verticalLayout_2.addWidget(self.items_tree) 48 | 49 | 50 | self.horizontalLayout.addWidget(self.items_frame) 51 | 52 | self.frame = SimpleCardWidget(Form) 53 | self.frame.setObjectName(u"frame") 54 | self.frame.setFrameShape(QFrame.StyledPanel) 55 | self.frame.setFrameShadow(QFrame.Raised) 56 | self.verticalLayout = QVBoxLayout(self.frame) 57 | self.verticalLayout.setObjectName(u"verticalLayout") 58 | self.delete_btn = ToolButton(self.frame) 59 | self.delete_btn.setObjectName(u"delete_btn") 60 | 61 | self.verticalLayout.addWidget(self.delete_btn) 62 | 63 | self.verticalSpacer = QSpacerItem(20, 231, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) 64 | 65 | self.verticalLayout.addItem(self.verticalSpacer) 66 | 67 | 68 | self.horizontalLayout.addWidget(self.frame) 69 | 70 | 71 | self.retranslateUi(Form) 72 | 73 | QMetaObject.connectSlotsByName(Form) 74 | # setupUi 75 | 76 | def retranslateUi(self, Form): 77 | pass 78 | # retranslateUi 79 | 80 | -------------------------------------------------------------------------------- /data/ui/widgets/history.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 767 10 | 391 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 31 | 32 | QFrame::StyledPanel 33 | 34 | 35 | QFrame::Raised 36 | 37 | 38 | 39 | 40 | 41 | Qt::CustomContextMenu 42 | 43 | 44 | false 45 | 46 | 47 | 48 | 1 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | QFrame::StyledPanel 60 | 61 | 62 | QFrame::Raised 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | Qt::Vertical 72 | 73 | 74 | 75 | 20 76 | 231 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | ToolButton 89 | QToolButton 90 |
qfluentwidgets
91 |
92 | 93 | SimpleCardWidget 94 | CardWidget 95 |
qfluentwidgets
96 | 1 97 |
98 | 99 | TreeWidget 100 | QTreeWidget 101 |
qfluentwidgets
102 |
103 |
104 | 105 | 106 |
107 | -------------------------------------------------------------------------------- /data/ui/widgets/library.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ################################################################################ 4 | ## Form generated from reading UI file 'library.ui' 5 | ## 6 | ## Created by: Qt User Interface Compiler version 6.9.1 7 | ## 8 | ## WARNING! All changes made in this file will be lost when recompiling UI file! 9 | ################################################################################ 10 | 11 | from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, 12 | QMetaObject, QObject, QPoint, QRect, 13 | QSize, QTime, QUrl, Qt) 14 | from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, 15 | QFont, QFontDatabase, QGradient, QIcon, 16 | QImage, QKeySequence, QLinearGradient, QPainter, 17 | QPalette, QPixmap, QRadialGradient, QTransform) 18 | from PySide6.QtWidgets import (QApplication, QHBoxLayout, QSizePolicy, QSpacerItem, 19 | QVBoxLayout, QWidget) 20 | 21 | from qfluentwidgets import (CardWidget, PushButton, SimpleCardWidget) 22 | 23 | class Ui_Form(object): 24 | def setupUi(self, Form): 25 | if not Form.objectName(): 26 | Form.setObjectName(u"Form") 27 | Form.resize(562, 350) 28 | Form.setLocale(QLocale(QLocale.English, QLocale.UnitedStates)) 29 | self.horizontalLayout = QHBoxLayout(Form) 30 | self.horizontalLayout.setObjectName(u"horizontalLayout") 31 | self.horizontalLayout.setContentsMargins(0, 0, 0, 0) 32 | self.items_frame = SimpleCardWidget(Form) 33 | self.items_frame.setObjectName(u"items_frame") 34 | sizePolicy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) 35 | sizePolicy.setHorizontalStretch(0) 36 | sizePolicy.setVerticalStretch(0) 37 | sizePolicy.setHeightForWidth(self.items_frame.sizePolicy().hasHeightForWidth()) 38 | self.items_frame.setSizePolicy(sizePolicy) 39 | self.items_layout = QVBoxLayout(self.items_frame) 40 | self.items_layout.setObjectName(u"items_layout") 41 | 42 | self.horizontalLayout.addWidget(self.items_frame) 43 | 44 | self.lists_frame = SimpleCardWidget(Form) 45 | self.lists_frame.setObjectName(u"lists_frame") 46 | self.verticalLayout_2 = QVBoxLayout(self.lists_frame) 47 | self.verticalLayout_2.setObjectName(u"verticalLayout_2") 48 | self.planned_btn = PushButton(self.lists_frame) 49 | self.planned_btn.setObjectName(u"planned_btn") 50 | self.planned_btn.setCheckable(True) 51 | self.planned_btn.setChecked(True) 52 | self.planned_btn.setAutoExclusive(True) 53 | 54 | self.verticalLayout_2.addWidget(self.planned_btn) 55 | 56 | self.completed_btn = PushButton(self.lists_frame) 57 | self.completed_btn.setObjectName(u"completed_btn") 58 | self.completed_btn.setCheckable(True) 59 | self.completed_btn.setAutoExclusive(True) 60 | 61 | self.verticalLayout_2.addWidget(self.completed_btn) 62 | 63 | self.reading_btn = PushButton(self.lists_frame) 64 | self.reading_btn.setObjectName(u"reading_btn") 65 | self.reading_btn.setCheckable(True) 66 | self.reading_btn.setAutoExclusive(True) 67 | 68 | self.verticalLayout_2.addWidget(self.reading_btn) 69 | 70 | self.re_reading_btn = PushButton(self.lists_frame) 71 | self.re_reading_btn.setObjectName(u"re_reading_btn") 72 | self.re_reading_btn.setCheckable(True) 73 | self.re_reading_btn.setAutoExclusive(True) 74 | 75 | self.verticalLayout_2.addWidget(self.re_reading_btn) 76 | 77 | self.on_hold_btn = PushButton(self.lists_frame) 78 | self.on_hold_btn.setObjectName(u"on_hold_btn") 79 | self.on_hold_btn.setCheckable(True) 80 | self.on_hold_btn.setAutoExclusive(True) 81 | 82 | self.verticalLayout_2.addWidget(self.on_hold_btn) 83 | 84 | self.dropped_btn = PushButton(self.lists_frame) 85 | self.dropped_btn.setObjectName(u"dropped_btn") 86 | self.dropped_btn.setCheckable(True) 87 | self.dropped_btn.setAutoExclusive(True) 88 | 89 | self.verticalLayout_2.addWidget(self.dropped_btn) 90 | 91 | self.verticalSpacer = QSpacerItem(20, 91, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding) 92 | 93 | self.verticalLayout_2.addItem(self.verticalSpacer) 94 | 95 | 96 | self.horizontalLayout.addWidget(self.lists_frame) 97 | 98 | 99 | self.retranslateUi(Form) 100 | 101 | QMetaObject.connectSlotsByName(Form) 102 | # setupUi 103 | 104 | def retranslateUi(self, Form): 105 | self.planned_btn.setText(QCoreApplication.translate("Form", u"Planned", None)) 106 | self.completed_btn.setText(QCoreApplication.translate("Form", u"Completed", None)) 107 | self.reading_btn.setText(QCoreApplication.translate("Form", u"Reading", None)) 108 | self.re_reading_btn.setText(QCoreApplication.translate("Form", u"Re-reading", None)) 109 | self.on_hold_btn.setText(QCoreApplication.translate("Form", u"On hold", None)) 110 | self.dropped_btn.setText(QCoreApplication.translate("Form", u"Dropped", None)) 111 | pass 112 | # retranslateUi 113 | 114 | -------------------------------------------------------------------------------- /data/ui/widgets/library.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Form 4 | 5 | 6 | 7 | 0 8 | 0 9 | 562 10 | 350 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 0 22 | 23 | 24 | 0 25 | 26 | 27 | 0 28 | 29 | 30 | 31 | 32 | 33 | 0 34 | 0 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | Planned 47 | 48 | 49 | true 50 | 51 | 52 | true 53 | 54 | 55 | true 56 | 57 | 58 | 59 | 60 | 61 | 62 | Completed 63 | 64 | 65 | true 66 | 67 | 68 | true 69 | 70 | 71 | 72 | 73 | 74 | 75 | Reading 76 | 77 | 78 | true 79 | 80 | 81 | true 82 | 83 | 84 | 85 | 86 | 87 | 88 | Re-reading 89 | 90 | 91 | true 92 | 93 | 94 | true 95 | 96 | 97 | 98 | 99 | 100 | 101 | On hold 102 | 103 | 104 | true 105 | 106 | 107 | true 108 | 109 | 110 | 111 | 112 | 113 | 114 | Dropped 115 | 116 | 117 | true 118 | 119 | 120 | true 121 | 122 | 123 | 124 | 125 | 126 | 127 | Qt::Vertical 128 | 129 | 130 | 131 | 20 132 | 91 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | PushButton 145 | QPushButton 146 |
qfluentwidgets
147 |
148 | 149 | CardWidget 150 | QFrame 151 |
qfluentwidgets
152 | 1 153 |
154 | 155 | SimpleCardWidget 156 | CardWidget 157 |
qfluentwidgets
158 | 1 159 |
160 |
161 | 162 | 163 |
164 | -------------------------------------------------------------------------------- /make_version_file.py: -------------------------------------------------------------------------------- 1 | import pyinstaller_versionfile 2 | 3 | from nlightreader.consts.app import APP_NAME, APP_VERSION 4 | 5 | pyinstaller_versionfile.create_versionfile( 6 | output_file="pkg_res/version_info.txt", 7 | version=APP_VERSION, 8 | company_name="brandonzorn", 9 | file_description=f"{APP_NAME} - Manga Reader", 10 | internal_name=APP_NAME, 11 | legal_copyright="Copyright (c) 2022 brandonzorn", 12 | original_filename=f"{APP_NAME}.exe", 13 | product_name=APP_NAME, 14 | ) 15 | -------------------------------------------------------------------------------- /makepkg.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | DEB_FILE=Nlight.deb 4 | RPM_FILE=Nlight.rpm 5 | TAR_FILE=Nlight.tar.zst 6 | 7 | [ -e package ] && rm -r package 8 | [ -e dist ] && rm -r dist 9 | [ -e $RPM_FILE ] && rm -r $RPM_FILE 10 | [ -e $TAR_FILE ] && rm -r $TAR_FILE 11 | [ -e $DEB_FILE ] && rm -r $DEB_FILE 12 | 13 | 14 | . venv/bin/activate 15 | sudo apt install ruby-dev build-essential && sudo gem i fpm -f 16 | pip install -r requirements.txt 17 | pip install pyinstaller 18 | 19 | pyinstaller --noconfirm --windowed --name "Nlight" "main.py" 20 | 21 | mkdir -p package/opt 22 | mkdir -p package/usr/share/applications 23 | mkdir -p package/usr/share/icons/hicolor/scalable/apps 24 | 25 | cp -r dist/Nlight package/opt/Nlight 26 | cp pkg_res/Nlight.svg package/usr/share/icons/hicolor/scalable/apps/Nlight.svg 27 | cp pkg_res/Nlight.desktop package/usr/share/applications 28 | 29 | find package/opt/Nlight -type f -exec chmod 644 -- {} + 30 | find package/opt/Nlight -type d -exec chmod 755 -- {} + 31 | find package/usr/share -type f -exec chmod 644 -- {} + 32 | chmod +x package/opt/Nlight/Nlight 33 | 34 | 35 | function make_package() 36 | { 37 | local DESCRIPTION="Open source manga and ranobe reading application" 38 | local URL="https://github.com/brandonzorn/Nlight" 39 | local MAINTAINER="brandonzorn " 40 | local PACKAGE_TYPE=$1 41 | local FILE_NAME=$2 42 | fpm -C package -s dir -t $PACKAGE_TYPE -n "nlight" -p dist/$FILE_NAME --license mit --architecture native --version 1.10.0.5 --description "$DESCRIPTION" --url "$URL" --maintainer "$MAINTAINER" 43 | } 44 | 45 | make_package deb $DEB_FILE 46 | make_package rpm $RPM_FILE 47 | -------------------------------------------------------------------------------- /nlightreader/__init__.py: -------------------------------------------------------------------------------- 1 | from .windows import ParentWindow 2 | -------------------------------------------------------------------------------- /nlightreader/consts/__init__.py: -------------------------------------------------------------------------------- 1 | from . import app, colors, enums, files, paths, urls 2 | -------------------------------------------------------------------------------- /nlightreader/consts/app.py: -------------------------------------------------------------------------------- 1 | APP_NAME = "Nlight" 2 | APP_VERSION = "1.10.6" 3 | APP_BRANCH = "1.10" 4 | -------------------------------------------------------------------------------- /nlightreader/consts/colors.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtGui import QColor 2 | from qfluentwidgets import FluentIcon 3 | 4 | 5 | class ItemsIcons: 6 | READ = FluentIcon.ACCEPT_MEDIUM 7 | UNREAD = FluentIcon.ACCEPT_MEDIUM.icon(color=QColor("RED")) 8 | 9 | 10 | __all__ = [ 11 | "ItemsIcons", 12 | ] 13 | -------------------------------------------------------------------------------- /nlightreader/consts/enums.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from enum import IntEnum, unique 3 | 4 | LIB_LISTS = ( 5 | "planned", 6 | "completed", 7 | "reading", 8 | "re-reading", 9 | "on hold", 10 | "dropped", 11 | ) 12 | 13 | 14 | class Nl: 15 | @unique 16 | class Language(IntEnum): 17 | undefined = 0 18 | en = 1 19 | ru = 2 20 | uk = 3 21 | jp = 4 22 | 23 | @classmethod 24 | def from_str(cls, string: str): 25 | if string in ("en", "eng"): 26 | return cls.en 27 | if string in ("ru", "rus"): 28 | return cls.ru 29 | if string in ("uk", "ukr"): 30 | return cls.uk 31 | if string in ("jp", "jap"): 32 | return cls.jp 33 | if string in ("undefined",): 34 | return cls.undefined 35 | logging.warning(f"Unknown language {string}") 36 | return cls.undefined 37 | 38 | def to_str(self) -> str: 39 | names = [ 40 | "Undefined", 41 | "English", 42 | "Russian", 43 | "Ukrainian", 44 | "Japanese", 45 | ] 46 | return names[self.value] 47 | 48 | @unique 49 | class CatalogType(IntEnum): 50 | manga = 0 51 | hentai_manga = 1 52 | ranobe = 2 53 | anime = 3 54 | 55 | @unique 56 | class MangaKind(IntEnum): 57 | undefined = 0 58 | manga = 1 59 | manhwa = 2 60 | manhua = 3 61 | one_shot = 4 62 | doujin = 5 63 | ranobe = 6 64 | comics = 7 65 | 66 | @classmethod 67 | def from_str(cls, string: str | None): 68 | def matching_the_pattern(text, pattern): 69 | text = text.lower() 70 | return any([i in text for i in pattern]) 71 | 72 | if string is None or string in ("undefined", "Другое"): 73 | return cls.undefined 74 | if matching_the_pattern(string, ("manga", "манга")): 75 | return cls.manga 76 | if matching_the_pattern(string, ("manhwa", "манхва")): 77 | return cls.manhwa 78 | if matching_the_pattern(string, ("manhua", "маньхуа")): 79 | return cls.manhua 80 | if matching_the_pattern(string, ("one_shot",)): 81 | return cls.one_shot 82 | if matching_the_pattern(string, ("doujin",)): 83 | return cls.doujin 84 | if matching_the_pattern(string, ("ranobe", "novel")): 85 | return cls.ranobe 86 | if matching_the_pattern(string, ("комикс", "comic")): 87 | return cls.comics 88 | logging.warning(f"Unknown manga kind: {string}") 89 | return cls.undefined 90 | 91 | def to_str(self) -> str: 92 | names = [ 93 | "Undefined", 94 | "Manga", 95 | "Manhwa", 96 | "Manhua", 97 | "Oneshot", 98 | "Doujin", 99 | "Ranobe", 100 | "Comics", 101 | ] 102 | return names[self.value] 103 | 104 | @unique 105 | class LibList(IntEnum): 106 | planned = 0 107 | completed = 1 108 | reading = 2 109 | re_reading = 3 110 | on_hold = 4 111 | dropped = 5 112 | 113 | @classmethod 114 | def from_str(cls, string: str): 115 | string = string.lower() 116 | if string in ("planned",): 117 | return cls.planned 118 | if string in ("completed",): 119 | return cls.completed 120 | if string in ("reading", "watching"): 121 | return cls.reading 122 | if string in ("re-reading", "rewatching"): 123 | return cls.re_reading 124 | if string in ("on hold", "on_hold"): 125 | return cls.on_hold 126 | if string in ("dropped",): 127 | return cls.dropped 128 | raise ValueError(f"Unknown lib_list: {string}") 129 | 130 | def to_str(self) -> str: 131 | return LIB_LISTS[self.value] 132 | 133 | @unique 134 | class MangaStatus(IntEnum): 135 | undefined = 0 136 | ongoing = 1 137 | released = 2 138 | frozen = 3 139 | 140 | @classmethod 141 | def from_str(cls, string: str | None): 142 | if string is None or string.lower() in ("undefined", "неизвестно"): 143 | return cls.undefined 144 | 145 | string = string.lower() 146 | if string in ("ongoing", "в процессе"): 147 | return cls.ongoing 148 | if string in ("released", "completed", "завершено"): 149 | return cls.released 150 | if string in ("frozen", "заморожено"): 151 | return cls.frozen 152 | logging.warning(f"Unknown manga status: {string}") 153 | return cls.undefined 154 | 155 | def to_str(self) -> str: 156 | names = [ 157 | "Undefined", 158 | "Ongoing", 159 | "Released", 160 | "Frozen", 161 | ] 162 | return names[self.value] 163 | 164 | 165 | __all__ = [ 166 | "LIB_LISTS", 167 | "Nl", 168 | ] 169 | -------------------------------------------------------------------------------- /nlightreader/consts/files/__init__.py: -------------------------------------------------------------------------------- 1 | from .files import Icons, LangIcons, NlFluentIcons 2 | -------------------------------------------------------------------------------- /nlightreader/consts/files/files.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from qfluentwidgets import FluentIconBase, getIconColor, Theme 4 | 5 | 6 | class LangIcons: 7 | Gb = ":/lang_icons/icons/lang/gb.svg" 8 | Ru = ":/lang_icons/icons/lang/ru.svg" 9 | Jp = ":/lang_icons/icons/lang/jp.svg" 10 | Ua = ":/lang_icons/icons/lang/ua.svg" 11 | 12 | 13 | class Icons: 14 | App = ":/png_white/icons/icon.png" 15 | 16 | 17 | class NlFluentIcons(FluentIconBase, Enum): 18 | """Custom icons""" 19 | 20 | SHIKIMORI = "shikimori" 21 | 22 | def path(self, theme=Theme.AUTO): 23 | return ( 24 | f":/actions_{getIconColor(theme)}" 25 | f"/icons/buttons/svg_24dp_{getIconColor(theme)}" 26 | f"/actions/{self.value}.svg" 27 | ) 28 | 29 | 30 | __all__ = [ 31 | "Icons", 32 | "LangIcons", 33 | "NlFluentIcons", 34 | ] 35 | -------------------------------------------------------------------------------- /nlightreader/consts/items/__init__.py: -------------------------------------------------------------------------------- 1 | from .anilib_items import AniLibItems 2 | from .desu_items import DesuItems 3 | from .mangadex_items import MangaDexItems 4 | from .mangalib_items import MangaLibItems 5 | from .ranobehub_items import ( 6 | RanobehubItems, 7 | ) 8 | from .ranobelib_items import ( 9 | RanobeLibItems, 10 | ) 11 | from .remanga_items import RemangaItems 12 | from .rulate_items import RulateItems 13 | from .shikimori_items import ( 14 | ShikimoriAnimeItems, 15 | ShikimoriItems, 16 | ) 17 | -------------------------------------------------------------------------------- /nlightreader/consts/items/anilib_items.py: -------------------------------------------------------------------------------- 1 | from nlightreader.consts.items.lib_base_items import ( 2 | LibBaseItems, 3 | ) 4 | from nlightreader.consts.items.preset_items import ( 5 | PresetKinds as Pk, 6 | PresetOrders as Po, 7 | ) 8 | 9 | 10 | class AniLibItems(LibBaseItems): 11 | ORDERS = [ 12 | {"value": None} | Po.POPULARITY, 13 | {"value": "rate_avg"} | Po.RATING, 14 | {"value": "views"} | Po.VIEWS, 15 | {"value": "episodes_count"} | Po.EPISODES_COUNT, 16 | {"value": "releaseDate"} | Po.AIRED_ON, 17 | {"value": "last_episode_at"} | Po.UPDATED, 18 | {"value": "created_at"} | Po.CREATED, 19 | {"value": "name"} | Po.NAME, 20 | {"value": "rus_name"} | Po.RUS_NAME, 21 | ] 22 | 23 | KINDS = [ 24 | {"value": 16} | Pk.TV, 25 | {"value": 17} | Pk.MOVIE, 26 | { 27 | "value": 18, 28 | "name": "Short film", 29 | "russian": "Короткометражка", 30 | }, 31 | {"value": 19} | Pk.SPECIAL, 32 | {"value": 20} | Pk.OVA, 33 | {"value": 21} | Pk.ONA, 34 | {"value": 22} | Pk.MUSIC, 35 | ] 36 | 37 | 38 | __all__ = [ 39 | "AniLibItems", 40 | ] 41 | -------------------------------------------------------------------------------- /nlightreader/consts/items/mangadex_items.py: -------------------------------------------------------------------------------- 1 | from nlightreader.consts.items.parser_items import ( 2 | ParserItems, 3 | ) 4 | 5 | 6 | class MangaDexItems(ParserItems): 7 | ORDERS = [ 8 | { 9 | "value": "relevance", 10 | "name": "Best Match", 11 | "russian": "", 12 | }, 13 | { 14 | "value": "latestUploadedChapter", 15 | "name": "Latest Upload", 16 | "russian": "", 17 | }, 18 | { 19 | "value": "title", 20 | "name": "Title Descending", 21 | "russian": "", 22 | }, 23 | { 24 | "value": "rating", 25 | "name": "Highest Rating", 26 | "russian": "", 27 | }, 28 | { 29 | "value": "followedCount", 30 | "name": "Most Follows", 31 | "russian": "", 32 | }, 33 | { 34 | "value": "createdAt", 35 | "name": "Recently Added", 36 | "russian": "", 37 | }, 38 | { 39 | "value": "year", 40 | "name": "Year Descending", 41 | "russian": "", 42 | }, 43 | ] 44 | 45 | 46 | __all__ = [ 47 | "MangaDexItems", 48 | ] 49 | -------------------------------------------------------------------------------- /nlightreader/consts/items/mangalib_items.py: -------------------------------------------------------------------------------- 1 | from nlightreader.consts.items.lib_base_items import ( 2 | LibBaseItems, 3 | ) 4 | from nlightreader.consts.items.preset_items import ( 5 | PresetKinds as Pk, 6 | PresetOrders as Po, 7 | ) 8 | 9 | 10 | class MangaLibItems(LibBaseItems): 11 | ORDERS = [ 12 | {"value": "rating_score"} | Po.RATING, 13 | {"value": "rate"} | Po.RATE_COUNT, 14 | {"value": "views"} | Po.VIEWS, 15 | {"value": "created_at"} | Po.CREATED, 16 | {"value": "last_chapter_at"} | Po.UPDATED, 17 | {"value": "chap_count"} | Po.CHAPTERS_COUNT, 18 | ] 19 | 20 | KINDS = [ 21 | {"value": 1} | Pk.MANGA, 22 | {"value": 5} | Pk.MANHWA, 23 | {"value": 8} | Pk.RU_MANGA, 24 | {"value": 9} | Pk.WESTERN_COMIC, 25 | {"value": 4} | Pk.OEL_MANGA, 26 | {"value": 6} | Pk.MANHUA, 27 | ] 28 | 29 | 30 | __all__ = [ 31 | "MangaLibItems", 32 | ] 33 | -------------------------------------------------------------------------------- /nlightreader/consts/items/parser_items.py: -------------------------------------------------------------------------------- 1 | from abc import ABC 2 | 3 | 4 | class ParserItems(ABC): 5 | ORDERS: list[dict[str, str]] = [] 6 | KINDS: list[dict[str, str]] = [] 7 | GENRES: list[dict[str, str]] = [] 8 | 9 | 10 | __all__ = [ 11 | "ParserItems", 12 | ] 13 | -------------------------------------------------------------------------------- /nlightreader/consts/items/preset_items.py: -------------------------------------------------------------------------------- 1 | class PresetKinds: 2 | MANGA = { 3 | "name": "Manga", 4 | "russian": "Манга", 5 | } 6 | OEL_MANGA = { 7 | "name": "OEL-manga", 8 | "russian": "OEL-манга", 9 | } 10 | RU_MANGA = { 11 | "name": "Rumanga", 12 | "russian": "Руманга", 13 | } 14 | MANHWA = { 15 | "name": "Manhwa", 16 | "russian": "Манхва", 17 | } 18 | MANHUA = { 19 | "name": "Manhua", 20 | "russian": "Маньхуа", 21 | } 22 | ONESHOT = { 23 | "name": "Oneshot", 24 | "russian": "Ваншот", 25 | } 26 | COMIC = { 27 | "name": "Comic", 28 | "russian": "Комикс", 29 | } 30 | WESTERN_COMIC = { 31 | "name": "Western comic", 32 | "russian": "Западный комикс", 33 | } 34 | RU_COMIC = { 35 | "name": "Rucomic", 36 | "russian": "Рукомикс", 37 | } 38 | INDONESIAN_COMIC = { 39 | "name": "Indonesian comic", 40 | "russian": "Индонезийский комикс", 41 | } 42 | DOUJIN = { 43 | "name": "doujin", 44 | "russian": "Додзинси", 45 | } 46 | OTHER = { 47 | "name": "Other", 48 | "russian": "Другое", 49 | } 50 | 51 | TV = { 52 | "name": "TV Series", 53 | "russian": "TV Сериал", 54 | } 55 | TV_13 = { 56 | "name": "Short", 57 | "russian": "Короткие", 58 | } 59 | TV_24 = { 60 | "name": "Medium", 61 | "russian": "Средние", 62 | } 63 | TV_48 = { 64 | "name": "Long", 65 | "russian": "Длинные", 66 | } 67 | MOVIE = { 68 | "name": "Movie", 69 | "russian": "Фильм", 70 | } 71 | OVA = { 72 | "name": "OVA", 73 | "russian": "OVA", 74 | } 75 | ONA = { 76 | "name": "ONA", 77 | "russian": "ONA", 78 | } 79 | SPECIAL = { 80 | "name": "Special", 81 | "russian": "Спецвыпуск", 82 | } 83 | TV_SPECIAL = { 84 | "name": "TV Special", 85 | "russian": "TV Спецвыпуск", 86 | } 87 | MUSIC = { 88 | "name": "Clip", 89 | "russian": "Клип", 90 | } 91 | PV = { 92 | "name": "Promo clip", 93 | "russian": "Проморолик", 94 | } 95 | CM = { 96 | "name": "Advertising", 97 | "russian": "Реклама", 98 | } 99 | 100 | 101 | class PresetOrders: 102 | ID = { 103 | "name": "By ID", 104 | "russian": "По ID", 105 | } 106 | NAME = { 107 | "name": "By name", 108 | "russian": "По названию", 109 | } 110 | RUS_NAME = { 111 | "name": "By name", 112 | "russian": "По названию на русском", 113 | } 114 | POPULARITY = { 115 | "name": "By popularity", 116 | "russian": "По популярности", 117 | } 118 | LIKES_NUM = { 119 | "name": "By likes", 120 | "russian": "По лайкам", 121 | } 122 | VIEWS = { 123 | "name": "By views", 124 | "russian": "По просмотрам", 125 | } 126 | STATUS = { 127 | "name": "By status", 128 | "russian": "По статусу", 129 | } 130 | RATING = { 131 | "name": "By rating", 132 | "russian": "По рейтингу", 133 | } 134 | RANDOM = { 135 | "name": "By random", 136 | "russian": "Мне повезет", 137 | } 138 | 139 | UPDATED = { 140 | "name": "By update date", 141 | "russian": "По дате обновления", 142 | } 143 | CREATED = { 144 | "name": "By date added", 145 | "russian": "По дате добавления", 146 | } 147 | AIRED_ON = { 148 | "name": "By release date", 149 | "russian": "По дате выхода", 150 | } 151 | 152 | CHAPTERS_COUNT = { 153 | "name": "By number of chapters", 154 | "russian": "По количеству глав", 155 | } 156 | EPISODES_COUNT = { 157 | "name": "By number of episodes", 158 | "russian": "По количеству эпизодов", 159 | } 160 | VOLUMES_COUNT = { 161 | "name": "By number of volumes", 162 | "russian": "По количеству томов", 163 | } 164 | RATE_COUNT = { 165 | "name": "By number of ratings", 166 | "russian": "По количеству оценок", 167 | } 168 | 169 | TRANSLATION_VOLUME = { 170 | "name": "By volume of translation", 171 | "russian": "По объему перевода", 172 | } 173 | 174 | 175 | __all__ = [ 176 | "PresetKinds", 177 | "PresetOrders", 178 | ] 179 | -------------------------------------------------------------------------------- /nlightreader/consts/items/ranobelib_items.py: -------------------------------------------------------------------------------- 1 | from nlightreader.consts.items.lib_base_items import ( 2 | LibBaseItems, 3 | ) 4 | from nlightreader.consts.items.preset_items import ( 5 | PresetOrders as Po, 6 | ) 7 | 8 | 9 | class RanobeLibItems(LibBaseItems): 10 | ORDERS = [ 11 | {"value": None} | Po.POPULARITY, 12 | {"value": "rate_avg"} | Po.RATING, 13 | {"value": "views"} | Po.VIEWS, 14 | {"value": "chap_count"} | Po.CHAPTERS_COUNT, 15 | {"value": "releaseDate"} | Po.AIRED_ON, 16 | {"value": "last_chapter_at"} | Po.UPDATED, 17 | {"value": "created_at"} | Po.CREATED, 18 | {"value": "name"} | Po.NAME, 19 | {"value": "rus_name"} | Po.RUS_NAME, 20 | ] 21 | 22 | KINDS = [ 23 | { 24 | "value": 10, 25 | "name": "Japan", 26 | "russian": "Япония", 27 | }, 28 | { 29 | "value": 11, 30 | "name": "Korea", 31 | "russian": "Корея", 32 | }, 33 | { 34 | "value": 12, 35 | "name": "China", 36 | "russian": "Китай", 37 | }, 38 | { 39 | "value": 13, 40 | "name": "English", 41 | "russian": "Английский", 42 | }, 43 | { 44 | "value": 14, 45 | "name": "Original", 46 | "russian": "Авторский", 47 | }, 48 | { 49 | "value": 15, 50 | "name": "Fanfiction", 51 | "russian": "Фанфик", 52 | }, 53 | ] 54 | 55 | 56 | __all__ = [ 57 | "RanobeLibItems", 58 | ] 59 | -------------------------------------------------------------------------------- /nlightreader/consts/items/remanga_items.py: -------------------------------------------------------------------------------- 1 | from nlightreader.consts.items.parser_items import ( 2 | ParserItems, 3 | ) 4 | from nlightreader.consts.items.preset_items import ( 5 | PresetKinds as Pk, 6 | PresetOrders as Po, 7 | ) 8 | 9 | 10 | class RemangaItems(ParserItems): 11 | ORDERS = [ 12 | {"value": "-id"} | Po.CREATED, 13 | {"value": "-chapter_date"} | Po.UPDATED, 14 | {"value": "-rating"} | Po.POPULARITY, 15 | {"value": "-votes"} | Po.LIKES_NUM, 16 | {"value": "-views"} | Po.VIEWS, 17 | {"value": "-count_chapters"} | Po.CHAPTERS_COUNT, 18 | {"value": "-random"} | Po.RANDOM, 19 | ] 20 | 21 | KINDS = [ 22 | {"value": 1} | Pk.MANGA, 23 | {"value": 2} | Pk.MANHWA, 24 | {"value": 3} | Pk.MANHUA, 25 | {"value": 4} | Pk.WESTERN_COMIC, 26 | {"value": 5} | Pk.RU_COMIC, 27 | {"value": 6} | Pk.INDONESIAN_COMIC, 28 | {"value": 7} | Pk.OTHER, 29 | ] 30 | 31 | 32 | __all__ = [ 33 | "RemangaItems", 34 | ] 35 | -------------------------------------------------------------------------------- /nlightreader/consts/items/rulate_items.py: -------------------------------------------------------------------------------- 1 | from nlightreader.consts.items.parser_items import ( 2 | ParserItems, 3 | ) 4 | from nlightreader.consts.items.preset_items import ( 5 | PresetOrders as Po, 6 | ) 7 | 8 | 9 | class RulateItems(ParserItems): 10 | ORDERS = [ 11 | { 12 | "value": "0", 13 | "name": "By degree of readiness", 14 | "russian": "По степени готовности", 15 | }, 16 | { 17 | "value": "1", 18 | "name": "Title in original language", 19 | "russian": "По названию на языке оригинала", 20 | }, 21 | { 22 | "value": "2", 23 | "name": "By title in target language", 24 | "russian": "По названию на языке перевода", 25 | }, 26 | { 27 | "value": "3", 28 | } 29 | | Po.CREATED, 30 | { 31 | "value": "4", 32 | "name": "By last activity date", 33 | "russian": "По дате последней активности", 34 | }, 35 | { 36 | "value": "5", 37 | } 38 | | Po.VIEWS, 39 | { 40 | "value": "6", 41 | } 42 | | Po.RATING, 43 | { 44 | "value": "7", 45 | "name": "By number of translated chapters", 46 | "russian": "По кол-ву переведённых глав", 47 | }, 48 | { 49 | "value": "8", 50 | } 51 | | Po.LIKES_NUM, 52 | { 53 | "value": "10", 54 | "name": "By number of pages", 55 | "russian": "По кол-ву страниц", 56 | }, 57 | { 58 | "value": "11", 59 | "name": "By number of free chapters", 60 | "russian": "По кол-ву бесплатных глав", 61 | }, 62 | { 63 | "value": "12", 64 | "name": "By number of reviews", 65 | "russian": "По кол-ву рецензий", 66 | }, 67 | { 68 | "value": "13", 69 | "name": "By number in bookmarks", 70 | "russian": "По кол-ву в закладках", 71 | }, 72 | { 73 | "value": "14", 74 | "name": "By number in favorites", 75 | "russian": "По кол-ву в избранном", 76 | }, 77 | ] 78 | 79 | 80 | __all__ = [ 81 | "RulateItems", 82 | ] 83 | -------------------------------------------------------------------------------- /nlightreader/consts/items/shikimori_items.py: -------------------------------------------------------------------------------- 1 | from nlightreader.consts.items.parser_items import ( 2 | ParserItems, 3 | ) 4 | from nlightreader.consts.items.preset_items import ( 5 | PresetKinds as Pk, 6 | PresetOrders as Po, 7 | ) 8 | 9 | 10 | class ShikimoriItems(ParserItems): 11 | ORDERS = [ 12 | {"value": "ranked"} | Po.RATING, 13 | {"value": "popularity"} | Po.POPULARITY, 14 | {"value": "name"} | Po.NAME, 15 | {"value": "aired_on"} | Po.AIRED_ON, 16 | {"value": "volumes"} | Po.VOLUMES_COUNT, 17 | {"value": "chapters"} | Po.CHAPTERS_COUNT, 18 | {"value": "status"} | Po.STATUS, 19 | {"value": "id"} | Po.ID, 20 | ] 21 | 22 | KINDS = [ 23 | {"value": "manga"} | Pk.MANGA, 24 | {"value": "manhwa"} | Pk.MANHWA, 25 | {"value": "manhua"} | Pk.MANHUA, 26 | {"value": "one_shot"} | Pk.ONESHOT, 27 | {"value": "doujin"} | Pk.DOUJIN, 28 | ] 29 | 30 | 31 | class ShikimoriAnimeItems(ParserItems): 32 | ORDERS = [ 33 | {"value": "ranked"} | Po.RATING, 34 | {"value": "popularity"} | Po.POPULARITY, 35 | {"value": "name"} | Po.NAME, 36 | {"value": "aired_on"} | Po.AIRED_ON, 37 | {"value": "status"} | Po.STATUS, 38 | {"value": "id"} | Po.ID, 39 | ] 40 | 41 | KINDS = [ 42 | {"value": "tv"} | Pk.TV, 43 | {"value": "ova"} | Pk.OVA, 44 | {"value": "ona"} | Pk.ONA, 45 | {"value": "special"} | Pk.SPECIAL, 46 | {"value": "tv_special"} | Pk.TV_SPECIAL, 47 | {"value": "music"} | Pk.MUSIC, 48 | {"value": "pv"} | Pk.PV, 49 | {"value": "cm"} | Pk.CM, 50 | ] 51 | 52 | 53 | __all__ = [ 54 | "ShikimoriAnimeItems", 55 | "ShikimoriItems", 56 | ] 57 | -------------------------------------------------------------------------------- /nlightreader/consts/paths/__init__.py: -------------------------------------------------------------------------------- 1 | from .paths import APP_DATA_PATH, TOKEN_PATH 2 | -------------------------------------------------------------------------------- /nlightreader/consts/paths/paths.py: -------------------------------------------------------------------------------- 1 | import platformdirs 2 | 3 | from nlightreader.consts.app import APP_NAME 4 | 5 | APP_DATA_PATH = platformdirs.user_data_path() / APP_NAME 6 | TOKEN_PATH = APP_DATA_PATH / "tokens" 7 | -------------------------------------------------------------------------------- /nlightreader/consts/urls.py: -------------------------------------------------------------------------------- 1 | URL_DESU = "https://desu.work" 2 | URL_DESU_API = f"{URL_DESU}/manga/api" 3 | 4 | URL_SHIKIMORI = "https://shikimori.one" 5 | URL_SHIKIMORI_API = f"{URL_SHIKIMORI}/api" 6 | URL_SHIKIMORI_TOKEN = f"{URL_SHIKIMORI}/oauth/token" 7 | 8 | URL_MANGA_DEX = "https://mangadex.org" 9 | URL_MANGA_DEX_API = "https://api.mangadex.org" 10 | URL_MANGA_DEX_TOKEN = ( 11 | "https://auth.mangadex.org/" 12 | "realms/mangadex/protocol/openid-connect/token" 13 | ) 14 | 15 | URL_RULATE = "https://tl.rulate.ru" 16 | URL_EROLATE = "https://erolate.com" 17 | 18 | URL_RANOBEHUB = "https://ranobehub.org" 19 | URL_RANOBEHUB_API = f"{URL_RANOBEHUB}/api" 20 | 21 | URL_REMANGA = "https://remanga.org" 22 | URL_REMANGA_API = f"{URL_REMANGA}/api" 23 | 24 | URL_NHENTAI = "https://nhentai.net" 25 | 26 | URL_ALLHENTAI = "https://20.allhen.online" 27 | 28 | URL_SLASHLIB = "https://v2.slashlib.me" 29 | URL_MANGALIB = "https://mangalib.me" 30 | URL_RANOBELIB = "https://ranobelib.me" 31 | URL_ANILIB = "https://anilib.me" 32 | URL_LIB_API = "https://api.cdnlibs.org/api" 33 | 34 | URL_ANISTAR = "https://v3.astar.bz" 35 | 36 | 37 | DEFAULT_HEADERS = { 38 | "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:99.0)" 39 | "Gecko/20100101 Firefox/99.0", 40 | } 41 | DESU_HEADERS = {"User-Agent": "Nlight"} 42 | SHIKIMORI_HEADERS = {"User-Agent": "Nlight"} 43 | MANGA_DEX_HEADERS = {"User-Agent": "Nlight"} 44 | 45 | 46 | GITHUB_REPO = "https://github.com/brandonzorn/Nlight/" 47 | GITHUB_REPO_API = "https://api.github.com/repos/brandonzorn/Nlight" 48 | -------------------------------------------------------------------------------- /nlightreader/controlers/__init__.py: -------------------------------------------------------------------------------- 1 | from .filters_controller import FiltersController 2 | -------------------------------------------------------------------------------- /nlightreader/controlers/filters_controller.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QLayout 2 | from qfluentwidgets import CheckBox, RadioButton 3 | 4 | from nlightreader.models import Genre, Kind, Order 5 | from nlightreader.widgets.dialogs import GenresDialog 6 | 7 | 8 | class FiltersController: 9 | def __init__(self): 10 | self._order_items = {} 11 | self._kind_items = {} 12 | 13 | self._orders_container: QLayout | None = None 14 | self._kinds_container: QLayout | None = None 15 | self._genres_container: GenresDialog | None = None 16 | 17 | def get_active_order(self) -> Order | None: 18 | for item in self._order_items: 19 | if item.isChecked(): 20 | return self._order_items[item] 21 | return None 22 | 23 | def get_active_kinds(self) -> list[Kind]: 24 | return [self._kind_items[i] for i in self._kind_items if i.isChecked()] 25 | 26 | def get_active_genres(self) -> list[Genre]: 27 | return self._genres_container.selected_genres 28 | 29 | def add_orders(self, items: list[Order]): 30 | if not self._orders_container: 31 | raise ValueError("Orders container is not set") 32 | for item in items: 33 | item_widget = RadioButton() 34 | item_widget.setText(item.get_name()) 35 | if not self._order_items: 36 | item_widget.setChecked(True) 37 | self._orders_container.addWidget(item_widget) 38 | self._order_items.update({item_widget: item}) 39 | 40 | def add_kinds(self, items: list[Kind]): 41 | if not self._orders_container: 42 | raise ValueError("Kinds container is not set") 43 | for item in items: 44 | item_widget = CheckBox() 45 | item_widget.setText(item.get_name()) 46 | self._kinds_container.addWidget(item_widget) 47 | self._kind_items.update({item_widget: item}) 48 | 49 | def add_genres(self, items: list[Genre]): 50 | if not self._orders_container: 51 | raise ValueError("Genres container is not set") 52 | self._genres_container.set_genres(items) 53 | 54 | def set_orders_container(self, container): 55 | if not isinstance(container, QLayout): 56 | raise ValueError("Container must be a QLayout") 57 | if self._orders_container: 58 | raise ValueError("Orders container is already set") 59 | self._orders_container = container 60 | 61 | def set_kinds_container(self, container): 62 | if not isinstance(container, QLayout): 63 | raise ValueError("Container must be a QLayout") 64 | if self._kinds_container: 65 | raise ValueError("Kinds container is already set") 66 | self._kinds_container = container 67 | 68 | def set_genres_container(self, container): 69 | if not isinstance(container, GenresDialog): 70 | raise ValueError("Container must be a FormGenres") 71 | if self._genres_container: 72 | raise ValueError("Genres container is already set") 73 | self._genres_container = container 74 | 75 | def clear(self): 76 | [item.deleteLater() for item in self._order_items] 77 | [item.deleteLater() for item in self._kind_items] 78 | self._order_items.clear() 79 | self._kind_items.clear() 80 | self._genres_container.clear() 81 | 82 | def reset_items(self): 83 | if self._order_items: 84 | list(self._order_items.keys())[0].setChecked(True) 85 | [i.setChecked(False) for i in self._kind_items] 86 | self._genres_container.reset_items() 87 | 88 | 89 | __all__ = [ 90 | "FiltersController", 91 | ] 92 | -------------------------------------------------------------------------------- /nlightreader/exceptions/__init__.py: -------------------------------------------------------------------------------- 1 | from . import parser_content_exc 2 | -------------------------------------------------------------------------------- /nlightreader/exceptions/parser_content_exc.py: -------------------------------------------------------------------------------- 1 | class NoContentError(Exception): 2 | pass 3 | 4 | 5 | class FetchContentError(Exception): 6 | pass 7 | 8 | 9 | class RequestsParamsError(Exception): 10 | pass 11 | 12 | 13 | __all__ = [ 14 | "FetchContentError", 15 | "NoContentError", 16 | "RequestsParamsError", 17 | ] 18 | -------------------------------------------------------------------------------- /nlightreader/items/RequestForm.py: -------------------------------------------------------------------------------- 1 | from nlightreader.consts.enums import Nl 2 | from nlightreader.models.sort_models import Genre, Kind, Order 3 | 4 | 5 | class RequestForm: 6 | def __init__(self): 7 | self.limit = 50 8 | self.search = "" 9 | self.page = 1 10 | self.__genres: list[Genre] = [] 11 | self.__order: Order | None = None 12 | self.__kinds: list[Kind] = [] 13 | self.lib_list = Nl.LibList.planned 14 | 15 | @property 16 | def offset(self): 17 | return (self.page - 1) * 50 18 | 19 | def set_order(self, order: Order): 20 | self.__order = order 21 | 22 | def set_kinds(self, kinds: list[Kind]): 23 | self.__kinds = kinds 24 | 25 | def set_genres(self, genres: list[Genre]): 26 | self.__genres = genres 27 | 28 | def get_order_id(self) -> str: 29 | return self.__order.content_id 30 | 31 | def get_kind_ids(self) -> list[str]: 32 | return [kind.content_id for kind in self.__kinds] 33 | 34 | def get_genre_ids(self) -> list[str]: 35 | return [genre.content_id for genre in self.__genres] 36 | 37 | def clear(self): 38 | self.limit = 50 39 | self.search = "" 40 | self.page = 1 41 | self.__genres = [] 42 | self.__order = None 43 | self.__kinds = [] 44 | self.lib_list = Nl.LibList.planned 45 | 46 | 47 | __all__ = [ 48 | "RequestForm", 49 | ] 50 | -------------------------------------------------------------------------------- /nlightreader/items/__init__.py: -------------------------------------------------------------------------------- 1 | from .other_items import HistoryNote, User, UserRate 2 | from .RequestForm import RequestForm 3 | -------------------------------------------------------------------------------- /nlightreader/items/other_items.py: -------------------------------------------------------------------------------- 1 | from nlightreader.consts.enums import Nl 2 | from nlightreader.models import Chapter, Manga 3 | 4 | 5 | class HistoryNote: 6 | def __init__(self, chapter: Chapter, manga: Manga, is_completed: bool): 7 | self.chapter = chapter 8 | self.manga = manga 9 | self.is_completed = is_completed 10 | 11 | def get_name(self): 12 | return f"{self.manga.get_name()}: {self.chapter.get_name()}" 13 | 14 | def to_dict(self) -> dict: 15 | return { 16 | "manga_id": self.manga.id, 17 | "chapter_id": self.chapter.id, 18 | "is_completed": self.is_completed, 19 | } 20 | 21 | 22 | class UserRate: 23 | def __init__( 24 | self, 25 | rate_id, 26 | user_id, 27 | target_id, 28 | score: int, 29 | status: Nl.LibList, 30 | chapters, 31 | ): 32 | self.id = rate_id 33 | self.user_id = user_id 34 | self.target_id = target_id 35 | self.score = score 36 | self.status = status 37 | self.chapters = chapters 38 | 39 | 40 | class User: 41 | def __init__(self, user_id, nickname, avatar): 42 | self.id = user_id 43 | self.nickname = nickname 44 | self.avatar = avatar 45 | 46 | 47 | __all__ = [ 48 | "HistoryNote", 49 | "UserRate", 50 | "User", 51 | ] 52 | -------------------------------------------------------------------------------- /nlightreader/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .chapter_model import Chapter 2 | from .character_model import Character 3 | from .image_model import Image 4 | from .manga_model import Manga 5 | from .sort_models import Genre, Kind, Order 6 | -------------------------------------------------------------------------------- /nlightreader/models/base_model.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import QLocale 2 | 3 | from nlightreader.utils.config import cfg 4 | 5 | 6 | class BaseModel: 7 | def __init__(self, content_id: str, catalog_id: int): 8 | self.__id = f"|{catalog_id}|_|{content_id}|" 9 | self.__content_id = content_id 10 | self.__catalog_id = catalog_id 11 | 12 | def __eq__(self, other): 13 | return other.id == self.id 14 | 15 | def __hash__(self): 16 | return hash(self.id) 17 | 18 | @property 19 | def id(self): 20 | return self.__id 21 | 22 | @property 23 | def content_id(self): 24 | return self.__content_id 25 | 26 | @property 27 | def catalog_id(self): 28 | return self.__catalog_id 29 | 30 | def to_dict(self) -> dict: 31 | raise NotImplementedError 32 | 33 | 34 | class NamedBaseModel(BaseModel): 35 | def __init__( 36 | self, 37 | content_id: str, 38 | catalog_id: int, 39 | name: str, 40 | russian: str, 41 | ): 42 | super().__init__(content_id, catalog_id) 43 | self.__name = name 44 | self.__russian = russian 45 | 46 | @property 47 | def name(self): 48 | return self.__name 49 | 50 | @property 51 | def russian(self): 52 | return self.__russian 53 | 54 | def get_name(self) -> str: 55 | locale = cfg.get(cfg.language).value.language() 56 | if ( 57 | locale 58 | in ( 59 | QLocale.Language.Russian, 60 | QLocale.Language.Ukrainian, 61 | ) 62 | and self.__russian 63 | ): 64 | return self.__russian 65 | return self.__name 66 | 67 | 68 | __all__ = [ 69 | "BaseModel", 70 | "NamedBaseModel", 71 | ] 72 | -------------------------------------------------------------------------------- /nlightreader/models/chapter_model.py: -------------------------------------------------------------------------------- 1 | from types import NoneType 2 | 3 | from nlightreader.consts.enums import Nl 4 | from nlightreader.models.base_model import BaseModel 5 | 6 | 7 | class Chapter(BaseModel): 8 | def __init__( 9 | self, 10 | content_id: str, 11 | catalog_id: int, 12 | volume_number: str | None, 13 | chapter_number: str | None, 14 | title: str, 15 | language: Nl.Language = Nl.Language.undefined, 16 | ): 17 | super().__init__(content_id, catalog_id) 18 | self.__volume_number = volume_number 19 | self.__chapter_number = chapter_number 20 | self.__title = title 21 | self.__language = language 22 | self.__translator: str | None = None 23 | 24 | @property 25 | def volume_number(self): 26 | return self.__volume_number 27 | 28 | @property 29 | def chapter_number(self): 30 | return self.__chapter_number 31 | 32 | @property 33 | def language(self): 34 | return self.__language 35 | 36 | @property 37 | def translator(self): 38 | return self.__translator 39 | 40 | @translator.setter 41 | def translator(self, translator: str): 42 | if not isinstance(translator, (str, NoneType)): 43 | raise TypeError( 44 | f"Translator must be a string or None got {type(translator)}", 45 | ) 46 | self.__translator = translator 47 | 48 | def get_name(self) -> str: 49 | if not self.__volume_number and not self.__chapter_number: 50 | return self.__title 51 | 52 | vol_ch_name = f"{self.__volume_number}-{self.__chapter_number}" 53 | if self.__title: 54 | return f"{vol_ch_name} {self.__title}" 55 | return vol_ch_name 56 | 57 | def to_dict(self): 58 | return { 59 | "id": self.id, 60 | "content_id": self.content_id, 61 | "catalog_id": self.catalog_id, 62 | "vol": self.__volume_number, 63 | "ch": self.__chapter_number, 64 | "title": self.__title, 65 | "language": self.language.name, 66 | } 67 | 68 | 69 | __all__ = [ 70 | "Chapter", 71 | ] 72 | -------------------------------------------------------------------------------- /nlightreader/models/character_model.py: -------------------------------------------------------------------------------- 1 | from nlightreader.models.base_model import NamedBaseModel 2 | 3 | 4 | class Character(NamedBaseModel): 5 | def __init__( 6 | self, 7 | content_id: str, 8 | catalog_id: int, 9 | name: str, 10 | russian: str, 11 | description: str, 12 | role: str, 13 | ): 14 | super().__init__(content_id, catalog_id, name, russian) 15 | self.__description = description 16 | self.__role = role 17 | 18 | @property 19 | def description(self): 20 | return self.__description 21 | 22 | @description.setter 23 | def description(self, description: str): 24 | if not isinstance(description, str): 25 | raise TypeError( 26 | f"Description must be a string got {type(description)}", 27 | ) 28 | self.__description = description 29 | 30 | @property 31 | def role(self): 32 | return self.__role 33 | 34 | @role.setter 35 | def role(self, role: str): 36 | if not isinstance(role, str): 37 | raise TypeError(f"Role must be a string got {type(role)}") 38 | self.__role = role 39 | 40 | 41 | __all__ = [ 42 | "Character", 43 | ] 44 | -------------------------------------------------------------------------------- /nlightreader/models/image_model.py: -------------------------------------------------------------------------------- 1 | from types import NoneType 2 | 3 | import validators 4 | 5 | 6 | class Image: 7 | def __init__(self, image_id: str, page_number: int, url: str | None): 8 | self.id = image_id 9 | self.page_number = page_number 10 | self.__url = None 11 | 12 | self.url = url 13 | 14 | @property 15 | def url(self): 16 | return self.__url 17 | 18 | @url.setter 19 | def url(self, url: str | None): 20 | if not isinstance(url, (str, NoneType)): 21 | raise TypeError(f"Url must be str or None got {type(url)}") 22 | if url is not None and not validators.url(url): 23 | raise ValueError(f"Url {url} is not valid") 24 | self.__url = url 25 | 26 | 27 | __all__ = [ 28 | "Image", 29 | ] 30 | -------------------------------------------------------------------------------- /nlightreader/models/sort_models.py: -------------------------------------------------------------------------------- 1 | from nlightreader.models.base_model import NamedBaseModel 2 | 3 | 4 | class Kind(NamedBaseModel): 5 | def __init__( 6 | self, 7 | content_id: str, 8 | catalog_id: int, 9 | name: str, 10 | russian: str, 11 | ): 12 | super().__init__(content_id, catalog_id, name, russian) 13 | 14 | 15 | class Order(NamedBaseModel): 16 | def __init__( 17 | self, 18 | content_id: str, 19 | catalog_id: int, 20 | name: str, 21 | russian: str, 22 | ): 23 | super().__init__(content_id, catalog_id, name, russian) 24 | 25 | 26 | class Genre(NamedBaseModel): 27 | def __init__( 28 | self, 29 | content_id: str, 30 | catalog_id: int, 31 | name: str, 32 | russian: str, 33 | ): 34 | super().__init__(content_id, catalog_id, name, russian) 35 | 36 | 37 | class Status(NamedBaseModel): 38 | def __init__( 39 | self, 40 | content_id: str, 41 | catalog_id: int, 42 | name: str, 43 | russian: str, 44 | ): 45 | super().__init__(content_id, catalog_id, name, russian) 46 | 47 | 48 | __all__ = [ 49 | "Kind", 50 | "Order", 51 | "Genre", 52 | "Status", 53 | ] 54 | -------------------------------------------------------------------------------- /nlightreader/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | from .combined.lib import ( 2 | LibAnilib, 3 | LibBase, 4 | LibMangalib, 5 | LibRanobelib, 6 | ) 7 | from .combined.shikimori import ( 8 | ShikimoriAnime, 9 | ShikimoriBase, 10 | ShikimoriLib, 11 | ShikimoriManga, 12 | ShikimoriRanobe, 13 | ) 14 | from .hentai_manga import AllHentai, NHentai 15 | from .local_library import LocalLibrary 16 | from .manga import Desu, MangaDex, MangaDexLib, Remanga, SlashLib 17 | from .ranobe import Erolate, Ranobehub, Rulate 18 | -------------------------------------------------------------------------------- /nlightreader/parsers/catalog.py: -------------------------------------------------------------------------------- 1 | from nlightreader.consts.items.parser_items import ParserItems 2 | from nlightreader.consts.urls import DEFAULT_HEADERS 3 | from nlightreader.items import ( 4 | RequestForm, 5 | User, 6 | UserRate, 7 | ) 8 | from nlightreader.models import ( 9 | Chapter, 10 | Character, 11 | Genre, 12 | Image, 13 | Kind, 14 | Manga, 15 | Order, 16 | ) 17 | 18 | 19 | class AbstractCatalog: 20 | CATALOG_NAME = "CATALOG" 21 | CATALOG_ID = -1 22 | is_primary = False 23 | 24 | def __init__(self): 25 | self.headers = DEFAULT_HEADERS 26 | self.cookies = None 27 | self.items = ParserItems 28 | 29 | def get_manga(self, manga: Manga) -> Manga: 30 | return manga 31 | 32 | def get_character(self, character: Character) -> Character: 33 | return character 34 | 35 | def search_manga(self, form: RequestForm) -> list[Manga]: 36 | return [] 37 | 38 | def get_chapters(self, manga: Manga) -> list[Chapter]: 39 | return [] 40 | 41 | def get_images(self, manga: Manga, chapter: Chapter) -> list[Image]: 42 | return [] 43 | 44 | def get_image(self, image: Image): 45 | return 46 | 47 | def get_preview(self, manga: Manga): 48 | return 49 | 50 | def get_character_preview(self, character: Character): 51 | return 52 | 53 | def get_genres(self): 54 | return [ 55 | Genre( 56 | i["value"], 57 | self.CATALOG_ID, 58 | i["name"], 59 | i["russian"], 60 | ) 61 | for i in self.items.GENRES 62 | ] 63 | 64 | def get_kinds(self) -> list[Kind]: 65 | return [ 66 | Kind( 67 | i["value"], 68 | self.CATALOG_ID, 69 | i["name"], 70 | i["russian"], 71 | ) 72 | for i in self.items.KINDS 73 | ] 74 | 75 | def get_orders(self) -> list[Order]: 76 | return [ 77 | Order( 78 | i["value"], 79 | self.CATALOG_ID, 80 | i["name"], 81 | i["russian"], 82 | ) 83 | for i in self.items.ORDERS 84 | ] 85 | 86 | def get_relations(self, manga: Manga) -> list[Manga]: 87 | return [] 88 | 89 | def get_characters(self, manga: Manga) -> list[Character]: 90 | return [] 91 | 92 | def get_manga_url(self, manga: Manga) -> str: 93 | pass 94 | 95 | 96 | class LibParser: 97 | def search_manga(self, form: RequestForm) -> list[Manga]: 98 | return [] 99 | 100 | def get_user(self) -> User: 101 | return User(None, None, None) 102 | 103 | def create_user_rate(self, manga: Manga) -> None: 104 | pass 105 | 106 | def check_user_rate(self, manga: Manga) -> None: 107 | pass 108 | 109 | def delete_user_rate(self, user_rate: UserRate): 110 | pass 111 | 112 | def get_user_rate(self, manga: Manga) -> UserRate: 113 | pass 114 | 115 | def update_user_rate(self, user_rate: UserRate): 116 | pass 117 | 118 | 119 | __all__ = [ 120 | "AbstractCatalog", 121 | "LibParser", 122 | ] 123 | -------------------------------------------------------------------------------- /nlightreader/parsers/catalogs_base.py: -------------------------------------------------------------------------------- 1 | from nlightreader.consts.enums import Nl 2 | from nlightreader.parsers.catalog import AbstractCatalog 3 | 4 | 5 | class AbstractMangaCatalog(AbstractCatalog): 6 | CATALOG_TYPE = Nl.CatalogType.manga 7 | 8 | def __init__(self): 9 | super().__init__() 10 | 11 | 12 | class AbstractHentaiMangaCatalog(AbstractCatalog): 13 | CATALOG_TYPE = Nl.CatalogType.hentai_manga 14 | 15 | def __init__(self): 16 | super().__init__() 17 | 18 | 19 | class AbstractRanobeCatalog(AbstractCatalog): 20 | CATALOG_TYPE = Nl.CatalogType.ranobe 21 | 22 | def __init__(self): 23 | super().__init__() 24 | 25 | 26 | class AbstractAnimeCatalog(AbstractCatalog): 27 | CATALOG_TYPE = Nl.CatalogType.anime 28 | 29 | def __init__(self): 30 | super().__init__() 31 | 32 | 33 | __all__ = [ 34 | "AbstractMangaCatalog", 35 | "AbstractAnimeCatalog", 36 | "AbstractRanobeCatalog", 37 | "AbstractHentaiMangaCatalog", 38 | ] 39 | -------------------------------------------------------------------------------- /nlightreader/parsers/combined/lib/__init__.py: -------------------------------------------------------------------------------- 1 | from .lib_anilib import LibAnilib 2 | from .lib_base import LibBase 3 | from .lib_mangalib import LibMangalib 4 | from .lib_ranobelib import LibRanobelib 5 | -------------------------------------------------------------------------------- /nlightreader/parsers/combined/lib/lib_anilib.py: -------------------------------------------------------------------------------- 1 | from nlightreader.consts.enums import Nl 2 | from nlightreader.consts.items import AniLibItems 3 | from nlightreader.consts.urls import URL_ANILIB 4 | from nlightreader.models import Chapter, Manga 5 | from nlightreader.parsers.catalogs_base import AbstractAnimeCatalog 6 | from nlightreader.parsers.combined.lib.lib_base import LibBase 7 | from nlightreader.utils.utils import get_html 8 | 9 | 10 | class LibAnilib(LibBase, AbstractAnimeCatalog): 11 | CATALOG_NAME = "AniLib" 12 | CATALOG_ID = 14 13 | 14 | def __init__(self): 15 | super().__init__() 16 | self.url = URL_ANILIB 17 | self.items = AniLibItems 18 | 19 | self.content_name = "anime" 20 | self.site_id = 5 21 | 22 | def get_chapters(self, manga: Manga) -> list[Chapter]: 23 | url = f"{self.url_api}/episodes?anime_id={manga.content_id}" 24 | episodes: list[Chapter] = [] 25 | episodes_response = get_html(url, content_type="json") 26 | if episodes_response: 27 | for i in episodes_response["data"]: 28 | episode = Chapter( 29 | i["id"], 30 | self.CATALOG_ID, 31 | None, 32 | "", 33 | f"Episode {i['number']}", 34 | Nl.Language.ru, 35 | ) 36 | episodes.append(episode) 37 | episodes.reverse() 38 | return episodes 39 | 40 | 41 | __all__ = [ 42 | "LibAnilib", 43 | ] 44 | -------------------------------------------------------------------------------- /nlightreader/parsers/combined/lib/lib_base.py: -------------------------------------------------------------------------------- 1 | from nlightreader.consts.enums import Nl 2 | from nlightreader.consts.urls import URL_LIB_API 3 | from nlightreader.items import RequestForm 4 | from nlightreader.models import Chapter, Manga 5 | from nlightreader.parsers.catalog import AbstractCatalog 6 | from nlightreader.utils.utils import get_html 7 | 8 | 9 | class LibBase(AbstractCatalog): 10 | def __init__(self): 11 | super().__init__() 12 | self.url_api = URL_LIB_API 13 | 14 | # override in subclass 15 | self.content_name = None 16 | self.site_id = None 17 | 18 | def get_manga(self, manga: Manga) -> Manga: 19 | url = f"{self.url_api}/{self.content_name}/{manga.content_id}" 20 | params = {"fields[]": ["summary", "rate_avg"]} 21 | response = get_html(url, params=params, content_type="json") 22 | if response: 23 | data = response["data"] 24 | manga.preview_url = data["cover"]["md"] 25 | manga.score = float(data["rating"]["average"]) 26 | if description := data.get("summary"): 27 | manga.add_description(Nl.Language.ru, description) 28 | return manga 29 | 30 | def search_manga(self, form: RequestForm) -> list[Manga]: 31 | url = f"{self.url_api}/{self.content_name}" 32 | params = { 33 | "site_id[]": self.site_id, 34 | "sort_by": form.get_order_id(), 35 | "types[]": form.get_kind_ids(), 36 | "genres[]": form.get_genre_ids(), 37 | "q": form.search, 38 | } 39 | cookies = {"adult_caution": '{"media":true,"content":true}'} 40 | response = get_html( 41 | url, 42 | params=params, 43 | cookies=cookies, 44 | content_type="json", 45 | ) 46 | 47 | mangas = [] 48 | if response: 49 | for i in response.get("data"): 50 | manga = Manga( 51 | i["slug_url"], 52 | self.CATALOG_ID, 53 | i["name"], 54 | i["rus_name"], 55 | ) 56 | manga.preview_url = i["cover"]["md"] 57 | manga.score = float(i["rating"]["average"]) 58 | mangas.append(manga) 59 | return mangas 60 | 61 | def get_chapters(self, manga: Manga) -> list[Chapter]: 62 | branches_url = ( 63 | f"{self.url_api}/branches/{manga.content_id.split('--')[0]}" 64 | ) 65 | 66 | branches = {} 67 | if branches_response := get_html(branches_url, content_type="json"): 68 | for branch in branches_response["data"]: 69 | branches.update({branch["id"]: branch["teams"][0]["name"]}) 70 | 71 | chapters_url = ( 72 | f"{self.url_api}/{self.content_name}/{manga.content_id}/chapters" 73 | ) 74 | 75 | chapters: list[Chapter] = [] 76 | if chapters_response := get_html(chapters_url, content_type="json"): 77 | for i in chapters_response["data"]: 78 | chapter = Chapter( 79 | str(i["id"]), 80 | self.CATALOG_ID, 81 | i["volume"], 82 | i["number"], 83 | i["name"], 84 | Nl.Language.ru, 85 | ) 86 | if branches_data := i.get("branches"): 87 | chapter.translator = branches.get( 88 | branches_data[0]["branch_id"], 89 | ) 90 | chapters.append(chapter) 91 | chapters.reverse() 92 | return chapters 93 | 94 | def get_preview(self, manga: Manga): 95 | return get_html(manga.preview_url, content_type="content") 96 | 97 | 98 | __all__ = [ 99 | "LibBase", 100 | ] 101 | -------------------------------------------------------------------------------- /nlightreader/parsers/combined/lib/lib_mangalib.py: -------------------------------------------------------------------------------- 1 | from nlightreader.consts.items import MangaLibItems 2 | from nlightreader.consts.urls import URL_MANGALIB 3 | from nlightreader.models import Chapter, Image, Manga 4 | from nlightreader.parsers.catalogs_base import AbstractMangaCatalog 5 | from nlightreader.parsers.combined.lib.lib_base import LibBase 6 | from nlightreader.utils.utils import get_html 7 | 8 | 9 | class LibMangalib(LibBase, AbstractMangaCatalog): 10 | CATALOG_NAME = "MangaLib" 11 | CATALOG_ID = 10 12 | 13 | def __init__(self): 14 | super().__init__() 15 | self.url = URL_MANGALIB 16 | self.items = MangaLibItems 17 | 18 | self.content_name = "manga" 19 | self.site_id = 1 20 | 21 | def get_images(self, manga: Manga, chapter: Chapter) -> list[Image]: 22 | url = f"{self.url_api}/{self.content_name}/{manga.content_id}/chapter" 23 | params = { 24 | "number": chapter.chapter_number, 25 | "volume": chapter.volume_number, 26 | } 27 | response = get_html(url, params=params, content_type="json") 28 | images = [] 29 | if response: 30 | data = response["data"] 31 | for i, page_data in enumerate(data.get("pages", [])): 32 | images.append( 33 | Image( 34 | page_data["id"], 35 | i + 1, 36 | f"https://img33.imgslib.link{page_data['url']}", 37 | ), 38 | ) 39 | return images 40 | 41 | def get_image(self, image: Image): 42 | return get_html( 43 | image.url, 44 | content_type="content", 45 | headers={"Referer": f"{self.url}/"}, 46 | ) 47 | 48 | def get_manga_url(self, manga: Manga) -> str: 49 | return f"{self.url}/ru/manga/{manga.content_id}" 50 | 51 | 52 | __all__ = [ 53 | "LibMangalib", 54 | ] 55 | -------------------------------------------------------------------------------- /nlightreader/parsers/combined/lib/lib_ranobelib.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import re 3 | 4 | from nlightreader.consts.enums import Nl 5 | from nlightreader.consts.items import RanobeLibItems 6 | from nlightreader.consts.urls import URL_RANOBELIB 7 | from nlightreader.models import Chapter, Image, Manga 8 | from nlightreader.parsers.catalogs_base import AbstractRanobeCatalog 9 | from nlightreader.parsers.combined.lib.lib_base import LibBase 10 | from nlightreader.utils.utils import get_html 11 | 12 | 13 | class LibRanobelib(LibBase, AbstractRanobeCatalog): 14 | CATALOG_NAME = "RanobeLib" 15 | CATALOG_ID = 13 16 | 17 | def __init__(self): 18 | super().__init__() 19 | self.url = URL_RANOBELIB 20 | self.items = RanobeLibItems 21 | 22 | self.content_name = "manga" 23 | self.site_id = 3 24 | 25 | def get_manga(self, manga: Manga) -> Manga: 26 | manga.kind = Nl.MangaKind.ranobe 27 | return super().get_manga(manga) 28 | 29 | def get_images(self, manga: Manga, chapter: Chapter) -> list[Image]: 30 | url = ( 31 | f"{self.url_api}/{self.content_name}/{manga.content_id}/chapter" 32 | f"?number={chapter.chapter_number}" 33 | f"&volume={chapter.volume_number}" 34 | ) 35 | return [Image("", 1, url)] 36 | 37 | def get_image(self, image: Image): 38 | def get_chapter_content_image(media_id: str): 39 | url = ( 40 | media_id 41 | if media_id.startswith( 42 | "http", 43 | ) 44 | else f"{self.url}{media_id}" 45 | ) 46 | chapter_image = get_html( 47 | url, 48 | headers=self.headers, 49 | ).content 50 | str_equivalent_image = base64.b64encode(chapter_image).decode() 51 | return f"data:image/png;base64,{str_equivalent_image}" 52 | 53 | def replace_images(text: str): 54 | pattern = r'src="([^"]+)"' 55 | return re.sub( 56 | pattern, 57 | lambda x: f'src="{get_chapter_content_image(x.group(1))}"', 58 | text, 59 | ) 60 | 61 | response = get_html(image.url, content_type="json") 62 | if response: 63 | data = response["data"] 64 | content = data["content"] 65 | if isinstance(content, str): 66 | content = content.replace("\n", "") 67 | content = content.replace("\r", "") 68 | return replace_images(content) 69 | return None 70 | 71 | def get_manga_url(self, manga: Manga) -> str: 72 | return f"{self.url}/ru/book/{manga.content_id}" 73 | 74 | 75 | __all__ = [ 76 | "LibRanobelib", 77 | ] 78 | -------------------------------------------------------------------------------- /nlightreader/parsers/combined/shikimori/__init__.py: -------------------------------------------------------------------------------- 1 | from .shikimori_anime import ShikimoriAnime 2 | from .shikimori_base import ShikimoriBase 3 | from .shikimori_lib import ShikimoriLib 4 | from .shikimori_manga import ShikimoriManga 5 | from .shikimori_ranobe import ShikimoriRanobe 6 | -------------------------------------------------------------------------------- /nlightreader/parsers/combined/shikimori/shikimori_base.py: -------------------------------------------------------------------------------- 1 | from nlightreader.consts.enums import Nl 2 | from nlightreader.consts.items import ShikimoriItems 3 | from nlightreader.consts.urls import ( 4 | SHIKIMORI_HEADERS, 5 | URL_SHIKIMORI, 6 | URL_SHIKIMORI_API, 7 | ) 8 | from nlightreader.models import Character, Genre, Manga, Order 9 | from nlightreader.parsers.catalog import AbstractCatalog 10 | from nlightreader.utils.utils import get_html 11 | 12 | 13 | class ShikimoriBase(AbstractCatalog): 14 | CATALOG_ID = 1 15 | CATALOG_NAME = "Shikimori" 16 | 17 | def __init__(self): 18 | super().__init__() 19 | self.url = URL_SHIKIMORI 20 | self.url_api = URL_SHIKIMORI_API 21 | self.headers = SHIKIMORI_HEADERS 22 | self.is_primary = True 23 | 24 | def setup_manga(self, data: dict) -> Manga: 25 | return Manga( 26 | str(data.get("id")), 27 | self.CATALOG_ID, 28 | data.get("name"), 29 | data.get("russian"), 30 | ) 31 | 32 | def get_manga(self, manga: Manga) -> Manga: 33 | url = f"{self.url_api}/mangas/{manga.content_id}" 34 | response = get_html(url, headers=self.headers, content_type="json") 35 | if response: 36 | data = response 37 | manga.kind = Nl.MangaKind.from_str(data.get("kind")) 38 | manga.score = float(data.get("score")) 39 | manga.status = Nl.MangaStatus.from_str(data.get("status")) 40 | if data.get("volumes"): 41 | manga.volumes = int(data.get("volumes")) 42 | if data.get("chapters"): 43 | manga.chapters = int(data.get("chapters")) 44 | 45 | if description := data.get("description"): 46 | manga.add_description( 47 | Nl.Language.undefined, 48 | description, 49 | ) 50 | return manga 51 | 52 | def get_character(self, character: Character) -> Character: 53 | url = f"{self.url_api}/characters/{character.content_id}" 54 | response = get_html(url, headers=self.headers, content_type="json") 55 | if response and (description := response.get("description")): 56 | character.description = description 57 | return character 58 | 59 | def get_preview(self, manga: Manga): 60 | return get_html( 61 | f"{self.url}/system/mangas/original/{manga.content_id}.jpg", 62 | content_type="content", 63 | ) 64 | 65 | def get_character_preview(self, character: Character): 66 | return get_html( 67 | f"{self.url}/system/characters/" 68 | f"original/{character.content_id}.jpg", 69 | content_type="content", 70 | ) 71 | 72 | def get_genres(self): 73 | url = f"{self.url_api}/genres" 74 | response = get_html(url, headers=self.headers, content_type="json") 75 | if not response: 76 | return [] 77 | 78 | return [ 79 | Genre( 80 | str(i["id"]), 81 | self.CATALOG_ID, 82 | i["name"], 83 | i["russian"], 84 | ) 85 | for i in response 86 | if i["entry_type"] == "Manga" 87 | ] 88 | 89 | def get_orders(self) -> list[Order]: 90 | return [ 91 | Order( 92 | i["value"], 93 | self.CATALOG_ID, 94 | i["name"], 95 | i["russian"], 96 | ) 97 | for i in ShikimoriItems.ORDERS 98 | ] 99 | 100 | def get_relations(self, manga: Manga) -> list[Manga]: 101 | mangas = [] 102 | url = f"{self.url_api}/mangas/{manga.content_id}/related" 103 | response = get_html(url, headers=self.headers, content_type="json") 104 | if response: 105 | for i in response: 106 | if manga_data := i.get("manga"): 107 | mangas.append( 108 | self.setup_manga(manga_data), 109 | ) 110 | return mangas 111 | 112 | def get_characters(self, manga: Manga) -> list[Character]: 113 | characters = [] 114 | url = f"{self.url_api}/mangas/{manga.content_id}/roles" 115 | response = get_html(url, headers=self.headers, content_type="json") 116 | if not response: 117 | return characters 118 | 119 | for i in response: 120 | if roles_data := i.get("roles"): 121 | role = roles_data[0] 122 | if role in ["Supporting", "Main"]: 123 | if data := i.get("character"): 124 | characters.append( 125 | Character( 126 | str(data.get("id")), 127 | self.CATALOG_ID, 128 | data.get("name"), 129 | data.get("russian"), 130 | "", 131 | role, 132 | ), 133 | ) 134 | characters.sort(key=lambda x: x.role) 135 | return characters 136 | 137 | def get_manga_url(self, manga: Manga) -> str: 138 | return f"{self.url}/mangas/{manga.content_id}" 139 | 140 | 141 | __all__ = [ 142 | "ShikimoriBase", 143 | ] 144 | -------------------------------------------------------------------------------- /nlightreader/parsers/combined/shikimori/shikimori_manga.py: -------------------------------------------------------------------------------- 1 | from nlightreader.consts.items import ShikimoriItems 2 | from nlightreader.items import RequestForm 3 | from nlightreader.models import Kind 4 | from nlightreader.parsers.catalogs_base import AbstractMangaCatalog 5 | from nlightreader.parsers.combined.shikimori.shikimori_base import ( 6 | ShikimoriBase, 7 | ) 8 | from nlightreader.utils.utils import get_html 9 | 10 | 11 | class ShikimoriManga(ShikimoriBase, AbstractMangaCatalog): 12 | CATALOG_NAME = "Shikimori(Manga)" 13 | 14 | def search_manga(self, form: RequestForm): 15 | url = f"{self.url_api}/mangas" 16 | params = { 17 | "limit": form.limit, 18 | "search": form.search, 19 | "page": form.page, 20 | "order": form.get_order_id(), 21 | "genre": ",".join(form.get_genre_ids()), 22 | "kind": ",".join(form.get_kind_ids()), 23 | } 24 | response = get_html( 25 | url, 26 | headers=self.headers, 27 | params=params, 28 | content_type="json", 29 | ) 30 | 31 | mangas = [] 32 | if response: 33 | for i in response: 34 | mangas.append(self.setup_manga(i)) 35 | return mangas 36 | 37 | def get_kinds(self): 38 | return [ 39 | Kind( 40 | i["value"], 41 | self.CATALOG_ID, 42 | i["name"], 43 | i["russian"], 44 | ) 45 | for i in ShikimoriItems.KINDS 46 | ] 47 | 48 | 49 | __all__ = [ 50 | "ShikimoriManga", 51 | ] 52 | -------------------------------------------------------------------------------- /nlightreader/parsers/combined/shikimori/shikimori_ranobe.py: -------------------------------------------------------------------------------- 1 | from nlightreader.items import RequestForm 2 | from nlightreader.parsers.catalogs_base import AbstractRanobeCatalog 3 | from nlightreader.parsers.combined.shikimori.shikimori_base import ( 4 | ShikimoriBase, 5 | ) 6 | from nlightreader.utils.utils import get_html 7 | 8 | 9 | class ShikimoriRanobe(ShikimoriBase, AbstractRanobeCatalog): 10 | CATALOG_NAME = "Shikimori(Ranobe)" 11 | 12 | def search_manga(self, form: RequestForm): 13 | url = f"{self.url_api}/ranobe" 14 | params = { 15 | "limit": form.limit, 16 | "search": form.search, 17 | "page": form.page, 18 | "order": form.get_order_id(), 19 | "genre": ",".join(form.get_genre_ids()), 20 | "kind": ",".join(form.get_kind_ids()), 21 | } 22 | response = get_html( 23 | url, 24 | headers=self.headers, 25 | params=params, 26 | content_type="json", 27 | ) 28 | 29 | mangas = [] 30 | if response: 31 | for i in response: 32 | mangas.append(self.setup_manga(i)) 33 | return mangas 34 | 35 | 36 | __all__ = [ 37 | "ShikimoriRanobe", 38 | ] 39 | -------------------------------------------------------------------------------- /nlightreader/parsers/hentai_manga/__init__.py: -------------------------------------------------------------------------------- 1 | from .allhentai_hmanga import AllHentai 2 | from .nhentai_hmanga import NHentai 3 | -------------------------------------------------------------------------------- /nlightreader/parsers/hentai_manga/allhentai_hmanga.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | 3 | from nlightreader.consts.enums import Nl 4 | from nlightreader.consts.urls import URL_ALLHENTAI 5 | from nlightreader.exceptions import parser_content_exc 6 | from nlightreader.models import Chapter, Image, Manga 7 | from nlightreader.parsers.catalogs_base import AbstractHentaiMangaCatalog 8 | from nlightreader.utils.utils import get_html, make_request 9 | 10 | 11 | class AllHentai(AbstractHentaiMangaCatalog): 12 | CATALOG_ID = 8 13 | CATALOG_NAME = "AllHentai" 14 | 15 | def __init__(self): 16 | super().__init__() 17 | self.url = URL_ALLHENTAI 18 | 19 | def search_manga(self, form): 20 | url = f"{self.url}/search" 21 | if not form.search: 22 | raise parser_content_exc.RequestsParamsError( 23 | "Search field is empty", 24 | ) 25 | params = { 26 | "q": form.search, 27 | "+": "Искать!", 28 | "fast-filter": "CREATION", 29 | } 30 | response = make_request( 31 | url, 32 | "POST", 33 | headers=self.headers, 34 | data=params, 35 | content_type="text", 36 | ) 37 | mangas = [] 38 | if response: 39 | soup = BeautifulSoup(response, "html.parser") 40 | html_items = soup.findAll("div", class_="tile") 41 | for i in html_items: 42 | manga_desc = i.find("div", class_="desc") 43 | base_info = manga_desc.find("a") 44 | manga_id = base_info.get("href") 45 | name = base_info.get("title") 46 | if manga_id and name: 47 | mangas.append( 48 | Manga(manga_id, self.CATALOG_ID, name, ""), 49 | ) 50 | return mangas 51 | 52 | def get_chapters(self, manga: Manga): 53 | url = f"{self.url}/{manga.content_id}" 54 | response = get_html(url, headers=self.headers, content_type="text") 55 | 56 | chapters = [] 57 | if not response: 58 | return chapters 59 | 60 | soup = BeautifulSoup(response, "html.parser") 61 | chapters_list_item = soup.find("div", id="chapters-list") 62 | for chapter_item in chapters_list_item.findAll( 63 | "tr", 64 | class_="item-row", 65 | ): 66 | volume: str = chapter_item.get("data-vol") 67 | chapter_num: str = chapter_item.get("data-num") 68 | if chapter_num.isdigit(): 69 | chapter_as_num = int(chapter_num) / 10 70 | if chapter_as_num.is_integer(): 71 | chapter_as_num = int(chapter_as_num) 72 | chapter_num = str(chapter_as_num) 73 | 74 | chapter = Chapter( 75 | manga.content_id, 76 | self.CATALOG_ID, 77 | volume, 78 | chapter_num, 79 | "", 80 | Nl.Language.ru, 81 | ) 82 | chapters.append(chapter) 83 | return chapters 84 | 85 | def get_images(self, manga: Manga, chapter: Chapter): 86 | return [] 87 | 88 | def get_image(self, image: Image): 89 | return 90 | 91 | def get_preview(self, manga: Manga): 92 | url = f"{self.url}/{manga.content_id}" 93 | response = get_html(url, headers=self.headers, content_type="text") 94 | if not response: 95 | return None 96 | soup = BeautifulSoup(response, "html.parser") 97 | html_item = soup.find("img", class_="") 98 | if not (html_item and (img_src := html_item.get("src"))): 99 | return None 100 | return get_html( 101 | img_src, 102 | content_type="content", 103 | headers=self.headers, 104 | ) 105 | 106 | def get_manga_url(self, manga: Manga) -> str: 107 | return f"{self.url}/{manga.content_id}" 108 | 109 | 110 | __all__ = [ 111 | "AllHentai", 112 | ] 113 | -------------------------------------------------------------------------------- /nlightreader/parsers/hentai_manga/nhentai_hmanga.py: -------------------------------------------------------------------------------- 1 | import validators 2 | from bs4 import BeautifulSoup, element 3 | 4 | from nlightreader.consts.urls import URL_NHENTAI 5 | from nlightreader.exceptions import parser_content_exc 6 | from nlightreader.models import Chapter, Image, Manga 7 | from nlightreader.parsers.catalogs_base import AbstractHentaiMangaCatalog 8 | from nlightreader.utils.utils import get_html 9 | 10 | 11 | class NHentai(AbstractHentaiMangaCatalog): 12 | CATALOG_ID = 7 13 | CATALOG_NAME = "NHentai" 14 | 15 | def __init__(self): 16 | super().__init__() 17 | self.url = URL_NHENTAI 18 | 19 | def search_manga(self, form): 20 | url = f"{self.url}/search" 21 | if not form.search: 22 | raise parser_content_exc.RequestsParamsError( 23 | "Search field is empty", 24 | ) 25 | params = { 26 | "page": form.page, 27 | "q": form.search, 28 | } 29 | response = get_html( 30 | url, 31 | headers=self.headers, 32 | params=params, 33 | content_type="text", 34 | ) 35 | 36 | mangas = [] 37 | if not response: 38 | return mangas 39 | 40 | soup = BeautifulSoup(response, "html.parser") 41 | html_items = soup.findAll("div", class_="gallery") 42 | for i in html_items: 43 | caption_tag = i.find("div", class_="caption") 44 | if caption_tag is not None: 45 | name = i.find("div", class_="caption").text 46 | cover_tag: element.Tag = i.find("a", {"class": "cover"}) 47 | if cover_tag is not None: 48 | manga_id = cover_tag["href"].split("/")[-2] 49 | if not manga_id: 50 | continue 51 | 52 | manga = Manga( 53 | manga_id, 54 | self.CATALOG_ID, 55 | name, 56 | "", 57 | ) 58 | 59 | if (noscript_img_tag := cover_tag.find("noscript")) and ( 60 | img_tag := noscript_img_tag.find("img") 61 | ): 62 | src = img_tag.get("src") 63 | if validators.url(src): 64 | manga.preview_url = src 65 | mangas.append(manga) 66 | return mangas 67 | 68 | def get_chapters(self, manga: Manga): 69 | return [ 70 | Chapter( 71 | manga.content_id, 72 | self.CATALOG_ID, 73 | "1", 74 | "1", 75 | "", 76 | ), 77 | ] 78 | 79 | def get_images(self, manga: Manga, chapter: Chapter): 80 | url = f"{self.url}/g/{manga.content_id}" 81 | images = [] 82 | response = get_html(url, headers=self.headers, content_type="text") 83 | if response: 84 | soup = BeautifulSoup(response, "html.parser") 85 | html_items = soup.findAll("a", class_="gallerythumb") 86 | for i in html_items: 87 | img_tag = i.find("img") 88 | img_url: str = img_tag.get("data-src") 89 | if not validators.url(img_url): 90 | continue 91 | images.append(Image("", html_items.index(i) + 1, img_url)) 92 | return images 93 | 94 | def get_image(self, image: Image): 95 | img_request_headers = self.headers | { 96 | "Referer": URL_NHENTAI, 97 | } 98 | return get_html( 99 | image.url, 100 | headers=img_request_headers, 101 | content_type="content", 102 | ) 103 | 104 | def get_preview(self, manga: Manga): 105 | return get_html( 106 | manga.preview_url, 107 | headers=self.headers, 108 | content_type="content", 109 | ) 110 | 111 | def get_manga_url(self, manga: Manga) -> str: 112 | return f"{self.url}/g/{manga.content_id}" 113 | 114 | 115 | __all__ = [ 116 | "NHentai", 117 | ] 118 | -------------------------------------------------------------------------------- /nlightreader/parsers/local_library.py: -------------------------------------------------------------------------------- 1 | from nlightreader.items import RequestForm 2 | from nlightreader.models import Manga 3 | from nlightreader.utils.database import Database 4 | 5 | 6 | class LocalLibrary: 7 | CATALOG_NAME = "LocalLib" 8 | 9 | def __init__(self): 10 | self.db: Database = Database() 11 | 12 | def search_manga(self, params: RequestForm) -> list[Manga]: 13 | return self.db.get_manga_library(params.lib_list) 14 | 15 | 16 | __all__ = [ 17 | "LocalLibrary", 18 | ] 19 | -------------------------------------------------------------------------------- /nlightreader/parsers/manga/Lib.py: -------------------------------------------------------------------------------- 1 | from nlightreader.consts.items import MangaLibItems 2 | from nlightreader.consts.urls import URL_SLASHLIB 3 | from nlightreader.models import Manga 4 | from nlightreader.parsers.catalogs_base import AbstractMangaCatalog 5 | from nlightreader.utils.utils import get_html 6 | 7 | 8 | class LibBase(AbstractMangaCatalog): 9 | def __init__(self): 10 | super().__init__() 11 | self.url = None 12 | self.items = MangaLibItems 13 | 14 | def get_preview(self, manga: Manga): 15 | headers = self.headers | {"Referer": f"{self.url}/"} 16 | return get_html( 17 | manga.preview_url, 18 | headers=headers, 19 | content_type="content", 20 | ) 21 | 22 | def get_manga_url(self, manga: Manga): 23 | return f"{self.url}/{manga.content_id}" 24 | 25 | 26 | class SlashLib(LibBase): 27 | CATALOG_NAME = "SlashLib(Legacy)" 28 | CATALOG_ID = 9 29 | 30 | def __init__(self): 31 | super().__init__() 32 | self.url = URL_SLASHLIB 33 | 34 | 35 | __all__ = [ 36 | "LibBase", 37 | "SlashLib", 38 | ] 39 | -------------------------------------------------------------------------------- /nlightreader/parsers/manga/__init__.py: -------------------------------------------------------------------------------- 1 | from .desu_manga import Desu 2 | from .Lib import SlashLib 3 | from .mangadex_manga import MangaDex, MangaDexLib 4 | from .remanga_manga import Remanga 5 | -------------------------------------------------------------------------------- /nlightreader/parsers/manga/desu_manga.py: -------------------------------------------------------------------------------- 1 | from nlightreader.consts.enums import Nl 2 | from nlightreader.consts.items import DesuItems 3 | from nlightreader.consts.urls import DESU_HEADERS, URL_DESU, URL_DESU_API 4 | from nlightreader.items import RequestForm 5 | from nlightreader.models import Chapter, Image, Manga 6 | from nlightreader.parsers.catalogs_base import AbstractMangaCatalog 7 | from nlightreader.utils.utils import get_data, get_html 8 | 9 | 10 | class Desu(AbstractMangaCatalog): 11 | CATALOG_ID = 0 12 | CATALOG_NAME = "Desu" 13 | 14 | def __init__(self): 15 | super().__init__() 16 | self.url = URL_DESU 17 | self.url_api = URL_DESU_API 18 | self.headers = DESU_HEADERS 19 | self.items = DesuItems 20 | 21 | def get_manga(self, manga: Manga): 22 | url = f"{self.url_api}/{manga.content_id}" 23 | response = get_html(url, headers=self.headers, content_type="json") 24 | if response: 25 | data = get_data(response, ["response"], {}) 26 | manga.score = data.get("score") 27 | manga.kind = Nl.MangaKind.from_str(data.get("kind")) 28 | manga.volumes = int(data["chapters"].get("last").get("vol")) 29 | manga.chapters = int(data["chapters"]["count"]) 30 | manga.status = Nl.MangaStatus.from_str(data.get("status")) 31 | 32 | manga.add_description( 33 | Nl.Language.undefined, 34 | data.get("description"), 35 | ) 36 | return manga 37 | 38 | def search_manga(self, form: RequestForm): 39 | url = f"{self.url_api}" 40 | params = { 41 | "limit": form.limit, 42 | "search": form.search, 43 | "page": form.page, 44 | "genres": ",".join(form.get_genre_ids()), 45 | "order": form.get_order_id(), 46 | "kinds": ",".join(form.get_kind_ids()), 47 | } 48 | response = get_html( 49 | url, 50 | headers=self.headers, 51 | params=params, 52 | content_type="json", 53 | ) 54 | 55 | mangas = [] 56 | if not response: 57 | return mangas 58 | 59 | for i in get_data(response, ["response"]): 60 | mangas.append( 61 | Manga( 62 | str(i.get("id")), 63 | self.CATALOG_ID, 64 | i.get("name"), 65 | i.get("russian"), 66 | ), 67 | ) 68 | return mangas 69 | 70 | def get_chapters(self, manga: Manga): 71 | url = f"{self.url_api}/{manga.content_id}" 72 | response = get_html(url, headers=self.headers, content_type="json") 73 | chapters = [] 74 | if response: 75 | for i in get_data(response, ["response", "chapters", "list"]): 76 | vol = i.get("vol") 77 | ch = i.get("ch") 78 | vol = str(vol) if vol is not None else vol 79 | ch = str(ch) if ch is not None else ch 80 | chapter = Chapter( 81 | str(i.get("id")), 82 | self.CATALOG_ID, 83 | vol, 84 | ch, 85 | i.get("title"), 86 | Nl.Language.ru, 87 | ) 88 | chapters.append(chapter) 89 | return chapters 90 | 91 | def get_images(self, manga: Manga, chapter: Chapter): 92 | url = f"{self.url_api}/{manga.content_id}/chapter/{chapter.content_id}" 93 | response = get_html(url, headers=self.headers, content_type="json") 94 | images = [] 95 | if response: 96 | for i in get_data(response, ["response", "pages", "list"]): 97 | image_id = str(i.get("id")) 98 | page = i.get("page") 99 | img: str = i.get("img") 100 | img = img.replace("desu.me", "desu.win") 101 | images.append(Image(image_id, page, img)) 102 | return images 103 | 104 | def get_image(self, image: Image): 105 | headers = self.headers | {"Referer": f"{self.url}/"} 106 | return get_html(image.url, headers=headers, content_type="content") 107 | 108 | def get_preview(self, manga: Manga): 109 | return get_html( 110 | f"{self.url}/data/manga/covers/preview/{manga.content_id}.jpg", 111 | content_type="content", 112 | ) 113 | 114 | def get_manga_url(self, manga: Manga) -> str: 115 | return f"{self.url}/manga/{manga.content_id}" 116 | 117 | 118 | __all__ = [ 119 | "Desu", 120 | ] 121 | -------------------------------------------------------------------------------- /nlightreader/parsers/ranobe/__init__.py: -------------------------------------------------------------------------------- 1 | from .ranobehub_ranobe import Ranobehub 2 | from .rulate_ranobe import Erolate, Rulate 3 | -------------------------------------------------------------------------------- /nlightreader/parsers/service/kodik.py: -------------------------------------------------------------------------------- 1 | from keys import KODIK_TOKEN 2 | from nlightreader.utils.utils import get_html 3 | 4 | 5 | class KodikTranslator: 6 | def __init__( 7 | self, 8 | content_id: str, 9 | kodik_url: str, 10 | episodes, 11 | translator: str, 12 | tr_type: str, 13 | ): 14 | self.content_id = content_id 15 | self.kodik_url = kodik_url 16 | self.episodes = int(episodes) 17 | self.translator = translator 18 | self.tr_type = tr_type 19 | 20 | 21 | class Kodik: 22 | URL_API = "https://kodikapi.com" 23 | 24 | @classmethod 25 | def search(cls, shikimori_id) -> list[KodikTranslator]: 26 | translators: list[KodikTranslator] = [] 27 | url = f"{cls.URL_API}/search" 28 | params = { 29 | "token": KODIK_TOKEN, 30 | "shikimori_id": shikimori_id, 31 | } 32 | response = get_html(url, params=params, content_type="json") 33 | if response and (results := response.get("results")): 34 | for data in results: 35 | translators.append( 36 | KodikTranslator( 37 | data["id"], 38 | data["link"], 39 | data["last_episode"] if "last_episode" in data else 1, 40 | data["translation"]["title"], 41 | data["translation"]["type"], 42 | ), 43 | ) 44 | return translators 45 | 46 | 47 | __all__ = [ 48 | "Kodik", 49 | ] 50 | -------------------------------------------------------------------------------- /nlightreader/utils/catalog_manager.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from nlightreader.parsers import ( 4 | AllHentai, 5 | Desu, 6 | Erolate, 7 | LibAnilib, 8 | LibMangalib, 9 | LibRanobelib, 10 | MangaDex, 11 | MangaDexLib, 12 | NHentai, 13 | Ranobehub, 14 | Remanga, 15 | Rulate, 16 | ShikimoriAnime, 17 | ShikimoriBase, 18 | ShikimoriLib, 19 | ShikimoriManga, 20 | ShikimoriRanobe, 21 | SlashLib, 22 | ) 23 | from nlightreader.parsers.catalog import AbstractCatalog 24 | 25 | CATALOGS = { 26 | 0: Desu, 27 | 1: ShikimoriBase, 28 | 2: MangaDex, 29 | 3: Rulate, 30 | 4: Ranobehub, 31 | 5: Erolate, 32 | 6: Remanga, 33 | 7: NHentai, 34 | 8: AllHentai, 35 | 9: SlashLib, 36 | 10: LibMangalib, 37 | 11: ShikimoriAnime, 38 | # 12: 39 | 13: LibRanobelib, 40 | 14: LibAnilib, 41 | } 42 | USER_CATALOGS = [ 43 | Desu, 44 | MangaDex, 45 | Remanga, 46 | ShikimoriManga, 47 | ShikimoriRanobe, 48 | ShikimoriAnime, 49 | LibMangalib, 50 | LibRanobelib, 51 | LibAnilib, 52 | Rulate, 53 | Erolate, 54 | Ranobehub, 55 | NHentai, 56 | AllHentai, 57 | ] 58 | LIB_CATALOGS = {ShikimoriBase: ShikimoriLib, MangaDex: MangaDexLib} 59 | 60 | 61 | def get_catalog_by_id(catalog_id): 62 | if catalog_id not in CATALOGS: 63 | logging.warning(f"Catalog with id {catalog_id} not found.") 64 | return AbstractCatalog() 65 | return CATALOGS[catalog_id]() 66 | 67 | 68 | def get_lib_catalog(base_catalog): 69 | return LIB_CATALOGS.get(base_catalog)() 70 | 71 | 72 | __all__ = [ 73 | "get_catalog_by_id", 74 | "get_lib_catalog", 75 | "USER_CATALOGS", 76 | ] 77 | -------------------------------------------------------------------------------- /nlightreader/utils/config.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from PySide6.QtCore import QLocale 4 | from qfluentwidgets import ( 5 | BoolValidator, 6 | ConfigItem, 7 | ConfigSerializer, 8 | OptionsConfigItem, 9 | OptionsValidator, 10 | QConfig, 11 | qconfig, 12 | ) 13 | 14 | from nlightreader.consts.paths.paths import APP_DATA_PATH 15 | 16 | 17 | class Language(Enum): 18 | RUSSIAN = QLocale(QLocale.Language.Russian) 19 | UKRAINIAN = QLocale(QLocale.Language.Ukrainian) 20 | ENGLISH = QLocale(QLocale.Language.English) 21 | AUTO = QLocale() 22 | 23 | 24 | class LanguageSerializer(ConfigSerializer): 25 | def serialize(self, language): 26 | return language.value.name() if language != Language.AUTO else "Auto" 27 | 28 | def deserialize(self, value: str): 29 | return Language(QLocale(value)) if value != "Auto" else Language.AUTO 30 | 31 | 32 | class Config(QConfig): 33 | theme_mode = OptionsConfigItem( 34 | "MainWindow", 35 | "ThemeMode", 36 | "Auto", 37 | OptionsValidator( 38 | [ 39 | "Light", 40 | "Dark", 41 | "Auto", 42 | ], 43 | ), 44 | ) 45 | dpi_scale = OptionsConfigItem( 46 | "MainWindow", 47 | "DpiScale", 48 | "Auto", 49 | OptionsValidator( 50 | [ 51 | 1, 52 | 1.25, 53 | 1.5, 54 | 1.75, 55 | 2, 56 | "Auto", 57 | ], 58 | ), 59 | restart=True, 60 | ) 61 | language = OptionsConfigItem( 62 | "MainWindow", 63 | "Language", 64 | Language.ENGLISH, 65 | OptionsValidator(Language), 66 | LanguageSerializer(), 67 | restart=True, 68 | ) 69 | check_updates_at_startup = ConfigItem( 70 | "Update", 71 | "CheckUpdateAtStartUp", 72 | True, 73 | BoolValidator(), 74 | ) 75 | enable_kodik_server = ConfigItem( 76 | "Utils", 77 | "EnableKodikServer", 78 | True, 79 | BoolValidator(), 80 | restart=True, 81 | ) 82 | 83 | 84 | cfg = Config() 85 | qconfig.load(APP_DATA_PATH / "config.json", cfg) 86 | 87 | 88 | __all__ = [ 89 | "cfg", 90 | ] 91 | -------------------------------------------------------------------------------- /nlightreader/utils/decorators.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | 4 | def singleton(cls): 5 | """ 6 | A decorator that transforms a class into a singleton. 7 | 8 | Parameters: 9 | cls (type): The class to be transformed. 10 | 11 | Returns: 12 | callable: Wrapped class as a singleton. 13 | """ 14 | instance = [None] 15 | 16 | @wraps(cls) 17 | def wrapper(*args, **kwargs): 18 | if instance[0] is None: 19 | instance[0] = cls(*args, **kwargs) 20 | return instance[0] 21 | 22 | return wrapper 23 | 24 | 25 | __all__ = [ 26 | "singleton", 27 | ] 28 | -------------------------------------------------------------------------------- /nlightreader/utils/html_video.py: -------------------------------------------------------------------------------- 1 | from render_html import render_in_browser 2 | 3 | from nlightreader.models import Chapter, Manga 4 | 5 | 6 | def start_html_video(manga: Manga, chapter: Chapter): 7 | src_url = chapter.__getattribute__("url") 8 | render_in_browser( 9 | f""" 10 | 11 | 12 | {manga.get_name()} 13 | 14 | 15 | 19 | 79 | 80 | 81 | """, 82 | ) 83 | 84 | 85 | __all__ = [ 86 | "start_html_video", 87 | ] 88 | -------------------------------------------------------------------------------- /nlightreader/utils/kodik_server.py: -------------------------------------------------------------------------------- 1 | import json 2 | from http.server import BaseHTTPRequestHandler 3 | 4 | from nlightreader.items import HistoryNote 5 | from nlightreader.utils.database import Database 6 | 7 | 8 | class KodikHTTPRequestHandler(BaseHTTPRequestHandler): 9 | def do_POST(self): 10 | content_length = int(self.headers["Content-Length"]) 11 | post_data = self.rfile.read(content_length) 12 | data = json.loads(post_data) 13 | 14 | manga_id = data.get("manga_id", "") 15 | chapter_id = data.get("chapter_id", "") 16 | is_completed = bool(data.get("is_completed", False)) 17 | 18 | db = Database() 19 | manga = db.get_manga(manga_id) 20 | chapter = db.get_chapter(chapter_id) 21 | note = HistoryNote(chapter, manga, is_completed) 22 | db.add_history_note(note) 23 | 24 | self.send_header("Access-Control-Allow-Origin", "*") 25 | self.send_header("Access-Control-Allow-Methods", "POST") 26 | self.send_header("Access-Control-Allow-Headers", "Content-Type") 27 | 28 | self.send_response(200) 29 | self.end_headers() 30 | 31 | def do_OPTIONS(self): 32 | self.send_response(200) 33 | self.send_header("Access-Control-Allow-Origin", "*") 34 | self.send_header("Access-Control-Allow-Methods", "POST") 35 | self.send_header("Access-Control-Allow-Headers", "Content-Type") 36 | self.end_headers() 37 | 38 | 39 | __all__ = [ 40 | "KodikHTTPRequestHandler", 41 | ] 42 | -------------------------------------------------------------------------------- /nlightreader/utils/text_formatter.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | class TextFormatter: 5 | """ 6 | A class for formatting text with spoilers, character names, and URLs. 7 | 8 | :param text: 9 | The text to be formatted. 10 | show_spoilers (bool, optional): 11 | Whether to show spoiler text. Defaults to False. 12 | 13 | Methods: 14 | replace_spoilers(): 15 | Replaces spoiler tags in the text with HTML span tags. 16 | replace_characters(): 17 | Replaces character tags in the text with HTML span tags. 18 | replace_urls(): 19 | Replaces URL tags in the text with HTML anchor tags. 20 | to_html_text(): 21 | Formats the text and returns it as HTML text. 22 | """ 23 | 24 | def __init__(self, text: str, show_spoilers=False): 25 | self._text = " ".join(text.splitlines()) 26 | self._show_spoilers = show_spoilers 27 | 28 | def replace_spoilers(self): 29 | for i, spoiler_text in re.findall( 30 | r"\[spoiler=(\w+)](.+?)\[/spoiler]", 31 | self._text, 32 | re.DOTALL, 33 | ): 34 | spoiler = spoiler_text 35 | if not self._show_spoilers: 36 | spoiler = "" 37 | self._text = self._text.replace( 38 | f"[spoiler={i}]{spoiler_text}[/spoiler]", 39 | f'{spoiler}', 40 | ) 41 | for spoiler_text in re.findall( 42 | r"\[spoiler](.+?)\[/spoiler]", 43 | self._text, 44 | re.DOTALL, 45 | ): 46 | spoiler = spoiler_text 47 | if not self._show_spoilers: 48 | spoiler = "" 49 | self._text = self._text.replace( 50 | f"[spoiler]{spoiler_text}[/spoiler]", 51 | f'{spoiler}', 52 | ) 53 | 54 | def replace_characters(self): 55 | for ch_id, name in re.findall( 56 | r"\[character=(\w+)](.+?)\[/character]", 57 | self._text, 58 | re.DOTALL, 59 | ): 60 | self._text = self._text.replace( 61 | f"[character={ch_id}]{name}[/character]", 62 | f'{name}', 63 | ) 64 | for ch_id, name in re.findall( 65 | r"\[character=(\w+) (.+?)]", 66 | self._text, 67 | re.DOTALL, 68 | ): 69 | self._text = self._text.replace( 70 | f"[character={ch_id} {name}]", 71 | f'{name}', 72 | ) 73 | 74 | def replace_urls(self): 75 | for url, url_text in re.findall( 76 | r"\[url=(.+?)](.+?)\[/url]", 77 | self._text, 78 | re.DOTALL, 79 | ): 80 | self._text = self._text.replace( 81 | f"[url={url}]{url_text}[/url]", 82 | f'' 83 | f'{url_text}', 85 | ) 86 | 87 | def to_html_text(self) -> str: 88 | self.replace_urls() 89 | self.replace_characters() 90 | self.replace_spoilers() 91 | return self._text 92 | 93 | 94 | def description_to_html(text: str, show_spoilers=False) -> str: 95 | if not text: 96 | return "" 97 | return TextFormatter(text, show_spoilers).to_html_text() 98 | 99 | 100 | __all__ = [ 101 | "TextFormatter", 102 | "description_to_html", 103 | ] 104 | -------------------------------------------------------------------------------- /nlightreader/utils/threads.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | from typing import Callable 3 | 4 | from PySide6.QtCore import ( 5 | QObject, 6 | QRunnable, 7 | QThread, 8 | QThreadPool, 9 | Signal, 10 | Slot, 11 | ) 12 | 13 | 14 | class Signals(QObject): 15 | error = Signal(Exception) 16 | finished = Signal(object) 17 | 18 | 19 | class NlThread: 20 | def __init__( 21 | self, 22 | target: Callable, 23 | args=(), 24 | kwargs=None, 25 | *, 26 | callback=None, 27 | error_callback=None, 28 | ): 29 | super().__init__() 30 | if kwargs is None: 31 | kwargs = {} 32 | self._target = target 33 | self._args = args 34 | self._kwargs = kwargs 35 | self.signals = Signals() 36 | if callback: 37 | self.signals.finished.connect(callback) 38 | if error_callback: 39 | self.signals.error.connect(error_callback) 40 | 41 | @Slot() 42 | def run(self): 43 | try: 44 | result = self._target(*self._args, **self._kwargs) 45 | except Exception as e: 46 | traceback.print_exc() 47 | self.signals.error.emit(e) 48 | else: 49 | self.signals.finished.emit(result) 50 | 51 | 52 | class Worker(NlThread, QRunnable): 53 | """ 54 | Initializes a new `Runnable` instance. 55 | 56 | :param target: 57 | A callable object representing 58 | the target function to run in the thread. 59 | :param args: 60 | An optional tuple or list containing 61 | the arguments to pass to the target function. 62 | Defaults to an empty tuple. 63 | :param kwargs: 64 | An optional dictionary containing 65 | keyword arguments to pass to the target function. 66 | Defaults to an empty dictionary. 67 | :param callback: 68 | An optional callable object to invoke 69 | when the thread finishes running. Defaults to None. 70 | """ 71 | 72 | def __init__( 73 | self, 74 | target: Callable, 75 | args=(), 76 | kwargs=None, 77 | *, 78 | callback=None, 79 | error_callback=None, 80 | ): 81 | super().__init__( 82 | target, 83 | args, 84 | kwargs, 85 | callback=callback, 86 | error_callback=error_callback, 87 | ) 88 | 89 | def start(self, pool=None): 90 | if pool is None: 91 | pool = QThreadPool.globalInstance() 92 | if pool.activeThreadCount() == pool.maxThreadCount(): 93 | return 94 | pool.start(self) 95 | 96 | 97 | class Thread(NlThread, QThread): 98 | """ 99 | Initializes a new `Thread` instance. 100 | 101 | :param target: 102 | A callable object representing 103 | the target function to run in the thread. 104 | :param args: 105 | An optional tuple or list containing 106 | the arguments to pass to the target function. 107 | Defaults to an empty tuple. 108 | :param kwargs: 109 | An optional dictionary containing 110 | keyword arguments to pass to the target function. 111 | Defaults to an empty dictionary. 112 | :param callback: 113 | An optional callable object to invoke 114 | when the thread finishes running. Defaults to None. 115 | """ 116 | 117 | def __init__( 118 | self, 119 | target: Callable, 120 | args=(), 121 | kwargs=None, 122 | *, 123 | callback=None, 124 | error_callback=None, 125 | ): 126 | super().__init__( 127 | target, 128 | args, 129 | kwargs, 130 | callback=callback, 131 | error_callback=error_callback, 132 | ) 133 | 134 | 135 | __all__ = [ 136 | "Worker", 137 | "Thread", 138 | ] 139 | -------------------------------------------------------------------------------- /nlightreader/utils/token.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from nlightreader.consts.paths import TOKEN_PATH 5 | 6 | 7 | class TokenManager: 8 | @staticmethod 9 | def save_token(token: dict, catalog_name: str): 10 | """ 11 | Saves a token dictionary to disk. 12 | 13 | :param token: A dictionary containing 14 | authentication token data. 15 | :param catalog_name: The name of the parser 16 | for which the token is being saved. 17 | """ 18 | path = Path(TOKEN_PATH, catalog_name) 19 | token_file_path = path / "token.json" 20 | if not path.exists(): 21 | path.mkdir(parents=True, exist_ok=True) 22 | with token_file_path.open("w") as f: 23 | f.write(json.dumps(token)) 24 | 25 | @staticmethod 26 | def load_token(catalog_name): 27 | """ 28 | Loads a token dictionary from disk. 29 | 30 | :param catalog_name: 31 | The name of the parser for which the token is being loaded. 32 | :return: 33 | A dictionary containing authentication token data, 34 | or an empty dictionary if no token is found. 35 | """ 36 | path = Path(TOKEN_PATH, catalog_name) 37 | token_file_path = path / "token.json" 38 | if token_file_path.exists(): 39 | with token_file_path.open() as f: 40 | data = json.load(f) 41 | if data: 42 | return data 43 | return {} 44 | 45 | 46 | __all__ = [ 47 | "TokenManager", 48 | ] 49 | -------------------------------------------------------------------------------- /nlightreader/utils/translator.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import QLocale, QTranslator 2 | from PySide6.QtWidgets import QApplication 3 | 4 | 5 | class NlightTranslator(QTranslator): 6 | def __init__(self, locale: QLocale = None, parent=None): 7 | super().__init__(parent=parent) 8 | self.load(locale or QLocale()) 9 | 10 | def load(self, locale: QLocale): 11 | super().load(f":/translations/i18n/{locale.name()}.qm") 12 | 13 | 14 | def translate(context, string): 15 | """ 16 | Translates a string using the current translation context. 17 | 18 | Args: 19 | context: The context in which the string appears. 20 | string: The string to be translated. 21 | 22 | Returns: 23 | A translated version of the input string. 24 | """ 25 | return QApplication.translate(context, string, None) 26 | 27 | 28 | __all__ = [ 29 | "NlightTranslator", 30 | "translate", 31 | ] 32 | -------------------------------------------------------------------------------- /nlightreader/widgets/containers/__init__.py: -------------------------------------------------------------------------------- 1 | from .manga_area import MangaArea 2 | from .text_area import TextArea 3 | -------------------------------------------------------------------------------- /nlightreader/widgets/containers/content_container.py: -------------------------------------------------------------------------------- 1 | from enum import Enum, unique 2 | 3 | from PySide6.QtWidgets import QWidget 4 | from qfluentwidgets import ( 5 | FluentIcon, 6 | IndeterminateProgressRing, 7 | TransparentPushButton, 8 | ) 9 | 10 | from nlightreader.utils.translator import translate 11 | 12 | 13 | @unique 14 | class ContentContainerState(Enum): 15 | empty = 0 16 | show_content = 1 17 | fetch_content = 2 18 | no_content = 3 19 | fetch_error = 4 20 | 21 | 22 | class AbstractContentContainer: 23 | def __init__(self): 24 | self._progress_ring = IndeterminateProgressRing() 25 | self._progress_ring.setVisible(False) 26 | 27 | self._fetch_error_widget = TransparentPushButton( 28 | FluentIcon.CLOUD, 29 | translate("Message", "No connection"), 30 | ) 31 | self._fetch_error_widget.setEnabled(False) 32 | self._fetch_error_widget.setVisible(False) 33 | 34 | self._no_content_error_widget = TransparentPushButton( 35 | FluentIcon.CLOUD, 36 | translate("Message", "Nothing found"), 37 | ) 38 | self._no_content_error_widget.setEnabled(False) 39 | self._no_content_error_widget.setVisible(False) 40 | 41 | self._content_widget = None 42 | 43 | self._state = ContentContainerState.empty 44 | 45 | def install(self, parent): 46 | self.get_content_widget().layout().addWidget( 47 | self._no_content_error_widget, 48 | ) 49 | self.get_content_widget().layout().addWidget( 50 | self._fetch_error_widget, 51 | ) 52 | self.get_content_widget().layout().addWidget( 53 | self._progress_ring, 54 | ) 55 | parent.addWidget(self) 56 | 57 | def _reset_area(self) -> None: 58 | raise NotImplementedError 59 | 60 | def set_state(self, state: ContentContainerState): 61 | state_objects = ( 62 | self._progress_ring, 63 | self._no_content_error_widget, 64 | self._fetch_error_widget, 65 | self._content_widget, 66 | ) 67 | if not isinstance(state, ContentContainerState): 68 | raise TypeError( 69 | f"state must be ContentContainerState got {type(state)}", 70 | ) 71 | 72 | self._state = state 73 | 74 | state_obj = None 75 | if state == ContentContainerState.empty: 76 | state_obj = None 77 | 78 | elif state == ContentContainerState.show_content: 79 | state_obj = self._content_widget 80 | 81 | elif state == ContentContainerState.fetch_content: 82 | state_obj = self._progress_ring 83 | 84 | elif state == ContentContainerState.no_content: 85 | state_obj = self._no_content_error_widget 86 | 87 | elif state == ContentContainerState.fetch_error: 88 | state_obj = self._fetch_error_widget 89 | 90 | [ 91 | obj.setVisible( 92 | obj == state_obj, 93 | ) 94 | for obj in state_objects 95 | if obj is not None 96 | ] 97 | 98 | def set_content(self, content) -> None: 99 | raise NotImplementedError 100 | 101 | def get_content_widget(self) -> QWidget: 102 | raise NotImplementedError 103 | 104 | 105 | __all__ = [ 106 | "AbstractContentContainer", 107 | "ContentContainerState", 108 | ] 109 | -------------------------------------------------------------------------------- /nlightreader/widgets/containers/image_area.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import QSize, Qt 2 | from PySide6.QtGui import QPixmap 3 | from PySide6.QtWidgets import QWidget 4 | 5 | from data.ui.containers.image_area import Ui_Form 6 | from nlightreader.widgets.containers.content_container import ( 7 | AbstractContentContainer, 8 | ) 9 | 10 | 11 | class ImageArea(QWidget, AbstractContentContainer): 12 | def __init__(self): 13 | super().__init__() 14 | self.ui = Ui_Form() 15 | self.ui.setupUi(self) 16 | self.setStyleSheet( 17 | """ 18 | QWidget {background: transparent;} 19 | QScrollArea {border: none;} 20 | """, 21 | ) 22 | self._content_widget = self.ui.img_lbl 23 | self.__image_pixmap = None 24 | 25 | def resizeEvent(self, event): 26 | super().resizeEvent(event) 27 | if (self.__image_pixmap is None) or (event.oldSize() == event.size()): 28 | return 29 | view_w = self.ui.scrollArea.viewport().width() 30 | self.ui.img_lbl.setFixedWidth(view_w) 31 | self.ui.scrollAreaWidgetContents.setFixedWidth(view_w) 32 | self.ui.scrollAreaWidgetContents.resize( 33 | self.ui.scrollArea.viewport().size(), 34 | ) 35 | self.__update_image() 36 | 37 | def _reset_area(self): 38 | self.ui.img_lbl.clear() 39 | self.ui.scrollArea.verticalScrollBar().setValue(0) 40 | self.ui.scrollArea.horizontalScrollBar().setValue(0) 41 | view_w = self.ui.scrollArea.viewport().width() 42 | self.ui.img_lbl.setFixedWidth(view_w) 43 | self.ui.scrollAreaWidgetContents.setFixedWidth(view_w) 44 | self.ui.scrollAreaWidgetContents.resize( 45 | self.ui.scrollArea.viewport().size(), 46 | ) 47 | 48 | def _resize_pixmap(self, pixmap: QPixmap) -> QPixmap: 49 | if pixmap is None or pixmap.isNull(): 50 | return QPixmap() 51 | if 0.5 < pixmap.width() / pixmap.height() < 2: 52 | viewport_size = self.ui.scrollArea.viewport().size() 53 | self.ui.scrollArea.setVerticalScrollBarPolicy( 54 | Qt.ScrollBarPolicy.ScrollBarAlwaysOff, 55 | ) 56 | else: 57 | viewport_size = QSize( 58 | self.ui.scrollArea.viewport().width(), 59 | pixmap.height(), 60 | ) 61 | self.ui.scrollArea.setVerticalScrollBarPolicy( 62 | Qt.ScrollBarPolicy.ScrollBarAsNeeded, 63 | ) 64 | device_pixel_ratio = self.devicePixelRatio() 65 | scaled_pixmap = pixmap.scaled( 66 | viewport_size * device_pixel_ratio, 67 | Qt.AspectRatioMode.KeepAspectRatio, 68 | Qt.TransformationMode.SmoothTransformation, 69 | ) 70 | scaled_pixmap.setDevicePixelRatio(device_pixel_ratio) 71 | return scaled_pixmap 72 | 73 | def __update_image(self): 74 | pixmap = self._resize_pixmap(self.__image_pixmap) 75 | self.ui.img_lbl.setPixmap(pixmap) 76 | 77 | def set_content(self, img_pixmap: QPixmap): 78 | self.__image_pixmap = img_pixmap 79 | self._reset_area() 80 | self.__update_image() 81 | 82 | def get_content_widget(self): 83 | return self.ui.img_lbl.parent() 84 | 85 | 86 | __all__ = [ 87 | "ImageArea", 88 | ] 89 | -------------------------------------------------------------------------------- /nlightreader/widgets/containers/manga_area.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import Qt, QThreadPool 2 | from PySide6.QtWidgets import QGridLayout, QHBoxLayout, QWidget 3 | from qfluentwidgets import ( 4 | ScrollArea, 5 | ) 6 | 7 | from nlightreader.utils.threads import Thread 8 | from nlightreader.widgets.containers.content_container import ( 9 | AbstractContentContainer, 10 | ContentContainerState, 11 | ) 12 | from nlightreader.widgets.items import MangaItem 13 | 14 | 15 | class MangaArea(ScrollArea, AbstractContentContainer): 16 | def __init__(self): 17 | super().__init__() 18 | self.setWidgetResizable(True) 19 | self.setStyleSheet( 20 | """ 21 | QWidget {background: transparent;} 22 | QScrollArea {border: none;} 23 | """, 24 | ) 25 | self._column_count = 5 26 | self._spacing = 12 27 | self._manga_items: list[MangaItem] = [] 28 | 29 | self._scrollAreaWidgetContents = QWidget() 30 | self._scrollAreaWidgetContents.setObjectName( 31 | "scrollAreaWidgetContents", 32 | ) 33 | 34 | self._scroll_layout = QHBoxLayout(self._scrollAreaWidgetContents) 35 | self._scroll_layout.setSpacing(0) 36 | self._scroll_layout.setContentsMargins(0, 0, 0, 0) 37 | 38 | self._content_grid = QGridLayout() 39 | self._content_grid.setSpacing(self._spacing) 40 | self._content_grid.setContentsMargins(0, 0, 24, 0) 41 | self._content_grid.setAlignment(Qt.AlignmentFlag.AlignTop) 42 | 43 | self._scroll_layout.addLayout(self._content_grid) 44 | 45 | self.setWidget(self._scrollAreaWidgetContents) 46 | 47 | self.manga_thread_pool = QThreadPool() 48 | self.manga_thread_pool.setMaxThreadCount(self._column_count) 49 | self._set_images_thread = Thread(target=self.partial_image_addition) 50 | 51 | def resizeEvent(self, arg__1): 52 | super().resizeEvent(arg__1) 53 | if arg__1.oldSize().width() != arg__1.size().width(): 54 | self._scrollAreaWidgetContents.setFixedWidth(arg__1.size().width()) 55 | if self._state == ContentContainerState.show_content: 56 | self.update_items() 57 | 58 | def add_items(self, items: list[MangaItem]): 59 | if self._state != ContentContainerState.show_content: 60 | raise PermissionError("this method is now available in this state") 61 | i, j = 0, 0 62 | for item in items: 63 | self._manga_items.append(item) 64 | self._content_grid.addWidget( 65 | item, 66 | i, 67 | j, 68 | Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignTop, 69 | ) 70 | j += 1 71 | if j == self._column_count: 72 | j = 0 73 | i += 1 74 | self._set_images_thread.start() 75 | 76 | def partial_image_addition(self): 77 | for item in self._manga_items: 78 | if ( 79 | self.manga_thread_pool.activeThreadCount() 80 | == self.manga_thread_pool.maxThreadCount() 81 | ): 82 | self.manga_thread_pool.waitForDone() 83 | item.update_image() 84 | 85 | def delete_items(self): 86 | self._set_images_thread.terminate() 87 | self.verticalScrollBar().setValue(0) 88 | for item in self._manga_items: 89 | self._content_grid.removeWidget(item) 90 | item.deleteLater() 91 | self._manga_items.clear() 92 | 93 | def update_items(self): 94 | if self._state != ContentContainerState.show_content: 95 | raise PermissionError("this method is now available in this state") 96 | size = ( 97 | self.size().width() 98 | - (self._content_grid.horizontalSpacing() * self._column_count) 99 | ) // self._column_count 100 | [item.set_size(size) for item in self._manga_items] 101 | 102 | def get_content_widget(self): 103 | return self._scrollAreaWidgetContents 104 | 105 | 106 | __all__ = [ 107 | "MangaArea", 108 | ] 109 | -------------------------------------------------------------------------------- /nlightreader/widgets/containers/text_area.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import Slot 2 | from PySide6.QtWidgets import QWidget 3 | 4 | from data.ui.containers.text_area import Ui_Form 5 | from nlightreader.widgets.containers.content_container import ( 6 | AbstractContentContainer, 7 | ) 8 | 9 | 10 | class TextArea(QWidget, AbstractContentContainer): 11 | def __init__(self): 12 | super().__init__() 13 | self.ui = Ui_Form() 14 | self.ui.setupUi(self) 15 | self.ui.size_slider.valueChanged.connect(self.__update_text_size) 16 | self._content_widget = self.ui.text_browser 17 | 18 | @Slot() 19 | def __update_text_size(self): 20 | font = self.ui.text_browser.font() 21 | font.setPointSize(self.ui.size_slider.value()) 22 | self.ui.text_browser.setFont(font) 23 | 24 | def _reset_area(self): 25 | self.ui.text_browser.clear() 26 | 27 | def set_content(self, content: str): 28 | self._reset_area() 29 | self.ui.text_browser.setHtml(content) 30 | 31 | def get_content_widget(self) -> QWidget: 32 | return self.ui.text_browser.parent() 33 | 34 | 35 | __all__ = [ 36 | "TextArea", 37 | ] 38 | -------------------------------------------------------------------------------- /nlightreader/widgets/contexts/__init__.py: -------------------------------------------------------------------------------- 1 | from .history_note_menu import HistoryNoteMenu 2 | from .library_manga_menu import LibraryMangaMenu 3 | from .read_mark_menu import ReadMarkMenu 4 | -------------------------------------------------------------------------------- /nlightreader/widgets/contexts/history_note_menu.py: -------------------------------------------------------------------------------- 1 | from qfluentwidgets import Action, FluentIcon, RoundMenu 2 | 3 | from nlightreader.utils.translator import translate 4 | 5 | 6 | class HistoryNoteMenu(RoundMenu): 7 | def __init__(self): 8 | super().__init__() 9 | self.set_as_read = Action( 10 | FluentIcon.ACCEPT_MEDIUM, 11 | translate("Menu", "Mark as read"), 12 | ) 13 | self.remove_all = Action( 14 | FluentIcon.REMOVE, 15 | translate("Menu", "Remove all"), 16 | ) 17 | 18 | def set_mode(self, mode: int): 19 | """ 20 | Sets the mode of this object and 21 | adds the appropriate actions based on the mode. 22 | 23 | Args: 24 | mode (int): The mode to set. Valid values are 0, 1. 25 | 26 | Raises: 27 | ValueError: If an invalid mode is provided. 28 | """ 29 | actions = { 30 | 0: [self.remove_all, self.set_as_read], 31 | 1: [self.remove_all], 32 | } 33 | if mode not in actions: 34 | raise ValueError("Invalid mode: must be 0 or 1") 35 | self.addActions(actions[mode]) 36 | 37 | 38 | __all__ = [ 39 | "HistoryNoteMenu", 40 | ] 41 | -------------------------------------------------------------------------------- /nlightreader/widgets/contexts/library_manga_menu.py: -------------------------------------------------------------------------------- 1 | from qfluentwidgets import Action, FluentIcon, RoundMenu 2 | 3 | from nlightreader.utils.translator import translate 4 | 5 | 6 | class LibraryMangaMenu(RoundMenu): 7 | def __init__(self): 8 | super().__init__() 9 | self.add_to_lib = Action( 10 | FluentIcon.ADD_TO, 11 | translate("Menu", "Add to Library"), 12 | ) 13 | self.remove_from_lib = Action( 14 | FluentIcon.REMOVE_FROM, 15 | translate("Menu", "Remove from library"), 16 | ) 17 | self.open_in_browser = Action( 18 | FluentIcon.LINK, 19 | translate("Menu", "Open in browser"), 20 | ) 21 | self.remove_files = Action( 22 | FluentIcon.DELETE, 23 | translate("Menu", "Clear local files"), 24 | ) 25 | self.open_local_files = Action( 26 | FluentIcon.FOLDER, 27 | translate("Menu", "Open local files"), 28 | ) 29 | 30 | def set_mode(self, mode: int): 31 | """ 32 | Sets the mode of this object and 33 | adds the appropriate actions based on the mode. 34 | 35 | Args: 36 | mode: The mode to set. Valid values are 0, 1, 2. 37 | 38 | Raises: 39 | ValueError: If an invalid mode is provided. 40 | """ 41 | actions = { 42 | 0: [ 43 | self.open_in_browser, 44 | self.add_to_lib, 45 | self.open_local_files, 46 | self.remove_files, 47 | ], 48 | 1: [ 49 | self.open_in_browser, 50 | self.remove_from_lib, 51 | self.open_local_files, 52 | self.remove_files, 53 | ], 54 | 2: [ 55 | self.open_in_browser, 56 | self.open_local_files, 57 | self.remove_files, 58 | ], 59 | } 60 | if mode not in actions: 61 | raise ValueError("Invalid mode: must be 0, 1 or 2") 62 | self.addActions(actions.get(mode, [])) 63 | 64 | 65 | __all__ = [ 66 | "LibraryMangaMenu", 67 | ] 68 | -------------------------------------------------------------------------------- /nlightreader/widgets/contexts/read_mark_menu.py: -------------------------------------------------------------------------------- 1 | from qfluentwidgets import Action, FluentIcon, RoundMenu 2 | 3 | from nlightreader.utils.translator import translate 4 | 5 | 6 | class ReadMarkMenu(RoundMenu): 7 | def __init__(self): 8 | super().__init__() 9 | self.set_as_read = Action( 10 | FluentIcon.ACCEPT_MEDIUM, 11 | translate("Menu", "Mark as read"), 12 | ) 13 | self.set_as_read_all = Action( 14 | FluentIcon.COMPLETED, 15 | translate("Menu", "Mark as read all previous"), 16 | ) 17 | self.remove_read_state = Action( 18 | FluentIcon.REMOVE, 19 | translate("Menu", "Remove read mark"), 20 | ) 21 | 22 | def set_mode(self, mode: int): 23 | """ 24 | Sets the mode of this object and 25 | adds the appropriate actions based on the mode. 26 | 27 | Args: 28 | mode (int): The mode to set. Valid values are 0, 1, 2. 29 | 30 | Raises: 31 | ValueError: If an invalid mode is provided. 32 | """ 33 | actions = { 34 | 0: [self.set_as_read, self.set_as_read_all], 35 | 1: [self.remove_read_state, self.set_as_read_all], 36 | 2: [ 37 | self.set_as_read, 38 | self.remove_read_state, 39 | self.set_as_read_all, 40 | ], 41 | } 42 | if mode not in actions: 43 | raise ValueError("Invalid mode: must be 0, 1 or 2") 44 | self.addActions(actions.get(mode, [])) 45 | 46 | 47 | __all__ = [ 48 | "ReadMarkMenu", 49 | ] 50 | -------------------------------------------------------------------------------- /nlightreader/widgets/dialogs/__init__.py: -------------------------------------------------------------------------------- 1 | from .auth_dialog import TokenAuthMessageBox, UserDataAuthMessageBox 2 | from .character_info_dialog import CharacterInfoDialog 3 | from .genres_dialog import GenresDialog 4 | from .rate_dialog import RateDialog 5 | -------------------------------------------------------------------------------- /nlightreader/widgets/dialogs/auth_dialog.py: -------------------------------------------------------------------------------- 1 | import webbrowser 2 | 3 | from qfluentwidgets import ( 4 | LineEdit, 5 | MessageBoxBase, 6 | PasswordLineEdit, 7 | PushButton, 8 | SubtitleLabel, 9 | ) 10 | 11 | 12 | class AbstractAuthDialog(MessageBoxBase): 13 | def __init__(self, catalog, parent): 14 | super().__init__(parent) 15 | self.session = catalog.session 16 | self.widget.setMinimumWidth(350) 17 | 18 | self.titleLabel = SubtitleLabel(self.tr("Authentication"), parent=self) 19 | 20 | self.yesButton.setText(self.tr("Sign in")) 21 | self.yesButton.setEnabled(False) 22 | 23 | self.cancelButton.setText(self.tr("Cancel")) 24 | 25 | self.viewLayout.addWidget(self.titleLabel) 26 | 27 | def verify_user_data(self): 28 | raise NotImplementedError 29 | 30 | def get_user_data(self): 31 | raise NotImplementedError 32 | 33 | 34 | class TokenAuthMessageBox(AbstractAuthDialog): 35 | def __init__(self, catalog, parent): 36 | super().__init__(catalog, parent) 37 | self.getCodeButton = PushButton(self.tr("Get code")) 38 | self.getCodeButton.clicked.connect(self.__open_login_page) 39 | 40 | self.tokenLineEdit = LineEdit(self) 41 | self.tokenLineEdit.setPlaceholderText(self.tr("Authorization code")) 42 | self.tokenLineEdit.setClearButtonEnabled(True) 43 | self.tokenLineEdit.textChanged.connect(self.verify_user_data) 44 | 45 | self.viewLayout.addWidget(self.tokenLineEdit) 46 | self.viewLayout.addWidget(self.getCodeButton) 47 | 48 | def verify_user_data(self): 49 | self.yesButton.setEnabled(bool(self.tokenLineEdit.text())) 50 | 51 | def get_user_data(self): 52 | return { 53 | "token": self.tokenLineEdit.text(), 54 | } 55 | 56 | def __open_login_page(self): 57 | webbrowser.open_new_tab(self.session.get_auth_url()) 58 | 59 | 60 | class UserDataAuthMessageBox(AbstractAuthDialog): 61 | def __init__(self, catalog, parent): 62 | super().__init__(catalog, parent) 63 | self.loginLineEdit = LineEdit(self) 64 | self.loginLineEdit.setPlaceholderText(self.tr("Login")) 65 | self.loginLineEdit.setClearButtonEnabled(True) 66 | self.loginLineEdit.textChanged.connect(self.verify_user_data) 67 | 68 | self.passwordLineEdit = PasswordLineEdit(self) 69 | self.passwordLineEdit.setPlaceholderText(self.tr("Password")) 70 | self.passwordLineEdit.setClearButtonEnabled(True) 71 | self.passwordLineEdit.textChanged.connect(self.verify_user_data) 72 | 73 | self.viewLayout.addWidget(self.loginLineEdit) 74 | self.viewLayout.addWidget(self.passwordLineEdit) 75 | 76 | def verify_user_data(self): 77 | self.yesButton.setEnabled( 78 | bool( 79 | self.loginLineEdit.text(), 80 | ) 81 | and bool( 82 | self.passwordLineEdit.text(), 83 | ), 84 | ) 85 | 86 | def get_user_data(self): 87 | return { 88 | "username": self.loginLineEdit.text(), 89 | "password": self.passwordLineEdit.text(), 90 | } 91 | 92 | 93 | __all__ = [ 94 | "TokenAuthMessageBox", 95 | "UserDataAuthMessageBox", 96 | ] 97 | -------------------------------------------------------------------------------- /nlightreader/widgets/dialogs/character_info_dialog.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import Qt, Slot 2 | from PySide6.QtWidgets import ( 3 | QHBoxLayout, 4 | QLabel, 5 | QSizePolicy, 6 | QSpacerItem, 7 | QVBoxLayout, 8 | ) 9 | from qfluentwidgets import ( 10 | BodyLabel, 11 | MessageBoxBase, 12 | SimpleCardWidget, 13 | SwitchButton, 14 | TextEdit, 15 | ) 16 | 17 | from nlightreader.models import Character 18 | from nlightreader.utils.catalog_manager import get_catalog_by_id 19 | from nlightreader.utils.file_manager import FileManager 20 | from nlightreader.utils.text_formatter import description_to_html 21 | from nlightreader.utils.threads import Worker 22 | 23 | 24 | class CharacterInfoDialog(MessageBoxBase): 25 | def __init__(self, character: Character, parent): 26 | super().__init__(parent) 27 | self.__character = character 28 | self.__catalog = get_catalog_by_id(character.catalog_id) 29 | 30 | self.image_frame = SimpleCardWidget() 31 | self.image_frame_layout = QVBoxLayout(self.image_frame) 32 | self.image_label = QLabel() 33 | self.image_frame_layout.addWidget(self.image_label) 34 | 35 | self.title_frame = SimpleCardWidget() 36 | self.title_frame_layout = QVBoxLayout(self.title_frame) 37 | self.name_label = BodyLabel(self.__character.name) 38 | self.russian_label = BodyLabel(self.__character.russian) 39 | self.title_frame_spacer = QSpacerItem( 40 | 20, 41 | 40, 42 | QSizePolicy.Policy.Minimum, 43 | QSizePolicy.Policy.Expanding, 44 | ) 45 | self.show_spoilers_switch = SwitchButton() 46 | self.show_spoilers_switch.setText(self.tr("Show spoilers")) 47 | self.show_spoilers_switch.setOnText(self.tr("Show spoilers")) 48 | self.show_spoilers_switch.setOffText(self.tr("Show spoilers")) 49 | self.show_spoilers_switch.checkedChanged.connect( 50 | self.update_description, 51 | ) 52 | self.title_frame_layout.addWidget(self.name_label) 53 | self.title_frame_layout.addWidget(self.russian_label) 54 | self.title_frame_layout.addItem(self.title_frame_spacer) 55 | self.title_frame_layout.addWidget(self.show_spoilers_switch) 56 | 57 | self.description_text = TextEdit() 58 | self.description_text.setFocusPolicy( 59 | Qt.FocusPolicy.NoFocus, 60 | ) 61 | self.description_text.setTextInteractionFlags( 62 | Qt.TextInteractionFlag.NoTextInteraction, 63 | ) 64 | 65 | self.row = QHBoxLayout() 66 | self.row.addWidget(self.image_frame) 67 | self.row.addWidget(self.title_frame) 68 | 69 | self.viewLayout.addLayout(self.row) 70 | self.viewLayout.addWidget(self.description_text) 71 | 72 | self.cancelButton.hide() 73 | 74 | self.update_description() 75 | Worker(self.setup_image).start() 76 | 77 | def closeEvent(self, arg__1): 78 | self.deleteLater() 79 | 80 | @Slot() 81 | def update_description(self): 82 | self.description_text.setHtml( 83 | description_to_html( 84 | self.__character.description, 85 | self.show_spoilers_switch.isChecked(), 86 | ), 87 | ) 88 | 89 | def setup_image(self): 90 | self.image_label.setPixmap( 91 | FileManager.get_character_preview( 92 | self.__character, 93 | self.__catalog, 94 | ), 95 | ) 96 | 97 | 98 | __all__ = [ 99 | "CharacterInfoDialog", 100 | ] 101 | -------------------------------------------------------------------------------- /nlightreader/widgets/dialogs/genres_dialog.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QGridLayout 2 | from qfluentwidgets import CheckBox, MessageBoxBase, SubtitleLabel 3 | 4 | from nlightreader.models import Genre 5 | 6 | 7 | class GenresDialogUi(MessageBoxBase): 8 | def __init__(self, genres: dict[Genre, bool], parent=None): 9 | super().__init__(parent) 10 | self.genres_items = {} 11 | self.max_genres_per_row = 5 12 | 13 | self.title_label = SubtitleLabel(self.tr("Genres"), parent=self) 14 | self.genres_layout = QGridLayout() 15 | self._populate_genres(genres) 16 | 17 | self.viewLayout.addWidget(self.title_label) 18 | self.viewLayout.addLayout(self.genres_layout) 19 | 20 | def get_selected_genres(self): 21 | return [ 22 | genre 23 | for checkbox, genre in self.genres_items.items() 24 | if checkbox.isChecked() 25 | ] 26 | 27 | def _populate_genres(self, genres: dict[Genre, bool]): 28 | for index, (genre, selected) in enumerate(genres.items()): 29 | checkbox = CheckBox(genre.get_name()) 30 | checkbox.setChecked(selected) 31 | self.genres_items[checkbox] = genre 32 | row, col = divmod(index, self.max_genres_per_row) 33 | self.genres_layout.addWidget(checkbox, row, col) 34 | 35 | 36 | class GenresDialog: 37 | def __init__(self, parent): 38 | self.parent = parent 39 | self.genres: dict[Genre, bool] = {} 40 | 41 | def set_genres(self, genres: list[Genre]): 42 | self.genres = {genre: False for genre in genres} 43 | 44 | @property 45 | def selected_genres(self): 46 | return [genre for genre, selected in self.genres.items() if selected] 47 | 48 | def show(self): 49 | w = GenresDialogUi(self.genres, parent=self.parent) 50 | if w.exec(): 51 | for genre in w.get_selected_genres(): 52 | self.genres[genre] = True 53 | 54 | def reset_items(self): 55 | for genre in self.genres: 56 | self.genres[genre] = False 57 | 58 | def clear(self): 59 | self.genres.clear() 60 | 61 | 62 | __all__ = [ 63 | "GenresDialog", 64 | ] 65 | -------------------------------------------------------------------------------- /nlightreader/widgets/dialogs/rate_dialog.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import Qt, Slot 2 | from PySide6.QtWidgets import QHBoxLayout 3 | from qfluentwidgets import ( 4 | BodyLabel, 5 | CardWidget, 6 | ComboBox, 7 | HorizontalSeparator, 8 | MessageBoxBase, 9 | PushButton, 10 | SpinBox, 11 | SubtitleLabel, 12 | ) 13 | 14 | from nlightreader.consts.enums import LIB_LISTS, Nl 15 | from nlightreader.models import Manga 16 | from nlightreader.utils.catalog_manager import ( 17 | get_catalog_by_id, 18 | get_lib_catalog, 19 | ) 20 | from nlightreader.utils.translator import translate 21 | 22 | 23 | class RateDialog(MessageBoxBase): 24 | def __init__(self, manga: Manga, parent): 25 | super().__init__(parent) 26 | self.__manga = manga 27 | self.__catalog = get_lib_catalog( 28 | get_catalog_by_id( 29 | self.__manga.catalog_id, 30 | ).__class__, 31 | ) 32 | self.__user_rate = self._fetch_user_rate() 33 | 34 | self.title_label = SubtitleLabel(self.tr("Change rating"), parent=self) 35 | 36 | self.chapters_frame = CardWidget() 37 | self.chapters_frame_layout = QHBoxLayout(self.chapters_frame) 38 | self.chapters_frame_label = BodyLabel( 39 | self.tr("Chapters read"), 40 | parent=self.chapters_frame, 41 | ) 42 | self.chapters_frame_separator = HorizontalSeparator( 43 | self.chapters_frame, 44 | ) 45 | self.chapters_count_spin = SpinBox(parent=self.chapters_frame) 46 | self.chapters_count_spin.setMaximum(999) 47 | self.chapters_frame_layout.addWidget(self.chapters_frame_label) 48 | self.chapters_frame_layout.addWidget(self.chapters_frame_separator) 49 | self.chapters_frame_layout.addWidget(self.chapters_count_spin) 50 | 51 | self.score_frame = CardWidget() 52 | self.score_frame_layout = QHBoxLayout(self.score_frame) 53 | self.score_frame_label = BodyLabel( 54 | self.tr("Rating"), 55 | parent=self.score_frame, 56 | ) 57 | self.score_frame_separator = HorizontalSeparator(self.score_frame) 58 | self.score_spin = SpinBox() 59 | self.score_spin.setMaximum(10) 60 | self.score_frame_layout.addWidget(self.score_frame_label) 61 | self.score_frame_layout.addWidget(self.score_frame_separator) 62 | self.score_frame_layout.addWidget(self.score_spin) 63 | 64 | self.lib_list_frame = CardWidget() 65 | self.lib_list_frame_layout = QHBoxLayout(self.lib_list_frame) 66 | self.lib_list_frame_label = BodyLabel( 67 | self.tr("List"), 68 | parent=self.lib_list_frame, 69 | ) 70 | self.lib_list_frame_separator = HorizontalSeparator( 71 | self.lib_list_frame, 72 | ) 73 | self.lib_list_combo = ComboBox() 74 | self.lib_list_combo.addItems( 75 | [translate("Form", i.capitalize()) for i in LIB_LISTS], 76 | ) 77 | self.lib_list_frame_layout.addWidget(self.lib_list_frame_label) 78 | self.lib_list_frame_layout.addWidget(self.lib_list_frame_separator) 79 | self.lib_list_frame_layout.addWidget(self.lib_list_combo) 80 | 81 | self.delete_rate_button = PushButton() 82 | self.delete_rate_button.clicked.connect(self._delete_user_rate) 83 | self.delete_rate_button.setText(self.tr("Delete")) 84 | self.buttonLayout.addWidget( 85 | self.delete_rate_button, 86 | 1, 87 | Qt.AlignmentFlag.AlignVCenter, 88 | ) 89 | 90 | self.viewLayout.addWidget(self.title_label) 91 | self.viewLayout.addWidget(self.chapters_frame) 92 | self.viewLayout.addWidget(self.score_frame) 93 | self.viewLayout.addWidget(self.lib_list_frame) 94 | 95 | self.accepted.connect(self._send_user_rate) 96 | self.rejected.connect(self.close) 97 | 98 | self._display_user_rate() 99 | 100 | def closeEvent(self, arg__1): 101 | self.deleteLater() 102 | 103 | def _fetch_user_rate(self): 104 | if not self.__catalog.check_user_rate(self.__manga): 105 | self.__catalog.create_user_rate(self.__manga) 106 | return self.__catalog.get_user_rate(self.__manga) 107 | 108 | def _display_user_rate(self): 109 | self.score_spin.setValue(self.__user_rate.score) 110 | self.chapters_count_spin.setValue(self.__user_rate.chapters) 111 | if self.__manga.chapters: 112 | self.chapters_count_spin.setMaximum(self.__manga.chapters) 113 | self.lib_list_combo.setCurrentIndex(self.__user_rate.status.value) 114 | 115 | @Slot() 116 | def _send_user_rate(self): 117 | self.__user_rate.score = self.score_spin.value() 118 | self.__user_rate.chapters = self.chapters_count_spin.value() 119 | self.__user_rate.status = Nl.LibList( 120 | self.lib_list_combo.currentIndex(), 121 | ) 122 | self.__catalog.update_user_rate(self.__user_rate) 123 | self.close() 124 | 125 | @Slot() 126 | def _delete_user_rate(self): 127 | self.__catalog.delete_user_rate(self.__user_rate) 128 | self.close() 129 | 130 | 131 | __all__ = [ 132 | "RateDialog", 133 | ] 134 | -------------------------------------------------------------------------------- /nlightreader/widgets/items/__init__.py: -------------------------------------------------------------------------------- 1 | from .manga_item import MangaItem 2 | from .title_tree_item import ChapterTreeItem 3 | -------------------------------------------------------------------------------- /nlightreader/widgets/items/title_tree_item.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtWidgets import QTreeWidgetItem 2 | 3 | from nlightreader.models import Chapter 4 | 5 | 6 | class ChapterTreeItem(QTreeWidgetItem): 7 | def __init__(self, chapter: Chapter): 8 | super().__init__([chapter.get_name()]) 9 | self.chapter = chapter 10 | 11 | 12 | __all__ = [ 13 | "ChapterTreeItem", 14 | ] 15 | -------------------------------------------------------------------------------- /nlightreader/widgets/pages/__init__.py: -------------------------------------------------------------------------------- 1 | from .external_library_page import ExternalLibraryPage 2 | from .history_page import HistoryPage 3 | from .info_page import InfoPage 4 | from .library_page import LibraryPage 5 | from .main_page import MainPage 6 | from .settings_intefrace import SettingsPage 7 | -------------------------------------------------------------------------------- /nlightreader/widgets/pages/base_page.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from PySide6.QtCore import Signal, Slot 4 | from PySide6.QtWidgets import QWidget 5 | 6 | from nlightreader.consts.enums import Nl 7 | from nlightreader.exceptions.parser_content_exc import ( 8 | FetchContentError, 9 | NoContentError, 10 | RequestsParamsError, 11 | ) 12 | from nlightreader.items import RequestForm 13 | from nlightreader.models import Manga 14 | from nlightreader.utils.threads import Thread 15 | from nlightreader.widgets.containers.content_container import ( 16 | ContentContainerState, 17 | ) 18 | from nlightreader.widgets.containers.manga_area import MangaArea 19 | from nlightreader.widgets.items.manga_item import MangaItem 20 | 21 | 22 | class BasePage(QWidget): 23 | manga_open = Signal(Manga) 24 | 25 | def __init__(self, parent=None): 26 | super().__init__(parent=parent) 27 | self.manga_area = MangaArea() 28 | self.mangas: list[Manga] = [] 29 | 30 | self._get_content_thread = Thread( 31 | target=self._get_content_thread_func, 32 | callback=self.update_content, 33 | error_callback=self.__process_errors, 34 | ) 35 | 36 | self.catalog = None 37 | self.request_params = RequestForm() 38 | 39 | def setup(self): 40 | self.get_content() 41 | 42 | def update_content(self): 43 | self.manga_area.delete_items() 44 | items = [self._setup_manga_item(manga) for manga in self.mangas] 45 | self.manga_area.set_state(ContentContainerState.show_content) 46 | self.manga_area.add_items(items) 47 | self.manga_area.update_items() 48 | 49 | def __process_errors(self, exception: Exception): 50 | try: 51 | raise exception 52 | except FetchContentError: 53 | self.manga_area.set_state(ContentContainerState.fetch_error) 54 | except (NoContentError, RequestsParamsError): 55 | self.manga_area.set_state(ContentContainerState.no_content) 56 | 57 | @Slot() 58 | def turn_page_next(self): 59 | if self.request_params.page == 999: 60 | return 61 | self.request_params.page += 1 62 | self.get_content() 63 | 64 | @Slot() 65 | def turn_page_prev(self): 66 | if self.request_params.page == 1: 67 | return 68 | self.request_params.page -= 1 69 | self.get_content() 70 | 71 | def get_content(self): 72 | self.update_page() 73 | self._get_content_thread.terminate() 74 | self._get_content_thread.wait() 75 | self.manga_area.delete_items() 76 | self.manga_area.set_state(ContentContainerState.fetch_content) 77 | self._get_content_thread.start() 78 | 79 | def _get_content_thread_func(self): 80 | page = self.request_params.page 81 | lib_list = self.request_params.lib_list 82 | time.sleep(0.25) 83 | if ( 84 | page != self.request_params.page 85 | or lib_list != self.request_params.lib_list 86 | ): 87 | return 88 | self.mangas = self.catalog.search_manga(self.request_params) 89 | if not self.mangas: 90 | raise NoContentError 91 | 92 | def _setup_manga_item(self, manga: Manga) -> MangaItem: 93 | raise NotImplementedError 94 | 95 | def update_page(self): 96 | pass 97 | 98 | @Slot(Nl.LibList) 99 | def change_list(self, lst: Nl.LibList): 100 | self.request_params.lib_list = lst 101 | self.get_content() 102 | 103 | 104 | __all__ = [ 105 | "BasePage", 106 | ] 107 | -------------------------------------------------------------------------------- /nlightreader/widgets/pages/external_library_page.py: -------------------------------------------------------------------------------- 1 | from typing import override 2 | 3 | from PySide6.QtCore import Slot 4 | from qfluentwidgets import FluentIcon 5 | 6 | from data.ui.widgets.shikimori import Ui_Form 7 | from nlightreader.consts.enums import Nl 8 | from nlightreader.items import User 9 | from nlightreader.models import Manga 10 | from nlightreader.parsers import ShikimoriLib 11 | from nlightreader.utils.threads import Worker 12 | from nlightreader.utils.translator import translate 13 | from nlightreader.widgets.dialogs import ( 14 | TokenAuthMessageBox, 15 | UserDataAuthMessageBox, 16 | ) 17 | from nlightreader.widgets.items.manga_item import MangaItem 18 | from nlightreader.widgets.pages.base_page import BasePage 19 | 20 | 21 | class ExternalLibraryPage(BasePage): 22 | def __init__(self, parent=None): 23 | super().__init__(parent=parent) 24 | self.ui = Ui_Form() 25 | self.ui.setupUi(self) 26 | 27 | self.ui.next_btn.setIcon(FluentIcon.RIGHT_ARROW) 28 | self.ui.prev_btn.setIcon(FluentIcon.LEFT_ARROW) 29 | 30 | self.setObjectName("FormShikimori") 31 | 32 | self.manga_area.install(self.ui.items_layout) 33 | 34 | self.ui.planned_btn.clicked.connect( 35 | lambda: self.change_list(Nl.LibList.planned), 36 | ) 37 | self.ui.reading_btn.clicked.connect( 38 | lambda: self.change_list(Nl.LibList.reading), 39 | ) 40 | self.ui.on_hold_btn.clicked.connect( 41 | lambda: self.change_list(Nl.LibList.on_hold), 42 | ) 43 | self.ui.completed_btn.clicked.connect( 44 | lambda: self.change_list(Nl.LibList.completed), 45 | ) 46 | self.ui.dropped_btn.clicked.connect( 47 | lambda: self.change_list(Nl.LibList.dropped), 48 | ) 49 | self.ui.re_reading_btn.clicked.connect( 50 | lambda: self.change_list(Nl.LibList.re_reading), 51 | ) 52 | self.ui.next_btn.clicked.connect(self.turn_page_next) 53 | self.ui.prev_btn.clicked.connect(self.turn_page_prev) 54 | self.ui.title_line.searchSignal.connect(self.search) 55 | self.ui.auth_btn.clicked.connect(self.authorize) 56 | self.catalog = ShikimoriLib() 57 | Worker(target=self.get_user_info, callback=self.set_user_info).start() 58 | 59 | @override 60 | def _setup_manga_item(self, manga: Manga): 61 | item = MangaItem( 62 | manga, 63 | is_added_to_lib=False, 64 | pool=self.manga_area.manga_thread_pool, 65 | ) 66 | item.manga_clicked.connect(self.manga_open.emit) 67 | return item 68 | 69 | def get_user_info(self): 70 | self.ui.auth_btn.setEnabled(False) 71 | return self.catalog.get_user() 72 | 73 | def set_user_info(self, user: User): 74 | if user.nickname: 75 | self.ui.auth_btn.setText(user.nickname) 76 | else: 77 | self.ui.auth_btn.setText( 78 | translate("Other", "Sign in"), 79 | ) 80 | self.ui.auth_btn.setEnabled(True) 81 | 82 | @override 83 | def update_page(self): 84 | self.ui.page_label.setText( 85 | f"{translate('Other', 'Page')} {self.request_params.page}", 86 | ) 87 | 88 | def auth_success_callback(self, user: User): 89 | self.set_user_info(user) 90 | self.get_content() 91 | 92 | @Slot() 93 | def authorize(self): 94 | if self.catalog.fields == 1: 95 | w = TokenAuthMessageBox(self.catalog, parent=self) 96 | else: 97 | w = UserDataAuthMessageBox(self.catalog, parent=self) 98 | if w.exec(): 99 | self.catalog.session.auth_login(w.get_user_data()) 100 | Worker( 101 | target=self.get_user_info, 102 | callback=self.auth_success_callback, 103 | ).start() 104 | 105 | @Slot() 106 | def search(self): 107 | self.request_params.page = 1 108 | self.request_params.search = self.ui.title_line.text() 109 | self.get_content() 110 | 111 | 112 | __all__ = [ 113 | "ExternalLibraryPage", 114 | ] 115 | -------------------------------------------------------------------------------- /nlightreader/widgets/pages/history_page.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import Signal, Slot 2 | from PySide6.QtWidgets import QTreeWidgetItem, QWidget 3 | from qfluentwidgets import FluentIcon 4 | 5 | from data.ui.widgets.history import Ui_Form 6 | from nlightreader.consts.colors import ItemsIcons 7 | from nlightreader.items import HistoryNote 8 | from nlightreader.models import Manga 9 | from nlightreader.utils.database import Database 10 | from nlightreader.widgets.contexts import HistoryNoteMenu 11 | 12 | 13 | class HistoryPage(QWidget): 14 | manga_open = Signal(Manga) 15 | 16 | def __init__(self, parent=None): 17 | super().__init__(parent=parent) 18 | self.ui = Ui_Form() 19 | self.ui.setupUi(self) 20 | 21 | self.ui.delete_btn.setIcon(FluentIcon.DELETE) 22 | 23 | self.setObjectName("FormHistory") 24 | 25 | self.ui.items_tree.customContextMenuRequested.connect( 26 | self.on_context_menu, 27 | ) 28 | self.db: Database = Database() 29 | self.ui.delete_btn.clicked.connect(self.delete_note) 30 | self.ui.items_tree.doubleClicked.connect(self.open_info) 31 | self.notes: list[HistoryNote] = [] 32 | self.sorted_notes = {} 33 | 34 | def on_context_menu(self, pos): 35 | def set_as_read(): 36 | self.db.add_history_note( 37 | HistoryNote( 38 | selected_note.chapter, 39 | selected_note.manga, 40 | True, 41 | ), 42 | ) 43 | selected_item.setIcon(0, ItemsIcons.READ) 44 | 45 | def remove_all(): 46 | self.db.del_history_notes(selected_manga) 47 | self.get_content() 48 | 49 | menu = HistoryNoteMenu() 50 | selected_item = self.ui.items_tree.itemAt(pos) 51 | if not selected_item: 52 | return 53 | 54 | selected_note = self._get_selected_note() 55 | selected_manga = self._get_selected_manga() 56 | 57 | if selected_item.parent() and not self.db.get_complete_status( 58 | selected_note.chapter, 59 | ): 60 | menu.set_mode(0) 61 | else: 62 | menu.set_mode(1) 63 | 64 | menu.set_as_read.triggered.connect(set_as_read) 65 | menu.remove_all.triggered.connect(remove_all) 66 | menu.exec(self.ui.items_tree.mapToGlobal(pos)) 67 | 68 | def setup(self): 69 | self.get_content() 70 | 71 | @Slot() 72 | def open_info(self): 73 | selected_item = self.ui.items_tree.currentItem() 74 | if selected_item.parent(): 75 | self.manga_open.emit(self._get_selected_manga()) 76 | 77 | def sort_notes(self): 78 | self.sorted_notes.clear() 79 | for note in self.notes: 80 | if note.manga in self.sorted_notes: 81 | self.sorted_notes[note.manga].append(note) 82 | else: 83 | self.sorted_notes.update({note.manga: [note]}) 84 | 85 | def update_content(self): 86 | self.ui.items_tree.clear() 87 | self.notes: list[HistoryNote] = self.db.get_history_notes() 88 | self.sort_notes() 89 | for manga in self.sorted_notes: 90 | top_item = QTreeWidgetItem([manga.get_name()]) 91 | self.ui.items_tree.addTopLevelItem(top_item) 92 | for note in self.sorted_notes[manga]: 93 | ch_item = QTreeWidgetItem([note.chapter.get_name()]) 94 | if note.is_completed: 95 | ch_item.setIcon(0, ItemsIcons.READ.qicon()) 96 | else: 97 | ch_item.setIcon(0, ItemsIcons.UNREAD) 98 | top_item.addChild(ch_item) 99 | 100 | def _get_selected_note(self) -> HistoryNote | None: 101 | selected_item = self.ui.items_tree.currentItem() 102 | if not selected_item.parent(): 103 | return None 104 | parent_index = self.ui.items_tree.indexFromItem( 105 | selected_item.parent(), 106 | ).row() 107 | note_index = self.ui.items_tree.indexFromItem( 108 | selected_item, 109 | ).row() 110 | return self.sorted_notes[list(self.sorted_notes.keys())[parent_index]][ 111 | note_index 112 | ] 113 | 114 | def _get_selected_manga(self) -> Manga: 115 | selected_item = self.ui.items_tree.currentItem() 116 | if not selected_item.parent(): 117 | index = self.ui.items_tree.indexFromItem(selected_item).row() 118 | return list(self.sorted_notes.keys())[index] 119 | return self._get_selected_note().manga 120 | 121 | @Slot() 122 | def delete_note(self): 123 | selected_item = self.ui.items_tree.currentItem() 124 | if not selected_item or not selected_item.parent(): 125 | return 126 | self.db.del_history_note(self._get_selected_note().chapter) 127 | selected_item.parent().removeChild(selected_item) 128 | self.get_content() 129 | 130 | def get_content(self): 131 | self.update_content() 132 | 133 | 134 | __all__ = [ 135 | "HistoryPage", 136 | ] 137 | -------------------------------------------------------------------------------- /nlightreader/widgets/pages/library_page.py: -------------------------------------------------------------------------------- 1 | from typing import override 2 | 3 | from data.ui.widgets.library import Ui_Form 4 | from nlightreader.consts.enums import Nl 5 | from nlightreader.models import Manga 6 | from nlightreader.parsers import LocalLibrary 7 | from nlightreader.widgets.items.manga_item import MangaItem 8 | from nlightreader.widgets.pages.base_page import BasePage 9 | 10 | 11 | class LibraryPage(BasePage): 12 | def __init__(self, parent=None): 13 | super().__init__(parent=parent) 14 | self.ui = Ui_Form() 15 | self.ui.setupUi(self) 16 | 17 | self.setObjectName("FormLibrary") 18 | 19 | self.manga_area.install(self.ui.items_layout) 20 | 21 | self.ui.planned_btn.clicked.connect( 22 | lambda: self.change_list(Nl.LibList.planned), 23 | ) 24 | self.ui.reading_btn.clicked.connect( 25 | lambda: self.change_list(Nl.LibList.reading), 26 | ) 27 | self.ui.on_hold_btn.clicked.connect( 28 | lambda: self.change_list(Nl.LibList.on_hold), 29 | ) 30 | self.ui.completed_btn.clicked.connect( 31 | lambda: self.change_list(Nl.LibList.completed), 32 | ) 33 | self.ui.dropped_btn.clicked.connect( 34 | lambda: self.change_list(Nl.LibList.dropped), 35 | ) 36 | self.ui.re_reading_btn.clicked.connect( 37 | lambda: self.change_list(Nl.LibList.re_reading), 38 | ) 39 | self.catalog = LocalLibrary() 40 | 41 | @override 42 | def _setup_manga_item(self, manga: Manga): 43 | item = MangaItem(manga, pool=self.manga_area.manga_thread_pool) 44 | item.manga_clicked.connect(self.manga_open.emit) 45 | item.manga_changed.connect(self.get_content) 46 | return item 47 | 48 | 49 | __all__ = [ 50 | "LibraryPage", 51 | ] 52 | -------------------------------------------------------------------------------- /nlightreader/windows/__init__.py: -------------------------------------------------------------------------------- 1 | from .parent_window import ParentWindow 2 | from .reader_window import ReaderWindow 3 | -------------------------------------------------------------------------------- /nlightreader/windows/parent_window.py: -------------------------------------------------------------------------------- 1 | from PySide6.QtCore import QSize, Slot 2 | from qfluentwidgets import FluentIcon, FluentWindow, NavigationItemPosition 3 | 4 | from nlightreader.consts.files.files import NlFluentIcons 5 | from nlightreader.models import Manga 6 | from nlightreader.utils.translator import translate 7 | from nlightreader.widgets.pages import ( 8 | ExternalLibraryPage, 9 | HistoryPage, 10 | InfoPage, 11 | LibraryPage, 12 | MainPage, 13 | SettingsPage, 14 | ) 15 | 16 | 17 | class ParentWindow(FluentWindow): 18 | def __init__(self): 19 | super().__init__() 20 | 21 | self.library_interface = LibraryPage() 22 | self.main_interface = MainPage() 23 | self.external_library_interface = ExternalLibraryPage() 24 | self.history_interface = HistoryPage() 25 | 26 | self.settings_interface = SettingsPage() 27 | 28 | self.info_interface: InfoPage | None = None 29 | 30 | self.library_interface.manga_open.connect(self.open_info) 31 | self.main_interface.manga_open.connect(self.open_info) 32 | self.external_library_interface.manga_open.connect(self.open_info) 33 | self.history_interface.manga_open.connect(self.open_info) 34 | 35 | self.stackedWidget.currentChanged.connect(self.on_widget_change) 36 | 37 | self.init_navigation() 38 | 39 | def init_navigation(self): 40 | self.addSubInterface( 41 | self.library_interface, 42 | FluentIcon.LIBRARY, 43 | translate("MainWindow", "Library"), 44 | ) 45 | self.addSubInterface( 46 | self.main_interface, 47 | FluentIcon.HOME, 48 | translate("MainWindow", "Main"), 49 | ) 50 | self.addSubInterface( 51 | self.external_library_interface, 52 | NlFluentIcons.SHIKIMORI, 53 | translate("MainWindow", "Shikimori"), 54 | ) 55 | self.addSubInterface( 56 | self.history_interface, 57 | FluentIcon.HISTORY, 58 | translate("MainWindow", "History"), 59 | ) 60 | self.addSubInterface( 61 | self.settings_interface, 62 | FluentIcon.SETTING, 63 | translate("MainWindow", "Settings"), 64 | position=NavigationItemPosition.BOTTOM, 65 | ) 66 | 67 | @Slot(int) 68 | def on_widget_change(self, value): 69 | if value in range(4): 70 | if any( 71 | i.objectName() == "FormInfo" 72 | for i in self.stackedWidget.view.children() 73 | ): 74 | self.delete_info_interface() 75 | self.navigationInterface.setReturnButtonVisible( 76 | self.stackedWidget.count() > 5, 77 | ) 78 | if self.stackedWidget.currentWidget().objectName() in ( 79 | "FormInfo", 80 | "ReaderWidget", 81 | ): 82 | return 83 | self.stackedWidget.currentWidget().setup() 84 | 85 | def set_min_size_by_screen(self): 86 | self.setMinimumSize( 87 | QSize( 88 | self.screen().size().width() // 2, 89 | self.screen().size().height() // 2, 90 | ), 91 | ) 92 | 93 | def delete_info_interface(self): 94 | self.stackedWidget.view.removeWidget(self.info_interface) 95 | self.info_interface.deleteLater() 96 | self.info_interface = None 97 | 98 | @Slot(Manga) 99 | def open_info(self, manga: Manga): 100 | stack = self.stackedWidget.view 101 | self.stackedWidget.setEnabled(False) 102 | 103 | @Slot() 104 | def set_info_widget(): 105 | stack.addWidget(self.info_interface) 106 | self.switchTo(self.info_interface) 107 | self.stackedWidget.setEnabled(True) 108 | 109 | @Slot() 110 | def delete_info_widget(): 111 | self.info_interface.close() 112 | self.stackedWidget.setEnabled(True) 113 | 114 | if self.stackedWidget.currentWidget().objectName() == "FormInfo": 115 | self.stackedWidget.view.removeWidget(self.info_interface) 116 | 117 | self.info_interface = InfoPage() 118 | self.info_interface.opened_related_manga.connect(self.open_info) 119 | self.info_interface.setup_done.connect(set_info_widget) 120 | self.info_interface.setup_error.connect(delete_info_widget) 121 | self.info_interface.setup(manga) 122 | 123 | 124 | __all__ = [ 125 | "ParentWindow", 126 | ] 127 | -------------------------------------------------------------------------------- /pkg_res/Nlight.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | # The type of the thing this desktop file refers to (e.g. can be Link) 3 | Type=Application 4 | # The application name. 5 | Name=Nlight 6 | # Tooltip comment to show in menus. 7 | Comment=Open source manga and ranobe reading application. 8 | # The path (folder) in which the executable is run 9 | Path=/opt/Nlight 10 | # The executable (can include arguments) 11 | Exec=/opt/Nlight/Nlight 12 | # The icon for the entry, using the name from `hicolor/scalable` without the extension. 13 | # You can also use a full path to a file in /opt. 14 | Icon=Nlight 15 | 16 | -------------------------------------------------------------------------------- /pkg_res/Nlight.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brandonzorn/Nlight/8b7b8924c10b49fcca9281a0b2596ef3e485e27d/pkg_res/Nlight.ico -------------------------------------------------------------------------------- /pkg_res/Nlight.svg: -------------------------------------------------------------------------------- 1 | 3 | 27 | 62 | 82 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 79 3 | exclude = """ 4 | /( 5 | build/ 6 | | build_nuitka/ 7 | | data/ 8 | | dist/ 9 | | venv/ 10 | ) 11 | """ 12 | 13 | [tool.flake8] 14 | max-line-length = 79 15 | inline-quotes = "double" 16 | import-order-style = "pep8" 17 | application-import-names = [ 18 | "data", 19 | "nlightreader", 20 | "keys", 21 | ] 22 | ignore = [ 23 | "F401", 24 | "N802", 25 | "W503", 26 | ] 27 | exclude = [ 28 | "build", 29 | "build_nuitka", 30 | "data", 31 | "dist", 32 | "venv", 33 | ] -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4>=4.11.1 2 | darkdetect>=0.8.0 3 | oauthlib>=3.2.0 4 | platformdirs>=3.1.1 5 | PySide6>=6.3.0 6 | PySide6-Fluent-Widgets>=1.4.1 7 | render-html>=1.0.1 8 | requests>=2.28.1 9 | requests-oauthlib>=1.3.1 10 | SQLAlchemy>=2.0.25 11 | validators>=0.33.0 12 | -------------------------------------------------------------------------------- /requirements/dev.txt: -------------------------------------------------------------------------------- 1 | -r prod.txt 2 | -r test.txt 3 | pyinstaller-versionfile>=2.1.1 4 | sort-requirements>=1.3.0 -------------------------------------------------------------------------------- /requirements/prod.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4>=4.11.1 2 | darkdetect>=0.8.0 3 | oauthlib>=3.2.0 4 | platformdirs>=3.1.1 5 | PySide6>=6.3.0 6 | PySide6-Fluent-Widgets>=1.4.1 7 | render-html>=1.0.1 8 | requests>=2.28.1 9 | requests-oauthlib>=1.3.1 10 | SQLAlchemy>=2.0.25 11 | validators>=0.33.0 12 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | flake8>=6.1.0 2 | flake8-all-not-strings>=0.0.2 3 | flake8-commas>=2.1.0 4 | flake8-comments>=0.1.2 5 | flake8-dunder-all>=0.4.1 6 | flake8-encodings>=0.5.1 7 | flake8-eradicate>=1.5.0 8 | flake8-import-order>=0.18.2 9 | flake8-print>=5.0.0 10 | Flake8-pyproject>=1.2.3 11 | flake8-quotes>=3.3.2 12 | flake8-return>=1.2.0 13 | flake8-use-pathlib>=0.3.0 14 | pep8-naming>=0.13.3 --------------------------------------------------------------------------------