feat(ios): add light mode support, iPad keyboard shortcuts, and extended toolbar (#141)

This commit is contained in:
Peter Steinberger 2025-06-30 01:00:12 +01:00 committed by GitHub
parent 824c9134d5
commit 1472b0dee5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 517 additions and 21 deletions

View file

@ -7,6 +7,7 @@ struct VibeTunnelApp: App {
@State private var connectionManager = ConnectionManager()
@State private var navigationManager = NavigationManager()
@State private var networkMonitor = NetworkMonitor.shared
@AppStorage("colorSchemePreference") private var colorSchemePreferenceRaw = "system"
init() {
// Configure app logging level
@ -26,11 +27,20 @@ struct VibeTunnelApp: App {
// Initialize network monitoring
_ = networkMonitor
}
.preferredColorScheme(colorScheme)
#if targetEnvironment(macCatalyst)
.macCatalystWindowStyle(getStoredWindowStyle())
#endif
}
}
private var colorScheme: ColorScheme? {
switch colorSchemePreferenceRaw {
case "light": return .light
case "dark": return .dark
default: return nil // System default
}
}
#if targetEnvironment(macCatalyst)
private func getStoredWindowStyle() -> MacWindowStyle {

View file

@ -130,6 +130,44 @@ struct TerminalInput: Codable {
/// Control-E (move to end of line).
case ctrlE = "\u{0005}"
// MARK: - Function Keys
/// F1 key.
case f1 = "\u{001B}OP"
/// F2 key.
case f2 = "\u{001B}OQ"
/// F3 key.
case f3 = "\u{001B}OR"
/// F4 key.
case f4 = "\u{001B}OS"
/// F5 key.
case f5 = "\u{001B}[15~"
/// F6 key.
case f6 = "\u{001B}[17~"
/// F7 key.
case f7 = "\u{001B}[18~"
/// F8 key.
case f8 = "\u{001B}[19~"
/// F9 key.
case f9 = "\u{001B}[20~"
/// F10 key.
case f10 = "\u{001B}[21~"
/// F11 key.
case f11 = "\u{001B}[23~"
/// F12 key.
case f12 = "\u{001B}[24~"
// MARK: - Additional Special Characters
/// Backslash character.
case backslash = "\\"
/// Pipe character.
case pipe = "|"
/// Backtick character.
case backtick = "`"
/// Tilde character.
case tilde = "~"
// MARK: - Web Compatibility
/// Control-Enter combination (web frontend compatibility).

View file

@ -88,7 +88,6 @@ struct ConnectionView: View {
}
}
.navigationViewStyle(StackNavigationViewStyle())
.preferredColorScheme(.dark)
.onAppear {
viewModel.loadLastConnection()
}

View file

@ -81,7 +81,6 @@ struct EnhancedConnectionView: View {
}
}
.navigationViewStyle(StackNavigationViewStyle())
.preferredColorScheme(.dark)
.onAppear {
profilesViewModel.loadProfiles()
}

View file

@ -58,7 +58,6 @@ struct FilePreviewView: View {
}
}
}
.preferredColorScheme(.dark)
.task {
await loadPreview()
}
@ -240,7 +239,6 @@ struct GitDiffView: View {
}
}
}
.preferredColorScheme(.dark)
}
}

View file

@ -389,7 +389,7 @@ struct FileBrowserView: View {
.overlay {
if quickLookManager.isDownloading {
ZStack {
Color.black.opacity(0.8)
Theme.Colors.overlayBackground
.ignoresSafeArea()
VStack(spacing: 20) {
@ -414,7 +414,6 @@ struct FileBrowserView: View {
}
}
}
.preferredColorScheme(.dark)
.onAppear {
viewModel.loadDirectory(path: initialPath)
}

View file

@ -109,7 +109,6 @@ struct FileEditorView: View {
Text(error)
}
}
.preferredColorScheme(.dark)
.onAppear {
isTextEditorFocused = true
}

View file

@ -307,7 +307,6 @@ struct SessionCreateView: View {
focusedField = .command
}
}
.preferredColorScheme(.dark)
.sheet(isPresented: $showFileBrowser) {
FileBrowserView(initialPath: workingDirectory) { selectedPath in
workingDirectory = selectedPath

View file

@ -81,7 +81,6 @@ struct SettingsView: View {
}
}
}
.preferredColorScheme(.dark)
}
}
@ -97,9 +96,55 @@ struct GeneralSettingsView: View {
private var enableURLDetection = true
@AppStorage("enableLivePreviews")
private var enableLivePreviews = true
@AppStorage("colorSchemePreference")
private var colorSchemePreferenceRaw = "system"
enum ColorSchemePreference: String, CaseIterable {
case system = "system"
case light = "light"
case dark = "dark"
var displayName: String {
switch self {
case .system: return "System"
case .light: return "Light"
case .dark: return "Dark"
}
}
}
private var colorSchemePreference: ColorSchemePreference {
ColorSchemePreference(rawValue: colorSchemePreferenceRaw) ?? .system
}
var body: some View {
VStack(alignment: .leading, spacing: Theme.Spacing.large) {
// Appearance Section
VStack(alignment: .leading, spacing: Theme.Spacing.medium) {
Text("Appearance")
.font(.headline)
.foregroundColor(Theme.Colors.terminalForeground)
VStack(spacing: Theme.Spacing.medium) {
// Color Scheme
VStack(alignment: .leading, spacing: Theme.Spacing.small) {
Text("Color Scheme")
.font(Theme.Typography.terminalSystem(size: 14))
.foregroundColor(Theme.Colors.terminalForeground.opacity(0.7))
Picker("Color Scheme", selection: $colorSchemePreferenceRaw) {
ForEach(ColorSchemePreference.allCases, id: \.self) { preference in
Text(preference.displayName).tag(preference.rawValue)
}
}
.pickerStyle(SegmentedPickerStyle())
}
.padding()
.background(Theme.Colors.cardBackground)
.cornerRadius(Theme.CornerRadius.card)
}
}
// Terminal Defaults Section
VStack(alignment: .leading, spacing: Theme.Spacing.medium) {
Text("Terminal Defaults")

View file

@ -149,7 +149,6 @@ struct SystemLogsView: View {
}
}
}
.preferredColorScheme(.dark)
.task {
await loadLogs()
startAutoRefresh()

View file

@ -45,7 +45,6 @@ struct CastPlayerView: View {
}
}
.navigationViewStyle(StackNavigationViewStyle())
.preferredColorScheme(.dark)
.onAppear {
viewModel.loadCastFile(from: castFileURL)
}

View file

@ -101,7 +101,6 @@ struct CtrlKeyGrid: View {
}
}
}
.preferredColorScheme(.dark)
}
private var currentKeys: [(String, String)] {

View file

@ -121,6 +121,5 @@ struct FontSizeSheet: View {
}
}
}
.preferredColorScheme(.dark)
}
}

View file

@ -172,7 +172,6 @@ struct FullscreenTextInput: View {
}
}
}
.preferredColorScheme(.dark)
.onAppear {
isFocused = true
}

View file

@ -108,7 +108,6 @@ struct TerminalThemeSheet: View {
}
}
}
.preferredColorScheme(.dark)
}
}

View file

@ -145,7 +145,78 @@ struct TerminalToolbar: View {
}
}
// Third row - custom Ctrl key input
// Third row - F-keys (F1-F6)
HStack(spacing: Theme.Spacing.extraSmall) {
ForEach(["F1", "F2", "F3", "F4", "F5", "F6"], id: \.self) { fkey in
ToolbarButton(label: fkey, width: 44) {
HapticFeedback.impact(.light)
switch fkey {
case "F1": onSpecialKey(.f1)
case "F2": onSpecialKey(.f2)
case "F3": onSpecialKey(.f3)
case "F4": onSpecialKey(.f4)
case "F5": onSpecialKey(.f5)
case "F6": onSpecialKey(.f6)
default: break
}
}
}
Spacer()
}
// Fourth row - F-keys (F7-F12)
HStack(spacing: Theme.Spacing.extraSmall) {
ForEach(["F7", "F8", "F9", "F10", "F11", "F12"], id: \.self) { fkey in
ToolbarButton(label: fkey, width: 44) {
HapticFeedback.impact(.light)
switch fkey {
case "F7": onSpecialKey(.f7)
case "F8": onSpecialKey(.f8)
case "F9": onSpecialKey(.f9)
case "F10": onSpecialKey(.f10)
case "F11": onSpecialKey(.f11)
case "F12": onSpecialKey(.f12)
default: break
}
}
}
Spacer()
}
// Fifth row - Special characters
HStack(spacing: Theme.Spacing.extraSmall) {
ToolbarButton(label: "\\") {
HapticFeedback.impact(.light)
onSpecialKey(.backslash)
}
ToolbarButton(label: "|") {
HapticFeedback.impact(.light)
onSpecialKey(.pipe)
}
ToolbarButton(label: "`") {
HapticFeedback.impact(.light)
onSpecialKey(.backtick)
}
ToolbarButton(label: "~") {
HapticFeedback.impact(.light)
onSpecialKey(.tilde)
}
ToolbarButton(label: "END") {
HapticFeedback.impact(.light)
// Send Ctrl+E for end
onSpecialKey(.ctrlE)
}
Spacer()
}
// Sixth row - custom Ctrl key input
HStack(spacing: Theme.Spacing.extraSmall) {
Text("CTRL +")
.font(Theme.Typography.terminalSystem(size: 12))

View file

@ -51,7 +51,6 @@ struct TerminalView: View {
recordingIndicator
}
}
.preferredColorScheme(.dark)
.focusable()
.onAppear {
viewModel.connect()
@ -149,6 +148,7 @@ struct TerminalView: View {
showScrollToBottom = !newValue
}
}
// iPad keyboard shortcuts
.onKeyPress(keys: ["o"]) { press in
if press.modifiers.contains(.command) && session.isRunning {
showingFileBrowser = true
@ -156,6 +156,96 @@ struct TerminalView: View {
}
return .ignored
}
.onKeyPress(keys: ["+"]) { press in
if press.modifiers.contains(.command) {
// Increase font size
withAnimation(Theme.Animation.quick) {
fontSize = min(fontSize + 2, 30)
}
return .handled
}
return .ignored
}
.onKeyPress(keys: ["-"]) { press in
if press.modifiers.contains(.command) {
// Decrease font size
withAnimation(Theme.Animation.quick) {
fontSize = max(fontSize - 2, 8)
}
return .handled
}
return .ignored
}
.onKeyPress(keys: ["t"]) { press in
if press.modifiers.contains(.command) {
// Toggle theme
let themes = TerminalTheme.allThemes
if let currentIndex = themes.firstIndex(where: { $0.id == selectedTheme.id }) {
let nextIndex = (currentIndex + 1) % themes.count
selectedTheme = themes[nextIndex]
TerminalTheme.selected = selectedTheme
}
return .handled
}
return .ignored
}
.onKeyPress(keys: ["k"]) { press in
if press.modifiers.contains(.command) {
// Clear terminal
viewModel.sendSpecialKey(.ctrlL)
return .handled
}
return .ignored
}
.onKeyPress(keys: ["c"]) { press in
if press.modifiers.contains(.command) && !press.modifiers.contains(.shift) {
// Copy to clipboard
if let content = viewModel.getBufferContent() {
UIPasteboard.general.string = content
HapticFeedback.notification(.success)
}
return .handled
}
return .ignored
}
.onKeyPress(keys: ["r"]) { press in
if press.modifiers.contains(.command) {
// Start/stop recording
if viewModel.castRecorder.isRecording {
viewModel.stopRecording()
} else {
viewModel.startRecording()
}
return .handled
}
return .ignored
}
.onKeyPress(keys: ["w"]) { press in
if press.modifiers.contains(.command) {
// Change terminal width
showingTerminalWidthSheet = true
return .handled
}
return .ignored
}
.onKeyPress(keys: ["d"]) { press in
if press.modifiers.contains(.command) {
// Toggle debug menu
showingDebugMenu.toggle()
return .handled
}
return .ignored
}
.onKeyPress(keys: [.escape]) { _ in
// Send escape key to terminal
viewModel.sendSpecialKey(.escape)
return .handled
}
.onKeyPress(keys: [.tab]) { _ in
// Send tab key to terminal
viewModel.sendSpecialKey(.tab)
return .handled
}
.sheet(isPresented: $showingExportSheet) {
if let url = exportedFileURL {
ShareSheet(items: [url])

View file

@ -274,7 +274,6 @@ struct TerminalWidthSheet: View {
}
}
}
.preferredColorScheme(.dark)
}
private func applyCustomWidth() {

View file

@ -72,7 +72,6 @@ struct WidthSelectorPopover: View {
}
}
}
.preferredColorScheme(.dark)
.frame(width: 320, height: 400)
.sheet(isPresented: $showCustomInput) {
CustomWidthSheet(
@ -188,7 +187,6 @@ private struct CustomWidthSheet: View {
}
}
}
.preferredColorScheme(.dark)
.onAppear {
isFocused = true
}

View file

@ -46,7 +46,7 @@ struct WelcomeView: View {
HStack(spacing: 8) {
ForEach(0..<5) { index in
Circle()
.fill(index == currentPage ? Theme.Colors.primaryAccent : Color.gray.opacity(0.3))
.fill(index == currentPage ? Theme.Colors.primaryAccent : Theme.Colors.secondaryText.opacity(0.3))
.frame(width: 8, height: 8)
.animation(.easeInOut, value: currentPage)
}

258
ios/conversion.md Normal file
View file

@ -0,0 +1,258 @@
# VibeTunnel iOS Feature Parity Conversion Guide
This document provides a comprehensive comparison between the web frontend and iOS app features, with recommendations for achieving feature parity while maintaining a native iOS experience.
## Executive Summary
The iOS app already implements most core functionality but lacks several features present in the web frontend. Key missing features include: full theme support (light mode), SSH key management UI, advanced keyboard shortcuts, notification support, and some terminal features. Most missing features can be adapted to iOS with appropriate native patterns.
## Feature Comparison Table
### ✅ Core Terminal Features
| Feature | Web | iOS | Status | iOS Adaptation Notes |
|---------|-----|-----|--------|---------------------|
| Terminal emulation | xterm.js | SwiftTerm + xterm.js | ✅ Complete | Dual renderer approach is excellent |
| Copy/paste | Native clipboard | Touch selection | ✅ Complete | iOS implementation is more intuitive |
| URL highlighting | Clickable URLs | URL detection (configurable) | ✅ Complete | Native iOS text detection |
| Font size control | 8-32px range | 8-32pt with presets | ✅ Complete | Quick preset buttons are better for mobile |
| Terminal width presets | 80/100/120/132/160/unlimited | 80/100/120/160/unlimited | ✅ Complete | Good selection for mobile |
| Fit horizontally | ✓ | ✓ | ✅ Complete | - |
| Cursor following | Auto-scroll | Auto-scroll (configurable) | ✅ Complete | Toggle in settings is good |
| ANSI color support | Full 256 + true color | Full support | ✅ Complete | - |
| Unicode support | Full | Full | ✅ Complete | - |
### 🔧 Session Management
| Feature | Web | iOS | Status | iOS Adaptation Notes |
|---------|-----|-----|--------|---------------------|
| Session list | Grid/list view | List view | ✅ Complete | List view better for mobile |
| Session cards | Status, command, path | All info + live preview | ✅ Enhanced | Live preview is iOS advantage |
| Create sessions | Local + SSH | Local only (UI) | ⚠️ Partial | SSH UI needed |
| Kill sessions | Individual + kill all | Individual + kill all | ✅ Complete | - |
| Hide exited sessions | Toggle | Toggle | ✅ Complete | - |
| Clean exited | Bulk remove | Bulk remove | ✅ Complete | - |
| Auto-refresh | 3 seconds | 3 seconds | ✅ Complete | - |
| Session search | By command/path | By any field | ✅ Enhanced | Better search in iOS |
### ⌨️ Input & Keyboard Features
| Feature | Web | iOS | Status | iOS Adaptation Notes |
|---------|-----|-----|--------|---------------------|
| Quick keys bar | F-keys, Ctrl combos, arrows | Special keys toolbar | ✅ Complete | iOS toolbar is more accessible |
| Ctrl+Alpha overlay | Grid overlay | Grid sheet | ✅ Complete | Native sheet presentation |
| Function keys | F1-F12 in quick bar | Missing | ❌ TODO | Add to extended keyboard |
| Arrow key repeat | Hold to repeat | Single tap only | ❌ TODO | Implement long press repeat |
| Special characters | Full set in quick bar | Limited set | ⚠️ Partial | Add more special chars |
| Keyboard shortcuts | Cmd/Ctrl+O, Escape | Missing | ❌ TODO | Add iPad keyboard shortcuts |
| Raw input mode | Direct keyboard | Raw mode toggle | ✅ Complete | - |
### 🎨 Themes & Appearance
| Feature | Web | iOS | Status | iOS Adaptation Notes |
|---------|-----|-----|--------|---------------------|
| Dark theme | Full dark theme | Dark only | ✅ Complete | - |
| Light theme | Not implemented | Not implemented | ❌ TODO | **Critical: Add light mode** |
| Theme selection | N/A | 5 themes (dark only) | ✅ Enhanced | More themes than web |
| Custom themes | No | No | 🔮 Future | Consider theme editor |
### 📁 File Browser
| Feature | Web | iOS | Status | iOS Adaptation Notes |
|---------|-----|-----|--------|---------------------|
| Directory navigation | Breadcrumb nav | Hierarchical nav | ✅ Complete | iOS navigation is cleaner |
| File preview | Monaco editor | Native viewer | ✅ Complete | Quick Look is better |
| Syntax highlighting | Monaco | Native | ✅ Complete | - |
| Git status badges | ✓ | ✓ | ✅ Complete | - |
| Git diff viewer | Side-by-side | Not implemented | ❌ TODO | Use native diff view |
| Hidden files toggle | ✓ | ✓ | ✅ Complete | - |
| Path copying | ✓ | Name + path options | ✅ Enhanced | More copy options |
| Git filter | All/changed | All/changed | ✅ Complete | - |
| Image preview | Inline | Quick Look | ✅ Complete | Native preview is better |
| File editing | Monaco editor | View only | ❌ TODO | Add basic text editor |
| File operations | Read only | Create dirs only | ❌ TODO | Add rename, delete |
### 🔐 Authentication & Security
| Feature | Web | iOS | Status | iOS Adaptation Notes |
|---------|-----|-----|--------|---------------------|
| Password auth | PAM-based | ✓ | ✅ Complete | - |
| SSH key management | Generate/import/manage | Backend only | ❌ TODO | **Critical: Add SSH key UI** |
| Browser SSH agent | In-browser agent | Not applicable | N/A | Use iOS keychain |
| JWT tokens | ✓ | ✓ | ✅ Complete | - |
| No-auth mode | ✓ | ✓ | ✅ Complete | - |
| Biometric auth | No | No | ❌ TODO | Add Face ID/Touch ID |
### 🔔 Notifications & Feedback
| Feature | Web | iOS | Status | iOS Adaptation Notes |
|---------|-----|-----|--------|---------------------|
| Toast notifications | Success/error toasts | Alert/banner | ✅ Complete | Native alerts better |
| Push notifications | Browser push | Not implemented | ❌ TODO | **Add push support** |
| Sound/vibration | Settings available | Haptics only | ⚠️ Partial | Add sound options |
| Terminal bell | Visual/audio | Haptic feedback | ✅ Complete | Haptics are perfect |
### 🛠️ Advanced Features
| Feature | Web | iOS | Status | iOS Adaptation Notes |
|---------|-----|-----|--------|---------------------|
| Split view | Side-by-side list/terminal | iPad multitasking | ✅ Different | iPad split view is better |
| WebSocket binary | ✓ | ✓ | ✅ Complete | - |
| SSE streaming | Text output | Not used | N/A | WebSocket is sufficient |
| Offline support | Service worker | Basic offline handling | ⚠️ Partial | Improve offline mode |
| PWA features | Installable | Native app | N/A | Already native |
| URL routing | Deep links | URL schemes | ✅ Complete | - |
| Hot reload | Dev mode | N/A | N/A | Not needed |
| Terminal search | Not implemented | Not implemented | ❌ TODO | Add find in buffer |
| Export session | Not implemented | Text export only | ⚠️ Partial | Add PDF export |
### 📱 Mobile-Specific Features
| Feature | Web | iOS | Status | iOS Adaptation Notes |
|---------|-----|-----|--------|---------------------|
| Swipe gestures | Right for sidebar | Left to dismiss | ✅ Enhanced | More gesture support |
| Pinch to zoom | No | ✓ | ✅ iOS Exclusive | Great for accessibility |
| Haptic feedback | No | Throughout app | ✅ iOS Exclusive | Excellent feedback |
| Safe area handling | CSS env() | Native | ✅ Complete | Better implementation |
| Cast file support | Converter utility | Full support + sharing | ✅ Enhanced | File type registration |
| Recording | No | Asciinema recording | ✅ iOS Exclusive | Unique feature |
## Priority Implementation Recommendations
### 🚨 Critical (Implement Immediately)
1. **Light Mode Support**
- Implement full light/dark mode switching
- Update all colors to use semantic colors
- Test all UI elements in both modes
- Add automatic mode based on system settings
2. **SSH Key Management UI**
- Create SSH key list view
- Add key generation (RSA, Ed25519)
- Import key functionality
- Key deletion and management
- Integration with iOS Keychain
3. **iPad Keyboard Shortcuts**
- Cmd+O: Open file browser
- Cmd+K: Clear terminal
- Cmd+F: Find in buffer
- Cmd+N: New session
- Escape: Exit session
### ⚠️ High Priority
4. **Advanced Keyboard Features**
- Function keys (F1-F12) in extended toolbar
- Arrow key repeat on long press
- More special characters (|, \, ~, `, {, }, [, ])
- Customizable quick keys
5. **Push Notifications**
- Session completion notifications
- Error notifications
- Background session monitoring
- Notification settings UI
6. **File Editor**
- Basic text editor using TextEditor
- Syntax highlighting
- Save functionality
- Integration with terminal (open in editor)
### 🔔 Medium Priority
7. **Terminal Search**
- Find in buffer functionality
- Search highlighting
- Next/previous navigation
- Case sensitive toggle
8. **Git Diff Viewer**
- Native diff view component
- Side-by-side comparison
- Syntax highlighting in diffs
- Integration with file browser
9. **Enhanced File Operations**
- File/folder rename
- File deletion (with confirmation)
- File permissions viewer
- Bulk operations
10. **Session Export**
- Export as PDF with formatting
- Export with ANSI colors preserved
- Share sheet integration
### 💡 Nice to Have
11. **Biometric Authentication**
- Face ID/Touch ID for app access
- Biometric protection for SSH keys
- Quick unlock option
12. **Advanced Terminal Features**
- Terminal multiplexing (split panes)
- Session templates
- Command aliases
- Snippet management
13. **Collaboration Features**
- Session sharing via URL
- Read-only session viewing
- Collaborative editing
## iOS-Specific Design Considerations
### Native Patterns to Embrace
1. **Navigation**: Use standard iOS navigation patterns instead of web-style routing
2. **Sheets & Popovers**: Replace overlays with native sheets and popovers
3. **Gestures**: Leverage iOS gesture recognizers for intuitive interactions
4. **Haptics**: Continue extensive use of haptic feedback
5. **System Integration**: Deeper integration with iOS features (Shortcuts, Widgets)
### UI Adaptations
1. **Compact Layouts**: Optimize for smaller iPhone screens
2. **Dynamic Type**: Support for accessibility text sizes
3. **iPad Features**: Leverage larger screen with split views, multiple windows
4. **Context Menus**: Add long-press context menus throughout
5. **Pull to Refresh**: Already implemented, maintain consistency
### Performance Considerations
1. **Background Execution**: Handle background session monitoring efficiently
2. **Memory Management**: Optimize terminal buffer memory usage
3. **Battery Life**: Minimize background network activity
4. **Smooth Scrolling**: Maintain 60fps scrolling in terminal output
## Implementation Timeline
### Phase 1 (Week 1-2)
- Light mode support
- SSH key management UI
- iPad keyboard shortcuts
### Phase 2 (Week 3-4)
- Advanced keyboard features
- Push notifications
- Basic file editor
### Phase 3 (Week 5-6)
- Terminal search
- Git diff viewer
- File operations
### Phase 4 (Week 7-8)
- Session export improvements
- Biometric authentication
- Polish and testing
## Conclusion
The iOS app has a strong foundation with excellent mobile-specific features like haptics, recording, and native UI patterns. The main gaps are in theme support, SSH key management, and some advanced terminal features. By implementing the recommended features while maintaining iOS design principles, VibeTunnel can achieve feature parity while providing a superior mobile experience.
The focus should be on making the app feel completely native while matching the web's functionality. Features like biometric authentication, widgets, and Shortcuts integration could make the iOS app exceed the web version in mobile-specific scenarios.