├── .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 | 
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 | |  |  |  |
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 | 
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 | |  |  |  |
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 | 
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 | |  |  |  |
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 | 
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 | |  |  |  |
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 |
16 |
--------------------------------------------------------------------------------
/data/icons/buttons/svg_24dp_white/actions/shikimori.svg:
--------------------------------------------------------------------------------
1 |
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 |
113 | 1
114 |
115 |
116 | SimpleCardWidget
117 | QFrame
118 |
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 |
83 |
84 |
85 | TextEdit
86 | QTextEdit
87 |
88 |
89 |
90 | CardWidget
91 | QFrame
92 |
93 | 1
94 |
95 |
96 | SimpleCardWidget
97 | CardWidget
98 |
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 |
79 |
80 |
81 | CardWidget
82 | QWidget
83 |
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 |
91 |
92 |
93 | SimpleCardWidget
94 | CardWidget
95 |
96 | 1
97 |
98 |
99 | TreeWidget
100 | QTreeWidget
101 |
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 |
147 |
148 |
149 | CardWidget
150 | QFrame
151 |
152 | 1
153 |
154 |
155 | SimpleCardWidget
156 | CardWidget
157 |
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 |
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------