From f80e336e1fc1af964fca50db1ce63dc68d1e6bb4 Mon Sep 17 00:00:00 2001 From: 3252a8 <3252a8@proton.me> Date: Fri, 15 May 2026 21:46:10 +0300 Subject: [PATCH] feat: tune visual of windows 95 theme --- bot/app/web/themes/windows95/style.css | 259 ++++++++++++++++++------ bot/app/web/themes/windows95/theme.json | 2 +- bot/app/web/webapp/assets.py | 12 +- config/webapp_themes_config.py | 2 + tests/test_webapp_assets.py | 27 ++- tests/test_webapp_themes_config.py | 6 +- 6 files changed, 238 insertions(+), 70 deletions(-) diff --git a/bot/app/web/themes/windows95/style.css b/bot/app/web/themes/windows95/style.css index b9b66b7..d5aaede 100644 --- a/bot/app/web/themes/windows95/style.css +++ b/bot/app/web/themes/windows95/style.css @@ -41,51 +41,57 @@ --admin-text: #000000; --admin-muted: #202020; --admin-dim: #404040; - --win95-icon-arrow-left: url("/webapp-theme-assets/windows95/icons/arrow-left.png?v=4"); - --win95-icon-arrow-right: url("/webapp-theme-assets/windows95/icons/arrow-right.png?v=4"); - --win95-icon-bitcoin: url("/webapp-theme-assets/windows95/icons/bitcoin.png?v=4"); - --win95-icon-check: url("/webapp-theme-assets/windows95/icons/check.png?v=4"); - --win95-icon-chevrons: url("/webapp-theme-assets/windows95/icons/chevrons.png?v=4"); - --win95-icon-coins: url("/webapp-theme-assets/windows95/icons/coins.png?v=4"); - --win95-icon-dashboard: url("/webapp-theme-assets/windows95/icons/dashboard.png?v=4"); - --win95-icon-database: url("/webapp-theme-assets/windows95/icons/database.png?v=4"); - --win95-icon-download: url("/webapp-theme-assets/windows95/icons/download.png?v=4"); - --win95-icon-error: url("/webapp-theme-assets/windows95/icons/error.png?v=4"); - --win95-icon-file-text: url("/webapp-theme-assets/windows95/icons/file-text.png?v=4"); - --win95-icon-folder: url("/webapp-theme-assets/windows95/icons/folder.png?v=4"); - --win95-icon-gift: url("/webapp-theme-assets/windows95/icons/gift.png?v=4"); - --win95-icon-globe: url("/webapp-theme-assets/windows95/icons/globe.png?v=4"); - --win95-icon-help: url("/webapp-theme-assets/windows95/icons/help.png?v=4"); - --win95-icon-home: url("/webapp-theme-assets/windows95/icons/home.png?v=4"); - --win95-icon-info: url("/webapp-theme-assets/windows95/icons/info.png?v=4"); - --win95-icon-key: url("/webapp-theme-assets/windows95/icons/key.png?v=4"); - --win95-icon-lock: url("/webapp-theme-assets/windows95/icons/lock.png?v=4"); - --win95-icon-megaphone: url("/webapp-theme-assets/windows95/icons/megaphone.png?v=4"); - --win95-icon-paintbrush: url("/webapp-theme-assets/windows95/icons/paintbrush.png?v=4"); - --win95-icon-payment-card: url("/webapp-theme-assets/windows95/icons/payment-card.png?v=4"); - --win95-icon-print: url("/webapp-theme-assets/windows95/icons/print.png?v=4"); - --win95-icon-refresh: url("/webapp-theme-assets/windows95/icons/refresh.png?v=4"); - --win95-icon-save: url("/webapp-theme-assets/windows95/icons/save.png?v=4"); - --win95-icon-search: url("/webapp-theme-assets/windows95/icons/search.png?v=4"); - --win95-icon-send: url("/webapp-theme-assets/windows95/icons/send.png?v=4"); - --win95-icon-settings: url("/webapp-theme-assets/windows95/icons/settings.png?v=4"); - --win95-icon-shield: url("/webapp-theme-assets/windows95/icons/shield.png?v=4"); - --win95-icon-smartphone: url("/webapp-theme-assets/windows95/icons/smartphone.png?v=4"); - --win95-icon-sliders: url("/webapp-theme-assets/windows95/icons/sliders.png?v=4"); - --win95-icon-sparkles: url("/webapp-theme-assets/windows95/icons/sparkles.png?v=4"); - --win95-icon-tag: url("/webapp-theme-assets/windows95/icons/tag.png?v=4"); - --win95-icon-ticket: url("/webapp-theme-assets/windows95/icons/ticket.png?v=4"); - --win95-icon-trash: url("/webapp-theme-assets/windows95/icons/trash.png?v=4"); - --win95-icon-user: url("/webapp-theme-assets/windows95/icons/user.png?v=4"); - --win95-icon-users: url("/webapp-theme-assets/windows95/icons/users.png?v=4"); - --win95-icon-warning: url("/webapp-theme-assets/windows95/icons/warning.png?v=4"); - --win95-icon-x: url("/webapp-theme-assets/windows95/icons/x.png?v=4"); + --win95-icon-arrow-left: url("/webapp-theme-assets/windows95/icons/arrow-left.png?v=6"); + --win95-icon-arrow-right: url("/webapp-theme-assets/windows95/icons/arrow-right.png?v=6"); + --win95-icon-bitcoin: url("/webapp-theme-assets/windows95/icons/bitcoin.png?v=6"); + --win95-icon-check: url("/webapp-theme-assets/windows95/icons/check.png?v=6"); + --win95-icon-chevrons: url("/webapp-theme-assets/windows95/icons/chevrons.png?v=6"); + --win95-icon-coins: url("/webapp-theme-assets/windows95/icons/coins.png?v=6"); + --win95-icon-dashboard: url("/webapp-theme-assets/windows95/icons/dashboard.png?v=6"); + --win95-icon-database: url("/webapp-theme-assets/windows95/icons/database.png?v=6"); + --win95-icon-download: url("/webapp-theme-assets/windows95/icons/download.png?v=6"); + --win95-icon-error: url("/webapp-theme-assets/windows95/icons/error.png?v=6"); + --win95-icon-file-text: url("/webapp-theme-assets/windows95/icons/file-text.png?v=6"); + --win95-icon-folder: url("/webapp-theme-assets/windows95/icons/folder.png?v=6"); + --win95-icon-gift: url("/webapp-theme-assets/windows95/icons/gift.png?v=6"); + --win95-icon-globe: url("/webapp-theme-assets/windows95/icons/globe.png?v=6"); + --win95-icon-help: url("/webapp-theme-assets/windows95/icons/help.png?v=6"); + --win95-icon-home: url("/webapp-theme-assets/windows95/icons/home.png?v=6"); + --win95-icon-info: url("/webapp-theme-assets/windows95/icons/info.png?v=6"); + --win95-icon-key: url("/webapp-theme-assets/windows95/icons/key.png?v=6"); + --win95-icon-lock: url("/webapp-theme-assets/windows95/icons/lock.png?v=6"); + --win95-icon-megaphone: url("/webapp-theme-assets/windows95/icons/megaphone.png?v=6"); + --win95-icon-paintbrush: url("/webapp-theme-assets/windows95/icons/paintbrush.png?v=6"); + --win95-icon-payment-card: url("/webapp-theme-assets/windows95/icons/payment-card.png?v=6"); + --win95-icon-print: url("/webapp-theme-assets/windows95/icons/print.png?v=6"); + --win95-icon-refresh: url("/webapp-theme-assets/windows95/icons/refresh.png?v=6"); + --win95-icon-save: url("/webapp-theme-assets/windows95/icons/save.png?v=6"); + --win95-icon-search: url("/webapp-theme-assets/windows95/icons/search.png?v=6"); + --win95-icon-send: url("/webapp-theme-assets/windows95/icons/send.png?v=6"); + --win95-icon-settings: url("/webapp-theme-assets/windows95/icons/settings.png?v=6"); + --win95-icon-shield: url("/webapp-theme-assets/windows95/icons/shield.png?v=6"); + --win95-icon-smartphone: url("/webapp-theme-assets/windows95/icons/smartphone.png?v=6"); + --win95-icon-sliders: url("/webapp-theme-assets/windows95/icons/sliders.png?v=6"); + --win95-icon-sparkles: url("/webapp-theme-assets/windows95/icons/sparkles.png?v=6"); + --win95-icon-tag: url("/webapp-theme-assets/windows95/icons/tag.png?v=6"); + --win95-icon-ticket: url("/webapp-theme-assets/windows95/icons/ticket.png?v=6"); + --win95-icon-trash: url("/webapp-theme-assets/windows95/icons/trash.png?v=6"); + --win95-icon-user: url("/webapp-theme-assets/windows95/icons/user.png?v=6"); + --win95-icon-users: url("/webapp-theme-assets/windows95/icons/users.png?v=6"); + --win95-icon-warning: url("/webapp-theme-assets/windows95/icons/warning.png?v=6"); + --win95-icon-x: url("/webapp-theme-assets/windows95/icons/x.png?v=6"); } .theme-key-windows95.app-shell { background: var(--bg) !important; } +.theme-key-windows95 *, +.theme-key-windows95 *::before, +.theme-key-windows95 *::after { + border-radius: 0 !important; +} + .theme-key-windows95 svg.lucide-arrow-left, .theme-key-windows95 svg.lucide-arrow-right, .theme-key-windows95 svg.lucide-bitcoin, @@ -456,6 +462,24 @@ 2px 2px 0 #000000; } +body:has(.theme-key-windows95) .language-select-content, +body:has(.theme-key-windows95) .admin-select-content { + border-width: 2px; + border-style: solid; + border-color: #ffffff #404040 #404040 #ffffff; + border-radius: 0 !important; + background: #c0c0c0; + box-shadow: + inset 1px 1px 0 #dfdfdf, + inset -1px -1px 0 #808080, + 2px 2px 0 #000000; +} + +body:has(.theme-key-windows95) .language-select-item, +body:has(.theme-key-windows95) .admin-select-item { + border-radius: 0 !important; +} + .theme-key-windows95 .card::before, .theme-key-windows95 .dialog-card::before { content: ""; @@ -555,23 +579,18 @@ } .theme-key-windows95 .progress { - height: 14px; - border: 2px solid; - border-color: #404040 #ffffff #ffffff #404040; - border-radius: 0; - background: #ffffff; + height: 8px; + margin-top: 12px; + overflow: hidden; + border: 0; + border-radius: 999px !important; + background: color-mix(in srgb, var(--muted) 18%, transparent); } .theme-key-windows95 .progress span { - border-radius: 0; - background: repeating-linear-gradient( - 90deg, - var(--accent) 0, - var(--accent) 8px, - #ffffff 8px, - #ffffff 10px - ); - box-shadow: none; + border-radius: inherit !important; + background: linear-gradient(90deg, var(--accent), #1084d0); + box-shadow: 0 0 18px color-mix(in srgb, var(--accent) 30%, transparent); } .theme-key-windows95 .language-select-item { @@ -726,17 +745,132 @@ .theme-key-windows95 .admin-textarea, .theme-key-windows95 .admin-screen-wrap textarea, .theme-key-windows95 .admin-dialog textarea, -.theme-key-windows95 .input { +.theme-key-windows95 .input, +.theme-key-windows95 input:not([type="checkbox"]):not([type="radio"]):not([type="range"]), +.theme-key-windows95 textarea, +.theme-key-windows95 select { + min-height: 28px; + padding: 3px 6px; border: 2px solid; border-color: #404040 #ffffff #ffffff #404040; border-radius: 0; background: #ffffff; color: #000000; + font: 13px "MS Sans Serif", Tahoma, "Segoe UI", sans-serif; + line-height: 1.35; + box-shadow: + inset 1px 1px 0 #000000, + inset 1px 1px 0 #808080, + inset -1px -1px 0 #dfdfdf; + transition: none; +} + +.theme-key-windows95 input[type="color"] { + min-height: 28px; + padding: 2px; +} + +.theme-key-windows95 input[type="file"] { + background: var(--panel); +} + +.theme-key-windows95 input[type="range"] { + width: 100%; + height: 22px; + padding: 0; + border: 0; + background: transparent; + accent-color: var(--accent); + cursor: pointer; + -webkit-appearance: none; + appearance: none; +} + +.theme-key-windows95 input[type="range"]::-webkit-slider-runnable-track { + height: 6px; + border: 2px solid; + border-color: #404040 #ffffff #ffffff #404040; + border-radius: 0 !important; + background: #ffffff; box-shadow: inset 1px 1px 0 #808080, inset -1px -1px 0 #dfdfdf; } +.theme-key-windows95 input[type="range"]::-webkit-slider-thumb { + width: 14px; + height: 20px; + margin-top: -9px; + border: 2px solid; + border-color: #ffffff #404040 #404040 #ffffff; + border-radius: 0 !important; + background: #c0c0c0; + box-shadow: + inset 1px 1px 0 #dfdfdf, + inset -1px -1px 0 #808080; + -webkit-appearance: none; + appearance: none; +} + +.theme-key-windows95 input[type="range"]:active::-webkit-slider-thumb { + border-color: #404040 #ffffff #ffffff #404040; + box-shadow: + inset 1px 1px 0 #808080, + inset -1px -1px 0 #dfdfdf; +} + +.theme-key-windows95 input[type="range"]::-moz-range-track { + height: 6px; + border: 2px solid; + border-color: #404040 #ffffff #ffffff #404040; + border-radius: 0 !important; + background: #ffffff; + box-shadow: + inset 1px 1px 0 #808080, + inset -1px -1px 0 #dfdfdf; +} + +.theme-key-windows95 input[type="range"]::-moz-range-progress { + height: 6px; + border-radius: 0 !important; + background: var(--accent); +} + +.theme-key-windows95 input[type="range"]::-moz-range-thumb { + width: 14px; + height: 20px; + border: 2px solid; + border-color: #ffffff #404040 #404040 #ffffff; + border-radius: 0 !important; + background: #c0c0c0; + box-shadow: + inset 1px 1px 0 #dfdfdf, + inset -1px -1px 0 #808080; +} + +.theme-key-windows95 input[type="range"]:active::-moz-range-thumb { + border-color: #404040 #ffffff #ffffff #404040; + box-shadow: + inset 1px 1px 0 #808080, + inset -1px -1px 0 #dfdfdf; +} + +.theme-key-windows95 input[type="range"]:focus-visible { + outline: 1px dotted #000000; + outline-offset: 2px; +} + +.theme-key-windows95 input[type="range"]:disabled { + cursor: not-allowed; + opacity: 0.52; +} + +.theme-key-windows95 input::placeholder, +.theme-key-windows95 textarea::placeholder, +.theme-key-windows95 .input::placeholder { + color: #808080; +} + .theme-key-windows95 .admin-cn-badge, .theme-key-windows95 .admin-badge { border-radius: 0; @@ -763,7 +897,10 @@ .theme-key-windows95 .admin-textarea:focus, .theme-key-windows95 .admin-screen-wrap textarea:focus, .theme-key-windows95 .admin-dialog textarea:focus, -.theme-key-windows95 .input:focus { +.theme-key-windows95 .input:focus, +.theme-key-windows95 input:not([type="checkbox"]):not([type="radio"]):not([type="range"]):focus, +.theme-key-windows95 textarea:focus, +.theme-key-windows95 select:focus { outline: none; border-color: #404040 #ffffff #ffffff #404040; box-shadow: @@ -871,14 +1008,10 @@ background: var(--panel); } -.theme-key-windows95 .progress span { - background: repeating-linear-gradient( - 90deg, - var(--accent) 0, - var(--accent) 6px, - #1084d0 6px, - #1084d0 8px - ); +.theme-key-windows95 .ui-spinner, +.theme-key-windows95 .telegram-button-spinner { + border-radius: 999px !important; + color: var(--accent); } .theme-key-windows95 .card::before, diff --git a/bot/app/web/themes/windows95/theme.json b/bot/app/web/themes/windows95/theme.json index 4631f6a..73583f0 100644 --- a/bot/app/web/themes/windows95/theme.json +++ b/bot/app/web/themes/windows95/theme.json @@ -9,7 +9,7 @@ "use_primary_accent": false, "use_in_admin": true, "css_file": "style.css", - "assets_version": 5, + "assets_version": 6, "tokens": { "color_scheme": "light", "style_preset": "win95" diff --git a/bot/app/web/webapp/assets.py b/bot/app/web/webapp/assets.py index cf8c770..ca3beee 100644 --- a/bot/app/web/webapp/assets.py +++ b/bot/app/web/webapp/assets.py @@ -118,8 +118,13 @@ async def theme_asset_route(request: web.Request) -> web.Response: if not body or len(body) > WEBAPP_THEME_ASSET_MAX_BYTES: raise web.HTTPNotFound(text="theme_asset_not_found") + query = getattr(request, "query", {}) response = web.Response(body=body, content_type=content_type) - response.headers["Cache-Control"] = "public, max-age=3600" + response.headers["Cache-Control"] = ( + "public, max-age=31536000, immutable" + if query.get("v") + else "public, max-age=3600" + ) return response @@ -1058,10 +1063,11 @@ def _initial_theme_head_markup(request: web.Request, theme: Any, primary_color: href = _theme_css_href_for_html(theme) if not href: return style_tag - return ( + stylesheet = ( f'\n' + style_tag + f'data-initial-theme-css="{html.escape(str(theme.key), quote=True)}">' ) + return stylesheet + "\n" + style_tag def _favicon_head_markup(favicon_url: str) -> str: diff --git a/config/webapp_themes_config.py b/config/webapp_themes_config.py index 495cdeb..8346c5e 100644 --- a/config/webapp_themes_config.py +++ b/config/webapp_themes_config.py @@ -375,6 +375,8 @@ def _builtin_theme_assets_need_refresh(key: str, target_dir: Path) -> bool: or "lucide-earth" not in style or "lucide-circle-check" not in style or "border-radius: 0 !important" not in style + or "::-webkit-slider-thumb" not in style + or "?v=6" not in style or any(not (target_dir / "icons" / icon).exists() for icon in required_icons) ) diff --git a/tests/test_webapp_assets.py b/tests/test_webapp_assets.py index bd53f4d..664ae12 100644 --- a/tests/test_webapp_assets.py +++ b/tests/test_webapp_assets.py @@ -163,7 +163,10 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase): with patch.object(admin_themes, "WEBAPP_FAVICON_DIR", Path(tmpdir)): payload = admin_themes._write_favicon_set(png_body, "image/png", "icon.png") - self.assertRegex(payload["favicon_url"], r"^/webapp-favicon/[0-9a-f]{16}/icon-180\.png$") + self.assertRegex( + payload["favicon_url"], + r"^/webapp-favicon/[0-9a-f]{16}/icon-180\.png$", + ) digest = payload["favicon_url"].split("/")[2] self.assertTrue((Path(tmpdir) / digest / "icon-32.png").exists()) self.assertTrue((Path(tmpdir) / digest / "apple-touch-icon.png").exists()) @@ -390,6 +393,28 @@ class WebAppAssetTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(response.headers["Cache-Control"], "public, max-age=3600") self.assertEqual(response.body, b"png-bytes") + async def test_theme_asset_route_uses_immutable_cache_for_versioned_assets(self): + with tempfile.TemporaryDirectory() as tmpdir: + themes_dir = Path(tmpdir) + (themes_dir / "custom" / "icons").mkdir(parents=True) + (themes_dir / "custom" / "icons" / "save.png").write_bytes(b"png-bytes") + request = SimpleNamespace( + app={ + "settings": SimpleNamespace( + WEBAPP_ENABLED=True, + WEBAPP_THEMES_DIR=str(themes_dir), + ) + }, + match_info={"path": "custom/icons/save.png"}, + query={"v": "6"}, + ) + + response = await subscription_webapp.theme_asset_route(request) + + self.assertEqual( + response.headers["Cache-Control"], "public, max-age=31536000, immutable" + ) + async def test_theme_asset_route_serves_default_theme_icon_from_theme_folder(self): with tempfile.TemporaryDirectory() as tmpdir: request = SimpleNamespace( diff --git a/tests/test_webapp_themes_config.py b/tests/test_webapp_themes_config.py index a643603..9e430c7 100644 --- a/tests/test_webapp_themes_config.py +++ b/tests/test_webapp_themes_config.py @@ -33,7 +33,7 @@ class WebappThemesConfigTests(unittest.TestCase): self.assertEqual(win95.tokens.style_preset, "win95") self.assertFalse(win95.use_primary_accent) self.assertTrue(win95.use_in_admin) - self.assertEqual(win95.assets_version, 5) + self.assertEqual(win95.assets_version, 6) ascii_theme = cfg.theme_by_key("ascii") self.assertIsNotNone(ascii_theme) self.assertEqual(ascii_theme.css_file, "style.css") @@ -380,13 +380,15 @@ class WebappThemesConfigTests(unittest.TestCase): descriptor["assets_version"], cfg.theme_by_key("windows95").assets_version, ) - self.assertEqual(descriptor["assets_version"], 5) + self.assertEqual(descriptor["assets_version"], 6) self.assertIn("lucide-house", css) self.assertIn("lucide-earth", css) self.assertIn("lucide-circle-check", css) self.assertIn("lucide-circle-check-big", css) self.assertIn("filter: none !important", css) self.assertIn("Press Start 2P", css) + self.assertIn("::-webkit-slider-thumb", css) + self.assertIn("?v=6", css) self.assertIn(".theme-key-windows95 .traffic-top strong", css) self.assertTrue((stale_theme_dir / "icons" / "dashboard.png").exists())