v2.11.1: 통계 차트(frozen) 수정 + 통계/도움말/도전과제 테마 대응
- fix: main.exe에서 통계 차트 안 뜨던 문제 (backend_qt5agg→backend_qtagg 우선 import + spec 보강 + 실패 로깅) - fix: 통계/도움말/도전과제 + 차트가 라이트 테마에서도 다크 고정 → 현재 테마(ThemeColors) 추종 - dark_components/chart_widget를 테마 인식형으로 리팩터 (등급 카드·차트 막대 등 강조색은 유지) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
130c61ea62
commit
e7e85dcf7b
11
CHANGELOG.md
11
CHANGELOG.md
@ -4,6 +4,17 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
|
|
||||||
|
## [2.11.1] — 2026-06-04
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **빌드(main.exe)에서 통계 차트가 표시되지 않던 문제** — frozen 빌드는 PyInstaller가
|
||||||
|
matplotlib `QtAgg`(backend_qtagg)만 번들하는데 `chart_widget`이 `backend_qt5agg`를
|
||||||
|
import해 실패 → "matplotlib 필요" 폴백만 보였음. **backend_qtagg 우선 import**(+ qt5agg
|
||||||
|
폴백) + 실패 원인 로깅, `main.spec`에 `backend_qtagg`/`PyQt5.sip` 명시.
|
||||||
|
- **통계·도움말·도전과제 화면이 라이트 테마에서도 다크로 고정되던 문제** — `dark_components`와
|
||||||
|
세 화면(+통계 차트 배경/그리드/텍스트)을 현재 테마(`ThemeColors`)에 따르도록 변경.
|
||||||
|
다크 기본값은 그대로, 라이트 전환 시 함께 라이트로. 다크 등급 카드/차트 막대 등 강조색은 유지.
|
||||||
|
|
||||||
## [2.11.0] — 2026-06-04
|
## [2.11.0] — 2026-06-04
|
||||||
|
|
||||||
### Changed — UI 전면 다크 리디자인
|
### Changed — UI 전면 다크 리디자인
|
||||||
|
|||||||
@ -4,4 +4,4 @@
|
|||||||
릴리스 시 이 값을 올린 후 git tag → push.
|
릴리스 시 이 값을 올린 후 git tag → push.
|
||||||
CHANGELOG.md의 최상단 항목과 일치시킬 것.
|
CHANGELOG.md의 최상단 항목과 일치시킬 것.
|
||||||
"""
|
"""
|
||||||
__version__ = '2.11.0'
|
__version__ = '2.11.1'
|
||||||
|
|||||||
@ -32,8 +32,10 @@ a = Analysis(
|
|||||||
hiddenimports=[
|
hiddenimports=[
|
||||||
'holidays', 'holidays.countries.south_korea',
|
'holidays', 'holidays.countries.south_korea',
|
||||||
'win32evtlog', 'win32evtlogutil',
|
'win32evtlog', 'win32evtlogutil',
|
||||||
|
'matplotlib.backends.backend_qtagg', # frozen 차트 백엔드 (chart_widget 우선 import)
|
||||||
'matplotlib.backends.backend_qt5agg',
|
'matplotlib.backends.backend_qt5agg',
|
||||||
'PyQt5.QtSvg',
|
'PyQt5.QtSvg',
|
||||||
|
'PyQt5.sip', # matplotlib qt_compat가 sip 사용
|
||||||
],
|
],
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
hooksconfig={},
|
hooksconfig={},
|
||||||
|
|||||||
@ -17,6 +17,7 @@ from PyQt5.QtGui import QFont
|
|||||||
|
|
||||||
from core.achievements import get_all_with_status, get_stats
|
from core.achievements import get_all_with_status, get_stats
|
||||||
from ui.styles import apply_dark_titlebar
|
from ui.styles import apply_dark_titlebar
|
||||||
|
from ui.dark_components import tc, tabs_qss, button_qss, scroll_qss, ACCENT_GOLD, _is_dark
|
||||||
|
|
||||||
|
|
||||||
# 등급별 색상 팔레트 (배경 그라디언트, 외곽선, 강조)
|
# 등급별 색상 팔레트 (배경 그라디언트, 외곽선, 강조)
|
||||||
@ -89,9 +90,9 @@ class AchievementsView(QDialog):
|
|||||||
self.setMinimumSize(960, 720)
|
self.setMinimumSize(960, 720)
|
||||||
self.resize(1100, 800)
|
self.resize(1100, 800)
|
||||||
self._increment_view_count()
|
self._increment_view_count()
|
||||||
self.setStyleSheet("QDialog { background: #1A1B1E; }")
|
self.setStyleSheet(f"QDialog {{ background: {tc('bg')}; }}")
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
apply_dark_titlebar(self, dark=True)
|
apply_dark_titlebar(self) # 현재 테마에 맞춰
|
||||||
|
|
||||||
def _increment_view_count(self) -> None:
|
def _increment_view_count(self) -> None:
|
||||||
try:
|
try:
|
||||||
@ -136,14 +137,7 @@ class AchievementsView(QDialog):
|
|||||||
btn_row.addStretch()
|
btn_row.addStretch()
|
||||||
close_btn = QPushButton("닫기")
|
close_btn = QPushButton("닫기")
|
||||||
close_btn.setMinimumWidth(100)
|
close_btn.setMinimumWidth(100)
|
||||||
close_btn.setStyleSheet("""
|
close_btn.setStyleSheet(button_qss('default'))
|
||||||
QPushButton {
|
|
||||||
background: #2a2a36; color: #e0e0e8;
|
|
||||||
border: 1px solid #44446a; border-radius: 6px;
|
|
||||||
padding: 8px 20px; font-size: 10pt;
|
|
||||||
}
|
|
||||||
QPushButton:hover { background: #3a3a4a; border-color: #6b9eff; }
|
|
||||||
""")
|
|
||||||
close_btn.clicked.connect(self.accept)
|
close_btn.clicked.connect(self.accept)
|
||||||
btn_row.addWidget(close_btn)
|
btn_row.addWidget(close_btn)
|
||||||
layout.addLayout(btn_row)
|
layout.addLayout(btn_row)
|
||||||
@ -153,14 +147,13 @@ class AchievementsView(QDialog):
|
|||||||
# ----- 헤더 -----
|
# ----- 헤더 -----
|
||||||
def _build_header(self, stats: dict) -> QWidget:
|
def _build_header(self, stats: dict) -> QWidget:
|
||||||
container = QFrame()
|
container = QFrame()
|
||||||
container.setStyleSheet("""
|
container.setStyleSheet(f"""
|
||||||
QFrame {
|
QFrame {{
|
||||||
background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
|
background: {tc('panel')};
|
||||||
stop:0 #1a1a30, stop:1 #2a1a3a);
|
border: 1px solid {tc('border')};
|
||||||
border: 1px solid #2C2E33;
|
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
}
|
}}
|
||||||
QLabel { background: transparent; border: none; color: #e8e8f4; }
|
QLabel {{ background: transparent; border: none; color: {tc('text')}; }}
|
||||||
""")
|
""")
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
layout.setContentsMargins(20, 16, 20, 16)
|
layout.setContentsMargins(20, 16, 20, 16)
|
||||||
@ -173,21 +166,21 @@ class AchievementsView(QDialog):
|
|||||||
num_row.setSpacing(24)
|
num_row.setSpacing(24)
|
||||||
|
|
||||||
big = QLabel(f"<span style='font-size: 32pt; font-weight: bold; color: #ffd24a;'>{stats['earned']}</span>"
|
big = QLabel(f"<span style='font-size: 32pt; font-weight: bold; color: #ffd24a;'>{stats['earned']}</span>"
|
||||||
f"<span style='font-size: 18pt; color: #909296;'> / {stats['total']}</span>")
|
f"<span style='font-size: 18pt; color: {tc('text_dim')};'> / {stats['total']}</span>")
|
||||||
big.setTextFormat(Qt.RichText)
|
big.setTextFormat(Qt.RichText)
|
||||||
num_row.addWidget(big)
|
num_row.addWidget(big)
|
||||||
|
|
||||||
spacer = QFrame()
|
spacer = QFrame()
|
||||||
spacer.setFrameShape(QFrame.VLine)
|
spacer.setFrameShape(QFrame.VLine)
|
||||||
spacer.setStyleSheet("color: #2C2E33;")
|
spacer.setStyleSheet(f"color: {tc('border')};")
|
||||||
num_row.addWidget(spacer)
|
num_row.addWidget(spacer)
|
||||||
|
|
||||||
secret_lbl = QLabel(
|
secret_lbl = QLabel(
|
||||||
f"<div style='line-height: 1.3;'>"
|
f"<div style='line-height: 1.3;'>"
|
||||||
f"<span style='font-size: 9pt; color: #909296;'>🌑 시크릿</span><br>"
|
f"<span style='font-size: 9pt; color: {tc('text_dim')};'>🌑 시크릿</span><br>"
|
||||||
f"<span style='font-size: 18pt; font-weight: bold; color: #ff90b8;'>"
|
f"<span style='font-size: 18pt; font-weight: bold; color: #ff90b8;'>"
|
||||||
f"{stats['secret_earned']}</span>"
|
f"{stats['secret_earned']}</span>"
|
||||||
f"<span style='font-size: 12pt; color: #909296;'> / {stats['secret_total']}</span>"
|
f"<span style='font-size: 12pt; color: {tc('text_dim')};'> / {stats['secret_total']}</span>"
|
||||||
f"</div>"
|
f"</div>"
|
||||||
)
|
)
|
||||||
secret_lbl.setTextFormat(Qt.RichText)
|
secret_lbl.setTextFormat(Qt.RichText)
|
||||||
@ -197,7 +190,7 @@ class AchievementsView(QDialog):
|
|||||||
|
|
||||||
pct_lbl = QLabel(
|
pct_lbl = QLabel(
|
||||||
f"<div style='text-align: right; line-height: 1.3;'>"
|
f"<div style='text-align: right; line-height: 1.3;'>"
|
||||||
f"<span style='font-size: 9pt; color: #909296;'>달성률</span><br>"
|
f"<span style='font-size: 9pt; color: {tc('text_dim')};'>달성률</span><br>"
|
||||||
f"<span style='font-size: 24pt; font-weight: bold; color: #4adef0;'>"
|
f"<span style='font-size: 24pt; font-weight: bold; color: #4adef0;'>"
|
||||||
f"{pct:.1f}%</span></div>"
|
f"{pct:.1f}%</span></div>"
|
||||||
)
|
)
|
||||||
@ -235,17 +228,7 @@ class AchievementsView(QDialog):
|
|||||||
def _build_grid_tab(self, items: list, secret_mode: bool = False) -> QWidget:
|
def _build_grid_tab(self, items: list, secret_mode: bool = False) -> QWidget:
|
||||||
scroll = QScrollArea()
|
scroll = QScrollArea()
|
||||||
scroll.setWidgetResizable(True)
|
scroll.setWidgetResizable(True)
|
||||||
scroll.setStyleSheet("""
|
scroll.setStyleSheet(scroll_qss())
|
||||||
QScrollArea { background: transparent; border: none; }
|
|
||||||
QScrollBar:vertical {
|
|
||||||
background: #1a1a24; width: 10px; border-radius: 5px;
|
|
||||||
}
|
|
||||||
QScrollBar::handle:vertical {
|
|
||||||
background: #44446a; border-radius: 5px; min-height: 30px;
|
|
||||||
}
|
|
||||||
QScrollBar::handle:vertical:hover { background: #6b9eff; }
|
|
||||||
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }
|
|
||||||
""")
|
|
||||||
container = QWidget()
|
container = QWidget()
|
||||||
container.setStyleSheet("background: transparent;")
|
container.setStyleSheet("background: transparent;")
|
||||||
grid = QGridLayout()
|
grid = QGridLayout()
|
||||||
@ -256,7 +239,7 @@ class AchievementsView(QDialog):
|
|||||||
empty = QLabel("(아직 없음)")
|
empty = QLabel("(아직 없음)")
|
||||||
empty.setAlignment(Qt.AlignCenter)
|
empty.setAlignment(Qt.AlignCenter)
|
||||||
empty.setStyleSheet(
|
empty.setStyleSheet(
|
||||||
"color: #6C6E73; padding: 60px; font-size: 12pt; background: transparent;"
|
f"color: {tc('text_faint')}; padding: 60px; font-size: 12pt; background: transparent;"
|
||||||
)
|
)
|
||||||
grid.addWidget(empty, 0, 0)
|
grid.addWidget(empty, 0, 0)
|
||||||
else:
|
else:
|
||||||
@ -279,11 +262,18 @@ class AchievementsView(QDialog):
|
|||||||
tier = item['tier'] or 'bronze'
|
tier = item['tier'] or 'bronze'
|
||||||
theme = TIER_THEMES.get(tier, TIER_THEMES['bronze'])
|
theme = TIER_THEMES.get(tier, TIER_THEMES['bronze'])
|
||||||
|
|
||||||
# 시크릿 미발견은 회색 톤으로
|
# 라이트 테마: 카드 배경을 패널색으로(등급색은 보더/강조로 유지), 다크: 등급 그라디언트
|
||||||
|
light = not _is_dark()
|
||||||
if is_locked_secret:
|
if is_locked_secret:
|
||||||
bg_top, bg_bot = '#1a1a26', '#0e0e16'
|
if light:
|
||||||
border = '#3a3a4a'
|
bg_top = bg_bot = tc('panel'); border = tc('border')
|
||||||
text_color = '#6C6E73'
|
else:
|
||||||
|
bg_top, bg_bot = '#1a1a26', '#0e0e16'; border = '#3a3a4a'
|
||||||
|
text_color = tc('text_faint')
|
||||||
|
elif light:
|
||||||
|
bg_top = bg_bot = tc('panel')
|
||||||
|
border = theme['border_strong'] if is_earned else theme['border']
|
||||||
|
text_color = tc('text') if is_earned else tc('text_dim')
|
||||||
else:
|
else:
|
||||||
bg_top = theme['bg_top']
|
bg_top = theme['bg_top']
|
||||||
bg_bot = theme['bg_bot']
|
bg_bot = theme['bg_bot']
|
||||||
@ -342,7 +332,7 @@ class AchievementsView(QDialog):
|
|||||||
name = QLabel(name_text)
|
name = QLabel(name_text)
|
||||||
name.setStyleSheet(
|
name.setStyleSheet(
|
||||||
f"font-size: 12pt; font-weight: bold; "
|
f"font-size: 12pt; font-weight: bold; "
|
||||||
f"color: {'#ffffff' if is_earned else '#d0d0e0'}; "
|
f"color: {tc('text') if is_earned else tc('text_dim')}; "
|
||||||
f"background: transparent; border: none;"
|
f"background: transparent; border: none;"
|
||||||
)
|
)
|
||||||
name.setWordWrap(True)
|
name.setWordWrap(True)
|
||||||
@ -378,7 +368,7 @@ class AchievementsView(QDialog):
|
|||||||
desc = QLabel(desc_text)
|
desc = QLabel(desc_text)
|
||||||
desc.setWordWrap(True)
|
desc.setWordWrap(True)
|
||||||
desc.setStyleSheet(
|
desc.setStyleSheet(
|
||||||
f"color: #a0a0b8; font-size: 9.5pt; "
|
f"color: {tc('text_dim')}; font-size: 9.5pt; "
|
||||||
f"background: transparent; border: none; padding: 0;"
|
f"background: transparent; border: none; padding: 0;"
|
||||||
)
|
)
|
||||||
outer.addWidget(desc)
|
outer.addWidget(desc)
|
||||||
@ -453,32 +443,5 @@ class AchievementsView(QDialog):
|
|||||||
|
|
||||||
# ----- 탭 QSS (다이얼로그 전용) -----
|
# ----- 탭 QSS (다이얼로그 전용) -----
|
||||||
def _tabs_qss(self) -> str:
|
def _tabs_qss(self) -> str:
|
||||||
return """
|
# 공통 테마 인식형 탭 스타일 (도전과제는 골드 강조 유지)
|
||||||
QTabWidget::pane {
|
return tabs_qss(ACCENT_GOLD)
|
||||||
background: #25262B;
|
|
||||||
border: 1px solid #2a2a3a;
|
|
||||||
border-radius: 10px;
|
|
||||||
top: -1px;
|
|
||||||
}
|
|
||||||
QTabBar::tab {
|
|
||||||
background: #1c1c28;
|
|
||||||
color: #a0a0b8;
|
|
||||||
padding: 9px 18px;
|
|
||||||
border: 1px solid #2a2a3a;
|
|
||||||
border-bottom: none;
|
|
||||||
border-top-left-radius: 8px;
|
|
||||||
border-top-right-radius: 8px;
|
|
||||||
margin-right: 3px;
|
|
||||||
font-size: 10pt;
|
|
||||||
}
|
|
||||||
QTabBar::tab:selected {
|
|
||||||
background: #25262B;
|
|
||||||
color: #ffd24a;
|
|
||||||
font-weight: bold;
|
|
||||||
border-bottom: 2px solid #ffd24a;
|
|
||||||
}
|
|
||||||
QTabBar::tab:hover:!selected {
|
|
||||||
background: #2a2a36;
|
|
||||||
color: #e0e0e8;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
|
|||||||
@ -10,17 +10,30 @@ from PyQt5.QtWidgets import QWidget, QVBoxLayout, QLabel
|
|||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from matplotlib.figure import Figure
|
|
||||||
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
|
||||||
import matplotlib
|
import matplotlib
|
||||||
matplotlib.rcParams['font.family'] = ['Malgun Gothic', 'Apple SD Gothic Neo', 'sans-serif']
|
from matplotlib.figure import Figure
|
||||||
|
# frozen(main.exe) 빌드는 PyInstaller matplotlib hook이 'QtAgg'(backend_qtagg)만
|
||||||
|
# 번들함 → backend_qt5agg import가 실패해 차트가 안 뜨던 문제.
|
||||||
|
# 번들된 backend_qtagg를 우선 사용하고, 구버전(dev) 호환으로 qt5agg 폴백.
|
||||||
|
try:
|
||||||
|
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg as FigureCanvas
|
||||||
|
except Exception:
|
||||||
|
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
|
||||||
|
matplotlib.rcParams['font.family'] = ['NanumSquare', 'Malgun Gothic', 'Apple SD Gothic Neo', 'sans-serif']
|
||||||
matplotlib.rcParams['axes.unicode_minus'] = False
|
matplotlib.rcParams['axes.unicode_minus'] = False
|
||||||
_MPL = True
|
_MPL = True
|
||||||
except ImportError:
|
except Exception as _mpl_err:
|
||||||
|
# ImportError 외 backend/sip 로딩 오류도 폴백 처리 + 실제 원인 기록(진단용)
|
||||||
_MPL = False
|
_MPL = False
|
||||||
|
try:
|
||||||
|
from utils.debug_log import dlog
|
||||||
|
dlog(f"chart_widget: matplotlib unavailable: {type(_mpl_err).__name__}: {_mpl_err}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# 다크 테마 색상 (dark_components / styles.py 톤과 일치)
|
# 차트 색상 — 배경/그리드/텍스트는 현재 테마를 따름(_refresh_chart_colors),
|
||||||
|
# 막대/선은 데이터 구분용 고정 색.
|
||||||
_CHART_BG = '#25262B'
|
_CHART_BG = '#25262B'
|
||||||
_CHART_GRID = '#2C2E33'
|
_CHART_GRID = '#2C2E33'
|
||||||
_CHART_TEXT = '#909296'
|
_CHART_TEXT = '#909296'
|
||||||
@ -30,6 +43,18 @@ _CHART_BAR_WEEKEND = '#fcd34d' # gold (데이터 구분용)
|
|||||||
_CHART_AVG_LINE = '#51CF66' # green
|
_CHART_AVG_LINE = '#51CF66' # green
|
||||||
|
|
||||||
|
|
||||||
|
def _refresh_chart_colors() -> None:
|
||||||
|
"""배경/그리드/텍스트 색을 현재 앱 테마로 갱신 (라이트/다크 추종)."""
|
||||||
|
global _CHART_BG, _CHART_GRID, _CHART_TEXT
|
||||||
|
try:
|
||||||
|
from ui.styles import ThemeColors
|
||||||
|
_CHART_BG = ThemeColors.get('bg_secondary')
|
||||||
|
_CHART_GRID = ThemeColors.get('border_subtle')
|
||||||
|
_CHART_TEXT = ThemeColors.get('text_secondary')
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _apply_dark_axes(ax) -> None:
|
def _apply_dark_axes(ax) -> None:
|
||||||
"""차트 ax에 다크 테마 적용 — 텍스트, 그리드, spines, 배경."""
|
"""차트 ax에 다크 테마 적용 — 텍스트, 그리드, spines, 배경."""
|
||||||
ax.set_facecolor(_CHART_BG)
|
ax.set_facecolor(_CHART_BG)
|
||||||
@ -43,7 +68,8 @@ def _apply_dark_axes(ax) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def _apply_dark_figure(fig) -> None:
|
def _apply_dark_figure(fig) -> None:
|
||||||
"""figure 배경을 다크 톤으로."""
|
"""figure 배경을 현재 테마 톤으로 (모든 draw_* 진입점에서 호출됨)."""
|
||||||
|
_refresh_chart_colors()
|
||||||
fig.patch.set_facecolor(_CHART_BG)
|
fig.patch.set_facecolor(_CHART_BG)
|
||||||
|
|
||||||
|
|
||||||
@ -64,6 +90,7 @@ def make_chart_widget(parent=None) -> QWidget:
|
|||||||
"""차트가 그려질 빈 캔버스 위젯. matplotlib 없으면 fallback."""
|
"""차트가 그려질 빈 캔버스 위젯. matplotlib 없으면 fallback."""
|
||||||
if not _MPL:
|
if not _MPL:
|
||||||
return _Fallback("차트 표시에는 matplotlib가 필요합니다.\npip install matplotlib")
|
return _Fallback("차트 표시에는 matplotlib가 필요합니다.\npip install matplotlib")
|
||||||
|
_refresh_chart_colors()
|
||||||
widget = QWidget(parent)
|
widget = QWidget(parent)
|
||||||
widget.setStyleSheet(f"background: {_CHART_BG}; border-radius: 8px;")
|
widget.setStyleSheet(f"background: {_CHART_BG}; border-radius: 8px;")
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
|
|||||||
@ -78,26 +78,59 @@ CARD_THEMES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ── 테마 연동 ──────────────────────────────────────────────────
|
||||||
|
# 통계/도움말/도전과제 다이얼로그는 열 때마다 새로 생성되므로, 빌드 시점에 현재
|
||||||
|
# 앱 테마(ThemeColors)를 읽으면 라이트/다크를 자동으로 따른다.
|
||||||
|
|
||||||
|
def _pal() -> dict:
|
||||||
|
"""현재 앱 테마 팔레트를 dark_components 역할명으로 매핑."""
|
||||||
|
from ui.styles import ThemeColors
|
||||||
|
g = ThemeColors.get
|
||||||
|
return {
|
||||||
|
'bg': g('bg_primary'), 'panel': g('bg_secondary'), 'panel2': g('bg_tertiary'),
|
||||||
|
'border': g('border_subtle'), 'border_strong': g('border_default'),
|
||||||
|
'text': g('text_primary'), 'text_dim': g('text_secondary'),
|
||||||
|
'text_faint': g('text_tertiary'),
|
||||||
|
'blue': g('accent_primary'), 'green': g('accent_success'),
|
||||||
|
'red': g('accent_danger'),
|
||||||
|
'blue_hover': g('accent_primary_hover'), 'blue_pressed': g('accent_primary_pressed'),
|
||||||
|
'green_hover': g('accent_success_hover'), 'red_hover': g('accent_danger_hover'),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_dark() -> bool:
|
||||||
|
from ui.styles import ThemeColors, DARK_COLORS
|
||||||
|
return ThemeColors.current is DARK_COLORS
|
||||||
|
|
||||||
|
|
||||||
|
def tc(role: str) -> str:
|
||||||
|
"""뷰에서 단일 색을 테마 인식형으로 가져올 때 사용 (예: tc('text'))."""
|
||||||
|
return _pal().get(role, '#FF00FF')
|
||||||
|
|
||||||
|
|
||||||
# ── QSS 헬퍼 ───────────────────────────────────────────────────
|
# ── QSS 헬퍼 ───────────────────────────────────────────────────
|
||||||
|
|
||||||
def dialog_qss() -> str:
|
def dialog_qss() -> str:
|
||||||
"""다이얼로그 전체 배경."""
|
"""다이얼로그 전체 배경 (현재 테마)."""
|
||||||
return f"QDialog {{ background: {DARK_BG}; }}"
|
return f"QDialog {{ background: {_pal()['bg']}; }}"
|
||||||
|
|
||||||
|
|
||||||
def tabs_qss(accent: str = ACCENT_BLUE) -> str:
|
def tabs_qss(accent: str = None) -> str:
|
||||||
|
p = _pal()
|
||||||
|
if accent is None:
|
||||||
|
accent = p['blue']
|
||||||
return f"""
|
return f"""
|
||||||
QTabWidget::pane {{
|
QTabWidget::pane {{
|
||||||
background: {DARK_PANEL};
|
background: {p['panel']};
|
||||||
border: 1px solid {DARK_BORDER};
|
border: 1px solid {p['border']};
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
}}
|
}}
|
||||||
QTabBar::tab {{
|
QTabBar::tab {{
|
||||||
background: {DARK_PANEL_2};
|
background: {p['panel2']};
|
||||||
color: {DARK_TEXT_DIM};
|
color: {p['text_dim']};
|
||||||
padding: 9px 18px;
|
padding: 9px 18px;
|
||||||
border: 1px solid {DARK_BORDER};
|
border: 1px solid {p['border']};
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
border-top-left-radius: 8px;
|
border-top-left-radius: 8px;
|
||||||
border-top-right-radius: 8px;
|
border-top-right-radius: 8px;
|
||||||
@ -105,88 +138,90 @@ def tabs_qss(accent: str = ACCENT_BLUE) -> str:
|
|||||||
font-size: 10pt;
|
font-size: 10pt;
|
||||||
}}
|
}}
|
||||||
QTabBar::tab:selected {{
|
QTabBar::tab:selected {{
|
||||||
background: {DARK_PANEL};
|
background: {p['panel']};
|
||||||
color: {accent};
|
color: {accent};
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
border-bottom: 2px solid {accent};
|
border-bottom: 2px solid {accent};
|
||||||
}}
|
}}
|
||||||
QTabBar::tab:hover:!selected {{
|
QTabBar::tab:hover:!selected {{
|
||||||
background: #2a2a36;
|
background: {p['border_strong']};
|
||||||
color: {DARK_TEXT};
|
color: {p['text']};
|
||||||
}}
|
}}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def scroll_qss() -> str:
|
def scroll_qss() -> str:
|
||||||
|
p = _pal()
|
||||||
return f"""
|
return f"""
|
||||||
QScrollArea {{ background: transparent; border: none; }}
|
QScrollArea {{ background: transparent; border: none; }}
|
||||||
QScrollBar:vertical {{
|
QScrollBar:vertical {{
|
||||||
background: {DARK_PANEL_2}; width: 10px; border-radius: 5px;
|
background: {p['panel2']}; width: 10px; border-radius: 5px;
|
||||||
}}
|
}}
|
||||||
QScrollBar::handle:vertical {{
|
QScrollBar::handle:vertical {{
|
||||||
background: {DARK_BORDER_STRONG}; border-radius: 5px; min-height: 30px;
|
background: {p['border_strong']}; border-radius: 5px; min-height: 30px;
|
||||||
}}
|
}}
|
||||||
QScrollBar::handle:vertical:hover {{ background: {ACCENT_BLUE}; }}
|
QScrollBar::handle:vertical:hover {{ background: {p['blue']}; }}
|
||||||
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height: 0; }}
|
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height: 0; }}
|
||||||
QScrollBar:horizontal {{
|
QScrollBar:horizontal {{
|
||||||
background: {DARK_PANEL_2}; height: 10px; border-radius: 5px;
|
background: {p['panel2']}; height: 10px; border-radius: 5px;
|
||||||
}}
|
}}
|
||||||
QScrollBar::handle:horizontal {{
|
QScrollBar::handle:horizontal {{
|
||||||
background: {DARK_BORDER_STRONG}; border-radius: 5px; min-width: 30px;
|
background: {p['border_strong']}; border-radius: 5px; min-width: 30px;
|
||||||
}}
|
}}
|
||||||
QScrollBar::handle:horizontal:hover {{ background: {ACCENT_BLUE}; }}
|
QScrollBar::handle:horizontal:hover {{ background: {p['blue']}; }}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def button_qss(variant: str = 'default') -> str:
|
def button_qss(variant: str = 'default') -> str:
|
||||||
""" variant: default | primary | success | danger | ghost """
|
""" variant: default | primary | success | danger | ghost (현재 테마) """
|
||||||
|
p = _pal()
|
||||||
if variant == 'primary':
|
if variant == 'primary':
|
||||||
return f"""
|
return f"""
|
||||||
QPushButton {{
|
QPushButton {{
|
||||||
background: {ACCENT_BLUE}; color: white;
|
background: {p['blue']}; color: white;
|
||||||
border: none; border-radius: 8px;
|
border: none; border-radius: 8px;
|
||||||
padding: 8px 18px; font-weight: bold; font-size: 10pt;
|
padding: 8px 18px; font-weight: bold; font-size: 10pt;
|
||||||
}}
|
}}
|
||||||
QPushButton:hover {{ background: #69B6F8; }}
|
QPushButton:hover {{ background: {p['blue_hover']}; }}
|
||||||
QPushButton:pressed {{ background: #3D97E0; }}
|
QPushButton:pressed {{ background: {p['blue_pressed']}; }}
|
||||||
QPushButton:disabled {{ background: {DARK_PANEL_2}; color: {DARK_TEXT_FAINT}; }}
|
QPushButton:disabled {{ background: {p['panel2']}; color: {p['text_faint']}; }}
|
||||||
"""
|
"""
|
||||||
if variant == 'success':
|
if variant == 'success':
|
||||||
return f"""
|
return f"""
|
||||||
QPushButton {{
|
QPushButton {{
|
||||||
background: {ACCENT_GREEN}; color: #0e2a1a;
|
background: {p['green']}; color: white;
|
||||||
border: none; border-radius: 8px;
|
border: none; border-radius: 8px;
|
||||||
padding: 8px 18px; font-weight: bold; font-size: 10pt;
|
padding: 8px 18px; font-weight: bold; font-size: 10pt;
|
||||||
}}
|
}}
|
||||||
QPushButton:hover {{ background: #69DB7C; }}
|
QPushButton:hover {{ background: {p['green_hover']}; }}
|
||||||
"""
|
"""
|
||||||
if variant == 'danger':
|
if variant == 'danger':
|
||||||
return f"""
|
return f"""
|
||||||
QPushButton {{
|
QPushButton {{
|
||||||
background: {ACCENT_RED}; color: white;
|
background: {p['red']}; color: white;
|
||||||
border: none; border-radius: 8px;
|
border: none; border-radius: 8px;
|
||||||
padding: 8px 18px; font-weight: bold; font-size: 10pt;
|
padding: 8px 18px; font-weight: bold; font-size: 10pt;
|
||||||
}}
|
}}
|
||||||
QPushButton:hover {{ background: #FF6B6B; }}
|
QPushButton:hover {{ background: {p['red_hover']}; }}
|
||||||
"""
|
"""
|
||||||
if variant == 'ghost':
|
if variant == 'ghost':
|
||||||
return f"""
|
return f"""
|
||||||
QPushButton {{
|
QPushButton {{
|
||||||
background: transparent; color: {DARK_TEXT_DIM};
|
background: transparent; color: {p['text_dim']};
|
||||||
border: 1px solid {DARK_BORDER_STRONG}; border-radius: 8px;
|
border: 1px solid {p['border_strong']}; border-radius: 8px;
|
||||||
padding: 6px 14px; font-size: 9.5pt;
|
padding: 6px 14px; font-size: 9.5pt;
|
||||||
}}
|
}}
|
||||||
QPushButton:hover {{ background: {DARK_PANEL_2}; color: {DARK_TEXT};
|
QPushButton:hover {{ background: {p['panel2']}; color: {p['text']};
|
||||||
border-color: {ACCENT_BLUE}; }}
|
border-color: {p['blue']}; }}
|
||||||
"""
|
"""
|
||||||
# default
|
# default
|
||||||
return f"""
|
return f"""
|
||||||
QPushButton {{
|
QPushButton {{
|
||||||
background: {DARK_PANEL_2}; color: {DARK_TEXT};
|
background: {p['panel2']}; color: {p['text']};
|
||||||
border: 1px solid {DARK_BORDER_STRONG}; border-radius: 8px;
|
border: 1px solid {p['border_strong']}; border-radius: 8px;
|
||||||
padding: 8px 18px; font-size: 10pt;
|
padding: 8px 18px; font-size: 10pt;
|
||||||
}}
|
}}
|
||||||
QPushButton:hover {{ background: {DARK_BORDER_STRONG}; border-color: {ACCENT_BLUE}; }}
|
QPushButton:hover {{ background: {p['border_strong']}; border-color: {p['blue']}; }}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
@ -204,14 +239,15 @@ def build_gradient_header(title: str, big_value: str, subtitle: str = '',
|
|||||||
big_color: 큰 숫자 색
|
big_color: 큰 숫자 색
|
||||||
extra_widgets: 우측에 배치할 위젯 (예: 추가 통계, 토글)
|
extra_widgets: 우측에 배치할 위젯 (예: 추가 통계, 토글)
|
||||||
"""
|
"""
|
||||||
|
p = _pal()
|
||||||
container = QFrame()
|
container = QFrame()
|
||||||
container.setStyleSheet(f"""
|
container.setStyleSheet(f"""
|
||||||
QFrame {{
|
QFrame {{
|
||||||
background: {DARK_PANEL};
|
background: {p['panel']};
|
||||||
border: 1px solid {DARK_BORDER};
|
border: 1px solid {p['border']};
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}}
|
}}
|
||||||
QLabel {{ background: transparent; border: none; color: {DARK_TEXT}; }}
|
QLabel {{ background: transparent; border: none; color: {p['text']}; }}
|
||||||
""")
|
""")
|
||||||
layout = QHBoxLayout()
|
layout = QHBoxLayout()
|
||||||
layout.setContentsMargins(20, 14, 20, 14)
|
layout.setContentsMargins(20, 14, 20, 14)
|
||||||
@ -223,13 +259,13 @@ def build_gradient_header(title: str, big_value: str, subtitle: str = '',
|
|||||||
if title:
|
if title:
|
||||||
t = QLabel(title)
|
t = QLabel(title)
|
||||||
t.setStyleSheet(
|
t.setStyleSheet(
|
||||||
f"font-size: 9pt; color: {DARK_TEXT_DIM}; "
|
f"font-size: 9pt; color: {p['text_dim']}; "
|
||||||
f"background: transparent; border: none;"
|
f"background: transparent; border: none;"
|
||||||
)
|
)
|
||||||
left.addWidget(t)
|
left.addWidget(t)
|
||||||
big = QLabel(
|
big = QLabel(
|
||||||
f"<span style='font-size: 28pt; font-weight: bold; color: {big_color};'>"
|
f"<span style='font-size: 28pt; font-weight: bold; color: {big_color};'>"
|
||||||
f"{big_value}</span>" + (f"<span style='font-size: 14pt; color: {DARK_TEXT_DIM};'>"
|
f"{big_value}</span>" + (f"<span style='font-size: 14pt; color: {p['text_dim']};'>"
|
||||||
f" {subtitle}</span>" if subtitle else '')
|
f" {subtitle}</span>" if subtitle else '')
|
||||||
)
|
)
|
||||||
big.setTextFormat(Qt.RichText)
|
big.setTextFormat(Qt.RichText)
|
||||||
@ -253,16 +289,29 @@ def build_stat_card(title: str, value: str, subtitle: str = '',
|
|||||||
theme: str = 'blue', icon: str = '') -> QFrame:
|
theme: str = 'blue', icon: str = '') -> QFrame:
|
||||||
"""단일 통계 카드 — 제목, 큰 숫자, 부제, 좌측 큰 이모지."""
|
"""단일 통계 카드 — 제목, 큰 숫자, 부제, 좌측 큰 이모지."""
|
||||||
t = CARD_THEMES.get(theme, CARD_THEMES['blue'])
|
t = CARD_THEMES.get(theme, CARD_THEMES['blue'])
|
||||||
|
p = _pal()
|
||||||
|
dark = _is_dark()
|
||||||
|
# 다크: 등급색 그라디언트 카드 / 라이트: 패널 배경 + 가독성 위해 값은 기본 텍스트색
|
||||||
|
if dark:
|
||||||
|
card_bg = (f"qlineargradient(x1:0, y1:0, x2:0, y2:1, "
|
||||||
|
f"stop:0 {t['bg_top']}, stop:1 {t['bg_bot']})")
|
||||||
|
card_border = t['border']
|
||||||
|
label_color = t['text']
|
||||||
|
value_color = t['border_strong']
|
||||||
|
else:
|
||||||
|
card_bg = p['panel']
|
||||||
|
card_border = p['border']
|
||||||
|
label_color = p['text']
|
||||||
|
value_color = p['text']
|
||||||
card = QFrame()
|
card = QFrame()
|
||||||
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
|
||||||
card.setStyleSheet(f"""
|
card.setStyleSheet(f"""
|
||||||
QFrame {{
|
QFrame {{
|
||||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
background: {card_bg};
|
||||||
stop:0 {t['bg_top']}, stop:1 {t['bg_bot']});
|
border: 1px solid {card_border};
|
||||||
border: 1px solid {t['border']};
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}}
|
}}
|
||||||
QLabel {{ background: transparent; border: none; color: {t['text']}; }}
|
QLabel {{ background: transparent; border: none; color: {label_color}; }}
|
||||||
""")
|
""")
|
||||||
outer = QHBoxLayout()
|
outer = QHBoxLayout()
|
||||||
outer.setContentsMargins(16, 12, 16, 12)
|
outer.setContentsMargins(16, 12, 16, 12)
|
||||||
@ -290,13 +339,13 @@ def build_stat_card(title: str, value: str, subtitle: str = '',
|
|||||||
|
|
||||||
title_lbl = QLabel(title)
|
title_lbl = QLabel(title)
|
||||||
title_lbl.setStyleSheet(
|
title_lbl.setStyleSheet(
|
||||||
f"font-size: 9.5pt; color: {DARK_TEXT_DIM}; "
|
f"font-size: 9.5pt; color: {p['text_dim']}; "
|
||||||
f"background: transparent; border: none;"
|
f"background: transparent; border: none;"
|
||||||
)
|
)
|
||||||
text_box.addWidget(title_lbl)
|
text_box.addWidget(title_lbl)
|
||||||
|
|
||||||
val_lbl = QLabel(
|
val_lbl = QLabel(
|
||||||
f"<span style='font-size: 18pt; font-weight: bold; color: {t['border_strong']};'>"
|
f"<span style='font-size: 18pt; font-weight: bold; color: {value_color};'>"
|
||||||
f"{value}</span>"
|
f"{value}</span>"
|
||||||
)
|
)
|
||||||
val_lbl.setTextFormat(Qt.RichText)
|
val_lbl.setTextFormat(Qt.RichText)
|
||||||
@ -306,7 +355,7 @@ def build_stat_card(title: str, value: str, subtitle: str = '',
|
|||||||
if subtitle:
|
if subtitle:
|
||||||
sub_lbl = QLabel(subtitle)
|
sub_lbl = QLabel(subtitle)
|
||||||
sub_lbl.setStyleSheet(
|
sub_lbl.setStyleSheet(
|
||||||
f"font-size: 9pt; color: {DARK_TEXT_DIM}; "
|
f"font-size: 9pt; color: {p['text_dim']}; "
|
||||||
f"background: transparent; border: none;"
|
f"background: transparent; border: none;"
|
||||||
)
|
)
|
||||||
sub_lbl.setWordWrap(True)
|
sub_lbl.setWordWrap(True)
|
||||||
@ -321,16 +370,25 @@ def build_section_card(title: str, content: QWidget,
|
|||||||
theme: str = 'gray', icon: str = '') -> QFrame:
|
theme: str = 'gray', icon: str = '') -> QFrame:
|
||||||
"""제목 + 내용 큰 카드 (세로 레이아웃)."""
|
"""제목 + 내용 큰 카드 (세로 레이아웃)."""
|
||||||
t = CARD_THEMES.get(theme, CARD_THEMES['gray'])
|
t = CARD_THEMES.get(theme, CARD_THEMES['gray'])
|
||||||
|
p = _pal()
|
||||||
|
if _is_dark():
|
||||||
|
card_bg = (f"qlineargradient(x1:0, y1:0, x2:0, y2:1, "
|
||||||
|
f"stop:0 {t['bg_top']}, stop:1 {t['bg_bot']})")
|
||||||
|
card_border = t['border']
|
||||||
|
label_color = t['text']
|
||||||
|
else:
|
||||||
|
card_bg = p['panel']
|
||||||
|
card_border = p['border']
|
||||||
|
label_color = p['text']
|
||||||
card = QFrame()
|
card = QFrame()
|
||||||
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
card.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
card.setStyleSheet(f"""
|
card.setStyleSheet(f"""
|
||||||
QFrame {{
|
QFrame {{
|
||||||
background: qlineargradient(x1:0, y1:0, x2:0, y2:1,
|
background: {card_bg};
|
||||||
stop:0 {t['bg_top']}, stop:1 {t['bg_bot']});
|
border: 1px solid {card_border};
|
||||||
border: 1px solid {t['border']};
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
}}
|
}}
|
||||||
QLabel {{ background: transparent; border: none; color: {t['text']}; }}
|
QLabel {{ background: transparent; border: none; color: {label_color}; }}
|
||||||
""")
|
""")
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
layout.setContentsMargins(16, 12, 16, 14)
|
layout.setContentsMargins(16, 12, 16, 14)
|
||||||
@ -351,7 +409,7 @@ def build_section_card(title: str, content: QWidget,
|
|||||||
head.addWidget(i)
|
head.addWidget(i)
|
||||||
title_lbl = QLabel(title)
|
title_lbl = QLabel(title)
|
||||||
title_lbl.setStyleSheet(
|
title_lbl.setStyleSheet(
|
||||||
f"font-size: 12pt; font-weight: bold; color: {DARK_TEXT}; "
|
f"font-size: 12pt; font-weight: bold; color: {p['text']}; "
|
||||||
f"background: transparent; border: none;"
|
f"background: transparent; border: none;"
|
||||||
)
|
)
|
||||||
head.addWidget(title_lbl)
|
head.addWidget(title_lbl)
|
||||||
@ -385,9 +443,11 @@ def style_progressbar(pb: QProgressBar, theme: str = 'blue',
|
|||||||
|
|
||||||
|
|
||||||
def transparent_label(text: str, size: int = 10, weight: str = 'normal',
|
def transparent_label(text: str, size: int = 10, weight: str = 'normal',
|
||||||
color: str = DARK_TEXT) -> QLabel:
|
color: str = None) -> QLabel:
|
||||||
"""글로벌 QSS와 격리된 다크 라벨 (배경 없음, 외곽선 없음)."""
|
"""글로벌 QSS와 격리된 라벨 (배경 없음, 외곽선 없음). color 미지정 시 현재 테마 텍스트색."""
|
||||||
lbl = QLabel(text)
|
lbl = QLabel(text)
|
||||||
|
if color is None:
|
||||||
|
color = _pal()['text']
|
||||||
weight_str = 'bold' if weight == 'bold' else 'normal'
|
weight_str = 'bold' if weight == 'bold' else 'normal'
|
||||||
lbl.setStyleSheet(
|
lbl.setStyleSheet(
|
||||||
f"font-size: {size}pt; font-weight: {weight_str}; color: {color}; "
|
f"font-size: {size}pt; font-weight: {weight_str}; color: {color}; "
|
||||||
|
|||||||
@ -10,10 +10,7 @@ from PyQt5.QtCore import Qt
|
|||||||
|
|
||||||
from core.i18n import tr, tr_html
|
from core.i18n import tr, tr_html
|
||||||
from ui.styles import apply_dark_titlebar
|
from ui.styles import apply_dark_titlebar
|
||||||
from ui.dark_components import (
|
from ui.dark_components import dialog_qss, tabs_qss, button_qss, tc
|
||||||
dialog_qss, tabs_qss, button_qss,
|
|
||||||
DARK_BG, DARK_PANEL, DARK_BORDER, DARK_TEXT, ACCENT_GOLD,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class HelpView(QDialog):
|
class HelpView(QDialog):
|
||||||
@ -37,7 +34,7 @@ class HelpView(QDialog):
|
|||||||
self.resize(820, 760)
|
self.resize(820, 760)
|
||||||
self.setStyleSheet(dialog_qss())
|
self.setStyleSheet(dialog_qss())
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
apply_dark_titlebar(self, dark=True)
|
apply_dark_titlebar(self) # 현재 테마에 맞춰
|
||||||
|
|
||||||
def init_ui(self):
|
def init_ui(self):
|
||||||
main_layout = QVBoxLayout()
|
main_layout = QVBoxLayout()
|
||||||
@ -47,7 +44,7 @@ class HelpView(QDialog):
|
|||||||
# 다크 타이틀
|
# 다크 타이틀
|
||||||
title = QLabel(tr('window.help'))
|
title = QLabel(tr('window.help'))
|
||||||
title.setStyleSheet(
|
title.setStyleSheet(
|
||||||
f"font-size: 18pt; font-weight: bold; color: {DARK_TEXT}; "
|
f"font-size: 18pt; font-weight: bold; color: {tc('text')}; "
|
||||||
f"background: transparent; border: none; padding: 4px 0;"
|
f"background: transparent; border: none; padding: 4px 0;"
|
||||||
)
|
)
|
||||||
main_layout.addWidget(title)
|
main_layout.addWidget(title)
|
||||||
@ -88,7 +85,7 @@ class HelpView(QDialog):
|
|||||||
|
|
||||||
def _make_tab(self, html: str) -> QWidget:
|
def _make_tab(self, html: str) -> QWidget:
|
||||||
container = QWidget()
|
container = QWidget()
|
||||||
container.setStyleSheet(f"background: {DARK_PANEL};")
|
container.setStyleSheet(f"background: {tc('panel')};")
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
layout.setContentsMargins(0, 0, 0, 0)
|
layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
@ -99,21 +96,21 @@ class HelpView(QDialog):
|
|||||||
browser.setHtml(styled_html)
|
browser.setHtml(styled_html)
|
||||||
browser.setStyleSheet(f"""
|
browser.setStyleSheet(f"""
|
||||||
QTextBrowser {{
|
QTextBrowser {{
|
||||||
background: {DARK_PANEL};
|
background: {tc('panel')};
|
||||||
color: {DARK_TEXT};
|
color: {tc('text')};
|
||||||
border: none;
|
border: none;
|
||||||
padding: 16px 20px;
|
padding: 16px 20px;
|
||||||
font-size: 10.5pt;
|
font-size: 10.5pt;
|
||||||
selection-background-color: {ACCENT_GOLD};
|
selection-background-color: {tc('blue')};
|
||||||
selection-color: #1a1a26;
|
selection-color: #ffffff;
|
||||||
}}
|
}}
|
||||||
QScrollBar:vertical {{
|
QScrollBar:vertical {{
|
||||||
background: {DARK_PANEL}; width: 10px; border-radius: 5px;
|
background: {tc('panel')}; width: 10px; border-radius: 5px;
|
||||||
}}
|
}}
|
||||||
QScrollBar::handle:vertical {{
|
QScrollBar::handle:vertical {{
|
||||||
background: {DARK_BORDER}; border-radius: 5px; min-height: 30px;
|
background: {tc('border_strong')}; border-radius: 5px; min-height: 30px;
|
||||||
}}
|
}}
|
||||||
QScrollBar::handle:vertical:hover {{ background: {ACCENT_GOLD}; }}
|
QScrollBar::handle:vertical:hover {{ background: {tc('blue')}; }}
|
||||||
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height: 0; }}
|
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height: 0; }}
|
||||||
""")
|
""")
|
||||||
layout.addWidget(browser)
|
layout.addWidget(browser)
|
||||||
@ -122,61 +119,67 @@ class HelpView(QDialog):
|
|||||||
|
|
||||||
def _inject_dark_styles(self, html: str) -> str:
|
def _inject_dark_styles(self, html: str) -> str:
|
||||||
"""HelpHTML 내용에 다크 톤 CSS 주입 (제목/링크/코드/테이블)."""
|
"""HelpHTML 내용에 다크 톤 CSS 주입 (제목/링크/코드/테이블)."""
|
||||||
|
# 현재 테마 색으로 (라이트/다크 모두 가독성 확보)
|
||||||
|
text = tc('text')
|
||||||
|
dim = tc('text_dim')
|
||||||
|
blue = tc('blue')
|
||||||
|
green = tc('green')
|
||||||
|
panel2 = tc('panel2')
|
||||||
|
border = tc('border')
|
||||||
css = f"""
|
css = f"""
|
||||||
<style>
|
<style>
|
||||||
body, p, li {{
|
body, p, li {{
|
||||||
color: #e8e8f4;
|
color: {text};
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
}}
|
}}
|
||||||
h1, h2, h3, h4 {{
|
h1, h2, h3, h4 {{
|
||||||
color: #ffd24a;
|
color: {blue};
|
||||||
margin-top: 1.2em;
|
margin-top: 1.2em;
|
||||||
margin-bottom: 0.5em;
|
margin-bottom: 0.5em;
|
||||||
}}
|
}}
|
||||||
h2 {{ font-size: 16pt; border-bottom: 2px solid #44446a; padding-bottom: 6px; }}
|
h2 {{ font-size: 16pt; border-bottom: 2px solid {border}; padding-bottom: 6px; }}
|
||||||
h3 {{ font-size: 13pt; color: #6b9eff; }}
|
h3 {{ font-size: 13pt; color: {blue}; }}
|
||||||
h4 {{ font-size: 11pt; color: #4ade80; }}
|
h4 {{ font-size: 11pt; color: {green}; }}
|
||||||
b, strong {{ color: #ff90b8; }}
|
b, strong {{ color: {text}; }}
|
||||||
code {{
|
code {{
|
||||||
background: #1c1c28;
|
background: {panel2};
|
||||||
color: #ffd24a;
|
color: {blue};
|
||||||
padding: 2px 6px;
|
padding: 2px 6px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-family: Consolas, monospace;
|
font-family: Consolas, monospace;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}}
|
}}
|
||||||
pre {{
|
pre {{
|
||||||
background: #1c1c28;
|
background: {panel2};
|
||||||
border: 1px solid #2a2a3a;
|
border: 1px solid {border};
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
color: #e8e8f4;
|
color: {text};
|
||||||
}}
|
}}
|
||||||
ul, ol {{ margin-left: 0; padding-left: 24px; }}
|
ul, ol {{ margin-left: 0; padding-left: 24px; }}
|
||||||
li {{ margin-bottom: 4px; }}
|
li {{ margin-bottom: 4px; }}
|
||||||
a {{ color: #4adef0; text-decoration: none; }}
|
a {{ color: {blue}; text-decoration: none; }}
|
||||||
a:hover {{ text-decoration: underline; }}
|
a:hover {{ text-decoration: underline; }}
|
||||||
table {{ border-collapse: collapse; margin: 10px 0; }}
|
table {{ border-collapse: collapse; margin: 10px 0; }}
|
||||||
th {{
|
th {{
|
||||||
background: #2a2a3a;
|
background: {panel2};
|
||||||
color: #ffd24a;
|
color: {text};
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border: 1px solid #44446a;
|
border: 1px solid {border};
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}}
|
}}
|
||||||
td {{
|
td {{
|
||||||
padding: 6px 12px;
|
padding: 6px 12px;
|
||||||
border: 1px solid #2a2a3a;
|
border: 1px solid {border};
|
||||||
color: #e8e8f4;
|
color: {text};
|
||||||
}}
|
}}
|
||||||
hr {{ border: none; border-top: 1px solid #2a2a3a; margin: 16px 0; }}
|
hr {{ border: none; border-top: 1px solid {border}; margin: 16px 0; }}
|
||||||
blockquote {{
|
blockquote {{
|
||||||
border-left: 3px solid #6b9eff;
|
border-left: 3px solid {blue};
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
padding: 4px 16px;
|
padding: 4px 16px;
|
||||||
color: #a0a0b8;
|
color: {dim};
|
||||||
background: rgba(107, 158, 255, 0.05);
|
|
||||||
}}
|
}}
|
||||||
</style>
|
</style>
|
||||||
"""
|
"""
|
||||||
|
|||||||
@ -15,7 +15,7 @@ from core.i18n import tr
|
|||||||
from ui.styles import apply_dark_titlebar
|
from ui.styles import apply_dark_titlebar
|
||||||
from ui.dark_components import (
|
from ui.dark_components import (
|
||||||
dialog_qss, tabs_qss, button_qss, build_stat_card, build_section_card,
|
dialog_qss, tabs_qss, button_qss, build_stat_card, build_section_card,
|
||||||
transparent_label, ACCENT_GOLD, ACCENT_GREEN, DARK_TEXT, DARK_TEXT_DIM,
|
transparent_label, tc,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ class StatsView(QDialog):
|
|||||||
self.db = db if db else Database()
|
self.db = db if db else Database()
|
||||||
self.init_ui()
|
self.init_ui()
|
||||||
self.load_stats()
|
self.load_stats()
|
||||||
apply_dark_titlebar(self, dark=True)
|
apply_dark_titlebar(self) # 현재 테마에 맞춰 타이틀바
|
||||||
|
|
||||||
def init_ui(self):
|
def init_ui(self):
|
||||||
"""UI 초기화"""
|
"""UI 초기화"""
|
||||||
@ -44,7 +44,7 @@ class StatsView(QDialog):
|
|||||||
# 다크 톤 타이틀
|
# 다크 톤 타이틀
|
||||||
title = QLabel(f"{tr('stats.title')}")
|
title = QLabel(f"{tr('stats.title')}")
|
||||||
title.setStyleSheet(
|
title.setStyleSheet(
|
||||||
f"font-size: 18pt; font-weight: bold; color: {DARK_TEXT}; "
|
f"font-size: 18pt; font-weight: bold; color: {tc('text')}; "
|
||||||
f"background: transparent; border: none; padding: 4px 0;"
|
f"background: transparent; border: none; padding: 4px 0;"
|
||||||
)
|
)
|
||||||
layout.addWidget(title)
|
layout.addWidget(title)
|
||||||
@ -143,9 +143,9 @@ class StatsView(QDialog):
|
|||||||
# 추정 급여 (옵션 활성 시)
|
# 추정 급여 (옵션 활성 시)
|
||||||
self.salary_label = QLabel("")
|
self.salary_label = QLabel("")
|
||||||
self.salary_label.setStyleSheet(
|
self.salary_label.setStyleSheet(
|
||||||
f"background: rgba(74, 222, 128, 0.12); "
|
f"background: rgba(81, 207, 102, 0.12); "
|
||||||
f"border: 1px solid {ACCENT_GREEN}; border-radius: 8px; "
|
f"border: 1px solid {tc('green')}; border-radius: 8px; "
|
||||||
f"color: {ACCENT_GREEN}; font-weight: bold; "
|
f"color: {tc('green')}; font-weight: bold; "
|
||||||
f"padding: 10px 14px; font-size: 11pt;"
|
f"padding: 10px 14px; font-size: 11pt;"
|
||||||
)
|
)
|
||||||
self.salary_label.setVisible(False)
|
self.salary_label.setVisible(False)
|
||||||
@ -179,7 +179,7 @@ class StatsView(QDialog):
|
|||||||
self.pattern_text.setWordWrap(True)
|
self.pattern_text.setWordWrap(True)
|
||||||
self.pattern_text.setAlignment(Qt.AlignTop | Qt.AlignLeft)
|
self.pattern_text.setAlignment(Qt.AlignTop | Qt.AlignLeft)
|
||||||
self.pattern_text.setStyleSheet(
|
self.pattern_text.setStyleSheet(
|
||||||
f"font-size: 11pt; color: {DARK_TEXT}; "
|
f"font-size: 11pt; color: {tc('text')}; "
|
||||||
f"background: transparent; border: none; padding: 4px 0;"
|
f"background: transparent; border: none; padding: 4px 0;"
|
||||||
)
|
)
|
||||||
layout.addWidget(build_section_card("패턴 인사이트", self.pattern_text,
|
layout.addWidget(build_section_card("패턴 인사이트", self.pattern_text,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user