Fix video controls, tag input and collection deletion
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
#define MyAppName "RuleGel"
|
#define MyAppName "RuleGel"
|
||||||
#define MyAppVersion "0.2.0"
|
#define MyAppVersion "0.4.3"
|
||||||
#define MyAppPublisher "RarDog"
|
#define MyAppPublisher "RarDog"
|
||||||
#define MyAppExeName "gel_rule_app.exe"
|
#define MyAppExeName "gel_rule_app.exe"
|
||||||
|
|
||||||
@@ -32,9 +32,13 @@ Name: "desktopicon"; Description: "Create a desktop shortcut"; GroupDescription:
|
|||||||
[Files]
|
[Files]
|
||||||
Source: "..\build\windows\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: "*.lnk"
|
Source: "..\build\windows\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs; Excludes: "*.lnk"
|
||||||
|
|
||||||
|
[InstallDelete]
|
||||||
|
Type: files; Name: "{group}\{#MyAppName}.lnk"
|
||||||
|
Type: files; Name: "{autodesktop}\{#MyAppName}.lnk"
|
||||||
|
|
||||||
[Icons]
|
[Icons]
|
||||||
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\{#MyAppExeName}"
|
||||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
Filename: "{app}\{#MyAppExeName}"; Description: "Launch {#MyAppName}"; Flags: nowait postinstall skipifsilent
|
Filename: "{app}\{#MyAppExeName}"; Description: "Launch {#MyAppName}"; Flags: nowait postinstall skipifsilent
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
const appDisplayVersion = '0.4.2';
|
const appDisplayVersion = '0.4.3';
|
||||||
const appBuildNumber = 6;
|
const appBuildNumber = 7;
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class CollectionRepository {
|
|||||||
.filter()
|
.filter()
|
||||||
.collectionIdEqualTo(id)
|
.collectionIdEqualTo(id)
|
||||||
.deleteAll();
|
.deleteAll();
|
||||||
await isar.collectionEntitys.filter().collectionIdEqualTo(id).deleteAll();
|
await isar.collectionEntitys.deleteByCollectionId(id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -347,6 +347,11 @@ class SettingsService {
|
|||||||
const JsonEncoder.withIndent(' ').convert({
|
const JsonEncoder.withIndent(' ').convert({
|
||||||
'schemaVersion': 2,
|
'schemaVersion': 2,
|
||||||
'settings': settings.toJson(),
|
'settings': settings.toJson(),
|
||||||
|
'filters': {
|
||||||
|
'blacklistedTags': settings.blacklistedTags,
|
||||||
|
'whitelistedTags': settings.whitelistedTags,
|
||||||
|
'smartBlacklistRules': settings.smartBlacklistRules,
|
||||||
|
},
|
||||||
'providers': providers.map((provider) => provider.toJson()).toList(),
|
'providers': providers.map((provider) => provider.toJson()).toList(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -357,6 +362,15 @@ class SettingsService {
|
|||||||
final decoded = jsonDecode(json) as Map<String, dynamic>;
|
final decoded = jsonDecode(json) as Map<String, dynamic>;
|
||||||
final settingsJson =
|
final settingsJson =
|
||||||
(decoded['settings'] as Map?)?.cast<String, dynamic>() ?? decoded;
|
(decoded['settings'] as Map?)?.cast<String, dynamic>() ?? decoded;
|
||||||
|
final filtersJson =
|
||||||
|
(decoded['filters'] as Map?)?.cast<String, dynamic>() ?? const {};
|
||||||
|
for (final key in [
|
||||||
|
'blacklistedTags',
|
||||||
|
'whitelistedTags',
|
||||||
|
'smartBlacklistRules',
|
||||||
|
]) {
|
||||||
|
settingsJson.putIfAbsent(key, () => filtersJson[key]);
|
||||||
|
}
|
||||||
final settings = AppSettings.fromJson(settingsJson);
|
final settings = AppSettings.fromJson(settingsJson);
|
||||||
final result = await updateSettings(settings);
|
final result = await updateSettings(settings);
|
||||||
if (result is Error<void>) return result;
|
if (result is Error<void>) return result;
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ class CollectionsController extends AsyncNotifier<List<Collection>> {
|
|||||||
|
|
||||||
Future<void> delete(String id) async {
|
Future<void> delete(String id) async {
|
||||||
await ref.read(collectionServiceProvider).deleteCollection(id);
|
await ref.read(collectionServiceProvider).deleteCollection(id);
|
||||||
|
ref.invalidate(collectionPostsProvider(id));
|
||||||
state = AsyncData(await _load());
|
state = AsyncData(await _load());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,6 +63,21 @@ class _FeedScreenState extends ConsumerState<FeedScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant FeedScreen oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
final nextQuery = widget.initialQuery;
|
||||||
|
if (nextQuery != null &&
|
||||||
|
nextQuery.isNotEmpty &&
|
||||||
|
nextQuery != oldWidget.initialQuery) {
|
||||||
|
_usedInitialQuery = true;
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (!mounted) return;
|
||||||
|
ref.read(feedControllerProvider.notifier).search(nextQuery);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_scrollSaveDebounce?.cancel();
|
_scrollSaveDebounce?.cancel();
|
||||||
|
|||||||
@@ -279,7 +279,14 @@ class _PostMediaViewerState extends State<PostMediaViewer>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _retryVideo() async {
|
Future<void> _retryVideo() async {
|
||||||
_videoIndex = 0;
|
final player = _player;
|
||||||
|
if (player == null) return;
|
||||||
|
setState(() {
|
||||||
|
_videoError = null;
|
||||||
|
_videoIndex = 0;
|
||||||
|
_controlsVisible = true;
|
||||||
|
});
|
||||||
|
await player.stop();
|
||||||
await _openVideo(play: true);
|
await _openVideo(play: true);
|
||||||
_showControls();
|
_showControls();
|
||||||
}
|
}
|
||||||
@@ -994,7 +1001,7 @@ class _VideoSurface extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (errorMessage != null)
|
if (errorMessage != null)
|
||||||
_VideoErrorOverlay(message: errorMessage!, onRetry: onRetry),
|
VideoErrorOverlay(message: errorMessage!, onRetry: onRetry),
|
||||||
if (errorMessage == null)
|
if (errorMessage == null)
|
||||||
StreamBuilder<bool>(
|
StreamBuilder<bool>(
|
||||||
stream: player.stream.playing,
|
stream: player.stream.playing,
|
||||||
@@ -1040,21 +1047,24 @@ class _VideoSurface extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
AnimatedOpacity(
|
IgnorePointer(
|
||||||
opacity: controlsVisible || errorMessage != null ? 1 : 0,
|
ignoring: !controlsVisible || errorMessage != null,
|
||||||
duration: const Duration(milliseconds: 180),
|
child: AnimatedOpacity(
|
||||||
child: _VideoControls(
|
opacity: controlsVisible || errorMessage != null ? 1 : 0,
|
||||||
player: player,
|
duration: const Duration(milliseconds: 180),
|
||||||
muted: muted,
|
child: _VideoControls(
|
||||||
halfVolume: halfVolume,
|
player: player,
|
||||||
loopVideo: loopVideo,
|
muted: muted,
|
||||||
coverVideo: coverVideo,
|
halfVolume: halfVolume,
|
||||||
fullscreen: fullscreen,
|
loopVideo: loopVideo,
|
||||||
onToggleFit: onToggleFit,
|
coverVideo: coverVideo,
|
||||||
onToggleMute: onToggleMute,
|
fullscreen: fullscreen,
|
||||||
onToggleHalfVolume: onToggleHalfVolume,
|
onToggleFit: onToggleFit,
|
||||||
onToggleLoop: onToggleLoop,
|
onToggleMute: onToggleMute,
|
||||||
onFullscreen: onFullscreen,
|
onToggleHalfVolume: onToggleHalfVolume,
|
||||||
|
onToggleLoop: onToggleLoop,
|
||||||
|
onFullscreen: onFullscreen,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -1080,10 +1090,11 @@ class _VideoSurface extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _VideoErrorOverlay extends StatelessWidget {
|
class VideoErrorOverlay extends StatelessWidget {
|
||||||
const _VideoErrorOverlay({
|
const VideoErrorOverlay({
|
||||||
required this.message,
|
required this.message,
|
||||||
required this.onRetry,
|
required this.onRetry,
|
||||||
|
super.key,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String message;
|
final String message;
|
||||||
@@ -1210,186 +1221,184 @@ class _VideoControls extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final scheme = Theme.of(context).colorScheme;
|
final scheme = Theme.of(context).colorScheme;
|
||||||
return Container(
|
return Align(
|
||||||
decoration: BoxDecoration(
|
alignment: Alignment.bottomCenter,
|
||||||
gradient: LinearGradient(
|
child: SafeArea(
|
||||||
begin: Alignment.bottomCenter,
|
minimum: EdgeInsets.fromLTRB(
|
||||||
end: Alignment.topCenter,
|
fullscreen ? 14 : 8,
|
||||||
colors: [
|
0,
|
||||||
Colors.black.withValues(alpha: 0.72),
|
fullscreen ? 14 : 8,
|
||||||
Colors.black.withValues(alpha: 0.12),
|
fullscreen ? 10 : 8,
|
||||||
Colors.transparent,
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
child: DecoratedBox(
|
||||||
child: Align(
|
decoration: BoxDecoration(
|
||||||
alignment: Alignment.bottomCenter,
|
color: Colors.black.withValues(alpha: 0.58),
|
||||||
child: SafeArea(
|
borderRadius: BorderRadius.circular(fullscreen ? 18 : 14),
|
||||||
minimum: EdgeInsets.fromLTRB(
|
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
|
||||||
fullscreen ? 14 : 8,
|
|
||||||
0,
|
|
||||||
fullscreen ? 14 : 8,
|
|
||||||
fullscreen ? 10 : 8,
|
|
||||||
),
|
),
|
||||||
child: StreamBuilder<bool>(
|
child: Padding(
|
||||||
stream: player.stream.playing,
|
padding: const EdgeInsets.fromLTRB(10, 4, 10, 8),
|
||||||
initialData: player.state.playing,
|
child: StreamBuilder<bool>(
|
||||||
builder: (context, playingSnapshot) {
|
stream: player.stream.playing,
|
||||||
final playing = playingSnapshot.data ?? false;
|
initialData: player.state.playing,
|
||||||
return StreamBuilder<Duration>(
|
builder: (context, playingSnapshot) {
|
||||||
stream: player.stream.duration,
|
final playing = playingSnapshot.data ?? false;
|
||||||
initialData: player.state.duration,
|
return StreamBuilder<Duration>(
|
||||||
builder: (context, durationSnapshot) {
|
stream: player.stream.duration,
|
||||||
final duration = durationSnapshot.data ?? Duration.zero;
|
initialData: player.state.duration,
|
||||||
return StreamBuilder<Duration>(
|
builder: (context, durationSnapshot) {
|
||||||
stream: player.stream.position,
|
final duration = durationSnapshot.data ?? Duration.zero;
|
||||||
initialData: player.state.position,
|
return StreamBuilder<Duration>(
|
||||||
builder: (context, positionSnapshot) {
|
stream: player.stream.position,
|
||||||
final position = positionSnapshot.data ?? Duration.zero;
|
initialData: player.state.position,
|
||||||
final maxMs =
|
builder: (context, positionSnapshot) {
|
||||||
duration.inMilliseconds.clamp(1, 1 << 31).toDouble();
|
final position = positionSnapshot.data ?? Duration.zero;
|
||||||
final valueMs = position.inMilliseconds
|
final maxMs = duration.inMilliseconds
|
||||||
.clamp(0, maxMs.toInt())
|
.clamp(1, 1 << 31)
|
||||||
.toDouble();
|
.toDouble();
|
||||||
return LayoutBuilder(
|
final valueMs = position.inMilliseconds
|
||||||
builder: (context, constraints) {
|
.clamp(0, maxMs.toInt())
|
||||||
final compact = constraints.maxWidth < 560;
|
.toDouble();
|
||||||
final tiny = constraints.maxWidth < 390;
|
return LayoutBuilder(
|
||||||
return Column(
|
builder: (context, constraints) {
|
||||||
mainAxisSize: MainAxisSize.min,
|
final compact = constraints.maxWidth < 560;
|
||||||
children: [
|
final tiny = constraints.maxWidth < 390;
|
||||||
SliderTheme(
|
return Column(
|
||||||
data: SliderTheme.of(context).copyWith(
|
mainAxisSize: MainAxisSize.min,
|
||||||
activeTrackColor: scheme.primary,
|
children: [
|
||||||
inactiveTrackColor:
|
SliderTheme(
|
||||||
Colors.white.withValues(alpha: 0.24),
|
data: SliderTheme.of(context).copyWith(
|
||||||
trackHeight: 3,
|
activeTrackColor: scheme.primary,
|
||||||
thumbColor: scheme.primary,
|
inactiveTrackColor:
|
||||||
overlayColor:
|
Colors.white.withValues(alpha: 0.24),
|
||||||
scheme.primary.withValues(alpha: 0.18),
|
trackHeight: 3,
|
||||||
thumbShape: const RoundSliderThumbShape(
|
thumbColor: scheme.primary,
|
||||||
enabledThumbRadius: 5,
|
overlayColor:
|
||||||
|
scheme.primary.withValues(alpha: 0.18),
|
||||||
|
thumbShape: const RoundSliderThumbShape(
|
||||||
|
enabledThumbRadius: 5,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
child: Slider(
|
||||||
child: Slider(
|
value: valueMs,
|
||||||
value: valueMs,
|
max: maxMs,
|
||||||
max: maxMs,
|
onChanged: duration == Duration.zero
|
||||||
onChanged: duration == Duration.zero
|
? null
|
||||||
? null
|
: (value) => player.seek(
|
||||||
: (value) => player.seek(
|
Duration(
|
||||||
Duration(
|
milliseconds: value.round(),
|
||||||
milliseconds: value.round(),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
Row(
|
||||||
Row(
|
children: [
|
||||||
children: [
|
|
||||||
_RoundControlButton(
|
|
||||||
tooltip: playing ? 'Pause' : 'Play',
|
|
||||||
emphasized: true,
|
|
||||||
onPressed: player.playOrPause,
|
|
||||||
icon: playing
|
|
||||||
? Icons.pause_rounded
|
|
||||||
: Icons.play_arrow_rounded,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
tiny
|
|
||||||
? _format(position)
|
|
||||||
: '${_format(position)} / ${_format(duration)}',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Spacer(),
|
|
||||||
if (!compact) ...[
|
|
||||||
_RoundControlButton(
|
_RoundControlButton(
|
||||||
tooltip: 'Back 10s',
|
tooltip: playing ? 'Pause' : 'Play',
|
||||||
onPressed: () => player.seek(
|
emphasized: true,
|
||||||
_clampSeek(
|
onPressed: player.playOrPause,
|
||||||
position -
|
icon: playing
|
||||||
const Duration(seconds: 10),
|
? Icons.pause_rounded
|
||||||
duration,
|
: Icons.play_arrow_rounded,
|
||||||
),
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
tiny
|
||||||
|
? _format(position)
|
||||||
|
: '${_format(position)} / ${_format(duration)}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
icon: Icons.replay_10_rounded,
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 5),
|
const Spacer(),
|
||||||
_RoundControlButton(
|
if (!compact) ...[
|
||||||
tooltip: 'Forward 10s',
|
_RoundControlButton(
|
||||||
onPressed: () => player.seek(
|
tooltip: 'Back 10s',
|
||||||
_clampSeek(
|
onPressed: () => player.seek(
|
||||||
position +
|
_clampSeek(
|
||||||
const Duration(seconds: 10),
|
position -
|
||||||
duration,
|
const Duration(seconds: 10),
|
||||||
|
duration,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
icon: Icons.replay_10_rounded,
|
||||||
),
|
),
|
||||||
icon: Icons.forward_10_rounded,
|
const SizedBox(width: 5),
|
||||||
),
|
_RoundControlButton(
|
||||||
const SizedBox(width: 5),
|
tooltip: 'Forward 10s',
|
||||||
],
|
onPressed: () => player.seek(
|
||||||
_RoundControlButton(
|
_clampSeek(
|
||||||
tooltip: muted ? 'Unmute' : 'Mute',
|
position +
|
||||||
onPressed: onToggleMute,
|
const Duration(seconds: 10),
|
||||||
icon: muted
|
duration,
|
||||||
? Icons.volume_off_rounded
|
),
|
||||||
: Icons.volume_up_rounded,
|
),
|
||||||
),
|
icon: Icons.forward_10_rounded,
|
||||||
const SizedBox(width: 5),
|
),
|
||||||
_RoundControlButton(
|
const SizedBox(width: 5),
|
||||||
tooltip: halfVolume
|
],
|
||||||
? 'Normal app volume'
|
|
||||||
: 'Half app volume',
|
|
||||||
selected: halfVolume,
|
|
||||||
onPressed: onToggleHalfVolume,
|
|
||||||
icon: Icons.volume_down_rounded,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 5),
|
|
||||||
_RoundControlButton(
|
|
||||||
tooltip: loopVideo
|
|
||||||
? 'Disable repeat'
|
|
||||||
: 'Repeat video',
|
|
||||||
selected: loopVideo,
|
|
||||||
onPressed: onToggleLoop,
|
|
||||||
icon: loopVideo
|
|
||||||
? Icons.repeat_one_on_rounded
|
|
||||||
: Icons.repeat_one_rounded,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 5),
|
|
||||||
if (!compact) ...[
|
|
||||||
_RoundControlButton(
|
_RoundControlButton(
|
||||||
tooltip: coverVideo ? 'Fit' : 'Fill',
|
tooltip: muted ? 'Unmute' : 'Mute',
|
||||||
selected: coverVideo,
|
onPressed: onToggleMute,
|
||||||
onPressed: onToggleFit,
|
icon: muted
|
||||||
icon: coverVideo
|
? Icons.volume_off_rounded
|
||||||
? Icons.fit_screen_rounded
|
: Icons.volume_up_rounded,
|
||||||
: Icons.crop_free_rounded,
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 5),
|
const SizedBox(width: 5),
|
||||||
|
_RoundControlButton(
|
||||||
|
tooltip: halfVolume
|
||||||
|
? 'Normal app volume'
|
||||||
|
: 'Half app volume',
|
||||||
|
selected: halfVolume,
|
||||||
|
onPressed: onToggleHalfVolume,
|
||||||
|
icon: Icons.volume_down_rounded,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
_RoundControlButton(
|
||||||
|
tooltip: loopVideo
|
||||||
|
? 'Disable repeat'
|
||||||
|
: 'Repeat video',
|
||||||
|
selected: loopVideo,
|
||||||
|
onPressed: onToggleLoop,
|
||||||
|
icon: loopVideo
|
||||||
|
? Icons.repeat_one_on_rounded
|
||||||
|
: Icons.repeat_one_rounded,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
if (!compact) ...[
|
||||||
|
_RoundControlButton(
|
||||||
|
tooltip: coverVideo ? 'Fit' : 'Fill',
|
||||||
|
selected: coverVideo,
|
||||||
|
onPressed: onToggleFit,
|
||||||
|
icon: coverVideo
|
||||||
|
? Icons.fit_screen_rounded
|
||||||
|
: Icons.crop_free_rounded,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 5),
|
||||||
|
],
|
||||||
|
_RoundControlButton(
|
||||||
|
tooltip: fullscreen
|
||||||
|
? 'Exit fullscreen'
|
||||||
|
: 'Fullscreen',
|
||||||
|
onPressed: onFullscreen,
|
||||||
|
icon: fullscreen
|
||||||
|
? Icons.fullscreen_exit_rounded
|
||||||
|
: Icons.fullscreen_rounded,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
_RoundControlButton(
|
),
|
||||||
tooltip: fullscreen
|
],
|
||||||
? 'Exit fullscreen'
|
);
|
||||||
: 'Fullscreen',
|
},
|
||||||
onPressed: onFullscreen,
|
);
|
||||||
icon: fullscreen
|
},
|
||||||
? Icons.fullscreen_exit_rounded
|
);
|
||||||
: Icons.fullscreen_rounded,
|
},
|
||||||
),
|
);
|
||||||
],
|
},
|
||||||
),
|
),
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -133,11 +133,8 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
|
|||||||
autocorrect: false,
|
autocorrect: false,
|
||||||
enableSuggestions: false,
|
enableSuggestions: false,
|
||||||
textInputAction: TextInputAction.search,
|
textInputAction: TextInputAction.search,
|
||||||
inputFormatters: [
|
|
||||||
_TagCommitFormatter(onCommit: _commitDraft),
|
|
||||||
],
|
|
||||||
onSubmitted: (_) => _submit(),
|
onSubmitted: (_) => _submit(),
|
||||||
onChanged: (_) => _notifyChangedDebounced(),
|
onChanged: _handleDraftChanged,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
isDense: true,
|
isDense: true,
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
@@ -200,6 +197,12 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
|
|||||||
|
|
||||||
KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) {
|
KeyEventResult _handleKeyEvent(FocusNode node, KeyEvent event) {
|
||||||
if (event is! KeyDownEvent) return KeyEventResult.ignored;
|
if (event is! KeyDownEvent) return KeyEventResult.ignored;
|
||||||
|
if (event.logicalKey == LogicalKeyboardKey.enter ||
|
||||||
|
event.logicalKey == LogicalKeyboardKey.numpadEnter) {
|
||||||
|
_commitDraft(_controller.text);
|
||||||
|
_submit();
|
||||||
|
return KeyEventResult.handled;
|
||||||
|
}
|
||||||
if (event.logicalKey != LogicalKeyboardKey.backspace) {
|
if (event.logicalKey != LogicalKeyboardKey.backspace) {
|
||||||
return KeyEventResult.ignored;
|
return KeyEventResult.ignored;
|
||||||
}
|
}
|
||||||
@@ -216,6 +219,17 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
|
|||||||
return KeyEventResult.handled;
|
return KeyEventResult.handled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _handleDraftChanged(String value) {
|
||||||
|
final composing = _controller.value.composing;
|
||||||
|
if (!composing.isValid &&
|
||||||
|
value.isNotEmpty &&
|
||||||
|
RegExp(r'\s$').hasMatch(value)) {
|
||||||
|
_commitDraft(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_notifyChangedDebounced();
|
||||||
|
}
|
||||||
|
|
||||||
void _setFromQuery(String query) {
|
void _setFromQuery(String query) {
|
||||||
_tags = _parseTags(query);
|
_tags = _parseTags(query);
|
||||||
_controller.clear();
|
_controller.clear();
|
||||||
@@ -310,25 +324,6 @@ class _TagInputSearchBarState extends State<TagInputSearchBar> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TagCommitFormatter extends TextInputFormatter {
|
|
||||||
_TagCommitFormatter({required this.onCommit});
|
|
||||||
|
|
||||||
final ValueChanged<String> onCommit;
|
|
||||||
|
|
||||||
@override
|
|
||||||
TextEditingValue formatEditUpdate(
|
|
||||||
TextEditingValue oldValue,
|
|
||||||
TextEditingValue newValue,
|
|
||||||
) {
|
|
||||||
if (newValue.text.endsWith(' ') || newValue.text.endsWith('\n')) {
|
|
||||||
final value = newValue.text.trim();
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => onCommit(value));
|
|
||||||
return const TextEditingValue();
|
|
||||||
}
|
|
||||||
return newValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TagSuggestionDropdown extends StatelessWidget {
|
class _TagSuggestionDropdown extends StatelessWidget {
|
||||||
const _TagSuggestionDropdown({
|
const _TagSuggestionDropdown({
|
||||||
required this.suggestions,
|
required this.suggestions,
|
||||||
|
|||||||
@@ -8,9 +8,16 @@ class TagChip extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final scheme = Theme.of(context).colorScheme;
|
||||||
return ActionChip(
|
return ActionChip(
|
||||||
onPressed: onTap,
|
onPressed: onTap,
|
||||||
label: Text(tag),
|
label: Text(tag),
|
||||||
|
labelStyle: TextStyle(
|
||||||
|
color: scheme.onSecondaryContainer,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
backgroundColor: scheme.secondaryContainer.withValues(alpha: 0.62),
|
||||||
|
side: BorderSide(color: scheme.outlineVariant.withValues(alpha: 0.65)),
|
||||||
visualDensity: VisualDensity.compact,
|
visualDensity: VisualDensity.compact,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
name: gel_rule_app
|
name: gel_rule_app
|
||||||
description: Local backend/core layer for a Flutter booru browser app.
|
description: Local backend/core layer for a Flutter booru browser app.
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 0.4.2+6
|
version: 0.4.3+7
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.3.0 <4.0.0"
|
sdk: ">=3.3.0 <4.0.0"
|
||||||
|
|||||||
@@ -139,6 +139,47 @@ void main() {
|
|||||||
'2');
|
'2');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('deleted collection stays deleted after repository reload', () async {
|
||||||
|
final repository = CollectionRepository(databaseService, postRepository);
|
||||||
|
final service = CollectionService(repository);
|
||||||
|
final created = await service.createCollection('Temp', null) as Success;
|
||||||
|
final item = post('collection-delete');
|
||||||
|
|
||||||
|
await service.addPostToCollection(created.data.id, item);
|
||||||
|
await service.deleteCollection(created.data.id);
|
||||||
|
|
||||||
|
final reloadedService = CollectionService(
|
||||||
|
CollectionRepository(databaseService, postRepository),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect((await reloadedService.getCollections() as Success).data, isEmpty);
|
||||||
|
expect(
|
||||||
|
(await reloadedService.getCollectionPosts(created.data.id) as Success)
|
||||||
|
.data,
|
||||||
|
isEmpty,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('settings export keeps blacklist whitelist and smart rules', () async {
|
||||||
|
final service = SettingsService(databaseService);
|
||||||
|
await service.updateSettings(
|
||||||
|
AppSettings.defaults.copyWith(
|
||||||
|
blacklistedTags: ['blocked'],
|
||||||
|
whitelistedTags: ['allowed'],
|
||||||
|
smartBlacklistRules: ['provider:e621 score:<10'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final exported = await service.exportSettingsToJson() as Success<String>;
|
||||||
|
|
||||||
|
expect(exported.data, contains('"blacklistedTags"'));
|
||||||
|
expect(exported.data, contains('"blocked"'));
|
||||||
|
expect(exported.data, contains('"whitelistedTags"'));
|
||||||
|
expect(exported.data, contains('"allowed"'));
|
||||||
|
expect(exported.data, contains('"smartBlacklistRules"'));
|
||||||
|
expect(exported.data, contains('provider:e621 score:<10'));
|
||||||
|
});
|
||||||
|
|
||||||
test('cache service prunes to max item count', () async {
|
test('cache service prunes to max item count', () async {
|
||||||
final service = CacheService(databaseService);
|
final service = CacheService(databaseService);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:gel_rule_app/features/post/presentation/widgets/post_media_viewer.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('video error retry button invokes callback', (tester) async {
|
||||||
|
var retried = false;
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
body: VideoErrorOverlay(
|
||||||
|
message: 'network failed',
|
||||||
|
onRetry: () => retried = true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Retry'));
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(retried, isTrue);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -66,7 +66,7 @@ include(flutter/generated_plugins.cmake)
|
|||||||
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
|
set(BUILD_BUNDLE_DIR "$<TARGET_FILE_DIR:${BINARY_NAME}>")
|
||||||
# Make the "install" step default, as it's required to run.
|
# Make the "install" step default, as it's required to run.
|
||||||
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
|
set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1)
|
||||||
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT)
|
if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT OR CMAKE_INSTALL_PREFIX MATCHES "Program Files")
|
||||||
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user