SVG: view source XML

This commit is contained in:
Thibault Deckers 2020-11-16 19:03:10 +09:00
parent 93e385d7c3
commit 0f773563f4
12 changed files with 245 additions and 14 deletions

View file

@ -182,7 +182,7 @@ abstract class ImageProvider {
}
if (newFields.isEmpty()) {
cont.resumeWithException(Exception("failed to get item details from provider at contentUri=$contentUri"))
cont.resumeWithException(Exception("failed to get item details from provider at contentUri=$contentUri (from newUri=$newUri)"))
} else {
cont.resume(newFields)
}

View file

@ -321,7 +321,8 @@ class MediaStoreImageProvider : ImageProvider() {
MediaStore.MediaColumns._ID,
MediaColumns.PATH,
MediaStore.MediaColumns.MIME_TYPE,
MediaStore.MediaColumns.SIZE, // TODO TLAD use `DISPLAY_NAME` instead/along `TITLE`?
MediaStore.MediaColumns.SIZE,
// TODO TLAD use `DISPLAY_NAME` instead/along `TITLE`?
MediaStore.MediaColumns.TITLE,
MediaStore.MediaColumns.WIDTH,
MediaStore.MediaColumns.HEIGHT,

View file

@ -23,6 +23,7 @@ class ImageEntry {
String _path, _directory, _filename, _extension;
int contentId;
final String sourceMimeType;
// TODO TLAD use SVG viewport as width/height
int width;
int height;
int sourceRotationDegrees;

View file

@ -107,6 +107,12 @@ class Constants {
licenseUrl: 'https://github.com/AndreHaueisen/flushbar/blob/master/LICENSE',
sourceUrl: 'https://github.com/AndreHaueisen/flushbar',
),
Dependency(
name: 'Flutter Highlight',
license: 'MIT',
licenseUrl: 'https://github.com/git-touch/highlight/blob/master/LICENSE',
sourceUrl: 'https://github.com/git-touch/highlight',
),
Dependency(
name: 'Flutter ijkplayer',
license: 'MIT',

View file

@ -282,6 +282,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
void _onCollectionActionSelected(CollectionAction action) async {
// wait for the popup menu to hide before proceeding with the action
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
switch (action) {
case CollectionAction.copy:
case CollectionAction.move:

View file

@ -2,6 +2,7 @@ import 'package:aves/model/image_entry.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/services/android_app_service.dart';
import 'package:aves/services/image_file_service.dart';
import 'package:aves/utils/durations.dart';
import 'package:aves/widgets/common/action_delegates/feedback.dart';
import 'package:aves/widgets/common/action_delegates/permission_aware.dart';
import 'package:aves/widgets/common/action_delegates/rename_entry_dialog.dart';
@ -9,7 +10,9 @@ import 'package:aves/widgets/common/aves_dialog.dart';
import 'package:aves/widgets/common/entry_actions.dart';
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
import 'package:aves/widgets/fullscreen/fullscreen_debug_page.dart';
import 'package:aves/widgets/fullscreen/source_viewer_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:pdf/pdf.dart';
@ -28,46 +31,52 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin {
bool get hasCollection => collection != null;
void onActionSelected(BuildContext context, ImageEntry entry, EntryAction action) {
void onActionSelected(BuildContext context, ImageEntry entry, EntryAction action) async {
// wait for the popup menu to hide before proceeding with the action
await Future.delayed(Durations.popupMenuAnimation * timeDilation);
switch (action) {
case EntryAction.toggleFavourite:
entry.toggleFavourite();
break;
case EntryAction.delete:
_showDeleteDialog(context, entry);
unawaited(_showDeleteDialog(context, entry));
break;
case EntryAction.edit:
AndroidAppService.edit(entry.uri, entry.mimeType);
unawaited(AndroidAppService.edit(entry.uri, entry.mimeType));
break;
case EntryAction.info:
showInfo();
break;
case EntryAction.rename:
_showRenameDialog(context, entry);
unawaited(_showRenameDialog(context, entry));
break;
case EntryAction.open:
AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype);
unawaited(AndroidAppService.open(entry.uri, entry.mimeTypeAnySubtype));
break;
case EntryAction.openMap:
AndroidAppService.openMap(entry.geoUri);
unawaited(AndroidAppService.openMap(entry.geoUri));
break;
case EntryAction.print:
_print(entry);
unawaited(_print(entry));
break;
case EntryAction.rotateCCW:
_rotate(context, entry, clockwise: false);
unawaited(_rotate(context, entry, clockwise: false));
break;
case EntryAction.rotateCW:
_rotate(context, entry, clockwise: true);
unawaited(_rotate(context, entry, clockwise: true));
break;
case EntryAction.flip:
_flip(context, entry);
unawaited(_flip(context, entry));
break;
case EntryAction.setAs:
AndroidAppService.setAs(entry.uri, entry.mimeType);
unawaited(AndroidAppService.setAs(entry.uri, entry.mimeType));
break;
case EntryAction.share:
AndroidAppService.share({entry});
unawaited(AndroidAppService.share({entry}));
break;
case EntryAction.viewSource:
_goToSourceViewer(context, entry);
break;
case EntryAction.debug:
_goToDebug(context, entry);
@ -181,6 +190,16 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin {
showFeedback(context, await entry.rename(newName) ? 'Done!' : 'Failed');
}
void _goToSourceViewer(BuildContext context, ImageEntry entry) {
Navigator.push(
context,
MaterialPageRoute(
settings: RouteSettings(name: SourceViewerPage.routeName),
builder: (context) => SourceViewerPage(entry: entry),
),
);
}
void _goToDebug(BuildContext context, ImageEntry entry) {
Navigator.push(
context,

View file

@ -0,0 +1,101 @@
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:highlight/highlight.dart' show highlight, Node;
// TODO TLAD use the TextSpan getter instead of this modified `HighlightView` when this is fixed: https://github.com/git-touch/highlight/issues/6
/// Highlight Flutter Widget
class AvesHighlightView extends StatelessWidget {
/// The original code to be highlighted
final String source;
/// Highlight language
///
/// It is recommended to give it a value for performance
///
/// [All available languages](https://github.com/pd4d10/highlight/tree/master/highlight/lib/languages)
final String language;
/// Highlight theme
///
/// [All available themes](https://github.com/pd4d10/highlight/blob/master/flutter_highlight/lib/themes)
final Map<String, TextStyle> theme;
/// Padding
final EdgeInsetsGeometry padding;
/// Text styles
///
/// Specify text styles such as font family and font size
final TextStyle textStyle;
AvesHighlightView(
String input, {
this.language,
this.theme = const {},
this.padding,
this.textStyle,
int tabSize = 8, // TODO: https://github.com/flutter/flutter/issues/50087
}) : source = input.replaceAll('\t', ' ' * tabSize);
List<TextSpan> _convert(List<Node> nodes) {
final spans = <TextSpan>[];
var currentSpans = spans;
final stack = <List<TextSpan>>[];
void _traverse(Node node) {
if (node.value != null) {
currentSpans.add(node.className == null ? TextSpan(text: node.value) : TextSpan(text: node.value, style: theme[node.className]));
} else if (node.children != null) {
final tmp = <TextSpan>[];
currentSpans.add(TextSpan(children: tmp, style: theme[node.className]));
stack.add(currentSpans);
currentSpans = tmp;
node.children.forEach((n) {
_traverse(n);
if (n == node.children.last) {
currentSpans = stack.isEmpty ? spans : stack.removeLast();
}
});
}
}
for (var node in nodes) {
_traverse(node);
}
return spans;
}
static const _rootKey = 'root';
static const _defaultFontColor = Color(0xff000000);
static const _defaultBackgroundColor = Color(0xffffffff);
// TODO: dart:io is not available at web platform currently
// See: https://github.com/flutter/flutter/issues/39998
// So we just use monospace here for now
static const _defaultFontFamily = 'monospace';
@override
Widget build(BuildContext context) {
var _textStyle = TextStyle(
fontFamily: _defaultFontFamily,
color: theme[_rootKey]?.color ?? _defaultFontColor,
);
if (textStyle != null) {
_textStyle = _textStyle.merge(textStyle);
}
return Container(
color: theme[_rootKey]?.backgroundColor ?? _defaultBackgroundColor,
padding: padding,
child: SelectableText.rich(
TextSpan(
style: _textStyle,
children: _convert(highlight.parse(source, language: language).nodes),
),
),
);
}
}

View file

@ -15,6 +15,7 @@ enum EntryAction {
setAs,
share,
toggleFavourite,
viewSource,
debug,
}
@ -31,6 +32,7 @@ class EntryActions {
EntryAction.delete,
EntryAction.rename,
EntryAction.print,
EntryAction.viewSource,
];
static const externalApp = [
@ -64,6 +66,8 @@ extension ExtraEntryAction on EntryAction {
return 'Print';
case EntryAction.share:
return 'Share';
case EntryAction.viewSource:
return 'View source';
// external app actions
case EntryAction.edit:
return 'Edit with…';
@ -101,6 +105,8 @@ extension ExtraEntryAction on EntryAction {
return AIcons.print;
case EntryAction.share:
return AIcons.share;
case EntryAction.viewSource:
return AIcons.vector;
// external app actions
case EntryAction.edit:
case EntryAction.open:

View file

@ -109,6 +109,8 @@ class FullscreenTopOverlay extends StatelessWidget {
return entry.canPrint;
case EntryAction.openMap:
return entry.hasGps;
case EntryAction.viewSource:
return entry.isSvg;
case EntryAction.share:
case EntryAction.info:
case EntryAction.open:
@ -191,6 +193,7 @@ class _TopOverlayRow extends StatelessWidget {
case EntryAction.rotateCW:
case EntryAction.flip:
case EntryAction.print:
case EntryAction.viewSource:
child = IconButton(
icon: Icon(action.getIcon()),
onPressed: onPressed,
@ -233,6 +236,7 @@ class _TopOverlayRow extends StatelessWidget {
case EntryAction.rotateCW:
case EntryAction.flip:
case EntryAction.print:
case EntryAction.viewSource:
case EntryAction.debug:
child = MenuRow(text: action.getText(), icon: action.getIcon());
break;

View file

@ -0,0 +1,77 @@
import 'dart:convert';
import 'package:aves/model/image_entry.dart';
import 'package:aves/services/image_file_service.dart';
import 'package:aves/widgets/common/aves_highlight.dart';
import 'package:flutter/material.dart';
import 'package:flutter_highlight/themes/darcula.dart';
class SourceViewerPage extends StatefulWidget {
static const routeName = '/fullscreen/source';
final ImageEntry entry;
const SourceViewerPage({
@required this.entry,
});
@override
_SourceViewerPageState createState() => _SourceViewerPageState();
}
class _SourceViewerPageState extends State<SourceViewerPage> {
Future<String> _loader;
ImageEntry get entry => widget.entry;
@override
void initState() {
super.initState();
_loader = ImageFileService.getImage(entry.uri, entry.mimeType, 0, false).then(utf8.decode);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Source'),
),
body: SafeArea(
child: FutureBuilder<String>(
future: _loader,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
if (snapshot.connectionState != ConnectionState.done) {
return SizedBox.shrink();
}
final source = snapshot.data;
final highlightView = AvesHighlightView(
source,
language: 'xml',
theme: darculaTheme,
padding: EdgeInsets.all(8),
textStyle: TextStyle(
fontSize: 12,
),
tabSize: 4,
);
return Container(
constraints: BoxConstraints.expand(),
child: Scrollbar(
child: SingleChildScrollView(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: highlightView,
),
),
),
);
},
),
),
);
}
}

View file

@ -288,6 +288,13 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
flutter_highlight:
dependency: "direct main"
description:
name: flutter_highlight
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.0"
flutter_ijkplayer:
dependency: "direct main"
description:
@ -389,6 +396,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.4"
highlight:
dependency: transitive
description:
name: highlight
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.0"
http:
dependency: transitive
description:

View file

@ -59,6 +59,7 @@ dependencies:
firebase_analytics:
firebase_crashlytics:
flushbar:
flutter_highlight:
flutter_ijkplayer:
# path: ../flutter_ijkplayer
git: