Posts in category “Programming”

Desktop Mouse Swipe Delete Troubleshooting

Problem

Mouse left swipe to delete notes was not working on desktop platforms (Windows, macOS, Linux, Web browsers).

Root Cause Analysis

  1. Flutter Dismissible Limitations: Dismissible widget is optimized for touch interactions, not mouse gestures
  2. SelectionArea Gesture Conflict: SelectionArea intercepted mouse drag events for text selection, blocking Dismissible swipe gestures

Solution

Initial Approach (Failed)

Attempted platform-specific custom GestureDetector implementation:

  • Added desktop platform detection logic
  • Implemented custom mouse drag threshold calculations
  • Issue: GestureDetector only detects gestures but provides no visual feedback

Final Solution (Working)

Two-part fix for Dismissible widget:

  1. Immediate Mouse Response
Dismissible(
  dragStartBehavior: DragStartBehavior.down, // Key fix
  // ... other properties
)
  1. Gesture Conflict Resolution
// Before: SelectionArea wrapping entire content
SelectionArea(
  child: GestureDetector(...) // Blocked swipe gestures
)

// After: SelectionArea only around text content
GestureDetector(
  child: Column([
    metadata,
    SelectionArea(child: noteContent), // Limited scope
    footer,
  ])
)

Technical Details

dragStartBehavior Impact

  • DragStartBehavior.start (default): Waits for drag distance threshold
  • DragStartBehavior.down: Starts drag immediately on mouse down
  • Desktop users expect immediate response to mouse actions

SelectionArea Scope Reduction

  • Problem: Full-content SelectionArea captured all mouse events
  • Solution: Limit SelectionArea to text content only
  • Result: Swipe gestures work on margins, text selection works on content

Testing Strategy

Created comprehensive test suite covering:

  • Dismissible configuration validation
  • Platform-specific behavior
  • Gesture conflict scenarios
  • Integration with existing callbacks

Key Learnings

  1. Flutter's Dismissible supports desktop with proper configuration
  2. Widget event hierarchies can cause unexpected gesture conflicts
  3. Scope reduction often beats complex custom implementations
  4. Platform-specific UX requires careful gesture management

Code Impact

  • Files Modified: note_list_item.dart
  • Tests Added: note_list_item_test.dart
  • Lines Changed: 30 additions, 15 deletions
  • Breaking Changes: None

Verification

  • All tests pass
  • Code analysis clean
  • Desktop mouse swipe delete functional
  • Text selection preserved

Flutter iOS Safari Double Selection Issue - Technical Workaround

Problem

Flutter web apps on iOS Safari exhibit a "double selection" bug where text selection creates two overlapping selection layers, causing visual artifacts and interaction issues.

Root Cause

iOS Safari creates both native browser text selection AND Flutter's custom SelectionArea selection simultaneously, resulting in conflicting selection states.

Working Workaround

HTML Solution (web/index.html)

<head>
<style>
  * {
	-webkit-user-select: none;
	-moz-user-select: none;
	-ms-user-select: none;
	user-select: none;
	/* Disable caret to prevent selection artifacts */
	caret-color: rgba(255, 255, 255, 0) !important;
  }
</style>
</head>
<body oncontextmenu="event.preventDefault();" >
...

Flutter Integration

// In main.dart or app initialization
import 'package:flutter/gestures.dart';

void main() {
    // Disable browser context menu to let Flutter handle selection
    if (kIsWeb) {
        BrowserContextMenu.disableContextMenu();
    }
    runApp(MyApp());
}

How It Works

  1. Disables native selection - user-select: none prevents Safari's text selection
  2. Blocks touch events - Prevents iOS touch selection gestures
  3. Maintains Flutter selection - BrowserContextMenu.disableContextMenu() allows Flutter's SelectionArea to work
  4. Hides caret artifacts - caret-color: transparent eliminates visual glitches

Trade-offs

  • ❌ Breaks native web text selection outside Flutter widgets
  • ❌ May affect accessibility tools
  • ✅ Provides consistent cross-platform selection behavior
  • ✅ Eliminates iOS-specific double selection bug

Status

This workaround is recommended for Flutter web apps requiring text selection on iOS Safari until the official framework fix is released.

Solving Flutter Web Image Rendering Issues: A Cross-Platform Approach

If you've ever tried to display images in a Flutter app that needs to work seamlessly across web and mobile platforms, you've probably run into some frustrating limitations. Recently, I tackled this exact problem when our markdown image renderer started choking on web deployments due to CORS restrictions and lack of proper zoom functionality.

The Problem

Our original implementation was painfully simple - just Image.network(src) wrapped in a GestureDetector. This worked fine on mobile, or on web with --web-renderer html. However, the HTML web renderer is obsolete and will be removed shortly. We needed a more reliable solution.

The Solution: Platform-Specific Implementations

The key insight was to leverage Flutter's conditional imports to create platform-specific implementations while maintaining a clean, unified API.

Setting Up Conditional Imports

// Conditional imports for web
import 'web_image_stub.dart'
    if (dart.library.html) 'web_image_impl.dart';

This pattern lets you have different implementations for web vs mobile while keeping your main code clean. The stub file handles non-web platforms, while the implementation file contains the web-specific logic.

Web Implementation: HTML Elements to the Rescue

For web, I ditched Flutter's built-in image widgets entirely and went straight to HTML elements using HtmlElementView. This bypasses CORS issues since the browser handles the image loading directly.

final imgElement = html.ImageElement()
  ..src = src
  ..style.width = '100%'
  ..style.objectFit = 'contain'
  ..style.cursor = 'pointer';

The magic happens when you register this as a platform view. Flutter treats it like any other widget, but under the hood, it's pure HTML - which means it plays nicely with browser security policies.

Adding Zoom Functionality

The fullscreen implementation includes both mouse wheel and touch gesture zoom:

// Mouse wheel zoom
imgElement.onWheel.listen((event) {
  event.preventDefault();
  scale += event.deltaY > 0 ? -0.1 : 0.1;
  scale = scale.clamp(0.5, 3.0);
  imgElement.style.transform = 'scale($scale)';
});

For touch devices, I implemented pinch-to-zoom by tracking multiple touch points and calculating the distance between them. It's more complex than the mouse wheel version, but it gives web users the same intuitive zoom experience they expect.

Mobile Implementation: Keep It Simple

For mobile platforms, I stuck with the tried-and-true approach but improved the UX:

Dialog.fullscreen(
  backgroundColor: Colors.black,
  child: Stack(
    children: [
      Center(
        child: PhotoView(
          imageProvider: NetworkImage(url),
          minScale: PhotoViewComputedScale.contained,
          maxScale: PhotoViewComputedScale.covered * 4,
        ),
      ),
      // Close button positioned in top-right
    ],
  ),
)

The key improvements were switching to Dialog.fullscreen instead of a regular dialog and adding a proper close button with consistent styling.

Key Takeaways

  1. Conditional imports are your friend - They let you maintain clean separation between platform-specific code without cluttering your main logic.

  2. HTML elements can solve web-specific problems - When Flutter widgets don't cut it on web, dropping down to HTML often provides better browser compatibility.

  3. Consistent UX matters - Users expect zoom functionality on images, especially in fullscreen mode. Don't skimp on these details.

  4. Don't fight the platform - Web and mobile have different strengths. Embrace them instead of trying to force a one-size-fits-all solution.

The Result

After implementing these changes, our image handling works consistently across platforms. Web users get smooth zoom functionality without CORS headaches, mobile users get the native experience they expect, and the codebase remains maintainable with clear separation of concerns.

Sometimes the best solution isn't the most elegant one - it's the one that actually works for your users across all the platforms they're using.

Introducing changesummary: A Git Change Summary Script

The changesummary script is a powerful tool for developers to quickly understand the key changes made between two Git commits. It leverages AI to analyze the diff and provide a concise summary of the modifications.

Functionality

The script takes one or two arguments: the start commit hash and an optional end commit hash. If the end commit hash is not provided, it defaults to HEAD.

Usage

To use changesummary, simply run it in your Git Bash terminal:

./changesummary <start_commit_hash> [<end_commit_hash>]

Example

./changesummary abc123 def456

Result:

The key changes between the specified commit hashes are:

* Renamed `TrmWithRiaSettlementPricingTask` to `TrmWithRiaDetailsPricingTask` and refactored its logic into a base class `TrmConfigPricingTaskBase`.
* Removed `RiaRawRateDataDecorator` and updated `TrmWithRiaDetailsPricingTask` to use `RiaRate` instead of `RiaRawRate`.
* Updated the `RequiredData` enum to remove `RiaRawRate` and updated the `TrmWithRiaDetailsPricingTask` to require `RiaRate`.
* Updated error codes and messages to reflect the changes.

These changes simplify the pricing task logic and improve maintainability.

Benefits

  • Provides a quick and meaningful summary of changes, saving time during code reviews.
  • Helps in understanding the impact of changes made between commits.
  • Easy to integrate into existing Git workflows.
  • Flexible comparison range with optional end commit hash.

By using changesummary, developers can streamline their code review process and focus on the most important changes.

here's the source code:

#!/bin/bash

# Check if commit hash is provided
if [ -z "$1" ]; then
    echo "Error: Commit hash is required as an argument."
    exit 1
fi

convert_to_uppercase() {
    # Enable case-insensitive matching
    shopt -s nocasematch
    
    if [[ $1 =~ ^(HEAD|FETCH_HEAD|ORIG_HEAD|MERGE_HEAD)(\~[0-9]+|\^[0-9]*)* ]]; then
        # Convert to uppercase
        echo "${1^^}"
    else
        # Return original string
        echo "$1"
    fi
    
    # Reset case-sensitivity to default
    shopt -u nocasematch
}

start_hash=$(convert_to_uppercase "$1")
end_hash=$(convert_to_uppercase "${2:-HEAD}")

# Define static prompt text
static_prompt=$(cat <<-END
Analyze the following code diff. Generate a concise summary (under 100 words) of the **key changes** made between the specified commit hashes. Present the changes in a bullet-point list format, focusing on the main modifications and their impact.
Code changes:
END
)

# Define model and system message variables
model="meta-llama/llama-4-maverick:free"
system_message="You are a programmer"

# Execute git diff and pipe its output to the AI model
git diff -w -b $start_hash..$end_hash | jq -R -s --arg model "$model" --arg system_content "$system_message" --arg static_prompt "$static_prompt" \
    '{
        model: $model,
        messages: [
            {role: "system", content: $system_content},
            {role: "user", content: ($static_prompt + .) }
        ],
        max_tokens: 16384,
        temperature: 0
    }' | curl -s --request POST \
        --url https://openrouter.ai/api/v1/chat/completions \
        --header "Authorization: Bearer $OR_FOR_CI_API_KEY" \
        --header "Content-Type: application/json" \
        --data-binary @- | jq -r '.choices[0].message.content'

利用正则表达式精准匹配 Markdown 中的 Tag

在 Markdown 文档中,我们经常会使用 # 来标记标签(tag)。例如,#todo#feature 都可以作为标记嵌入文本中。然而,在 Markdown 中还存在另一种情况:代码块。代码块通常由单个或连续三个反引号(```)包裹,并且代码块内部可能会出现类似 #ff0000 这样的颜色值。如果不加以区分,就会把这些颜色值也识别为标签。

此外,为了避免误判,转义的反引号(\`)也不应被当作真正的分界符。为此,我们需要设计一个既能排除转义反引号,又能将连续的三个反引号作为一个整体处理的正则表达式。

挑战与需求

  • 处理代码块边界:Markdown 支持用单个反引号表示行内代码、用三个反引号表示多行代码块。正则表达式需要区分这两种情况,确保在代码块中的 # 不会被误认为标签前缀。
  • 转义字符的处理:例如 \` 这样的转义反引号不应计入反引号的配对判断中。
  • Tag 匹配规则:标签由 # 开头,后跟 1 至 32 个 Unicode 字母、数字或下划线,且必须在正确的边界下出现。

正则表达式解决方案

经过多次改进,我们最终得到了下面这个正则表达式:

(?=(?:(?:[^`]|\\`)*(?<!\\)(?<delim>```|`)(?:[^`]|\\`)*(?<!\\)(\k<delim>))*(?:[^`]|\\`)*$)(?<=(?:^|[^\\])#)[\p{L}_\p{N}]{1,32}(?=[^\p{L}\p{N}_]|$)

让我们逐步解析这条表达式的关键部分:

1. 保证反引号成对出现

表达式的开头部分使用了复杂的正向前瞻:

(?=(?:(?:[^`]|\\`)*(?<!\\)(?<delim>```|`)(?:[^`]|\\`)*(?<!\\)(\k<delim>))*(?:[^`]|\\`)*$)
  • 核心思想:通过匹配一系列非反引号或转义反引号的字符,再加上捕获组 (?<delim>```|)`,我们捕获了第一次出现的“分界符”——它可能是单个反引号,也可能是连续三个反引号。
  • 反向引用:利用 \k<delim>,确保后续匹配的分界符与前面捕获到的完全一致。这样,无论遇到的是行内代码还是代码块,都只视作一个整体。
  • 成对匹配:整个正向前瞻确保了目标区域内所有未转义的反引号都是成对出现的,避免了误将代码块内部的内容当作标签判断依据。

2. 确保标签前缀正确

接下来的部分:

(?<=(?:^|[^\\])#)
  • 作用:这个正向后查断言确保了标签必须由一个 # 开头,而这个 # 不能被反斜杠转义(即不应为 \#)。

3. 匹配 Tag 内容

标签的主体部分由下面这段完成:

[\p{L}_\p{N}]{1,32}(?=[^\p{L}\p{N}_]|$)
  • 匹配范围:这里 [\\p{L}_\\p{N}] 表示 Unicode 字母、数字以及下划线。{1,32} 限定了标签的长度为 1 至 32 个字符。
  • 边界条件:紧跟的正向查找断言 (?=[^\p{L}\p{N}_]|$) 确保标签后面紧跟的是非单词字符或已经到达字符串末尾,防止匹配到类似 #todoing 这种不完整的标签。

小结

这条正则表达式综合了多种复杂情况,既能处理 Markdown 中单反引号和三反引号的代码块,又能排除转义反引号的干扰,同时严格匹配标签格式。对于开发者来说,这样的表达式在解析 Markdown 文档、提取标签或进行格式化处理时非常有用。

当然,正则表达式的可读性和维护性是一个平衡点。虽然这条表达式在处理边界情况时表现出色,但在实际应用中可能还需要根据具体场景做出微调。希望这篇文章能为你在 Markdown 文档解析方面提供一些灵感和帮助。