413 lines
12 KiB
Dart
413 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';
|
|
|
|
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);
|
|
|
|
// Load saved fullscreen
|
|
FullScreen.setFullScreen(widget.config.fullscreen);
|
|
|
|
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) {
|
|
final data = await widget.apiClient.fetchPdfFileData(widget.sheet.uuid);
|
|
|
|
document = await PdfDocument.openData(data);
|
|
} else {
|
|
final file = await widget.apiClient.getPdfFileCached(widget.sheet.uuid);
|
|
|
|
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) {
|
|
log.warning("Error loading pdf:", snapshot.error);
|
|
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; // Starts at 1
|
|
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;
|
|
if (numPages > currentPageNumber) {
|
|
rightPageSizeUnscaled = _getPageSizeUnscaled(currentPageNumber + 1);
|
|
} else {
|
|
rightPageSizeUnscaled = leftPageSizeUnscaled;
|
|
}
|
|
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 - 1).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);
|
|
}
|
|
}
|
|
}
|