feat(ui): refresh app UI and add SillyTavern push flow#32
Conversation
…d push to ST (experimental)
There was a problem hiding this comment.
Code Review
This pull request introduces a major UI/UX overhaul, integrating Material 3 Expressive components and a centralized icon system. Key updates include a new floating bottom navigation bar, a service for pushing profiles to SillyTavern, and a redesigned logs page with request grouping and reveal animations. The accounts page now features a custom avatar picker and enhanced filtering. Feedback focuses on performance bottlenecks in the logs page caused by expensive JSON parsing during UI rebuilds and suggests moving grouping logic to a controller. Additionally, the SillyTavern settings patching logic should be refined to avoid potential schema redundancies.
| List<_LogDisplayItem> _buildLogDisplayItems(List<AppLogEntry> entries) { | ||
| final items = <_LogDisplayItem>[]; | ||
| final requestGroups = <String, _RequestLogDisplayItem>{}; | ||
| var requestNumber = 0; | ||
|
|
||
| for (final entry in entries) { | ||
| final requestId = _requestIdForEntry(entry); | ||
| if (requestId == null) { | ||
| items.add(_SingleLogDisplayItem(entry)); | ||
| continue; | ||
| } | ||
|
|
||
| var group = requestGroups[requestId]; | ||
| if (group == null) { | ||
| requestNumber += 1; | ||
| group = _RequestLogDisplayItem(requestId: requestId, requestNumber: requestNumber); | ||
| requestGroups[requestId] = group; | ||
| items.add(group); | ||
| } | ||
| group._entries.add(entry); | ||
| } | ||
|
|
||
| return items; | ||
| } |
There was a problem hiding this comment.
The _buildLogDisplayItems function is called directly within the build method of _LogsPageState. This function iterates through the entire list of log entries and performs grouping logic, which includes multiple calls to _requestIdForEntry. Since _requestIdForEntry performs expensive jsonDecode operations on each entry's payload, this can lead to significant performance degradation and UI jank, especially as the log history grows.
Consider moving this grouping logic into the LogsController or a dedicated provider, so that the display items are computed only when the underlying log entries change, rather than on every rebuild.
| final payloadRequestId = _payloadString(_decodeLogPayload(entry.maskedPayload), 'request_id'); | ||
| if (payloadRequestId?.isNotEmpty == true) { | ||
| return payloadRequestId; | ||
| } | ||
| final rawRequestId = _payloadString(_decodeLogPayload(entry.rawPayload), 'request_id'); | ||
| if (rawRequestId?.isNotEmpty == true) { | ||
| return rawRequestId; | ||
| } | ||
| return null; | ||
| } |
There was a problem hiding this comment.
The _requestIdForEntry function calls _decodeLogPayload twice per entry (once for maskedPayload and once for rawPayload). Each call to _decodeLogPayload invokes jsonDecode. For a large list of logs, this results in thousands of redundant JSON parsing operations during every build cycle.
To improve performance, consider caching the extracted requestId within the AppLogEntry model itself or using a memoization strategy to avoid re-parsing the same JSON strings repeatedly.
| final candidates = <Map<String, Object?>>[ | ||
| settings, | ||
| if (settings['oai_settings'] is Map) | ||
| (settings['oai_settings'] as Map).map((key, value) => MapEntry(key.toString(), value)), | ||
| if (settings['openai_settings'] is Map) | ||
| (settings['openai_settings'] as Map).map((key, value) => MapEntry(key.toString(), value)), | ||
| ]; | ||
|
|
||
| for (final target in candidates) { | ||
| target['chat_completion_source'] = 'custom'; | ||
| target['custom_url'] = proxyEndpoint; | ||
| target['custom_model'] = model; | ||
| target['bypass_status_check'] = true; | ||
| target['custom_prompt_post_processing'] ??= 'merge'; | ||
| } | ||
|
|
||
| if (settings['oai_settings'] is Map) { | ||
| settings['oai_settings'] = candidates[1]; | ||
| } | ||
| if (settings['openai_settings'] is Map) { | ||
| settings['openai_settings'] = candidates.last; | ||
| } | ||
| } |
There was a problem hiding this comment.
The _patchOpenAiSettings function modifies the root settings map by adding keys like chat_completion_source, custom_url, and custom_model directly to it (via candidates[0]). While this might be intended for compatibility, SillyTavern typically expects these specific chat completion settings to reside within the oai_settings or openai_settings sub-maps. Adding them to the root map might be redundant or could potentially conflict with future SillyTavern schema updates.
There was a problem hiding this comment.
Pull request overview
This PR modernizes KiCk’s UI with new shared UI primitives (actions, haptics, smooth scrolling, Material Symbols/M3E components), adds an experimental “Push to SillyTavern” flow from Home (CSRF/cookie/secret/profile handling), and upgrades Logs/Accounts UX with richer controls and visual states. It also updates localizations, assets, dependencies, and tests to cover the new behaviors (notably log refresh/animation and SillyTavern cookie handling).
Changes:
- Introduce shared UI building blocks (KickActions, KickHaptics, KickSmoothScroll*, KickIcons) and adopt them across Home/Accounts/Logs/Settings/About/update banner.
- Add SillyTavern push service + Home dialog/CTA and tests for folded
Set-Cookiehandling. - Enhance logs with request grouping + “newly visible entries” animation and add/adjust widget/unit tests and l10n strings.
Reviewed changes
Copilot reviewed 30 out of 37 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test/silly_tavern_push_service_test.dart | Adds coverage for folded Set-Cookie parsing and cookie persistence across SillyTavern push calls. |
| test/proxy_configuration_sync_test.dart | Adds widget test ensuring logs refresh correctly on repeated proxy activity events. |
| test/logs_page_test.dart | Updates logs page expectations for refreshed UI (e.g., refresh icon removal). |
| test/logs_controller_test.dart | Adds test for “appearing entries” behavior after refreshing logs state. |
| test/kick_theme_test.dart | Adds tests for Kick color scheme hue range and M3E/Kick theme extensions. |
| test/home_page_test.dart | Adds/updates tests for new Home UI states and components (loading indicator, responsiveness, actions). |
| test/home_page_golden_test.dart | Adjusts golden locale (ru → en) for updated Home visuals. |
| test/flutter_test_config.dart | Ensures additional icon fonts are loaded in tests for Material Symbols usage. |
| test/accounts_page_test.dart | Updates tests for new accounts metrics/sort UI affordances. |
| pubspec.yaml | Adds dependencies (m3e_collection, material_symbols_icons) and registers SillyTavern logo asset. |
| pubspec.lock | Locks new direct/transitive dependencies pulled in by M3E + Material Symbols. |
| lib/l10n/generated/app_localizations.dart | Regenerates localization API with new strings (avatars, logs grouping, ST push, About sections). |
| lib/l10n/generated/app_localizations_ru.dart | Adds RU translations for new UI strings and updates Project ID labels. |
| lib/l10n/generated/app_localizations_en.dart | Adds EN strings for new UI and updates Project ID labels. |
| lib/l10n/app_ru.arb | Adds RU source strings for avatars, logs grouping, ST push, About sections, and Project ID text updates. |
| lib/l10n/app_en.arb | Adds EN source strings for avatars, logs grouping, ST push, About sections, and Project ID text updates. |
| lib/features/shared/kick_scroll.dart | Introduces smooth scroll controller + wrappers for common scroll views with scrollbar integration. |
| lib/features/shared/kick_haptics.dart | Adds centralized haptics helpers used by new action components. |
| lib/features/shared/kick_actions.dart | Adds shared primary/secondary/icon actions, loading indicator wrapper, and expressive refresh wrapper. |
| lib/features/shared/app_update_banner.dart | Updates update banner UI to use shared actions/icons and M3E progress indicator. |
| lib/features/settings/settings_sections.dart | Refreshes settings section layout, adds haptics, and adopts shared actions/icons. |
| lib/features/settings/settings_page.dart | Switches to smooth scrolling, uses shared loading indicator and KickIcons. |
| lib/features/settings/about_page.dart | Refreshes About UI (analytics card + info rows with external links) and adopts shared components. |
| lib/features/logs/logs_page.dart | Major logs UI refresh: request grouping/timeline, filter/search UX, appearing-entry animation, shared actions/icons. |
| lib/features/logs/log_message_localizer.dart | Minor formatting cleanup for RegExp declaration. |
| lib/features/home/silly_tavern_push_service.dart | Adds SillyTavern push implementation (CSRF fetch, cookie jar, secret write, settings upsert/save). |
| lib/features/home/home_page.dart | Refreshes Home layout and adds “Push to ST” dialog/CTA wired to the push service. |
| lib/features/app_state/providers.dart | Adds SillyTavern push service provider and extends LogsViewState with appearingEntryIds. |
| lib/features/app_shell/app_shell.dart | Refreshes shell navigation with floating bottom nav, M3E rail, transitions, and clearance propagation. |
| lib/features/accounts/accounts_page.dart | Refreshes accounts UX (controls/metrics) and adds avatar preview/picker with DiceBear + custom file support. |
| lib/features/accounts/account_usage_page.dart | Updates usage page to use shared actions/icons, smooth scrolling, and KickRefresh. |
| lib/core/theme/kick_theme.dart | Updates theme tokens/typography and installs M3E theme extension. |
| lib/core/theme/kick_icons.dart | Adds KickIcons wrapper over Material Symbols icons. |
| lib/core/platform/android_foreground_runtime.dart | Ensures plugin registrant initialization for Android foreground runtime entrypoint. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
|
||
| @override | ||
| void initState() { | ||
| super.initState(); | ||
| _urlController = TextEditingController(text: 'http://127.0.0.1:8000'); | ||
| _profileNameController = TextEditingController(text: 'KiCk'); | ||
| _modelController = TextEditingController(text: _defaultSillyTavernModel(widget.settings)); | ||
| } | ||
|
|
||
| @override |
| if (uri == null) { | ||
| return; | ||
| } | ||
| await launchUrl(uri, mode: LaunchMode.externalApplication); |
| } else if (_isFileAvatarUrl(selectedUrl!)) { | ||
| image = ClipRRect( | ||
| borderRadius: BorderRadius.circular(radius), | ||
| child: SizedBox( | ||
| width: size, | ||
| height: size, | ||
| child: Image.file(File.fromUri(Uri.parse(selectedUrl!)), fit: BoxFit.cover), | ||
| ), | ||
| ); | ||
| } else { | ||
| image = ClipRRect( | ||
| borderRadius: BorderRadius.circular(radius), | ||
| child: SizedBox( | ||
| width: size, | ||
| height: size, | ||
| child: Image.network(selectedUrl!, fit: BoxFit.cover), | ||
| ), | ||
| ); |
| padding: const EdgeInsets.all(6), | ||
| child: ClipRRect( | ||
| borderRadius: BorderRadius.circular(12), | ||
| child: Image.network(url, fit: BoxFit.cover), |
Summary
Testing
flutter analyzeflutter test