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
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,9 @@ void main() {
.widget<mk.Video>(find.byType(mk.Video))
.controller
.player;
expect(second, isNot(same(first)));
// A healthy player is reused across source changes; only a failed
// source gets a fresh one (see VideoPlayer.didUpdateWidget).
expect(second, same(first));
await waitFor(tester, () => second.state.position.inMilliseconds > 500);
expect(errors, isEmpty);
await tester.pumpWidget(const SizedBox.shrink());
Expand Down
3 changes: 3 additions & 0 deletions cb_file_manager/lib/config/languages/app_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,9 @@ abstract class AppLocalizations {
String exitTagScope(String parent);
String childTagHint(String parent);
String addingUnderTag(String parent);
String get tagInputHelp;
String get tagSuggestionsLabel;
String childTagCount(int count);
String deleteTagConfirmation(String tag);
String get tagDeleteConfirmationText;
String tagDeleted(String tag);
Expand Down
10 changes: 9 additions & 1 deletion cb_file_manager/lib/config/languages/english_localizations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -822,7 +822,15 @@ class EnglishLocalizations implements AppLocalizations {
String childTagHint(String parent) => 'New tag inside "$parent"...';
@override
String addingUnderTag(String parent) =>
'Adding inside "$parent" β€” Enter adds another child, Backspace leaves.';
'Adding children to "$parent". Esc leaves this parent.';
@override
String get tagInputHelp =>
'Type a parent tag, then press ":" or β†’ to enter a child inside its chip.';
@override
String get tagSuggestionsLabel => 'Suggested tags';
@override
String childTagCount(int count) =>
'$count ${count == 1 ? 'child' : 'children'}';
@override
String deleteTagConfirmation(String tag) => 'Delete tag "$tag"?';
@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -817,7 +817,14 @@ class VietnameseLocalizations implements AppLocalizations {
String childTagHint(String parent) => 'ThαΊ» con mα»›i trong "$parent"...';
@override
String addingUnderTag(String parent) =>
'Đang thΓͺm trong "$parent" β€” Enter để thΓͺm thαΊ» con tiαΊΏp theo, Backspace để thoΓ‘t.';
'ThΓͺm thαΊ» con vΓ o "$parent". Esc để thoΓ‘t thαΊ» cha.';
@override
String get tagInputHelp =>
'GΓ΅ thαΊ» cha, rα»“i nhαΊ₯n ":" hoαΊ·c β†’ để nhαΊ­p thαΊ» con ngay trong chip.';
@override
String get tagSuggestionsLabel => 'Thẻ gợi ý';
@override
String childTagCount(int count) => '$count thαΊ» con';
@override
String deleteTagConfirmation(String tag) => 'XΓ³a thαΊ» "$tag"?';
@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,8 @@ class _VideoPlayerState extends _VideoPlayerSettingsHost
userPreferences.getVideoPlayerVolume(),
userPreferences.getVideoPlayerMute(),
).wait;
// Disposed while loading preferences: don't create an orphaned player.
if (!mounted) return;
_lastVolume = savedVolume > 0 ? savedVolume : _lastVolume;

setState(() {
Expand Down Expand Up @@ -775,6 +777,7 @@ class _VideoPlayerState extends _VideoPlayerSettingsHost

// Open media based on source type
await _openMediaSource();
if (!mounted) return;

// Apply saved volume preferences with multiple attempts
await _applyVolumeSettings();
Expand Down Expand Up @@ -932,23 +935,25 @@ class _VideoPlayerState extends _VideoPlayerSettingsHost
}

Future<void> _openMediaSource() async {
// Held locally: dispose() can null _player while open() is in flight.
final player = _player!;
if (widget.file != null) {
// Local file playback
await _player!.open(
await player.open(
PlaybackMedia(widget.file!.path),
play: widget.autoPlay,
);
if (widget.autoPlay) {
await _player!.play();
if (widget.autoPlay && mounted) {
await player.play();
}
} else if (widget.streamingUrl != null) {
// Streaming URL playback
await _player!.open(
await player.open(
PlaybackMedia(widget.streamingUrl!),
play: widget.autoPlay,
);
if (widget.autoPlay) {
await _player!.play();
if (widget.autoPlay && mounted) {
await player.play();
}
} else if (widget.smbMrl != null) {
// Direct SMB playback uses the same media_kit backend as local files.
Expand Down
32 changes: 21 additions & 11 deletions cb_file_manager/lib/ui/tab_manager/components/tag_dialogs.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import 'package:cb_file_manager/ui/components/common/app_toast.dart';
import 'package:cb_file_manager/ui/widgets/resizable_dialog.dart';
import 'package:cb_file_manager/ui/widgets/tag_browse_section.dart';
import 'package:cb_file_manager/ui/widgets/tag_chips_field.dart';
import 'package:cb_file_manager/ui/widgets/chips_input.dart';
import 'package:cb_file_manager/ui/widgets/tag_management_section.dart';
import 'package:cb_file_manager/utils/app_logger.dart';
import '../../utils/route.dart';
Expand Down Expand Up @@ -68,19 +69,21 @@ class _SingleFileTagDialog extends StatefulWidget {
}

class _SingleFileTagDialogState extends State<_SingleFileTagDialog> {
final _inputKey = GlobalKey<ChipsInputState<String>>();
List<String> _originalTags = <String>[];
List<String> _selectedTags = <String>[];
List<String> _tagSuggestions = <String>[];
String _draftTagText = '';

/// Parent tag the input is scoped to, shown as a pill inside the field.
/// Parent tag whose inline chip contains the child draft.
/// While set, the draft is only the child name and every submit composes
/// "parent:child" β€” so a run of children goes in without retyping the
/// parent once.
String? _scopeParent;
bool _isLoading = true;
bool _isSaving = false;
Timer? _debounceTimer;
int _suggestionGeneration = 0;
final _thumbnailManager = TagThumbnailManager.instance;
final _hierarchyManager = TagHierarchyManager.instance;

Expand All @@ -101,6 +104,7 @@ class _SingleFileTagDialogState extends State<_SingleFileTagDialog> {

@override
void dispose() {
_suggestionGeneration++;
_debounceTimer?.cancel();
super.dispose();
}
Expand Down Expand Up @@ -161,7 +165,9 @@ class _SingleFileTagDialogState extends State<_SingleFileTagDialog> {

Future<void> _updateTagSuggestions(String text) async {
_debounceTimer?.cancel();
final generation = ++_suggestionGeneration;
final query = _scopedQuery(text);
if (mounted) setState(() => _tagSuggestions = []);

if (query.isEmpty) {
if (!mounted) return;
Expand All @@ -178,7 +184,7 @@ class _SingleFileTagDialogState extends State<_SingleFileTagDialog> {
hierarchyManager: _hierarchyManager,
isSelected: _containsTag,
);
if (!mounted) return;
if (!mounted || generation != _suggestionGeneration) return;
setState(() {
_tagSuggestions = suggestions;
});
Expand Down Expand Up @@ -209,6 +215,7 @@ class _SingleFileTagDialogState extends State<_SingleFileTagDialog> {

void _addTag(String rawTag) {
final tag = rawTag.trim();
_inputKey.currentState?.clearDraft();
if (tag.isEmpty) {
_draftTagText = '';
return;
Expand Down Expand Up @@ -371,19 +378,18 @@ class _SingleFileTagDialogState extends State<_SingleFileTagDialog> {
return _buildSectionCard(
icon: PhosphorIconsLight.pencilSimpleLine,
title: l10n.addTag,
subtitle: scope != null
? l10n.addingUnderTag(scope)
: 'Type a tag. Press ":" or β†’ on a suggestion to add inside it.',
subtitle: scope != null ? l10n.addingUnderTag(scope) : l10n.tagInputHelp,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TagChipsField(
fieldKey: _inputKey,
tags: _selectedTags,
suggestions: _tagSuggestions,
scopeParent: scope,
onScopeChanged: _onScopeChanged,
onSuggestionSelected: _onSuggestionSelected,
hintText: '${l10n.enterTagName} (e.g. Actress:Hung)',
hintText: l10n.enterTagName,
onTextChanged: (value) {
_draftTagText = value;
_updateTagSuggestions(value);
Expand Down Expand Up @@ -714,10 +720,11 @@ void showDeleteTagDialog(
/// Dialog for batch adding tags
void showBatchAddTagDialog(BuildContext context, List<String> selectedFiles) {
final focusNode = FocusNode();
final TextEditingController textController = TextEditingController();
final inputKey = GlobalKey<ChipsInputState<String>>();
List<String> tagSuggestions = [];
List<String> selectedTags = [];
String draftTagText = '';
int suggestionGeneration = 0;

/// Parent tag the input is scoped to β€” see _SingleFileTagDialogState.
String? scopeParent;
Expand All @@ -741,11 +748,13 @@ void showBatchAddTagDialog(BuildContext context, List<String> selectedFiles) {
}

Future<void> updateTagSuggestions(String text) async {
tagSuggestions = await computeTagSuggestions(
final generation = ++suggestionGeneration;
final suggestions = await computeTagSuggestions(
scopedQuery(text),
hierarchyManager: hierarchyManager,
isSelected: selectedTags.contains,
);
if (generation == suggestionGeneration) tagSuggestions = suggestions;
}

/// Handles "parent:child1,child2" input by adding the parent + children to
Expand All @@ -762,7 +771,7 @@ void showBatchAddTagDialog(BuildContext context, List<String> selectedFiles) {
.toList(growable: false);

selectedTags.addAll(tagsToAdd);
textController.clear();
inputKey.currentState?.clearDraft();
draftTagText = '';
tagSuggestions = [];

Expand Down Expand Up @@ -790,7 +799,7 @@ void showBatchAddTagDialog(BuildContext context, List<String> selectedFiles) {

if (!selectedTags.contains(trimmed)) {
selectedTags.add(trimmed);
textController.clear();
inputKey.currentState?.clearDraft();
draftTagText = '';
}
}
Expand Down Expand Up @@ -846,7 +855,7 @@ void showBatchAddTagDialog(BuildContext context, List<String> selectedFiles) {

void handleTextChange(String value) {
draftTagText = value;
setState(() {});
setState(() => tagSuggestions = []);
refreshSuggestions(value);
}

Expand Down Expand Up @@ -918,6 +927,7 @@ void showBatchAddTagDialog(BuildContext context, List<String> selectedFiles) {
Focus(
focusNode: focusNode,
child: TagChipsField(
fieldKey: inputKey,
tags: selectedTags,
suggestions: tagSuggestions,
scopeParent: scopeParent,
Expand Down
Loading
Loading