blob: 7584762a41808be08ed8146eac2589b2f62e45c3 [file] [log] [blame] [edit]
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:args/args.dart';
import 'package:googleapis/slides/v1.dart' hide List;
import 'package:http/http.dart' show Client, get;
import 'package:http/retry.dart';
import 'package:test_integration/test_integration.dart';
Future<void> main(List<String> args) async {
final result = _parser.parse(args);
final presentationId = result.rest.single;
int? lastSlide;
if (result.wasParsed('last-slide')) {
lastSlide = int.tryParse(result['last-slide'] as String);
}
final renderHtml = result['html'] as String?;
await withClientFromUserCredentials(
[SlidesApi.presentationsReadonlyScope],
customClient: RetryClient(
Client(),
delay: (count) {
const duration = Duration(seconds: 60);
print('*** Retry #$count - waiting $duration');
return duration;
},
when:
(response) =>
response.statusCode == 503 || response.statusCode == 429,
),
(client) async {
final api = SlidesApi(client).presentations;
if (renderHtml != null) {
await _doHtml(
outputFilePath: renderHtml,
presentationId: presentationId,
resource: api,
lastSlide: lastSlide,
);
} else {
final result = await api.get(presentationId);
print(_printSlides(result.slides!, lastSlide: lastSlide).join('\n'));
}
},
);
}
Future<void> _doHtml({
required String outputFilePath,
required String presentationId,
required PresentationsResource resource,
required int? lastSlide,
}) async {
final result = await resource.get(presentationId);
final htmlOutput = File(outputFilePath);
if (htmlOutput.existsSync()) {
print('Please delete $htmlOutput first!');
exitCode = 1;
return;
}
final writer = await htmlOutput.open(mode: FileMode.writeOnlyAppend);
try {
await for (var line in _yieldSlideHtml(
result.slides!,
title: result.title,
api: resource,
presentationId: presentationId,
lastSlide: lastSlide,
)) {
writer.writeStringSync('$line\n');
}
} finally {
writer.closeSync();
}
}
Stream<String> _yieldSlideHtml(
List<Page> pages, {
String? title,
required PresentationsResource api,
required String presentationId,
required int? lastSlide,
}) async* {
var count = 0;
final titleElement =
title == null
? ''
: '''
<title>${const HtmlEscape(HtmlEscapeMode.element).convert(title)}</title>
''';
yield '''
<html lang="">
<head>
$titleElement
<style>
table, th, td {
border: 1px solid black;
border-collapse: collapse;
}
td, th {
padding: 1ex;
vertical-align: top;
}
td.slide-number {
text-align: center;
}
img {
border: 2px lightgray solid;
object-fit: contain;
max-width: 600px;
max-height: 350px;
}
th {
background: lightgray;
}
</style>
</head>
<body>
<table>
<tr>
<th>#</th>
<th>Script</th>
<th>Slide</th>
</tr>
''';
for (var slide in pages) {
if (slide.slideProperties?.isSkipped == true) {
// TODO: give an option to include skipped?
continue;
}
yield '<tr>';
// number
yield '<td class="slide-number">${++count}</td>';
stderr.writeln('Slide #$count');
// notes
yield '<td class="notes">';
for (var entry in slide.noteLines) {
yield '<p>${_htmlEscape.convert(entry)}</p>';
}
yield '</td>';
final thumb = await api.pages.getThumbnail(
presentationId,
slide.objectId!,
thumbnailProperties_thumbnailSize: 'MEDIUM',
);
final response = await get(Uri.parse(thumb.contentUrl!));
assert(response.headers['content-type'] == 'image/png');
assert(response.statusCode == 200);
final dataUri = Uri.dataFromBytes(
response.bodyBytes,
mimeType: response.headers['content-type']!,
);
// image
yield '<td><img src="$dataUri"></td>';
yield '</tr>';
if (lastSlide != null && count >= lastSlide) {
break;
}
}
yield '''
</table>
</body>
</html>
''';
}
const _htmlEscape = HtmlEscape(HtmlEscapeMode.element);
Iterable<String> _printSlides(List<Page> pages, {int? lastSlide}) sync* {
var count = 0;
for (var slide in pages) {
yield '*** SLIDE ${++count} ***\n';
for (var entry in slide.noteLines) {
yield entry;
}
yield '';
if (lastSlide != null && count >= lastSlide) {
break;
}
}
}
extension on Page {
Iterable<String> get noteLines sync* {
final elements = slideProperties?.notesPage?.pageElements;
if (elements == null) {
return;
}
for (var element in elements) {
final text = element.shape?.text;
if (text == null) {
continue;
}
yield* text.lines;
}
}
}
extension on TextContent {
Iterable<String> get lines sync* {
final value =
textElements!
.map((e) => e.textRun?.content)
.whereType<String>()
.where((element) => element.isNotEmpty)
.join()
.trim();
if (value.isEmpty) {
return;
}
// `value` may contain sections split by new-lines. Turn these into
// individual returned "lines" removing extra whitespace and
// blank lines
yield* LineSplitter.split(
value,
).map((line) => line.trim()).where((line) => line.isNotEmpty);
}
}
final _parser =
ArgParser()
..addOption('last-slide')
..addOption('html', help: 'HTML file to write content.');