Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
#!/usr/bin/env bash
set -euo pipefail

repo_root=$(git rev-parse --show-toplevel)
local_home="$repo_root/.dart_tool_home"
mkdir -p "$local_home"

staged_files=()
while IFS= read -r -d '' file; do
staged_files+=("$file")
done < <(git diff --cached --name-only -z --diff-filter=ACMR)

if command -v fvm >/dev/null 2>&1; then
"$repo_root/scripts/sync_readme_version.sh"

local_dart="$repo_root/.fvm/flutter_sdk/bin/dart"
if [[ -x "$local_dart" ]]; then
HOME="$local_home" DART_DISABLE_TELEMETRY=1 "$local_dart" format .
elif command -v fvm >/dev/null 2>&1; then
DART_DISABLE_TELEMETRY=1 fvm dart format .
else
DART_DISABLE_TELEMETRY=1 dart format .
HOME="$local_home" DART_DISABLE_TELEMETRY=1 dart format .
fi

if ((${#staged_files[@]})); then
git add -- "${staged_files[@]}"
fi

if ! git diff --quiet -- README.md; then
git add README.md
fi
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ jobs:
- name: Check formatting
run: dart format --output=none --set-exit-if-changed .

- name: Check README version
run: bash scripts/sync_readme_version.sh --check

- name: Run analysis
run: flutter analyze

Expand All @@ -55,6 +58,9 @@ jobs:
if: steps.changes.outputs.code == 'true'
run: flutter test test/golden_test.dart

- name: Pub publish dry run
run: dart pub publish --dry-run

- name: Install pana
run: |
dart pub global activate pana
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ migrate_working_dir/
/pubspec.lock
**/doc/api/
.dart_tool/
.dart_tool_home/
.packages
build/

# FVM Version Cache
.fvm/
example/macos/Flutter/GeneratedPluginRegistrant.swift
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
# Changelog

## 1.0.24

* **Packaging:**
* Removed a gitignored tracked file warning for `dart pub publish --dry-run`.

## 1.0.23

* **Static analysis:**
* Switched shader uniform color components to `.r/.g/.b` and replaced deprecated color value usage.
* Updated the example app/test to avoid deprecated `SpoilerText`.
* Added file-level ignores where legacy config fields are intentionally exercised.
* **Docs:**
* Refreshed README examples and configuration tables for current APIs.
* Added missing dartdoc comments across the public API.
* **CI:**
* Added `flutter analyze` for the package and example app.
* Added `dart pub publish --dry-run` for publish validation.

## 1.0.22

Expand Down
106 changes: 60 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Spoiler Animation for Flutter

![Spoiler Widget](https://github.com/zhukeev/spoiler_widget/blob/main/assets/spoiler_widget.jpg)

![Pub Likes](https://img.shields.io/pub/likes/spoiler_widget)
![Pub Points](https://img.shields.io/pub/points/spoiler_widget)
[![Pub Version](https://img.shields.io/pub/v/spoiler_widget.svg)](https://pub.dev/packages/spoiler_widget)
Expand Down Expand Up @@ -42,7 +44,7 @@ In your `pubspec.yaml`:

```yaml
dependencies:
spoiler_widget: latest
spoiler_widget: ^1.0.24
```

Then run:
Expand All @@ -63,38 +65,40 @@ Import the package:
import 'package:spoiler_widget/spoiler_widget.dart';
```

Wrap **text** or **widgets** you want to hide in a spoiler:
Wrap any widget you want to hide in a spoiler:

```dart
SpoilerOverlay(
config: WidgetSpoilerConfig(
isEnabled: true,
fadeConfig: FadeConfig(radius: 3.0),
fadeConfig: const FadeConfig(padding: 3.0, edgeThickness: 20.0),
enableGestureReveal: true,
imageFilter: ImageFilter.blur(sigmaX:30, sigmaY:30),
imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 30),
onSpoilerVisibilityChanged: (isVisible) {
debugPrint('Spoiler is now: ${isVisible ? 'Visible' : 'Hidden'}');
},
),
child: Text('Hidden Content'),
child: const Text('Hidden content'),
);

```

Or use the text-specific widget:
For text-only content, use `SpoilerTextWrapper` (preferred over deprecated `SpoilerText`):

```dart
SpoilerText(
text: 'Tap me to reveal secret text!',
SpoilerTextWrapper(
config: TextSpoilerConfig(
isEnabled: true,
fadeConfig: FadeConfig(radius: 3.0),
fadeConfig: const FadeConfig(padding: 3.0, edgeThickness: 20.0),
enableGestureReveal: true,
textStyle: TextStyle(fontSize: 16, color: Colors.black),
onSpoilerVisibilityChanged: (isVisible) {
debugPrint('Spoiler is now: ${isVisible ? 'Visible' : 'Hidden'}');
},
),
child: const Text(
'Tap me to reveal secret text!',
style: TextStyle(fontSize: 16, color: Colors.black),
),
);

```
Expand All @@ -105,9 +109,9 @@ If you already have a text subtree and just need to hide it with particles, use

```dart
SpoilerTextWrapper(
config: SpoilerConfig(
config: TextSpoilerConfig(
isEnabled: true,
fadeConfig: const FadeConfig(),
fadeConfig: const FadeConfig(padding: 10.0, edgeThickness: 20.0),
enableGestureReveal: true,
),
child: Column(
Expand All @@ -122,17 +126,17 @@ SpoilerTextWrapper(

### 1.2 Form field integration

Use `SpoilerTextFormField` to keep the native context menu/cursor while hiding parts of the input:
Use `SpoilerTextFieldWrapper` to keep the native context menu/cursor while hiding parts of the input:

```dart
final controller = TextEditingController(text: 'Type here...');

SpoilerTextFormField(
SpoilerTextFieldWrapper(
controller: controller,
focusNode: FocusNode(),
config: const TextSpoilerConfig(
isEnabled: true,
fadeConfig: FadeConfig(),
fadeConfig: FadeConfig(padding: 10.0, edgeThickness: 20.0),
enableGestureReveal: true,
textSelection: TextSelection(baseOffset: 0, extentOffset: 5),
textStyle: TextStyle(fontSize: 18, color: Colors.white),
Expand All @@ -154,9 +158,9 @@ class WaveDemo extends StatelessWidget {
config: WidgetSpoilerConfig(
isEnabled: true,
maxActiveWaves: 3,
fadeConfig: const FadeConfig(),
fadeConfig: const FadeConfig(padding: 10.0, edgeThickness: 20.0),
enableGestureReveal: true,
imageFilter: ImageFilter.blur(sigmaX:30, sigmaY:20),
imageFilter: ImageFilter.blur(sigmaX: 30, sigmaY: 20),
onSpoilerVisibilityChanged: (isVisible) {
debugPrint('Spoiler is now: ${isVisible ? 'Visible' : 'Hidden'}');
},
Expand All @@ -174,15 +178,16 @@ You can apply a custom-shaped mask using `maskConfig` in both `TextSpoilerConfig
This allows the spoiler effect to only appear inside specific areas defined by a `Path`.

```dart
SpoilerText(
text: 'Masked spoiler!',
SpoilerTextWrapper(
config: TextSpoilerConfig(
isEnabled: true,
enableGestureReveal: true,
particleConfig: const ParticleConfig(
density: 0.1,
speed: 0.2,
color: Colors.white,
maxParticleSize: 1.0,
),
textStyle: TextStyle(fontSize: 24, color: Colors.white),
maskConfig: SpoilerMask(
maskPath: myCustomPath,
maskOperation: PathOperation.intersect,
Expand All @@ -192,6 +197,10 @@ SpoilerText(
debugPrint('Spoiler is now: ${isVisible ? 'Visible' : 'Hidden'}');
},
),
child: const Text(
'Masked spoiler!',
style: TextStyle(fontSize: 24, color: Colors.white),
),
);

```
Expand All @@ -204,24 +213,29 @@ You can find a complete, runnable example in the [example/lib/main.dart](https:/

##### Common Fields

Table showing common config parameters for both TextSpoilerConfiguration and WidgetSpoilerConfiguration.

| Field | Type | Description |
|------------------|-----------------|--------------------------------------------------------------|
| `isEnabled` | bool | Whether the spoiler starts covered `true`. |
| `enableFadeAnimation` | bool | Enables smooth fade-in/out.<br/>Deprecated, use `fadeConfig`.|
| `fadeRadius` | double | Deprecated, use `fadeConfig`. |
| `particleDensity`| double | Deprecated, use `particleConfig`. |
| `maxParticleSize`| double | Deprecated, use `particleConfig`. |
| `particleSpeed` | double | Deprecated, use `particleConfig`. |
| `particleColor` | Color | Deprecated, use `particleConfig`. |
| `fadeEdgeThickness` | double | Deprecated, use `fadeConfig`. |
| `particleConfig` | ParticleConfig? | Particle system parameters (density, speed, color, size, shape paths). |
| `fadeConfig` | FadeConfig? | Fade animation parameters (radius, edge thickness). |
| `shaderConfig` | ShaderConfig? | Custom fragment shader configuration for particles. |
| `enableGestureReveal` | bool | Whether tapped toggle should be out of the box. |
| `maskConfig` | SpoilerMask? | Optional mask to apply using a `Path`. |
| `onSpoilerVisibilityChanged` | ValueChanged? | Optional callback fired when spoiler becomes visible/hidden. |
Table showing common config parameters for both TextSpoilerConfig and WidgetSpoilerConfig.

| Field | Type | Description |
|------------------|----------------------|--------------------------------------------------------------|
| `isEnabled` | bool | Whether the spoiler starts enabled (hidden). |
| `enableGestureReveal` | bool | Whether tap should toggle visibility. |
| `particleConfig` | ParticleConfig | Particle system parameters (density, speed, color, size, shape). |
| `fadeConfig` | FadeConfig? | Fade parameters (`padding`, `edgeThickness`). |
| `shaderConfig` | ShaderConfig? | Custom fragment shader configuration for particles. |
| `maskConfig` | SpoilerMask? | Optional mask to apply using a `Path`. |
| `onSpoilerVisibilityChanged` | ValueChanged<bool>? | Callback fired when spoiler becomes visible/hidden. |

##### Legacy fields (deprecated)

These fields are kept for backward compatibility; prefer `particleConfig` and `fadeConfig`.

- `enableFadeAnimation`
- `fadeRadius` (maps to `FadeConfig.padding`)
- `fadeEdgeThickness`
- `particleDensity`
- `particleSpeed`
- `particleColor`
- `maxParticleSize`

#### ParticleConfig

Expand All @@ -231,13 +245,12 @@ Table showing common config parameters for both TextSpoilerConfiguration and Wid
| `speed` | double | Particle speed (px/frame). |
| `color` | Color | Base particle color. |
| `maxParticleSize` | double | Particle diameter in pixels. |
| `shapePreset` | ParticlePathPreset? | Built-in shapes (circle, star, snowflake). Defaults to circle. |
| `shapePath` | Path? | Optional custom particle path. Use `ParticlePathPreset` for built-ins; atlas draws the path and shader uses a sprite sampler. |
| `shapePreset` | ParticlePathPreset? | Built-in shapes or `ParticlePathPreset.custom(...)` for your own path. |
| `enableWaves` | bool | Enables ripple waves that push particles. |
| `maxWaveRadius` | double | Wave radius limit in pixels. |
| `maxWaveCount` | int | Maximum number of simultaneous waves. |

#### TextSpoilerConfiguration
#### TextSpoilerConfig

| Field | Type | Description |
|------------|---------------|---------------------------------------------|
Expand All @@ -247,7 +260,7 @@ Table showing common config parameters for both TextSpoilerConfiguration and Wid
| `maxLines` | int? | An optional maximum number of lines for the text to span, wrapping if necessary. |
| `isEllipsis` | bool? | Determines whether overflowing text should display an ellipsis ("…") at the end. |

#### WidgetSpoilerConfiguration
#### WidgetSpoilerConfig

| Field | Type | Description |
|-----------------|-------------|----------------------------------------------|
Expand Down Expand Up @@ -276,7 +289,7 @@ To enable **Shader Rendering**, simply provide a `ShaderConfig` to your spoiler

### Particle Shapes

Pick a shape once in `ParticleConfig`, and it works for both atlas and shader modes. Leave `shapePath` null for the default circle (fastest path).
Pick a shape once in `ParticleConfig`, and it works for both atlas and shader modes. Leave `shapePreset` null for the default circle (fastest path).

```dart
particleConfig: ParticleConfig(
Expand All @@ -303,7 +316,7 @@ final starPath = Path()

particleConfig: ParticleConfig(
density: 0.1,
shapePath: starPath,
shapePreset: ParticlePathPreset.custom(starPath),
),
```

Expand All @@ -313,13 +326,14 @@ You can use the default high-performance shader for particle rendering:

```dart
SpoilerTextWrapper(
config: SpoilerConfig(
config: TextSpoilerConfig(
isEnabled: true,
enableGestureReveal: true,
particleConfig: ParticleConfig(
particleConfig: const ParticleConfig(
density: 0.15,
speed: 0.25,
color: Colors.white,
maxParticleSize: 1.0,
shapePreset: ParticlePathPreset.star,
),
shaderConfig: ShaderConfig.particles(),
Expand Down
5 changes: 5 additions & 0 deletions lib/extension/rect_x.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:math';

import 'package:flutter/rendering.dart';

/// Geometry helpers for [Rect].
extension RectX on Rect {
/// Check if {offset} is inside the rectangle
///
Expand All @@ -20,6 +21,7 @@ extension RectX on Rect {
right >= offset.dx;
}

/// Splits the rect into four equal quadrants.
(Rect, Rect, Rect, Rect) divideRect() {
final halfWidth = width / 2;
final halfHeight = height / 2;
Expand Down Expand Up @@ -85,7 +87,9 @@ extension RectX on Rect {
}
}

/// Collection helpers for lists of [Rect].
extension ListRectX on List<Rect> {
/// Returns a bounding rect covering all items.
Rect getBounds() {
if (isEmpty) {
return Rect.zero;
Expand Down Expand Up @@ -117,6 +121,7 @@ extension ListRectX on List<Rect> {
return Rect.fromLTRB(left, top, right, bottom);
}

/// Merges adjacent rects that touch horizontally.
List<Rect> mergeRects() {
final merged = <Rect>[];

Expand Down
4 changes: 4 additions & 0 deletions lib/models/fade_config.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
part of 'spoiler_configs.dart';

/// Configuration for spoiler fade behavior.
@immutable
class FadeConfig {
/// Padding used to expand the reveal/cover area.
final double padding;

/// Thickness of the fade edge band.
final double edgeThickness;

const FadeConfig({
Expand Down
Loading