Files
sheetless/lib/sheet_viewer_page.dart

410 lines
12 KiB
Dart

import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_drawing_board/flutter_drawing_board.dart';
import 'package:logging/logging.dart';
import 'package:pdfrx/pdfrx.dart';
import 'package:sheetless/api.dart';
import 'package:sheetless/bt_pedal_shortcuts.dart';
import 'package:sheetless/sheet.dart';
import 'package:sheetless/storage_helper.dart';
import 'package:flutter_fullscreen/flutter_fullscreen.dart';
import 'package:sheetless/utility.dart';
class SheetViewerPage extends StatefulWidget {
final Sheet sheet;
final ApiClient apiClient;
final Config config;
const SheetViewerPage({
super.key,
required this.sheet,
required this.apiClient,
required this.config,
});
@override
State<SheetViewerPage> createState() => _SheetViewerPageState();
}
class _SheetViewerPageState extends State<SheetViewerPage>
with FullScreenListener {
final log = Logger("SheetViewerPage");
final StorageHelper storageHelper = StorageHelper();
int currentPageNumber = 1;
int numPages = 1;
late Future<bool> documentLoaded;
PdfDocument? document;
bool paintMode = false;
Pages? pages;
@override
void initState() {
FullScreen.addListener(this);
super.initState();
documentLoaded = loadPdf();
}
@override
void dispose() {
FullScreen.removeListener(this);
document?.dispose(); // Make sure document gets garbage collected
super.dispose();
}
Future<bool> loadPdf() async {
if (kIsWeb) {
var data = await widget.apiClient.fetchPdfFileData(widget.sheet.uuid);
if (data == null) {
throw Exception("Failed fetching pdf file");
}
document = await PdfDocument.openData(data);
} else {
var file = await widget.apiClient.getPdfFileCached(widget.sheet.uuid);
if (file == null) {
throw Exception("Failed fetching pdf file");
}
document = await PdfDocument.openFile(file.path);
}
return true;
}
@override
void onFullScreenChanged(bool enabled, SystemUiMode? systemUiMode) {
setState(() {
widget.config.fullscreen = enabled;
storageHelper.writeConfig(widget.config);
});
}
void toggleFullscreen() {
FullScreen.setFullScreen(!widget.config.fullscreen);
}
void turnPage(int numTurns) {
setState(() {
currentPageNumber += numTurns;
currentPageNumber = currentPageNumber.clamp(1, numPages);
});
}
AppBar? buildAppBar() {
if (widget.config.fullscreen) {
return null;
}
return AppBar(
title: Text(widget.sheet.name),
actions: [
IconButton(
onPressed: () {
setState(() {
if (widget.config.twoPageMode) {
// TODO: notification that paint mode only in single page mode
} else {
paintMode = !paintMode;
}
});
},
icon: Icon(Icons.brush),
),
IconButton(
onPressed: () {
setState(() {
widget.config.twoPageMode = !widget.config.twoPageMode;
storageHelper.writeConfig(widget.config);
if (widget.config.twoPageMode) {
paintMode = false;
// TODO: notification that paint mode was deactivated since only possible in single page mode
}
});
},
icon: Icon(
widget.config.twoPageMode ? Icons.filter_1 : Icons.filter_2,
),
),
],
);
}
@override
Widget build(BuildContext context) {
return BtPedalShortcuts(
onTurnPageForward: () => turnPage(1),
onTurnPageBackward: () => turnPage(-1),
child: Scaffold(
appBar: buildAppBar(),
body: FutureBuilder(
future: documentLoaded,
builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
if (snapshot.hasData && document != null) {
numPages = document!.pages.length;
pages = Pages(
document: document!,
numPages: numPages,
config: widget.config,
currentPageNumber: currentPageNumber,
);
return Stack(
children: [
Stack(
children: [
Visibility(
visible: !paintMode,
child: TouchablePages(
pages: pages!,
onToggleFullscreen: () {
toggleFullscreen();
},
onExitSheetViewer: () {
Navigator.pop(context);
},
onTurnPage: (int numTurns) {
turnPage(numTurns);
},
),
),
Visibility(
visible: paintMode,
child: PaintablePages(pages: pages!),
),
],
),
],
);
} else if (snapshot.hasError) {
return Center(
child: Text(
style: Theme.of(
context,
).textTheme.displaySmall!.copyWith(color: Colors.red),
textAlign: TextAlign.center,
snapshot.error.toString(),
),
);
} else {
return const Center(child: CircularProgressIndicator());
}
},
),
),
);
}
}
typedef PageturnCallback = void Function(int numTurns);
class TouchablePages extends StatelessWidget {
final Pages pages;
final VoidCallback onToggleFullscreen;
final VoidCallback onExitSheetViewer;
final PageturnCallback onTurnPage;
const TouchablePages({
super.key,
required this.pages,
required this.onToggleFullscreen,
required this.onExitSheetViewer,
required this.onTurnPage,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
behavior:
HitTestBehavior.opaque, // Also register when outside of pdf pages
onTapUp: (TapUpDetails details) {
final mediaQuery = MediaQuery.of(context);
final pixelsPerInch =
mediaQuery.devicePixelRatio *
160; // 160 dpi = 1 logical inch baseline
final pixelsPerCm = pixelsPerInch / 2.54;
final touchAreaWidth = mediaQuery.size.width;
final (leftPageSize, rightPageSize) = pages.calcPageSizesScaled(
mediaQuery.size,
);
if (details.localPosition.dy < 2 * pixelsPerCm &&
details.localPosition.dx >= touchAreaWidth - 2 * pixelsPerCm &&
pages.config.fullscreen) {
onExitSheetViewer();
} else if (details.localPosition.dy < 2 * pixelsPerCm) {
onToggleFullscreen();
} else if (pages.config.twoPageMode &&
details.localPosition.dx <
touchAreaWidth / 2 - leftPageSize.width / 2) {
onTurnPage(-2);
} else if (details.localPosition.dx < touchAreaWidth / 2) {
onTurnPage(-1);
} else if (pages.config.twoPageMode &&
details.localPosition.dx >
touchAreaWidth / 2 + rightPageSize!.width / 2) {
onTurnPage(2);
} else {
onTurnPage(1);
}
},
child: pages,
);
}
}
class PaintablePages extends StatelessWidget {
final Pages pages;
const PaintablePages({super.key, required this.pages});
@override
Widget build(BuildContext context) {
return SizedBox.expand(
child: LayoutBuilder(
builder: (context, constraints) {
final maxSize = Size(constraints.maxWidth, constraints.maxHeight);
final (pageSizeScaled, _) = pages.calcPageSizesScaled(maxSize);
return DrawingBoard(
background: SizedBox(
width: pageSizeScaled.width,
height: pageSizeScaled.height,
child: pages,
),
// showDefaultTools: true,
// showDefaultActions: true,
boardConstrained: true,
minScale: 1,
maxScale: 3,
alignment: Alignment.topRight,
boardBoundaryMargin: EdgeInsets.all(0),
);
},
),
);
}
}
class Pages extends StatelessWidget {
final PdfDocument document;
final int numPages;
final int currentPageNumber;
final Config config;
const Pages({
super.key,
required this.document,
required this.numPages,
required this.currentPageNumber,
required this.config,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
spacing: 0,
children: [
Expanded(
child: Stack(
children: [
PdfPageView(
key: ValueKey(currentPageNumber),
document: document,
pageNumber: currentPageNumber,
maximumDpi: 300,
alignment: config.twoPageMode
? Alignment.centerRight
: Alignment.center,
),
Positioned.fill(
child: Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.only(bottom: 5),
child: Text('$currentPageNumber / $numPages'),
),
),
],
),
),
Visibility(
visible: config.twoPageMode == true,
child: Expanded(
child: Stack(
children: [
PdfPageView(
key: ValueKey(currentPageNumber + 1),
document: document,
pageNumber: currentPageNumber + 1,
maximumDpi: 300,
alignment: Alignment.centerLeft,
// alignment: Alignment.center,
),
Positioned.fill(
child: Container(
alignment: Alignment.bottomCenter,
padding: EdgeInsets.only(bottom: 5),
child: Text('${currentPageNumber + 1} / $numPages'),
),
),
],
),
),
),
],
);
}
(Size, Size?) calcPageSizesScaled(Size parentSize) {
if (config.twoPageMode) {
Size leftPageSizeUnscaled = _getPageSizeUnscaled(currentPageNumber);
Size rightPageSizeUnscaled = _getPageSizeUnscaled(currentPageNumber + 1);
Size combinedPageSizesUnscaled = Size(
leftPageSizeUnscaled.width + rightPageSizeUnscaled.width,
max(leftPageSizeUnscaled.height, rightPageSizeUnscaled.height),
);
Size combinedPageSizesScaled = _calcScaledPageSize(
parentSize,
combinedPageSizesUnscaled,
);
double scaleFactor =
combinedPageSizesScaled.width / combinedPageSizesUnscaled.width;
return (
leftPageSizeUnscaled * scaleFactor,
rightPageSizeUnscaled * scaleFactor,
);
} else {
return (
_calcScaledPageSize(
parentSize,
_getPageSizeUnscaled(currentPageNumber),
),
null,
);
}
}
Size _getPageSizeUnscaled(int pageNumber) {
return document.pages.elementAt(pageNumber).size;
}
Size _calcScaledPageSize(Size parentSize, Size pageSize) {
// page restricted by height
if (parentSize.aspectRatio > pageSize.aspectRatio) {
final height = parentSize.height;
final width = height * pageSize.aspectRatio;
return Size(width, height);
}
// page restricted by width
else {
final width = parentSize.width;
final height = width / pageSize.aspectRatio;
return Size(width, height);
}
}
}