SVG: view source XML
This commit is contained in:
parent
93e385d7c3
commit
0f773563f4
12 changed files with 245 additions and 14 deletions
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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,
|
||||
|
|
101
lib/widgets/common/aves_highlight.dart
Normal file
101
lib/widgets/common/aves_highlight.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
|
|
77
lib/widgets/fullscreen/source_viewer_page.dart
Normal file
77
lib/widgets/fullscreen/source_viewer_page.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
14
pubspec.lock
14
pubspec.lock
|
@ -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:
|
||||
|
|
|
@ -59,6 +59,7 @@ dependencies:
|
|||
firebase_analytics:
|
||||
firebase_crashlytics:
|
||||
flushbar:
|
||||
flutter_highlight:
|
||||
flutter_ijkplayer:
|
||||
# path: ../flutter_ijkplayer
|
||||
git:
|
||||
|
|
Loading…
Reference in a new issue