4 Commits

Author SHA1 Message Date
ac0d4fec8a wip: navigation system - NavItemWidgets 2023-11-23 00:42:34 +08:00
7821816d09 wip: navigation components 2023-11-18 23:05:10 +08:00
a930cffe39 wip: navigating system 2023-11-07 22:04:31 +08:00
4442e496aa feat: SlidingStackedWidget 2023-10-30 01:55:16 +08:00
133 changed files with 2356 additions and 6826 deletions

View File

@ -1,65 +0,0 @@
name: Build Executable from latest `arcaea-offline-*` dependencies
run-name: ${{ github.actor }} started a build request.
on:
workflow_dispatch:
permissions:
contents: write
discussions: write
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
# install dependencies
- run: "pip install -r requirements.txt"
- 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-ocr"
- run: "pip install imageio"
- name: Install UPX
uses: crazy-max/ghaction-upx@v3
with:
install-only: true
- name: Release builtin files
run: |
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
- name: Build Executable
uses: Nuitka/Nuitka-Action@main
with:
nuitka-version: main
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,78 +0,0 @@
name: Build Executable
run-name: ${{ github.actor }} started a build request.
on:
workflow_dispatch:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+"
permissions:
contents: write
discussions: write
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install imageio
- name: Install UPX
uses: crazy-max/ghaction-upx@v3
with:
install-only: true
- name: Release builtin files
run: |
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
- name: Build Executable
uses: Nuitka/Nuitka-Action@main
with:
nuitka-version: main
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,9 +1,6 @@
__debug*
.vscode/
# QML type cache
internal/
arcaea_offline.db
arcaea_offline.ini
/data

View File

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

View File

@ -1,29 +1,9 @@
# Arcaea Offline PySide UI
[![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)
GUI for both [283375/arcaea-offline](https://github.com/283375/arcaea-offline) and [283375/arcaea-offline-ocr](https://github.com/283375/arcaea-offline-ocr)
GUI for both [283375/arcaea-offline](https://github.com/283375/arcaea-offline) and [ArcaeaOffline/core-ocr](https://github.com/ArcaeaOffline/core-ocr).
## Prerequisites
## Before you run `python index.py`...
* Install requirements
* Release translation files from `ui/resources/lang/*.ts`
* Run `prebuild.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
View File

@ -1,91 +0,0 @@
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()

View File

@ -1,10 +0,0 @@
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)

View File

@ -1,12 +0,0 @@
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",
]

View File

@ -1,31 +0,0 @@
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

@ -1,30 +0,0 @@
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

View File

@ -1,28 +0,0 @@
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

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

View File

@ -1,44 +0,0 @@
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()

View File

@ -1,27 +0,0 @@
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

View File

@ -1,23 +0,0 @@
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

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

View File

@ -40,6 +40,7 @@ def getBuildToolsVer():
def writeVersionFile():
versionFile = Path("ui/resources/VERSION")
assert versionFile.exists()
versionText = (
"arcaea-offline-pyside-ui\n{gitDesc}\n{buildToolsVer}\n\n"

View File

@ -1,84 +1,42 @@
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "arcaea-offline-pyside-ui"
version = "0.3.9"
version = "0.1.0"
authors = [{ name = "283375", email = "log_283375@163.com" }]
description = "No description."
readme = "README.md"
requires-python = ">=3.9"
dependencies = [
"arcaea-offline==0.2.2",
"arcaea-offline-ocr==0.0.99",
"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",
"arcaea-offline==0.1.0",
"arcaea-offline-ocr==0.1.0",
"exif==1.6.0",
"PySide6==6.5.2",
]
classifiers = [
"Development Status :: 3 - Alpha",
"Programming Language :: Python :: 3",
]
[project.optional-dependencies]
dev = [
"ruff>=0.14.2",
"pre-commit>=4.3.0",
"imageio",
"Nuitka~=2.7.6",
]
[project.urls]
"Homepage" = "https://github.com/ArcaeaOffline/client-pyside6"
"Bug Tracker" = "https://github.com/ArcaeaOffline/client-pyside6/issues"
"Homepage" = "https://github.com/283375/arcaea-offline-pyside-ui"
"Bug Tracker" = "https://github.com/283375/arcaea-offline-pyside-ui/issues"
[tool.ruff]
exclude = ["*_ui.py", "*_rc.py"]
[tool.black]
force-exclude = '''
(
ui/designer
| .*_ui.py
| .*_rc.py
)
'''
[tool.ruff.lint]
# Full list: https://docs.astral.sh/ruff/rules
select = [
"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.isort]
profile = "black"
extend_skip = ["ui/designer"]
extend_skip_glob = ["*_ui.py", "*_rc.py"]
[tool.pyright]
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",
]

5
requirements.dev.txt Normal file
View File

@ -0,0 +1,5 @@
black == 23.7.0
isort == 5.12.0
imageio==2.31.4
Nuitka==1.8.4
pytest==7.4.3

5
requirements.txt Normal file
View File

@ -0,0 +1,5 @@
arcaea-offline==0.1.0
arcaea-offline-ocr==0.1.0
exif==1.6.0
PySide6==6.5.2
typing-extensions==4.8.0

View File

View File

@ -0,0 +1,31 @@
from ui.navigation.navhost import NavHost
from ui.navigation.navitem import NavItem
class TestNavHost:
def test_auto_append_parent(self):
navHost = NavHost()
navHost.registerNavItem(NavItem(id="aaa.bbb.ccc.ddd"))
navItems = navHost.navItems
assert NavItem(id="aaa.bbb.ccc.ddd") in navItems
assert NavItem(id="aaa.bbb.ccc") in navItems
assert NavItem(id="aaa.bbb") in navItems
assert NavItem(id="aaa") in navItems
def test_auto_select_child(self):
navHost = NavHost()
navHost.registerNavItem(NavItem(id="aaa"))
navHost.registerNavItem(NavItem(id="bbb"))
assert navHost.currentNavItem.id == "aaa"
navHost.registerNavItem(NavItem(id="aaa.bbb"))
navHost.registerNavItem(NavItem(id="aaa.ccc"))
navHost.navigate("aaa")
assert navHost.currentNavItem.id == "aaa.bbb"

View File

View File

View File

@ -0,0 +1,47 @@
from PySide6.QtWidgets import QWidget
from ui.navigation.navhost import NavHost, navHost
from ui.navigation.navitem import NavItem
from ui.navigation.navitemwidgets import NavItemWidgets
from ui.widgets.slidingstackedwidget import SlidingStackedWidget
class AnimatedStackedNavItemsWidgets(SlidingStackedWidget):
def __init__(
self, navItemWidgets: NavItemWidgets, navHost: NavHost = navHost, parent=None
):
super().__init__(parent)
self.navItemWidgets = navItemWidgets
self.navHost = navHost
self.navHost.activated.connect(self.__switchTo)
self.animationFinished.connect(self.endChangingWidget)
def __switchTo(self, oldNavItem: NavItem, newNavItem: NavItem):
oldNavItemDepth = self.navHost.getNavItemDepth(oldNavItem.id)
newNavItemDepth = self.navHost.getNavItemDepth(newNavItem.id)
if oldNavItemDepth != newNavItemDepth:
slidingDirection = (
self.slidingDirection.RightToLeft
if newNavItemDepth > oldNavItemDepth
else self.slidingDirection.LeftToRight
)
else:
slidingDirection = self.slidingDirection.TopToBottom
newWidget = self.navItemWidgets.get(newNavItem.id) or QWidget()
self.startChangingWidget(newWidget, slidingDirection)
def startChangingWidget(self, newWidget: QWidget, slidingDirection):
newIndex = self.addWidget(newWidget)
[self.widget(i).setEnabled(False) for i in range(self.count())]
self.slideInIdx(newIndex, slidingDirection)
def endChangingWidget(self):
oldWidget = self.widget(0)
self.removeWidget(oldWidget)
newWidget = self.widget(0)
newWidget.setEnabled(True)

View File

@ -22,6 +22,39 @@
<string>queue.title</string>
</property>
<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>
<widget class="QPushButton" name="ocr_addImageButton">
<property name="text">
@ -62,13 +95,6 @@
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="optionsDialogButton">
<property name="text">
<string>queue.optionsButton</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="ocr_startButton">
<property name="text">

View File

@ -1,140 +0,0 @@
<?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

@ -1,107 +0,0 @@
# -*- 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)
from PySide6.QtWidgets import (QAbstractItemView, QApplication, QCheckBox, QGroupBox,
QHBoxLayout, QHeaderView, QLabel, QProgressBar,
QPushButton, QSizePolicy, QSpacerItem, QTableView,
QVBoxLayout, QWidget)
QPushButton, QRadioButton, QSizePolicy, QSpacerItem,
QTableView, QVBoxLayout, QWidget)
class Ui_OcrQueue(object):
def setupUi(self, OcrQueue):
@ -34,6 +34,29 @@ class Ui_OcrQueue(object):
self.groupBox_3.setObjectName(u"groupBox_3")
self.verticalLayout_2 = QVBoxLayout(self.groupBox_3)
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.setObjectName(u"ocr_addImageButton")
@ -55,11 +78,6 @@ class Ui_OcrQueue(object):
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.setObjectName(u"ocr_startButton")
@ -136,10 +154,13 @@ class Ui_OcrQueue(object):
def retranslateUi(self, OcrQueue):
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_removeSelectedButton.setText(QCoreApplication.translate("OcrQueue", u"queue.removeSelected", 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.groupBox_5.setTitle(QCoreApplication.translate("OcrQueue", u"results", None))
self.ocr_acceptSelectedButton.setText(QCoreApplication.translate("OcrQueue", u"results.acceptSelectedButton", None))
@ -148,3 +169,4 @@ class Ui_OcrQueue(object):
self.statusLabel.setText("")
pass
# retranslateUi

View File

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>580</width>
<height>551</height>
<width>630</width>
<height>528</height>
</rect>
</property>
<property name="windowTitle">
@ -17,21 +17,70 @@
<property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<item row="1" column="0">
<item row="0" column="0">
<widget class="QPushButton" name="syncArcSongDbButton">
<property name="text">
<string>syncArcSongDbButton</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLabel" name="label">
<property name="text">
<string>syncArcSongDb.description</string>
</property>
</widget>
</item>
<item row="5" column="0">
<widget class="QPushButton" name="importSt3Button">
<property name="text">
<string>importSt3Button</string>
</property>
</widget>
</item>
<item row="5" column="1">
<widget class="QLabel" name="label_2">
<property name="text">
<string>importSt3.description</string>
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="Line" name="line">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QPushButton" name="exportScoresButton">
<property name="text">
<string>exportScoresButton</string>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QLabel" name="label_3">
<property name="text">
<string>exportScores.description</string>
</property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="Line" name="line_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QPushButton" name="importPacklistButton">
<property name="text">
<string>importPacklistButton</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLabel" name="label_4">
<property name="text">
<string>importPacklist.description</string>
</property>
</widget>
</item>
<item row="2" column="0">
<item row="3" column="0">
<widget class="QPushButton" name="importSonglistButton">
<property name="text">
<string>importSonglistButton</string>
@ -39,129 +88,69 @@
</widget>
</item>
<item row="2" column="1">
<widget class="QLabel" name="label_4">
<property name="text">
<string>importPacklist.description</string>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QLabel" name="label_5">
<property name="text">
<string>importSonglist.description</string>
</property>
</widget>
</item>
<item row="3" column="0">
<item row="9" column="0">
<widget class="QPushButton" name="exportArcsongJsonButton">
<property name="text">
<string>exportArcsongJsonButton</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QLabel" name="label_6">
<property name="text">
<string>exportArcsongJson.description</string>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QPushButton" name="importApkButton">
<property name="text">
<string>importApkButton</string>
</property>
</widget>
</item>
<item row="3" column="1">
<item row="4" column="1">
<widget class="QLabel" name="label_7">
<property name="text">
<string>importApk.description</string>
</property>
</widget>
</item>
<item row="5" column="0" colspan="2">
<widget class="QLabel" name="label_11">
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>chartInfoGroup</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QPushButton" name="syncArcSongDbButton">
<property name="text">
<string>syncArcSongDbButton</string>
</property>
</widget>
</item>
<item row="6" column="1">
<widget class="QLabel" name="label">
<property name="text">
<string>syncArcSongDb.description</string>
</property>
</widget>
</item>
<item row="9" column="0" colspan="2">
<widget class="QLabel" name="label_12">
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>importScoreGroup</string>
</property>
</widget>
</item>
<item row="10" column="0">
<widget class="QPushButton" name="importSt3Button">
<property name="text">
<string>importSt3Button</string>
</property>
</widget>
</item>
<item row="10" column="1">
<widget class="QLabel" name="label_2">
<property name="text">
<string>importSt3.description</string>
</property>
</widget>
</item>
<item row="11" column="0">
<widget class="QPushButton" name="importOnlineButton">
<property name="text">
<string>importOnlineButton</string>
</property>
</widget>
</item>
<item row="11" column="1">
<item row="6" column="1">
<widget class="QLabel" name="label_8">
<property name="text">
<string>importOnline.description</string>
</property>
</widget>
</item>
<item row="13" column="0" colspan="2">
<widget class="QLabel" name="label_13">
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>exportScoreGroup</string>
</property>
</widget>
</item>
<item row="14" column="0">
<widget class="QPushButton" name="exportScoresButton">
<property name="text">
<string>exportScoresButton</string>
</property>
</widget>
</item>
<item row="14" column="1">
<widget class="QLabel" name="label_3">
<property name="text">
<string>exportScores.description</string>
</property>
</widget>
</item>
<item row="15" column="0">
<item row="10" column="0">
<widget class="QPushButton" name="exportSmartRteB30Button">
<property name="text">
<string>exportSmartRteB30Button</string>
</property>
</widget>
</item>
<item row="15" column="1">
<item row="10" column="1">
<widget class="QLabel" name="label_9">
<property name="text">
<string>exportSmartRteB30.description</string>
@ -174,124 +163,6 @@
</property>
</widget>
</item>
<item row="17" column="0" colspan="2">
<widget class="QLabel" name="label_14">
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>miscGroup</string>
</property>
</widget>
</item>
<item row="18" column="0">
<widget class="QPushButton" name="exportArcsongJsonButton">
<property name="text">
<string>exportArcsongJsonButton</string>
</property>
</widget>
</item>
<item row="18" column="1">
<widget class="QLabel" name="label_6">
<property name="text">
<string>exportArcsongJson.description</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2">
<widget class="QLabel" name="label_10">
<property name="font">
<font>
<pointsize>12</pointsize>
<bold>false</bold>
</font>
</property>
<property name="text">
<string>packSongInfoGroup</string>
</property>
</widget>
</item>
<item row="4" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="8" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="12" column="0">
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="16" column="0">
<spacer name="verticalSpacer_4">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Minimum</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="7" column="0">
<widget class="QPushButton" name="syncChartInfoDbButton">
<property name="text">
<string>syncChartInfoDbButton</string>
</property>
</widget>
</item>
<item row="7" column="1">
<widget class="QLabel" name="label_15">
<property name="text">
<string>syncChartInfoDb.description</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>

View File

@ -15,168 +15,123 @@ 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, QFormLayout, QLabel, QPushButton,
QSizePolicy, QSpacerItem, QWidget)
from PySide6.QtWidgets import (QApplication, QFormLayout, QFrame, QLabel,
QPushButton, QSizePolicy, QWidget)
class Ui_TabDb_Manage(object):
def setupUi(self, TabDb_Manage):
if not TabDb_Manage.objectName():
TabDb_Manage.setObjectName(u"TabDb_Manage")
TabDb_Manage.resize(580, 551)
TabDb_Manage.resize(630, 528)
TabDb_Manage.setWindowTitle(u"TabDb_Manage")
self.formLayout = QFormLayout(TabDb_Manage)
self.formLayout.setObjectName(u"formLayout")
self.formLayout.setLabelAlignment(Qt.AlignRight|Qt.AlignTrailing|Qt.AlignVCenter)
self.importPacklistButton = QPushButton(TabDb_Manage)
self.importPacklistButton.setObjectName(u"importPacklistButton")
self.formLayout.setWidget(1, QFormLayout.LabelRole, self.importPacklistButton)
self.label_4 = QLabel(TabDb_Manage)
self.label_4.setObjectName(u"label_4")
self.formLayout.setWidget(1, QFormLayout.FieldRole, self.label_4)
self.importSonglistButton = QPushButton(TabDb_Manage)
self.importSonglistButton.setObjectName(u"importSonglistButton")
self.formLayout.setWidget(2, QFormLayout.LabelRole, self.importSonglistButton)
self.label_5 = QLabel(TabDb_Manage)
self.label_5.setObjectName(u"label_5")
self.formLayout.setWidget(2, QFormLayout.FieldRole, self.label_5)
self.importApkButton = QPushButton(TabDb_Manage)
self.importApkButton.setObjectName(u"importApkButton")
self.formLayout.setWidget(3, QFormLayout.LabelRole, self.importApkButton)
self.label_7 = QLabel(TabDb_Manage)
self.label_7.setObjectName(u"label_7")
self.formLayout.setWidget(3, QFormLayout.FieldRole, self.label_7)
self.label_11 = QLabel(TabDb_Manage)
self.label_11.setObjectName(u"label_11")
font = QFont()
font.setPointSize(12)
font.setBold(False)
self.label_11.setFont(font)
self.formLayout.setWidget(5, QFormLayout.SpanningRole, self.label_11)
self.syncArcSongDbButton = QPushButton(TabDb_Manage)
self.syncArcSongDbButton.setObjectName(u"syncArcSongDbButton")
self.formLayout.setWidget(6, QFormLayout.LabelRole, self.syncArcSongDbButton)
self.formLayout.setWidget(0, QFormLayout.LabelRole, self.syncArcSongDbButton)
self.label = QLabel(TabDb_Manage)
self.label.setObjectName(u"label")
self.formLayout.setWidget(6, QFormLayout.FieldRole, self.label)
self.label_12 = QLabel(TabDb_Manage)
self.label_12.setObjectName(u"label_12")
self.label_12.setFont(font)
self.formLayout.setWidget(9, QFormLayout.SpanningRole, self.label_12)
self.formLayout.setWidget(0, QFormLayout.FieldRole, self.label)
self.importSt3Button = QPushButton(TabDb_Manage)
self.importSt3Button.setObjectName(u"importSt3Button")
self.formLayout.setWidget(10, QFormLayout.LabelRole, self.importSt3Button)
self.formLayout.setWidget(5, QFormLayout.LabelRole, self.importSt3Button)
self.label_2 = QLabel(TabDb_Manage)
self.label_2.setObjectName(u"label_2")
self.formLayout.setWidget(10, QFormLayout.FieldRole, self.label_2)
self.formLayout.setWidget(5, QFormLayout.FieldRole, self.label_2)
self.importOnlineButton = QPushButton(TabDb_Manage)
self.importOnlineButton.setObjectName(u"importOnlineButton")
self.line = QFrame(TabDb_Manage)
self.line.setObjectName(u"line")
self.line.setFrameShape(QFrame.HLine)
self.line.setFrameShadow(QFrame.Sunken)
self.formLayout.setWidget(11, QFormLayout.LabelRole, self.importOnlineButton)
self.label_8 = QLabel(TabDb_Manage)
self.label_8.setObjectName(u"label_8")
self.formLayout.setWidget(11, QFormLayout.FieldRole, self.label_8)
self.label_13 = QLabel(TabDb_Manage)
self.label_13.setObjectName(u"label_13")
self.label_13.setFont(font)
self.formLayout.setWidget(13, QFormLayout.SpanningRole, self.label_13)
self.formLayout.setWidget(7, QFormLayout.SpanningRole, self.line)
self.exportScoresButton = QPushButton(TabDb_Manage)
self.exportScoresButton.setObjectName(u"exportScoresButton")
self.formLayout.setWidget(14, QFormLayout.LabelRole, self.exportScoresButton)
self.formLayout.setWidget(8, QFormLayout.LabelRole, self.exportScoresButton)
self.label_3 = QLabel(TabDb_Manage)
self.label_3.setObjectName(u"label_3")
self.formLayout.setWidget(14, QFormLayout.FieldRole, self.label_3)
self.formLayout.setWidget(8, QFormLayout.FieldRole, self.label_3)
self.line_2 = QFrame(TabDb_Manage)
self.line_2.setObjectName(u"line_2")
self.line_2.setFrameShape(QFrame.HLine)
self.line_2.setFrameShadow(QFrame.Sunken)
self.formLayout.setWidget(1, QFormLayout.SpanningRole, self.line_2)
self.importPacklistButton = QPushButton(TabDb_Manage)
self.importPacklistButton.setObjectName(u"importPacklistButton")
self.formLayout.setWidget(2, QFormLayout.LabelRole, self.importPacklistButton)
self.importSonglistButton = QPushButton(TabDb_Manage)
self.importSonglistButton.setObjectName(u"importSonglistButton")
self.formLayout.setWidget(3, QFormLayout.LabelRole, self.importSonglistButton)
self.label_4 = QLabel(TabDb_Manage)
self.label_4.setObjectName(u"label_4")
self.formLayout.setWidget(2, QFormLayout.FieldRole, self.label_4)
self.label_5 = QLabel(TabDb_Manage)
self.label_5.setObjectName(u"label_5")
self.formLayout.setWidget(3, QFormLayout.FieldRole, self.label_5)
self.exportArcsongJsonButton = QPushButton(TabDb_Manage)
self.exportArcsongJsonButton.setObjectName(u"exportArcsongJsonButton")
self.formLayout.setWidget(9, QFormLayout.LabelRole, self.exportArcsongJsonButton)
self.label_6 = QLabel(TabDb_Manage)
self.label_6.setObjectName(u"label_6")
self.formLayout.setWidget(9, QFormLayout.FieldRole, self.label_6)
self.importApkButton = QPushButton(TabDb_Manage)
self.importApkButton.setObjectName(u"importApkButton")
self.formLayout.setWidget(4, QFormLayout.LabelRole, self.importApkButton)
self.label_7 = QLabel(TabDb_Manage)
self.label_7.setObjectName(u"label_7")
self.formLayout.setWidget(4, QFormLayout.FieldRole, self.label_7)
self.importOnlineButton = QPushButton(TabDb_Manage)
self.importOnlineButton.setObjectName(u"importOnlineButton")
self.formLayout.setWidget(6, QFormLayout.LabelRole, self.importOnlineButton)
self.label_8 = QLabel(TabDb_Manage)
self.label_8.setObjectName(u"label_8")
self.formLayout.setWidget(6, QFormLayout.FieldRole, self.label_8)
self.exportSmartRteB30Button = QPushButton(TabDb_Manage)
self.exportSmartRteB30Button.setObjectName(u"exportSmartRteB30Button")
self.formLayout.setWidget(15, QFormLayout.LabelRole, self.exportSmartRteB30Button)
self.formLayout.setWidget(10, QFormLayout.LabelRole, self.exportSmartRteB30Button)
self.label_9 = QLabel(TabDb_Manage)
self.label_9.setObjectName(u"label_9")
self.label_9.setOpenExternalLinks(True)
self.label_9.setTextInteractionFlags(Qt.LinksAccessibleByKeyboard|Qt.LinksAccessibleByMouse)
self.formLayout.setWidget(15, QFormLayout.FieldRole, self.label_9)
self.label_14 = QLabel(TabDb_Manage)
self.label_14.setObjectName(u"label_14")
self.label_14.setFont(font)
self.formLayout.setWidget(17, QFormLayout.SpanningRole, self.label_14)
self.exportArcsongJsonButton = QPushButton(TabDb_Manage)
self.exportArcsongJsonButton.setObjectName(u"exportArcsongJsonButton")
self.formLayout.setWidget(18, QFormLayout.LabelRole, self.exportArcsongJsonButton)
self.label_6 = QLabel(TabDb_Manage)
self.label_6.setObjectName(u"label_6")
self.formLayout.setWidget(18, QFormLayout.FieldRole, self.label_6)
self.label_10 = QLabel(TabDb_Manage)
self.label_10.setObjectName(u"label_10")
self.label_10.setFont(font)
self.formLayout.setWidget(0, QFormLayout.SpanningRole, self.label_10)
self.verticalSpacer = QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Minimum)
self.formLayout.setItem(4, QFormLayout.LabelRole, self.verticalSpacer)
self.verticalSpacer_2 = QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Minimum)
self.formLayout.setItem(8, QFormLayout.LabelRole, self.verticalSpacer_2)
self.verticalSpacer_3 = QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Minimum)
self.formLayout.setItem(12, QFormLayout.LabelRole, self.verticalSpacer_3)
self.verticalSpacer_4 = QSpacerItem(20, 20, QSizePolicy.Minimum, QSizePolicy.Minimum)
self.formLayout.setItem(16, QFormLayout.LabelRole, self.verticalSpacer_4)
self.syncChartInfoDbButton = QPushButton(TabDb_Manage)
self.syncChartInfoDbButton.setObjectName(u"syncChartInfoDbButton")
self.formLayout.setWidget(7, QFormLayout.LabelRole, self.syncChartInfoDbButton)
self.label_15 = QLabel(TabDb_Manage)
self.label_15.setObjectName(u"label_15")
self.formLayout.setWidget(7, QFormLayout.FieldRole, self.label_15)
self.formLayout.setWidget(10, QFormLayout.FieldRole, self.label_9)
self.retranslateUi(TabDb_Manage)
@ -185,30 +140,24 @@ class Ui_TabDb_Manage(object):
# setupUi
def retranslateUi(self, TabDb_Manage):
self.importPacklistButton.setText(QCoreApplication.translate("TabDb_Manage", u"importPacklistButton", None))
self.label_4.setText(QCoreApplication.translate("TabDb_Manage", u"importPacklist.description", None))
self.importSonglistButton.setText(QCoreApplication.translate("TabDb_Manage", u"importSonglistButton", None))
self.label_5.setText(QCoreApplication.translate("TabDb_Manage", u"importSonglist.description", None))
self.importApkButton.setText(QCoreApplication.translate("TabDb_Manage", u"importApkButton", None))
self.label_7.setText(QCoreApplication.translate("TabDb_Manage", u"importApk.description", None))
self.label_11.setText(QCoreApplication.translate("TabDb_Manage", u"chartInfoGroup", None))
self.syncArcSongDbButton.setText(QCoreApplication.translate("TabDb_Manage", u"syncArcSongDbButton", None))
self.label.setText(QCoreApplication.translate("TabDb_Manage", u"syncArcSongDb.description", None))
self.label_12.setText(QCoreApplication.translate("TabDb_Manage", u"importScoreGroup", None))
self.importSt3Button.setText(QCoreApplication.translate("TabDb_Manage", u"importSt3Button", None))
self.label_2.setText(QCoreApplication.translate("TabDb_Manage", u"importSt3.description", None))
self.importOnlineButton.setText(QCoreApplication.translate("TabDb_Manage", u"importOnlineButton", None))
self.label_8.setText(QCoreApplication.translate("TabDb_Manage", u"importOnline.description", None))
self.label_13.setText(QCoreApplication.translate("TabDb_Manage", u"exportScoreGroup", None))
self.exportScoresButton.setText(QCoreApplication.translate("TabDb_Manage", u"exportScoresButton", None))
self.label_3.setText(QCoreApplication.translate("TabDb_Manage", u"exportScores.description", None))
self.exportSmartRteB30Button.setText(QCoreApplication.translate("TabDb_Manage", u"exportSmartRteB30Button", None))
self.label_9.setText(QCoreApplication.translate("TabDb_Manage", u"exportSmartRteB30.description", None))
self.label_14.setText(QCoreApplication.translate("TabDb_Manage", u"miscGroup", None))
self.importPacklistButton.setText(QCoreApplication.translate("TabDb_Manage", u"importPacklistButton", None))
self.importSonglistButton.setText(QCoreApplication.translate("TabDb_Manage", u"importSonglistButton", None))
self.label_4.setText(QCoreApplication.translate("TabDb_Manage", u"importPacklist.description", None))
self.label_5.setText(QCoreApplication.translate("TabDb_Manage", u"importSonglist.description", None))
self.exportArcsongJsonButton.setText(QCoreApplication.translate("TabDb_Manage", u"exportArcsongJsonButton", None))
self.label_6.setText(QCoreApplication.translate("TabDb_Manage", u"exportArcsongJson.description", None))
self.label_10.setText(QCoreApplication.translate("TabDb_Manage", u"packSongInfoGroup", None))
self.syncChartInfoDbButton.setText(QCoreApplication.translate("TabDb_Manage", u"syncChartInfoDbButton", None))
self.label_15.setText(QCoreApplication.translate("TabDb_Manage", u"syncChartInfoDb.description", None))
self.importApkButton.setText(QCoreApplication.translate("TabDb_Manage", u"importApkButton", None))
self.label_7.setText(QCoreApplication.translate("TabDb_Manage", u"importApk.description", None))
self.importOnlineButton.setText(QCoreApplication.translate("TabDb_Manage", u"importOnlineButton", None))
self.label_8.setText(QCoreApplication.translate("TabDb_Manage", u"importOnline.description", None))
self.exportSmartRteB30Button.setText(QCoreApplication.translate("TabDb_Manage", u"exportSmartRteB30Button", None))
self.label_9.setText(QCoreApplication.translate("TabDb_Manage", u"exportSmartRteB30.description", None))
pass
# retranslateUi

View File

@ -0,0 +1,124 @@
<?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

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

View File

@ -0,0 +1,39 @@
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

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

View File

@ -1,4 +1,14 @@
from PySide6.QtCore import QObject, Signal
from typing import Type
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):

View File

@ -82,7 +82,6 @@ class ChartDelegate(TextSegmentDelegate):
QColor("#809955"),
QColor("#702d60"),
QColor("#710f25"),
QColor("#8b77a4"),
]
ChartInvalidBackgroundColor = QColor("#e6a23c")

View File

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

View File

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

View File

@ -0,0 +1,111 @@
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

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

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 PySide6.QtGui import QImage
from ui.extends.components.ocrQueue import OcrRunnable
logger = logging.getLogger(__name__)
from ui.extends.components.ocrQueue import OcrRunnable
class ChieriV4OcrRunnable(OcrRunnable):
def __init__(self, ocr: ChieriBotV4Ocr, component):

View File

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

View File

@ -41,7 +41,7 @@ class AndrealExecuteRunnable(QRunnable):
self.signals.completed.emit(self.jsonPath, imageBytes)
except Exception as e:
imageBytes = None
logger.exception("%s error", self.__class__.__name__)
logger.exception(f"{self.__class__.__name__} error")
self.signals.error.emit(self.jsonPath, str(e))
finally:
os.unlink(self.jsonPath)
@ -84,10 +84,7 @@ class AndrealHelper(QObject):
def request(self, jsonPath: str, arguments: list[str]):
logger.debug(
"%s received request jsonPath=%r arguments=%r",
self.__class__.__name__,
jsonPath,
arguments,
f"{self.__class__.__name__} received request {jsonPath=} {arguments=}"
)
runnable = AndrealExecuteRunnable(self.andrealExecutable, jsonPath, arguments)
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.QtWidgets import QFileDialog, QWidget
from core.settings import settings
from ui.designer.components.fileSelector_ui import Ui_FileSelector
from ui.extends.shared.language import LanguageChangeEventFilter
from ui.extends.shared.settings import Settings
class FileSelector(Ui_FileSelector, QWidget):
@ -122,13 +122,13 @@ class FileSelector(Ui_FileSelector, QWidget):
if self.__selectedFiles:
return
if value := settings.value(self.settingsKey):
if value := Settings().value(self.settingsKey):
self.selectFile(value)
settings.updated.connect(self.settingsUpdated)
Settings().updated.connect(self.settingsUpdated)
def disconnectSettings(self):
settings.updated.disconnect(self.settingsUpdated)
Settings().updated.disconnect(self.settingsUpdated)
self.settingsKey = None
def settingsUpdated(self, key: str):
@ -139,4 +139,4 @@ class FileSelector(Ui_FileSelector, QWidget):
if self.__selectedFiles:
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.QtGui import QColor, QPalette
from PySide6.QtWidgets import QWidget
from PySide6.QtWidgets import QButtonGroup, QWidget
from ui.designer.components.ocrQueue_ui import Ui_OcrQueue
from ui.extends.components.ocrQueue import (
@ -13,7 +13,6 @@ from ui.extends.components.ocrQueue import (
OcrScoreDelegate,
)
from ui.extends.shared.language import LanguageChangeEventFilter
from ui.implements.components.ocrQueueOptionsDialog import OcrQueueOptionsDialog
class OcrQueue(Ui_OcrQueue, QWidget):
@ -27,9 +26,6 @@ class OcrQueue(Ui_OcrQueue, QWidget):
self.__model: Optional[OcrQueueModel] = None
self.__tableProxyModel: Optional[OcrQueueTableProxyModel] = None
self.optionsDialog = OcrQueueOptionsDialog(self)
self.optionsDialog.iccOptionsChanged.connect(self.setIccOption)
self.__firstResizeDone = False
self.resizeTimer = QTimer(self)
self.resizeTimer.timeout.connect(self.tableView.resizeRowsToContents)
@ -45,6 +41,13 @@ class OcrQueue(Ui_OcrQueue, QWidget):
tableViewPalette.setColor(QPalette.ColorRole.Highlight, highlightColor)
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.setSingleShot(True)
self.statusLabelClearTimer.timeout.connect(self.clearStatusMessage)
@ -90,10 +93,9 @@ class OcrQueue(Ui_OcrQueue, QWidget):
self.ocr_acceptAllButton.setEnabled(__bool)
self.ocr_ignoreValidateCheckBox.setEnabled(__bool)
@Slot(int)
def setIccOption(self, option):
def updateIccOption(self):
if self.model():
self.model().iccOption = option
self.model().iccOption = self.iccOptionButtonGroup.checkedId()
def showStatusMessage(self, message: str):
self.statusLabel.setText(message)
@ -129,10 +131,6 @@ class OcrQueue(Ui_OcrQueue, QWidget):
def modelReseted(self):
self.progressBar.setMaximum(0)
@Slot()
def on_optionsDialogButton_clicked(self):
self.optionsDialog.exec()
@Slot()
def on_ocr_removeSelectedButton_clicked(self):
if self.model():

View File

@ -1,49 +0,0 @@
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.QtWidgets import QGraphicsColorizeEffect, QRadioButton
from core.color import mixColor
from ui.extends.shared.color import mix_color
STYLESHEET = """
QRadioButton {{
@ -40,7 +40,7 @@ class RatingClassRadioButton(QRadioButton):
def setColors(self, dark_color: QColor, text_color: QColor):
self._dark_color = dark_color
self._text_color = text_color
self._mid_color = mixColor(dark_color, text_color, 0.616)
self._mid_color = mix_color(dark_color, text_color, 0.616)
self.updateEffects()
def isColorsSet(self) -> bool:

View File

@ -1,4 +1,3 @@
import logging
from typing import Type
from PySide6.QtCore import Signal
@ -7,8 +6,6 @@ from PySide6.QtWidgets import QHBoxLayout, QSizePolicy, QVBoxLayout, QWidget
from ui.implements.components.ratingClassRadioButton import RatingClassRadioButton
logger = logging.getLogger(__name__)
class RatingClassSelector(QWidget):
valueChanged = Signal()
@ -44,30 +41,16 @@ class RatingClassSelector(QWidget):
self.bydButton.setAutoExclusive(False)
self.preferredLayout.addWidget(self.bydButton)
self.etrButton = RatingClassRadioButton(self)
self.etrButton.setObjectName("etrButton")
self.etrButton.setText("ETERNAL")
self.etrButton.setAutoExclusive(False)
self.preferredLayout.addWidget(self.etrButton)
self.buttons = [
self.pstButton,
self.prsButton,
self.ftrButton,
self.bydButton,
self.etrButton,
]
self.buttons = [self.pstButton, self.prsButton, self.ftrButton, self.bydButton]
self.pstButton.setColors(QColor("#399bb2"), QColor("#f0f8fa"))
self.prsButton.setColors(QColor("#809955"), QColor("#f7f9f4"))
self.ftrButton.setColors(QColor("#702d60"), QColor("#f7ebf4"))
self.bydButton.setColors(QColor("#710f25"), QColor("#f9ced8"))
self.etrButton.setColors(QColor("#4f2c7a"), QColor("#e4daf1"))
self.pstButton.clicked.connect(self.select)
self.prsButton.clicked.connect(self.select)
self.ftrButton.clicked.connect(self.select)
self.bydButton.clicked.connect(self.select)
self.etrButton.clicked.connect(self.select)
self.reset()
self.setButtonsEnabled([])
@ -123,10 +106,9 @@ class RatingClassSelector(QWidget):
if ratingClass is None or isinstance(ratingClass, bool):
button = self.sender()
elif ratingClass in range(len(self.buttons)):
elif ratingClass in range(4):
button = self.buttons[ratingClass]
else:
logger.debug("Cannot select ratingClass=%s, condition check failed", ratingClass)
return
if not button.isEnabled():

View File

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

View File

@ -1,18 +1,14 @@
import dataclasses
import logging
import re
from enum import IntEnum
from typing import Any
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 QModelIndex, QObject, QSignalMapper, Qt, Signal, Slot
from PySide6.QtGui import QStandardItem, QStandardItemModel
from PySide6.QtWidgets import QComboBox, QCompleter, QWidget
from PySide6.QtCore import QModelIndex, QSignalMapper, Qt, Signal, Slot
from PySide6.QtWidgets import QCompleter, QWidget
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.delegates.descriptionDelegate import DescriptionDelegate
from ui.extends.shared.language import LanguageChangeEventFilter
@ -25,168 +21,6 @@ class SongIdSelectorMode(IntEnum):
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):
valueChanged = Signal()
quickSearchActivated = Signal(Chart)
@ -199,10 +33,6 @@ class SongIdSelector(Ui_SongIdSelector, QWidget):
self.languageChangeEventFilter = LanguageChangeEventFilter(self)
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
self.quickSwitchSignalMapper = QSignalMapper(self)
self.previousPackageButton.clicked.connect(self.quickSwitchSignalMapper.map)
@ -245,7 +75,6 @@ class SongIdSelector(Ui_SongIdSelector, QWidget):
databaseUpdateSignals.songAddOrDelete.connect(self.updateDatabase)
def setMode(self, mode: SongIdSelectorMode):
self.vm.mode = mode
self.mode = mode
@Slot(str)
@ -287,7 +116,7 @@ class SongIdSelector(Ui_SongIdSelector, QWidget):
pack = self.packComboBox.currentData()
songId = self.songIdComboBox.currentData()
self.vm.updatePackComboBoxItems()
self.fillPackComboBox()
if pack:
self.selectPack(pack)
@ -296,24 +125,58 @@ class SongIdSelector(Ui_SongIdSelector, QWidget):
def fillPackComboBox(self):
self.packComboBox.clear()
for item in self.vm.packComboBoxItems:
item.apply(self.packComboBox)
packs = self.db.get_packs()
for pack in packs:
if isAppendPack := re.search(r"_append_.*$", pack.id):
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)
def fillSongIdComboBox(self):
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
for item in self.vm.songIdComboBoxItems:
item.apply(self.songIdComboBox)
if itemId not in insertedSongIds:
self.songIdComboBox.addItem(f"{item.title} ({itemId})", itemId)
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)
@Slot()
def on_packComboBox_currentIndexChanged(self):
if packId := self.packComboBox.currentData():
self.vm.updateSongIdComboBoxItems(packId)
self.fillSongIdComboBox()
@Slot(str)
def on_searchLineEdit_textChanged(self, text: str):
@ -326,10 +189,10 @@ class SongIdSelector(Ui_SongIdSelector, QWidget):
packIdIndex = self.packComboBox.findData(packId)
if packIdIndex > -1:
self.packComboBox.setCurrentIndex(packIdIndex)
self.vm.updateSongIdComboBoxItems(packId)
self.fillSongIdComboBox()
return True
else:
logger.warning("Attempting to select an unknown pack [%s]", packId)
logger.warning(f'Attempting to select an unknown pack "{packId}"')
return False
def selectSongId(self, songId: str) -> bool:
@ -339,8 +202,7 @@ class SongIdSelector(Ui_SongIdSelector, QWidget):
return True
else:
logger.warning(
"Attempting to select an unknown song [%s], maybe try selecting a pack first?",
songId,
f'Attempting to select an unknown song "{songId}", maybe try selecting a pack first?'
)
return False
@ -351,7 +213,7 @@ class SongIdSelector(Ui_SongIdSelector, QWidget):
@Slot(QModelIndex)
def searchCompleterSetSelection(self, index: QModelIndex):
chart: Chart = index.data(SearchCompleterModel.ChartRole)
chart: Chart = index.data(Qt.ItemDataRole.UserRole + 10)
self.selectChart(chart)
self.quickSearchActivated.emit(chart)

View File

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

View File

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

View File

@ -1,4 +1,6 @@
from PySide6.QtCore import QCoreApplication, QDir, QLocale
import sys
from PySide6.QtCore import QCoreApplication, QDir, QLocale, QProcess
from PySide6.QtWidgets import (
QApplication,
QCheckBox,
@ -8,8 +10,8 @@ from PySide6.QtWidgets import (
QPushButton,
)
from core.settings import SettingsKeys, settings
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
@ -31,8 +33,8 @@ class SettingsGeneral(SettingsBaseWidget):
self.languageFollowSystemCheckBox.toggled.connect(
self.changeLanguageFollowSystem
)
if language := settings.stringValue(SettingsKeys.General.Language):
locale = QLocale(language)
if self.settings.language():
locale = QLocale(self.settings.language())
index = self.languageValueWidget.findData(locale)
if index > -1:
self.languageValueWidget.setCurrentIndex(index)
@ -49,7 +51,7 @@ class SettingsGeneral(SettingsBaseWidget):
self.insertItem(
"dbUrl",
self.dbUrlLabel,
QLabel(settings.stringValue(SettingsKeys.General.DatabaseUrl)),
QLabel(self.settings.databaseUrl()),
self.dbUrlResetButton,
)
@ -57,13 +59,13 @@ class SettingsGeneral(SettingsBaseWidget):
locale = self.languageValueWidget.currentData()
if locale:
changeAppLanguage(locale)
settings.setValue(SettingsKeys.General.Language, localeToCode(locale))
self.settings.setLanguage(localeToCode(locale))
def changeLanguageFollowSystem(self):
followSystem = self.languageFollowSystemCheckBox.isChecked()
self.languageValueWidget.setCurrentIndex(-1)
if followSystem:
settings.remove(SettingsKeys.General.Language)
self.settings.remove(LANGUAGE)
changeAppLanguage(QLocale.system())
self.languageValueWidget.setEnabled(False)
else:
@ -78,7 +80,7 @@ class SettingsGeneral(SettingsBaseWidget):
QMessageBox.StandardButton.No,
)
if userConfirm == QMessageBox.StandardButton.Yes:
settings.remove(SettingsKeys.General.DatabaseUrl)
self.settings.remove(DATABASE_URL)
QApplication.instance().quit()
def setupUi(self, *args):

View File

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

View File

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

View File

@ -14,10 +14,9 @@ from arcaea_offline.external.arcaea import (
)
from arcaea_offline.external.arcaea.common import ArcaeaParser
from arcaea_offline.external.arcsong import ArcsongDbParser
from arcaea_offline.external.chart_info_db import ChartInfoDbParser
from arcaea_offline.external.smartrte import SmartRteB30CsvConverter
from arcaea_offline.models import Difficulty, Pack, Song
from PySide6.QtCore import QDateTime, QDir, Slot
from PySide6.QtCore import QDir, Slot
from PySide6.QtWidgets import QFileDialog, QMessageBox, QWidget
from ui.designer.tabs.tabDb.tabDb_Manage_ui import Ui_TabDb_Manage
@ -58,29 +57,6 @@ class TabDb_Manage(Ui_TabDb_Manage, QWidget):
self, "Sync Error", "\n".join(traceback.format_exception(e))
)
@Slot()
def on_syncChartInfoDbButton_clicked(self):
dbFile, filter = QFileDialog.getOpenFileName(
self, None, "", "DB File (*.db);;*"
)
if not dbFile:
return
try:
db = Database()
parser = ChartInfoDbParser(dbFile)
with db.sessionmaker() as session:
parser.write_database(session)
session.commit()
databaseUpdateSignals.chartInfoUpdated.emit()
QMessageBox.information(self, None, "OK")
except Exception as e:
logging.exception("Sync chart info database error")
QMessageBox.critical(
self, "Sync Error", "\n".join(traceback.format_exception(e))
)
def importFromArcaeaParser(
self, parser: ArcaeaParser, instance, logName, path
) -> int:
@ -91,7 +67,7 @@ class TabDb_Manage(Ui_TabDb_Manage, QWidget):
session.commit()
databaseUpdateSignals.songAddOrDelete.emit()
itemNum = len([item for item in parser.parse() if isinstance(item, instance)])
logger.info("updated %d %s from %s", itemNum, logName, path)
logger.info(f"updated {itemNum} {logName} from {path}")
return itemNum
def importPacklist(self, packlistPath):
@ -161,7 +137,7 @@ class TabDb_Manage(Ui_TabDb_Manage, QWidget):
return
try:
logger.info("Importing %s", apkFile)
logger.info(f"Importing {apkFile}")
with zipfile.ZipFile(apkFile) as zf:
packlistPath = zipfile.Path(zf) / "assets" / "songs" / "packlist"
@ -193,9 +169,7 @@ class TabDb_Manage(Ui_TabDb_Manage, QWidget):
db = Database()
parser = St3ScoreParser(dbFile)
logger.info(
"Got %d items from %s, writing into database...",
len(parser.parse()),
dbFile,
f"Got {len(parser.parse())} items from {dbFile}, writing into database..."
)
with db.sessionmaker() as session:
parser.write_database(session)
@ -220,9 +194,7 @@ class TabDb_Manage(Ui_TabDb_Manage, QWidget):
db = Database()
parser = ArcaeaOnlineParser(apiResultFile)
logger.info(
"Got %d items from %s, writing into database...",
len(parser.parse()),
apiResultFile,
f"Got {len(parser.parse())} items from {apiResultFile}, writing into database..."
)
with db.sessionmaker() as session:
parser.write_database(session)
@ -236,14 +208,14 @@ class TabDb_Manage(Ui_TabDb_Manage, QWidget):
@Slot()
def on_exportScoresButton_clicked(self):
scores = Database().export_scores_def_v2()
timestamp = QDateTime.currentMSecsSinceEpoch()
scores = Database().export_scores()
version = Database().version()
content = json.dumps(scores, ensure_ascii=False)
exportLocation, _filter = QFileDialog.getSaveFileName(
self,
"Save your scores to...",
QDir.current().filePath(f"arcaea-offline-def-v2-scores-{timestamp}.json"),
QDir.current().filePath(f"arcaea-offline-scores-v{version}.json"),
"JSON (*.json);;*",
)
with open(exportLocation, "w", encoding="utf-8") as f:

View File

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

View File

@ -9,7 +9,6 @@ from PIL import Image
from PySide6.QtCore import Signal, Slot
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.extends.components.ocrQueue import OcrQueueModel
from ui.extends.ocr.dependencies import (
@ -17,6 +16,11 @@ from ui.extends.ocr.dependencies import (
getPhashDatabaseStatusText,
)
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
logger = logging.getLogger(__name__)
@ -51,15 +55,9 @@ class TabOcr_B30(Ui_TabOcr_B30, QWidget):
self.ocr = None
logger.info("Applying settings...")
self.dependencies_knnModelSelector.connectSettings(
SettingsKeys.Ocr.KnnModelFile
)
self.dependencies_b30KnnModelSelector.connectSettings(
SettingsKeys.Ocr.B30KnnModelFile
)
self.dependencies_phashDatabaseSelector.connectSettings(
SettingsKeys.Ocr.PhashDatabaseFile
)
self.dependencies_knnModelSelector.connectSettings(KNN_MODEL_FILE)
self.dependencies_b30KnnModelSelector.connectSettings(B30_KNN_MODEL_FILE)
self.dependencies_phashDatabaseSelector.connectSettings(PHASH_DATABASE_FILE)
self.ocrQueueModel = OcrQueueModel(self)
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.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.extends.components.ocrQueue import OcrQueueModel
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
logger = logging.getLogger(__name__)
@ -54,12 +54,8 @@ class TabOcr_Device(Ui_TabOcr_Device, QWidget):
)
logger.info("Applying settings...")
self.dependencies_knnModelSelector.connectSettings(
SettingsKeys.Ocr.KnnModelFile
)
self.dependencies_phashDatabaseSelector.connectSettings(
SettingsKeys.Ocr.PhashDatabaseFile
)
self.dependencies_knnModelSelector.connectSettings(KNN_MODEL_FILE)
self.dependencies_phashDatabaseSelector.connectSettings(PHASH_DATABASE_FILE)
self.options_usePresetCheckBox.setChecked(True)
self.options_usePresetCheckBox.setEnabled(False)

View File

@ -0,0 +1,55 @@
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.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.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.implements.components.chartSelector import ChartSelector
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.andrealExecutableSelector.filesSelected.connect(self.setHelperPaths)
self.andrealFolderSelector.connectSettings(SettingsKeys.Andreal.Folder)
self.andrealExecutableSelector.connectSettings(SettingsKeys.Andreal.Executable)
self.andrealFolderSelector.connectSettings(ANDREAL_FOLDER)
self.andrealExecutableSelector.connectSettings(ANDREAL_EXECUTABLE)
self.generatePreviewButton.clicked.connect(self.requestPreview)
self.generateImageButton.clicked.connect(self.requestGenerate)
@ -131,8 +131,13 @@ class TabTools_Andreal(Ui_TabTools_Andreal, QWidget):
@Slot()
def on_imageTypeWhatIsThisButton_clicked(self):
message = QCoreApplication.translate("TabTools_Andreal", "imageWhatIsThisDialog.description") # fmt: skip
QMessageBox.information(self, None, message)
QMessageBox.information(
self,
None,
# fmt: off
QCoreApplication.translate("TabTools_Andreal", "imageWhatIsThisDialog.description"),
# fmt: on
)
def imageFormat(self):
buttonId = self.imageFormatButtonGroup.checkedId()

View File

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

View File

151
ui/navigation/navhost.py Normal file
View File

@ -0,0 +1,151 @@
import logging
from dataclasses import dataclass
from typing import Optional
from PySide6.QtCore import QObject, Signal
from .navitem import NavItem
logger = logging.getLogger(__name__)
@dataclass
class NavItemRelatives:
parent: Optional[NavItem]
children: list[NavItem]
class NavHost(QObject):
activated = Signal(NavItem, NavItem)
navItemsChanged = Signal()
def __init__(self, parent=None):
super().__init__(parent)
self.__navItems: list[NavItem] = []
self.__cachedNavItems: list[NavItem] = []
self.__currentNavItem: NavItem = None
def __flushCachedNavItems(self):
navItems = set(self.__navItems)
for item in self.__navItems:
parts = item.id.split(".")
for i in range(1, len(parts)):
parentItemId = ".".join(parts[:i])
navItems.add(NavItem(id=parentItemId))
self.__cachedNavItems = list(navItems)
@property
def navItems(self) -> list[NavItem]:
return self.__cachedNavItems
@property
def currentNavItem(self) -> NavItem:
if self.__currentNavItem:
return self.__currentNavItem
if self.__navItems:
self.__currentNavItem = self.__navItems[0]
return self.__currentNavItem
def findNavItem(self, navItemId: str) -> Optional[NavItem]:
navItemIds = [item.id for item in self.__navItems]
try:
index = navItemIds.index(navItemId)
return self.__navItems[index]
except IndexError:
return None
def isNavigatingBack(self, oldNavItemId: str, newNavItemId: str) -> bool:
# sourcery skip: class-extract-method
# | oldNavItemId | newNavItemId | back? |
# |-----------------------|--------------------|-------|
# | database.manage.packs | database | True |
# | ocr.device | ocr | True |
# | database.manage.songs | ocr.b30 | False |
# | database | database | False |
oldNavItemIdSplitted = self.getNavItemIdSplitted(oldNavItemId)
newNavItemIdSplitted = self.getNavItemIdSplitted(newNavItemId)
return self.getNavItemDepth(newNavItemId) < self.getNavItemDepth(
oldNavItemId
) and all((idFrag in oldNavItemIdSplitted) for idFrag in newNavItemIdSplitted)
def isChild(self, childNavItemId: str, parentNavItemId: str) -> bool:
childNavItemIdSplitted = self.getNavItemIdSplitted(childNavItemId)
parentNavItemIdSplitted = self.getNavItemIdSplitted(parentNavItemId)
return all(
(idFrag in childNavItemIdSplitted) for idFrag in parentNavItemIdSplitted
)
def getNavItemIdSplitted(self, navItemId: str) -> list[str]:
return navItemId.split(".")
def getNavItemDepth(self, navItemId: str) -> int:
return len(self.getNavItemIdSplitted(navItemId))
def getNavItemRelatives(self, navItemId: str) -> NavItemRelatives:
parent = None
if "." in navItemId:
navItemIdSplitted = navItemId.split(".")
parentId = navItemIdSplitted[:-1]
parent = self.findNavItem(".".join(parentId))
if not navItemId:
# return root navItems
children = [navItem for navItem in self.__navItems if "." not in navItem.id]
else:
children = [
navItem
for navItem in self.__navItems
if navItem.id.startswith(navItemId)
and navItem.id != navItemId
and self.getNavItemDepth(navItem.id)
== self.getNavItemDepth(navItemId) + 1
]
return NavItemRelatives(parent=parent, children=children)
def registerNavItem(self, item: NavItem):
self.__navItems.append(item)
self.__flushCachedNavItems()
self.navItemsChanged.emit()
def navigate(self, navItemId: str):
oldNavItem = self.__currentNavItem or NavItem("")
newNavItem = self.findNavItem(navItemId)
if newNavItem is None:
raise IndexError(
f"Cannot find '{navItemId}' in {repr(self)}. "
"Maybe try registering it?"
)
# if the navItem have children, navigate to first child
# but if the navItem is going back, e.g. 'database.manage' -> 'database'
# then don't navigate to it's child.
if self.isNavigatingBack(oldNavItem.id, newNavItem.id):
# navItem is going back
currentNavItem = newNavItem
else:
newNavItemRelatives = self.getNavItemRelatives(newNavItem.id)
if newNavItemRelatives.children:
currentNavItem = newNavItemRelatives.children[0]
else:
currentNavItem = newNavItem
self.__currentNavItem = currentNavItem
self.activated.emit(oldNavItem, self.currentNavItem)
def navigateUp(self):
navItemRelatives = self.getNavItemRelatives(self.currentNavItem.id)
if navItemRelatives.parent:
self.navigate(navItemRelatives.parent.id)
navHost = NavHost()

17
ui/navigation/navitem.py Normal file
View File

@ -0,0 +1,17 @@
from dataclasses import dataclass
from typing import Optional
from PySide6.QtCore import QCoreApplication
from PySide6.QtGui import QIcon, QPixmap
@dataclass
class NavItem:
id: str
icon: Optional[QIcon | QPixmap | str] = None
def text(self):
return QCoreApplication.translate("NavItem", f"{self.id}.title")
def __hash__(self):
return hash(self.id)

View File

@ -0,0 +1,84 @@
from PySide6.QtCore import QObject
from PySide6.QtGui import QFont
from PySide6.QtWidgets import QLabel, QSizePolicy, QSpacerItem, QVBoxLayout, QWidget
from ui.navigation.navhost import NavHost, navHost
from ui.navigation.navitem import NavItem
from ui.navigation.navsidebar import NavigationSideBar
class DefaultParentNavItemWidget(QWidget):
def __init__(self, navItem: NavItem, navItemChildren: list[NavItem], parent=None):
super().__init__(parent)
self.navItem = navItem
self.partialNavHost = NavHost(self)
self.partialNavHost.registerNavItem(navItem)
for _navItem in navItemChildren:
self.partialNavHost.registerNavItem(_navItem)
self.partialNavHost.navigate(navItem.id)
self.verticalLayout = QVBoxLayout(self)
self.navItemLabelFont = QFont(self.font())
self.navItemLabelFont.setPointSize(14)
self.navItemLabel = QLabel(self)
self.navItemLabel.setFont(self.navItemLabelFont)
spacer = QSpacerItem(
20, 20, QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Expanding
)
self.verticalLayout.addSpacerItem(spacer)
self.navSideBar = NavigationSideBar(self, self.partialNavHost)
self.navSideBar.navigateUpButton.setEnabled(False)
self.verticalLayout.addWidget(self.navSideBar)
self.verticalLayout.addSpacerItem(spacer)
self.retranslateUi()
def retranslateUi(self):
self.navItemLabel.setText(self.navItem.text())
class NavItemWidgets(QObject):
def __init__(self, parent=None):
super().__init__(parent)
self.__map: dict[str, QWidget] = {}
# this reference holds all the `DefaultParentNavItemWidget`s
# since these widgets are created with no parents, not keeping a reference to
# them may result in errors, even a silent app crash that is hard to debug
self.__defaultParentWidgetRefs: dict[str, QWidget] = {}
def register(self, navItemId: str, widget: QWidget):
self.__map[navItemId] = widget
def unregister(self, navItemId: str) -> bool:
try:
widget = self.__map.pop(navItemId)
widget.deleteLater()
return True
except KeyError:
return False
def get(self, navItemId: str) -> QWidget | None:
widget = self.__map.get(navItemId)
if widget is not None:
return widget
elif navItemChildren := navHost.getNavItemRelatives(navItemId).children:
if self.__defaultParentWidgetRefs.get(navItemId) is None:
defaultParentNavItemWidget = DefaultParentNavItemWidget(
navHost.findNavItem(navItemId), navItemChildren
)
self.__defaultParentWidgetRefs[navItemId] = defaultParentNavItemWidget
defaultParentNavItemWidget.partialNavHost.activated.connect(
lambda o, n: navHost.navigate(n.id)
)
return self.__defaultParentWidgetRefs.get(navItemId)
else:
return None

190
ui/navigation/navsidebar.py Normal file
View File

@ -0,0 +1,190 @@
from PySide6.QtCore import QModelIndex, Qt, Signal, Slot
from PySide6.QtGui import QFont, QIcon, QKeySequence, QShortcut
from PySide6.QtWidgets import (
QListWidget,
QListWidgetItem,
QPushButton,
QVBoxLayout,
QWidget,
)
from ui.navigation.navhost import NavHost, NavItem, navHost
from ui.widgets.slidingstackedwidget import SlidingStackedWidget
class NavItemListWidget(QListWidget):
NavItemRole = Qt.ItemDataRole.UserRole
def __init__(self, parent=None):
super().__init__(parent)
font = QFont(self.font())
font.setPointSize(14)
self.setFont(font)
self.clicked.connect(self.activated)
def setNavItems(self, items: list[NavItem]):
self.clear()
for navItem in items:
if navItem.icon:
listWidgetItem = QListWidgetItem(QIcon(navItem.icon), navItem.text())
else:
listWidgetItem = QListWidgetItem(navItem.text())
listWidgetItem.setData(self.NavItemRole, navItem)
listWidgetItem.setTextAlignment(
Qt.AlignmentFlag.AlignLeading | Qt.AlignmentFlag.AlignVCenter
)
self.addItem(listWidgetItem)
def selectNavItem(self, navItemId: str):
navItemIds = [
self.item(r).data(self.NavItemRole).id for r in range(self.count())
]
index = navItemIds.index(navItemId)
self.setCurrentIndex(self.model().index(index, 0))
class NavigationWidget(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.navHost = navHost
self.verticalLayout = QVBoxLayout(self)
self.verticalLayout.setContentsMargins(0, 0, 0, 0)
self.backButton = QPushButton(QIcon(":/icons/back.svg"), "")
self.backButton.setFlat(True)
self.backButton.setFixedHeight(20)
self.verticalLayout.addWidget(self.backButton)
self.navListWidget = NavItemListWidget(self)
self.verticalLayout.addWidget(self.navListWidget)
def setNavigationItems(self, items: list[NavItem]):
self.navListWidget.setNavItems(items)
class NavigationSideBar(QWidget):
navItemActivated = Signal(NavItem)
def __init__(self, parent=None, navHost=navHost):
super().__init__(parent)
self.navHost = None
self.navigateUpKeyboardShortcut = QShortcut(
QKeySequence(Qt.Modifier.ALT | Qt.Key.Key_Left), self, lambda: True
)
self.verticalLayout = QVBoxLayout(self)
self.navigateUpButton = QPushButton(QIcon(":/icons/back.svg"), "")
self.navigateUpButton.setFlat(True)
self.navigateUpButton.setFixedHeight(20)
self.verticalLayout.addWidget(self.navigateUpButton)
self.slidingStackedWidget = SlidingStackedWidget(self)
self.slidingStackedWidget.animationFinished.connect(
self.endChangingNavItemListWidget
)
self.verticalLayout.addWidget(self.slidingStackedWidget)
navItemListWidget = NavItemListWidget(self)
navItemListWidget.activated.connect(self.navItemListWidgetActivatedProxy)
self.slidingStackedWidget.addWidget(navItemListWidget)
self.setNavHost(navHost)
self.reloadNavWidget()
def setNavHost(self, navHost: NavHost):
if self.navHost is not None:
self.navHost.navItemsChanged.disconnect(self.reloadNavWidget)
self.navHost.activated.disconnect(self.navItemChanged)
self.navigateUpKeyboardShortcut.activated.disconnect(
self.navHost.navigateUp
)
self.navigateUpButton.clicked.disconnect(self.navHost.navigateUp)
self.navHost = navHost
self.navHost.navItemsChanged.connect(self.reloadNavWidget)
self.navHost.activated.connect(self.navItemChanged)
self.navigateUpKeyboardShortcut.activated.connect(self.navHost.navigateUp)
self.navigateUpButton.clicked.connect(self.navHost.navigateUp)
@Slot(QModelIndex)
def navItemListWidgetActivatedProxy(self, index: QModelIndex):
self.navHost.navigate(index.data(NavItemListWidget.NavItemRole).id)
self.navItemActivated.emit(index.data(NavItemListWidget.NavItemRole))
def fillNavItemListWidget(
self, currentNavItem: NavItem, listWidget: NavItemListWidget
):
currentNavItemParent = self.navHost.getNavItemRelatives(
currentNavItem.id
).parent
currentNavItems = self.navHost.getNavItemRelatives(
currentNavItemParent.id if currentNavItemParent else ""
)
listWidget.setNavItems(currentNavItems.children)
listWidget.selectNavItem(currentNavItem.id)
def reloadNavWidget(self):
self.fillNavItemListWidget(
self.navHost.currentNavItem, self.slidingStackedWidget.widget(0)
)
self.navItemChanged(self.navHost.currentNavItem, self.navHost.currentNavItem)
@Slot(NavItem, NavItem)
def navItemChanged(self, oldNavItem: NavItem, newNavItem: NavItem):
# update navigateUpButton text
if newNavItemParent := self.navHost.getNavItemRelatives(newNavItem.id).parent:
self.navigateUpButton.setText(newNavItemParent.text())
else:
self.navigateUpButton.setText("Arcaea Offline")
# update navItemListWidget
oldNavItemIdSplitted = self.navHost.getNavItemIdSplitted(oldNavItem.id)
newNavItemIdSplitted = self.navHost.getNavItemIdSplitted(newNavItem.id)
oldNavItemDepth = len(oldNavItemIdSplitted)
newNavItemDepth = len(newNavItemIdSplitted)
if oldNavItemDepth != newNavItemDepth:
# navItem depth changed, replace current NavItemListWidget
newNavItemListWidget = NavItemListWidget(self)
slidingDirection = (
self.slidingStackedWidget.slidingDirection.RightToLeft
if newNavItemDepth > oldNavItemDepth
else self.slidingStackedWidget.slidingDirection.LeftToRight
)
self.fillNavItemListWidget(newNavItem, newNavItemListWidget)
newNavItemListWidget.activated.connect(self.navItemListWidgetActivatedProxy)
self.startChangingNavItemListWidget(newNavItemListWidget, slidingDirection)
def startChangingNavItemListWidget(
self, newNavItemListWidget: NavItemListWidget, slidingDirection
):
newIndex = self.slidingStackedWidget.addWidget(newNavItemListWidget)
[
self.slidingStackedWidget.widget(i).setEnabled(False)
for i in range(self.slidingStackedWidget.count())
]
self.navigateUpButton.setEnabled(False)
self.navigateUpKeyboardShortcut.setEnabled(False)
self.slidingStackedWidget.slideInIdx(newIndex, slidingDirection)
def endChangingNavItemListWidget(self):
oldWidget = self.slidingStackedWidget.widget(0)
self.slidingStackedWidget.removeWidget(oldWidget)
oldWidget.deleteLater()
newWidget = self.slidingStackedWidget.widget(0)
newWidget.setEnabled(True)
self.navigateUpButton.setEnabled(True)
self.navigateUpKeyboardShortcut.setEnabled(True)

View File

@ -1,22 +0,0 @@
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
}
}
}

View File

@ -1,63 +0,0 @@
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 {}
}
}

View File

@ -1,93 +0,0 @@
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

@ -1,25 +0,0 @@
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

@ -1,33 +0,0 @@
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

@ -1,124 +0,0 @@
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

@ -1,13 +0,0 @@
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

@ -1,64 +0,0 @@
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

@ -1,9 +0,0 @@
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

@ -1,56 +0,0 @@
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

@ -1,31 +0,0 @@
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

@ -1,34 +0,0 @@
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

@ -1,13 +0,0 @@
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

@ -1,124 +0,0 @@
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

@ -1,137 +0,0 @@
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

@ -1,69 +0,0 @@
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()
}
}
}

View File

@ -1,62 +0,0 @@
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

@ -1,18 +0,0 @@
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,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="24"
height="24"
viewBox="0 0 24 24"
version="1.1"
id="svg1"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
sodipodi:docname="back.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="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="px"
showgrid="true"
inkscape:zoom="24.40625"
inkscape:cx="15.323944"
inkscape:cy="10.632522"
inkscape:current-layer="layer1">
<inkscape:grid
id="grid4"
units="px"
originx="12"
originy="12"
spacingx="1"
spacingy="1"
empcolor="#0099e5"
empopacity="0.30196078"
color="#0099e5"
opacity="0.14901961"
empspacing="5"
dotted="false"
gridanglex="30"
gridanglez="30"
visible="true" />
</sodipodi:namedview>
<defs
id="defs1" />
<g
inkscape:label="图层 1"
inkscape:groupmode="layer"
id="layer1">
<path
style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2;stroke-dasharray:none;stroke-opacity:1"
d="M 17,2 7,12 17,22"
id="path2" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -1,126 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -1,126 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -1,138 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,138 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -1,133 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -1,133 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 4.8 KiB

View File

@ -1,123 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 4.6 KiB

View File

@ -1,123 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -1,136 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 4.4 KiB

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