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
25 changes: 25 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,30 @@ jobs:
printf 'This program comes with ABSOLUTELY NO WARRANTY.\n'
} > release_notes.md

- name: Generate SHA-256 checksums
shell: bash
run: |
: > SHA256SUMS
for file in \
ZPlay-Windows-Setup.exe \
ZPlay-Windows-x64-Portable.zip \
app-arm64-v8a-release.apk \
app-armeabi-v7a-release.apk \
app-x86_64-release.apk \
ZPlay-Linux-x86_64.AppImage \
ZPlay-Linux-x86_64.tar.gz \
ZPlay-macOS-arm64.dmg \
ZPlay-macOS-arm64.zip \
ZPlay-macOS-intel.dmg \
ZPlay-macOS-intel.zip \
ZPlay-iOS.ipa
do
if [ -f "$file" ]; then
sha256sum "$file" >> SHA256SUMS
fi
done
cat SHA256SUMS

- name: Publish Release with Assets
# v3 runs on Node 24. v2 is the final v2 release and pins the Node 20
# runtime that GitHub Actions has deprecated.
Expand Down Expand Up @@ -663,3 +687,4 @@ jobs:
ZPlay-macOS-intel.dmg
ZPlay-macOS-intel.zip
ZPlay-iOS.ipa
SHA256SUMS
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,12 @@ class CloudStreamNativeBridge(private val context: Context, private val activity
val entry = entries.nextElement()
if (entry.name.startsWith(prefix) && !entry.isDirectory) {
val libName = entry.name.substring(prefix.length)
val outFile = File(targetDir, libName)
val root = targetDir.canonicalFile
val outFile = File(root, libName).canonicalFile
if (!outFile.path.startsWith(root.path + File.separator)) {
Log.w(TAG, "Skipping native library outside target: ${entry.name}")
continue
}
if (!outFile.exists() || outFile.length() != entry.size) {
if (outFile.exists()) outFile.setWritable(true)
zip.getInputStream(entry).use { input ->
Expand Down
456 changes: 456 additions & 0 deletions docs/audit/security-performance-2026-10-24.md

Large diffs are not rendered by default.

29 changes: 15 additions & 14 deletions lib/pages/audiobooks/audiobook_player_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ class _AudiobookPlayerScreenState extends State<AudiobookPlayerScreen>

_initChapter(_currentChapterIndex);

// Save progress every 5 seconds
_progressTimer = Timer.periodic(const Duration(seconds: 5), (_) {
// Save progress periodically; pause, seek, and dispose also flush it.
_progressTimer = Timer.periodic(const Duration(seconds: 30), (_) {
_saveProgress();
});
}
Expand Down Expand Up @@ -169,7 +169,7 @@ class _AudiobookPlayerScreenState extends State<AudiobookPlayerScreen>
// The first chapter has nowhere to go back to, so the button restarts it,
// which is what a listener reaching for it at the start of a book expects.
if (_currentChapterIndex == 0) {
await _player?.seek(Duration.zero);
await seek(Duration.zero);
return;
}
await _initChapter(_currentChapterIndex - 1);
Expand All @@ -180,6 +180,7 @@ class _AudiobookPlayerScreenState extends State<AudiobookPlayerScreen>
await _player?.seek(position);
if (!mounted) return;
setState(() => _position = position);
_saveProgress();
}

/// Stops the book and removes the bar, by leaving this screen.
Expand Down Expand Up @@ -424,6 +425,7 @@ class _AudiobookPlayerScreenState extends State<AudiobookPlayerScreen>
_player!.pause();
_discAnimController.stop();
setState(() => _isPlaying = false);
_saveProgress();
} else {
_player!.play();
_discAnimController.repeat();
Expand All @@ -441,6 +443,8 @@ class _AudiobookPlayerScreenState extends State<AudiobookPlayerScreen>
? Duration.zero
: (target > _duration ? _duration : target);
_player!.seek(clamped);
if (mounted) setState(() => _position = clamped);
_saveProgress();
}

void _cycleSpeed() {
Expand Down Expand Up @@ -1139,7 +1143,7 @@ class _AudiobookPlayerScreenState extends State<AudiobookPlayerScreen>
duration: _duration,
isPlaying: _isPlaying,
style: seekStyle,
onSeek: (pos) => _player?.seek(pos),
onSeek: (pos) => seek(pos),
),
const SizedBox(height: 12),

Expand Down Expand Up @@ -1192,7 +1196,7 @@ class _AudiobookPlayerScreenState extends State<AudiobookPlayerScreen>
duration: _duration,
isPlaying: _isPlaying,
style: AudiobookSeekbarStyle.audioWaveformCanvas,
onSeek: (pos) => _player?.seek(pos),
onSeek: (pos) => seek(pos),
),
const SizedBox(height: 14),

Expand Down Expand Up @@ -1298,7 +1302,7 @@ class _AudiobookPlayerScreenState extends State<AudiobookPlayerScreen>
duration: _duration,
isPlaying: _isPlaying,
style: AudiobookSeekbarStyle.gradientProgress,
onSeek: (pos) => _player?.seek(pos),
onSeek: (pos) => seek(pos),
),
const SizedBox(height: 12),
_buildMainControlsCluster(palette),
Expand Down Expand Up @@ -1342,7 +1346,7 @@ class _AudiobookPlayerScreenState extends State<AudiobookPlayerScreen>
duration: _duration,
isPlaying: _isPlaying,
style: AudiobookSeekbarStyle.audioWaveformCanvas,
onSeek: (pos) => _player?.seek(pos),
onSeek: (pos) => seek(pos),
),
const SizedBox(height: 16),
_buildMainControlsCluster(palette),
Expand Down Expand Up @@ -1409,7 +1413,7 @@ class _AudiobookPlayerScreenState extends State<AudiobookPlayerScreen>
duration: _duration,
isPlaying: _isPlaying,
style: seekStyle,
onSeek: (pos) => _player?.seek(pos),
onSeek: (pos) => seek(pos),
),
);
case 'mainControls':
Expand Down Expand Up @@ -2030,9 +2034,8 @@ class _AudiobookPlayerScreenState extends State<AudiobookPlayerScreen>
),
),
FocusableInkWell(
onTap: () => setState(
() => _showChaptersDrawer = false,
),
onTap: () =>
setState(() => _showChaptersDrawer = false),
borderRadius: ZplayRadius.fullAll,
hoverColor: Colors.white.withValues(
alpha: ZplayOpacity.borderDefault,
Expand Down Expand Up @@ -2265,9 +2268,7 @@ class _ChapterListItemTile extends StatelessWidget {
child: FocusableCard(
onTap: onTap,
builder: (context, state) {
final scale = state.pressed
? 0.98
: (state.hovered ? 1.015 : 1.0);
final scale = state.pressed ? 0.98 : (state.hovered ? 1.015 : 1.0);

return AnimatedScale(
scale: scale,
Expand Down
68 changes: 46 additions & 22 deletions lib/pages/books/book_detail_sheet.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,7 @@ import '../../widgets/common/section_header.dart';
class BookDetailSheet extends StatefulWidget {
final BookResult book;

const BookDetailSheet({
super.key,
required this.book,
});
const BookDetailSheet({super.key, required this.book});

static Future<void> show(BuildContext context, BookResult book) {
return showModalBottomSheet(
Expand Down Expand Up @@ -48,10 +45,15 @@ class _BookDetailSheetState extends State<BookDetailSheet> {
}

Future<void> _checkDownloadStatus() async {
final downloaded = await BookDownloadService.instance.getDownloadedBook(
widget.book.md5,
widget.book.bookFiletype,
);
File? downloaded;
try {
downloaded = await BookDownloadService.instance.getDownloadedBook(
widget.book.md5,
widget.book.bookFiletype,
);
} catch (e) {
debugPrint('[BookDetailSheet] Could not check download status: $e');
}
if (!mounted) return;
setState(() {
_isDownloaded = downloaded != null;
Expand Down Expand Up @@ -218,19 +220,32 @@ class _BookDetailSheetState extends State<BookDetailSheet> {
placeholder: (_, __) => Container(
color: tokens.surface,
child: Center(
child: Icon(Icons.menu_book_rounded, color: tokens.textDisabled, size: 36),
child: Icon(
Icons.menu_book_rounded,
color: tokens.textDisabled,
size: 36,
),
),
),
errorWidget: (_, __, ___) => Container(
color: tokens.surface,
child: Center(
child: Icon(Icons.menu_book_rounded, color: tokens.textDisabled, size: 36),
child: Icon(
Icons.menu_book_rounded,
color: tokens.textDisabled,
size: 36,
),
),
))
),
)
: Container(
color: tokens.surface,
child: Center(
child: Icon(Icons.menu_book_rounded, color: tokens.textDisabled, size: 36),
child: Icon(
Icons.menu_book_rounded,
color: tokens.textDisabled,
size: 36,
),
),
),
),
Expand All @@ -245,7 +260,9 @@ class _BookDetailSheetState extends State<BookDetailSheet> {
children: [
Text(
book.displayTitle,
style: ZplayType.title.toStyle(color: tokens.textPrimary),
style: ZplayType.title.toStyle(
color: tokens.textPrimary,
),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
Expand Down Expand Up @@ -273,8 +290,9 @@ class _BookDetailSheetState extends State<BookDetailSheet> {
if (book.year.isNotEmpty) book.year,
if (book.bookLang.isNotEmpty) book.bookLang,
].join(' · '),
style: ZplayType.label
.toStyle(color: tokens.textSecondary),
style: ZplayType.label.toStyle(
color: tokens.textSecondary,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
Expand Down Expand Up @@ -336,8 +354,8 @@ class _BookDetailSheetState extends State<BookDetailSheet> {
label: _isDownloading
? 'Downloading ${(_downloadProgress * 100).round()}%...'
: hasProgress
? 'Resume Reading'
: 'Read Now',
? 'Resume Reading'
: 'Read Now',
icon: hasProgress
? Icons.play_arrow_rounded
: Icons.auto_stories_rounded,
Expand All @@ -357,7 +375,9 @@ class _BookDetailSheetState extends State<BookDetailSheet> {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const GenerateAudiobookScreen()),
MaterialPageRoute(
builder: (_) => const GenerateAudiobookScreen(),
),
);
},
),
Expand Down Expand Up @@ -388,7 +408,9 @@ class _BookDetailSheetState extends State<BookDetailSheet> {
// header bakes a 16 dp inset of its own - the translate
// cancels it, the same way the details pages do, so the
// heading lines up with the cover above it.
if (book.publisher.isNotEmpty || book.isbn.isNotEmpty || book.series.isNotEmpty) ...[
if (book.publisher.isNotEmpty ||
book.isbn.isNotEmpty ||
book.series.isNotEmpty) ...[
Transform.translate(
offset: const Offset(-ZplaySpacing.s16, 0),
child: const SectionHeader(title: 'Information'),
Expand All @@ -398,11 +420,13 @@ class _BookDetailSheetState extends State<BookDetailSheet> {
_buildInfoRow('Publisher', book.publisher),
if (book.series.isNotEmpty)
_buildInfoRow('Series', book.series),
if (book.isbn.isNotEmpty)
_buildInfoRow('ISBN', book.isbn),
if (book.isbn.isNotEmpty) _buildInfoRow('ISBN', book.isbn),
if (book.bookLang.isNotEmpty)
_buildInfoRow('Language', book.bookLang),
_buildInfoRow('File Format', book.bookFiletype.toUpperCase()),
_buildInfoRow(
'File Format',
book.bookFiletype.toUpperCase(),
),
if (book.bookSize.isNotEmpty)
_buildInfoRow('Size', book.bookSize),
const SizedBox(height: ZplaySpacing.s16),
Expand Down
Loading
Loading