Skip to content

Commit 9fbda07

Browse files
committed
Fix Bookmark pinning numbers
1 parent 2fd2784 commit 9fbda07

File tree

8 files changed

+255
-127
lines changed

8 files changed

+255
-127
lines changed

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ windows/runner/Resources/bin/
5656
**/generated_plugins.cmake
5757
**/GeneratedPluginRegistrant.swift
5858

59+
# Ignore all binaries in assets
60+
assets/bin/**/*
61+
assets/whisper/**/*
62+
63+
# But keep the .gitkeep files
64+
!assets/bin/**/.gitkeep
65+
!assets/whisper/**/.gitkeep
66+
5967
# macOS Pods
6068
macos/Pods/
6169
macos/Podfile.lock

lib/models/bookmark.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ class Bookmark {
2828
DateTime? created,
2929
String? note,
3030
int? pinNumber,
31+
bool clearPin = false,
3132
}) {
3233
return Bookmark(
3334
audiobookPath: audiobookPath ?? this.audiobookPath,
@@ -37,7 +38,7 @@ class Bookmark {
3738
position: position ?? this.position,
3839
created: created ?? this.created,
3940
note: note ?? this.note,
40-
pinNumber: pinNumber ?? this.pinNumber,
41+
pinNumber: clearPin ? null : (pinNumber ?? this.pinNumber),
4142
);
4243
}
4344

lib/models/encoding_config.dart

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ class EncodingConfig {
2323
final filters = <String>[];
2424

2525
if (removeSilence && silenceDb != null) {
26+
// Remove silence from START of audio
2627
filters.add(
27-
'silenceremove=start_periods=0:stop_periods=-1:'
28-
'start_threshold=-${silenceDb}dB:stop_threshold=-${silenceDb}dB:'
29-
'start_silence=1:start_duration=0:stop_duration=1:detection=rms'
28+
'silenceremove=start_periods=1:start_threshold=-${silenceDb}dB:start_silence=0:start_duration=0:detection=rms'
29+
);
30+
31+
// Remove silence from END and MIDDLE of audio
32+
filters.add(
33+
'silenceremove=start_periods=0:stop_periods=-1:stop_threshold=-${silenceDb}dB:stop_duration=1:detection=rms'
3034
);
3135
}
3236

lib/screens/player_screen.dart

Lines changed: 129 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -305,7 +305,6 @@ class _PlayerScreenState extends State<PlayerScreen> {
305305
_currentPosition = position;
306306
});
307307
_checkChapterBoundary(position);
308-
_checkSleepTimer();
309308
_checkPauseTrigger();
310309
_updateCurrentSubtitle();
311310
if (_isPlaying && position.inSeconds % 10 == 0) {
@@ -849,46 +848,81 @@ class _PlayerScreenState extends State<PlayerScreen> {
849848
}).toList();
850849
}
851850

852-
void _checkSleepTimer() {
853-
if (_sleepDuration == null) return;
854-
if (_sleepDuration == Duration.zero) {
855-
final chapter = _currentAudiobook!.chapters[_currentChapterIndex];
856-
if (_currentPosition >= chapter.endTime) {
857-
exit(0);
858-
}
859-
} else if (_sleepDuration!.inMinutes == -1) {
860-
if (_currentPosition >= _totalDuration) {
861-
exit(0);
862-
}
863-
}
864-
}
865-
866851
void _setSleepTimer(Duration? duration) {
867852
_sleepTimer?.cancel();
868-
if (duration == null) {
853+
_sleepTimer = null;
854+
855+
if (duration == null || duration.inSeconds == -1) {
869856
setState(() {
870857
_sleepDuration = null;
871858
});
859+
if (mounted) {
860+
ScaffoldMessenger.of(context).showSnackBar(
861+
const SnackBar(
862+
content: Text('Sleep timer off'),
863+
duration: Duration(seconds: 1),
864+
),
865+
);
866+
}
872867
return;
873868
}
869+
874870
if (duration == Duration.zero) {
871+
if (_currentAudiobook == null) return;
872+
final currentChapter = _currentAudiobook!.chapters[_currentChapterIndex];
873+
final timeUntilChapterEnd = currentChapter.endTime - _currentPosition;
875874
setState(() {
876875
_sleepDuration = Duration.zero;
877876
});
877+
_sleepTimer = Timer(timeUntilChapterEnd, () {
878+
exit(0);
879+
});
880+
if (mounted) {
881+
ScaffoldMessenger.of(context).showSnackBar(
882+
const SnackBar(
883+
content: Text('Sleep timer: Chapter end'),
884+
duration: Duration(seconds: 1),
885+
),
886+
);
887+
}
878888
return;
879889
}
890+
880891
if (duration.inMinutes == -1) {
892+
if (_currentAudiobook == null) return;
893+
final lastChapter = _currentAudiobook!.chapters.last;
894+
final timeUntilBookEnd = lastChapter.endTime - _currentPosition;
881895
setState(() {
882896
_sleepDuration = Duration(minutes: -1);
883897
});
898+
_sleepTimer = Timer(timeUntilBookEnd, () {
899+
exit(0);
900+
});
901+
if (mounted) {
902+
ScaffoldMessenger.of(context).showSnackBar(
903+
const SnackBar(
904+
content: Text('Sleep timer: End of audiobook'),
905+
duration: Duration(seconds: 1),
906+
),
907+
);
908+
}
884909
return;
885910
}
911+
886912
setState(() {
887913
_sleepDuration = duration;
888914
});
889915
_sleepTimer = Timer(duration, () {
890916
exit(0);
891917
});
918+
if (mounted) {
919+
ScaffoldMessenger.of(context).showSnackBar(
920+
SnackBar(
921+
content: Text('Sleep timer: ${duration.inMinutes} minutes'),
922+
duration: const Duration(seconds: 1),
923+
),
924+
);
925+
}
892926
}
893927

894928
Color _adjustColorIfBright(String hexColor) {
@@ -1820,18 +1854,28 @@ class _PlayerScreenState extends State<PlayerScreen> {
18201854

18211855
Future<void> _addBookmark() async {
18221856
if (_currentAudiobook == null) return;
1857+
18231858
final currentChapter = _currentAudiobook!.chapters[_currentChapterIndex];
1859+
final timeFromChapterStart = _currentPosition - currentChapter.startTime;
18241860
final timeUntilChapterEnd = currentChapter.endTime - _currentPosition;
1861+
18251862
Duration bookmarkPosition = _currentPosition;
18261863
int bookmarkChapterIndex = _currentChapterIndex;
18271864
String bookmarkChapterTitle = currentChapter.title;
1828-
if (timeUntilChapterEnd.inSeconds <= 10 &&
1865+
1866+
if (timeFromChapterStart.inSeconds <= 10) {
1867+
bookmarkPosition = currentChapter.startTime;
1868+
bookmarkChapterTitle = currentChapter.title;
1869+
bookmarkChapterIndex = _currentChapterIndex;
1870+
}
1871+
else if (timeUntilChapterEnd.inSeconds <= 10 &&
18291872
_currentChapterIndex < _currentAudiobook!.chapters.length - 1) {
18301873
bookmarkChapterIndex = _currentChapterIndex + 1;
18311874
final nextChapter = _currentAudiobook!.chapters[bookmarkChapterIndex];
18321875
bookmarkPosition = nextChapter.startTime;
18331876
bookmarkChapterTitle = nextChapter.title;
18341877
}
1878+
18351879
final bookmark = Bookmark(
18361880
audiobookPath: _currentAudiobook!.path,
18371881
audiobookTitle: _currentAudiobook!.title,
@@ -1840,15 +1884,21 @@ class _PlayerScreenState extends State<PlayerScreen> {
18401884
position: bookmarkPosition,
18411885
created: DateTime.now(),
18421886
);
1887+
18431888
setState(() {
18441889
_bookmarks.insert(0, bookmark);
18451890
});
18461891
await _saveBookmarks();
1892+
18471893
if (mounted) {
18481894
ScaffoldMessenger.of(context).showSnackBar(
1849-
const SnackBar(
1850-
content: Text('Bookmark added'),
1851-
duration: Duration(seconds: 1),
1895+
SnackBar(
1896+
content: Text(
1897+
timeFromChapterStart.inSeconds <= 10 || timeUntilChapterEnd.inSeconds <= 10
1898+
? 'Bookmark added (snapped to chapter start)'
1899+
: 'Bookmark added'
1900+
),
1901+
duration: const Duration(seconds: 1),
18521902
),
18531903
);
18541904
}
@@ -3357,6 +3407,9 @@ class _PlayerScreenState extends State<PlayerScreen> {
33573407
} else if (event.logicalKey == LogicalKeyboardKey.keyY && event is KeyDownEvent) {
33583408
_toggleFullscreen();
33593409
return KeyEventResult.handled;
3410+
} else if (event.logicalKey == LogicalKeyboardKey.keyZ && event is KeyDownEvent) {
3411+
_setSleepTimer(Duration.zero);
3412+
return KeyEventResult.handled;
33603413
} else if (event.logicalKey == LogicalKeyboardKey.keyA && event is KeyDownEvent) {
33613414
_applyDefaultSettings();
33623415
return KeyEventResult.handled;
@@ -4525,12 +4578,65 @@ class _PlayerScreenState extends State<PlayerScreen> {
45254578
return '';
45264579
}
45274580

4528-
Future<void> _setPinNumber(int bookmarkIndex, int? pinNumber) async {
4529-
final bookmark = _bookmarks[bookmarkIndex].copyWith(pinNumber: pinNumber);
4581+
Future<void> _setPinNumber(int displayIndex, int? pinNumber) async {
4582+
// Get the filtered bookmarks list
4583+
final filteredBookmarks = _getFilteredBookmarks();
4584+
4585+
// The displayIndex from the UI might be 1-indexed or include a header
4586+
// Let's use the actual index directly
4587+
if (displayIndex >= filteredBookmarks.length) return;
4588+
4589+
// Get the actual bookmark from the filtered list
4590+
final targetBookmark = filteredBookmarks[displayIndex];
4591+
4592+
// Find the index of this bookmark in the full _bookmarks list
4593+
final actualIndex = _bookmarks.indexWhere((b) =>
4594+
b.audiobookPath == targetBookmark.audiobookPath &&
4595+
b.chapterIndex == targetBookmark.chapterIndex &&
4596+
b.position == targetBookmark.position &&
4597+
b.created == targetBookmark.created
4598+
);
4599+
4600+
if (actualIndex == -1) return;
4601+
4602+
// Debug print to verify
4603+
print('Display index: $displayIndex, Actual index: $actualIndex');
4604+
print('Target bookmark: ${targetBookmark.chapterTitle}');
4605+
45304606
setState(() {
4531-
_bookmarks[bookmarkIndex] = bookmark;
4607+
// If setting a pin number (not null), first check if another bookmark already has this pin
4608+
if (pinNumber != null) {
4609+
// Find if any other bookmark has this pin number
4610+
for (int i = 0; i < _bookmarks.length; i++) {
4611+
if (i != actualIndex && _bookmarks[i].pinNumber == pinNumber) {
4612+
// Remove the pin from the other bookmark
4613+
_bookmarks[i] = _bookmarks[i].copyWith(clearPin: true);
4614+
}
4615+
}
4616+
}
4617+
4618+
// Now set the pin for the requested bookmark
4619+
if (pinNumber == null) {
4620+
_bookmarks[actualIndex] = _bookmarks[actualIndex].copyWith(clearPin: true);
4621+
} else {
4622+
_bookmarks[actualIndex] = _bookmarks[actualIndex].copyWith(pinNumber: pinNumber);
4623+
}
45324624
});
4625+
45334626
await _saveBookmarks();
4627+
4628+
if (mounted) {
4629+
ScaffoldMessenger.of(context).showSnackBar(
4630+
SnackBar(
4631+
content: Text(
4632+
pinNumber == null
4633+
? 'Bookmark unpinned: ${targetBookmark.chapterTitle}'
4634+
: 'Bookmark pinned to $pinNumber: ${targetBookmark.chapterTitle}'
4635+
),
4636+
duration: const Duration(seconds: 2),
4637+
),
4638+
);
4639+
}
45344640
}
45354641

45364642
Future<void> _jumpToPinnedBookmark(int pinNumber) async {
@@ -4546,7 +4652,6 @@ class _PlayerScreenState extends State<PlayerScreen> {
45464652
Future<void> _skipToPreviousSubtitle() async {
45474653
if (_subtitles.isEmpty) return;
45484654

4549-
// Find current subtitle index first
45504655
int currentIndex = -1;
45514656
for (int i = 0; i < _subtitles.length; i++) {
45524657
if (_subtitles[i].startTime <= _currentPosition &&
@@ -4556,7 +4661,6 @@ class _PlayerScreenState extends State<PlayerScreen> {
45564661
}
45574662
}
45584663

4559-
// Go to previous subtitle if it exists
45604664
if (currentIndex > 0) {
45614665
await _seekTo(_subtitles[currentIndex - 1].startTime);
45624666
} else if (currentIndex == 0) {

lib/services/cjk_tokenizer.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ class CJKTokenizer {
6161
}
6262
}
6363

64-
// Determine language with priority
6564
TextLanguage detected;
6665
if (hasJapanese) {
6766
detected = TextLanguage.japanese;
@@ -70,7 +69,6 @@ class CJKTokenizer {
7069
} else if (hasChinese) {
7170
detected = TextLanguage.chinese;
7271
} else if (hasArabic && hasLatin) {
73-
// Mixed Arabic/English - treat as Arabic so we don't filter short words
7472
detected = TextLanguage.arabic;
7573
} else if (hasLatin) {
7674
detected = TextLanguage.english;
@@ -80,7 +78,7 @@ class CJKTokenizer {
8078
detected = TextLanguage.unknown;
8179
}
8280

83-
print('🔍 Language Detection: "${text.substring(0, text.length > 50 ? 50 : text.length)}" -> $detected (hasArabic: $hasArabic, hasLatin: $hasLatin)');
81+
// print('🔍 Language Detection: "${text.substring(0, text.length > 50 ? 50 : text.length)}" -> $detected (hasArabic: $hasArabic, hasLatin: $hasLatin)');
8482

8583
return detected;
8684
}

lib/services/whisper_service.dart

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:io';
22
import 'dart:async';
33
import 'package:path/path.dart' as path;
44
import 'package:shared_preferences/shared_preferences.dart';
5+
import 'whisper_bundled.dart';
56

67
class WhisperService {
78
String? whisperExecutablePath;
@@ -23,7 +24,16 @@ class WhisperService {
2324

2425
Future<void> initialize() async {
2526
final prefs = await SharedPreferences.getInstance();
26-
whisperExecutablePath = prefs.getString('whisperExecutablePath');
27+
28+
try {
29+
whisperExecutablePath = await WhisperBundled.getWhisperExecutablePath();
30+
await prefs.setString('whisperExecutablePath', whisperExecutablePath!);
31+
print('✅ Using bundled whisper: $whisperExecutablePath');
32+
} catch (e) {
33+
print('⚠️ Could not find bundled whisper: $e');
34+
whisperExecutablePath = prefs.getString('whisperExecutablePath');
35+
}
36+
2737
modelDirectory = prefs.getString('whisperModelDirectory');
2838
language = prefs.getString('whisperLanguage') ?? 'auto';
2939
selectedModel = prefs.getString('whisperModel') ?? 'large-v3-turbo';
@@ -35,7 +45,7 @@ class WhisperService {
3545
customPrompt = prefs.getString('whisperPrompt') ?? customPrompt;
3646
translateToEnglish = prefs.getBool('whisperTranslate') ?? false;
3747
}
38-
48+
3949
Future<void> saveSettings() async {
4050
final prefs = await SharedPreferences.getInstance();
4151
if (whisperExecutablePath != null) {
@@ -53,7 +63,7 @@ class WhisperService {
5363
await prefs.setString('whisperPrompt', customPrompt);
5464
await prefs.setBool('whisperTranslate', translateToEnglish);
5565
}
56-
66+
5767
Future<void> setWhisperExecutable(String path) async {
5868
whisperExecutablePath = path;
5969
await saveSettings();
@@ -449,7 +459,6 @@ class WhisperService {
449459
await _addWebvttHeader(stitchedTemp4, stitchedTemp2);
450460

451461
final chapterName = path.basenameWithoutExtension(originalOpusPath);
452-
// Return path in working dir (will be copied to encodedchapters root by caller)
453462
final finalVtt = path.join(workingDir, '$chapterName.vtt');
454463
await File(stitchedTemp2).copy(finalVtt);
455464

lib/widgets/player_controls.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ class PlayerControls extends StatelessWidget {
488488
onSelected: onSetSleepTimer,
489489
itemBuilder: (context) => [
490490
const PopupMenuItem(
491-
value: null,
491+
value: Duration(seconds: -1),
492492
child: Text('Off'),
493493
),
494494
const PopupMenuItem(
@@ -521,7 +521,7 @@ class PlayerControls extends StatelessWidget {
521521
),
522522
const PopupMenuItem(
523523
value: Duration.zero,
524-
child: Text('Chapter end'),
524+
child: Text('Chapter end (z)'),
525525
),
526526
],
527527
),

0 commit comments

Comments
 (0)