mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-06-28 18:03:14 +08:00
feat(image): ✨ Added Details view for Images
Users can zoom, quarter-rotate and freely rotate Images Updated flutterw Refs GitJournal#410 GitJournal#386
This commit is contained in:

committed by
Roland Fredenhagen

parent
5afe819aa4
commit
e8fd11c85b
2
.flutter
2
.flutter
Submodule .flutter updated: 9b2d32b605...4d7946a68d
@ -49,6 +49,12 @@ settings:
|
|||||||
validator:
|
validator:
|
||||||
empty: Tags cannot be empty.
|
empty: Tags cannot be empty.
|
||||||
same: Tag cannot be identical to a “DoNotCaption-Tag”.
|
same: Tag cannot be identical to a “DoNotCaption-Tag”.
|
||||||
|
detailsView:
|
||||||
|
header: Detail View
|
||||||
|
maxZoom: Maximal zoom level
|
||||||
|
rotateGestures:
|
||||||
|
title: Rotate Image with gestures
|
||||||
|
subtitle: Rotate by moving two fingers in a circle
|
||||||
theming:
|
theming:
|
||||||
title: Image Theming
|
title: Image Theming
|
||||||
subtitle: Configure how images are themed
|
subtitle: Configure how images are themed
|
||||||
|
@ -14,12 +14,17 @@ See the License for the specific language governing permissions and
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
|
||||||
import 'package:gitjournal/screens/settings_display_images_caption.dart';
|
import 'package:gitjournal/screens/settings_display_images_caption.dart';
|
||||||
import 'package:gitjournal/screens/settings_display_images_theming.dart';
|
import 'package:gitjournal/screens/settings_display_images_theming.dart';
|
||||||
|
import 'package:gitjournal/screens/settings_screen.dart';
|
||||||
|
import 'package:gitjournal/settings.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
class SettingsDisplayImagesScreen extends StatefulWidget {
|
class SettingsDisplayImagesScreen extends StatefulWidget {
|
||||||
@override
|
@override
|
||||||
@ -29,11 +34,11 @@ class SettingsDisplayImagesScreen extends StatefulWidget {
|
|||||||
|
|
||||||
class SettingsDisplayImagesScreenState
|
class SettingsDisplayImagesScreenState
|
||||||
extends State<SettingsDisplayImagesScreen> {
|
extends State<SettingsDisplayImagesScreen> {
|
||||||
final doNotThemeTagsKey = GlobalKey<FormFieldState<String>>();
|
|
||||||
final doThemeTagsKey = GlobalKey<FormFieldState<String>>();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final settings = Provider.of<Settings>(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
final body = ListView(children: <Widget>[
|
final body = ListView(children: <Widget>[
|
||||||
ListTile(
|
ListTile(
|
||||||
title: Text(tr("settings.display.images.theming.title")),
|
title: Text(tr("settings.display.images.theming.title")),
|
||||||
@ -59,6 +64,49 @@ class SettingsDisplayImagesScreenState
|
|||||||
Navigator.of(context).push(route);
|
Navigator.of(context).push(route);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
SettingsHeader(tr('settings.display.images.detailsView.header')),
|
||||||
|
ListTile(
|
||||||
|
title: Text(tr("settings.display.images.detailsView.maxZoom")),
|
||||||
|
subtitle: Row(children: [
|
||||||
|
Expanded(
|
||||||
|
child: Slider.adaptive(
|
||||||
|
value: min(settings.maxImageZoom, 30),
|
||||||
|
onChanged: (v) {
|
||||||
|
setState(() {
|
||||||
|
settings.maxImageZoom = v == 30 ? double.infinity : v;
|
||||||
|
settings.save();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
max: 30,
|
||||||
|
min: 1,
|
||||||
|
activeColor: theme.accentColor,
|
||||||
|
inactiveColor: theme.disabledColor,
|
||||||
|
)),
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
child: settings.maxImageZoom == double.infinity
|
||||||
|
? Icon(
|
||||||
|
Icons.all_inclusive,
|
||||||
|
color: theme.accentColor,
|
||||||
|
)
|
||||||
|
: Text(
|
||||||
|
NumberFormat("##.0").format(settings.maxImageZoom),
|
||||||
|
style: theme.textTheme.subtitle2
|
||||||
|
.copyWith(color: theme.accentColor),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
))
|
||||||
|
])),
|
||||||
|
SwitchListTile(
|
||||||
|
title: Text(
|
||||||
|
tr('settings.display.images.detailsView.rotateGestures.title')),
|
||||||
|
subtitle: Text(
|
||||||
|
tr('settings.display.images.detailsView.rotateGestures.subtitle')),
|
||||||
|
value: settings.rotateImageGestures,
|
||||||
|
onChanged: (bool newVal) {
|
||||||
|
settings.rotateImageGestures = newVal;
|
||||||
|
settings.save();
|
||||||
|
},
|
||||||
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
@ -70,6 +70,10 @@ class Settings extends ChangeNotifier {
|
|||||||
SettingsHomeScreen homeScreen = SettingsHomeScreen.Default;
|
SettingsHomeScreen homeScreen = SettingsHomeScreen.Default;
|
||||||
SettingsTheme theme = SettingsTheme.Default;
|
SettingsTheme theme = SettingsTheme.Default;
|
||||||
|
|
||||||
|
// Display - Image
|
||||||
|
bool rotateImageGestures = false;
|
||||||
|
double maxImageZoom = 10;
|
||||||
|
|
||||||
// Display - Image - Theming
|
// Display - Image - Theming
|
||||||
bool themeRasterGraphics = false;
|
bool themeRasterGraphics = false;
|
||||||
SettingsImageTextType themeOverrideTagLocation =
|
SettingsImageTextType themeOverrideTagLocation =
|
||||||
@ -171,6 +175,11 @@ class Settings extends ChangeNotifier {
|
|||||||
SettingsHomeScreen.fromInternalString(_getString(pref, "homeScreen"));
|
SettingsHomeScreen.fromInternalString(_getString(pref, "homeScreen"));
|
||||||
theme = SettingsTheme.fromInternalString(_getString(pref, "theme"));
|
theme = SettingsTheme.fromInternalString(_getString(pref, "theme"));
|
||||||
|
|
||||||
|
// Display - Image
|
||||||
|
rotateImageGestures =
|
||||||
|
_getBool(pref, "rotateImageGestures") ?? rotateImageGestures;
|
||||||
|
maxImageZoom = _getDouble(pref, "maxImageZoom") ?? maxImageZoom;
|
||||||
|
|
||||||
// Display - Image - Theming
|
// Display - Image - Theming
|
||||||
themeRasterGraphics =
|
themeRasterGraphics =
|
||||||
_getBool(pref, "themeRasterGraphics") ?? themeRasterGraphics;
|
_getBool(pref, "themeRasterGraphics") ?? themeRasterGraphics;
|
||||||
@ -244,6 +253,10 @@ class Settings extends ChangeNotifier {
|
|||||||
return pref.getInt(id + '_' + key);
|
return pref.getInt(id + '_' + key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double _getDouble(SharedPreferences pref, String key) {
|
||||||
|
return pref.getDouble(id + '_' + key);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> save() async {
|
Future<void> save() async {
|
||||||
var pref = await SharedPreferences.getInstance();
|
var pref = await SharedPreferences.getInstance();
|
||||||
var defaultSet = Settings(id);
|
var defaultSet = Settings(id);
|
||||||
@ -312,6 +325,12 @@ class Settings extends ChangeNotifier {
|
|||||||
await _setString(pref, "theme", theme.toInternalString(),
|
await _setString(pref, "theme", theme.toInternalString(),
|
||||||
defaultSet.theme.toInternalString());
|
defaultSet.theme.toInternalString());
|
||||||
|
|
||||||
|
// Display - Image
|
||||||
|
await _setBool(pref, "rotateImageGestures", rotateImageGestures,
|
||||||
|
defaultSet.rotateImageGestures);
|
||||||
|
await _setDouble(
|
||||||
|
pref, "maxImageZoom", maxImageZoom, defaultSet.maxImageZoom);
|
||||||
|
|
||||||
// Display - Image - Theme
|
// Display - Image - Theme
|
||||||
await _setBool(pref, "themeRasterGraphics", themeRasterGraphics,
|
await _setBool(pref, "themeRasterGraphics", themeRasterGraphics,
|
||||||
defaultSet.themeRasterGraphics);
|
defaultSet.themeRasterGraphics);
|
||||||
@ -427,6 +446,20 @@ class Settings extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _setDouble(
|
||||||
|
SharedPreferences pref,
|
||||||
|
String key,
|
||||||
|
double value,
|
||||||
|
double defaultValue,
|
||||||
|
) async {
|
||||||
|
key = id + '_' + key;
|
||||||
|
if (value == defaultValue) {
|
||||||
|
await pref.remove(key);
|
||||||
|
} else {
|
||||||
|
await pref.setDouble(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _setStringSet(
|
Future<void> _setStringSet(
|
||||||
SharedPreferences pref,
|
SharedPreferences pref,
|
||||||
String key,
|
String key,
|
||||||
@ -471,6 +504,9 @@ class Settings extends ChangeNotifier {
|
|||||||
'markdownLastUsedView': markdownLastUsedView.toInternalString(),
|
'markdownLastUsedView': markdownLastUsedView.toInternalString(),
|
||||||
'homeScreen': homeScreen.toInternalString(),
|
'homeScreen': homeScreen.toInternalString(),
|
||||||
'theme': theme.toInternalString(),
|
'theme': theme.toInternalString(),
|
||||||
|
// Display - Image
|
||||||
|
'rotateImageGestures': rotateImageGestures.toString(),
|
||||||
|
'maxImageZoom': maxImageZoom.toString(),
|
||||||
// Display - Image - Theming
|
// Display - Image - Theming
|
||||||
'themeRasterGraphics': themeRasterGraphics.toString(),
|
'themeRasterGraphics': themeRasterGraphics.toString(),
|
||||||
'themeOverrideTagLocation': themeOverrideTagLocation.toInternalString(),
|
'themeOverrideTagLocation': themeOverrideTagLocation.toInternalString(),
|
||||||
|
@ -1,480 +0,0 @@
|
|||||||
/*
|
|
||||||
Copyright 2020-2021 Roland Fredenhagen <important@van-fredenhagen.de>
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
you may not use this file except in compliance with the License.
|
|
||||||
You may obtain a copy of the License at
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter/rendering.dart';
|
|
||||||
import 'package:flutter/widgets.dart';
|
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
|
||||||
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
|
||||||
import 'package:flutter_svg/flutter_svg.dart';
|
|
||||||
import 'package:flutter_svg/svg.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
import 'package:gitjournal/settings.dart';
|
|
||||||
import 'package:gitjournal/utils/hero_dialog.dart';
|
|
||||||
import 'package:gitjournal/utils/logger.dart';
|
|
||||||
|
|
||||||
class ThemableImage extends StatelessWidget {
|
|
||||||
final double width;
|
|
||||||
final double height;
|
|
||||||
final String altText;
|
|
||||||
final String tooltip;
|
|
||||||
final Future<dynamic> data;
|
|
||||||
|
|
||||||
ThemableImage._(this.data, this.width, this.height, altText, tooltip)
|
|
||||||
: altText = altText ?? "",
|
|
||||||
tooltip = tooltip ?? "";
|
|
||||||
|
|
||||||
factory ThemableImage(Uri uri, String imageDirectory,
|
|
||||||
{double width, double height, String altText, String titel}) {
|
|
||||||
final file = ((uri.isScheme("http") || uri.isScheme("https"))
|
|
||||||
? DefaultCacheManager().getSingleFile(uri.toString())
|
|
||||||
: Future.sync(
|
|
||||||
() => File.fromUri(Uri.parse(imageDirectory + uri.toString()))));
|
|
||||||
|
|
||||||
final data = file.then(
|
|
||||||
(value) => value.path.endsWith(".svg") ? value.readAsString() : file);
|
|
||||||
|
|
||||||
return ThemableImage._(data, width, height, altText, titel);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final settings = Provider.of<Settings>(context);
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final dark = theme.brightness == Brightness.dark;
|
|
||||||
|
|
||||||
ThemeOverride override = ThemeOverride.None;
|
|
||||||
if (settings.themeOverrideTagLocation == SettingsImageTextType.Alt ||
|
|
||||||
settings.themeOverrideTagLocation == SettingsImageTextType.AltTool) {
|
|
||||||
if (hasTag(altText, settings.doThemeTags)) {
|
|
||||||
override = ThemeOverride.Do;
|
|
||||||
} else if (hasTag(altText, settings.doNotThemeTags)) {
|
|
||||||
override = ThemeOverride.No;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (settings.themeOverrideTagLocation == SettingsImageTextType.Tooltip ||
|
|
||||||
settings.themeOverrideTagLocation == SettingsImageTextType.AltTool) {
|
|
||||||
if (hasTag(tooltip, settings.doThemeTags)) {
|
|
||||||
override = ThemeOverride.Do;
|
|
||||||
} else if (hasTag(tooltip, settings.doNotThemeTags)) {
|
|
||||||
override = ThemeOverride.No;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return LayoutBuilder(
|
|
||||||
builder: (context, constraints) {
|
|
||||||
final small =
|
|
||||||
constraints.maxWidth < MediaQuery.of(context).size.width - 40;
|
|
||||||
final image = FutureBuilder(
|
|
||||||
future: data,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
if (snapshot.hasError) {
|
|
||||||
String errorMessage = snapshot.error.toString();
|
|
||||||
Log.e(errorMessage);
|
|
||||||
if (snapshot.error is HttpExceptionWithStatus) {
|
|
||||||
final httpError = snapshot.error as HttpExceptionWithStatus;
|
|
||||||
errorMessage = tr("widgets.imageRenderer.httpError",
|
|
||||||
namedArgs: {
|
|
||||||
"status": httpError.statusCode.toString(),
|
|
||||||
"url": httpError.uri.toString()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return SizedBox(
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Center(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.error,
|
|
||||||
color: theme.errorColor,
|
|
||||||
size: 36,
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
errorMessage,
|
|
||||||
style: theme.textTheme.bodyText1
|
|
||||||
.copyWith(color: theme.errorColor),
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
)
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (snapshot.hasData) {
|
|
||||||
Widget im;
|
|
||||||
if (snapshot.data is String) {
|
|
||||||
im = _handleSvg(
|
|
||||||
snapshot.data, width, height, context, override);
|
|
||||||
} else {
|
|
||||||
im = Image.file(snapshot.data, width: width, height: height);
|
|
||||||
if ((settings.themeRasterGraphics ||
|
|
||||||
override == ThemeOverride.Do) &&
|
|
||||||
override != ThemeOverride.No &&
|
|
||||||
dark) {
|
|
||||||
im = themeFilter(im, theme.canvasColor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (small) {
|
|
||||||
return GestureDetector(
|
|
||||||
child: Hero(tag: im, child: im),
|
|
||||||
onTap: () {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
HeroDialogRoute(
|
|
||||||
builder: (context) => GestureDetector(
|
|
||||||
child: Hero(tag: im, child: im),
|
|
||||||
onTap: () => Navigator.pop(context),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return im;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SizedBox(
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
child: const Padding(
|
|
||||||
padding: EdgeInsets.all(16),
|
|
||||||
child: Center(child: CircularProgressIndicator())));
|
|
||||||
});
|
|
||||||
|
|
||||||
if (small || !settings.overlayCaption) {
|
|
||||||
final caption = _imageCaption(context, altText, tooltip, false);
|
|
||||||
return Column(children: [image, if (caption != null) caption]);
|
|
||||||
}
|
|
||||||
final caption = _imageCaption(context, altText, tooltip, true);
|
|
||||||
return Stack(
|
|
||||||
alignment: AlignmentDirectional.bottomEnd,
|
|
||||||
children: [image, if (caption != null) caption]);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _cleanCaption(BuildContext context, String caption) {
|
|
||||||
final settings = Provider.of<Settings>(context);
|
|
||||||
final tags = [
|
|
||||||
...settings.doThemeTags,
|
|
||||||
...settings.doNotThemeTags,
|
|
||||||
...settings.doCaptionTags,
|
|
||||||
...settings.doNotCaptionTags
|
|
||||||
];
|
|
||||||
return caption
|
|
||||||
.replaceAll(
|
|
||||||
RegExp(
|
|
||||||
r"\s*(?<=\s|\b)(" +
|
|
||||||
tags.map(RegExp.escape).join("|") +
|
|
||||||
r")(?=\s|\b)\s*",
|
|
||||||
caseSensitive: false),
|
|
||||||
" ")
|
|
||||||
.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _imageCaption(
|
|
||||||
BuildContext context, String altText, String tooltip, bool overlay) {
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
final settings = Provider.of<Settings>(context);
|
|
||||||
final dark = theme.brightness == Brightness.dark;
|
|
||||||
|
|
||||||
bool altTextCaption =
|
|
||||||
settings.useAsCaption == SettingsImageTextType.AltTool ||
|
|
||||||
settings.useAsCaption == SettingsImageTextType.AltTool;
|
|
||||||
if (hasTag(altText, settings.doCaptionTags)) {
|
|
||||||
altTextCaption = true;
|
|
||||||
} else if (hasTag(altText, settings.doNotCaptionTags)) {
|
|
||||||
altTextCaption = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool tooltipCaption =
|
|
||||||
settings.useAsCaption == SettingsImageTextType.Tooltip ||
|
|
||||||
settings.useAsCaption == SettingsImageTextType.AltTool;
|
|
||||||
if (hasTag(tooltip, settings.doCaptionTags)) {
|
|
||||||
tooltipCaption = true;
|
|
||||||
} else if (hasTag(tooltip, settings.doNotCaptionTags)) {
|
|
||||||
tooltipCaption = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
altText = altTextCaption ? _cleanCaption(context, altText) : "";
|
|
||||||
tooltip = tooltipCaption ? _cleanCaption(context, tooltip) : "";
|
|
||||||
String text = "";
|
|
||||||
if (altText.isNotEmpty && tooltip.isNotEmpty) {
|
|
||||||
text = tr("widgets.imageRenderer.caption",
|
|
||||||
namedArgs: settings.tooltipFirst
|
|
||||||
? {"first": tooltip, "second": altText}
|
|
||||||
: {"first": altText, "second": tooltip});
|
|
||||||
} else {
|
|
||||||
text = altText + tooltip;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (text.isNotEmpty) {
|
|
||||||
if (!overlay) {
|
|
||||||
return Text(text, style: theme.textTheme.caption);
|
|
||||||
}
|
|
||||||
|
|
||||||
return LayoutBuilder(builder: (context, constraints) {
|
|
||||||
final maxWidth = constraints.constrainWidth(200);
|
|
||||||
const padding = 6.0, margin = 4.0, borderRadius = 5.0;
|
|
||||||
final blur = settings.blurBehindCaption ? 2.0 : 0.0;
|
|
||||||
|
|
||||||
final overflown = (TextPainter(
|
|
||||||
text: TextSpan(text: text),
|
|
||||||
maxLines: 2,
|
|
||||||
textScaleFactor: MediaQuery.of(context).textScaleFactor,
|
|
||||||
textDirection: Directionality.of(context))
|
|
||||||
..layout(maxWidth: maxWidth - 2 * (padding + margin)))
|
|
||||||
.didExceedMaxLines;
|
|
||||||
|
|
||||||
final bColor = settings.transparentCaption
|
|
||||||
? (dark ? Colors.black : Colors.white).withAlpha(150)
|
|
||||||
: theme.canvasColor;
|
|
||||||
final box = ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(borderRadius),
|
|
||||||
child: BackdropFilter(
|
|
||||||
filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
|
|
||||||
child: Container(
|
|
||||||
color: bColor,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(padding),
|
|
||||||
child: Text(
|
|
||||||
text,
|
|
||||||
maxLines: 2,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: theme.textTheme.bodyText2,
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (!overflown) {
|
|
||||||
return ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
|
||||||
child: Container(margin: const EdgeInsets.all(margin), child: box));
|
|
||||||
}
|
|
||||||
|
|
||||||
final caption = Hero(tag: "caption", child: box);
|
|
||||||
|
|
||||||
return ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(margin),
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
Navigator.push(context, HeroDialogRoute(builder: (context) {
|
|
||||||
return Dialog(
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
child: GestureDetector(
|
|
||||||
onTap: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
child: caption,
|
|
||||||
));
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
child: caption,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _handleSvg(final String string, double width, final double height,
|
|
||||||
final BuildContext context, final ThemeOverride override) {
|
|
||||||
final settings = Provider.of<Settings>(context);
|
|
||||||
final theme = Theme.of(context);
|
|
||||||
width ??= MediaQuery.of(context).size.width;
|
|
||||||
final dark = theme.brightness == Brightness.dark;
|
|
||||||
if (settings.themeVectorGraphics == SettingsThemeVectorGraphics.Off &&
|
|
||||||
override != ThemeOverride.Do ||
|
|
||||||
override == ThemeOverride.No) {
|
|
||||||
return SvgPicture.string(string, width: width, height: height);
|
|
||||||
}
|
|
||||||
if (settings.themeVectorGraphics == SettingsThemeVectorGraphics.Filter &&
|
|
||||||
dark) {
|
|
||||||
return themeFilter(SvgPicture.string(string, width: width, height: height),
|
|
||||||
theme.canvasColor);
|
|
||||||
}
|
|
||||||
final transformColor = dark
|
|
||||||
? themeDark(theme.canvasColor, settings.vectorGraphicsAdjustColors)
|
|
||||||
: settings.matchCanvasColor
|
|
||||||
? whiteToCanvas(theme.canvasColor)
|
|
||||||
: noTheme;
|
|
||||||
return SvgPicture(
|
|
||||||
StringPicture((data, colorFilter, key) async {
|
|
||||||
DrawableRoot svgRoot = await svg.fromSvgString(data, key);
|
|
||||||
if (settings.themeSvgWithBackground ||
|
|
||||||
override == ThemeOverride.Do ||
|
|
||||||
!hasBackground(svgRoot, svgRoot.viewport.viewBox.width,
|
|
||||||
svgRoot.viewport.viewBox.height)) {
|
|
||||||
svgRoot = themeDrawable(svgRoot, transformColor);
|
|
||||||
}
|
|
||||||
final Picture pic = svgRoot.toPicture(
|
|
||||||
clipToViewBox: false,
|
|
||||||
colorFilter: colorFilter,
|
|
||||||
);
|
|
||||||
return PictureInfo(
|
|
||||||
picture: pic,
|
|
||||||
viewport: svgRoot.viewport.viewBoxRect,
|
|
||||||
size: svgRoot.viewport.size,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
string +
|
|
||||||
'<?theme darkMode="$dark" ' +
|
|
||||||
'override="$override" ' +
|
|
||||||
'opaqueBackground="${settings.themeSvgWithBackground}" ' +
|
|
||||||
'whiteToCanvas="${settings.matchCanvasColor}" ' +
|
|
||||||
'adjustColors="${settings.vectorGraphicsAdjustColors.toInternalString()}"?>'),
|
|
||||||
width: width,
|
|
||||||
height: height,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Color Function(Color color) whiteToCanvas(Color canvasColor) =>
|
|
||||||
(Color color) => color.value == 0xffffffff ? canvasColor : color;
|
|
||||||
|
|
||||||
Color noTheme(Color color) => color;
|
|
||||||
|
|
||||||
Color Function(Color color) themeDark(
|
|
||||||
Color canvasColor, SettingsVectorGraphicsAdjustColors adjustColors) =>
|
|
||||||
(Color color) {
|
|
||||||
final hslColor = HSLColor.fromColor(color);
|
|
||||||
final backGroundLightness = HSLColor.fromColor(canvasColor).lightness;
|
|
||||||
if (adjustColors == SettingsVectorGraphicsAdjustColors.BnW) {
|
|
||||||
if (hslColor.lightness > 0.95 && hslColor.saturation < 0.02) {
|
|
||||||
return HSLColor.fromAHSL(hslColor.alpha, 0, 0, backGroundLightness)
|
|
||||||
.toColor();
|
|
||||||
}
|
|
||||||
if (hslColor.lightness < 0.02) {
|
|
||||||
return HSLColor.fromAHSL(hslColor.alpha, 0, 0, 1).toColor();
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
if (adjustColors == SettingsVectorGraphicsAdjustColors.Grays) {
|
|
||||||
if (hslColor.saturation < 0.02 || hslColor.lightness < 0.02) {
|
|
||||||
return HSLColor.fromAHSL(hslColor.alpha, 0, 0,
|
|
||||||
1 - (hslColor.lightness * (1 - backGroundLightness)))
|
|
||||||
.toColor();
|
|
||||||
}
|
|
||||||
return color;
|
|
||||||
}
|
|
||||||
|
|
||||||
return HSLColor.fromAHSL(
|
|
||||||
hslColor.alpha,
|
|
||||||
hslColor.hue,
|
|
||||||
hslColor.saturation,
|
|
||||||
1 - (hslColor.lightness * (1 - backGroundLightness)))
|
|
||||||
.toColor();
|
|
||||||
};
|
|
||||||
|
|
||||||
bool hasBackground(Drawable draw, double width, double height) {
|
|
||||||
if (draw is DrawableShape) {
|
|
||||||
final drawShape = draw;
|
|
||||||
return drawShape.style.fill != null &&
|
|
||||||
drawShape.style.fill.color.alpha > 0.99 &&
|
|
||||||
[
|
|
||||||
Offset(0 + width * 0.01, 0 + height * 0.01),
|
|
||||||
Offset(width - width * 0.01, 0 + height * 0.01),
|
|
||||||
Offset(width - width * 0.01, height - height * 0.01),
|
|
||||||
Offset(0 + width * 0.01, height - height * 0.01),
|
|
||||||
].every(drawShape.path.contains);
|
|
||||||
}
|
|
||||||
if (draw is DrawableParent) {
|
|
||||||
final drawParent = draw;
|
|
||||||
return drawParent.children
|
|
||||||
.any((element) => hasBackground(element, width, height));
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool hasTag(String text, Set<String> tags) {
|
|
||||||
return tags
|
|
||||||
.map((e) => RegExp(r"(?<=\s|\b)" + RegExp.escape(e) + r"(?=\s|\b)",
|
|
||||||
caseSensitive: false))
|
|
||||||
.any((e) => e.hasMatch(text));
|
|
||||||
}
|
|
||||||
|
|
||||||
Drawable themeDrawable(
|
|
||||||
Drawable draw, Color Function(Color color) transformColor) {
|
|
||||||
if (draw is DrawableStyleable && !(draw is DrawableGroup)) {
|
|
||||||
final DrawableStyleable drawStylable = draw;
|
|
||||||
draw = drawStylable.mergeStyle(DrawableStyle(
|
|
||||||
stroke: drawStylable.style.stroke != null &&
|
|
||||||
drawStylable.style.stroke.color != null
|
|
||||||
? DrawablePaint.merge(
|
|
||||||
DrawablePaint(drawStylable.style.stroke.style,
|
|
||||||
color: transformColor(drawStylable.style.stroke.color)),
|
|
||||||
drawStylable.style.stroke)
|
|
||||||
: null,
|
|
||||||
fill: drawStylable.style.fill != null &&
|
|
||||||
drawStylable.style.fill.color != null
|
|
||||||
? DrawablePaint.merge(
|
|
||||||
DrawablePaint(drawStylable.style.fill.style,
|
|
||||||
color: transformColor(drawStylable.style.fill.color)),
|
|
||||||
drawStylable.style.fill)
|
|
||||||
: null));
|
|
||||||
}
|
|
||||||
if (draw is DrawableParent) {
|
|
||||||
final DrawableParent drawParent = draw;
|
|
||||||
final children = drawParent.children
|
|
||||||
.map((e) => themeDrawable(e, transformColor))
|
|
||||||
.toList(growable: false);
|
|
||||||
if (draw is DrawableRoot) {
|
|
||||||
final DrawableRoot drawRoot = draw;
|
|
||||||
draw = DrawableRoot(drawRoot.id, drawRoot.viewport, children,
|
|
||||||
drawRoot.definitions, drawRoot.style,
|
|
||||||
transform: drawRoot.transform);
|
|
||||||
} else if (draw is DrawableGroup) {
|
|
||||||
final DrawableGroup drawGroup = draw;
|
|
||||||
draw = DrawableGroup(drawGroup.id, children, drawGroup.style,
|
|
||||||
transform: drawGroup.transform);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return draw;
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget themeFilter(Widget widget, Color background) {
|
|
||||||
final lightness = (1 - HSLColor.fromColor(background).lightness) * 255;
|
|
||||||
// TODO Switch to HSL Filter, when availible (https://github.com/flutter/flutter/issues/76729)
|
|
||||||
final stack = ColorFiltered(
|
|
||||||
colorFilter: ColorFilter.matrix(<double>[
|
|
||||||
-(lightness / 255), 0, 0, 0, 255, // R
|
|
||||||
0, -(lightness / 255), 0, 0, 255, // G
|
|
||||||
0, 0, -(lightness / 255), 0, 255, // B
|
|
||||||
0, 0, 0, 1, 0 // A
|
|
||||||
]),
|
|
||||||
child: widget);
|
|
||||||
return stack;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ThemeOverride { None, Do, No }
|
|
178
lib/widgets/images/image_caption.dart
Normal file
178
lib/widgets/images/image_caption.dart
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020-2021 Roland Fredenhagen <important@van-fredenhagen.de>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gitjournal/settings.dart';
|
||||||
|
import 'package:gitjournal/utils/hero_dialog.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class ImageCaption extends StatelessWidget {
|
||||||
|
final String altText;
|
||||||
|
final String tooltip;
|
||||||
|
final bool overlay;
|
||||||
|
ImageCaption(this.altText, this.tooltip, this.overlay);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final settings = Provider.of<Settings>(context);
|
||||||
|
|
||||||
|
final text = captionText(context, altText, tooltip);
|
||||||
|
|
||||||
|
if (!overlay) {
|
||||||
|
return Text(text, style: theme.textTheme.caption);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LayoutBuilder(builder: (context, constraints) {
|
||||||
|
final maxWidth = constraints.constrainWidth(200);
|
||||||
|
const padding = 6.0, margin = 4.0, borderRadius = 5.0;
|
||||||
|
final blur = settings.blurBehindCaption ? 2.0 : 0.0;
|
||||||
|
|
||||||
|
final overflown = (TextPainter(
|
||||||
|
text: TextSpan(text: text),
|
||||||
|
maxLines: 2,
|
||||||
|
textScaleFactor: MediaQuery.of(context).textScaleFactor,
|
||||||
|
textDirection: Directionality.of(context))
|
||||||
|
..layout(maxWidth: maxWidth - 2 * (padding + margin)))
|
||||||
|
.didExceedMaxLines;
|
||||||
|
|
||||||
|
final box = ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
|
child: BackdropFilter(
|
||||||
|
filter: ImageFilter.blur(sigmaX: blur, sigmaY: blur),
|
||||||
|
child: Container(
|
||||||
|
color: _overlayBackgroundColor(context),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(padding),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: theme.textTheme.bodyText2,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (!overflown) {
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||||
|
child: Container(margin: const EdgeInsets.all(margin), child: box));
|
||||||
|
}
|
||||||
|
|
||||||
|
final caption = Hero(tag: "caption", child: box);
|
||||||
|
|
||||||
|
return ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(margin),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(context, HeroDialogRoute(builder: (context) {
|
||||||
|
return Dialog(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: caption,
|
||||||
|
));
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
child: caption,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasTag(String text, Set<String> tags) {
|
||||||
|
return tags
|
||||||
|
.map((e) => RegExp(r"(?<=^|\s|\b)" + RegExp.escape(e) + r"(?=$|\s|\b)",
|
||||||
|
caseSensitive: false))
|
||||||
|
.any((e) => e.hasMatch(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shouldCaption(BuildContext context, String altText, String tooltip) {
|
||||||
|
return captionText(context, altText, tooltip).isNotEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
String captionText(BuildContext context, String altText, String tooltip) {
|
||||||
|
final settings = Provider.of<Settings>(context);
|
||||||
|
|
||||||
|
bool altTextCaption =
|
||||||
|
settings.useAsCaption == SettingsImageTextType.AltTool ||
|
||||||
|
settings.useAsCaption == SettingsImageTextType.AltTool;
|
||||||
|
if (hasTag(altText, settings.doCaptionTags)) {
|
||||||
|
altTextCaption = true;
|
||||||
|
} else if (hasTag(altText, settings.doNotCaptionTags)) {
|
||||||
|
altTextCaption = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool tooltipCaption =
|
||||||
|
settings.useAsCaption == SettingsImageTextType.Tooltip ||
|
||||||
|
settings.useAsCaption == SettingsImageTextType.AltTool;
|
||||||
|
if (hasTag(tooltip, settings.doCaptionTags)) {
|
||||||
|
tooltipCaption = true;
|
||||||
|
} else if (hasTag(tooltip, settings.doNotCaptionTags)) {
|
||||||
|
tooltipCaption = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _altText = altTextCaption ? _cleanCaption(context, altText) : "";
|
||||||
|
String _tooltip = tooltipCaption ? _cleanCaption(context, tooltip) : "";
|
||||||
|
String text = "";
|
||||||
|
if (_altText.isNotEmpty && _tooltip.isNotEmpty) {
|
||||||
|
text = tr("widgets.imageRenderer.caption",
|
||||||
|
namedArgs: settings.tooltipFirst
|
||||||
|
? {"first": _tooltip, "second": _altText}
|
||||||
|
: {"first": _altText, "second": _tooltip});
|
||||||
|
} else {
|
||||||
|
text = _altText + _tooltip;
|
||||||
|
}
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _cleanCaption(BuildContext context, String caption) {
|
||||||
|
final settings = Provider.of<Settings>(context);
|
||||||
|
final tags = [
|
||||||
|
...settings.doThemeTags,
|
||||||
|
...settings.doNotThemeTags,
|
||||||
|
...settings.doCaptionTags,
|
||||||
|
...settings.doNotCaptionTags
|
||||||
|
];
|
||||||
|
return caption
|
||||||
|
.replaceAll(
|
||||||
|
RegExp(
|
||||||
|
r"\s*(?<=^|\s|\b)(" +
|
||||||
|
tags.map(RegExp.escape).join("|") +
|
||||||
|
r")(?=$|\s|\b)\s*",
|
||||||
|
caseSensitive: false),
|
||||||
|
" ")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
Color _overlayBackgroundColor(context) {
|
||||||
|
final settings = Provider.of<Settings>(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return settings.transparentCaption
|
||||||
|
? (theme.brightness == Brightness.dark ? Colors.black : Colors.white)
|
||||||
|
.withAlpha(100)
|
||||||
|
: theme.canvasColor;
|
||||||
|
}
|
106
lib/widgets/images/image_details.dart
Normal file
106
lib/widgets/images/image_details.dart
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020-2021 Roland Fredenhagen <important@van-fredenhagen.de>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gitjournal/settings.dart';
|
||||||
|
import 'package:gitjournal/widgets/images/markdown_image.dart';
|
||||||
|
import 'package:gitjournal/widgets/images/themable_image.dart';
|
||||||
|
import 'package:photo_view/photo_view.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
class ImageDetails extends StatefulWidget {
|
||||||
|
final ThemableImage image;
|
||||||
|
final String caption;
|
||||||
|
ImageDetails(this.image, this.caption);
|
||||||
|
|
||||||
|
@override
|
||||||
|
_ImageDetailsState createState() => _ImageDetailsState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ImageDetailsState extends State<ImageDetails> {
|
||||||
|
int rotation = 0;
|
||||||
|
bool showUI = true;
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final settings = Provider.of<Settings>(context);
|
||||||
|
final bg =
|
||||||
|
theme.brightness == Brightness.dark ? Colors.black : Colors.white;
|
||||||
|
final overlayColor = getOverlayBackgroundColor(context,
|
||||||
|
light: Colors.white, dark: Colors.black);
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
PhotoView.customChild(
|
||||||
|
backgroundDecoration: BoxDecoration(color: bg),
|
||||||
|
child: RotatedBox(
|
||||||
|
quarterTurns: rotation,
|
||||||
|
child: ThemableImage.from(widget.image, bg: bg)),
|
||||||
|
minScale: 1.0,
|
||||||
|
maxScale: settings.maxImageZoom,
|
||||||
|
heroAttributes: PhotoViewHeroAttributes(tag: widget.image),
|
||||||
|
onTapUp: (context, details, controllerValue) =>
|
||||||
|
setState(() => showUI = !showUI),
|
||||||
|
enableRotation: settings.rotateImageGestures,
|
||||||
|
),
|
||||||
|
if (showUI)
|
||||||
|
Positioned(
|
||||||
|
top: MediaQuery.of(context).padding.top,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
height: 60,
|
||||||
|
child: Material(
|
||||||
|
color: overlayColor,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.fromSTEB(8, 0, 0, 0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
splashRadius: 20,
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => Navigator.pop(context)),
|
||||||
|
const Spacer(),
|
||||||
|
IconButton(
|
||||||
|
splashRadius: 20,
|
||||||
|
icon: const Icon(Icons.rotate_90_degrees_ccw),
|
||||||
|
onPressed: () => setState(() => rotation--))
|
||||||
|
],
|
||||||
|
)))),
|
||||||
|
// TODO use a DraggableScrollableSheet, when they can be dynamically
|
||||||
|
// height restricted https://github.com/flutter/flutter/issues/41599
|
||||||
|
if (showUI && widget.caption.isNotEmpty)
|
||||||
|
Positioned(
|
||||||
|
bottom: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
child: Hero(
|
||||||
|
tag: "caption",
|
||||||
|
child: Container(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxHeight: 200),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: Text(
|
||||||
|
widget.caption,
|
||||||
|
style: theme.primaryTextTheme.bodyText1,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
color: overlayColor,
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
)))
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
227
lib/widgets/images/markdown_image.dart
Normal file
227
lib/widgets/images/markdown_image.dart
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020-2021 Roland Fredenhagen <important@van-fredenhagen.de>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_cache_manager/flutter_cache_manager.dart';
|
||||||
|
import 'package:gitjournal/widgets/images/image_caption.dart';
|
||||||
|
import 'package:gitjournal/widgets/images/image_details.dart';
|
||||||
|
import 'package:gitjournal/widgets/images/themable_image.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import 'package:gitjournal/utils/logger.dart';
|
||||||
|
import 'package:gitjournal/settings.dart';
|
||||||
|
|
||||||
|
class MarkdownImage extends StatelessWidget {
|
||||||
|
final double width;
|
||||||
|
final double height;
|
||||||
|
final String altText;
|
||||||
|
final String tooltip;
|
||||||
|
final Future<dynamic> data;
|
||||||
|
|
||||||
|
MarkdownImage._(
|
||||||
|
this.data, this.width, this.height, String altText, String tooltip)
|
||||||
|
: altText = altText ?? "",
|
||||||
|
tooltip = tooltip ?? "";
|
||||||
|
|
||||||
|
factory MarkdownImage(Uri uri, String imageDirectory,
|
||||||
|
{double width, double height, String altText, String titel}) {
|
||||||
|
final file = ((uri.isScheme("http") || uri.isScheme("https"))
|
||||||
|
? DefaultCacheManager().getSingleFile(uri.toString())
|
||||||
|
: Future.sync(
|
||||||
|
() => File.fromUri(Uri.parse(imageDirectory + uri.toString()))));
|
||||||
|
|
||||||
|
final data = file.then(
|
||||||
|
(value) => value.path.endsWith(".svg") ? value.readAsString() : file);
|
||||||
|
|
||||||
|
return MarkdownImage._(data, width, height, altText, titel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final settings = Provider.of<Settings>(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final dark = theme.brightness == Brightness.dark;
|
||||||
|
|
||||||
|
// Test for override tags in AltText/Tooltip
|
||||||
|
ThemeOverride override = ThemeOverride.None;
|
||||||
|
if (settings.themeOverrideTagLocation == SettingsImageTextType.Alt ||
|
||||||
|
settings.themeOverrideTagLocation == SettingsImageTextType.AltTool) {
|
||||||
|
if (hasTag(altText, settings.doThemeTags)) {
|
||||||
|
override = ThemeOverride.Do;
|
||||||
|
} else if (hasTag(altText, settings.doNotThemeTags)) {
|
||||||
|
override = ThemeOverride.No;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (settings.themeOverrideTagLocation == SettingsImageTextType.Tooltip ||
|
||||||
|
settings.themeOverrideTagLocation == SettingsImageTextType.AltTool) {
|
||||||
|
if (hasTag(tooltip, settings.doThemeTags)) {
|
||||||
|
override = ThemeOverride.Do;
|
||||||
|
} else if (hasTag(tooltip, settings.doNotThemeTags)) {
|
||||||
|
override = ThemeOverride.No;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final small =
|
||||||
|
constraints.maxWidth < MediaQuery.of(context).size.width - 40;
|
||||||
|
final image = FutureBuilder(
|
||||||
|
future: data,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.hasError) {
|
||||||
|
String errorMessage = snapshot.error.toString();
|
||||||
|
Log.e(errorMessage);
|
||||||
|
if (snapshot.error is HttpExceptionWithStatus) {
|
||||||
|
final httpError = snapshot.error as HttpExceptionWithStatus;
|
||||||
|
errorMessage = tr("widgets.imageRenderer.httpError",
|
||||||
|
namedArgs: {
|
||||||
|
"status": httpError.statusCode.toString(),
|
||||||
|
"url": httpError.uri.toString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return SizedBox(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.error,
|
||||||
|
color: theme.errorColor,
|
||||||
|
size: 36,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
errorMessage,
|
||||||
|
style: theme.textTheme.bodyText1
|
||||||
|
.copyWith(color: theme.errorColor),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.hasData) {
|
||||||
|
Widget im;
|
||||||
|
if (snapshot.data is String) {
|
||||||
|
im = ThemableImage.svg(
|
||||||
|
snapshot.data,
|
||||||
|
width: width ?? MediaQuery.of(context).size.width,
|
||||||
|
height: height,
|
||||||
|
themingMethod: override == ThemeOverride.No ||
|
||||||
|
settings.themeVectorGraphics ==
|
||||||
|
SettingsThemeVectorGraphics.Off &&
|
||||||
|
override != ThemeOverride.Do
|
||||||
|
? ThemingMethod.none
|
||||||
|
: dark
|
||||||
|
? settings.themeVectorGraphics ==
|
||||||
|
SettingsThemeVectorGraphics.Filter
|
||||||
|
? ThemingMethod.filter
|
||||||
|
: ThemingMethod.invertBrightness
|
||||||
|
: settings.matchCanvasColor
|
||||||
|
? ThemingMethod.wToBg
|
||||||
|
: ThemingMethod.none,
|
||||||
|
themingCondition: settings.themeSvgWithBackground ||
|
||||||
|
override == ThemeOverride.Do
|
||||||
|
? ThemingCondition.none
|
||||||
|
: ThemingCondition.noBackground,
|
||||||
|
colorCondition: settings.vectorGraphicsAdjustColors ==
|
||||||
|
SettingsVectorGraphicsAdjustColors.All
|
||||||
|
? ColorCondition.all
|
||||||
|
: settings.vectorGraphicsAdjustColors ==
|
||||||
|
SettingsVectorGraphicsAdjustColors.BnW
|
||||||
|
? ColorCondition.bw
|
||||||
|
: ColorCondition.gray,
|
||||||
|
bg: settings.matchCanvasColor
|
||||||
|
? theme.canvasColor
|
||||||
|
: Colors.black,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
im = ThemableImage.image(
|
||||||
|
snapshot.data,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
doTheme: (settings.themeRasterGraphics ||
|
||||||
|
override == ThemeOverride.Do) &&
|
||||||
|
override != ThemeOverride.No &&
|
||||||
|
dark,
|
||||||
|
bg: theme.canvasColor,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
child: Hero(tag: im, child: im),
|
||||||
|
onTap: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => ImageDetails(
|
||||||
|
im, captionText(context, altText, tooltip))));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.all(16),
|
||||||
|
child: Center(child: CircularProgressIndicator())));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (shouldCaption(context, altText, tooltip)) {
|
||||||
|
if (small || !settings.overlayCaption) {
|
||||||
|
return Column(
|
||||||
|
children: [image, ImageCaption(altText, tooltip, false)]);
|
||||||
|
} else {
|
||||||
|
return Stack(
|
||||||
|
alignment: AlignmentDirectional.bottomEnd,
|
||||||
|
children: [image, ImageCaption(altText, tooltip, true)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return image;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Color getOverlayBackgroundColor(BuildContext context,
|
||||||
|
{Color light, Color dark}) {
|
||||||
|
final settings = Provider.of<Settings>(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
return theme.brightness == Brightness.dark
|
||||||
|
? settings.transparentCaption
|
||||||
|
? Colors.black.withAlpha(100)
|
||||||
|
: dark ?? theme.canvasColor
|
||||||
|
: settings.transparentCaption
|
||||||
|
? Colors.white.withAlpha(100)
|
||||||
|
: light ?? theme.canvasColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ThemeOverride { None, Do, No }
|
248
lib/widgets/images/themable_image.dart
Normal file
248
lib/widgets/images/themable_image.dart
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2020-2021 Roland Fredenhagen <important@van-fredenhagen.de>
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_svg/flutter_svg.dart';
|
||||||
|
import 'package:flutter_svg/svg.dart';
|
||||||
|
|
||||||
|
class ThemableImage extends StatelessWidget {
|
||||||
|
final double width;
|
||||||
|
final double height;
|
||||||
|
final File file;
|
||||||
|
final String string;
|
||||||
|
final ThemingMethod themingMethod;
|
||||||
|
final ThemingCondition themingCondition;
|
||||||
|
final ColorCondition colorCondition;
|
||||||
|
final Color bg;
|
||||||
|
|
||||||
|
ThemableImage.image(
|
||||||
|
this.file, {
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
doTheme = false,
|
||||||
|
this.bg = Colors.white,
|
||||||
|
}) : string = "",
|
||||||
|
themingMethod = doTheme ? ThemingMethod.filter : ThemingMethod.none,
|
||||||
|
themingCondition = ThemingCondition.none,
|
||||||
|
colorCondition = ColorCondition.all;
|
||||||
|
|
||||||
|
ThemableImage.svg(
|
||||||
|
this.string, {
|
||||||
|
this.width,
|
||||||
|
this.height,
|
||||||
|
this.themingMethod = ThemingMethod.none,
|
||||||
|
this.bg = Colors.white,
|
||||||
|
this.themingCondition = ThemingCondition.none,
|
||||||
|
this.colorCondition = ColorCondition.all,
|
||||||
|
}) : file = null;
|
||||||
|
|
||||||
|
ThemableImage.from(
|
||||||
|
ThemableImage ti, {
|
||||||
|
double width,
|
||||||
|
double height,
|
||||||
|
ThemingMethod themingMethod,
|
||||||
|
ThemingCondition themingCondition,
|
||||||
|
ColorCondition colorCondition,
|
||||||
|
Color bg,
|
||||||
|
}) : file = ti.file,
|
||||||
|
string = ti.string,
|
||||||
|
width = width ?? ti.width,
|
||||||
|
height = height ?? ti.height,
|
||||||
|
themingMethod = themingMethod ?? ti.themingMethod,
|
||||||
|
themingCondition = themingCondition ?? ti.themingCondition,
|
||||||
|
colorCondition = colorCondition ?? ti.colorCondition,
|
||||||
|
bg = bg ?? ti.bg;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
Widget image;
|
||||||
|
if (file != null) {
|
||||||
|
image = Image.file(file, width: width, height: height);
|
||||||
|
} else if (string.isNotEmpty) {
|
||||||
|
image = SvgPicture(
|
||||||
|
StringPicture(
|
||||||
|
_transformSVG,
|
||||||
|
string +
|
||||||
|
'<?theme '
|
||||||
|
'themingMethod="$themingMethod" '
|
||||||
|
'themingCondition="$themingCondition" '
|
||||||
|
'colorCondition="$colorCondition" '
|
||||||
|
'backgroundColor="$bg" '
|
||||||
|
'?>'),
|
||||||
|
width: width,
|
||||||
|
height: height);
|
||||||
|
} else {
|
||||||
|
throw Exception("Tried to render an image without File or SVG string");
|
||||||
|
}
|
||||||
|
return themingMethod == ThemingMethod.filter
|
||||||
|
? _themeFilter(image, bg)
|
||||||
|
: image;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<PictureInfo> _transformSVG(data, colorFilter, key) async {
|
||||||
|
DrawableRoot svgRoot = await svg.fromSvgString(data, key);
|
||||||
|
if (themingCondition != ThemingCondition.noBackground ||
|
||||||
|
!_hasBackground(svgRoot, svgRoot.viewport.viewBox.width,
|
||||||
|
svgRoot.viewport.viewBox.height) ||
|
||||||
|
themingMethod == ThemingMethod.wToBg) {
|
||||||
|
svgRoot = _themeDrawable(svgRoot, (Color color) {
|
||||||
|
switch (themingMethod) {
|
||||||
|
case ThemingMethod.wToBg:
|
||||||
|
return color == Colors.white ? bg : color;
|
||||||
|
|
||||||
|
case ThemingMethod.none:
|
||||||
|
case ThemingMethod.filter:
|
||||||
|
return color;
|
||||||
|
|
||||||
|
case ThemingMethod.invertBrightness:
|
||||||
|
final hslColor = HSLColor.fromColor(color);
|
||||||
|
final backGroundLightness = HSLColor.fromColor(bg).lightness;
|
||||||
|
switch (colorCondition) {
|
||||||
|
case ColorCondition.all:
|
||||||
|
return HSLColor.fromAHSL(
|
||||||
|
hslColor.alpha,
|
||||||
|
hslColor.hue,
|
||||||
|
hslColor.saturation,
|
||||||
|
1 - (hslColor.lightness * (1 - backGroundLightness)))
|
||||||
|
.toColor();
|
||||||
|
|
||||||
|
case ColorCondition.bw:
|
||||||
|
if (hslColor.lightness > 0.95 && hslColor.saturation < 0.02) {
|
||||||
|
return HSLColor.fromAHSL(
|
||||||
|
hslColor.alpha, 0, 0, backGroundLightness)
|
||||||
|
.toColor();
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
|
||||||
|
case ColorCondition.gray:
|
||||||
|
if (hslColor.saturation < 0.02 || hslColor.lightness < 0.02) {
|
||||||
|
return HSLColor.fromAHSL(hslColor.alpha, 0, 0,
|
||||||
|
1 - (hslColor.lightness * (1 - backGroundLightness)))
|
||||||
|
.toColor();
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final Picture pic = svgRoot.toPicture(
|
||||||
|
clipToViewBox: false,
|
||||||
|
colorFilter: colorFilter,
|
||||||
|
);
|
||||||
|
|
||||||
|
return PictureInfo(
|
||||||
|
picture: pic,
|
||||||
|
viewport: svgRoot.viewport.viewBoxRect,
|
||||||
|
size: svgRoot.viewport.size,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ThemingMethod { none, filter, wToBg, invertBrightness }
|
||||||
|
|
||||||
|
enum ThemingCondition { none, noBackground }
|
||||||
|
|
||||||
|
enum ColorCondition { all, bw, gray }
|
||||||
|
|
||||||
|
/// Tests if the [Drawable] [draw] has a non transparent background
|
||||||
|
///
|
||||||
|
/// [width] and [height] specify the area for a drawable to fill to be a valid background.
|
||||||
|
/// Use [minAlpha] and [maxBorder] to accept imperfect Backgrounds.
|
||||||
|
/// [maxBorder] is the fraction of the [width]/[height] that does not need to be filled.
|
||||||
|
/// Use [maxDepth] to controll how deep the Tree is traversed to find a background,
|
||||||
|
/// `maxDepth = -1` will traverse the whole Tree.
|
||||||
|
bool _hasBackground(Drawable draw, double width, double height,
|
||||||
|
{minAlpha = 0.99, maxBorder = 0.01, maxDepth = 10}) {
|
||||||
|
if (maxDepth == 0) {
|
||||||
|
if (draw is DrawableShape) {
|
||||||
|
final drawShape = draw;
|
||||||
|
return drawShape.style.fill != null &&
|
||||||
|
drawShape.style.fill.color.alpha > minAlpha &&
|
||||||
|
[
|
||||||
|
Offset(width * maxBorder, height * maxBorder),
|
||||||
|
Offset(width - width * maxBorder, height * maxBorder),
|
||||||
|
Offset(width - width * maxBorder, height - height * maxBorder),
|
||||||
|
Offset(width * maxBorder, height - height * maxBorder),
|
||||||
|
].every(drawShape.path.contains);
|
||||||
|
}
|
||||||
|
// TODO Allow for two shapes to be the background together
|
||||||
|
if (draw is DrawableParent) {
|
||||||
|
final drawParent = draw;
|
||||||
|
return drawParent.children.any((element) => _hasBackground(
|
||||||
|
element, width, height,
|
||||||
|
minAlpha: minAlpha, maxBorder: maxBorder, maxDepth: maxDepth - 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Drawable _themeDrawable(
|
||||||
|
Drawable draw, Color Function(Color color) transformColor) {
|
||||||
|
if (draw is DrawableStyleable && !(draw is DrawableGroup)) {
|
||||||
|
final DrawableStyleable drawStylable = draw;
|
||||||
|
draw = drawStylable.mergeStyle(DrawableStyle(
|
||||||
|
stroke: drawStylable.style.stroke != null &&
|
||||||
|
drawStylable.style.stroke.color != null
|
||||||
|
? DrawablePaint.merge(
|
||||||
|
DrawablePaint(drawStylable.style.stroke.style,
|
||||||
|
color: transformColor(drawStylable.style.stroke.color)),
|
||||||
|
drawStylable.style.stroke)
|
||||||
|
: null,
|
||||||
|
fill: drawStylable.style.fill != null &&
|
||||||
|
drawStylable.style.fill.color != null
|
||||||
|
? DrawablePaint.merge(
|
||||||
|
DrawablePaint(drawStylable.style.fill.style,
|
||||||
|
color: transformColor(drawStylable.style.fill.color)),
|
||||||
|
drawStylable.style.fill)
|
||||||
|
: null));
|
||||||
|
}
|
||||||
|
if (draw is DrawableParent) {
|
||||||
|
final DrawableParent drawParent = draw;
|
||||||
|
final children = drawParent.children
|
||||||
|
.map((e) => _themeDrawable(e, transformColor))
|
||||||
|
.toList(growable: false);
|
||||||
|
if (draw is DrawableRoot) {
|
||||||
|
final DrawableRoot drawRoot = draw;
|
||||||
|
draw = DrawableRoot(drawRoot.id, drawRoot.viewport, children,
|
||||||
|
drawRoot.definitions, drawRoot.style,
|
||||||
|
transform: drawRoot.transform);
|
||||||
|
} else if (draw is DrawableGroup) {
|
||||||
|
final DrawableGroup drawGroup = draw;
|
||||||
|
draw = DrawableGroup(drawGroup.id, children, drawGroup.style,
|
||||||
|
transform: drawGroup.transform);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return draw;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _themeFilter(Widget widget, Color background) {
|
||||||
|
final lightness = (1 - HSLColor.fromColor(background).lightness) * 255;
|
||||||
|
// TODO Switch to HSL Filter, when available (https://github.com/flutter/flutter/issues/76729)
|
||||||
|
final stack = ColorFiltered(
|
||||||
|
colorFilter: ColorFilter.matrix(<double>[
|
||||||
|
-(lightness / 255), 0, 0, 0, 255, // R
|
||||||
|
0, -(lightness / 255), 0, 0, 255, // G
|
||||||
|
0, 0, -(lightness / 255), 0, 255, // B
|
||||||
|
0, 0, 0, 1, 0 // A
|
||||||
|
]),
|
||||||
|
child: widget);
|
||||||
|
return stack;
|
||||||
|
}
|
@ -31,7 +31,7 @@ import 'package:gitjournal/folder_views/common.dart';
|
|||||||
import 'package:gitjournal/utils.dart';
|
import 'package:gitjournal/utils.dart';
|
||||||
import 'package:gitjournal/utils/link_resolver.dart';
|
import 'package:gitjournal/utils/link_resolver.dart';
|
||||||
import 'package:gitjournal/utils/logger.dart';
|
import 'package:gitjournal/utils/logger.dart';
|
||||||
import 'package:gitjournal/widgets/image_renderer.dart';
|
import 'package:gitjournal/widgets/images/markdown_image.dart';
|
||||||
|
|
||||||
class MarkdownRenderer extends StatelessWidget {
|
class MarkdownRenderer extends StatelessWidget {
|
||||||
final Note note;
|
final Note note;
|
||||||
@ -117,7 +117,7 @@ class MarkdownRenderer extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
imageBuilder: (url, title, alt) => ThemableImage(
|
imageBuilder: (url, title, alt) => MarkdownImage(
|
||||||
url, note.parent.folderPath + p.separator,
|
url, note.parent.folderPath + p.separator,
|
||||||
titel: title, altText: alt),
|
titel: title, altText: alt),
|
||||||
extensionSet: markdownExtensions(),
|
extensionSet: markdownExtensions(),
|
||||||
|
@ -771,6 +771,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.2"
|
version: "4.0.2"
|
||||||
|
photo_view:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: photo_view
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "0.10.3"
|
||||||
platform:
|
platform:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -59,6 +59,7 @@ dependencies:
|
|||||||
package_info: ">=0.4.1 <2.0.0"
|
package_info: ">=0.4.1 <2.0.0"
|
||||||
path: ^1.6.2
|
path: ^1.6.2
|
||||||
permission_handler: ^6.1.1
|
permission_handler: ^6.1.1
|
||||||
|
photo_view: ^0.10.3
|
||||||
provider: ^4.3.2+2
|
provider: ^4.3.2+2
|
||||||
quick_actions: ^0.4.0+10
|
quick_actions: ^0.4.0+10
|
||||||
receive_sharing_intent: ^1.4.0+2
|
receive_sharing_intent: ^1.4.0+2
|
||||||
|
Reference in New Issue
Block a user