42 Commits

Author SHA1 Message Date
2819c12ce0 wip(ui): play result delegate 2025-11-16 16:38:31 +08:00
a086573c0a feat(ui.theme): add arcaea colors 2025-11-15 13:54:09 +08:00
ef61ecf6ae fix(ui.theme): expose colors to qml 2025-11-15 13:20:58 +08:00
71e9f05632 impr(ui.theme): convert CustomPalette to dataclass 2025-11-15 13:20:18 +08:00
0966a3eb40 impr(ui.theme): add secondary & tertiary colors 2025-11-15 13:11:45 +08:00
a2148c7d24 chore(ui.resources): play result grade icons 2025-11-15 13:07:46 +08:00
65dab51734 fix(core): database logics 2025-11-09 00:40:23 +08:00
3679831201 wip: theme system
- Add theme id
- WIP theme cache key
- Force scheme (light/dark) for dynamic theme
- でびるんちゃんかわいい
2025-11-09 00:32:28 +08:00
7a3c186743 impr(ui): nav listview animation 2025-11-08 21:40:47 +08:00
4409986687 wip: basic theming support 2025-10-28 21:23:06 +08:00
9c96714c8f refactor: replace logging with structlog 2025-10-28 21:18:27 +08:00
9a761f1191 chore: update dependencies, introduce uv 2025-10-25 21:20:35 +08:00
42b3447b43 chore: configure pyside6-project 2025-10-24 22:58:25 +08:00
d90a165df8 wip: replace Text with Label 2025-10-08 16:29:35 +08:00
87304b02f7 wip: qml overview tab 2025-09-13 14:47:24 +08:00
5b44696fd7 wip: database object 2025-09-13 14:46:51 +08:00
de385e9b26 chore: update pre-commit 2025-09-13 14:40:05 +08:00
1453686de6 wip: database checker 2025-09-12 00:02:28 +08:00
5db2207ee0 wip: qml components 2025-08-15 01:49:01 +08:00
06a1ca00bd refactor: database initialize checker 2025-08-13 15:17:46 +08:00
806acd5793 refactor: SongIdSelectorViewModel 2025-06-05 17:05:47 +08:00
ceb6e2932e chore: update dependencies 2025-06-04 19:10:28 +08:00
035e2157a8 log tag support 2025-06-04 18:55:23 +08:00
e964a38e3d change # fmt: labels to ruff compatible 2025-06-04 18:55:23 +08:00
0e2026ff1c change logging format to % 2025-06-04 18:55:23 +08:00
3ce4c7bed9 core.color 2025-06-04 18:55:23 +08:00
ad09c95b03 disable strict ruff rules 2025-06-04 18:55:23 +08:00
c664ed7e8d refactor: moving ui.extends to core
* Settings and Singletons moved
2025-06-04 18:55:23 +08:00
4e7d54fbef using ruff as formatter & linter 2025-06-04 18:55:23 +08:00
0c6f4f4961 chore: v0.3.9 2024-06-20 00:04:46 +08:00
10fb98d530 chore: upgrade dependencies 2024-06-20 00:03:19 +08:00
bf034d1397 ci: update build actions
* Switch to official Nuitka GitHub Actions

* Adding Linux build support

* Upgrade deprecated actions
2024-06-20 00:00:28 +08:00
4f864611ee fix: B30 table order (#11) 2024-06-19 22:20:31 +08:00
d9c163431c feat: OCR score date source (#9)
* New settings entries

* Choose `birthTime`/`lastModified` for OCR score date source if the image EXIF fails
2024-06-19 22:18:25 +08:00
d5895fe230 chore: v0.3.8 2024-04-01 01:00:46 +08:00
cd2e3f51ca ci: update build actions 2024-04-01 01:00:27 +08:00
4a09dc210a Merge pull request #7 from ArcaeaOffline/fix-issue-6
fix: rating class selection logic
2024-03-24 16:40:24 +08:00
cc8ab11b78 fix: rating class selection logic 2024-03-24 16:17:48 +08:00
48c5682e55 Merge pull request #5 from ArcaeaOffline/fix-issue-4
fix: linux dbUrl issue
2024-03-23 19:08:23 +08:00
ee03770764 chore: update README 2024-03-23 18:21:48 +08:00
b45c7f7de5 chore: dependencies 2024-03-23 18:18:53 +08:00
15bc56e6f9 fix: linux dbUrl issue 2024-03-23 17:41:36 +08:00
115 changed files with 6273 additions and 1349 deletions

View File

@ -1,37 +1,65 @@
name: Build UI from latest `arcaea-offline-*` dependencies name: Build Executable from latest `arcaea-offline-*` dependencies
run-name: ${{ github.actor }} started a build request. run-name: ${{ github.actor }} started a build request.
on: [workflow_dispatch]
on:
workflow_dispatch:
permissions:
contents: write
discussions: write
jobs: jobs:
build-windows: build:
runs-on: windows-2022 strategy:
matrix:
os: [ubuntu-latest, windows-latest]
fail-fast: false
runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with: with:
python-version: '3.11' python-version: "3.11"
cache: "pip"
# install dependencies # install dependencies
- run: 'pip install -r requirements.txt' - run: "pip install -r requirements.txt"
- run: 'pip uninstall arcaea-offline arcaea-offline-ocr -y' - run: "pip uninstall arcaea-offline arcaea-offline-ocr -y"
- run: 'pip install git+https://github.com/283375/arcaea-offline' - run: "pip install git+https://github.com/283375/arcaea-offline"
- run: 'pip install git+https://github.com/283375/arcaea-offline-ocr' - run: "pip install git+https://github.com/283375/arcaea-offline-ocr"
- run: 'pip install nuitka imageio' - run: "pip install imageio"
- name: Install UPX - name: Install UPX
uses: crazy-max/ghaction-upx@v3 uses: crazy-max/ghaction-upx@v3
with: with:
install-only: true install-only: true
# release builtin files - name: Release builtin files
- run: 'pyside6-lrelease.exe .\ui\resources\lang\en_US.ts .\ui\resources\lang\zh_CN.ts' run: |
- run: 'python prebuild.py' pyside6-lrelease ui/resources/lang/en_US.ts ui/resources/lang/zh_CN.ts
- run: 'pyside6-rcc.exe .\ui\resources\resources.qrc -o .\ui\resources\resources_rc.py' python prebuild.py
pyside6-rcc ui/resources/resources.qrc -o ui/resources/resources_rc.py
# build - name: Build Executable
- run: 'python -m nuitka --plugin-enable=upx --enable-plugin=pyside6 --assume-yes-for-downloads --windows-icon-from-ico=./ui/resources/images/icon.png --standalone --onefile index.py' uses: Nuitka/Nuitka-Action@main
- uses: actions/upload-artifact@v3
with: with:
name: build-windows nuitka-version: main
path: index.exe script-name: index.py
standalone: true
onefile: true
enable-plugins: pyside6,upx
windows-icon-from-ico: ui/resources/images/icon.png
linux-icon: ui/resources/images/icon.png
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ runner.os }} Build
path: |
build/*.exe
build/*.bin
build/*.app/**/*

View File

@ -1,35 +1,78 @@
name: Build UI name: Build Executable
run-name: ${{ github.actor }} started a build request. run-name: ${{ github.actor }} started a build request.
on: [workflow_dispatch]
on:
workflow_dispatch:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
permissions:
contents: write
discussions: write
jobs: jobs:
build-windows: build:
runs-on: windows-2022 strategy:
matrix:
os: [ubuntu-latest, windows-latest]
fail-fast: false
runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-python@v4
with:
python-version: '3.11'
cache: 'pip'
# install dependencies - uses: actions/setup-python@v5
- run: 'pip install -r requirements.txt' with:
- run: 'pip install nuitka imageio' python-version: "3.11"
cache: "pip"
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install imageio
- name: Install UPX - name: Install UPX
uses: crazy-max/ghaction-upx@v3 uses: crazy-max/ghaction-upx@v3
with: with:
install-only: true install-only: true
# release builtin files - name: Release builtin files
- run: 'pyside6-lrelease.exe .\ui\resources\lang\en_US.ts .\ui\resources\lang\zh_CN.ts' run: |
- run: 'python prebuild.py' pyside6-lrelease ui/resources/lang/en_US.ts ui/resources/lang/zh_CN.ts
- run: 'pyside6-rcc.exe .\ui\resources\resources.qrc -o .\ui\resources\resources_rc.py' python prebuild.py
pyside6-rcc ui/resources/resources.qrc -o ui/resources/resources_rc.py
# build - name: Build Executable
- run: 'python -m nuitka --plugin-enable=upx --enable-plugin=pyside6 --assume-yes-for-downloads --windows-icon-from-ico=./ui/resources/images/icon.png --standalone --onefile index.py' uses: Nuitka/Nuitka-Action@main
- uses: actions/upload-artifact@v3
with: with:
name: build-windows nuitka-version: main
path: index.exe script-name: index.py
standalone: true
onefile: true
enable-plugins: pyside6,upx
windows-icon-from-ico: ui/resources/images/icon.png
linux-icon: ui/resources/images/icon.png
- name: Upload Artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ runner.os }} Build
path: |
build/*.exe
build/*.bin
build/*.app/**/*
- name: Draft a release
uses: softprops/action-gh-release@v2
with:
discussion_category_name: New releases
draft: true
generate_release_notes: true
files: |
build/*.exe
build/*.bin
build/*.app/**/*

3
.gitignore vendored
View File

@ -1,6 +1,9 @@
__debug* __debug*
.vscode/ .vscode/
# QML type cache
internal/
arcaea_offline.db arcaea_offline.db
arcaea_offline.ini arcaea_offline.ini
/data /data

View File

@ -1,14 +1,13 @@
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0 rev: v6.0.0
hooks: hooks:
- id: end-of-file-fixer - id: end-of-file-fixer
- id: trailing-whitespace - id: trailing-whitespace
- repo: https://github.com/psf/black
rev: 23.1.0 - repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.13.0
hooks: hooks:
- id: black - id: ruff
- repo: https://github.com/PyCQA/isort args: ["--fix"]
rev: 5.12.0 - id: ruff-format
hooks:
- id: isort

View File

@ -1,9 +1,29 @@
# Arcaea Offline PySide UI # Arcaea Offline PySide UI
GUI for both [283375/arcaea-offline](https://github.com/283375/arcaea-offline) and [283375/arcaea-offline-ocr](https://github.com/283375/arcaea-offline-ocr) [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
## Before you run `python index.py`... GUI for both [283375/arcaea-offline](https://github.com/283375/arcaea-offline) and [ArcaeaOffline/core-ocr](https://github.com/ArcaeaOffline/core-ocr).
## Prerequisites
* Install requirements * Install requirements
* Release translation files from `ui/resources/lang/*.ts` * Release translation files from `ui/resources/lang/*.ts`
* Run `prebuild.py`
* Compile `ui/resources/resources.qrc` to `ui/resources/resources_rc.py` * Compile `ui/resources/resources.qrc` to `ui/resources/resources_rc.py`
You can refer to the [GitHub Actions file](./.github/workflows/build.yml) for a rough reference.
```
pip install -r ./requirements.txt
pyside6-lrelease ./ui/resources/lang/en_US.ts ./ui/resources/lang/zh_CN.ts
python prebuild.py
pyside6-rcc ./ui/resources/resources.qrc -o ./ui/resources/resources_rc.py
```
Sometimes you have to install the latest, unpublished version of `arcaea-offline` and `arcaea-offline-ocr`.
```
pip uninstall -y arcaea-offline arcaea-offline-ocr
pip install git+https://github.com/283375/arcaea-offline
pip install git+https://github.com/ArcaeaOffline/core-ocr
```

91
app.py Normal file
View File

@ -0,0 +1,91 @@
import sys
from pathlib import Path
import structlog
from PySide6.QtCore import QCoreApplication, QEvent, QObject, Qt, QUrl
from PySide6.QtGui import QGuiApplication, QIcon
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtQuickControls2 import QQuickStyle
from ui.resources import resources_rc # noqa: F401
from ui.theme import ThemeManager
from ui.utils import url # noqa: F401
from ui.viewmodels import overview # noqa: F401
CURRENT_DIRECTORY = Path(__file__).resolve().parent
DEFAULT_FONTS = ["微软雅黑", "Microsoft YaHei UI", "Microsoft YaHei", "Segoe UI"]
logger: structlog.stdlib.BoundLogger = structlog.get_logger()
class ThemeChangeEventFilter(QObject):
logger: structlog.stdlib.BoundLogger = structlog.get_logger(
tag="ThemeChangeEventFilter",
)
def __init__(self, *, themeManager: ThemeManager):
super().__init__(None)
self.themeManager = themeManager
self.scheme = self.themeManager.getCurrentScheme()
def doSomething(self) -> None:
scheme = self.themeManager.getCurrentScheme()
if scheme == self.scheme:
self.logger.debug("Ignored same scheme event (%r)", scheme)
return
self.scheme = scheme
self.themeManager.updateTheme()
self.logger.debug("something done")
def eventFilter(self, obj: QObject, event: QEvent) -> bool:
if event.type() == QEvent.Type.ThemeChange:
self.doSomething()
return False
def main() -> None:
app = QGuiApplication(sys.argv)
app.setFont(DEFAULT_FONTS)
app.setApplicationName("arcaea-offline-pyside-ui")
app.setApplicationDisplayName("Arcaea Offline")
app.setWindowIcon(QIcon(":/images/icon.png"))
engine = QQmlApplicationEngine()
themeManager = ThemeManager(parent=app)
def onThemeManagerThemeChanged():
logger.debug("App palette changed")
app.setPalette(themeManager.qPalette) # pyright: ignore[reportArgumentType]
engine.rootContext().setContextProperty("appTheme", themeManager.qmlExposer)
onThemeManagerThemeChanged()
themeManager.themeChanged.connect(onThemeManagerThemeChanged)
QQuickStyle.setStyle("Fusion")
def onEngineObjectCreated(obj: QObject | None, objUrl: QUrl) -> None:
if obj is None:
logger.critical("rootObject is None! Exiting!")
QCoreApplication.exit(-1)
engine.objectCreated.connect(
onEngineObjectCreated,
Qt.ConnectionType.QueuedConnection,
)
engine.load("ui/qmls/App.qml")
rootObject = engine.rootObjects()[0]
ef = ThemeChangeEventFilter(themeManager=themeManager)
rootObject.installEventFilter(ef)
sys.exit(app.exec())
if __name__ == "__main__":
main()

10
core/color.py Normal file
View File

@ -0,0 +1,10 @@
from PySide6.QtGui import QColor
def mixColor(source: QColor, mix: QColor, ratio: float = 0.5):
r = round((mix.red() - source.red()) * ratio + source.red())
g = round((mix.green() - source.green()) * ratio + source.green())
b = round((mix.blue() - source.blue()) * ratio + source.blue())
a = round((mix.alpha() - source.alpha()) * ratio + source.alpha())
return QColor(r, g, b, a)

12
core/database/__init__.py Normal file
View File

@ -0,0 +1,12 @@
from .conn import Database
from .init_checker import DatabaseInitCheckResult, check_db_init
from .utils import create_engine, db_path_to_sqlite_url, sqlite_url_to_db_path
__all__ = [
"check_db_init",
"create_engine",
"db_path_to_sqlite_url",
"Database",
"DatabaseInitCheckResult",
"sqlite_url_to_db_path",
]

31
core/database/conn.py Normal file
View File

@ -0,0 +1,31 @@
from pathlib import Path
from arcaea_offline.models import CalculatedPotential
from sqlalchemy import select
from sqlalchemy.orm import sessionmaker
from core.settings import SettingsKeys, settings
from .utils import create_engine, db_path_to_sqlite_url
class Database:
def __init__(self):
if settings.stringValue(SettingsKeys.General.DatabaseType) != "file":
raise ValueError("DatabaseType is not file")
db_path = settings.stringValue(SettingsKeys.General.DatabaseConn)
if not db_path:
raise ValueError("DatabaseConn is empty")
db_path = Path(db_path)
if not db_path.exists():
raise FileNotFoundError(f"{db_path} does not exist")
self.engine = create_engine(db_path_to_sqlite_url(db_path))
self.sessionmaker = sessionmaker(bind=self.engine)
@property
def b30(self) -> float | None:
with self.sessionmaker() as session:
return session.scalar(select(CalculatedPotential.b30))

View File

@ -0,0 +1,30 @@
from enum import Flag, auto
from pathlib import Path
from arcaea_offline.database import Database
from .utils import create_engine, db_path_to_sqlite_url
class DatabaseInitCheckResult(Flag):
NONE = 0
FILE_EXISTS = auto()
INITIALIZED = auto()
OK = FILE_EXISTS | INITIALIZED
def check_db_init(file: Path) -> DatabaseInitCheckResult:
flags = DatabaseInitCheckResult.NONE
if not file.exists():
return flags
flags |= DatabaseInitCheckResult.FILE_EXISTS
db_url = db_path_to_sqlite_url(file)
db = Database(create_engine(db_url))
if db.check_init():
flags |= DatabaseInitCheckResult.INITIALIZED
return flags

28
core/database/utils.py Normal file
View File

@ -0,0 +1,28 @@
from pathlib import Path
from PySide6.QtCore import QSysInfo, QUrl
from sqlalchemy import Engine
from sqlalchemy import create_engine as sa_create_engine
from sqlalchemy.pool import NullPool, Pool
def db_path_to_sqlite_url(file: Path) -> QUrl:
kernelType = QSysInfo.kernelType()
# the slash count varies depending on the kernel
# https://docs.sqlalchemy.org/en/20/core/engines.html#sqlite
uri = file.resolve().as_uri()
if kernelType == "winnt":
return QUrl(uri.replace("file://", "sqlite://"))
else:
return QUrl(uri.replace("file://", "sqlite:///"))
def sqlite_url_to_db_path(url: str) -> Path:
db_file_url = url.replace("sqlite://", "file://")
return Path(QUrl(db_file_url).toLocalFile()).resolve()
def create_engine(_url: str | QUrl, pool: type[Pool] = NullPool) -> Engine:
url = _url.toString() if isinstance(_url, QUrl) else _url
return sa_create_engine(url, poolclass=pool)

View File

@ -0,0 +1,5 @@
from .base import Settings, settings
from .keys import SettingsKeys
from .values import SettingsValues
__all__ = ["settings", "Settings", "SettingsKeys", "SettingsValues"]

44
core/settings/base.py Normal file
View File

@ -0,0 +1,44 @@
import sys
from enum import Enum
from typing import Any
from PySide6.QtCore import QFileInfo, QSettings, Signal
from core.singleton import QSingleton
__all__ = ["Settings"]
TSettingsKey = str | Enum
class Settings(QSettings, metaclass=QSingleton):
updated = Signal(str)
def __init__(self, parent=None):
super().__init__(
QFileInfo(sys.argv[0]).dir().absoluteFilePath("arcaea_offline.ini"),
QSettings.Format.IniFormat,
parent,
)
def __settingsKey(self, key: TSettingsKey) -> str:
if isinstance(key, Enum):
return self.__settingsKey(key.value)
if isinstance(key, str):
return key
raise TypeError(f"{key!r} is not a valid key")
def setValue(self, key: TSettingsKey, value: Any) -> None:
_key = self.__settingsKey(key)
super().setValue(_key, value)
self.updated.emit(_key)
def stringValue(self, key: TSettingsKey) -> str | None:
_key = self.__settingsKey(key)
return self.value(_key, None, type=str)
settings = Settings()

27
core/settings/keys.py Normal file
View File

@ -0,0 +1,27 @@
from dataclasses import dataclass
from enum import StrEnum
class _General(StrEnum):
Language = "Language"
DatabaseType = "DatabaseType"
DatabaseConn = "DatabaseConn"
class _Ocr(StrEnum):
KnnModelFile = "Ocr/KnnModelFile"
B30KnnModelFile = "Ocr/B30KnnModelFile"
PhashDatabaseFile = "Ocr/PHashDatabaseFile"
DateSource = "Ocr/DateSource"
class _Andreal(StrEnum):
Folder = "Andreal/AndrealFolder"
Executable = "Andreal/AndrealExecutable"
@dataclass(frozen=True)
class SettingsKeys:
General = _General
Ocr = _Ocr
Andreal = _Andreal

23
core/settings/values.py Normal file
View File

@ -0,0 +1,23 @@
from dataclasses import dataclass
from enum import StrEnum
class GeneralDatabaseType(StrEnum):
FILE = "file"
URL = "url"
@dataclass(frozen=True)
class _Ocr_ScoreDateSource:
FileCreated: str = "FileCreated"
FileLastModified: str = "FileLastModified"
@dataclass(frozen=True)
class _Ocr:
DateSource = _Ocr_ScoreDateSource()
@dataclass(frozen=True)
class SettingsValues:
Ocr = _Ocr()

View File

@ -14,5 +14,5 @@ class Singleton(type, Generic[T]):
return cls._instance return cls._instance
class QObjectSingleton(type(QObject), Singleton): class QSingleton(type(QObject), Singleton):
pass pass

View File

@ -9,25 +9,21 @@ from PySide6.QtCore import QCoreApplication, QLocale
from PySide6.QtGui import QFontDatabase, QIcon from PySide6.QtGui import QFontDatabase, QIcon
from PySide6.QtWidgets import QApplication, QDialog, QMessageBox from PySide6.QtWidgets import QApplication, QDialog, QMessageBox
import ui.resources.resources_rc import ui.resources.resources_rc # noqa: F401
from core.settings import SettingsKeys, settings
from ui.extends.shared.language import changeAppLanguage from ui.extends.shared.language import changeAppLanguage
from ui.extends.shared.settings import Settings
from ui.implements.mainwindow import MainWindow from ui.implements.mainwindow import MainWindow
from ui.startup.databaseChecker import DatabaseChecker, DatabaseCheckerResult from ui.startup.databaseChecker import DatabaseChecker, DatabaseInitCheckResult
rootLogger = logging.getLogger("root") rootLogger = logging.getLogger("root")
rootLogger.setLevel(logging.DEBUG) rootLogger.setLevel(logging.DEBUG)
rootLoggerFormatter = logging.Formatter(
"[{levelname}]{asctime}|{name}: {msg}", "%m-%d %H:%M:%S", "{"
)
def handle_exception(exc_type, exc_value, exc_traceback): def handle_exception(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt): sys.__excepthook__(exc_type, exc_value, exc_traceback)
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
if issubclass(exc_type, KeyboardInterrupt):
return
rootLogger.critical( rootLogger.critical(
"Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback) "Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)
) )
@ -46,6 +42,11 @@ if __name__ == "__main__":
ymd = now.strftime("%Y%m%d") ymd = now.strftime("%Y%m%d")
hms = now.strftime("%H%M%S") hms = now.strftime("%H%M%S")
rootLoggerFormatter = logging.Formatter(
"[%(asctime)s/%(levelname)s] %(name)s (%(tag)s): %(message)s",
"%m-%d %H:%M:%S",
defaults={"tag": "/"},
)
rootLoggerFileHandler = logging.FileHandler( rootLoggerFileHandler = logging.FileHandler(
str(logFolder / f"arcaea-offline-pyside-ui-{ymd}-{hms}_debug.log"), str(logFolder / f"arcaea-offline-pyside-ui-{ymd}-{hms}_debug.log"),
encoding="utf-8", encoding="utf-8",
@ -59,18 +60,21 @@ if __name__ == "__main__":
rootLogger.addHandler(rootLoggerStdOutHandler) rootLogger.addHandler(rootLoggerStdOutHandler)
app = QApplication(sys.argv) app = QApplication(sys.argv)
locale = ( settingsLanguage = settings.stringValue(SettingsKeys.General.Language)
QLocale(Settings().language()) if Settings().language() else QLocale.system() locale = QLocale(settingsLanguage) if settingsLanguage else QLocale.system()
)
changeAppLanguage(locale) changeAppLanguage(locale)
QFontDatabase.addApplicationFont(":/fonts/GeosansLight.ttf") QFontDatabase.addApplicationFont(":/fonts/GeosansLight.ttf")
databaseChecker = DatabaseChecker() databaseChecker = DatabaseChecker()
databaseChecker.setWindowIcon(QIcon(":/images/icon.png")) databaseChecker.setWindowIcon(QIcon(":/images/icon.png"))
databaseCheckResult = databaseChecker.confirmDb() if Settings().databaseUrl() else 0 databaseCheckResult = (
databaseChecker.confirmDb()
if settings.stringValue(SettingsKeys.General.DatabaseUrl)
else DatabaseInitCheckResult.NONE
)
if not databaseCheckResult & DatabaseCheckerResult.Initted: if not databaseCheckResult & DatabaseInitCheckResult.INITIALIZED:
result = databaseChecker.exec() result = databaseChecker.exec()
if result == QDialog.DialogCode.Accepted: if result == QDialog.DialogCode.Accepted:

View File

@ -1,42 +1,84 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project] [project]
name = "arcaea-offline-pyside-ui" name = "arcaea-offline-pyside-ui"
version = "0.1.0" version = "0.3.9"
authors = [{ name = "283375", email = "log_283375@163.com" }] authors = [{ name = "283375", email = "log_283375@163.com" }]
description = "No description." description = "No description."
readme = "README.md" readme = "README.md"
requires-python = ">=3.9" requires-python = ">=3.9"
dependencies = [ dependencies = [
"arcaea-offline==0.2.1", "arcaea-offline==0.2.2",
"arcaea-offline-ocr==0.0.97", "arcaea-offline-ocr==0.0.99",
"exif==1.6.0",
"PySide6==6.5.2", "structlog~=25.4",
"colorama~=0.4.6",
"rich~=14.2",
"exif~=1.6.0",
"PySide6==6.10.0",
"Pillow~=10.1.0",
"materialyoucolor~=2.0.10",
] ]
classifiers = [ classifiers = [
"Development Status :: 3 - Alpha", "Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3", "Programming Language :: Python :: 3",
] ]
[project.optional-dependencies]
dev = [
"ruff>=0.14.2",
"pre-commit>=4.3.0",
"imageio",
"Nuitka~=2.7.6",
]
[project.urls] [project.urls]
"Homepage" = "https://github.com/283375/arcaea-offline-pyside-ui" "Homepage" = "https://github.com/ArcaeaOffline/client-pyside6"
"Bug Tracker" = "https://github.com/283375/arcaea-offline-pyside-ui/issues" "Bug Tracker" = "https://github.com/ArcaeaOffline/client-pyside6/issues"
[tool.black] [tool.ruff]
force-exclude = ''' exclude = ["*_ui.py", "*_rc.py"]
(
ui/designer
| .*_ui.py
| .*_rc.py
)
'''
[tool.isort] [tool.ruff.lint]
profile = "black" # Full list: https://docs.astral.sh/ruff/rules
extend_skip = ["ui/designer"] select = [
extend_skip_glob = ["*_ui.py", "*_rc.py"] "E", # pycodestyle (Error)
"W", # pycodestyle (Warning)
"F", # pyflakes
"I", # isort
"PL", # pylint
"N", # pep8-naming
"A", # flake8-builtins
"DTZ", # flake8-datetimez
"LOG", # flake8-logging
"Q", # flake8-quotes
"G", # flake8-logging-format
"PIE", # flake8-pie
"PT", # flake8-pytest-style
]
ignore = [
"E501", # line-too-long
"N802", # invalid-function-name
"N803", # invalid-argument-name
"N806", # non-lowercase-variable-in-function
"N815", # mixed-case-variable-in-class-scope
"N816", # mixed-case-variable-in-global-scope
"N999", # invalid-module-name
]
[tool.pyright] [tool.pyright]
ignore = ["**/__debug*.*"] ignore = ["**/__debug*.*"]
[tool.pyside6-project]
files = [
"app.py",
"ui/qmls/App.qml",
"ui/qmls/AppMain.qml",
"ui/qmls/Components/PlayResultDelegate.qml",
"ui/viewmodels/overview.py",
"ui/qmls/Overview.qml",
"ui/qmls/404.qml",
]

View File

@ -1,4 +0,0 @@
black == 23.7.0
isort == 5.12.0
imageio==2.31.4
Nuitka==1.8.4

View File

@ -1,4 +0,0 @@
arcaea-offline==0.2.1
arcaea-offline-ocr==0.0.97
exif==1.6.0
PySide6==6.5.2

View File

@ -22,39 +22,6 @@
<string>queue.title</string> <string>queue.title</string>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_2"> <layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>iccOptionsGroupBox</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QRadioButton" name="iccUseQtRadioButton">
<property name="text">
<string>icc.useQt</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="iccUsePILRadioButton">
<property name="text">
<string>icc.usePIL</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="iccTryFixRadioButton">
<property name="text">
<string>icc.tryFix</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item> <item>
<widget class="QPushButton" name="ocr_addImageButton"> <widget class="QPushButton" name="ocr_addImageButton">
<property name="text"> <property name="text">
@ -95,6 +62,13 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item>
<widget class="QPushButton" name="optionsDialogButton">
<property name="text">
<string>queue.optionsButton</string>
</property>
</widget>
</item>
<item> <item>
<widget class="QPushButton" name="ocr_startButton"> <widget class="QPushButton" name="ocr_startButton">
<property name="text"> <property name="text">

View File

@ -0,0 +1,140 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>OcrQueueOptionsDialog</class>
<widget class="QDialog" name="OcrQueueOptionsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>331</width>
<height>157</height>
</rect>
</property>
<property name="windowTitle">
<string>OCR Options</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>iccOptionsGroupBox</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QRadioButton" name="iccUseQtRadioButton">
<property name="text">
<string>icc.useQt</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="iccUsePILRadioButton">
<property name="text">
<string>icc.usePIL</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="iccTryFixRadioButton">
<property name="text">
<string>icc.tryFix</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>dateOptionsGroupBox</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QCheckBox" name="dateReadFromExifCheckbox">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>date.readFromExif</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="dateUseCreationDateRadioButton">
<property name="text">
<string>date.useCreationDate</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="dateUseModifyDateRadioButton">
<property name="text">
<string>date.useModifyDate</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>OcrQueueOptionsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>OcrQueueOptionsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -0,0 +1,107 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'ocrQueueOptionsDialog.ui'
##
## Created by: Qt User Interface Compiler version 6.5.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractButton, QApplication, QCheckBox, QDialog,
QDialogButtonBox, QGroupBox, QHBoxLayout, QRadioButton,
QSizePolicy, QVBoxLayout, QWidget)
class Ui_OcrQueueOptionsDialog(object):
def setupUi(self, OcrQueueOptionsDialog):
if not OcrQueueOptionsDialog.objectName():
OcrQueueOptionsDialog.setObjectName(u"OcrQueueOptionsDialog")
OcrQueueOptionsDialog.resize(331, 157)
self.verticalLayout = QVBoxLayout(OcrQueueOptionsDialog)
self.verticalLayout.setObjectName(u"verticalLayout")
self.horizontalLayout = QHBoxLayout()
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.groupBox = QGroupBox(OcrQueueOptionsDialog)
self.groupBox.setObjectName(u"groupBox")
self.verticalLayout_2 = QVBoxLayout(self.groupBox)
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
self.iccUseQtRadioButton = QRadioButton(self.groupBox)
self.iccUseQtRadioButton.setObjectName(u"iccUseQtRadioButton")
self.verticalLayout_2.addWidget(self.iccUseQtRadioButton)
self.iccUsePILRadioButton = QRadioButton(self.groupBox)
self.iccUsePILRadioButton.setObjectName(u"iccUsePILRadioButton")
self.iccUsePILRadioButton.setChecked(True)
self.verticalLayout_2.addWidget(self.iccUsePILRadioButton)
self.iccTryFixRadioButton = QRadioButton(self.groupBox)
self.iccTryFixRadioButton.setObjectName(u"iccTryFixRadioButton")
self.verticalLayout_2.addWidget(self.iccTryFixRadioButton)
self.horizontalLayout.addWidget(self.groupBox)
self.groupBox_2 = QGroupBox(OcrQueueOptionsDialog)
self.groupBox_2.setObjectName(u"groupBox_2")
self.verticalLayout_3 = QVBoxLayout(self.groupBox_2)
self.verticalLayout_3.setObjectName(u"verticalLayout_3")
self.dateReadFromExifCheckbox = QCheckBox(self.groupBox_2)
self.dateReadFromExifCheckbox.setObjectName(u"dateReadFromExifCheckbox")
self.dateReadFromExifCheckbox.setEnabled(False)
self.dateReadFromExifCheckbox.setChecked(True)
self.verticalLayout_3.addWidget(self.dateReadFromExifCheckbox)
self.dateUseCreationDateRadioButton = QRadioButton(self.groupBox_2)
self.dateUseCreationDateRadioButton.setObjectName(u"dateUseCreationDateRadioButton")
self.dateUseCreationDateRadioButton.setChecked(True)
self.verticalLayout_3.addWidget(self.dateUseCreationDateRadioButton)
self.dateUseModifyDateRadioButton = QRadioButton(self.groupBox_2)
self.dateUseModifyDateRadioButton.setObjectName(u"dateUseModifyDateRadioButton")
self.verticalLayout_3.addWidget(self.dateUseModifyDateRadioButton)
self.horizontalLayout.addWidget(self.groupBox_2)
self.verticalLayout.addLayout(self.horizontalLayout)
self.buttonBox = QDialogButtonBox(OcrQueueOptionsDialog)
self.buttonBox.setObjectName(u"buttonBox")
self.buttonBox.setOrientation(Qt.Horizontal)
self.buttonBox.setStandardButtons(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
self.verticalLayout.addWidget(self.buttonBox)
self.retranslateUi(OcrQueueOptionsDialog)
self.buttonBox.accepted.connect(OcrQueueOptionsDialog.accept)
self.buttonBox.rejected.connect(OcrQueueOptionsDialog.reject)
QMetaObject.connectSlotsByName(OcrQueueOptionsDialog)
# setupUi
def retranslateUi(self, OcrQueueOptionsDialog):
OcrQueueOptionsDialog.setWindowTitle(QCoreApplication.translate("OcrQueueOptionsDialog", u"OCR Options", None))
self.groupBox.setTitle(QCoreApplication.translate("OcrQueueOptionsDialog", u"iccOptionsGroupBox", None))
self.iccUseQtRadioButton.setText(QCoreApplication.translate("OcrQueueOptionsDialog", u"icc.useQt", None))
self.iccUsePILRadioButton.setText(QCoreApplication.translate("OcrQueueOptionsDialog", u"icc.usePIL", None))
self.iccTryFixRadioButton.setText(QCoreApplication.translate("OcrQueueOptionsDialog", u"icc.tryFix", None))
self.groupBox_2.setTitle(QCoreApplication.translate("OcrQueueOptionsDialog", u"dateOptionsGroupBox", None))
self.dateReadFromExifCheckbox.setText(QCoreApplication.translate("OcrQueueOptionsDialog", u"date.readFromExif", None))
self.dateUseCreationDateRadioButton.setText(QCoreApplication.translate("OcrQueueOptionsDialog", u"date.useCreationDate", None))
self.dateUseModifyDateRadioButton.setText(QCoreApplication.translate("OcrQueueOptionsDialog", u"date.useModifyDate", None))
# retranslateUi

View File

@ -17,8 +17,8 @@ from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QPalette, QPixmap, QRadialGradient, QTransform) QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QAbstractItemView, QApplication, QCheckBox, QGroupBox, from PySide6.QtWidgets import (QAbstractItemView, QApplication, QCheckBox, QGroupBox,
QHBoxLayout, QHeaderView, QLabel, QProgressBar, QHBoxLayout, QHeaderView, QLabel, QProgressBar,
QPushButton, QRadioButton, QSizePolicy, QSpacerItem, QPushButton, QSizePolicy, QSpacerItem, QTableView,
QTableView, QVBoxLayout, QWidget) QVBoxLayout, QWidget)
class Ui_OcrQueue(object): class Ui_OcrQueue(object):
def setupUi(self, OcrQueue): def setupUi(self, OcrQueue):
@ -34,29 +34,6 @@ class Ui_OcrQueue(object):
self.groupBox_3.setObjectName(u"groupBox_3") self.groupBox_3.setObjectName(u"groupBox_3")
self.verticalLayout_2 = QVBoxLayout(self.groupBox_3) self.verticalLayout_2 = QVBoxLayout(self.groupBox_3)
self.verticalLayout_2.setObjectName(u"verticalLayout_2") self.verticalLayout_2.setObjectName(u"verticalLayout_2")
self.groupBox = QGroupBox(self.groupBox_3)
self.groupBox.setObjectName(u"groupBox")
self.verticalLayout = QVBoxLayout(self.groupBox)
self.verticalLayout.setObjectName(u"verticalLayout")
self.iccUseQtRadioButton = QRadioButton(self.groupBox)
self.iccUseQtRadioButton.setObjectName(u"iccUseQtRadioButton")
self.verticalLayout.addWidget(self.iccUseQtRadioButton)
self.iccUsePILRadioButton = QRadioButton(self.groupBox)
self.iccUsePILRadioButton.setObjectName(u"iccUsePILRadioButton")
self.iccUsePILRadioButton.setChecked(True)
self.verticalLayout.addWidget(self.iccUsePILRadioButton)
self.iccTryFixRadioButton = QRadioButton(self.groupBox)
self.iccTryFixRadioButton.setObjectName(u"iccTryFixRadioButton")
self.verticalLayout.addWidget(self.iccTryFixRadioButton)
self.verticalLayout_2.addWidget(self.groupBox)
self.ocr_addImageButton = QPushButton(self.groupBox_3) self.ocr_addImageButton = QPushButton(self.groupBox_3)
self.ocr_addImageButton.setObjectName(u"ocr_addImageButton") self.ocr_addImageButton.setObjectName(u"ocr_addImageButton")
@ -78,6 +55,11 @@ class Ui_OcrQueue(object):
self.verticalLayout_2.addItem(self.verticalSpacer) self.verticalLayout_2.addItem(self.verticalSpacer)
self.optionsDialogButton = QPushButton(self.groupBox_3)
self.optionsDialogButton.setObjectName(u"optionsDialogButton")
self.verticalLayout_2.addWidget(self.optionsDialogButton)
self.ocr_startButton = QPushButton(self.groupBox_3) self.ocr_startButton = QPushButton(self.groupBox_3)
self.ocr_startButton.setObjectName(u"ocr_startButton") self.ocr_startButton.setObjectName(u"ocr_startButton")
@ -154,13 +136,10 @@ class Ui_OcrQueue(object):
def retranslateUi(self, OcrQueue): def retranslateUi(self, OcrQueue):
self.groupBox_3.setTitle(QCoreApplication.translate("OcrQueue", u"queue.title", None)) self.groupBox_3.setTitle(QCoreApplication.translate("OcrQueue", u"queue.title", None))
self.groupBox.setTitle(QCoreApplication.translate("OcrQueue", u"iccOptionsGroupBox", None))
self.iccUseQtRadioButton.setText(QCoreApplication.translate("OcrQueue", u"icc.useQt", None))
self.iccUsePILRadioButton.setText(QCoreApplication.translate("OcrQueue", u"icc.usePIL", None))
self.iccTryFixRadioButton.setText(QCoreApplication.translate("OcrQueue", u"icc.tryFix", None))
self.ocr_addImageButton.setText(QCoreApplication.translate("OcrQueue", u"queue.addImageButton", None)) self.ocr_addImageButton.setText(QCoreApplication.translate("OcrQueue", u"queue.addImageButton", None))
self.ocr_removeSelectedButton.setText(QCoreApplication.translate("OcrQueue", u"queue.removeSelected", None)) self.ocr_removeSelectedButton.setText(QCoreApplication.translate("OcrQueue", u"queue.removeSelected", None))
self.ocr_removeAllButton.setText(QCoreApplication.translate("OcrQueue", u"queue.removeAll", None)) self.ocr_removeAllButton.setText(QCoreApplication.translate("OcrQueue", u"queue.removeAll", None))
self.optionsDialogButton.setText(QCoreApplication.translate("OcrQueue", u"queue.optionsButton", None))
self.ocr_startButton.setText(QCoreApplication.translate("OcrQueue", u"queue.startOcrButton", None)) self.ocr_startButton.setText(QCoreApplication.translate("OcrQueue", u"queue.startOcrButton", None))
self.groupBox_5.setTitle(QCoreApplication.translate("OcrQueue", u"results", None)) self.groupBox_5.setTitle(QCoreApplication.translate("OcrQueue", u"results", None))
self.ocr_acceptSelectedButton.setText(QCoreApplication.translate("OcrQueue", u"results.acceptSelectedButton", None)) self.ocr_acceptSelectedButton.setText(QCoreApplication.translate("OcrQueue", u"results.acceptSelectedButton", None))
@ -169,4 +148,3 @@ class Ui_OcrQueue(object):
self.statusLabel.setText("") self.statusLabel.setText("")
pass pass
# retranslateUi # retranslateUi

View File

@ -1,124 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TabOverview</class>
<widget class="QWidget" name="TabOverview">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>696</width>
<height>509</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">TabOverview</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QWidget" name="widget" native="true"/>
</item>
<item>
<widget class="QLabel" name="databaseDescribeLabel">
<property name="text">
<string notr="true" extracomment="This database now have {} packs, {} songs, {} difficulties, {} chart info ({} complete) and {} scores.">...</string>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_2" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QWidget" name="widget_3" native="true">
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="b30Label">
<property name="font">
<font>
<pointsize>30</pointsize>
</font>
</property>
<property name="text">
<string notr="true">0.00</string>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="font">
<font>
<pointsize>20</pointsize>
</font>
</property>
<property name="text">
<string notr="true">B30</string>
</property>
<property name="alignment">
<set>Qt::AlignHCenter|Qt::AlignTop</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_4" native="true">
<property name="enabled">
<bool>false</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="r10Label">
<property name="enabled">
<bool>false</bool>
</property>
<property name="font">
<font>
<pointsize>30</pointsize>
</font>
</property>
<property name="text">
<string notr="true">--</string>
</property>
<property name="alignment">
<set>Qt::AlignBottom|Qt::AlignHCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="enabled">
<bool>false</bool>
</property>
<property name="font">
<font>
<pointsize>20</pointsize>
</font>
</property>
<property name="text">
<string notr="true">R10</string>
</property>
<property name="alignment">
<set>Qt::AlignHCenter|Qt::AlignTop</set>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -1,114 +0,0 @@
# -*- coding: utf-8 -*-
################################################################################
## Form generated from reading UI file 'tabOverview.ui'
##
## Created by: Qt User Interface Compiler version 6.5.2
##
## WARNING! All changes made in this file will be lost when recompiling UI file!
################################################################################
from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale,
QMetaObject, QObject, QPoint, QRect,
QSize, QTime, QUrl, Qt)
from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor,
QFont, QFontDatabase, QGradient, QIcon,
QImage, QKeySequence, QLinearGradient, QPainter,
QPalette, QPixmap, QRadialGradient, QTransform)
from PySide6.QtWidgets import (QApplication, QHBoxLayout, QLabel, QSizePolicy,
QVBoxLayout, QWidget)
class Ui_TabOverview(object):
def setupUi(self, TabOverview):
if not TabOverview.objectName():
TabOverview.setObjectName(u"TabOverview")
TabOverview.resize(696, 509)
TabOverview.setWindowTitle(u"TabOverview")
self.verticalLayout = QVBoxLayout(TabOverview)
self.verticalLayout.setObjectName(u"verticalLayout")
self.widget = QWidget(TabOverview)
self.widget.setObjectName(u"widget")
self.verticalLayout.addWidget(self.widget)
self.databaseDescribeLabel = QLabel(TabOverview)
self.databaseDescribeLabel.setObjectName(u"databaseDescribeLabel")
self.databaseDescribeLabel.setText(u"...")
self.verticalLayout.addWidget(self.databaseDescribeLabel)
self.widget_2 = QWidget(TabOverview)
self.widget_2.setObjectName(u"widget_2")
sizePolicy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Expanding)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.widget_2.sizePolicy().hasHeightForWidth())
self.widget_2.setSizePolicy(sizePolicy)
self.horizontalLayout = QHBoxLayout(self.widget_2)
self.horizontalLayout.setObjectName(u"horizontalLayout")
self.widget_3 = QWidget(self.widget_2)
self.widget_3.setObjectName(u"widget_3")
self.verticalLayout_2 = QVBoxLayout(self.widget_3)
self.verticalLayout_2.setObjectName(u"verticalLayout_2")
self.b30Label = QLabel(self.widget_3)
self.b30Label.setObjectName(u"b30Label")
font = QFont()
font.setPointSize(30)
self.b30Label.setFont(font)
self.b30Label.setText(u"0.00")
self.b30Label.setAlignment(Qt.AlignBottom|Qt.AlignHCenter)
self.verticalLayout_2.addWidget(self.b30Label)
self.label_2 = QLabel(self.widget_3)
self.label_2.setObjectName(u"label_2")
font1 = QFont()
font1.setPointSize(20)
self.label_2.setFont(font1)
self.label_2.setText(u"B30")
self.label_2.setAlignment(Qt.AlignHCenter|Qt.AlignTop)
self.verticalLayout_2.addWidget(self.label_2)
self.horizontalLayout.addWidget(self.widget_3)
self.widget_4 = QWidget(self.widget_2)
self.widget_4.setObjectName(u"widget_4")
self.widget_4.setEnabled(False)
self.verticalLayout_3 = QVBoxLayout(self.widget_4)
self.verticalLayout_3.setObjectName(u"verticalLayout_3")
self.r10Label = QLabel(self.widget_4)
self.r10Label.setObjectName(u"r10Label")
self.r10Label.setEnabled(False)
self.r10Label.setFont(font)
self.r10Label.setText(u"--")
self.r10Label.setAlignment(Qt.AlignBottom|Qt.AlignHCenter)
self.verticalLayout_3.addWidget(self.r10Label)
self.label_4 = QLabel(self.widget_4)
self.label_4.setObjectName(u"label_4")
self.label_4.setEnabled(False)
self.label_4.setFont(font1)
self.label_4.setText(u"R10")
self.label_4.setAlignment(Qt.AlignHCenter|Qt.AlignTop)
self.verticalLayout_3.addWidget(self.label_4)
self.horizontalLayout.addWidget(self.widget_4)
self.verticalLayout.addWidget(self.widget_2)
self.retranslateUi(TabOverview)
QMetaObject.connectSlotsByName(TabOverview)
# setupUi
def retranslateUi(self, TabOverview):
pass
# retranslateUi

View File

@ -5,7 +5,6 @@ from typing import Any, Callable, Optional, overload
from arcaea_offline.calculate import calculate_score_range from arcaea_offline.calculate import calculate_score_range
from arcaea_offline.database import Database from arcaea_offline.database import Database
from arcaea_offline.models import Chart, Score from arcaea_offline.models import Chart, Score
from arcaea_offline_ocr.b30.shared import B30OcrResultItem
from arcaea_offline_ocr.device.common import DeviceOcrResult from arcaea_offline_ocr.device.common import DeviceOcrResult
from PIL import Image from PIL import Image
from PIL.ImageQt import ImageQt from PIL.ImageQt import ImageQt
@ -140,7 +139,11 @@ class OcrQueueModel(QAbstractListModel):
return True return True
else: else:
logger.warning( logger.warning(
f"{repr(self)} setData at row {index.row()} with role {role} and value {value} rejected." "%r setData at row %d with role %d and value %s rejected!",
self,
index.row(),
role,
value,
) )
return False return False
@ -150,6 +153,7 @@ class OcrQueueModel(QAbstractListModel):
@iccOption.setter @iccOption.setter
def iccOption(self, opt: IccOption): def iccOption(self, opt: IccOption):
logger.debug("ICC option changed to %s", opt)
self.__iccOption = opt self.__iccOption = opt
@overload @overload
@ -158,8 +162,7 @@ class OcrQueueModel(QAbstractListModel):
image: str, image: str,
runnable: OcrRunnable = None, runnable: OcrRunnable = None,
process_func: Callable[[Optional[str], QImage, Any], Score] = None, process_func: Callable[[Optional[str], QImage, Any], Score] = None,
): ): ...
...
@overload @overload
def addItem( def addItem(
@ -167,8 +170,7 @@ class OcrQueueModel(QAbstractListModel):
image: QImage, image: QImage,
runnable: OcrRunnable = None, runnable: OcrRunnable = None,
process_func: Callable[[Optional[str], QImage, Any], Score] = None, process_func: Callable[[Optional[str], QImage, Any], Score] = None,
): ): ...
...
def addItem( def addItem(
self, self,
@ -178,7 +180,7 @@ class OcrQueueModel(QAbstractListModel):
): ):
if isinstance(image, str): if isinstance(image, str):
if image in self.imagePaths or not QFileInfo(image).exists(): if image in self.imagePaths or not QFileInfo(image).exists():
logger.warning(f"Attempting to add an invalid file {image}") logger.warning("Attempting to add an invalid file %s", image)
return return
imagePath = image imagePath = image
if self.iccOption == IccOption.TryFix: if self.iccOption == IccOption.TryFix:
@ -222,7 +224,7 @@ class OcrQueueModel(QAbstractListModel):
index = self.index(row, 0) index = self.index(row, 0)
imagePath: str = index.data(self.ImagePathRole) imagePath: str = index.data(self.ImagePathRole)
qImage: QImage = index.data(self.ImageQImageRole) qImage: QImage = index.data(self.ImageQImageRole)
logger.debug(f"update request: {result}@row{row}") logger.debug("update request: %r@row%d", result, row)
processOcrResultFunc = index.data(self.ProcessOcrResultFuncRole) processOcrResultFunc = index.data(self.ProcessOcrResultFuncRole)
chart, scoreInsert = processOcrResultFunc(imagePath, qImage, result) chart, scoreInsert = processOcrResultFunc(imagePath, qImage, result)
@ -293,8 +295,8 @@ class OcrQueueModel(QAbstractListModel):
self.__items.pop(row) self.__items.pop(row)
self.endRemoveRows() self.endRemoveRows()
return return
except Exception as e: except Exception:
logger.exception(f"Error accepting {repr(item)}") logger.exception("Error accepting %r", item)
return return
def acceptItems(self, __rows: list[int], ignoreValidate: bool = False): def acceptItems(self, __rows: list[int], ignoreValidate: bool = False):
@ -343,13 +345,11 @@ class OcrQueueTableProxyModel(QAbstractTableModel):
def retranslateHeaders(self): def retranslateHeaders(self):
self.__horizontalHeaders = [ self.__horizontalHeaders = [
# fmt: off
QCoreApplication.translate("OcrTableModel", "horizontalHeader.title.select"), QCoreApplication.translate("OcrTableModel", "horizontalHeader.title.select"),
QCoreApplication.translate("OcrTableModel", "horizontalHeader.title.imagePreview"), QCoreApplication.translate("OcrTableModel", "horizontalHeader.title.imagePreview"),
QCoreApplication.translate("OcrTableModel", "horizontalHeader.title.chart"), QCoreApplication.translate("OcrTableModel", "horizontalHeader.title.chart"),
QCoreApplication.translate("OcrTableModel", "horizontalHeader.title.score"), QCoreApplication.translate("OcrTableModel", "horizontalHeader.title.score"),
# fmt: on ] # fmt: skip
]
def sourceModel(self) -> OcrQueueModel: def sourceModel(self) -> OcrQueueModel:
return self.__sourceModel return self.__sourceModel

View File

@ -1,39 +0,0 @@
from arcaea_offline.database import Database
from arcaea_offline.models import Chart
from arcaea_offline.searcher import Searcher
from arcaea_offline.utils.rating import rating_class_to_short_text
from PySide6.QtCore import Qt
from PySide6.QtGui import QStandardItem, QStandardItemModel
class SearchCompleterModel(QStandardItemModel):
def __init__(self, parent=None):
super().__init__(parent)
self.searcher = Searcher()
self.db = Database()
def updateSearcherSongs(self):
with self.db.sessionmaker() as session:
self.searcher.import_songs(session)
def getSearchResult(self, kw: str):
self.clear()
songIds = self.searcher.search(kw)
charts: list[Chart] = []
for songId in songIds:
_charts = self.db.get_charts_by_song_id(songId)
_charts = sorted(_charts, key=lambda c: c.rating_class, reverse=True)
charts += _charts
for chart in charts:
displayText = (
f"{chart.title} [{rating_class_to_short_text(chart.rating_class)}]"
)
item = QStandardItem(kw)
item.setData(kw)
item.setData(displayText, Qt.ItemDataRole.UserRole + 75)
item.setData(f"{chart.song_id}, {chart.set}", Qt.ItemDataRole.UserRole + 76)
item.setData(chart, Qt.ItemDataRole.UserRole + 10)
self.appendRow(item)

View File

@ -1,16 +0,0 @@
from PySide6.QtGui import QColor
def mix_color(source_color: QColor, mix_color: QColor, mix_ratio: float = 0.5):
r = round((mix_color.red() - source_color.red()) * mix_ratio + source_color.red())
g = round(
(mix_color.green() - source_color.green()) * mix_ratio + source_color.green()
)
b = round(
(mix_color.blue() - source_color.blue()) * mix_ratio + source_color.blue()
)
a = round(
(mix_color.alpha() - source_color.alpha()) * mix_ratio + source_color.alpha()
)
return QColor(r, g, b, a)

View File

@ -7,7 +7,7 @@ from typing import Literal, Optional, overload
from arcaea_offline.models import Chart, Difficulty, Song from arcaea_offline.models import Chart, Difficulty, Song
from PySide6.QtCore import QFile from PySide6.QtCore import QFile
from .singleton import Singleton from core.singleton import Singleton
TPartnerModifier = dict[str, Literal[0, 1, 2]] TPartnerModifier = dict[str, Literal[0, 1, 2]]
@ -48,14 +48,12 @@ class Data(metaclass=Singleton):
return self.dataPath / "Arcaea" return self.dataPath / "Arcaea"
@overload @overload
def getJacketPath(self, chart: Chart, /) -> Path | None: def getJacketPath(self, chart: Chart, /) -> Path | None: ...
...
@overload @overload
def getJacketPath( def getJacketPath(
self, song: Song, difficulty: Optional[Difficulty] = None, / self, song: Song, difficulty: Optional[Difficulty] = None, /
) -> Path | None: ) -> Path | None: ...
...
def getJacketPath(self, *args) -> Path | None: def getJacketPath(self, *args) -> Path | None:
if isinstance(args[0], Chart): if isinstance(args[0], Chart):

View File

@ -1,14 +1,4 @@
from typing import Type from PySide6.QtCore import QObject, Signal
from PySide6.QtCore import QObject, QUrl, Signal
from sqlalchemy import Engine
from sqlalchemy import create_engine as sa_create_engine
from sqlalchemy.pool import NullPool, Pool
def create_engine(_url: str | QUrl, pool: Type[Pool] = NullPool) -> Engine:
url = _url.toString() if isinstance(_url, QUrl) else _url
return sa_create_engine(url, poolclass=pool)
class DatabaseUpdateSignals(QObject): class DatabaseUpdateSignals(QObject):

View File

@ -16,13 +16,11 @@ class DbB30TableModel(DbTableModel):
def retranslateHeaders(self): def retranslateHeaders(self):
self._horizontalHeaders = [ self._horizontalHeaders = [
# fmt: off
QCoreApplication.translate("DbB30TableModel", "horizontalHeader.id"), QCoreApplication.translate("DbB30TableModel", "horizontalHeader.id"),
QCoreApplication.translate("DbB30TableModel", "horizontalHeader.chart"), QCoreApplication.translate("DbB30TableModel", "horizontalHeader.chart"),
QCoreApplication.translate("DbB30TableModel", "horizontalHeader.score"), QCoreApplication.translate("DbB30TableModel", "horizontalHeader.score"),
QCoreApplication.translate("DbB30TableModel", "horizontalHeader.potential"), QCoreApplication.translate("DbB30TableModel", "horizontalHeader.potential"),
# fmt: on ] # fmt: skip
]
def syncDb(self): def syncDb(self):
self.beginResetModel() self.beginResetModel()
@ -39,7 +37,7 @@ class DbB30TableModel(DbTableModel):
(ScoreBest.song_id == Chart.song_id) (ScoreBest.song_id == Chart.song_id)
& (ScoreBest.rating_class == Chart.rating_class), & (ScoreBest.rating_class == Chart.rating_class),
) )
.order_by(ScoreBest.potential) .order_by(ScoreBest.potential.desc())
.limit(50) .limit(50)
.all() .all()
) )

View File

@ -24,13 +24,11 @@ class DbScoreTableModel(DbTableModel):
def retranslateHeaders(self): def retranslateHeaders(self):
self._horizontalHeaders = [ self._horizontalHeaders = [
# fmt: off
QCoreApplication.translate("DbScoreTableModel", "horizontalHeader.id"), QCoreApplication.translate("DbScoreTableModel", "horizontalHeader.id"),
QCoreApplication.translate("DbScoreTableModel", "horizontalHeader.chart"), QCoreApplication.translate("DbScoreTableModel", "horizontalHeader.chart"),
QCoreApplication.translate("DbScoreTableModel", "horizontalHeader.score"), QCoreApplication.translate("DbScoreTableModel", "horizontalHeader.score"),
QCoreApplication.translate("DbScoreTableModel", "horizontalHeader.potential"), QCoreApplication.translate("DbScoreTableModel", "horizontalHeader.potential"),
# fmt: on ] # fmt: skip
]
def syncDb(self): def syncDb(self):
self.beginResetModel() self.beginResetModel()
@ -154,7 +152,7 @@ class DbScoreTableModel(DbTableModel):
self.syncDb() self.syncDb()
return True return True
except Exception: except Exception:
logger.exception(f"Table[Score]: Cannot remove row {row}") logger.exception("Table[Score]: Cannot remove row %s", row)
return False return False
def removeRow(self, row: int, parent=...): def removeRow(self, row: int, parent=...):

View File

@ -1,111 +0,0 @@
import sys
from PySide6.QtCore import QFileInfo, QSettings, Signal
from .singleton import QObjectSingleton
__all__ = [
"LANGUAGE",
"DATABASE_URL",
"KNN_MODEL_FILE",
"B30_KNN_MODEL_FILE",
"PHASH_DATABASE_FILE",
"ANDREAL_FOLDER",
"ANDREAL_EXECUTABLE",
"Settings",
]
# a key without slashes will appear in the "General" section
# see https://doc.qt.io/qt-6/qsettings.html#Format-enum for details
LANGUAGE = "Language"
DATABASE_URL = "DatabaseUrl"
KNN_MODEL_FILE = "Ocr/KnnModelFile"
B30_KNN_MODEL_FILE = "Ocr/B30KnnModelFile"
PHASH_DATABASE_FILE = "Ocr/PHashDatabaseFile"
ANDREAL_FOLDER = "Andreal/AndrealFolder"
ANDREAL_EXECUTABLE = "Andreal/AndrealExecutable"
class Settings(QSettings, metaclass=QObjectSingleton):
updated = Signal(str)
def __init__(self, parent=None):
super().__init__(
QFileInfo(sys.argv[0]).dir().absoluteFilePath("arcaea_offline.ini"),
QSettings.Format.IniFormat,
parent,
)
def setValue(self, key: str, value) -> None:
super().setValue(key, value)
self.updated.emit(key)
def _strItem(self, key: str) -> str | None:
return self.value(key, None, str)
def _setStrItem(self, key: str, value: str):
self.setValue(key, value)
self.sync()
def _resetStrItem(self, key: str):
self.setValue(key, None)
self.sync()
def language(self):
return self._strItem(LANGUAGE)
def setLanguage(self, value: str):
self._setStrItem(LANGUAGE, value)
def databaseUrl(self):
return self._strItem(DATABASE_URL)
def setDatabaseUrl(self, value: str):
self._setStrItem(DATABASE_URL, value)
def knnModelFile(self):
return self._strItem(KNN_MODEL_FILE)
def setKnnModelFile(self, value: str):
self._setStrItem(KNN_MODEL_FILE, value)
def resetKnnModelFile(self):
self._resetStrItem(KNN_MODEL_FILE)
def b30KnnModelFile(self):
return self._strItem(B30_KNN_MODEL_FILE)
def setB30KnnModelFile(self, value: str):
self._setStrItem(B30_KNN_MODEL_FILE, value)
def resetB30KnnModelFile(self):
self._resetStrItem(B30_KNN_MODEL_FILE)
def phashDatabaseFile(self):
return self._strItem(PHASH_DATABASE_FILE)
def setPHashDatabaseFile(self, value: str):
self._setStrItem(PHASH_DATABASE_FILE, value)
def resetPHashDatabaseFile(self):
self._resetStrItem(PHASH_DATABASE_FILE)
def andrealFolder(self):
return self._strItem(ANDREAL_FOLDER)
def setAndrealFolder(self, value: str):
self._setStrItem(ANDREAL_FOLDER, value)
def resetAndrealFolder(self):
self._resetStrItem(ANDREAL_FOLDER)
def andrealExecutable(self):
return self._strItem(ANDREAL_EXECUTABLE)
def setAndrealExecutable(self, value: str):
self._setStrItem(ANDREAL_EXECUTABLE, value)
def resetAndrealExecutable(self):
self._resetStrItem(ANDREAL_EXECUTABLE)

View File

@ -6,10 +6,10 @@ from arcaea_offline_ocr.b30.chieri.v4.ocr import ChieriBotV4Ocr
from arcaea_offline_ocr.b30.shared import B30OcrResultItem from arcaea_offline_ocr.b30.shared import B30OcrResultItem
from PySide6.QtGui import QImage from PySide6.QtGui import QImage
logger = logging.getLogger(__name__)
from ui.extends.components.ocrQueue import OcrRunnable from ui.extends.components.ocrQueue import OcrRunnable
logger = logging.getLogger(__name__)
class ChieriV4OcrRunnable(OcrRunnable): class ChieriV4OcrRunnable(OcrRunnable):
def __init__(self, ocr: ChieriBotV4Ocr, component): def __init__(self, ocr: ChieriBotV4Ocr, component):

View File

@ -19,6 +19,7 @@ from arcaea_offline_ocr.phash_db import ImagePhashDatabase
from arcaea_offline_ocr.utils import imread_unicode from arcaea_offline_ocr.utils import imread_unicode
from PySide6.QtCore import QDateTime, QFileInfo from PySide6.QtCore import QDateTime, QFileInfo
from core.settings import SettingsKeys, SettingsValues, settings
from ui.extends.components.ocrQueue import OcrRunnable from ui.extends.components.ocrQueue import OcrRunnable
from ui.extends.shared.data import Data from ui.extends.shared.data import Data
@ -67,8 +68,14 @@ def getImageDate(imagePath: str) -> QDateTime:
if exifImage.has_exif and exifImage.get("datetime_original"): if exifImage.has_exif and exifImage.get("datetime_original"):
datetimeStr = exifImage.get("datetime_original") datetimeStr = exifImage.get("datetime_original")
datetime = QDateTime.fromString(datetimeStr, "yyyy:MM:dd hh:mm:ss") datetime = QDateTime.fromString(datetimeStr, "yyyy:MM:dd hh:mm:ss")
if not isinstance(datetime, QDateTime): if not isinstance(datetime, QDateTime):
datetime = QFileInfo(imagePath).birthTime() dateSource = settings.stringValue(SettingsKeys.Ocr.DateSource)
if dateSource == SettingsValues.Ocr.DateSource.FileLastModified:
datetime = QFileInfo(imagePath).lastModified()
else:
datetime = QFileInfo(imagePath).birthTime()
return datetime return datetime

View File

@ -41,7 +41,7 @@ class AndrealExecuteRunnable(QRunnable):
self.signals.completed.emit(self.jsonPath, imageBytes) self.signals.completed.emit(self.jsonPath, imageBytes)
except Exception as e: except Exception as e:
imageBytes = None imageBytes = None
logger.exception(f"{self.__class__.__name__} error") logger.exception("%s error", self.__class__.__name__)
self.signals.error.emit(self.jsonPath, str(e)) self.signals.error.emit(self.jsonPath, str(e))
finally: finally:
os.unlink(self.jsonPath) os.unlink(self.jsonPath)
@ -84,7 +84,10 @@ class AndrealHelper(QObject):
def request(self, jsonPath: str, arguments: list[str]): def request(self, jsonPath: str, arguments: list[str]):
logger.debug( logger.debug(
f"{self.__class__.__name__} received request {jsonPath=} {arguments=}" "%s received request jsonPath=%r arguments=%r",
self.__class__.__name__,
jsonPath,
arguments,
) )
runnable = AndrealExecuteRunnable(self.andrealExecutable, jsonPath, arguments) runnable = AndrealExecuteRunnable(self.andrealExecutable, jsonPath, arguments)
runnable.signals.error.connect(self.error) runnable.signals.error.connect(self.error)

View File

@ -2,9 +2,9 @@ from PySide6.QtCore import QDir, QFileInfo, Qt, Signal, Slot
from PySide6.QtGui import QDragEnterEvent, QDragLeaveEvent, QDropEvent from PySide6.QtGui import QDragEnterEvent, QDragLeaveEvent, QDropEvent
from PySide6.QtWidgets import QFileDialog, QWidget from PySide6.QtWidgets import QFileDialog, QWidget
from core.settings import settings
from ui.designer.components.fileSelector_ui import Ui_FileSelector from ui.designer.components.fileSelector_ui import Ui_FileSelector
from ui.extends.shared.language import LanguageChangeEventFilter from ui.extends.shared.language import LanguageChangeEventFilter
from ui.extends.shared.settings import Settings
class FileSelector(Ui_FileSelector, QWidget): class FileSelector(Ui_FileSelector, QWidget):
@ -122,13 +122,13 @@ class FileSelector(Ui_FileSelector, QWidget):
if self.__selectedFiles: if self.__selectedFiles:
return return
if value := Settings().value(self.settingsKey): if value := settings.value(self.settingsKey):
self.selectFile(value) self.selectFile(value)
Settings().updated.connect(self.settingsUpdated) settings.updated.connect(self.settingsUpdated)
def disconnectSettings(self): def disconnectSettings(self):
Settings().updated.disconnect(self.settingsUpdated) settings.updated.disconnect(self.settingsUpdated)
self.settingsKey = None self.settingsKey = None
def settingsUpdated(self, key: str): def settingsUpdated(self, key: str):
@ -139,4 +139,4 @@ class FileSelector(Ui_FileSelector, QWidget):
if self.__selectedFiles: if self.__selectedFiles:
return return
self.selectFile(Settings().value(self.settingsKey)) self.selectFile(settings.value(self.settingsKey))

View File

@ -2,7 +2,7 @@ from typing import Optional
from PySide6.QtCore import Qt, QTimer, Slot from PySide6.QtCore import Qt, QTimer, Slot
from PySide6.QtGui import QColor, QPalette from PySide6.QtGui import QColor, QPalette
from PySide6.QtWidgets import QButtonGroup, QWidget from PySide6.QtWidgets import QWidget
from ui.designer.components.ocrQueue_ui import Ui_OcrQueue from ui.designer.components.ocrQueue_ui import Ui_OcrQueue
from ui.extends.components.ocrQueue import ( from ui.extends.components.ocrQueue import (
@ -13,6 +13,7 @@ from ui.extends.components.ocrQueue import (
OcrScoreDelegate, OcrScoreDelegate,
) )
from ui.extends.shared.language import LanguageChangeEventFilter from ui.extends.shared.language import LanguageChangeEventFilter
from ui.implements.components.ocrQueueOptionsDialog import OcrQueueOptionsDialog
class OcrQueue(Ui_OcrQueue, QWidget): class OcrQueue(Ui_OcrQueue, QWidget):
@ -26,6 +27,9 @@ class OcrQueue(Ui_OcrQueue, QWidget):
self.__model: Optional[OcrQueueModel] = None self.__model: Optional[OcrQueueModel] = None
self.__tableProxyModel: Optional[OcrQueueTableProxyModel] = None self.__tableProxyModel: Optional[OcrQueueTableProxyModel] = None
self.optionsDialog = OcrQueueOptionsDialog(self)
self.optionsDialog.iccOptionsChanged.connect(self.setIccOption)
self.__firstResizeDone = False self.__firstResizeDone = False
self.resizeTimer = QTimer(self) self.resizeTimer = QTimer(self)
self.resizeTimer.timeout.connect(self.tableView.resizeRowsToContents) self.resizeTimer.timeout.connect(self.tableView.resizeRowsToContents)
@ -41,13 +45,6 @@ class OcrQueue(Ui_OcrQueue, QWidget):
tableViewPalette.setColor(QPalette.ColorRole.Highlight, highlightColor) tableViewPalette.setColor(QPalette.ColorRole.Highlight, highlightColor)
self.tableView.setPalette(tableViewPalette) self.tableView.setPalette(tableViewPalette)
self.iccOptionButtonGroup = QButtonGroup(self)
self.iccOptionButtonGroup.buttonToggled.connect(self.updateIccOption)
self.iccOptionButtonGroup.addButton(self.iccUseQtRadioButton, 0)
self.iccOptionButtonGroup.addButton(self.iccUsePILRadioButton, 1)
self.iccOptionButtonGroup.addButton(self.iccTryFixRadioButton, 2)
self.updateIccOption()
self.statusLabelClearTimer = QTimer(self) self.statusLabelClearTimer = QTimer(self)
self.statusLabelClearTimer.setSingleShot(True) self.statusLabelClearTimer.setSingleShot(True)
self.statusLabelClearTimer.timeout.connect(self.clearStatusMessage) self.statusLabelClearTimer.timeout.connect(self.clearStatusMessage)
@ -93,9 +90,10 @@ class OcrQueue(Ui_OcrQueue, QWidget):
self.ocr_acceptAllButton.setEnabled(__bool) self.ocr_acceptAllButton.setEnabled(__bool)
self.ocr_ignoreValidateCheckBox.setEnabled(__bool) self.ocr_ignoreValidateCheckBox.setEnabled(__bool)
def updateIccOption(self): @Slot(int)
def setIccOption(self, option):
if self.model(): if self.model():
self.model().iccOption = self.iccOptionButtonGroup.checkedId() self.model().iccOption = option
def showStatusMessage(self, message: str): def showStatusMessage(self, message: str):
self.statusLabel.setText(message) self.statusLabel.setText(message)
@ -131,6 +129,10 @@ class OcrQueue(Ui_OcrQueue, QWidget):
def modelReseted(self): def modelReseted(self):
self.progressBar.setMaximum(0) self.progressBar.setMaximum(0)
@Slot()
def on_optionsDialogButton_clicked(self):
self.optionsDialog.exec()
@Slot() @Slot()
def on_ocr_removeSelectedButton_clicked(self): def on_ocr_removeSelectedButton_clicked(self):
if self.model(): if self.model():

View File

@ -0,0 +1,49 @@
from PySide6.QtCore import Signal
from PySide6.QtWidgets import QButtonGroup, QDialog
from core.settings import SettingsKeys, SettingsValues, settings
from ui.designer.components.ocrQueueOptionsDialog_ui import Ui_OcrQueueOptionsDialog
class OcrQueueOptionsDialog(QDialog, Ui_OcrQueueOptionsDialog):
iccOptionsChanged = Signal(int)
def __init__(self, parent=None):
super(OcrQueueOptionsDialog, self).__init__(parent)
self.setupUi(self)
self.iccOptionButtonGroup = QButtonGroup(self)
self.iccOptionButtonGroup.buttonToggled.connect(
lambda: self.iccOptionsChanged.emit(self.iccOptionButtonGroup.checkedId())
)
self.iccOptionButtonGroup.addButton(self.iccUseQtRadioButton, 0)
self.iccOptionButtonGroup.addButton(self.iccUsePILRadioButton, 1)
self.iccOptionButtonGroup.addButton(self.iccTryFixRadioButton, 2)
self.scoreDateSourceButtonGroup = QButtonGroup(self)
self.scoreDateSourceButtonGroup.addButton(
self.dateUseCreationDateRadioButton, 0
)
self.scoreDateSourceButtonGroup.addButton(self.dateUseModifyDateRadioButton, 1)
self.scoreDateSourceButtonGroup.buttonClicked.connect(
self.on_scoreDateSourceButtonGroup_buttonClicked
)
settings.updated.connect(self.syncCheckboxesFromSettings)
self.syncCheckboxesFromSettings()
def syncCheckboxesFromSettings(self):
scoreDateSource = settings.stringValue(SettingsKeys.Ocr.DateSource)
if scoreDateSource == SettingsValues.Ocr.DateSource.FileLastModified:
self.dateUseModifyDateRadioButton.setChecked(True)
else:
self.dateUseCreationDateRadioButton.setChecked(True)
def on_scoreDateSourceButtonGroup_buttonClicked(self, button):
buttonId = self.scoreDateSourceButtonGroup.id(button)
if buttonId == 1:
value = SettingsValues.Ocr.DateSource.FileLastModified
else:
value = SettingsValues.Ocr.DateSource.FileCreated
settings.setValue(SettingsKeys.Ocr.DateSource, value)

View File

@ -2,7 +2,7 @@ from PySide6.QtCore import Slot
from PySide6.QtGui import QColor from PySide6.QtGui import QColor
from PySide6.QtWidgets import QGraphicsColorizeEffect, QRadioButton from PySide6.QtWidgets import QGraphicsColorizeEffect, QRadioButton
from ui.extends.shared.color import mix_color from core.color import mixColor
STYLESHEET = """ STYLESHEET = """
QRadioButton {{ QRadioButton {{
@ -40,7 +40,7 @@ class RatingClassRadioButton(QRadioButton):
def setColors(self, dark_color: QColor, text_color: QColor): def setColors(self, dark_color: QColor, text_color: QColor):
self._dark_color = dark_color self._dark_color = dark_color
self._text_color = text_color self._text_color = text_color
self._mid_color = mix_color(dark_color, text_color, 0.616) self._mid_color = mixColor(dark_color, text_color, 0.616)
self.updateEffects() self.updateEffects()
def isColorsSet(self) -> bool: def isColorsSet(self) -> bool:

View File

@ -1,3 +1,4 @@
import logging
from typing import Type from typing import Type
from PySide6.QtCore import Signal from PySide6.QtCore import Signal
@ -6,6 +7,8 @@ from PySide6.QtWidgets import QHBoxLayout, QSizePolicy, QVBoxLayout, QWidget
from ui.implements.components.ratingClassRadioButton import RatingClassRadioButton from ui.implements.components.ratingClassRadioButton import RatingClassRadioButton
logger = logging.getLogger(__name__)
class RatingClassSelector(QWidget): class RatingClassSelector(QWidget):
valueChanged = Signal() valueChanged = Signal()
@ -120,9 +123,10 @@ class RatingClassSelector(QWidget):
if ratingClass is None or isinstance(ratingClass, bool): if ratingClass is None or isinstance(ratingClass, bool):
button = self.sender() button = self.sender()
elif ratingClass in range(4): elif ratingClass in range(len(self.buttons)):
button = self.buttons[ratingClass] button = self.buttons[ratingClass]
else: else:
logger.debug("Cannot select ratingClass=%s, condition check failed", ratingClass)
return return
if not button.isEnabled(): if not button.isEnabled():

View File

@ -61,30 +61,22 @@ class ScoreEditor(Ui_ScoreEditor, QWidget):
VALIDATION_ITEMS_TEXT = [ VALIDATION_ITEMS_TEXT = [
[ [
# fmt: off
lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.chartIncomplete.title"), lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.chartIncomplete.title"),
lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.chartIncomplete.text"), lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.chartIncomplete.text"),
# fmt: on
], ],
[ [
# fmt: off
lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.scoreMismatch.title"), lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.scoreMismatch.title"),
lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.scoreMismatch.text"), lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.scoreMismatch.text"),
# fmt: on
], ],
[ [
# fmt: off
lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.emptyScore.title"), lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.emptyScore.title"),
lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.emptyScore.text"), lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.emptyScore.text"),
# fmt: on
], ],
[ [
# fmt: off
lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.scoreIncompleteForValidate.title"), lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.scoreIncompleteForValidate.title"),
lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.scoreIncompleteForValidate.text"), lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.scoreIncompleteForValidate.text"),
# fmt: on,
], ],
] ] # fmt: skip
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
@ -208,20 +200,16 @@ class ScoreEditor(Ui_ScoreEditor, QWidget):
if validate & ScoreValidateResult.ChartNotSet: if validate & ScoreValidateResult.ChartNotSet:
self.__triggerMessageBox( self.__triggerMessageBox(
"critical", "critical",
# fmt: off
QCoreApplication.translate("ScoreEditor", "confirmDialog.chartNotSet.title"), QCoreApplication.translate("ScoreEditor", "confirmDialog.chartNotSet.title"),
QCoreApplication.translate("ScoreEditor", "confirmDialog.chartNotSet.text"), QCoreApplication.translate("ScoreEditor", "confirmDialog.chartNotSet.text"),
# fmt: on ) # fmt: skip
)
return False return False
if validate & ScoreValidateResult.ScoreIncomplete: if validate & ScoreValidateResult.ScoreIncomplete:
self.__triggerMessageBox( self.__triggerMessageBox(
"critical", "critical",
# fmt: off
QCoreApplication.translate("ScoreEditor", "confirmDialog.scoreIncomplete.title"), QCoreApplication.translate("ScoreEditor", "confirmDialog.scoreIncomplete.title"),
QCoreApplication.translate("ScoreEditor", "confirmDialog.scoreIncomplete.text"), QCoreApplication.translate("ScoreEditor", "confirmDialog.scoreIncomplete.text"),
# fmt: on ) # fmt: skip
)
return False return False
# since validate may have multiple results # since validate may have multiple results
@ -347,10 +335,8 @@ class ScoreEditor(Ui_ScoreEditor, QWidget):
) )
if validate & ScoreValidateResult.ScoreIncompleteForValidate: if validate & ScoreValidateResult.ScoreIncompleteForValidate:
texts.append( texts.append(
# fmt: off
QCoreApplication.translate("ScoreEditor", "validate.scoreIncompleteForValidate") QCoreApplication.translate("ScoreEditor", "validate.scoreIncompleteForValidate")
# fmt: on ) # fmt: skip
)
if not texts: if not texts:
texts.append( texts.append(

View File

@ -1,14 +1,18 @@
import dataclasses
import logging import logging
import re import re
from enum import IntEnum from enum import IntEnum
from typing import Any
from arcaea_offline.database import Database from arcaea_offline.database import Database
from arcaea_offline.models import Chart from arcaea_offline.models import Chart
from PySide6.QtCore import QModelIndex, QSignalMapper, Qt, Signal, Slot from arcaea_offline.searcher import Searcher
from PySide6.QtWidgets import QCompleter, QWidget from arcaea_offline.utils.rating import rating_class_to_short_text
from PySide6.QtCore import QModelIndex, QObject, QSignalMapper, Qt, Signal, Slot
from PySide6.QtGui import QStandardItem, QStandardItemModel
from PySide6.QtWidgets import QComboBox, QCompleter, QWidget
from ui.designer.components.songIdSelector_ui import Ui_SongIdSelector from ui.designer.components.songIdSelector_ui import Ui_SongIdSelector
from ui.extends.components.songIdSelector import SearchCompleterModel
from ui.extends.shared.database import databaseUpdateSignals from ui.extends.shared.database import databaseUpdateSignals
from ui.extends.shared.delegates.descriptionDelegate import DescriptionDelegate from ui.extends.shared.delegates.descriptionDelegate import DescriptionDelegate
from ui.extends.shared.language import LanguageChangeEventFilter from ui.extends.shared.language import LanguageChangeEventFilter
@ -21,6 +25,168 @@ class SongIdSelectorMode(IntEnum):
Chart = 1 Chart = 1
# region logics
@dataclasses.dataclass
class _ComboBoxItem:
text: str
userData: Any
additionalData: dict[int, Any]
def apply(self, comboBox: QComboBox):
comboBox.addItem(self.text, self.userData)
index = comboBox.findText(self.text)
if index > -1:
for role, value in self.additionalData.items():
comboBox.setItemData(index, value, role)
class SearchCompleterModel(QStandardItemModel):
ChartRole = Qt.ItemDataRole.UserRole + 10
def __init__(self, parent=None):
super().__init__(parent)
self.searcher = Searcher()
self.db = Database()
def updateSearcherSongs(self):
with self.db.sessionmaker() as session:
self.searcher.import_songs(session)
def getSearchResult(self, kw: str):
self.clear()
songIds = self.searcher.search(kw)
charts: list[Chart] = []
for songId in songIds:
_charts = self.db.get_charts_by_song_id(songId)
_charts = sorted(_charts, key=lambda c: c.rating_class, reverse=True)
charts += _charts
for chart in charts:
displayText = (
f"{chart.title} [{rating_class_to_short_text(chart.rating_class)}]"
)
item = QStandardItem(kw)
item.setData(kw)
item.setData(displayText, DescriptionDelegate.MainTextRole)
item.setData(
f"{chart.song_id}, {chart.set}", DescriptionDelegate.DescriptionTextRole
)
item.setData(chart, self.ChartRole)
self.appendRow(item)
class SongIdSelectorViewModel(QObject):
packComboBoxItemsReady = Signal()
songIdComboBoxItemsReady = Signal()
def __init__(self, parent=None, *, db: Database):
super().__init__(parent)
if not isinstance(db, Database):
raise TypeError(
"`db` should be an instance of arcaea_offline.database.Database"
)
self.__db: Database = None # type: ignore
self.__mode = SongIdSelectorMode.SongId
self.__packComboBoxItems: list[_ComboBoxItem] = []
self.__songIdComboBoxItems: list[_ComboBoxItem] = []
self.setDatabase(db)
@property
def packComboBoxItems(self):
return self.__packComboBoxItems
@property
def songIdComboBoxItems(self):
return self.__songIdComboBoxItems
@property
def mode(self):
return self.__mode
@mode.setter
def mode(self, value):
if not isinstance(value, SongIdSelectorMode):
raise TypeError("value is not SongIdSelectorMode")
self.__mode = value
def setDatabase(self, db: Database):
self.__db = db
def updatePackComboBoxItems(self):
packs = self.__db.get_packs()
self.__packComboBoxItems.clear()
for pack in packs:
if re.search(r"_append_.*$", pack.id):
basePackId = re.sub(r"_append_.*$", "", pack.id)
basePackName = self.__db.get_pack(basePackId).name # type: ignore
packName = f"{basePackName} - {pack.name}"
else:
packName = pack.name
self.__packComboBoxItems.append(
_ComboBoxItem(
text=f"{packName} ({pack.id})",
userData=pack.id,
additionalData={
DescriptionDelegate.MainTextRole: packName,
DescriptionDelegate.DescriptionTextRole: pack.id,
},
)
)
self.packComboBoxItemsReady.emit()
def updateSongIdComboBoxItems(self, packId: str):
self.__songIdComboBoxItems.clear()
items = []
if self.mode == SongIdSelectorMode.SongId:
items = self.__db.get_songs_by_pack_id(packId)
elif self.mode == SongIdSelectorMode.Chart:
items = self.__db.get_charts_by_pack_id(packId)
else:
assert not "reachable"
insertedSongIds = []
for item in items:
if self.mode == SongIdSelectorMode.SongId:
itemId = item.id # type: ignore
elif self.mode == SongIdSelectorMode.Chart:
itemId = item.song_id # type: ignore
else:
continue
if itemId not in insertedSongIds:
self.__songIdComboBoxItems.append(
_ComboBoxItem(
text=f"{item.title} ({itemId})",
userData=itemId,
additionalData={
DescriptionDelegate.MainTextRole: item.title,
DescriptionDelegate.DescriptionTextRole: itemId,
},
)
)
insertedSongIds.append(itemId)
self.songIdComboBoxItemsReady.emit()
# endregion
class SongIdSelector(Ui_SongIdSelector, QWidget): class SongIdSelector(Ui_SongIdSelector, QWidget):
valueChanged = Signal() valueChanged = Signal()
quickSearchActivated = Signal(Chart) quickSearchActivated = Signal(Chart)
@ -33,6 +199,10 @@ class SongIdSelector(Ui_SongIdSelector, QWidget):
self.languageChangeEventFilter = LanguageChangeEventFilter(self) self.languageChangeEventFilter = LanguageChangeEventFilter(self)
self.installEventFilter(self.languageChangeEventFilter) self.installEventFilter(self.languageChangeEventFilter)
self.vm = SongIdSelectorViewModel(self, db=self.db)
self.vm.packComboBoxItemsReady.connect(self.fillPackComboBox)
self.vm.songIdComboBoxItemsReady.connect(self.fillSongIdComboBox)
# quick switch bindings # quick switch bindings
self.quickSwitchSignalMapper = QSignalMapper(self) self.quickSwitchSignalMapper = QSignalMapper(self)
self.previousPackageButton.clicked.connect(self.quickSwitchSignalMapper.map) self.previousPackageButton.clicked.connect(self.quickSwitchSignalMapper.map)
@ -75,6 +245,7 @@ class SongIdSelector(Ui_SongIdSelector, QWidget):
databaseUpdateSignals.songAddOrDelete.connect(self.updateDatabase) databaseUpdateSignals.songAddOrDelete.connect(self.updateDatabase)
def setMode(self, mode: SongIdSelectorMode): def setMode(self, mode: SongIdSelectorMode):
self.vm.mode = mode
self.mode = mode self.mode = mode
@Slot(str) @Slot(str)
@ -116,7 +287,7 @@ class SongIdSelector(Ui_SongIdSelector, QWidget):
pack = self.packComboBox.currentData() pack = self.packComboBox.currentData()
songId = self.songIdComboBox.currentData() songId = self.songIdComboBox.currentData()
self.fillPackComboBox() self.vm.updatePackComboBoxItems()
if pack: if pack:
self.selectPack(pack) self.selectPack(pack)
@ -125,58 +296,24 @@ class SongIdSelector(Ui_SongIdSelector, QWidget):
def fillPackComboBox(self): def fillPackComboBox(self):
self.packComboBox.clear() self.packComboBox.clear()
packs = self.db.get_packs()
for pack in packs: for item in self.vm.packComboBoxItems:
if isAppendPack := re.search(r"_append_.*$", pack.id): item.apply(self.packComboBox)
basePackId = re.sub(r"_append_.*$", "", pack.id)
basePackName = self.db.get_pack(basePackId).name
packName = f"{basePackName} - {pack.name}"
else:
packName = pack.name
self.packComboBox.addItem(f"{packName} ({pack.id})", pack.id)
row = self.packComboBox.count() - 1
self.packComboBox.setItemData(
row, packName, DescriptionDelegate.MainTextRole
)
self.packComboBox.setItemData(
row, pack.id, DescriptionDelegate.DescriptionTextRole
)
self.packComboBox.setCurrentIndex(-1) self.packComboBox.setCurrentIndex(-1)
def fillSongIdComboBox(self): def fillSongIdComboBox(self):
self.songIdComboBox.clear() self.songIdComboBox.clear()
if packId := self.packComboBox.currentData():
if self.mode == SongIdSelectorMode.SongId:
items = self.db.get_songs_by_pack_id(packId)
elif self.mode == SongIdSelectorMode.Chart:
items = self.db.get_charts_by_pack_id(packId)
else:
raise ValueError("Unknown SongIdSelectorMode.")
insertedSongIds = []
for item in items:
if self.mode == SongIdSelectorMode.SongId:
itemId = item.id
elif self.mode == SongIdSelectorMode.Chart:
itemId = item.song_id
else:
continue
if itemId not in insertedSongIds: for item in self.vm.songIdComboBoxItems:
self.songIdComboBox.addItem(f"{item.title} ({itemId})", itemId) item.apply(self.songIdComboBox)
insertedSongIds.append(itemId)
row = self.songIdComboBox.count() - 1
self.songIdComboBox.setItemData(
row, item.title, DescriptionDelegate.MainTextRole
)
self.songIdComboBox.setItemData(
row, itemId, DescriptionDelegate.DescriptionTextRole
)
self.songIdComboBox.setCurrentIndex(-1) self.songIdComboBox.setCurrentIndex(-1)
@Slot() @Slot()
def on_packComboBox_currentIndexChanged(self): def on_packComboBox_currentIndexChanged(self):
self.fillSongIdComboBox() if packId := self.packComboBox.currentData():
self.vm.updateSongIdComboBoxItems(packId)
@Slot(str) @Slot(str)
def on_searchLineEdit_textChanged(self, text: str): def on_searchLineEdit_textChanged(self, text: str):
@ -189,10 +326,10 @@ class SongIdSelector(Ui_SongIdSelector, QWidget):
packIdIndex = self.packComboBox.findData(packId) packIdIndex = self.packComboBox.findData(packId)
if packIdIndex > -1: if packIdIndex > -1:
self.packComboBox.setCurrentIndex(packIdIndex) self.packComboBox.setCurrentIndex(packIdIndex)
self.fillSongIdComboBox() self.vm.updateSongIdComboBoxItems(packId)
return True return True
else: else:
logger.warning(f'Attempting to select an unknown pack "{packId}"') logger.warning("Attempting to select an unknown pack [%s]", packId)
return False return False
def selectSongId(self, songId: str) -> bool: def selectSongId(self, songId: str) -> bool:
@ -202,7 +339,8 @@ class SongIdSelector(Ui_SongIdSelector, QWidget):
return True return True
else: else:
logger.warning( logger.warning(
f'Attempting to select an unknown song "{songId}", maybe try selecting a pack first?' "Attempting to select an unknown song [%s], maybe try selecting a pack first?",
songId,
) )
return False return False
@ -213,7 +351,7 @@ class SongIdSelector(Ui_SongIdSelector, QWidget):
@Slot(QModelIndex) @Slot(QModelIndex)
def searchCompleterSetSelection(self, index: QModelIndex): def searchCompleterSetSelection(self, index: QModelIndex):
chart: Chart = index.data(Qt.ItemDataRole.UserRole + 10) chart: Chart = index.data(SearchCompleterModel.ChartRole)
self.selectChart(chart) self.selectChart(chart)
self.quickSearchActivated.emit(chart) self.quickSearchActivated.emit(chart)

View File

@ -1,6 +1,7 @@
from PySide6.QtCore import QCoreApplication from PySide6.QtCore import QCoreApplication
from PySide6.QtWidgets import QLabel, QPushButton from PySide6.QtWidgets import QLabel, QPushButton
from core.settings import SettingsKeys, settings
from ui.implements.components.fileSelector import FileSelector from ui.implements.components.fileSelector import FileSelector
from ui.implements.settings.settingsBaseWidget import SettingsBaseWidget from ui.implements.settings.settingsBaseWidget import SettingsBaseWidget
@ -14,8 +15,8 @@ class SettingsAndreal(SettingsBaseWidget):
self.andrealFolderValueWidget.setMode( self.andrealFolderValueWidget.setMode(
self.andrealFolderValueWidget.getExistingDirectory self.andrealFolderValueWidget.getExistingDirectory
) )
if self.settings.andrealFolder(): if andrealFolder := settings.stringValue(SettingsKeys.Andreal.Folder):
self.andrealFolderValueWidget.selectFile(self.settings.andrealFolder()) self.andrealFolderValueWidget.selectFile(andrealFolder)
self.andrealFolderValueWidget.filesSelected.connect(self.setAndrealFolder) self.andrealFolderValueWidget.filesSelected.connect(self.setAndrealFolder)
self.andrealFolderResetButton.clicked.connect(self.resetAndrealFolder) self.andrealFolderResetButton.clicked.connect(self.resetAndrealFolder)
self.insertItem( self.insertItem(
@ -25,10 +26,8 @@ class SettingsAndreal(SettingsBaseWidget):
self.andrealFolderResetButton, self.andrealFolderResetButton,
) )
if self.settings.andrealExecutable(): if andrealExecutable := settings.stringValue(SettingsKeys.Andreal.Executable):
self.andrealExecutableValueWidget.selectFile( self.andrealExecutableValueWidget.selectFile(andrealExecutable)
self.settings.andrealExecutable()
)
self.andrealExecutableValueWidget.filesSelected.connect( self.andrealExecutableValueWidget.filesSelected.connect(
self.setAndrealExecutable self.setAndrealExecutable
) )
@ -44,21 +43,21 @@ class SettingsAndreal(SettingsBaseWidget):
selectedFile = self.andrealFolderValueWidget.selectedFiles() selectedFile = self.andrealFolderValueWidget.selectedFiles()
if selectedFile and selectedFile[0]: if selectedFile and selectedFile[0]:
file = selectedFile[0] file = selectedFile[0]
self.settings.setAndrealFolder(file) settings.setValue(SettingsKeys.Andreal.Folder, file)
def resetAndrealFolder(self): def resetAndrealFolder(self):
self.andrealFolderValueWidget.reset() self.andrealFolderValueWidget.reset()
self.settings.resetAndrealFolder() settings.setValue(SettingsKeys.Andreal.Folder, None)
def setAndrealExecutable(self): def setAndrealExecutable(self):
selectedFile = self.andrealExecutableValueWidget.selectedFiles() selectedFile = self.andrealExecutableValueWidget.selectedFiles()
if selectedFile and selectedFile[0]: if selectedFile and selectedFile[0]:
file = selectedFile[0] file = selectedFile[0]
self.settings.setAndrealExecutable(file) settings.setValue(SettingsKeys.Andreal.Executable, file)
def resetAndrealExecutable(self): def resetAndrealExecutable(self):
self.andrealExecutableValueWidget.reset() self.andrealExecutableValueWidget.reset()
self.settings.resetAndrealExecutable() settings.setValue(SettingsKeys.Andreal.Executable, None)
def setupUi(self, *args): def setupUi(self, *args):
self.andrealFolderLabel = QLabel(self) self.andrealFolderLabel = QLabel(self)

View File

@ -1,15 +1,15 @@
from PySide6.QtCore import Qt from PySide6.QtCore import Qt
from PySide6.QtWidgets import QLabel, QPushButton, QWidget from PySide6.QtWidgets import QLabel, QPushButton, QWidget
from core.settings import settings
from ui.designer.settings.settingsBaseWidget_ui import Ui_SettingsBaseWidget from ui.designer.settings.settingsBaseWidget_ui import Ui_SettingsBaseWidget
from ui.extends.shared.language import LanguageChangeEventFilter from ui.extends.shared.language import LanguageChangeEventFilter
from ui.extends.shared.settings import Settings
class SettingsBaseWidget(Ui_SettingsBaseWidget, QWidget): class SettingsBaseWidget(Ui_SettingsBaseWidget, QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.settings = Settings() self.settings = settings
self.languageChangeEventFilter = LanguageChangeEventFilter(self) self.languageChangeEventFilter = LanguageChangeEventFilter(self)
self.installEventFilter(self.languageChangeEventFilter) self.installEventFilter(self.languageChangeEventFilter)

View File

@ -1,6 +1,4 @@
import sys from PySide6.QtCore import QCoreApplication, QDir, QLocale
from PySide6.QtCore import QCoreApplication, QDir, QLocale, QProcess
from PySide6.QtWidgets import ( from PySide6.QtWidgets import (
QApplication, QApplication,
QCheckBox, QCheckBox,
@ -10,8 +8,8 @@ from PySide6.QtWidgets import (
QPushButton, QPushButton,
) )
from core.settings import SettingsKeys, settings
from ui.extends.shared.language import changeAppLanguage, localeToCode, localeToFullName from ui.extends.shared.language import changeAppLanguage, localeToCode, localeToFullName
from ui.extends.shared.settings import DATABASE_URL, LANGUAGE
from ui.implements.settings.settingsBaseWidget import SettingsBaseWidget from ui.implements.settings.settingsBaseWidget import SettingsBaseWidget
@ -33,8 +31,8 @@ class SettingsGeneral(SettingsBaseWidget):
self.languageFollowSystemCheckBox.toggled.connect( self.languageFollowSystemCheckBox.toggled.connect(
self.changeLanguageFollowSystem self.changeLanguageFollowSystem
) )
if self.settings.language(): if language := settings.stringValue(SettingsKeys.General.Language):
locale = QLocale(self.settings.language()) locale = QLocale(language)
index = self.languageValueWidget.findData(locale) index = self.languageValueWidget.findData(locale)
if index > -1: if index > -1:
self.languageValueWidget.setCurrentIndex(index) self.languageValueWidget.setCurrentIndex(index)
@ -51,7 +49,7 @@ class SettingsGeneral(SettingsBaseWidget):
self.insertItem( self.insertItem(
"dbUrl", "dbUrl",
self.dbUrlLabel, self.dbUrlLabel,
QLabel(self.settings.databaseUrl()), QLabel(settings.stringValue(SettingsKeys.General.DatabaseUrl)),
self.dbUrlResetButton, self.dbUrlResetButton,
) )
@ -59,13 +57,13 @@ class SettingsGeneral(SettingsBaseWidget):
locale = self.languageValueWidget.currentData() locale = self.languageValueWidget.currentData()
if locale: if locale:
changeAppLanguage(locale) changeAppLanguage(locale)
self.settings.setLanguage(localeToCode(locale)) settings.setValue(SettingsKeys.General.Language, localeToCode(locale))
def changeLanguageFollowSystem(self): def changeLanguageFollowSystem(self):
followSystem = self.languageFollowSystemCheckBox.isChecked() followSystem = self.languageFollowSystemCheckBox.isChecked()
self.languageValueWidget.setCurrentIndex(-1) self.languageValueWidget.setCurrentIndex(-1)
if followSystem: if followSystem:
self.settings.remove(LANGUAGE) settings.remove(SettingsKeys.General.Language)
changeAppLanguage(QLocale.system()) changeAppLanguage(QLocale.system())
self.languageValueWidget.setEnabled(False) self.languageValueWidget.setEnabled(False)
else: else:
@ -80,7 +78,7 @@ class SettingsGeneral(SettingsBaseWidget):
QMessageBox.StandardButton.No, QMessageBox.StandardButton.No,
) )
if userConfirm == QMessageBox.StandardButton.Yes: if userConfirm == QMessageBox.StandardButton.Yes:
self.settings.remove(DATABASE_URL) settings.remove(SettingsKeys.General.DatabaseUrl)
QApplication.instance().quit() QApplication.instance().quit()
def setupUi(self, *args): def setupUi(self, *args):
@ -99,10 +97,10 @@ class SettingsGeneral(SettingsBaseWidget):
# fmt: off # fmt: off
self.setTitle(QCoreApplication.translate("Settings", "general.title")) self.setTitle(QCoreApplication.translate("Settings", "general.title"))
self.languageLabel.setText(QCoreApplication.translate("Settings", "general.language.label")) self.languageLabel.setText(QCoreApplication.translate("Settings", "general.language.label"))
self.languageFollowSystemCheckBox.setText(QCoreApplication.translate("Settings", "general.language.followSystem")) self.languageFollowSystemCheckBox.setText(QCoreApplication.translate("Settings", "general.language.followSystem"))
self.dbUrlLabel.setText(QCoreApplication.translate("Settings", "general.dbUrl.label")) self.dbUrlLabel.setText(QCoreApplication.translate("Settings", "general.dbUrl.label"))
self.dbUrlResetButton.setText(QCoreApplication.translate("Settings", "resetButton")) self.dbUrlResetButton.setText(QCoreApplication.translate("Settings", "resetButton"))
# fmt: on # fmt: on

View File

@ -1,6 +1,7 @@
from PySide6.QtCore import QCoreApplication from PySide6.QtCore import QCoreApplication
from PySide6.QtWidgets import QLabel, QPushButton from PySide6.QtWidgets import QLabel, QPushButton
from core.settings import SettingsKeys, settings
from ui.implements.components.fileSelector import FileSelector from ui.implements.components.fileSelector import FileSelector
from ui.implements.settings.settingsBaseWidget import SettingsBaseWidget from ui.implements.settings.settingsBaseWidget import SettingsBaseWidget
@ -11,8 +12,8 @@ class SettingsOcr(SettingsBaseWidget):
self.setupUi(self) self.setupUi(self)
if self.settings.knnModelFile(): if knnModelFile := settings.stringValue(SettingsKeys.Ocr.KnnModelFile):
self.knnModelFileValueWidget.selectFile(self.settings.knnModelFile()) self.knnModelFileValueWidget.selectFile(knnModelFile)
self.knnModelFileValueWidget.filesSelected.connect(self.setKnnModelFile) self.knnModelFileValueWidget.filesSelected.connect(self.setKnnModelFile)
self.knnModelFileResetButton.clicked.connect(self.resetKnnModelFile) self.knnModelFileResetButton.clicked.connect(self.resetKnnModelFile)
self.insertItem( self.insertItem(
@ -22,8 +23,8 @@ class SettingsOcr(SettingsBaseWidget):
self.knnModelFileResetButton, self.knnModelFileResetButton,
) )
if self.settings.b30KnnModelFile(): if b30KnnModelFile := settings.stringValue(SettingsKeys.Ocr.B30KnnModelFile):
self.b30KnnModelFileValueWidget.selectFile(self.settings.b30KnnModelFile()) self.b30KnnModelFileValueWidget.selectFile(b30KnnModelFile)
self.b30KnnModelFileValueWidget.filesSelected.connect(self.setB30KnnModelFile) self.b30KnnModelFileValueWidget.filesSelected.connect(self.setB30KnnModelFile)
self.b30KnnModelFileResetButton.clicked.connect(self.resetB30KnnModelFile) self.b30KnnModelFileResetButton.clicked.connect(self.resetB30KnnModelFile)
self.insertItem( self.insertItem(
@ -33,10 +34,10 @@ class SettingsOcr(SettingsBaseWidget):
self.b30KnnModelFileResetButton, self.b30KnnModelFileResetButton,
) )
if self.settings.phashDatabaseFile(): if phashDatabaseFile := settings.stringValue(
self.phashDatabaseFileValueWidget.selectFile( SettingsKeys.Ocr.PhashDatabaseFile
self.settings.phashDatabaseFile() ):
) self.phashDatabaseFileValueWidget.selectFile(phashDatabaseFile)
self.phashDatabaseFileValueWidget.filesSelected.connect( self.phashDatabaseFileValueWidget.filesSelected.connect(
self.setPHashDatabaseFile self.setPHashDatabaseFile
) )
@ -52,31 +53,31 @@ class SettingsOcr(SettingsBaseWidget):
selectedFile = self.knnModelFileValueWidget.selectedFiles() selectedFile = self.knnModelFileValueWidget.selectedFiles()
if selectedFile and selectedFile[0]: if selectedFile and selectedFile[0]:
file = selectedFile[0] file = selectedFile[0]
self.settings.setKnnModelFile(file) settings.setValue(SettingsKeys.Ocr.KnnModelFile, file)
def resetKnnModelFile(self): def resetKnnModelFile(self):
self.knnModelFileValueWidget.reset() self.knnModelFileValueWidget.reset()
self.settings.resetKnnModelFile() settings.setValue(SettingsKeys.Ocr.KnnModelFile, None)
def setB30KnnModelFile(self): def setB30KnnModelFile(self):
selectedFile = self.b30KnnModelFileValueWidget.selectedFiles() selectedFile = self.b30KnnModelFileValueWidget.selectedFiles()
if selectedFile and selectedFile[0]: if selectedFile and selectedFile[0]:
file = selectedFile[0] file = selectedFile[0]
self.settings.setB30KnnModelFile(file) settings.setValue(SettingsKeys.Ocr.B30KnnModelFile, file)
def resetB30KnnModelFile(self): def resetB30KnnModelFile(self):
self.b30KnnModelFileValueWidget.reset() self.b30KnnModelFileValueWidget.reset()
self.settings.resetB30KnnModelFile() settings.setValue(SettingsKeys.Ocr.B30KnnModelFile, None)
def setPHashDatabaseFile(self): def setPHashDatabaseFile(self):
selectedFile = self.phashDatabaseFileValueWidget.selectedFiles() selectedFile = self.phashDatabaseFileValueWidget.selectedFiles()
if selectedFile and selectedFile[0]: if selectedFile and selectedFile[0]:
file = selectedFile[0] file = selectedFile[0]
self.settings.setPHashDatabaseFile(file) settings.setValue(SettingsKeys.Ocr.PhashDatabaseFile, file)
def resetPHashDatabaseFile(self): def resetPHashDatabaseFile(self):
self.phashDatabaseFileValueWidget.reset() self.phashDatabaseFileValueWidget.reset()
self.settings.resetPHashDatabaseFile() settings.setValue(SettingsKeys.Ocr.PhashDatabaseFile, None)
def setupUi(self, *args): def setupUi(self, *args):
self.knnModelFileLabel = QLabel(self) self.knnModelFileLabel = QLabel(self)

View File

@ -163,19 +163,15 @@ class TabDb_ChartInfoEditor(Ui_TabDb_ChartInfoEditor, QWidget):
QMessageBox.critical( QMessageBox.critical(
self, self,
None, None,
# fmt: off
QCoreApplication.translate("TabDb_ChartInfoEditor", "commit.chartNotSelected"), QCoreApplication.translate("TabDb_ChartInfoEditor", "commit.chartNotSelected"),
# fmt: on ) # fmt: skip
)
return return
if not self.constantLineEdit.hasAcceptableInput(): if not self.constantLineEdit.hasAcceptableInput():
QMessageBox.critical( QMessageBox.critical(
self, self,
None, None,
# fmt: off
QCoreApplication.translate("TabDb_ChartInfoEditor", "commit.constantRequired"), QCoreApplication.translate("TabDb_ChartInfoEditor", "commit.constantRequired"),
# fmt: on ) # fmt: skip
)
return return
constant = int(self.constantLineEdit.text()) constant = int(self.constantLineEdit.text())
@ -202,10 +198,8 @@ class TabDb_ChartInfoEditor(Ui_TabDb_ChartInfoEditor, QWidget):
QMessageBox.critical( QMessageBox.critical(
self, self,
None, None,
# fmt: off
QCoreApplication.translate("TabDb_ChartInfoEditor", "commit.chartNotSelected"), QCoreApplication.translate("TabDb_ChartInfoEditor", "commit.chartNotSelected"),
# fmt: on ) # fmt: skip
)
return return
chartInfo = self.db.get_chart_info(chart.song_id, chart.rating_class) chartInfo = self.db.get_chart_info(chart.song_id, chart.rating_class)
@ -213,12 +207,10 @@ class TabDb_ChartInfoEditor(Ui_TabDb_ChartInfoEditor, QWidget):
result = QMessageBox.warning( result = QMessageBox.warning(
self, self,
None, None,
# fmt: off
QCoreApplication.translate("TabDb_ChartInfoEditor", "deleteConfirm"), QCoreApplication.translate("TabDb_ChartInfoEditor", "deleteConfirm"),
# fmt: on
QMessageBox.StandardButton.Yes, QMessageBox.StandardButton.Yes,
QMessageBox.StandardButton.No, QMessageBox.StandardButton.No,
) ) # fmt: skip
if result == QMessageBox.StandardButton.Yes: if result == QMessageBox.StandardButton.Yes:
with self.db.sessionmaker() as session: with self.db.sessionmaker() as session:
session.delete(chartInfo) session.delete(chartInfo)

View File

@ -91,7 +91,7 @@ class TabDb_Manage(Ui_TabDb_Manage, QWidget):
session.commit() session.commit()
databaseUpdateSignals.songAddOrDelete.emit() databaseUpdateSignals.songAddOrDelete.emit()
itemNum = len([item for item in parser.parse() if isinstance(item, instance)]) itemNum = len([item for item in parser.parse() if isinstance(item, instance)])
logger.info(f"updated {itemNum} {logName} from {path}") logger.info("updated %d %s from %s", itemNum, logName, path)
return itemNum return itemNum
def importPacklist(self, packlistPath): def importPacklist(self, packlistPath):
@ -161,7 +161,7 @@ class TabDb_Manage(Ui_TabDb_Manage, QWidget):
return return
try: try:
logger.info(f"Importing {apkFile}") logger.info("Importing %s", apkFile)
with zipfile.ZipFile(apkFile) as zf: with zipfile.ZipFile(apkFile) as zf:
packlistPath = zipfile.Path(zf) / "assets" / "songs" / "packlist" packlistPath = zipfile.Path(zf) / "assets" / "songs" / "packlist"
@ -193,7 +193,9 @@ class TabDb_Manage(Ui_TabDb_Manage, QWidget):
db = Database() db = Database()
parser = St3ScoreParser(dbFile) parser = St3ScoreParser(dbFile)
logger.info( logger.info(
f"Got {len(parser.parse())} items from {dbFile}, writing into database..." "Got %d items from %s, writing into database...",
len(parser.parse()),
dbFile,
) )
with db.sessionmaker() as session: with db.sessionmaker() as session:
parser.write_database(session) parser.write_database(session)
@ -218,7 +220,9 @@ class TabDb_Manage(Ui_TabDb_Manage, QWidget):
db = Database() db = Database()
parser = ArcaeaOnlineParser(apiResultFile) parser = ArcaeaOnlineParser(apiResultFile)
logger.info( logger.info(
f"Got {len(parser.parse())} items from {apiResultFile}, writing into database..." "Got %d items from %s, writing into database...",
len(parser.parse()),
apiResultFile,
) )
with db.sessionmaker() as session: with db.sessionmaker() as session:
parser.write_database(session) parser.write_database(session)

View File

@ -154,23 +154,17 @@ class TabDb_RemoveDuplicateScores(Ui_TabDb_RemoveDuplicateScores, QWidget):
self.treeView.setItemDelegateForColumn(1, self.treeViewProxyDelegate) self.treeView.setItemDelegateForColumn(1, self.treeViewProxyDelegate)
self.quickSelect_comboBox.addItem( self.quickSelect_comboBox.addItem(
# fmt: off
QCoreApplication.translate("TabDb_RemoveDuplicateScores", "quickSelectComboBox.idEarlier"), QCoreApplication.translate("TabDb_RemoveDuplicateScores", "quickSelectComboBox.idEarlier"),
# fmt: on
QuickSelectComboBoxValues.ID_EARLIER QuickSelectComboBoxValues.ID_EARLIER
) ) # fmt: skip
self.quickSelect_comboBox.addItem( self.quickSelect_comboBox.addItem(
# fmt: off
QCoreApplication.translate("TabDb_RemoveDuplicateScores", "quickSelectComboBox.dateEarlier"), QCoreApplication.translate("TabDb_RemoveDuplicateScores", "quickSelectComboBox.dateEarlier"),
# fmt: on
QuickSelectComboBoxValues.DATE_EARLIER QuickSelectComboBoxValues.DATE_EARLIER
) ) # fmt: skip
self.quickSelect_comboBox.addItem( self.quickSelect_comboBox.addItem(
# fmt: off
QCoreApplication.translate("TabDb_RemoveDuplicateScores", "quickSelectComboBox.columnsIntegral"), QCoreApplication.translate("TabDb_RemoveDuplicateScores", "quickSelectComboBox.columnsIntegral"),
# fmt: on
QuickSelectComboBoxValues.COLUMNS_INTEGRAL QuickSelectComboBoxValues.COLUMNS_INTEGRAL
) ) # fmt: skip
def getQueryColumns(self): def getQueryColumns(self):
columns: list[InstrumentedAttribute] = [Score.song_id, Score.rating_class] columns: list[InstrumentedAttribute] = [Score.song_id, Score.rating_class]
@ -291,12 +285,12 @@ class TabDb_RemoveDuplicateScores(Ui_TabDb_RemoveDuplicateScores, QWidget):
confirm = QMessageBox.warning( confirm = QMessageBox.warning(
self, self,
None, None,
# fmt: off QCoreApplication.translate(
QCoreApplication.translate("TabDb_RemoveDuplicateScores", "deleteSelectionDialog.content {}").format(len(selectedScores)), "TabDb_RemoveDuplicateScores", "deleteSelectionDialog.content {}"
# fmt: on ).format(len(selectedScores)),
QMessageBox.StandardButton.Yes, QMessageBox.StandardButton.Yes,
QMessageBox.StandardButton.No, QMessageBox.StandardButton.No,
) ) # fmt: skip
if confirm != QMessageBox.StandardButton.Yes: if confirm != QMessageBox.StandardButton.Yes:
return return
@ -310,12 +304,11 @@ class TabDb_RemoveDuplicateScores(Ui_TabDb_RemoveDuplicateScores, QWidget):
@Slot() @Slot()
def on_scan_scanButton_clicked(self): def on_scan_scanButton_clicked(self):
if len(self.getQueryColumns()) <= 2: if len(self.getQueryColumns()) <= 2:
message = QCoreApplication.translate("TabDb_RemoveDuplicateScores", "scan_noColumnsDialog.content") # fmt: skip
result = QMessageBox.warning( result = QMessageBox.warning(
self, self,
None, None,
# fmt: off message,
QCoreApplication.translate("TabDb_RemoveDuplicateScores", "scan_noColumnsDialog.content"),
# fmt: on
QMessageBox.StandardButton.Yes, QMessageBox.StandardButton.Yes,
QMessageBox.StandardButton.No, QMessageBox.StandardButton.No,
) )

View File

@ -9,6 +9,7 @@ from PIL import Image
from PySide6.QtCore import Signal, Slot from PySide6.QtCore import Signal, Slot
from PySide6.QtWidgets import QFileDialog, QMessageBox, QWidget from PySide6.QtWidgets import QFileDialog, QMessageBox, QWidget
from core.settings import SettingsKeys
from ui.designer.tabs.tabOcr.tabOcr_B30_ui import Ui_TabOcr_B30 from ui.designer.tabs.tabOcr.tabOcr_B30_ui import Ui_TabOcr_B30
from ui.extends.components.ocrQueue import OcrQueueModel from ui.extends.components.ocrQueue import OcrQueueModel
from ui.extends.ocr.dependencies import ( from ui.extends.ocr.dependencies import (
@ -16,11 +17,6 @@ from ui.extends.ocr.dependencies import (
getPhashDatabaseStatusText, getPhashDatabaseStatusText,
) )
from ui.extends.shared.language import LanguageChangeEventFilter from ui.extends.shared.language import LanguageChangeEventFilter
from ui.extends.shared.settings import (
B30_KNN_MODEL_FILE,
KNN_MODEL_FILE,
PHASH_DATABASE_FILE,
)
from ui.extends.tabs.tabOcr.tabOcr_B30 import ChieriV4OcrRunnable, b30ResultToScore from ui.extends.tabs.tabOcr.tabOcr_B30 import ChieriV4OcrRunnable, b30ResultToScore
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -55,9 +51,15 @@ class TabOcr_B30(Ui_TabOcr_B30, QWidget):
self.ocr = None self.ocr = None
logger.info("Applying settings...") logger.info("Applying settings...")
self.dependencies_knnModelSelector.connectSettings(KNN_MODEL_FILE) self.dependencies_knnModelSelector.connectSettings(
self.dependencies_b30KnnModelSelector.connectSettings(B30_KNN_MODEL_FILE) SettingsKeys.Ocr.KnnModelFile
self.dependencies_phashDatabaseSelector.connectSettings(PHASH_DATABASE_FILE) )
self.dependencies_b30KnnModelSelector.connectSettings(
SettingsKeys.Ocr.B30KnnModelFile
)
self.dependencies_phashDatabaseSelector.connectSettings(
SettingsKeys.Ocr.PhashDatabaseFile
)
self.ocrQueueModel = OcrQueueModel(self) self.ocrQueueModel = OcrQueueModel(self)
self.ocrQueue.setModel(self.ocrQueueModel) self.ocrQueue.setModel(self.ocrQueueModel)

View File

@ -11,10 +11,10 @@ from arcaea_offline_ocr.phash_db import ImagePhashDatabase
from PySide6.QtCore import Slot from PySide6.QtCore import Slot
from PySide6.QtWidgets import QApplication, QFileDialog, QMessageBox, QWidget from PySide6.QtWidgets import QApplication, QFileDialog, QMessageBox, QWidget
from core.settings import SettingsKeys
from ui.designer.tabs.tabOcr.tabOcr_Device_ui import Ui_TabOcr_Device from ui.designer.tabs.tabOcr.tabOcr_Device_ui import Ui_TabOcr_Device
from ui.extends.components.ocrQueue import OcrQueueModel from ui.extends.components.ocrQueue import OcrQueueModel
from ui.extends.shared.language import LanguageChangeEventFilter from ui.extends.shared.language import LanguageChangeEventFilter
from ui.extends.shared.settings import KNN_MODEL_FILE, PHASH_DATABASE_FILE
from ui.extends.tabs.tabOcr.tabOcr_Device import ScoreConverter, TabDeviceOcrRunnable from ui.extends.tabs.tabOcr.tabOcr_Device import ScoreConverter, TabDeviceOcrRunnable
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -54,8 +54,12 @@ class TabOcr_Device(Ui_TabOcr_Device, QWidget):
) )
logger.info("Applying settings...") logger.info("Applying settings...")
self.dependencies_knnModelSelector.connectSettings(KNN_MODEL_FILE) self.dependencies_knnModelSelector.connectSettings(
self.dependencies_phashDatabaseSelector.connectSettings(PHASH_DATABASE_FILE) SettingsKeys.Ocr.KnnModelFile
)
self.dependencies_phashDatabaseSelector.connectSettings(
SettingsKeys.Ocr.PhashDatabaseFile
)
self.options_usePresetCheckBox.setChecked(True) self.options_usePresetCheckBox.setChecked(True)
self.options_usePresetCheckBox.setEnabled(False) self.options_usePresetCheckBox.setEnabled(False)

View File

@ -1,55 +0,0 @@
import logging
from arcaea_offline.database import Database
from PySide6.QtCore import QCoreApplication
from PySide6.QtGui import QShowEvent
from PySide6.QtWidgets import QWidget
from ui.designer.tabs.tabOverview_ui import Ui_TabOverview
from ui.extends.shared.language import LanguageChangeEventFilter
logger = logging.getLogger(__name__)
class TabOverview(Ui_TabOverview, QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setupUi(self)
self.languageChangeEventFilter = LanguageChangeEventFilter(self)
self.installEventFilter(self.languageChangeEventFilter)
self.db = Database()
def showEvent(self, event: QShowEvent) -> None:
self.updateOverview()
return super().showEvent(event)
def updateOverview(self):
try:
b30 = self.db.get_b30() or 0.00
self.b30Label.setText(str(f"{b30:.3f}"))
except Exception:
logger.exception("Cannot get b30:")
self.b30Label.setText("ERR")
try:
self.databaseDescribeLabel.setText(
self.describeFormatString.format(
self.db.count_packs(),
self.db.count_songs(),
self.db.count_difficulties(),
self.db.count_chart_infos(),
self.db.count_complete_chart_infos(),
self.db.count_scores(),
)
)
except Exception:
logger.exception("Cannot update overview:")
self.databaseDescribeLabel.setText("ERR")
def retranslateUi(self, *args):
super().retranslateUi(self)
# fmt: off
self.describeFormatString = QCoreApplication.translate("TabOverview", "databaseDescribeLabel {} {} {} {} {} {}")
# fmt: on

View File

@ -11,9 +11,9 @@ from PySide6.QtCore import QCoreApplication, QDir, QFileInfo, Qt, Slot
from PySide6.QtGui import QGuiApplication, QImage, QPainter, QPaintEvent, QPixmap from PySide6.QtGui import QGuiApplication, QImage, QPainter, QPaintEvent, QPixmap
from PySide6.QtWidgets import QButtonGroup, QFileDialog, QLabel, QMessageBox, QWidget from PySide6.QtWidgets import QButtonGroup, QFileDialog, QLabel, QMessageBox, QWidget
from core.settings import SettingsKeys
from ui.designer.tabs.tabTools.tabTools_Andreal_ui import Ui_TabTools_Andreal from ui.designer.tabs.tabTools.tabTools_Andreal_ui import Ui_TabTools_Andreal
from ui.extends.shared.language import LanguageChangeEventFilter from ui.extends.shared.language import LanguageChangeEventFilter
from ui.extends.shared.settings import ANDREAL_EXECUTABLE, ANDREAL_FOLDER
from ui.extends.tabs.tabTools.tabTools_Andreal import AndrealHelper from ui.extends.tabs.tabTools.tabTools_Andreal import AndrealHelper
from ui.implements.components.chartSelector import ChartSelector from ui.implements.components.chartSelector import ChartSelector
from ui.implements.components.songIdSelector import SongIdSelectorMode from ui.implements.components.songIdSelector import SongIdSelectorMode
@ -80,8 +80,8 @@ class TabTools_Andreal(Ui_TabTools_Andreal, QWidget):
self.andrealFolderSelector.filesSelected.connect(self.setHelperPaths) self.andrealFolderSelector.filesSelected.connect(self.setHelperPaths)
self.andrealExecutableSelector.filesSelected.connect(self.setHelperPaths) self.andrealExecutableSelector.filesSelected.connect(self.setHelperPaths)
self.andrealFolderSelector.connectSettings(ANDREAL_FOLDER) self.andrealFolderSelector.connectSettings(SettingsKeys.Andreal.Folder)
self.andrealExecutableSelector.connectSettings(ANDREAL_EXECUTABLE) self.andrealExecutableSelector.connectSettings(SettingsKeys.Andreal.Executable)
self.generatePreviewButton.clicked.connect(self.requestPreview) self.generatePreviewButton.clicked.connect(self.requestPreview)
self.generateImageButton.clicked.connect(self.requestGenerate) self.generateImageButton.clicked.connect(self.requestGenerate)
@ -131,13 +131,8 @@ class TabTools_Andreal(Ui_TabTools_Andreal, QWidget):
@Slot() @Slot()
def on_imageTypeWhatIsThisButton_clicked(self): def on_imageTypeWhatIsThisButton_clicked(self):
QMessageBox.information( message = QCoreApplication.translate("TabTools_Andreal", "imageWhatIsThisDialog.description") # fmt: skip
self, QMessageBox.information(self, None, message)
None,
# fmt: off
QCoreApplication.translate("TabTools_Andreal", "imageWhatIsThisDialog.description"),
# fmt: on
)
def imageFormat(self): def imageFormat(self):
buttonId = self.imageFormatButtonGroup.checkedId() buttonId = self.imageFormatButtonGroup.checkedId()

View File

@ -90,10 +90,8 @@ class PlayRatingCalculatorDialog(QDialog):
self.acceptButton = QPushButton(self) self.acceptButton = QPushButton(self)
self.acceptButton.setText( self.acceptButton.setText(
# fmt: off
QCoreApplication.translate("StepCalculator", "playRatingCalculatorDialog.acceptButton") QCoreApplication.translate("StepCalculator", "playRatingCalculatorDialog.acceptButton")
# fmt: on ) # fmt: skip
)
self.acceptButton.setEnabled(False) self.acceptButton.setEnabled(False)
self.verticalLayout.addWidget(self.acceptButton) self.verticalLayout.addWidget(self.acceptButton)

22
ui/qmls/404.qml Normal file
View File

@ -0,0 +1,22 @@
import QtQuick
import QtQuick.Controls
Page {
Column {
anchors.centerIn: parent
width: parent.width
Label {
width: parent.width
text: '?'
font.pointSize: 50
horizontalAlignment: Qt.AlignCenter
}
Label {
width: parent.width
text: 'Placeholder page'
horizontalAlignment: Qt.AlignCenter
}
}
}

63
ui/qmls/App.qml Normal file
View File

@ -0,0 +1,63 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "./DatabaseChecker" as DatabaseChecker
ApplicationWindow {
visible: true
width: 800
height: 600
SystemPalette {
id: systemPaletteActive
colorGroup: SystemPalette.Active
}
SystemPalette {
id: systemPaletteDisabled
colorGroup: SystemPalette.Disabled
}
palette {
accent: systemPaletteActive.accent
alternateBase: systemPaletteActive.alternateBase
base: systemPaletteActive.base
button: systemPaletteActive.button
buttonText: systemPaletteActive.buttonText
dark: systemPaletteActive.dark
highlight: systemPaletteActive.highlight
highlightedText: systemPaletteActive.highlightedText
light: systemPaletteActive.light
mid: systemPaletteActive.mid
midlight: systemPaletteActive.midlight
placeholderText: systemPaletteActive.placeholderText
shadow: systemPaletteActive.shadow
text: systemPaletteActive.text
window: systemPaletteActive.window
windowText: systemPaletteActive.windowText
disabled {
button: systemPaletteDisabled.button
buttonText: systemPaletteDisabled.buttonText
}
inactive {
button: systemPaletteDisabled.button
buttonText: systemPaletteDisabled.buttonText
}
}
StackLayout {
id: stackLayout
anchors.fill: parent
currentIndex: 0
DatabaseChecker.Index {
onReady: parent.currentIndex = 1
}
AppMain {}
}
}

93
ui/qmls/AppMain.qml Normal file
View File

@ -0,0 +1,93 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
RowLayout {
id: layout
spacing: 5
SystemPalette {
id: systemPalette
}
ListModel {
id: navListModel
ListElement {
_id: 'home'
label: 'Overview'
qmlSource: 'Overview.qml'
}
ListElement {
_id: 'database'
label: 'Database'
qmlSource: '404.qml'
}
}
ListView {
id: navListView
Layout.preferredWidth: 200
Layout.fillHeight: true
model: navListModel
focus: true
delegate: Item {
id: navListItem
required property int index
required property string label
property bool isActive: navListView.currentIndex === index
width: parent.width
height: 30
MouseArea {
anchors.fill: parent
onClicked: () => {
navListView.currentIndex = navListItem.index;
}
}
Label {
anchors.margins: 5
anchors.fill: parent
text: parent.label
color: parent.isActive ? systemPalette.highlightedText : systemPalette.text
z: 10
Behavior on color {
ColorAnimation {
duration: 150
}
}
}
Rectangle {
width: parent.isActive ? parent.width : 0
Behavior on width {
NumberAnimation {
easing.type: Easing.OutQuad
duration: 200
}
}
height: parent.height
color: systemPalette.highlight
z: 1
}
}
}
Loader {
Layout.preferredWidth: 500
Layout.fillWidth: true
Layout.fillHeight: true
source: navListView.currentIndex > -1 ? navListModel.get(navListView.currentIndex).qmlSource : '404.qml'
}
}

View File

@ -0,0 +1,25 @@
import QtQuick
import QtQuick.Dialogs
import internal.ui.utils
SelectorBase {
id: base
FolderDialog {
id: folderDialog
selectedFolder: base.directoryUrl
onAccepted: {
base.directoryUrl = this.selectedFolder;
this.currentFolder = this.selectedFolder;
}
}
property alias directoryUrl: base.url
shouldAcceptUrl: url => UrlUtils.isDir(url)
onBrowseButtonClicked: {
folderDialog.open();
}
placeholderText: '<font color="gray">Select a directory…</font>'
}

View File

@ -0,0 +1,33 @@
import QtQuick
import QtQuick.Dialogs
import internal.ui.utils
SelectorBase {
id: base
FileDialog {
id: fileDialog
onAccepted: {
base.url = this.selectedFile;
}
}
function isFileUrlValid(url: url): bool {
return url.toString().startsWith("file://");
}
property alias fileUrl: base.url
onFileUrlChanged: {
if (isFileUrlValid(fileUrl)) {
fileDialog.selectedFile = fileUrl;
fileDialog.currentFolder = UrlUtils.parent(fileUrl);
}
}
shouldAcceptUrl: url => UrlUtils.isFile(url)
onBrowseButtonClicked: {
fileDialog.open();
}
placeholderText: '<font color="gray">Select a file…</font>'
}

View File

@ -0,0 +1,124 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import QtQuick.VectorImage
import "../libs/formatters.mjs" as Formatters
RowLayout {
id: root
required property var playResult
property alias pr: root.playResult
spacing: 8
SystemPalette {
id: systemPalette
}
component PFLLabel: RowLayout {
required property string label
required property var value
property color color: systemPalette.text
spacing: 0.5
Label {
Layout.alignment: Qt.AlignBaseline
text: parent.label
font.pointSize: 8
font.bold: true
color: parent.color
}
Label {
Layout.alignment: Qt.AlignBaseline
text: parent.value ?? '-'
color: parent.color
}
}
function getGradeIcon(gradeLabel: string): string {
const scheme = Application.styleHints.colorScheme == Qt.ColorScheme.Dark ? 'dark' : 'light';
const filenameMap = {
'EX+': 'ex-plus',
'EX': 'ex',
'AA': 'aa',
'A': 'a',
'B': 'b',
'C': 'c',
'D': 'd'
};
let filenameBase = filenameMap[gradeLabel];
if (scheme === 'dark') {
filenameBase += '-dark';
}
return `qrc:/images/grades/${filenameBase}.svg`;
}
TextMetrics {
id: gradeTextMetrics
text: 'EX+'
font.pointSize: 18
font.bold: true
}
VectorImage {
id: gradeIcon
Layout.preferredWidth: gradeTextMetrics.width
Layout.preferredHeight: gradeTextMetrics.width
fillMode: VectorImage.PreserveAspectFit
preferredRendererType: VectorImage.CurveRenderer
source: root.getGradeIcon(Formatters.scoreToGrade(root.pr.score))
}
ColumnLayout {
Layout.fillWidth: true
spacing: 1
Label {
Layout.fillWidth: true
text: root.pr.score
font.pointSize: 16
}
RowLayout {
Layout.fillWidth: true
Layout.leftMargin: 2
spacing: 5
PFLLabel {
label: 'P'
value: root.pr.pure
color: appTheme.pure
}
PFLLabel {
label: 'F'
value: root.pr.far
color: appTheme.far
}
PFLLabel {
label: 'L'
value: root.pr.lost
color: appTheme.lost
}
}
PFLLabel {
Layout.fillWidth: true
Layout.leftMargin: 2
label: 'MR'
value: root.pr.maxRecall
}
}
}

View File

@ -0,0 +1,13 @@
import QtQuick.Controls
import QtQuick.Layouts
Label {
Layout.topMargin: 7
Layout.bottomMargin: 10
anchors.topMargin: 7
anchors.bottomMargin: 10
font.pointSize: 14
font.bold: true
}

View File

@ -0,0 +1,64 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import internal.ui.utils
RowLayout {
id: root
required property var shouldAcceptUrl // function (url)
signal browseButtonClicked
property string placeholderText: '<font color="gray">…</font>'
required property url url
onUrlChanged: {
Qt.callLater(() => {
updateLabel();
});
}
function updateLabel(): void {
urlLabel.text = url.toString().length > 0 ? UrlFormatUtils.toLocalFile(url) : root.placeholderText;
}
spacing: 2
Label {
id: urlLabel
Layout.fillWidth: true
text: root.placeholderText
DropArea {
anchors.fill: parent
onEntered: drag => {
if (!drag.hasUrls || drag.urls.length !== 1) {
drag.accepted = false;
return false;
}
const url = drag.urls[0];
const shouldAccept = root.shouldAcceptUrl(url);
if (!shouldAccept) {
drag.accepted = false;
return false;
}
urlLabel.text = `<font color="gray">Drop "<font color="text">${UrlUtils.name(url)}</font>"?</font>`;
}
onExited: {
root.updateLabel();
}
onDropped: drop => {
root.url = drop.urls[0];
root.updateLabel();
}
}
}
Button {
text: "Browse"
onClicked: root.browseButtonClicked()
}
}

View File

@ -0,0 +1,9 @@
module Components
internal SelectorBase SelectorBase.qml
DirectorySelector 1.0 DirectorySelector.qml
FileSelector 1.0 FileSelector.qml
SectionTitle 1.0 SectionTitle.qml
PlayResultDelegate 1.0 PlayResultDelegate.qml

View File

@ -0,0 +1,56 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "../Components"
ColumnLayout {
id: root
signal confirm
required property url directoryUrl
required property string filename
GridLayout {
columns: 2
Label {
text: "Directory"
}
DirectorySelector {
Layout.fillWidth: true
directoryUrl: root.directoryUrl
onDirectoryUrlChanged: {
root.directoryUrl = this.directoryUrl;
}
}
Label {
text: "Filename"
}
TextField {
Layout.fillWidth: true
text: root.filename
placeholderText: 'Please enter…'
onEditingFinished: {
root.filename = this.text;
}
onAccepted: {
confirmButton.click();
}
}
}
Button {
id: confirmButton
Layout.alignment: Qt.AlignRight
text: 'Confirm'
onClicked: root.confirm()
}
}

View File

@ -0,0 +1,31 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Dialog {
id: root
required property string databaseUrl
padding: 10
title: qsTr('Confirm Database Connection')
standardButtons: Dialog.Ok | Dialog.Cancel
modal: true
Overlay.modal: Rectangle {
color: Qt.alpha('gray', 0.2)
}
ColumnLayout {
Label {
text: 'The connection below will be saved to settings. Confirm?'
}
Pane {
Label {
id: databaseUrlLabel
text: root.databaseUrl
}
}
}
}

View File

@ -0,0 +1,34 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Dialog {
id: root
required property string errorTitle
required property string errorMessage
padding: 10
title: qsTr('Error')
standardButtons: Dialog.Ok
modal: true
Overlay.modal: Rectangle {
color: Qt.alpha('darkgray', 0.5)
}
ColumnLayout {
spacing: 5
Label {
font.pointSize: 12
font.bold: true
text: root.errorTitle
}
Label {
text: root.errorMessage
}
}
}

View File

@ -0,0 +1,13 @@
import QtQuick
import QtQuick.Layouts
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
Layout.topMargin: 5
Layout.bottomMargin: 5
color: "lightgray"
opacity: 0.5
}

View File

@ -0,0 +1,124 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import internal.ui.vm 1.0
import "../Components"
Page {
id: root
property bool showConfirmConnectionDialog: false
signal ready
function confirm(source: string): void {
if (!source)
throw new Error("source is required");
const shouldConfirm = (source === 'continueButton' && vm.canConfirmSilently) || source === 'dialog';
if (shouldConfirm) {
vm.confirmCurrentConnection();
root.ready();
} else {
root.showConfirmConnectionDialog = true;
}
}
DatabaseInitViewModel {
id: vm
onSelectFileUrlChanged: {
selectOrCreate.selectFileUrl = this.selectFileUrl;
}
onDirectoryUrlChanged: {
selectOrCreate.directoryUrl = this.directoryUrl;
}
onFilenameChanged: {
selectOrCreate.filename = this.filename;
}
onDatabaseUrlChanged: {
confirmConnectionDialog.databaseUrl = this.databaseUrl;
}
onDatabaseInfoChanged: {
databaseInfo.info = this.databaseInfo;
}
onCanContinueChanged: {
continueButton.enabled = this.canContinue;
}
}
Page {
padding: 10
anchors.fill: parent
Dialog_ConfirmConnection {
id: confirmConnectionDialog
anchors.centerIn: parent
visible: root.showConfirmConnectionDialog
onAccepted: root.confirm('dialog')
onClosed: root.showConfirmConnectionDialog = false
databaseUrl: vm.databaseUrl
}
ColumnLayout {
width: parent.width
spacing: 2
SectionTitle {
text: qsTr('Select or Create Database')
}
Section_SelectOrCreate {
id: selectOrCreate
selectFileUrl: vm.selectFileUrl
createDirectoryUrl: vm.directoryUrl
createFilename: vm.filename
onUiModeChanged: uiMode => vm.uiMode = uiMode
onSelectFileUrlChanged: {
vm.selectFileUrl = selectFileUrl;
}
onCreateDirectoryUrlChanged: {
vm.directoryUrl = createDirectoryUrl;
}
onCreateFilenameChanged: {
vm.filename = createFilename;
}
onConfirmCreate: {
vm.loadDatabaseInfo();
}
}
HorizontalDivider {}
SectionTitle {
text: 'Database Status'
}
Section_DatabaseInfo {
id: databaseInfo
info: vm.databaseInfo
}
}
footer: Pane {
padding: 5
RowLayout {
Button {
id: continueButton
Layout.alignment: Qt.AlignHCenter
text: qsTr('Continue')
enabled: vm.canContinue
onClicked: root.confirm('continueButton')
}
}
}
}
}

View File

@ -0,0 +1,137 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
ColumnLayout {
id: root
property var info: {
'url': undefined,
'error': {
'title': undefined,
'message': undefined
},
'initialized': undefined,
'version': undefined
}
property bool showErrorDialog: false
function hasError(): bool {
return info?.error?.title !== undefined || info?.error?.message !== undefined;
}
function displayText(value): string {
return value ?? '-';
}
function displayBool(value): string {
if (value === undefined)
return '-';
return value ? `<font color="${appTheme.success}">Yes</font>` : `<font color="${appTheme.error}">No</font>`;
}
component LabelLabel: Label {
Layout.alignment: Qt.AlignRight | Qt.AlignBaseline
font.pointSize: 10
}
SystemPalette {
id: palette
}
Dialog_Error {
parent: Overlay.overlay
anchors.centerIn: parent
visible: root.hasError() && root.showErrorDialog
errorTitle: root.displayText(root.info?.error?.title)
errorMessage: root.displayText(root.info?.error?.message)
onClosed: root.showErrorDialog = false
}
Pane {
clip: true
background: Rectangle {
color: Qt.darker(Qt.alpha(palette.window, 0.9), 0.2)
}
// Layout.preferredHeight: root.hasError() ? this.implicitHeight : 0
Layout.preferredHeight: 0
Behavior on Layout.preferredHeight {
PropertyAnimation {
duration: 300
easing.type: Easing.InOutCubic
}
}
ColumnLayout {
anchors.fill: parent
Label {
font.bold: true
text: root.displayText(root.info.error?.title)
}
Label {
text: root.displayText(root.info.error?.message)
}
}
}
GridLayout {
columns: 2
columnSpacing: 10
LabelLabel {
text: 'Connection'
}
Label {
text: root.info.url
}
LabelLabel {
text: 'Initialized'
}
Label {
text: root.displayBool(root.info?.initialized)
}
LabelLabel {
text: 'Version'
}
Label {
text: root.displayText(root.info?.version)
}
Column {
Layout.alignment: Qt.AlignRight | Qt.AlignBaseline
LabelLabel {
text: 'Error'
}
ToolButton {
Layout.preferredWidth: root.hasError() ? this.implicitWidth : 0
Behavior on Layout.preferredWidth {
PropertyAnimation {
duration: 300
easing.type: Easing.InOutCubic
}
}
text: '[?]'
onClicked: root.showErrorDialog = true
}
}
Label {
Layout.alignment: Qt.AlignBaseline
text: root.displayText(root.info?.error?.title)
}
}
}

View File

@ -0,0 +1,69 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import "../Components"
ColumnLayout {
id: root
property alias selectFileUrl: fileSelector.url
property alias createDirectoryUrl: fileCreator.directoryUrl
property alias createFilename: fileCreator.filename
signal uiModeChanged(string value)
signal confirmCreate
ListModel {
id: uiModeModel
ListElement {
value: 'select'
}
ListElement {
value: 'create'
}
}
TabBar {
id: tabBar
Layout.fillWidth: true
TabButton {
text: qsTr("Select Existing")
width: implicitWidth + 10
}
TabButton {
text: qsTr("Create New File")
width: implicitWidth + 10
}
onCurrentIndexChanged: {
const idx = this.currentIndex;
root.uiModeChanged(uiModeModel.get(idx).value);
}
}
StackLayout {
currentIndex: tabBar.currentIndex
Layout.fillWidth: true
Layout.preferredHeight: children[currentIndex].height
Behavior on Layout.preferredHeight {
PropertyAnimation {
duration: 300
easing.type: Easing.InOutCubic
}
}
clip: true
FileSelector {
id: fileSelector
}
DatabaseFileCreator {
id: fileCreator
onConfirm: root.confirmCreate()
}
}
}

62
ui/qmls/Overview.qml Normal file
View File

@ -0,0 +1,62 @@
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import internal.ui.vm 1.0
Page {
id: root
property alias b30: vm.b30
OverviewViewModel {
id: vm
}
component Display: RowLayout {
required property string label
required property string value
spacing: 5
// implicitHeight: valueText.implicitHeight
Label {
text: parent.label
Layout.alignment: Qt.AlignBaseline
}
Label {
id: valueText
text: parent.value
font.pointSize: 18
Layout.alignment: Qt.AlignBaseline
}
}
RowLayout {
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignBottom
Display {
label: 'B30'
value: root.b30 >= 0 ? root.b30.toFixed(3) : 'N/A'
}
Display {
label: 'R10'
value: 'Not supported'
}
}
Button {
Layout.alignment: Qt.AlignBottom
// TODO: icon
text: 'Reload'
onClicked: vm.reload()
}
}
}

View File

@ -0,0 +1,18 @@
export function scoreToGrade(score) {
const gradeThresholds = [
{ minimum: 9900000, grade: "EX+" },
{ minimum: 9800000, grade: "EX" },
{ minimum: 9500000, grade: "AA" },
{ minimum: 9200000, grade: "A" },
{ minimum: 8900000, grade: "B" },
{ minimum: 8600000, grade: "C" },
];
for (const threshold of gradeThresholds) {
if (score >= threshold.minimum) {
return threshold.grade;
}
}
return "D";
}

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="1 1 23 23"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="a-dark.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="false"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
labelstyle="default"
inkscape:clip-to-page="false"
inkscape:zoom="32"
inkscape:cx="9.3125"
inkscape:cy="16.015625"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true">
<sodipodi:guide
position="3.0000001,19"
orientation="14,0"
id="guide1"
inkscape:locked="false" />
<sodipodi:guide
position="3.0000001,4.0000001"
orientation="0,1"
id="guide2"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="21,5.0000001"
orientation="-14,0"
id="guide3"
inkscape:locked="false" />
<sodipodi:guide
position="21,20"
orientation="0,1"
id="guide4"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="12,9.0000002"
orientation="0,1"
id="guide10"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="15.5,20.4"
orientation="0,1"
id="guide19"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="12,20"
orientation="-1,0"
id="guide5"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<defs
id="defs1">
<linearGradient
id="linearGradient34"
inkscape:collect="always">
<stop
style="stop-color:#62476c;stop-opacity:1;"
offset="0"
id="stop34" />
<stop
style="stop-color:#ab73a3;stop-opacity:1;"
offset="1"
id="stop35" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient34"
id="linearGradient35"
x1="11.470703"
y1="3.5996094"
x2="11.470703"
y2="20"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:label="base"
inkscape:groupmode="layer"
id="layer1"
style="display:inline;fill:url(#linearGradient35);fill-opacity:1;stroke:none">
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#333333;fill-opacity:0.8;stroke:none;stroke-linecap:square;stroke-miterlimit:10;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="M 10.722656,3.0996094 5.0820312,19.513672 4.7421875,20.5 h 3.171875 l 1.375,-4 h 5.4218755 l 1.374999,4 h 3.171875 L 18.917969,19.513672 13.277344,3.0996094 Z M 12,8.6113281 13.679688,13.5 h -3.359375 z"
id="path6"
inkscape:label="stroke"
sodipodi:nodetypes="ccccccccccccccc" />
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:url(#linearGradient35);fill-opacity:1;stroke:none;stroke-linecap:square;stroke-miterlimit:10;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="M 11.080078,3.6000004 5.5546869,19.676172 5.4433588,20.000391 h 2.1132816 l 1.3750004,-4 h 6.1367182 l 1.375,4 h 2.113282 L 18.445312,19.676172 12.919922,3.6000004 Z M 12,7.0746098 14.380859,14.000391 H 9.6191408 Z"
id="path7"
inkscape:label="A" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,126 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="1 1 23 23"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="a.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="false"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
labelstyle="default"
inkscape:clip-to-page="false"
inkscape:zoom="45.254834"
inkscape:cx="7.2036503"
inkscape:cy="15.280136"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true">
<sodipodi:guide
position="3.0000001,19"
orientation="14,0"
id="guide1"
inkscape:locked="false" />
<sodipodi:guide
position="3.0000001,4.0000001"
orientation="0,1"
id="guide2"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="21,5.0000001"
orientation="-14,0"
id="guide3"
inkscape:locked="false" />
<sodipodi:guide
position="21,20"
orientation="0,1"
id="guide4"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="12,9.0000002"
orientation="0,1"
id="guide10"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="15.5,20.4"
orientation="0,1"
id="guide19"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="12,20"
orientation="-1,0"
id="guide5"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<defs
id="defs1">
<linearGradient
id="linearGradient34"
inkscape:collect="always">
<stop
style="stop-color:#46324d;stop-opacity:1;"
offset="0"
id="stop34" />
<stop
style="stop-color:#92588a;stop-opacity:1;"
offset="1"
id="stop35" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient34"
id="linearGradient35"
x1="11.470703"
y1="3.5996094"
x2="11.470703"
y2="20"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:label="base"
inkscape:groupmode="layer"
id="layer1"
style="display:inline;stroke:none;fill:url(#linearGradient35);fill-opacity:1">
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#cccccc;fill-opacity:0.8;stroke:none;stroke-linecap:square;stroke-miterlimit:10;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="M 10.722656,3.0996094 5.0820312,19.513672 4.7421875,20.5 h 3.171875 l 1.375,-4 h 5.4218755 l 1.374999,4 h 3.171875 L 18.917969,19.513672 13.277344,3.0996094 Z M 12,8.6113281 13.679688,13.5 h -3.359375 z"
id="path6"
inkscape:label="stroke"
sodipodi:nodetypes="ccccccccccccccc" />
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:url(#linearGradient35);fill-opacity:1;stroke:none;stroke-linecap:square;stroke-miterlimit:10;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="M 11.080078,3.6000004 5.5546869,19.676172 5.4433588,20.000391 h 2.1132816 l 1.3750004,-4 h 6.1367182 l 1.375,4 h 2.113282 L 18.445312,19.676172 12.919922,3.6000004 Z M 12,7.0746098 14.380859,14.000391 H 9.6191408 Z"
id="path7"
inkscape:label="A" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,138 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="1 1 23 23"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="aa-dark.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="false"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
labelstyle="default"
inkscape:clip-to-page="false"
inkscape:zoom="32"
inkscape:cx="9.75"
inkscape:cy="13.078125"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true">
<sodipodi:guide
position="3.0000001,19"
orientation="14,0"
id="guide1"
inkscape:locked="false" />
<sodipodi:guide
position="3.0000001,4.0000001"
orientation="0,1"
id="guide2"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="21,5.0000001"
orientation="-14,0"
id="guide3"
inkscape:locked="false" />
<sodipodi:guide
position="21,20"
orientation="0,1"
id="guide4"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="15.5,19"
orientation="-1,0"
id="guide7"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="10,19"
orientation="-1,0"
id="guide8"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="12,9.0000002"
orientation="0,1"
id="guide10"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="15.5,20.4"
orientation="0,1"
id="guide19"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<defs
id="defs1">
<linearGradient
id="linearGradient34"
inkscape:collect="always">
<stop
style="stop-color:#785880;stop-opacity:1;"
offset="0"
id="stop34" />
<stop
style="stop-color:#b464a6;stop-opacity:1;"
offset="1"
id="stop35" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient34"
id="linearGradient35"
x1="11.470703"
y1="3.5996094"
x2="11.470703"
y2="20"
gradientUnits="userSpaceOnUse" />
</defs>
<path
id="path29"
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#333333;fill-opacity:0.80000001;stroke-linecap:square;stroke-miterlimit:10;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="M 7.2226562 3.0996094 L 1.5820312 19.513672 L 1.2421875 20.5 L 4.4140625 20.5 L 5.7890625 16.5 L 9.6171875 16.5 L 8.5820312 19.513672 L 8.2421875 20.5 L 11.414062 20.5 L 12.789062 16.5 L 18.210938 16.5 L 19.585938 20.5 L 22.757812 20.5 L 22.417969 19.513672 L 16.777344 3.0996094 L 14.222656 3.0996094 L 12 9.5683594 C 11.259181 7.4122493 10.518269 5.2556825 9.7773438 3.0996094 L 7.2226562 3.0996094 z M 8.5 8.6113281 L 10.179688 13.5 L 6.8203125 13.5 L 8.5 8.6113281 z M 15.5 8.6113281 L 17.179688 13.5 L 13.820312 13.5 L 15.5 8.6113281 z "
inkscape:label="stroke" />
<g
inkscape:label="base"
inkscape:groupmode="layer"
id="layer1"
style="display:inline;stroke:none;fill:url(#linearGradient35);fill-opacity:1">
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:url(#linearGradient35);stroke-linecap:square;stroke-miterlimit:10;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1;stroke:none;fill-opacity:1"
d="M 14.580078,3.5996094 9.0546875,19.675781 8.9433594,20 h 2.1132816 l 1.375,-4 h 6.136718 l 1.375,4 h 2.113282 L 21.945312,19.675781 16.419922,3.5996094 Z M 15.5,7.0742188 17.880859,14 h -4.761718 z"
id="path30"
inkscape:label="A" />
<path
id="path23"
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:url(#linearGradient35);stroke-linecap:square;stroke-miterlimit:10;enable-background:accumulate;stop-color:#000000;stop-opacity:1;stroke:none;fill-opacity:1"
d="M 7.5800781,3.5996094 2.0546875,19.675781 1.9433594,20 h 2.1132812 l 1.375,-4 h 3.8300782 l 0.6875,-2 H 6.1191406 L 8.5,7.0742188 10.414062,12.644531 11.470703,9.5683594 9.4199219,3.5996094 Z"
sodipodi:nodetypes="ccccccccccccc"
inkscape:label="A_cut" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1,138 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="1 1 23 23"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="aa.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="false"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
labelstyle="default"
inkscape:clip-to-page="false"
inkscape:zoom="32"
inkscape:cx="9.75"
inkscape:cy="13.078125"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="svg1"
showguides="true">
<sodipodi:guide
position="3.0000001,19"
orientation="14,0"
id="guide1"
inkscape:locked="false" />
<sodipodi:guide
position="3.0000001,4.0000001"
orientation="0,1"
id="guide2"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="21,5.0000001"
orientation="-14,0"
id="guide3"
inkscape:locked="false" />
<sodipodi:guide
position="21,20"
orientation="0,1"
id="guide4"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="15.5,19"
orientation="-1,0"
id="guide7"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="10,19"
orientation="-1,0"
id="guide8"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="12,9.0000002"
orientation="0,1"
id="guide10"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="15.5,20.4"
orientation="0,1"
id="guide19"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<defs
id="defs1">
<linearGradient
id="linearGradient34"
inkscape:collect="always">
<stop
style="stop-color:#5a3463;stop-opacity:1;"
offset="0"
id="stop34" />
<stop
style="stop-color:#9b4b8d;stop-opacity:1;"
offset="1"
id="stop35" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient34"
id="linearGradient35"
x1="11.470703"
y1="3.5996094"
x2="11.470703"
y2="20"
gradientUnits="userSpaceOnUse" />
</defs>
<path
id="path29"
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#cccccc;fill-opacity:0.8;stroke-linecap:square;stroke-miterlimit:10;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="M 7.2226562 3.0996094 L 1.5820312 19.513672 L 1.2421875 20.5 L 4.4140625 20.5 L 5.7890625 16.5 L 9.6171875 16.5 L 8.5820312 19.513672 L 8.2421875 20.5 L 11.414062 20.5 L 12.789062 16.5 L 18.210938 16.5 L 19.585938 20.5 L 22.757812 20.5 L 22.417969 19.513672 L 16.777344 3.0996094 L 14.222656 3.0996094 L 12 9.5683594 C 11.259181 7.4122493 10.518269 5.2556825 9.7773438 3.0996094 L 7.2226562 3.0996094 z M 8.5 8.6113281 L 10.179688 13.5 L 6.8203125 13.5 L 8.5 8.6113281 z M 15.5 8.6113281 L 17.179688 13.5 L 13.820312 13.5 L 15.5 8.6113281 z "
inkscape:label="stroke" />
<g
inkscape:label="base"
inkscape:groupmode="layer"
id="layer1"
style="display:inline;stroke:none;fill:url(#linearGradient35);fill-opacity:1">
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:url(#linearGradient35);stroke-linecap:square;stroke-miterlimit:10;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1;stroke:none;fill-opacity:1"
d="M 14.580078,3.5996094 9.0546875,19.675781 8.9433594,20 h 2.1132816 l 1.375,-4 h 6.136718 l 1.375,4 h 2.113282 L 21.945312,19.675781 16.419922,3.5996094 Z M 15.5,7.0742188 17.880859,14 h -4.761718 z"
id="path30"
inkscape:label="A" />
<path
id="path23"
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:url(#linearGradient35);stroke-linecap:square;stroke-miterlimit:10;enable-background:accumulate;stop-color:#000000;stop-opacity:1;stroke:none;fill-opacity:1"
d="M 7.5800781,3.5996094 2.0546875,19.675781 1.9433594,20 h 2.1132812 l 1.375,-4 h 3.8300782 l 0.6875,-2 H 6.1191406 L 8.5,7.0742188 10.414062,12.644531 11.470703,9.5683594 9.4199219,3.5996094 Z"
sodipodi:nodetypes="ccccccccccccc"
inkscape:label="A_cut" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1,133 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="1 1 23 23"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="b-dark.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="false"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
labelstyle="default"
inkscape:clip-to-page="false"
inkscape:zoom="32"
inkscape:cx="10.625"
inkscape:cy="13.640625"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true">
<sodipodi:guide
position="3.0000001,5.0000001"
orientation="0,1"
id="guide2"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="21,19"
orientation="0,1"
id="guide4"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="12,12.5"
orientation="0,1"
id="guide10"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="12,20"
orientation="-1,0"
id="guide5"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="7.0000001,20"
orientation="-1,0"
id="guide11"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="17.5,3.9996091"
orientation="-1,0"
id="guide13"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<defs
id="defs1">
<linearGradient
id="linearGradient34"
inkscape:collect="always">
<stop
style="stop-color:#62476c;stop-opacity:1;"
offset="0"
id="stop34" />
<stop
style="stop-color:#ab73a3;stop-opacity:1;"
offset="1"
id="stop35" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient34"
id="linearGradient35"
x1="11.470703"
y1="3.5996094"
x2="11.470703"
y2="20"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient34"
id="linearGradient1"
gradientUnits="userSpaceOnUse"
x1="11.470703"
y1="3.5996094"
x2="11.470703"
y2="20" />
</defs>
<g
inkscape:label="base"
inkscape:groupmode="layer"
id="layer1"
style="display:inline;fill:url(#linearGradient35);fill-opacity:1;stroke:none"
transform="translate(0.375)">
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#333333;fill-opacity:0.8;stroke:none;stroke-miterlimit:10;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 5.5,3.5 v 17 h 6.75 c 2.973855,0 5.5,-2.314875 5.5,-5.25 0,-1.721917 -0.943723,-3.16211 -2.261719,-4.113281 C 16.093694,10.320901 16.5,9.3384477 16.5,8.25 16.5,5.7007829 14.533131,3.5 12,3.5 Z m 3,3 H 12 c 0.774581,0 1.5,0.6927751 1.5,1.75 C 13.5,9.3072249 12.774581,10 12,10 H 8.5 Z m 0,6.5 h 3.25 0.25 0.25 c 1.456677,0 2.5,1.044783 2.5,2.25 0,1.205217 -1.043323,2.25 -2.5,2.25 H 8.5 Z"
id="path22"
inkscape:label="stroke"
sodipodi:nodetypes="ccsscssccssscccccssscc" />
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:url(#linearGradient1);fill-opacity:1;stroke:none;stroke-miterlimit:10;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 6,4 v 16 h 6.25 c 2.715802,0 5,-2.102463 5,-4.75 0,-1.719783 -0.962948,-3.210042 -2.382812,-4.042969 C 15.571798,10.433759 16,9.3833207 16,8.25 16,5.9562068 14.242331,4 12,4 Z m 2,2 h 4 c 1.071377,0 2,0.9539424 2,2.25 0,1.2960576 -0.928623,2.25 -2,2.25 H 8 Z m 4.25,6.486328 V 12.5 c 1.702476,0 3,1.255402 3,2.75 0,1.494598 -1.297524,2.75 -3,2.75 H 8 v -5.5 h 4 c 0.08442,0 0.167024,-0.0082 0.25,-0.01367 z"
id="path23"
inkscape:label="B" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1,133 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="1 1 23 23"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="b.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="false"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
labelstyle="default"
inkscape:clip-to-page="false"
inkscape:zoom="32"
inkscape:cx="9.546875"
inkscape:cy="13.828125"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true">
<sodipodi:guide
position="3.0000001,5.0000001"
orientation="0,1"
id="guide2"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="21,19"
orientation="0,1"
id="guide4"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="12,12.5"
orientation="0,1"
id="guide10"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="12,20"
orientation="-1,0"
id="guide5"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="7.0000001,20"
orientation="-1,0"
id="guide11"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="17.5,3.9996091"
orientation="-1,0"
id="guide13"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<defs
id="defs1">
<linearGradient
id="linearGradient34"
inkscape:collect="always">
<stop
style="stop-color:#43334a;stop-opacity:1;"
offset="0"
id="stop34" />
<stop
style="stop-color:#755b7c;stop-opacity:1;"
offset="1"
id="stop35" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient34"
id="linearGradient35"
x1="11.470703"
y1="3.5996094"
x2="11.470703"
y2="20"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient34"
id="linearGradient23"
gradientUnits="userSpaceOnUse"
x1="11.470703"
y1="3.5996094"
x2="11.470703"
y2="20" />
</defs>
<g
inkscape:label="base"
inkscape:groupmode="layer"
id="layer1"
style="display:inline;fill:url(#linearGradient35);fill-opacity:1;stroke:none"
transform="translate(0.375)">
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#cccccc;fill-opacity:0.8;stroke:none;stroke-miterlimit:10;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 5.5,3.5 v 17 h 6.75 c 2.973855,0 5.5,-2.314875 5.5,-5.25 0,-1.721917 -0.943723,-3.16211 -2.261719,-4.113281 C 16.093694,10.320901 16.5,9.3384477 16.5,8.25 16.5,5.7007829 14.533131,3.5 12,3.5 Z m 3,3 H 12 c 0.774581,0 1.5,0.6927751 1.5,1.75 C 13.5,9.3072249 12.774581,10 12,10 H 8.5 Z m 0,6.5 h 3.25 0.25 0.25 c 1.456677,0 2.5,1.044783 2.5,2.25 0,1.205217 -1.043323,2.25 -2.5,2.25 H 8.5 Z"
id="path22"
inkscape:label="stroke"
sodipodi:nodetypes="ccsscssccssscccccssscc" />
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:url(#linearGradient23);fill-opacity:1;stroke:none;stroke-miterlimit:10;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 6,4 v 16 h 6.25 c 2.715802,0 5,-2.102463 5,-4.75 0,-1.719783 -0.962948,-3.210042 -2.382812,-4.042969 C 15.571798,10.433759 16,9.3833207 16,8.25 16,5.9562068 14.242331,4 12,4 Z m 2,2 h 4 c 1.071377,0 2,0.9539424 2,2.25 0,1.2960576 -0.928623,2.25 -2,2.25 H 8 Z m 4.25,6.486328 V 12.5 c 1.702476,0 3,1.255402 3,2.75 0,1.494598 -1.297524,2.75 -3,2.75 H 8 v -5.5 h 4 c 0.08442,0 0.167024,-0.0082 0.25,-0.01367 z"
id="path23"
inkscape:label="B" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="1 1 23 23"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="c-dark.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="false"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
labelstyle="default"
inkscape:clip-to-page="false"
inkscape:zoom="22.627417"
inkscape:cx="8.8167377"
inkscape:cy="14.164233"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true">
<sodipodi:guide
position="12,20"
orientation="-1,0"
id="guide5"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="12,12"
orientation="0.60181502,0.79863551"
id="guide6"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="12,12"
orientation="-0.60181502,0.79863551"
id="guide7"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<defs
id="defs1">
<linearGradient
id="linearGradient9"
inkscape:collect="always">
<stop
style="stop-color:#5c433d;stop-opacity:1;"
offset="0"
id="stop9" />
<stop
style="stop-color:#9d6c85;stop-opacity:1;"
offset="1"
id="stop10" />
</linearGradient>
<linearGradient
id="linearGradient34"
inkscape:collect="always">
<stop
style="stop-color:#46324d;stop-opacity:1;"
offset="0"
id="stop34" />
<stop
style="stop-color:#92588a;stop-opacity:1;"
offset="1"
id="stop35" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient34"
id="linearGradient35"
x1="11.470703"
y1="3.5996094"
x2="11.470703"
y2="20"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient9"
id="linearGradient10"
x1="12"
y1="3.5163455"
x2="12"
y2="20.483654"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:label="base"
inkscape:groupmode="layer"
id="layer1"
style="display:inline;stroke:none;fill:url(#linearGradient35);fill-opacity:1">
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#333333;fill-opacity:0.80000001;stroke:none;stroke-miterlimit:10;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="M 12.5,3 C 7.7659933,3 4,7.0984281 4,12 c 0,4.901572 3.7659934,9 8.5,9 2.899254,0 5.460617,-1.548293 6.980469,-3.859375 l 0.27539,-0.417969 -2.507812,-1.65039 -0.275391,0.417968 C 15.95623,17.035809 14.33618,18 12.5,18 9.5033162,18 7,15.389122 7,12 7,8.6108784 9.5033164,6 12.5,6 c 1.836197,0 3.456233,0.9641762 4.472656,2.5097656 L 17.248047,8.9277344 19.755859,7.2773437 19.480469,6.859375 C 17.96062,4.5482711 15.399265,3 12.5,3 Z"
id="path8"
inkscape:label="stroke"
sodipodi:nodetypes="sssccccsssccccs" />
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:url(#linearGradient10);stroke:none;stroke-miterlimit:10;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 12.5,3.5 c -4.4450362,0 -8,3.8534536 -8,8.5 0,4.646547 3.5549639,8.5 8,8.5 2.721277,0 5.127673,-1.452972 6.5625,-3.634766 L 17.390625,15.765625 C 16.291564,17.436853 14.512289,18.5 12.5,18.5 c -3.2869503,0 -6,-2.862275 -6,-6.5 0,-3.6377248 2.7130498,-6.5 6,-6.5 2.012302,0 3.791567,1.0631305 4.890625,2.734375 L 19.0625,7.1347656 C 17.627676,4.9529509 15.221292,3.5 12.5,3.5 Z"
id="path9"
inkscape:label="C" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="1 1 23 23"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="c.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="false"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
labelstyle="default"
inkscape:clip-to-page="false"
inkscape:zoom="32"
inkscape:cx="9.828125"
inkscape:cy="12.421875"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true">
<sodipodi:guide
position="12,20"
orientation="-1,0"
id="guide5"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="12,12"
orientation="0.60181502,0.79863551"
id="guide6"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="12,12"
orientation="-0.60181502,0.79863551"
id="guide7"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<defs
id="defs1">
<linearGradient
id="linearGradient9"
inkscape:collect="always">
<stop
style="stop-color:#3b2b27;stop-opacity:1;"
offset="0"
id="stop9" />
<stop
style="stop-color:#80566b;stop-opacity:1;"
offset="1"
id="stop10" />
</linearGradient>
<linearGradient
id="linearGradient34"
inkscape:collect="always">
<stop
style="stop-color:#46324d;stop-opacity:1;"
offset="0"
id="stop34" />
<stop
style="stop-color:#92588a;stop-opacity:1;"
offset="1"
id="stop35" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient34"
id="linearGradient35"
x1="11.470703"
y1="3.5996094"
x2="11.470703"
y2="20"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient9"
id="linearGradient10"
x1="12"
y1="3.5163455"
x2="12"
y2="20.483654"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:label="base"
inkscape:groupmode="layer"
id="layer1"
style="display:inline;stroke:none;fill:url(#linearGradient35);fill-opacity:1">
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#cccccc;fill-opacity:0.8;stroke:none;stroke-miterlimit:10;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="M 12.5,3 C 7.7659933,3 4,7.0984281 4,12 c 0,4.901572 3.7659934,9 8.5,9 2.899254,0 5.460617,-1.548293 6.980469,-3.859375 l 0.27539,-0.417969 -2.507812,-1.65039 -0.275391,0.417968 C 15.95623,17.035809 14.33618,18 12.5,18 9.5033162,18 7,15.389122 7,12 7,8.6108784 9.5033164,6 12.5,6 c 1.836197,0 3.456233,0.9641762 4.472656,2.5097656 L 17.248047,8.9277344 19.755859,7.2773437 19.480469,6.859375 C 17.96062,4.5482711 15.399265,3 12.5,3 Z"
id="path8"
inkscape:label="stroke"
sodipodi:nodetypes="sssccccsssccccs" />
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:url(#linearGradient10);stroke:none;stroke-miterlimit:10;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 12.5,3.5 c -4.4450362,0 -8,3.8534536 -8,8.5 0,4.646547 3.5549639,8.5 8,8.5 2.721277,0 5.127673,-1.452972 6.5625,-3.634766 L 17.390625,15.765625 C 16.291564,17.436853 14.512289,18.5 12.5,18.5 c -3.2869503,0 -6,-2.862275 -6,-6.5 0,-3.6377248 2.7130498,-6.5 6,-6.5 2.012302,0 3.791567,1.0631305 4.890625,2.734375 L 19.0625,7.1347656 C 17.627676,4.9529509 15.221292,3.5 12.5,3.5 Z"
id="path9"
inkscape:label="C" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,136 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="1 1 23 23"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="d-dark.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="false"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
labelstyle="default"
inkscape:clip-to-page="false"
inkscape:zoom="32"
inkscape:cx="12.1875"
inkscape:cy="12.109375"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true">
<sodipodi:guide
position="3.0000001,5.0000001"
orientation="0,1"
id="guide2"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="21,19"
orientation="0,1"
id="guide4"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="12,12.5"
orientation="0,1"
id="guide10"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="12,20"
orientation="-1,0"
id="guide5"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="7.0000001,20"
orientation="-1,0"
id="guide11"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="17.5,3.9996091"
orientation="-1,0"
id="guide13"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<defs
id="defs1">
<linearGradient
id="linearGradient8"
inkscape:collect="always">
<stop
style="stop-color:#842a4b;stop-opacity:1;"
offset="0"
id="stop7" />
<stop
style="stop-color:#bd516c;stop-opacity:1;"
offset="1"
id="stop8" />
</linearGradient>
<linearGradient
id="linearGradient5"
inkscape:collect="always">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop5" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop6" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient8"
id="linearGradient6"
x1="11"
y1="4"
x2="11"
y2="20"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:label="base"
inkscape:groupmode="layer"
id="layer1"
style="display:inline;fill:url(#linearGradient5);fill-opacity:1;stroke:none"
transform="translate(0.375)">
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#333333;fill-opacity:0.80000001;stroke:none;stroke-miterlimit:10;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 5.5,3.5 v 17 H 11 c 4.462075,0 8,-3.878181 8,-8.5 0,-4.621819 -3.537925,-8.5 -8,-8.5 z m 3,3 H 11 c 2.715731,0 5,2.3827768 5,5.5 0,3.117223 -2.284269,5.5 -5,5.5 H 8.5 Z"
id="path4"
inkscape:label="stroke"
sodipodi:nodetypes="ccsssccssscc" />
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:url(#linearGradient6);fill-opacity:1;stroke:none;stroke-miterlimit:10;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 6,4 v 16 h 5 c 4.171863,0 7.5,-3.63217 7.5,-8 0,-4.3678304 -3.328137,-8 -7.5,-8 z m 2,2 h 3 c 3.007839,0 5.5,2.635844 5.5,6 0,3.364156 -2.492161,6 -5.5,6 H 8 Z"
id="path5"
inkscape:label="D" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -0,0 +1,136 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="1 1 23 23"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="d.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="false"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
labelstyle="default"
inkscape:clip-to-page="false"
inkscape:zoom="32"
inkscape:cx="12.1875"
inkscape:cy="12.109375"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true">
<sodipodi:guide
position="3.0000001,5.0000001"
orientation="0,1"
id="guide2"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="21,19"
orientation="0,1"
id="guide4"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="12,12.5"
orientation="0,1"
id="guide10"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="12,20"
orientation="-1,0"
id="guide5"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="7.0000001,20"
orientation="-1,0"
id="guide11"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
<sodipodi:guide
position="17.5,3.9996091"
orientation="-1,0"
id="guide13"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<defs
id="defs1">
<linearGradient
id="linearGradient8"
inkscape:collect="always">
<stop
style="stop-color:#5d1d35;stop-opacity:1;"
offset="0"
id="stop7" />
<stop
style="stop-color:#9f3c55;stop-opacity:1;"
offset="1"
id="stop8" />
</linearGradient>
<linearGradient
id="linearGradient5"
inkscape:collect="always">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop5" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop6" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient8"
id="linearGradient6"
x1="11"
y1="4"
x2="11"
y2="20"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:label="base"
inkscape:groupmode="layer"
id="layer1"
style="display:inline;fill:url(#linearGradient5);fill-opacity:1;stroke:none"
transform="translate(0.375)">
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#cccccc;fill-opacity:1;stroke:none;stroke-miterlimit:10;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 5.5,3.5 v 17 H 11 c 4.462075,0 8,-3.878181 8,-8.5 0,-4.621819 -3.537925,-8.5 -8,-8.5 z m 3,3 H 11 c 2.715731,0 5,2.3827768 5,5.5 0,3.117223 -2.284269,5.5 -5,5.5 H 8.5 Z"
id="path4"
inkscape:label="stroke"
sodipodi:nodetypes="ccsssccssscc" />
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:url(#linearGradient6);fill-opacity:1;stroke:none;stroke-miterlimit:10;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 6,4 v 16 h 5 c 4.171863,0 7.5,-3.63217 7.5,-8 0,-4.3678304 -3.328137,-8 -7.5,-8 z m 2,2 h 3 c 3.007839,0 5.5,2.635844 5.5,6 0,3.364156 -2.492161,6 -5.5,6 H 8 Z"
id="path5"
inkscape:label="D" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="1 1 23 23"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="ex-dark.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="false"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
labelstyle="default"
inkscape:clip-to-page="false"
inkscape:zoom="22.627418"
inkscape:cx="14.429397"
inkscape:cy="14.517786"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true" />
<defs
id="defs1">
<linearGradient
id="linearGradient3"
inkscape:collect="always">
<stop
style="stop-color:#ba2cae;stop-opacity:1;"
offset="0"
id="stop2" />
<stop
style="stop-color:#397fc6;stop-opacity:1;"
offset="1"
id="stop3" />
</linearGradient>
<inkscape:path-effect
effect="spiro"
id="path-effect13"
is_visible="true"
lpeversion="1" />
<inkscape:path-effect
effect="spiro"
id="path-effect11"
is_visible="true"
lpeversion="1" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3"
id="linearGradient23"
x1="10.353516"
y1="4.000001"
x2="17.814453"
y2="20"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3"
id="linearGradient1"
gradientUnits="userSpaceOnUse"
x1="10.353516"
y1="4.000001"
x2="17.814453"
y2="20"
gradientTransform="translate(3.030469)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3"
id="linearGradient2"
gradientUnits="userSpaceOnUse"
x1="10.353516"
y1="4.000001"
x2="17.814453"
y2="20"
gradientTransform="translate(3.030469)" />
</defs>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="strokes">
<path
id="path6"
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#333333;fill-opacity:0.8;stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
inkscape:label="EX-stroke"
d="m 2.8,3.5 v 17 h 7.714844 1.285157 2.02539 L 16.133984,15.548828 18.442578,20.5 h 3.310547 L 17.78828,12 21.753125,3.5 H 18.442578 L 16.133984,8.4511719 13.825391,3.5 h -2.02539 -1.285157 z m 3,3 h 6.113281 l 2.566407,5.5 -2.566407,5.5 H 5.8 v -4 h 5 v -3 h -5 z" />
</g>
<g
inkscape:label="base"
inkscape:groupmode="layer"
id="layer1"
style="display:inline;fill:url(#linearGradient23);stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill">
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;fill:url(#linearGradient1);stroke:#cccccc;stroke-width:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 3.3,4 v 16 h 8.000001 V 18 H 5.3 v -5 h 5.000001 V 11 H 5.3 V 6 h 6.000001 V 4 Z"
id="path7"
inkscape:label="E" />
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;fill:url(#linearGradient2);stroke:#cccccc;stroke-width:0;stroke-linecap:square;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="M 11.3,4.0000008 15.030469,12 11.3,20 h 2.207031 L 16.133984,14.367188 18.760937,20 h 2.207031 L 17.2375,12 20.967968,4.0000008 H 18.760937 L 16.133984,9.6328133 13.507031,4.0000008 Z"
id="path4"
inkscape:label="X" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="0.755 1 23 23"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="exPlus-dark.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="false"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
labelstyle="default"
inkscape:clip-to-page="false"
inkscape:zoom="181.01934"
inkscape:cx="9.7890095"
inkscape:cy="6.7534222"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="layer2"
showguides="true" />
<defs
id="defs1">
<linearGradient
id="linearGradient22"
inkscape:collect="always">
<stop
style="stop-color:#bf33cc;stop-opacity:1;"
offset="0"
id="stop22" />
<stop
style="stop-color:#4791d1;stop-opacity:1;"
offset="1"
id="stop23" />
</linearGradient>
<inkscape:path-effect
effect="spiro"
id="path-effect13"
is_visible="true"
lpeversion="1" />
<inkscape:path-effect
effect="spiro"
id="path-effect11"
is_visible="true"
lpeversion="1" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient22"
id="linearGradient23"
x1="10.353516"
y1="4.000001"
x2="17.814453"
y2="20"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="strokes">
<path
id="path6"
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#333333;fill-opacity:0.80000001;stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
inkscape:label="EX-stroke"
d="M -0.230469 3.5 L -0.230469 20.5 L 7.4843747 20.5 L 8.769531 20.5 L 10.794922 20.5 L 13.103515 15.548828 L 15.412109 20.5 L 18.722656 20.5 L 14.757812 12 L 18.722656 3.5 L 15.412109 3.5 L 13.103515 8.4511719 L 10.794922 3.5 L 8.769531 3.5 L 7.4843747 3.5 L -0.230469 3.5 z M 2.769531 6.5 L 8.8828123 6.5 L 11.449219 12 L 8.8828123 17.5 L 2.769531 17.5 L 2.769531 13.5 L 7.769531 13.5 L 7.769531 10.5 L 2.769531 10.5 L 2.769531 6.5 z "
transform="translate(0.980469)" />
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#333333;fill-opacity:0.80000001;stroke:none;stroke-width:0;stroke-linecap:square;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 17.769531,8.5 v 2 h -2 v 3 h 2 v 2 h 3 v -2 h 2 v -3 h -2 v -2 z"
id="path1"
inkscape:label="plus-stroke"
transform="translate(0.980469)"
sodipodi:nodetypes="ccccccccccccc" />
</g>
<g
inkscape:label="base"
inkscape:groupmode="layer"
id="layer1"
style="display:inline;fill:url(#linearGradient23);stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill">
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;fill:url(#linearGradient23);stroke:#cccccc;stroke-width:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 0.269531,4 v 16 h 8 v -2 h -6 v -5 h 5 v -2 h -5 V 6 h 6 V 4 Z"
id="path7"
inkscape:label="E"
transform="translate(0.980469)" />
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;fill:url(#linearGradient23);stroke:#cccccc;stroke-width:0;stroke-linecap:square;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="M 8.2695306,4.0000008 12,12 8.2695306,20 H 10.476562 L 13.103515,14.367188 15.730468,20 H 17.9375 L 14.207031,12 17.9375,4.0000008 H 15.730468 L 13.103515,9.6328133 10.476562,4.0000008 Z"
id="path4"
inkscape:label="X"
transform="translate(0.980469)" />
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;fill:url(#linearGradient23);stroke:#cccccc;stroke-width:0;stroke-linecap:square;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 18.269531,9 v 1 1 h -1 -1 v 2 h 1 1 v 1 1 h 2 v -1 -1 h 1 1 v -2 h -1 -1 V 10 9 Z"
id="path2"
inkscape:label="plus"
transform="translate(0.980469)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="0.755 1 23 23"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="ex-plus.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="false"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
labelstyle="default"
inkscape:clip-to-page="false"
inkscape:zoom="32"
inkscape:cx="2.75"
inkscape:cy="12.296875"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="layer3"
showguides="true" />
<defs
id="defs1">
<linearGradient
id="linearGradient22"
inkscape:collect="always">
<stop
style="stop-color:#83238c;stop-opacity:1;"
offset="0"
id="stop22" />
<stop
style="stop-color:#2c72ae;stop-opacity:1;"
offset="1"
id="stop23" />
</linearGradient>
<inkscape:path-effect
effect="spiro"
id="path-effect13"
is_visible="true"
lpeversion="1" />
<inkscape:path-effect
effect="spiro"
id="path-effect11"
is_visible="true"
lpeversion="1" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient22"
id="linearGradient23"
x1="10.353516"
y1="4.000001"
x2="17.814453"
y2="20"
gradientUnits="userSpaceOnUse" />
</defs>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="strokes">
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#cccccc;fill-opacity:0.8;stroke:none;stroke-width:0;stroke-linecap:square;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 17.769531,8.5 v 2 h -2 v 3 h 2 v 2 h 3 v -2 h 2 v -3 h -2 v -2 z"
id="path26"
inkscape:label="plus-stroke"
transform="translate(0.980469)"
sodipodi:nodetypes="ccccccccccccc" />
<path
id="path28"
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#cccccc;fill-opacity:0.8;stroke:none;stroke-width:0;stroke-linecap:square;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
inkscape:label="X-stroke"
d="m -0.230469,3.5 v 17 H 7.4843747 8.769531 10.794922 L 13.103515,15.548828 15.412109,20.5 h 3.310547 L 14.757812,12 18.722656,3.5 H 15.412109 L 13.103515,8.4511719 10.794922,3.5 H 8.769531 7.4843747 Z m 3,3 H 8.8828123 L 11.449219,12 8.8828123,17.5 H 2.769531 v -4 h 5 v -3 h -5 z"
transform="translate(0.980469)" />
</g>
<g
inkscape:label="base"
inkscape:groupmode="layer"
id="layer1"
style="display:inline;fill:url(#linearGradient23);stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill">
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:url(#linearGradient23);stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 0.269531,4 v 16 h 8 v -2 h -6 v -5 h 5 v -2 h -5 V 6 h 6 V 4 Z"
id="path31"
inkscape:label="E"
transform="translate(0.980469)" />
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:url(#linearGradient23);stroke:none;stroke-width:0;stroke-linecap:square;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="M 8.2695306,4.0000008 12,12 8.2695306,20 H 10.476562 L 13.103515,14.367188 15.730468,20 H 17.9375 L 14.207031,12 17.9375,4.0000008 H 15.730468 L 13.103515,9.6328133 10.476562,4.0000008 Z"
id="path29"
inkscape:label="X"
transform="translate(0.980469)" />
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:url(#linearGradient23);stroke:none;stroke-width:0;stroke-linecap:square;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 18.269531,9 v 1 1 h -1 -1 v 2 h 1 1 v 1 1 h 2 v -1 -1 h 1 1 v -2 h -1 -1 V 10 9 Z"
id="path27"
inkscape:label="plus"
transform="translate(0.980469)" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="1 1 23 23"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="ex.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="false"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
labelstyle="default"
inkscape:clip-to-page="false"
inkscape:zoom="32.791667"
inkscape:cx="12"
inkscape:cy="12"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true" />
<defs
id="defs1">
<linearGradient
id="linearGradient22"
inkscape:collect="always">
<stop
style="stop-color:#721b6b;stop-opacity:1;"
offset="0"
id="stop22" />
<stop
style="stop-color:#295b8d;stop-opacity:1;"
offset="1"
id="stop23" />
</linearGradient>
<inkscape:path-effect
effect="spiro"
id="path-effect13"
is_visible="true"
lpeversion="1" />
<inkscape:path-effect
effect="spiro"
id="path-effect11"
is_visible="true"
lpeversion="1" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient22"
id="linearGradient23"
x1="10.353516"
y1="4.000001"
x2="17.814453"
y2="20"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient22"
id="linearGradient1"
gradientUnits="userSpaceOnUse"
x1="10.353516"
y1="4.000001"
x2="17.814453"
y2="20"
gradientTransform="translate(3.130469)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient22"
id="linearGradient2"
gradientUnits="userSpaceOnUse"
x1="10.353516"
y1="4.000001"
x2="17.814453"
y2="20"
gradientTransform="translate(3.130469)" />
</defs>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="strokes">
<path
id="path28"
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:#cccccc;fill-opacity:0.8;stroke:none;stroke-width:0;stroke-linecap:square;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
inkscape:label="X-stroke"
d="m 2.9,3.5 v 17 H 10.614844 11.9 13.925391 L 16.233984,15.548828 18.542578,20.5 h 3.310547 L 17.888281,12 21.853125,3.5 H 18.542578 L 16.233984,8.4511719 13.925391,3.5 H 11.9 10.614844 Z m 3,3 h 6.113281 l 2.566407,5.5 -2.566407,5.5 H 5.9 v -4 h 5 v -3 h -5 z" />
</g>
<g
inkscape:label="base"
inkscape:groupmode="layer"
id="layer1"
style="display:inline;fill:url(#linearGradient23);stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill">
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:url(#linearGradient1);stroke:none;stroke-width:0;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="m 3.4,4 v 16 h 8 v -2 h -6 v -5 h 5 v -2 h -5 V 6 h 6 V 4 Z"
id="path31"
inkscape:label="E" />
<path
style="baseline-shift:baseline;display:inline;overflow:visible;opacity:1;vector-effect:none;fill:url(#linearGradient2);stroke:none;stroke-width:0;stroke-linecap:square;stroke-miterlimit:10;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="M 11.4,4.0000008 15.130469,12 11.4,20 h 2.207031 L 16.233984,14.367188 18.860937,20 h 2.207032 L 17.3375,12 21.067969,4.0000008 H 18.860937 L 16.233984,9.6328133 13.607031,4.0000008 Z"
id="path29"
inkscape:label="X" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="1 1 23 23"
version="1.1"
id="svg1"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
sodipodi:docname="grade-template.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="false"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#505050"
inkscape:document-units="px"
labelstyle="default"
inkscape:clip-to-page="false"
inkscape:zoom="22.435241"
inkscape:cx="10.563738"
inkscape:cy="11.789488"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="-9"
inkscape:window-y="-9"
inkscape:window-maximized="1"
inkscape:current-layer="layer1"
showguides="true">
<sodipodi:guide
position="3.0000001,19"
orientation="14,0"
id="guide1"
inkscape:locked="false" />
<sodipodi:guide
position="3.0000001,5.0000001"
orientation="0,18"
id="guide2"
inkscape:locked="false" />
<sodipodi:guide
position="21,5.0000001"
orientation="-14,0"
id="guide3"
inkscape:locked="false" />
<sodipodi:guide
position="21,19"
orientation="0,-18"
id="guide4"
inkscape:locked="false" />
<sodipodi:guide
position="12,12.000001"
orientation="-1,0"
id="guide5"
inkscape:locked="false"
inkscape:label=""
inkscape:color="rgb(0,134,229)" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="图层 1"
inkscape:groupmode="layer"
id="layer1" />
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html>
<head>
<title>Grade Previews</title>
<meta charset="utf-8" />
<style>
.display {
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
img {
width: 150px;
height: 150px;
}
</style>
</head>
<body>
<div id="main" style="display: flex; flex-direction: column">
<div class="gallery">
<img src="./ex-plus.svg" />
<img src="./ex.svg" />
<img src="./aa.svg" />
<img src="./a.svg" />
<img src="./b.svg" />
<img src="./c.svg" />
<img src="./d.svg" />
</div>
<div class="gallery" style="background-color: #282828">
<img src="./ex-plus-dark.svg" />
<img src="./ex-dark.svg" />
<img src="./aa-dark.svg" />
<img src="./a-dark.svg" />
<img src="./b-dark.svg" />
<img src="./c-dark.svg" />
<img src="./d-dark.svg" />
</div>
</div>
</body>
</html>

View File

@ -70,12 +70,12 @@
<translation>Continue</translation> <translation>Continue</translation>
</message> </message>
<message> <message>
<location filename="../../startup/databaseChecker.py" line="117"/> <location filename="../../startup/databaseChecker.py" line="122"/>
<source>dialog.tryInitExistingDatabase</source> <source>dialog.tryInitExistingDatabase</source>
<translation>The existing database doesn&apos;t seem to be initialized properly, try initialize again?</translation> <translation>The existing database doesn&apos;t seem to be initialized properly, try initialize again?</translation>
</message> </message>
<message> <message>
<location filename="../../startup/databaseChecker.py" line="133"/> <location filename="../../startup/databaseChecker.py" line="138"/>
<source>dialog.confirmNewDatabase</source> <source>dialog.confirmNewDatabase</source>
<translation>Database file does not exist. Create now?</translation> <translation>Database file does not exist. Create now?</translation>
</message> </message>
@ -224,85 +224,118 @@
</message> </message>
<message> <message>
<location filename="../../designer/components/ocrQueue.ui" line="28"/> <location filename="../../designer/components/ocrQueue.ui" line="28"/>
<source>iccOptionsGroupBox</source>
<translation>ICC Profile Options</translation>
</message>
<message>
<location filename="../../designer/components/ocrQueue.ui" line="34"/>
<source>icc.useQt</source>
<translation>Use Qt</translation>
</message>
<message>
<location filename="../../designer/components/ocrQueue.ui" line="41"/>
<source>icc.usePIL</source>
<translation>Use PIL</translation>
</message>
<message>
<location filename="../../designer/components/ocrQueue.ui" line="51"/>
<source>icc.tryFix</source>
<translation>Try fix</translation>
</message>
<message>
<location filename="../../designer/components/ocrQueue.ui" line="61"/>
<source>queue.addImageButton</source> <source>queue.addImageButton</source>
<translation>Add Image</translation> <translation>Add Image</translation>
</message> </message>
<message> <message>
<location filename="../../designer/components/ocrQueue.ui" line="71"/> <location filename="../../designer/components/ocrQueue.ui" line="38"/>
<source>queue.removeSelected</source> <source>queue.removeSelected</source>
<translation>Remove Selected</translation> <translation>Remove Selected</translation>
</message> </message>
<message> <message>
<location filename="../../designer/components/ocrQueue.ui" line="81"/> <location filename="../../designer/components/ocrQueue.ui" line="48"/>
<source>queue.removeAll</source> <source>queue.removeAll</source>
<translation>Remove All</translation> <translation>Remove All</translation>
</message> </message>
<message> <message>
<location filename="../../designer/components/ocrQueue.ui" line="101"/> <location filename="../../designer/components/ocrQueue.ui" line="68"/>
<source>queue.optionsButton</source>
<translation>Options</translation>
</message>
<message>
<location filename="../../designer/components/ocrQueue.ui" line="75"/>
<source>queue.startOcrButton</source> <source>queue.startOcrButton</source>
<translation>Start OCR</translation> <translation>Start OCR</translation>
</message> </message>
<message> <message>
<location filename="../../designer/components/ocrQueue.ui" line="153"/> <location filename="../../designer/components/ocrQueue.ui" line="127"/>
<source>results</source> <source>results</source>
<translation>Results</translation> <translation>Results</translation>
</message> </message>
<message> <message>
<location filename="../../designer/components/ocrQueue.ui" line="162"/> <location filename="../../designer/components/ocrQueue.ui" line="136"/>
<source>results.acceptSelectedButton</source> <source>results.acceptSelectedButton</source>
<translation>Accept Selected</translation> <translation>Accept Selected</translation>
</message> </message>
<message> <message>
<location filename="../../designer/components/ocrQueue.ui" line="169"/> <location filename="../../designer/components/ocrQueue.ui" line="143"/>
<source>results.acceptAllButton</source> <source>results.acceptAllButton</source>
<translation>Accept All</translation> <translation>Accept All</translation>
</message> </message>
<message> <message>
<location filename="../../designer/components/ocrQueue.ui" line="189"/> <location filename="../../designer/components/ocrQueue.ui" line="163"/>
<source>results.ignoreValidate</source> <source>results.ignoreValidate</source>
<translation>Ignore <translation>Ignore
validation</translation> validation</translation>
</message> </message>
</context> </context>
<context>
<name>OcrQueueOptionsDialog</name>
<message>
<location filename="../../designer/components/ocrQueueOptionsDialog.ui" line="14"/>
<source>OCR Options</source>
<translation>OCR Options</translation>
</message>
<message>
<location filename="../../designer/components/ocrQueueOptionsDialog.ui" line="22"/>
<source>iccOptionsGroupBox</source>
<translation>ICC Profile Options</translation>
</message>
<message>
<location filename="../../designer/components/ocrQueueOptionsDialog.ui" line="28"/>
<source>icc.useQt</source>
<translation>Use Qt</translation>
</message>
<message>
<location filename="../../designer/components/ocrQueueOptionsDialog.ui" line="35"/>
<source>icc.usePIL</source>
<translation>Use PIL</translation>
</message>
<message>
<location filename="../../designer/components/ocrQueueOptionsDialog.ui" line="45"/>
<source>icc.tryFix</source>
<translation>Try fix</translation>
</message>
<message>
<location filename="../../designer/components/ocrQueueOptionsDialog.ui" line="55"/>
<source>dateOptionsGroupBox</source>
<translation>Date Source</translation>
</message>
<message>
<location filename="../../designer/components/ocrQueueOptionsDialog.ui" line="64"/>
<source>date.readFromExif</source>
<translation>Read from image EXIF</translation>
</message>
<message>
<location filename="../../designer/components/ocrQueueOptionsDialog.ui" line="74"/>
<source>date.useCreationDate</source>
<translation>File creation time</translation>
</message>
<message>
<location filename="../../designer/components/ocrQueueOptionsDialog.ui" line="84"/>
<source>date.useModifyDate</source>
<translation>File last modification time</translation>
</message>
</context>
<context> <context>
<name>OcrTableModel</name> <name>OcrTableModel</name>
<message> <message>
<location filename="../../extends/components/ocrQueue.py" line="347"/> <location filename="../../extends/components/ocrQueue.py" line="348"/>
<source>horizontalHeader.title.select</source> <source>horizontalHeader.title.select</source>
<translation>Select</translation> <translation>Select</translation>
</message> </message>
<message> <message>
<location filename="../../extends/components/ocrQueue.py" line="348"/> <location filename="../../extends/components/ocrQueue.py" line="351"/>
<source>horizontalHeader.title.imagePreview</source> <source>horizontalHeader.title.imagePreview</source>
<translation>Image Preview</translation> <translation>Image Preview</translation>
</message> </message>
<message> <message>
<location filename="../../extends/components/ocrQueue.py" line="349"/> <location filename="../../extends/components/ocrQueue.py" line="352"/>
<source>horizontalHeader.title.chart</source> <source>horizontalHeader.title.chart</source>
<translation>Chart</translation> <translation>Chart</translation>
</message> </message>
<message> <message>
<location filename="../../extends/components/ocrQueue.py" line="350"/> <location filename="../../extends/components/ocrQueue.py" line="353"/>
<source>horizontalHeader.title.score</source> <source>horizontalHeader.title.score</source>
<translation>Score</translation> <translation>Score</translation>
</message> </message>

View File

@ -70,12 +70,12 @@
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../../startup/databaseChecker.py" line="117"/> <location filename="../../startup/databaseChecker.py" line="122"/>
<source>dialog.tryInitExistingDatabase</source> <source>dialog.tryInitExistingDatabase</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../../startup/databaseChecker.py" line="133"/> <location filename="../../startup/databaseChecker.py" line="138"/>
<source>dialog.confirmNewDatabase</source> <source>dialog.confirmNewDatabase</source>
<translation></translation> <translation></translation>
</message> </message>
@ -224,84 +224,117 @@
</message> </message>
<message> <message>
<location filename="../../designer/components/ocrQueue.ui" line="28"/> <location filename="../../designer/components/ocrQueue.ui" line="28"/>
<source>iccOptionsGroupBox</source>
<translation>ICC </translation>
</message>
<message>
<location filename="../../designer/components/ocrQueue.ui" line="34"/>
<source>icc.useQt</source>
<translation>使 Qt</translation>
</message>
<message>
<location filename="../../designer/components/ocrQueue.ui" line="41"/>
<source>icc.usePIL</source>
<translation>使 PIL</translation>
</message>
<message>
<location filename="../../designer/components/ocrQueue.ui" line="51"/>
<source>icc.tryFix</source>
<translation></translation>
</message>
<message>
<location filename="../../designer/components/ocrQueue.ui" line="61"/>
<source>queue.addImageButton</source> <source>queue.addImageButton</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../../designer/components/ocrQueue.ui" line="71"/> <location filename="../../designer/components/ocrQueue.ui" line="38"/>
<source>queue.removeSelected</source> <source>queue.removeSelected</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../../designer/components/ocrQueue.ui" line="81"/> <location filename="../../designer/components/ocrQueue.ui" line="48"/>
<source>queue.removeAll</source> <source>queue.removeAll</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../../designer/components/ocrQueue.ui" line="101"/> <location filename="../../designer/components/ocrQueue.ui" line="68"/>
<source>queue.optionsButton</source>
<translation></translation>
</message>
<message>
<location filename="../../designer/components/ocrQueue.ui" line="75"/>
<source>queue.startOcrButton</source> <source>queue.startOcrButton</source>
<translation> OCR</translation> <translation> OCR</translation>
</message> </message>
<message> <message>
<location filename="../../designer/components/ocrQueue.ui" line="153"/> <location filename="../../designer/components/ocrQueue.ui" line="127"/>
<source>results</source> <source>results</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../../designer/components/ocrQueue.ui" line="162"/> <location filename="../../designer/components/ocrQueue.ui" line="136"/>
<source>results.acceptSelectedButton</source> <source>results.acceptSelectedButton</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../../designer/components/ocrQueue.ui" line="169"/> <location filename="../../designer/components/ocrQueue.ui" line="143"/>
<source>results.acceptAllButton</source> <source>results.acceptAllButton</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../../designer/components/ocrQueue.ui" line="189"/> <location filename="../../designer/components/ocrQueue.ui" line="163"/>
<source>results.ignoreValidate</source> <source>results.ignoreValidate</source>
<translation></translation> <translation></translation>
</message> </message>
</context> </context>
<context>
<name>OcrQueueOptionsDialog</name>
<message>
<location filename="../../designer/components/ocrQueueOptionsDialog.ui" line="14"/>
<source>OCR Options</source>
<translation>OCR </translation>
</message>
<message>
<location filename="../../designer/components/ocrQueueOptionsDialog.ui" line="22"/>
<source>iccOptionsGroupBox</source>
<translation>ICC </translation>
</message>
<message>
<location filename="../../designer/components/ocrQueueOptionsDialog.ui" line="28"/>
<source>icc.useQt</source>
<translation>使 Qt</translation>
</message>
<message>
<location filename="../../designer/components/ocrQueueOptionsDialog.ui" line="35"/>
<source>icc.usePIL</source>
<translation>使 PIL</translation>
</message>
<message>
<location filename="../../designer/components/ocrQueueOptionsDialog.ui" line="45"/>
<source>icc.tryFix</source>
<translation></translation>
</message>
<message>
<location filename="../../designer/components/ocrQueueOptionsDialog.ui" line="55"/>
<source>dateOptionsGroupBox</source>
<translation></translation>
</message>
<message>
<location filename="../../designer/components/ocrQueueOptionsDialog.ui" line="64"/>
<source>date.readFromExif</source>
<translation> EXIF </translation>
</message>
<message>
<location filename="../../designer/components/ocrQueueOptionsDialog.ui" line="74"/>
<source>date.useCreationDate</source>
<translation></translation>
</message>
<message>
<location filename="../../designer/components/ocrQueueOptionsDialog.ui" line="84"/>
<source>date.useModifyDate</source>
<translation></translation>
</message>
</context>
<context> <context>
<name>OcrTableModel</name> <name>OcrTableModel</name>
<message> <message>
<location filename="../../extends/components/ocrQueue.py" line="347"/> <location filename="../../extends/components/ocrQueue.py" line="348"/>
<source>horizontalHeader.title.select</source> <source>horizontalHeader.title.select</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../../extends/components/ocrQueue.py" line="348"/> <location filename="../../extends/components/ocrQueue.py" line="351"/>
<source>horizontalHeader.title.imagePreview</source> <source>horizontalHeader.title.imagePreview</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../../extends/components/ocrQueue.py" line="349"/> <location filename="../../extends/components/ocrQueue.py" line="352"/>
<source>horizontalHeader.title.chart</source> <source>horizontalHeader.title.chart</source>
<translation></translation> <translation></translation>
</message> </message>
<message> <message>
<location filename="../../extends/components/ocrQueue.py" line="350"/> <location filename="../../extends/components/ocrQueue.py" line="353"/>
<source>horizontalHeader.title.score</source> <source>horizontalHeader.title.score</source>
<translation></translation> <translation></translation>
</message> </message>

View File

@ -10,6 +10,20 @@
<file>images/icon.png</file> <file>images/icon.png</file>
<file>images/logo.png</file> <file>images/logo.png</file>
<file>images/grades/ex-plus.svg</file>
<file>images/grades/ex-plus-dark.svg</file>
<file>images/grades/ex.svg</file>
<file>images/grades/ex-dark.svg</file>
<file>images/grades/aa.svg</file>
<file>images/grades/aa-dark.svg</file>
<file>images/grades/a.svg</file>
<file>images/grades/a-dark.svg</file>
<file>images/grades/b.svg</file>
<file>images/grades/b-dark.svg</file>
<file>images/grades/c.svg</file>
<file>images/grades/c-dark.svg</file>
<file>images/grades/d.svg</file>
<file>images/grades/d-dark.svg</file>
<file>images/jacket-placeholder.png</file> <file>images/jacket-placeholder.png</file>
<file>images/stepCalculator/stamina.png</file> <file>images/stepCalculator/stamina.png</file>
<file>images/stepCalculator/play.png</file> <file>images/stepCalculator/play.png</file>
@ -19,5 +33,10 @@
<file>lang/zh_CN.qm</file> <file>lang/zh_CN.qm</file>
<file>lang/en_US.qm</file> <file>lang/en_US.qm</file>
<file>themes/m3-dynamic_default.json</file>
<file>themes/m3-dynamic_tempest.json</file>
<file>themes/m3-dynamic_devilun.json</file>
<file>themes/m3-dynamic_kupya.json</file>
</qresource> </qresource>
</RCC> </RCC>

View File

@ -0,0 +1,8 @@
{
"id": "default",
"name": "Default",
"colors": {
"primary": "#4e486c"
},
"options": {}
}

View File

@ -0,0 +1,11 @@
{
"id": "devilun",
"name": "Devilun",
"colors": {
"primary": "#d1779d",
"secondary": "#85a9a5"
},
"options": {
"forceScheme": "dark"
}
}

View File

@ -0,0 +1,10 @@
{
"id": "kupya",
"name": "Kupya",
"colors": {
"primary": "#ffeb9e"
},
"options": {
"forceScheme": "light"
}
}

View File

@ -0,0 +1,8 @@
{
"id": "tempest",
"name": "Tempest",
"colors": {
"primary": "#186d98"
},
"options": {}
}

View File

@ -1,148 +0,0 @@
import logging
import traceback
from enum import IntEnum
from arcaea_offline.database import Database
from PySide6.QtCore import QCoreApplication, QDir, QFileInfo, Qt, QUrl, Slot
from PySide6.QtWidgets import QDialog, QMessageBox
from ui.extends.shared.database import create_engine
from ui.extends.shared.settings import Settings
from .databaseChecker_ui import Ui_DatabaseChecker
logger = logging.getLogger(__name__)
class DatabaseCheckerResult(IntEnum):
FileExist = 0x001
Initted = 0x002
class DatabaseChecker(Ui_DatabaseChecker, QDialog):
def __init__(self, parent=None):
super().__init__(parent)
self.setupUi(self)
self.setWindowFlag(Qt.WindowType.WindowMinimizeButtonHint, False)
self.setWindowFlag(Qt.WindowType.WindowMaximizeButtonHint, False)
self.setWindowFlag(Qt.WindowType.WindowCloseButtonHint, True)
self.dbDirSelector.setMode(self.dbDirSelector.getExistingDirectory)
self.confirmDbByExistingSettings = False
self.settings = Settings(self)
if dbUrlString := self.settings.databaseUrl():
dbFileUrl = QUrl(dbUrlString.replace("sqlite://", "file://"))
dbFileInfo = QFileInfo(dbFileUrl.toLocalFile())
if dbFileInfo.exists():
self.dbDirSelector.selectFile(dbFileInfo.path())
self.dbFilenameLineEdit.setText(dbFileInfo.fileName())
self.confirmDbByExistingSettings = True
self.confirmDbPathButton.click()
else:
self.dbDirSelector.selectFile(QDir.currentPath())
self.dbFilenameLineEdit.setText("arcaea_offline.db")
else:
self.dbDirSelector.selectFile(QDir.currentPath())
self.dbFilenameLineEdit.setText("arcaea_offline.db")
def dbPath(self):
return QDir(self.dbDirSelector.selectedFiles()[0])
def dbFileInfo(self):
return QFileInfo(
QDir.cleanPath(
self.dbPath().absoluteFilePath(self.dbFilenameLineEdit.text())
)
)
def dbFileUrl(self):
return QUrl.fromLocalFile(self.dbFileInfo().filePath())
def dbSqliteUrl(self):
# dbSqliteUrl.setScheme("sqlite")
return QUrl(self.dbFileUrl().toString().replace("file://", "sqlite://"))
def confirmDb(self) -> DatabaseCheckerResult:
flags = 0x000
dbFileInfo = self.dbFileInfo()
dbSqliteUrl = self.dbSqliteUrl()
if not dbFileInfo.exists():
return flags
flags |= DatabaseCheckerResult.FileExist
db = Database(create_engine(dbSqliteUrl))
if db.check_init():
flags |= DatabaseCheckerResult.Initted
self.settings.setDatabaseUrl(self.dbSqliteUrl().toString())
return flags
def updateLabels(self):
result = self.confirmDb()
try:
db = Database()
version = db.version()
initted = db.check_init()
self.dbVersionLabel.setText(str(version))
self.dbCheckConnLabel.setText(
'<font color="green">OK</font>'
if initted
else '<font color="red">Not initted</font>'
)
self.continueButton.setEnabled(initted)
except Exception as e:
self.dbVersionLabel.setText("-")
self.dbCheckConnLabel.setText(
f'<font color="red">Error: {e}</font>'
if result & DatabaseCheckerResult.FileExist
else "-"
)
self.continueButton.setEnabled(False)
@Slot()
def on_confirmDbPathButton_clicked(self):
dbSqliteUrl = self.dbSqliteUrl()
self.settings.setDatabaseUrl(dbSqliteUrl.toString())
result = self.confirmDb()
if result & DatabaseCheckerResult.Initted:
if not self.confirmDbByExistingSettings:
self.settings.setDatabaseUrl(dbSqliteUrl.toString())
elif result & DatabaseCheckerResult.FileExist:
confirm_try_init = QMessageBox.question(
self,
None,
# fmt: off
QCoreApplication.translate("DatabaseChecker", "dialog.tryInitExistingDatabase"),
# fmt: on
)
if confirm_try_init == QMessageBox.StandardButton.Yes:
try:
Database().init(checkfirst=True)
except Exception as e:
logger.exception("Error while initializing an existing database")
QMessageBox.critical(
self, None, "\n".join(traceback.format_exception(e))
)
else:
confirm_new_database = QMessageBox.question(
self,
None,
# fmt: off
QCoreApplication.translate("DatabaseChecker", "dialog.confirmNewDatabase"),
# fmt: on
)
if confirm_new_database == QMessageBox.StandardButton.Yes:
db = Database(create_engine(dbSqliteUrl))
db.init()
self.updateLabels()
@Slot()
def on_dbReInitButton_clicked(self):
Database().init(checkfirst=True)
QMessageBox.information(self, None, "OK")
@Slot()
def on_continueButton_clicked(self):
self.accept()

View File

@ -1,154 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DatabaseChecker</class>
<widget class="QWidget" name="DatabaseChecker">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>350</width>
<height>250</height>
</rect>
</property>
<property name="windowTitle">
<string notr="true">DatabaseChecker</string>
</property>
<layout class="QFormLayout" name="formLayout">
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>dbPathLabel</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="FileSelector" name="dbDirSelector" native="true"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>dbFilenameLabel</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="dbFilenameLineEdit"/>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="confirmDbPathButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>confirmDbPathButton</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="4" column="1">
<widget class="QLabel" name="dbVersionLabel">
<property name="text">
<string notr="true">-</string>
</property>
</widget>
</item>
<item row="6" column="1">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>dbCheckConnLabel</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QLabel" name="dbCheckConnLabel">
<property name="text">
<string notr="true">...</string>
</property>
</widget>
</item>
<item row="8" column="0" colspan="2">
<widget class="QPushButton" name="continueButton">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>continueButton</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>dbVersionLabel</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>dbReInitLabel</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QPushButton" name="dbReInitButton">
<property name="text">
<string>dbReInitButton</string>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>FileSelector</class>
<extends>QWidget</extends>
<header>ui.implements.components.fileSelector</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>

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