Compare commits

...

7 Commits

Author SHA1 Message Date
Neeraj Gupta
6dce11713a Merge remote-tracking branch 'origin/main' into fixReqVal 2025-09-11 17:29:23 +05:30
Neeraj Gupta
8648f1bb3f [server] Fix req validation 2025-09-11 17:28:44 +05:30
Neeraj
8810f88236 [infra] web-based log parser for support (#7144)
## Description

## Tests
2025-09-11 14:30:22 +05:30
Neeraj
e1423f2030 Add web-based log viewer for Ente application logs
This adds a comprehensive web-based log viewer that provides similar
functionality to the mobile log viewer, allowing analysis of log files
from customer support requests.

Features:
- Upload and parse ZIP files containing daily log files
- Advanced filtering by log level, logger name, process, and timeline
- Text search with wildcard support (logger:Service*)
- Interactive analytics with click-to-filter charts
- Modern UI using Ente's design system and Material-UI components
- Infinite scroll for performance with large log files
- Export functionality for filtered results
- Responsive design for desktop and mobile

Technical highlights:
- Client-side ZIP processing with JSZip
- Efficient log parsing supporting Ente's super_logging format
- Real-time filtering with optimized algorithms
- Memory-efficient rendering with virtual scrolling
- CSS custom properties for theming consistency

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 13:55:24 +05:30
Neeraj
c2ba7c56be Add process prefix filtering and improve log filter dialog UI (#7142) 2025-09-11 11:43:03 +05:30
Neeraj
93618117c5 [mob] Bump version v1.2.6 2025-09-11 11:41:02 +05:30
Neeraj
08c38086a5 Add process prefix filtering and improve log filter dialog UI
- Add process prefix filter section with user-friendly display names
- Move process filter above loggers in dialog layout
- Compact dialog design: reduce sizes, padding, and font sizes
- Optimize filter chip layout for better mobile experience

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-11 11:32:58 +05:30
9 changed files with 2844 additions and 32 deletions

View File

@@ -0,0 +1,167 @@
# Ente Log Viewer
A web-based log viewer for analyzing Ente application logs. This tool provides similar functionality to the mobile log viewer, allowing you to upload, filter, and analyze log files from customer support requests.
## Features
### 📁 File Upload
- Drag and drop ZIP files containing log files
- Automatic extraction and parsing of log files
- Support for daily log files format (YYYY-M-D.log)
### 🔍 Search and Filtering
- **Text Search**: Search through log messages, logger names, and error content
- **Logger Filtering**: Use `logger:ServiceName` syntax to filter by specific loggers
- **Wildcard Support**: Use `logger:Auth*` to match all loggers starting with "Auth"
- **Level Filtering**: Filter by log levels (SEVERE, WARNING, INFO, etc.)
- **Process Filtering**: Filter by foreground/background processes
- **Timeline Filtering**: Filter by date/time ranges
### 📊 Analytics
- Logger statistics showing most active components
- Log level distribution charts
- Click-to-filter from analytics charts
### 🎨 UI Features
- **Color-coded log levels**: Red for SEVERE, orange for WARNING, etc.
- **Process indicators**: Visual distinction between foreground and background processes
- **Active filter chips**: Visual indication of applied filters with easy removal
- **Log detail view**: Click any log entry for detailed information
- **Sort options**: Sort by newest first or oldest first
- **Responsive design**: Works on desktop and mobile devices
### 📤 Export
- Export filtered logs as text files
- Copy individual log entries to clipboard
- Maintain original formatting and error details
## Usage
### Starting the Application
1. **Local Development**:
```bash
cd infra/experiments/logs-viewer
python3 -m http.server 8080
```
Open http://localhost:8080 in your browser
2. **Upload Log Files**:
- Drag and drop a ZIP file containing `.log` files
- Or click "Choose ZIP File" to browse for files
### Log Format Support
The viewer understands the Ente log format as generated by `super_logging.dart`:
```
[process] [loggerName] [LEVEL] [timestamp] message
```
**Examples**:
- `[bg] [SyncService] [INFO] [2025-08-24 01:36:03.677678] Syncing started`
- `[CollectionsService] [WARNING] [2025-08-24 01:36:04.123456] Connection failed`
**Multi-line Error Support**:
- Automatically parses `` error detail lines
- Extracts stack traces and error IDs
- Handles inline error messages and exceptions
### Filtering Examples
- **Search by text**: `connection failed`
- **Filter by logger**: `logger:SyncService`
- **Multiple loggers**: `logger:Sync* logger:Collection*`
- **Combined search**: `logger:Auth* login failed`
### Keyboard Shortcuts
- **Search**: Click search bar or start typing
- **Clear search**: Click X button or clear the input
- **Sort toggle**: Click sort arrow button
- **Filter dialog**: Click filter button
## Technical Details
### Supported Log Levels
- **SHOUT**: Purple - Highest priority
- **SEVERE**: Red - Errors and exceptions
- **WARNING**: Orange - Warning conditions
- **INFO**: Blue - Informational messages
- **CONFIG**: Green - Configuration messages
- **FINE/FINER/FINEST**: Gray - Debug messages
### Process Types
- **Foreground**: Main app processes
- **Background (bg)**: Background tasks
- **Firebase Background (fbg)**: Firebase-related background processes
### Performance
- Lazy loading: Only renders visible log entries
- Efficient filtering: Client-side filtering with optimized algorithms
- Memory management: Handles large log files (tested with 100k+ entries)
## Sample Log Files
For testing, you can use any ZIP file containing `.log` files from Ente mobile app logs.
## Development
### File Structure
```
logs-viewer/
├── index.html # Main HTML structure
├── styles.css # CSS styling
├── script.js # JavaScript logic
├── README.md # This documentation
└── references/ # UI reference screenshots
```
### Key JavaScript Classes
- `LogViewer`: Main application class
- Log parsing logic in `parseLogFile()` and `parseLogLine()`
- Filter management in `applyCurrentFilter()`
- UI rendering in `renderLogs()` and `createLogEntryHTML()`
### Adding Features
1. **New Filter Types**: Extend `currentFilter` object and `matchesFilter()` method
2. **New Log Formats**: Update parsing patterns in `parseLogLine()`
3. **UI Components**: Add HTML elements and CSS classes, wire up in `initializeEventListeners()`
## Troubleshooting
### Common Issues
1. **ZIP file not loading**:
- Ensure ZIP contains `.log` files
- Check browser console for errors
- Try a smaller file first
2. **Logs not parsing correctly**:
- Check log format matches expected pattern
- Look for console warnings about parsing failures
- Verify timestamp format is correct
3. **Performance issues with large files**:
- The viewer handles pagination (100 logs at a time)
- Use filters to reduce the dataset
- Close other browser tabs to free memory
4. **Search not working**:
- Check for typos in logger names
- Use wildcard syntax: `logger:Service*`
- Search is case-insensitive for message content
### Browser Compatibility
- **Recommended**: Chrome 80+, Firefox 75+, Safari 13+
- **Required**: ES6 support, Fetch API, File API
- **Dependencies**: JSZip library for ZIP file handling
## Future Enhancements
- Real-time log streaming
- Advanced regex search
- Log correlation and grouping
- Performance metrics dashboard
- Dark theme support
- Export to JSON/CSV formats

View File

@@ -0,0 +1,110 @@
/* Ente Theme Variables based on web/packages/base/components/utils/theme.ts */
:root {
/* Light theme colors */
--ente-color-accent-photos: #1db954;
--ente-color-accent-auth: #9610d6;
--ente-color-accent-locker: #5ba8ff;
/* Background colors */
--ente-background-default: #fff;
--ente-background-paper: #fff;
--ente-background-paper2: #fbfbfb;
--ente-background-search: #f3f3f3;
/* Text colors */
--ente-text-base: #000;
--ente-text-muted: rgba(0, 0, 0, 0.60);
--ente-text-faint: rgba(0, 0, 0, 0.50);
/* Fill colors */
--ente-fill-base: #000;
--ente-fill-muted: rgba(0, 0, 0, 0.12);
--ente-fill-faint: rgba(0, 0, 0, 0.04);
--ente-fill-faint-hover: rgba(0, 0, 0, 0.08);
--ente-fill-fainter: rgba(0, 0, 0, 0.02);
/* Stroke colors */
--ente-stroke-base: #000;
--ente-stroke-muted: rgba(0, 0, 0, 0.24);
--ente-stroke-faint: rgba(0, 0, 0, 0.12);
--ente-stroke-fainter: rgba(0, 0, 0, 0.06);
/* Shadow */
--ente-shadow-paper: 0px 0px 10px rgba(0, 0, 0, 0.25);
--ente-shadow-menu: 0px 0px 6px rgba(0, 0, 0, 0.16), 0px 3px 6px rgba(0, 0, 0, 0.12);
--ente-shadow-button: 0px 4px 4px rgba(0, 0, 0, 0.25);
/* Fixed colors */
--ente-success: #1db954;
--ente-warning: #ffc107;
--ente-danger: #ea3f3f;
--ente-danger-dark: #f53434;
--ente-danger-light: #ff6565;
/* Secondary colors */
--ente-secondary-main: #f5f5f5;
--ente-secondary-hover: #e9e9e9;
/* Action colors */
--ente-action-hover: rgba(0, 0, 0, 0.08);
--ente-action-disabled: rgba(0, 0, 0, 0.50);
/* Typography */
--ente-font-family: "Inter Variable", sans-serif;
--ente-font-weight-regular: 500;
--ente-font-weight-medium: 600;
--ente-font-weight-bold: 700;
/* Border radius */
--ente-border-radius: 8px;
--ente-border-radius-small: 4px;
/* Spacing */
--ente-spacing-xs: 4px;
--ente-spacing-sm: 8px;
--ente-spacing-md: 12px;
--ente-spacing-lg: 16px;
--ente-spacing-xl: 24px;
}
/* Dark theme */
@media (prefers-color-scheme: dark) {
:root {
/* Background colors */
--ente-background-default: #000;
--ente-background-paper: #1b1b1b;
--ente-background-paper2: #252525;
--ente-background-search: #1b1b1b;
/* Text colors */
--ente-text-base: #fff;
--ente-text-muted: rgba(255, 255, 255, 0.70);
--ente-text-faint: rgba(255, 255, 255, 0.50);
/* Fill colors */
--ente-fill-base: #fff;
--ente-fill-muted: rgba(255, 255, 255, 0.16);
--ente-fill-faint: rgba(255, 255, 255, 0.12);
--ente-fill-faint-hover: rgba(255, 255, 255, 0.16);
--ente-fill-fainter: rgba(255, 255, 255, 0.05);
/* Stroke colors */
--ente-stroke-base: #fff;
--ente-stroke-muted: rgba(255, 255, 255, 0.24);
--ente-stroke-faint: rgba(255, 255, 255, 0.16);
--ente-stroke-fainter: rgba(255, 255, 255, 0.12);
/* Shadow */
--ente-shadow-paper: 0px 2px 12px rgba(0, 0, 0, 0.75);
--ente-shadow-menu: 0px 0px 6px rgba(0, 0, 0, 0.50), 0px 3px 6px rgba(0, 0, 0, 0.25);
--ente-shadow-button: 0px 4px 4px rgba(0, 0, 0, 0.75);
/* Secondary colors */
--ente-secondary-main: #2b2b2b;
--ente-secondary-hover: #373737;
/* Action colors */
--ente-action-hover: rgba(255, 255, 255, 0.16);
--ente-action-disabled: rgba(255, 255, 255, 0.50);
}
}

View File

@@ -0,0 +1,218 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ente Log Viewer</title>
<!-- Material-UI CSS -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mui/material@latest/dist/index.css" />
<!-- Ente theme -->
<link rel="stylesheet" href="ente-theme.css">
<!-- Custom styles -->
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="app">
<!-- Header -->
<header class="header">
<div class="header-content">
<h1>📋 Ente Log Viewer</h1>
<div class="header-actions">
<button id="filter-btn" class="mui-icon-btn" title="Filter logs">
<span class="material-icons">filter_list</span>
<span class="filter-count" id="filter-count" style="display: none;"></span>
</button>
<button id="sort-btn" class="mui-icon-btn" title="Sort order">
<span class="material-icons">arrow_downward</span>
</button>
<div class="dropdown">
<button class="mui-icon-btn dropdown-toggle" title="More actions">
<span class="material-icons">more_vert</span>
</button>
<div class="dropdown-menu mui-menu">
<button id="analytics-btn" class="dropdown-item mui-menu-item">
<span class="material-icons">analytics</span>
<span>Analytics</span>
</button>
<button id="export-btn" class="dropdown-item mui-menu-item">
<span class="material-icons">download</span>
<span>Export Logs</span>
</button>
<button id="clear-btn" class="dropdown-item mui-menu-item danger">
<span class="material-icons">delete</span>
<span>Clear Logs</span>
</button>
</div>
</div>
</div>
</div>
</header>
<!-- Upload Section -->
<div class="upload-section" id="upload-section">
<div class="upload-area" id="upload-area">
<div class="upload-content">
<div class="upload-icon">📁</div>
<h2>Upload Log Files</h2>
<p>Drag and drop a zip file containing log files, or click to browse</p>
<input type="file" id="file-input" accept=".zip" hidden>
<button id="browse-btn" class="primary-btn">Choose ZIP File</button>
</div>
</div>
</div>
<!-- Main Content -->
<div class="main-content" id="main-content" style="display: none;">
<!-- Search Bar -->
<div class="search-section">
<div class="search-bar mui-search-container">
<div class="mui-textfield">
<input type="text" id="search-input" placeholder="Search logs... (try 'logger:SyncService' or text search)" class="mui-input" />
<span class="mui-search-icon material-icons">search</span>
<button id="clear-search" class="mui-clear-btn" style="display: none;">
<span class="material-icons">clear</span>
</button>
</div>
</div>
</div>
<!-- Timeline Filter -->
<div class="timeline-section" id="timeline-section" style="display: none;">
<div class="timeline-header">
<span class="material-icons">timeline</span>
<span>Timeline Filter</span>
<button id="timeline-toggle" class="mui-icon-btn timeline-btn">
<span class="material-icons">timeline</span>
</button>
</div>
<div class="timeline-controls" id="timeline-controls" style="display: none;">
<div class="timeline-range">
<div class="mui-textfield">
<input type="datetime-local" id="start-time" class="mui-input" />
</div>
<span class="range-separator">to</span>
<div class="mui-textfield">
<input type="datetime-local" id="end-time" class="mui-input" />
</div>
<button id="reset-timeline" class="mui-button secondary">
<span class="material-icons">refresh</span>
<span>Reset</span>
</button>
</div>
</div>
</div>
<!-- Active Filters -->
<div class="active-filters" id="active-filters" style="display: none;">
<div class="filter-chips" id="filter-chips"></div>
</div>
<!-- Log Stats -->
<div class="log-stats" id="log-stats">
<span id="log-count">0 logs loaded</span>
<span id="filtered-count"></span>
</div>
<!-- Log List -->
<div class="log-list-container">
<div class="log-list" id="log-list">
<!-- Log entries will be populated here -->
</div>
<div class="loading" id="loading" style="display: none;">Loading...</div>
<div class="load-more" id="load-more" style="display: none;">
<button class="secondary-btn">Load More</button>
</div>
</div>
</div>
</div>
<!-- Filter Dialog -->
<div class="dialog-overlay" id="filter-dialog" style="display: none;">
<div class="dialog">
<div class="dialog-header">
<h2>Filter Logs</h2>
<button class="close-btn" id="close-filter"></button>
</div>
<div class="dialog-content">
<!-- Log Levels -->
<div class="filter-section">
<h3>Log Levels</h3>
<div class="level-chips" id="level-chips">
<!-- Level chips will be populated here -->
</div>
</div>
<!-- Process -->
<div class="filter-section">
<h3>Process</h3>
<div class="process-list" id="process-list">
<!-- Process checkboxes will be populated here -->
</div>
</div>
<!-- Loggers -->
<div class="filter-section">
<h3>Loggers</h3>
<div class="logger-list" id="logger-list">
<!-- Logger checkboxes will be populated here -->
</div>
</div>
</div>
<div class="dialog-actions">
<button id="clear-filters" class="secondary-btn">Clear All</button>
<button id="cancel-filter" class="secondary-btn">Cancel</button>
<button id="apply-filters" class="primary-btn">Apply</button>
</div>
</div>
</div>
<!-- Analytics Dialog -->
<div class="dialog-overlay" id="analytics-dialog" style="display: none;">
<div class="dialog">
<div class="dialog-header">
<h2>Logger Analytics</h2>
<button class="close-btn" id="close-analytics"></button>
</div>
<div class="dialog-content">
<div id="analytics-content">
<!-- Analytics charts will be populated here -->
</div>
</div>
<div class="dialog-actions">
<button id="close-analytics-btn" class="secondary-btn">Close</button>
</div>
</div>
</div>
<!-- Log Detail Dialog -->
<div class="dialog-overlay" id="detail-dialog" style="display: none;">
<div class="dialog large">
<div class="dialog-header">
<h2>Log Details</h2>
<button class="close-btn" id="close-detail"></button>
</div>
<div class="dialog-content">
<div id="detail-content">
<!-- Log details will be populated here -->
</div>
</div>
<div class="dialog-actions">
<button id="copy-log" class="secondary-btn">Copy</button>
<button id="close-detail-btn" class="secondary-btn">Close</button>
</div>
</div>
</div>
<!-- Material-UI JavaScript -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@mui/material@latest/umd/material-ui.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js"></script>
<script src="script.js"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@ description: ente photos application
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.2.5+1205
version: 1.2.6+1205
publish_to: none
environment:

View File

@@ -1,3 +1,4 @@
- Neeraj: Fix lock error + improve logViewer
- Laurens: text embedding caching for memories and discover
- Neeraj: (i) Option to send qr for link
- Neeraj: (i) Debug option to enable logViewer

View File

@@ -69,7 +69,7 @@ class _LogFilterDialogState extends State<LogFilterDialog> {
level,
style: TextStyle(
color: isSelected ? Colors.white : color,
fontSize: 13,
fontSize: 9,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.w500,
),
),
@@ -79,9 +79,9 @@ class _LogFilterDialogState extends State<LogFilterDialog> {
checkmarkColor: Colors.white,
side: BorderSide(
color: isSelected ? color : color.withValues(alpha: 0.3),
width: isSelected ? 2 : 1,
width: isSelected ? 1.5 : 1,
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
onSelected: (selected) {
setState(() {
if (selected) {
@@ -103,7 +103,7 @@ class _LogFilterDialogState extends State<LogFilterDialog> {
borderRadius: BorderRadius.circular(16),
),
child: Container(
constraints: const BoxConstraints(maxWidth: 400, maxHeight: 600),
constraints: const BoxConstraints(maxWidth: 380, maxHeight: 500),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
),
@@ -112,7 +112,7 @@ class _LogFilterDialogState extends State<LogFilterDialog> {
children: [
// Header
Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: const BoxDecoration(
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
@@ -123,7 +123,7 @@ class _LogFilterDialogState extends State<LogFilterDialog> {
'Filter Logs',
style: theme.textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 20,
fontSize: 18,
),
),
IconButton(
@@ -140,7 +140,7 @@ class _LogFilterDialogState extends State<LogFilterDialog> {
Flexible(
child: SingleChildScrollView(
padding:
const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -149,19 +149,124 @@ class _LogFilterDialogState extends State<LogFilterDialog> {
'Log Levels',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 16,
fontSize: 14,
),
),
const SizedBox(height: 12),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
runSpacing: 3,
children: LogLevels.all
.where((level) => level != 'ALL' && level != 'OFF')
.map(_buildLevelChip)
.toList(),
),
const SizedBox(height: 20),
const SizedBox(height: 16),
// Process Prefixes
if (widget.availableProcesses.isNotEmpty) ...[
Text(
'Process',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 15,
),
),
const SizedBox(height: 8),
Container(
constraints: const BoxConstraints(maxHeight: 120),
decoration: BoxDecoration(
border: Border.all(
color: theme.dividerColor.withValues(alpha: 0.5),
width: 1,
),
borderRadius: BorderRadius.circular(6),
color: theme.cardColor,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: ListView.separated(
shrinkWrap: true,
itemCount: widget.availableProcesses.length,
separatorBuilder: (context, index) => Divider(
height: 1,
thickness: 0.5,
color: theme.dividerColor.withValues(alpha: 0.3),
),
itemBuilder: (context, index) {
final process = widget.availableProcesses[index];
final isSelected =
_selectedProcesses.contains(process);
final displayName = LogEntry(
message: '',
level: 'INFO',
timestamp: DateTime.now(),
loggerName: '',
processPrefix: process,
).processDisplayName;
return InkWell(
onTap: () {
setState(() {
if (isSelected) {
_selectedProcesses.remove(process);
} else {
_selectedProcesses.add(process);
}
});
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
child: Row(
children: [
Expanded(
child: Text(
displayName,
style: TextStyle(
fontSize: 13,
color: isSelected
? theme.primaryColor
: theme
.textTheme.bodyLarge?.color,
fontWeight: isSelected
? FontWeight.w500
: FontWeight.normal,
),
overflow: TextOverflow.ellipsis,
),
),
SizedBox(
width: 20,
height: 20,
child: Checkbox(
value: isSelected,
onChanged: (selected) {
setState(() {
if (selected == true) {
_selectedProcesses.add(process);
} else {
_selectedProcesses
.remove(process);
}
});
},
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
),
),
],
),
),
);
},
),
),
),
const SizedBox(height: 16),
],
// Loggers
if (widget.availableLoggers.isNotEmpty) ...[
@@ -169,22 +274,22 @@ class _LogFilterDialogState extends State<LogFilterDialog> {
'Loggers',
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
fontSize: 16,
fontSize: 15,
),
),
const SizedBox(height: 12),
const SizedBox(height: 8),
Container(
constraints: const BoxConstraints(maxHeight: 180),
constraints: const BoxConstraints(maxHeight: 120),
decoration: BoxDecoration(
border: Border.all(
color: theme.dividerColor.withValues(alpha: 0.5),
width: 1,
),
borderRadius: BorderRadius.circular(8),
borderRadius: BorderRadius.circular(6),
color: theme.cardColor,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
borderRadius: BorderRadius.circular(6),
child: ListView.separated(
shrinkWrap: true,
itemCount: widget.availableLoggers.length,
@@ -209,8 +314,8 @@ class _LogFilterDialogState extends State<LogFilterDialog> {
},
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
horizontal: 12,
vertical: 8,
),
child: Row(
children: [
@@ -218,7 +323,7 @@ class _LogFilterDialogState extends State<LogFilterDialog> {
child: Text(
logger,
style: TextStyle(
fontSize: 14,
fontSize: 13,
color: isSelected
? theme.primaryColor
: theme
@@ -231,8 +336,8 @@ class _LogFilterDialogState extends State<LogFilterDialog> {
),
),
SizedBox(
width: 24,
height: 24,
width: 20,
height: 20,
child: Checkbox(
value: isSelected,
onChanged: (selected) {
@@ -256,7 +361,7 @@ class _LogFilterDialogState extends State<LogFilterDialog> {
),
),
),
const SizedBox(height: 20),
const SizedBox(height: 16),
],
],
),
@@ -265,7 +370,7 @@ class _LogFilterDialogState extends State<LogFilterDialog> {
// Actions
Container(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border(
top: BorderSide(
@@ -284,8 +389,8 @@ class _LogFilterDialogState extends State<LogFilterDialog> {
style: TextButton.styleFrom(
foregroundColor: theme.colorScheme.error,
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 10,
horizontal: 12,
vertical: 8,
),
),
child: const Text(
@@ -299,8 +404,8 @@ class _LogFilterDialogState extends State<LogFilterDialog> {
onPressed: () => Navigator.pop(context),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 10,
horizontal: 16,
vertical: 8,
),
),
child: Text(
@@ -311,13 +416,13 @@ class _LogFilterDialogState extends State<LogFilterDialog> {
),
),
),
const SizedBox(width: 12),
const SizedBox(width: 8),
FilledButton(
onPressed: _applyFilters,
style: FilledButton.styleFrom(
padding: const EdgeInsets.symmetric(
horizontal: 24,
vertical: 10,
horizontal: 20,
vertical: 8,
),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),

View File

@@ -34,7 +34,7 @@ type UpdatePublicAccessTokenRequest struct {
func (ut *UpdatePublicAccessTokenRequest) Validate() error {
if ut.DeviceLimit == nil && ut.ValidTill == nil && ut.DisablePassword == nil &&
ut.Nonce == nil && ut.PassHash == nil && ut.EnableDownload == nil && ut.EnableCollect == nil {
ut.Nonce == nil && ut.PassHash == nil && ut.EnableDownload == nil && ut.EnableCollect == nil && ut.EnableJoin == nil {
return NewBadRequestWithMessage("all parameters are missing")
}