Skip to content

Commit 64f243e

Browse files
author
web3blind
committed
Improve mobile entry and nonintrusive game UI
1 parent 80fefa5 commit 64f243e

12 files changed

Lines changed: 234 additions & 36 deletions

File tree

app/css/main.css

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,23 @@ body {
101101
.input-feedback { margin-top: 4px; font-size: 0.875rem; min-height: 1.5em; }
102102
.input-feedback.success { color: var(--color-success); }
103103
.input-feedback.error { color: var(--color-error); }
104+
.form-group { margin-bottom: 16px; }
105+
.form-group label { display: block; margin-bottom: 6px; color: var(--color-text-muted); font-weight: 600; }
106+
.login-form { display: flex; flex-direction: column; gap: 12px; }
107+
.login-key-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 8px; }
108+
.login-key-actions .btn { flex: 1 1 140px; }
109+
.login-help,
110+
.home-install {
111+
margin: 12px 0;
112+
padding: 12px;
113+
background: var(--color-surface);
114+
border: 1px solid var(--color-border);
115+
border-radius: var(--radius-sm);
116+
color: var(--color-text-muted);
117+
}
118+
.login-help summary { color: var(--color-text); cursor: pointer; font-weight: 700; }
119+
.login-help p,
120+
.home-install p { margin-top: 8px; }
104121

105122
/* Landing */
106123
.landing { text-align: center; max-width: 480px; margin: 0 auto; }
@@ -289,16 +306,37 @@ body {
289306
.nav-icon { font-size: 1.25rem; margin-bottom: 2px; }
290307

291308
/* Toast */
292-
#toast-container { position: fixed; top: 16px; right: 16px; z-index: 1000; display: flex; flex-direction: column; gap: 8px; }
309+
#toast-container {
310+
position: fixed;
311+
top: 0;
312+
left: 0;
313+
right: 0;
314+
z-index: 1000;
315+
display: flex;
316+
flex-direction: column;
317+
align-items: center;
318+
gap: 4px;
319+
padding: max(4px, env(safe-area-inset-top)) 8px 0;
320+
pointer-events: none;
321+
}
293322
.toast {
294-
padding: 12px 20px; border-radius: var(--radius-sm); color: #fff;
295-
font-size: 0.9rem; opacity: 0; transform: translateX(100%);
296-
transition: all 0.3s ease; max-width: 300px;
297-
}
298-
.toast.show { opacity: 1; transform: translateX(0); }
299-
.toast-info { background: var(--color-surface-2); border: 1px solid var(--color-border); }
300-
.toast-success { background: #1a4d2e; border: 1px solid var(--color-success); }
301-
.toast-error { background: #4d1a1a; border: 1px solid var(--color-error); }
323+
padding: 6px 12px;
324+
border-radius: 0 0 var(--radius-sm) var(--radius-sm);
325+
color: #fff;
326+
font-size: 0.78rem;
327+
line-height: 1.25;
328+
opacity: 0;
329+
transform: translateY(-100%);
330+
transition: all 0.3s ease;
331+
max-width: min(560px, calc(100vw - 16px));
332+
box-shadow: var(--shadow);
333+
pointer-events: auto;
334+
}
335+
.toast.show { opacity: 0.96; transform: translateY(0); }
336+
.toast-strip { text-align: center; }
337+
.toast-info { background: rgba(33, 38, 45, 0.96); border: 1px solid var(--color-border); }
338+
.toast-success { background: rgba(26, 77, 46, 0.96); border: 1px solid var(--color-success); }
339+
.toast-error { background: rgba(77, 26, 26, 0.96); border: 1px solid var(--color-error); }
302340

303341
/* Modal */
304342
#modal-overlay {

app/index.html

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
<link rel="manifest" href="manifest.json">
1212
<link rel="icon" type="image/png" sizes="192x192" href="assets/icons/icon-192.png">
1313
<link rel="apple-touch-icon" href="assets/icons/icon-192.png">
14-
<link rel="stylesheet" href="css/main.css">
14+
<link rel="stylesheet" href="css/main.css?v=20260711a">
1515
<link rel="stylesheet" href="css/themes.css">
1616
<link rel="stylesheet" href="css/accessibility.css">
1717
</head>
@@ -114,8 +114,8 @@
114114
<script src="js/config.js?v=20260621m"></script>
115115

116116
<!-- I18n -->
117-
<script src="js/i18n/ru.js?v=20260621n"></script>
118-
<script src="js/i18n/en.js?v=20260621n"></script>
117+
<script src="js/i18n/ru.js?v=20260711a"></script>
118+
<script src="js/i18n/en.js?v=20260711a"></script>
119119

120120
<!-- Utilities -->
121121
<script src="js/utils/helpers.js?v=20260621l"></script>
@@ -171,20 +171,20 @@
171171
<!-- UI Components -->
172172
<script src="js/ui/sound.js"></script>
173173
<script src="js/ui/components/progress-bar.js"></script>
174-
<script src="js/ui/components/toast.js"></script>
174+
<script src="js/ui/components/toast.js?v=20260711a"></script>
175175
<script src="js/ui/components/modal.js"></script>
176176
<script src="js/ui/components/nav.js"></script>
177177
<script src="js/ui/components/battle-narrator.js"></script>
178178

179179
<!-- UI Screens -->
180180
<script src="js/ui/screens/landing.js"></script>
181-
<script src="js/ui/screens/login.js"></script>
181+
<script src="js/ui/screens/login.js?v=20260711a"></script>
182182
<script src="js/ui/screens/onboarding.js"></script>
183-
<script src="js/ui/screens/home.js"></script>
183+
<script src="js/ui/screens/home.js?v=20260711a"></script>
184184
<script src="js/ui/screens/character.js?v=20260621l"></script>
185185
<script src="js/ui/screens/hunt.js"></script>
186186
<script src="js/ui/screens/inventory.js?v=20260621l"></script>
187-
<script src="js/ui/screens/chronicle.js?v=20260621h"></script>
187+
<script src="js/ui/screens/chronicle.js?v=20260711a"></script>
188188
<script src="js/ui/screens/duel.js"></script>
189189
<script src="js/ui/screens/arena.js?v=20260621d"></script>
190190
<script src="js/ui/screens/guild.js?v=20260621l"></script>
@@ -198,6 +198,6 @@
198198
<script src="js/ui/screens/leaderboard.js"></script>
199199

200200
<!-- Main App Controller (must be last) -->
201-
<script src="js/ui/app.js?v=20260621d"></script>
201+
<script src="js/ui/app.js?v=20260711a"></script>
202202
</body>
203203
</html>

app/js/i18n/en.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ var LangEN = {
2828
login_connecting: 'Connecting to the Realm...',
2929
login_back: '← Back to main page',
3030
login_register_hint: 'If you do not have a VIZ account yet, register at start.viz.world',
31+
login_key_actions: 'Magic Word actions',
32+
login_paste_key: 'Paste from clipboard',
33+
login_show_key: 'Show word',
34+
login_hide_key: 'Hide word',
35+
login_clipboard_unavailable: 'The browser did not allow clipboard access. Long-press the Magic Word field and choose Paste.',
36+
login_keyboard_help: 'Keyboard will not paste the Magic Word?',
37+
login_keyboard_help_text: 'Try the Paste from clipboard button, show the word to check it, or paste from the long-press menu. If the keyboard still blocks the clipboard, Android users usually have better luck with Gboard or Microsoft SwiftKey; this is optional, only a fallback.',
3138
onboarding_existing_account_notice: 'VIZ account found: {account}. No character is awakened yet — choose your path of magic.',
3239
error_empty_account: 'Enter your mage name.',
3340
error_no_mage_name: 'Enter your mage name.',
@@ -110,6 +117,12 @@ var LangEN = {
110117
home_bag: 'Bag',
111118
home_map: 'Map',
112119
home_chronicle: 'Chronicle',
120+
home_primary_actions: 'Main bar',
121+
home_secondary_actions: 'More sections',
122+
home_install_shortcut: 'Game shortcut on your phone',
123+
home_install_shortcut_text: 'Add Viz Magic to your phone home screen so you do not have to find a saved link after the game window closes. This is a normal PWA shortcut install, no app store needed.',
124+
home_install_shortcut_button: 'Install or show instructions',
125+
home_install_shortcut_manual: 'If the install prompt does not appear: open the browser or Telegram menu and choose Add to Home screen / Install app.',
113126

114127
// Character
115128
char_title: 'Character',

app/js/i18n/ru.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ var LangRU = {
2828
login_connecting: 'Связь с Миром устанавливается...',
2929
login_back: '← Вернуться на главную',
3030
login_register_hint: 'Если аккаунта VIZ ещё нет, зарегистрируйтесь на start.viz.world',
31+
login_key_actions: 'Действия с Магическим Словом',
32+
login_paste_key: 'Вставить из буфера',
33+
login_show_key: 'Показать слово',
34+
login_hide_key: 'Скрыть слово',
35+
login_clipboard_unavailable: 'Браузер не дал доступ к буферу. Нажмите и удерживайте поле Магического Слова, затем выберите «Вставить».',
36+
login_keyboard_help: 'Клавиатура не даёт вставить Магическое Слово?',
37+
login_keyboard_help_text: 'Попробуйте кнопку «Вставить из буфера», включите показ слова для проверки, или вставьте через меню долгого нажатия. Если клавиатура всё равно блокирует буфер, на Android обычно помогают Gboard или Microsoft SwiftKey; это не обязательно, а запасной вариант.',
3138
onboarding_existing_account_notice: 'Аккаунт VIZ найден: {account}. Персонаж ещё не пробуждён — выберите путь магии.',
3239
error_empty_account: 'Введи имя мага.',
3340
error_no_mage_name: 'Введи имя мага.',
@@ -110,6 +117,12 @@ var LangRU = {
110117
home_bag: 'Сумка',
111118
home_map: 'Карта',
112119
home_chronicle: 'Хроника',
120+
home_primary_actions: 'Главная строка',
121+
home_secondary_actions: 'Ещё разделы',
122+
home_install_shortcut: 'Ярлык игры на телефоне',
123+
home_install_shortcut_text: 'Чтобы не искать сохранённую ссылку после закрытия окна, добавьте Viz Magic на главный экран телефона. Это обычная установка PWA-ярлыка без магазина приложений.',
124+
home_install_shortcut_button: 'Установить или показать инструкцию',
125+
home_install_shortcut_manual: 'Если кнопка установки не появилась: откройте меню браузера или Telegram, выберите «Добавить на главный экран» / «Установить приложение».',
113126

114127
// Character
115128
char_title: 'Персонаж',

app/js/ui/app.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ var App = (function() {
1414
var _syncStartBlock = 0;
1515
var _syncVisible = false;
1616
var _lastSyncPercent = -1;
17+
var _deferredInstallPrompt = null;
1718
var POLL_INTERVAL_MS = 3000;
1819

1920
/**
@@ -275,6 +276,10 @@ var App = (function() {
275276
*/
276277
function _registerPWA() {
277278
if ('file://' === document.location.origin) return;
279+
window.addEventListener('beforeinstallprompt', function(event) {
280+
event.preventDefault();
281+
_deferredInstallPrompt = event;
282+
});
278283
if ('serviceWorker' in navigator) {
279284
navigator.serviceWorker.register('/sw.js', { scope: '/' }).then(function() {
280285
console.log('Service Worker Registered');
@@ -284,6 +289,19 @@ var App = (function() {
284289
}
285290
}
286291

292+
function installShortcut() {
293+
if (_deferredInstallPrompt) {
294+
_deferredInstallPrompt.prompt();
295+
_deferredInstallPrompt.userChoice.then(function() {
296+
_deferredInstallPrompt = null;
297+
});
298+
return;
299+
}
300+
if (typeof Toast !== 'undefined') {
301+
Toast.info(Helpers.t('home_install_shortcut_manual'), 6000, { key: 'install_shortcut_manual' });
302+
}
303+
}
304+
287305
/**
288306
* Start periodic block polling for real-time state sync.
289307
* Every POLL_INTERVAL_MS checks chain head and processes new blocks
@@ -794,6 +812,7 @@ var App = (function() {
794812
return {
795813
init: init,
796814
navigateTo: navigateTo,
815+
installShortcut: installShortcut,
797816
getCurrentScreen: getCurrentScreen
798817
};
799818
})();

app/js/ui/components/toast.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,13 @@ var Toast = (function() {
3434
}
3535

3636
var toast = document.createElement('div');
37-
toast.className = 'toast toast-' + type;
37+
toast.className = 'toast toast-strip toast-' + type;
3838
if (options.key) {
3939
toast.setAttribute('data-toast-key', options.key);
4040
activeKeys[options.key] = toast;
4141
}
4242
toast.textContent = message;
43-
toast.setAttribute('role', 'alert');
43+
toast.setAttribute('role', type === 'error' ? 'alert' : 'status');
4444

4545
if (options.actionLabel) {
4646
toast.setAttribute('tabindex', '0');

app/js/ui/screens/chronicle.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,8 @@ var ChronicleScreen = (function() {
306306
var key;
307307
if (entries[i].actionType === 'blessing_sent') {
308308
key = (entries[i].account || '') + '|blessing_sent|' + (entries[i].receiver || '') + '|' + (entries[i].text || '');
309+
} else if (entries[i].actionType === 'chronicle_post') {
310+
key = (entries[i].account || '') + '|chronicle_post|' + _normalizeDedupeText(entries[i].text || '');
309311
} else {
310312
key = (entries[i].account || '') + '|' + (entries[i].blockNum || 0) + '|' + (entries[i].actionType || '') + '|' + (entries[i].text || '');
311313
}
@@ -316,6 +318,10 @@ var ChronicleScreen = (function() {
316318
return out;
317319
}
318320

321+
function _normalizeDedupeText(text) {
322+
return String(text || '').replace(/\s+/g, ' ').trim().toLowerCase();
323+
}
324+
319325
function _injectLocalPost(text) {
320326
var state = StateEngine.getState();
321327
var user = VizAccount.getCurrentUser();

app/js/ui/screens/home.js

Lines changed: 66 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
var HomeScreen = (function() {
66
'use strict';
77

8+
var PRIMARY_HOME_SCREENS = ['home', 'hunt', 'map', 'guild', 'marketplace', 'crafting', 'character', 'help', 'leaderboard'];
9+
var SECONDARY_HOME_SCREENS = ['inventory', 'chronicle', 'arena', 'quests', 'world-boss', 'settings'];
10+
811
function render() {
912
var t = Helpers.t;
1013
var el = Helpers.$('screen-home');
@@ -49,18 +52,23 @@ var HomeScreen = (function() {
4952
// Daily Prophecy
5053
_renderDailyProphecy(character, state, blockNum, t) +
5154

52-
'<section class="home-actions" aria-label="Quick actions">' +
55+
'<section class="home-actions" aria-label="' + t('home_primary_actions') + '">' +
56+
'<h2>' + t('home_primary_actions') + '</h2>' +
5357
'<div class="action-grid">' +
54-
_tile('hunt', '\u2694\uFE0F', t('home_quick_hunt')) +
55-
_tile('quests', '\uD83D\uDCDC', t('nav_quests')) +
56-
_tile('map', '\uD83D\uDDFA\uFE0F', t('nav_map')) +
57-
_tile('inventory', '\uD83C\uDF92', t('home_bag')) +
58-
_tile('chronicle', '\uD83D\uDCDD', t('home_chronicle')) +
59-
_tile('character', '\uD83E\uDDD9', t('nav_character')) +
60-
_tile('arena', '\u2694\uFE0F', t('nav_duel') || 'Арена') +
61-
_tile('settings', '\u2699\uFE0F', t('settings')) +
58+
_renderActionTiles(PRIMARY_HOME_SCREENS, true) +
6259
'</div>' +
6360
'</section>' +
61+
'<section class="home-actions home-actions-secondary" aria-label="' + t('home_secondary_actions') + '">' +
62+
'<h2>' + t('home_secondary_actions') + '</h2>' +
63+
'<div class="action-grid">' +
64+
_renderActionTiles(SECONDARY_HOME_SCREENS, false) +
65+
'</div>' +
66+
'</section>' +
67+
'<section class="home-install" aria-label="' + t('home_install_shortcut') + '">' +
68+
'<h2>' + t('home_install_shortcut') + '</h2>' +
69+
'<p>' + t('home_install_shortcut_text') + '</p>' +
70+
'<button type="button" class="btn btn-secondary" id="btn-install-shortcut">' + t('home_install_shortcut_button') + '</button>' +
71+
'</section>' +
6472
'</div>';
6573

6674
var tiles = el.querySelectorAll('.action-tile');
@@ -80,6 +88,16 @@ var HomeScreen = (function() {
8088
});
8189
}
8290

91+
var installBtn = Helpers.$('btn-install-shortcut');
92+
if (installBtn) {
93+
installBtn.addEventListener('click', function() {
94+
SoundManager.play('tap');
95+
if (typeof App !== 'undefined' && App.installShortcut) {
96+
App.installShortcut();
97+
}
98+
});
99+
}
100+
83101
// Fetch real mana from blockchain
84102
if (user) {
85103
VizAccount.getAccount(user, function(err, accountData) {
@@ -158,6 +176,45 @@ var HomeScreen = (function() {
158176
'</section>';
159177
}
160178

179+
function _renderActionTiles(screens, primary) {
180+
var html = '';
181+
for (var i = 0; i < screens.length; i++) {
182+
html += _tile(screens[i], _iconForScreen(screens[i]), _labelForScreen(screens[i], primary));
183+
}
184+
return html;
185+
}
186+
187+
function _labelForScreen(screen, primary) {
188+
var t = Helpers.t;
189+
if (primary && screen === 'marketplace') return t('nav_bazaar');
190+
if (primary && screen === 'crafting') return t('nav_crafting');
191+
if (screen === 'world-boss') return t('nav_world-boss');
192+
if (screen === 'settings') return t('nav_settings') || t('settings');
193+
if (screen === 'arena') return t('nav_duel') || t('nav_arena');
194+
return t('nav_' + screen) || screen;
195+
}
196+
197+
function _iconForScreen(screen) {
198+
var icons = {
199+
home: '\uD83C\uDFE0',
200+
hunt: '\u2694\uFE0F',
201+
map: '\uD83D\uDDFA\uFE0F',
202+
guild: '\uD83D\uDEE1\uFE0F',
203+
marketplace: '\uD83C\uDFEA',
204+
crafting: '\uD83D\uDD28',
205+
character: '\uD83E\uDDD9',
206+
help: '\u2753',
207+
leaderboard: '\uD83C\uDFC6',
208+
inventory: '\uD83C\uDF92',
209+
chronicle: '\uD83D\uDCDD',
210+
arena: '\u2694\uFE0F',
211+
quests: '\uD83D\uDCDC',
212+
'world-boss': '\uD83D\uDC32',
213+
settings: '\u2699\uFE0F'
214+
};
215+
return icons[screen] || '\u2728';
216+
}
217+
161218
function _tile(screen, icon, label) {
162219
return '<button class="action-tile" data-screen="' + screen + '" aria-label="' + label + '">' +
163220
'<span class="tile-icon" aria-hidden="true">' + icon + '</span>' +

0 commit comments

Comments
 (0)