feat: SVG Rendering, Image theming and Image caption

fixes issues #405 #410 #246
This commit is contained in:
Roland Fredenhagen
2021-03-05 01:07:19 +01:00
committed by Vishesh Handa
parent 27f314eb65
commit 24cdc236e1
9 changed files with 1399 additions and 92 deletions

View File

@ -16,6 +16,73 @@ settings:
title: Display Settings
homeScreen: Home Screen
theme: Theme
images:
title: Image Settings
subtitle: Configure how images are displayed
imageTextType:
alt: Alt Text
altAndTooltip: Alt Text and Tooltip
tooltip: Tooltip
none: None
captions:
title: Image Captions
subtitle: Configure the image captions
useAsCaption: Use as caption
overlayCaption: Draw caption on top of large enough images
transparentCaption: Overlay captions have a semitransparent background
blurBehindCaption: Blur Image behind caption
tooltipFirst:
title: Show tooltip before alt text
tooltip: Current order is “<tooltip> - <altText>”
altText: Current order is “<altText> - <tooltip>”
captionOverrides: Caption Overrides
tagDescription: Put these tags in “![altText](... "tooltip")” to override the behavior for it.
doNotCaptionTags:
hint: DoNotCaption-Tags
label: Never use as caption with tags
validator:
empty: Tags cannot be empty.
same: Tag cannot be identical to a “DoCaption-Tag”.
doCaptionTags:
hint: DoCaption-Tags
label: Always use as caption with tags
validator:
empty: Tags cannot be empty.
same: Tag cannot be identical to a “DoNotCaption-Tag”.
theming:
title: Image Theming
subtitle: Configure how images are themed
adjustColors:
all: All
blackAndWhite: Only black and white
grays: Only grays
doNotThemeTags:
hint: DoNotTheme-Tags
label: Never theme images with tags
validator:
empty: Tags cannot be empty.
same: Tag cannot be identical to a “DoTheme-Tag”.
doThemeTags:
hint: DoTheme-Tag
label: Always theme images with tags
validator:
empty: Tags cannot be empty.
same: Tag cannot be identical to a “DoNotTheme-Tag”.
matchCanvasColor:
title: Match Background Color
subtitle: Replaces white/black parts of vector graphics with the canvas color
tagDescription: Put these tags in “![altText](... "tooltip")” to override the behavior for the image.
themeOverrides: Theme Overrides
themeOverrideTagLocation: Theme Override Tag Location
themeRasterGraphics: Theme Raster Graphics (.png/.jpg)
themeSvgWithBackground: Theme SVGs With Background
themeVectorGraphics:
title: Theme Vector Graphics
filter: Using Color Filter
off: No
on: Yes
vectorGraphics: Vector Graphics (.svg)
vectorGraphicsAdjustColors: Colors to Adjust
theme:
light: Light
dark: Dark
@ -240,6 +307,9 @@ widgets:
two: "{} Notes link to this Note"
few: "{} Notes link to this Note"
other: "{} Notes link to this Note"
imageRenderer:
caption: "{first} - {second}"
httpError: "Code: {status} for {url}"
SortingOrderSelector:
title: Sorting Criteria
PurchaseButton:

View File

@ -0,0 +1,77 @@
/*
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:easy_localization/easy_localization.dart';
import 'package:gitjournal/screens/settings_display_images_caption.dart';
import 'package:gitjournal/screens/settings_display_images_theming.dart';
class SettingsDisplayImagesScreen extends StatefulWidget {
@override
SettingsDisplayImagesScreenState createState() =>
SettingsDisplayImagesScreenState();
}
class SettingsDisplayImagesScreenState
extends State<SettingsDisplayImagesScreen> {
final doNotThemeTagsKey = GlobalKey<FormFieldState<String>>();
final doThemeTagsKey = GlobalKey<FormFieldState<String>>();
@override
Widget build(BuildContext context) {
final body = ListView(children: <Widget>[
ListTile(
title: Text(tr("settings.display.images.theming.title")),
subtitle: Text(tr("settings.display.images.theming.subtitle")),
onTap: () {
var route = MaterialPageRoute(
builder: (context) => SettingsDisplayImagesThemingScreen(),
settings:
const RouteSettings(name: '/settings/display_images/theming'),
);
Navigator.of(context).push(route);
},
),
ListTile(
title: Text(tr("settings.display.images.captions.title")),
subtitle: Text(tr("settings.display.images.captions.subtitle")),
onTap: () {
var route = MaterialPageRoute(
builder: (context) => SettingsDisplayImagesCaptionScreen(),
settings:
const RouteSettings(name: '/settings/display_images/caption'),
);
Navigator.of(context).push(route);
},
),
]);
return Scaffold(
appBar: AppBar(
title: Text(tr('settings.display.images.title')),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(context).pop();
},
),
),
body: body,
);
}
}

View File

@ -0,0 +1,191 @@
/*
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:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:gitjournal/screens/settings_screen.dart';
import 'package:gitjournal/screens/settings_widgets.dart';
import 'package:gitjournal/settings.dart';
import 'package:provider/provider.dart';
class SettingsDisplayImagesCaptionScreen extends StatefulWidget {
@override
SettingsDisplayImagesCaptionScreenState createState() =>
SettingsDisplayImagesCaptionScreenState();
}
class SettingsDisplayImagesCaptionScreenState
extends State<SettingsDisplayImagesCaptionScreen> {
final doNotCaptionTagsKey = GlobalKey<FormFieldState<String>>();
final doCaptionTagsKey = GlobalKey<FormFieldState<String>>();
@override
Widget build(BuildContext context) {
var settings = Provider.of<Settings>(context);
var saveDoNotCaptionTag = (String doNotCaptionTags) {
settings.doNotCaptionTags = parseTags(doNotCaptionTags);
settings.save();
};
var doNotCaptionTagsForm = Form(
child: TextFormField(
key: doNotCaptionTagsKey,
style: Theme.of(context).textTheme.headline6,
decoration: InputDecoration(
hintText:
tr('settings.display.images.captions.doNotCaptionTags.hint'),
labelText:
tr('settings.display.images.captions.doNotCaptionTags.label'),
),
validator: (String value) {
value = value.trim();
if (parseTags(value).isEmpty) {
return tr(
'settings.display.images.captions.doNotCaptionTags.validator.empty');
}
if (parseTags(value)
.intersection(settings.doCaptionTags)
.isNotEmpty) {
return tr(
'settings.display.images.captions.doNotCaptionTags.validator.same');
}
return null;
},
textInputAction: TextInputAction.done,
onFieldSubmitted: saveDoNotCaptionTag,
onSaved: saveDoNotCaptionTag,
initialValue: csvTags(settings.doNotCaptionTags),
),
onChanged: () {
if (!doNotCaptionTagsKey.currentState.validate()) return;
saveDoNotCaptionTag(doNotCaptionTagsKey.currentState.value);
},
);
var saveDoThemeTag = (String doCaptionTags) {
settings.doCaptionTags = parseTags(doCaptionTags);
settings.save();
doNotCaptionTagsForm.createState();
};
var doCaptionTagsForm = Form(
child: TextFormField(
key: doCaptionTagsKey,
style: Theme.of(context).textTheme.headline6,
decoration: InputDecoration(
hintText: tr('settings.display.images.captions.doCaptionTags.hint'),
labelText: tr('settings.display.images.captions.doCaptionTags.label'),
),
validator: (String value) {
if (parseTags(value).isEmpty) {
return tr(
'settings.display.images.captions.doCaptionTags.validator.empty');
}
if (parseTags(value)
.intersection(settings.doNotCaptionTags)
.isNotEmpty) {
return tr(
'settings.display.images.captions.doCaptionTags.validator.same');
}
return null;
},
textInputAction: TextInputAction.done,
onFieldSubmitted: saveDoThemeTag,
onSaved: saveDoThemeTag,
initialValue: csvTags(settings.doCaptionTags),
),
onChanged: () {
if (!doCaptionTagsKey.currentState.validate()) return;
saveDoThemeTag(doCaptionTagsKey.currentState.value);
},
);
var body = ListView(children: <Widget>[
ListPreference(
title: tr("settings.display.images.captions.useAsCaption"),
currentOption: settings.useAsCaption.toPublicString(),
options: SettingsImageTextType.options
.map((e) => e.toPublicString())
.toList(),
onChange: (String publicStr) {
settings.useAsCaption =
SettingsImageTextType.fromPublicString(publicStr);
settings.save();
setState(() {});
},
),
SwitchListTile(
title: Text(tr('settings.display.images.captions.overlayCaption')),
value: settings.overlayCaption,
onChanged: (bool newVal) {
settings.overlayCaption = newVal;
settings.save();
},
),
if (settings.overlayCaption)
SwitchListTile(
title:
Text(tr('settings.display.images.captions.transparentCaption')),
value: settings.transparentCaption,
onChanged: (bool newVal) {
settings.transparentCaption = newVal;
settings.save();
},
),
if (settings.overlayCaption && settings.transparentCaption)
SwitchListTile(
title: Text(tr('settings.display.images.captions.blurBehindCaption')),
value: settings.blurBehindCaption,
onChanged: (bool newVal) {
settings.blurBehindCaption = newVal;
settings.save();
},
),
SwitchListTile(
title: Text(tr('settings.display.images.captions.tooltipFirst.title')),
value: settings.tooltipFirst,
subtitle: settings.tooltipFirst
? Text(tr('settings.display.images.captions.tooltipFirst.tooltip'))
: Text(tr('settings.display.images.captions.tooltipFirst.altText')),
onChanged: (bool newVal) {
settings.tooltipFirst = newVal;
settings.save();
},
),
SettingsHeader(tr('settings.display.images.captions.captionOverrides')),
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Text(tr("settings.display.images.captions.tagDescription")),
),
ListTile(title: doCaptionTagsForm),
ListTile(title: doNotCaptionTagsForm)
]);
return Scaffold(
appBar: AppBar(
title: Text(tr('settings.display.images.captions.title')),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(context).pop();
},
),
),
body: body,
);
}
}

View File

@ -0,0 +1,210 @@
/*
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:easy_localization/easy_localization.dart';
import 'package:provider/provider.dart';
import 'package:gitjournal/screens/settings_widgets.dart';
import 'package:gitjournal/screens/settings_screen.dart';
import 'package:gitjournal/settings.dart';
class SettingsDisplayImagesThemingScreen extends StatefulWidget {
@override
SettingsDisplayImagesThemingScreenState createState() =>
SettingsDisplayImagesThemingScreenState();
}
class SettingsDisplayImagesThemingScreenState
extends State<SettingsDisplayImagesThemingScreen> {
final doNotThemeTagsKey = GlobalKey<FormFieldState<String>>();
final doThemeTagsKey = GlobalKey<FormFieldState<String>>();
@override
Widget build(BuildContext context) {
var settings = Provider.of<Settings>(context);
var saveDoNotThemeTag = (String doNotThemeTags) {
settings.doNotThemeTags = parseTags(doNotThemeTags);
settings.save();
};
var doNotThemeTagsForm = Form(
child: TextFormField(
key: doNotThemeTagsKey,
style: Theme.of(context).textTheme.headline6,
decoration: InputDecoration(
hintText: tr('settings.display.images.theming.doNotThemeTags.hint'),
labelText: tr('settings.display.images.theming.doNotThemeTags.label'),
),
validator: (String value) {
value = value.trim();
if (parseTags(value).isEmpty) {
return tr(
'settings.display.images.theming.doNotThemeTags.validator.empty');
}
if (parseTags(value).intersection(settings.doThemeTags).isNotEmpty) {
return tr(
'settings.display.images.theming.doNotThemeTags.validator.same');
}
return null;
},
textInputAction: TextInputAction.done,
onFieldSubmitted: saveDoNotThemeTag,
onSaved: saveDoNotThemeTag,
initialValue: csvTags(settings.doNotThemeTags),
),
onChanged: () {
if (!doNotThemeTagsKey.currentState.validate()) return;
saveDoNotThemeTag(doNotThemeTagsKey.currentState.value);
},
);
var saveDoThemeTag = (String doThemeTags) {
settings.doThemeTags = parseTags(doThemeTags);
settings.save();
};
var doThemeTagsForm = Form(
child: TextFormField(
key: doThemeTagsKey,
style: Theme.of(context).textTheme.headline6,
decoration: InputDecoration(
hintText: tr('settings.display.images.theming.doThemeTags.hint'),
labelText: tr('settings.display.images.theming.doThemeTags.label'),
),
validator: (String value) {
if (parseTags(value).isEmpty) {
return tr(
'settings.display.images.theming.doThemeTags.validator.empty');
}
if (parseTags(value)
.intersection(settings.doNotThemeTags)
.isNotEmpty) {
return tr(
'settings.display.images.theming.doThemeTags.validator.same');
}
return null;
},
textInputAction: TextInputAction.done,
onFieldSubmitted: saveDoThemeTag,
onSaved: saveDoThemeTag,
initialValue: csvTags(settings.doThemeTags),
),
onChanged: () {
if (!doThemeTagsKey.currentState.validate()) return;
saveDoThemeTag(doThemeTagsKey.currentState.value);
},
);
var body = ListView(children: <Widget>[
SwitchListTile(
title: Text(tr('settings.display.images.theming.themeRasterGraphics')),
value: settings.themeRasterGraphics,
onChanged: (bool newVal) {
settings.themeRasterGraphics = newVal;
settings.save();
},
),
SettingsHeader(tr('settings.display.images.theming.themeOverrides')),
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 0),
child: Text(tr("settings.display.images.theming.tagDescription")),
),
ListPreference(
title: tr("settings.display.images.theming.themeOverrideTagLocation"),
currentOption: settings.themeOverrideTagLocation.toPublicString(),
options: SettingsImageTextType.options
.map((e) => e.toPublicString())
.toList(),
onChange: (String publicStr) {
settings.themeOverrideTagLocation =
SettingsImageTextType.fromPublicString(publicStr);
settings.save();
setState(() {});
},
),
ListTile(title: doThemeTagsForm),
ListTile(title: doNotThemeTagsForm),
SettingsHeader(tr('settings.display.images.theming.vectorGraphics')),
ListPreference(
title: tr("settings.display.images.theming.themeVectorGraphics.title"),
currentOption: settings.themeVectorGraphics.toPublicString(),
options: SettingsThemeVectorGraphics.options
.map((e) => e.toPublicString())
.toList(),
onChange: (String publicStr) {
settings.themeVectorGraphics =
SettingsThemeVectorGraphics.fromPublicString(publicStr);
settings.save();
setState(() {});
},
),
if (settings.themeVectorGraphics == SettingsThemeVectorGraphics.On)
SwitchListTile(
title: Text(
tr('settings.display.images.theming.themeSvgWithBackground')),
value: settings.themeSvgWithBackground,
onChanged: (bool newVal) {
settings.themeSvgWithBackground = newVal;
settings.save();
},
),
if (settings.themeVectorGraphics == SettingsThemeVectorGraphics.On)
SwitchListTile(
title: Text(
tr('settings.display.images.theming.matchCanvasColor.title')),
subtitle: Text(
tr('settings.display.images.theming.matchCanvasColor.subtitle')),
value: settings.matchCanvasColor,
onChanged: (bool newVal) {
settings.matchCanvasColor = newVal;
settings.save();
},
),
if (settings.themeVectorGraphics == SettingsThemeVectorGraphics.On)
ListPreference(
title:
tr("settings.display.images.theming.vectorGraphicsAdjustColors"),
currentOption: settings.vectorGraphicsAdjustColors.toPublicString(),
options: SettingsVectorGraphicsAdjustColors.options
.map((e) => e.toPublicString())
.toList(),
onChange: (String publicStr) {
settings.vectorGraphicsAdjustColors =
SettingsVectorGraphicsAdjustColors.fromPublicString(publicStr);
settings.save();
setState(() {});
},
),
]);
return Scaffold(
appBar: AppBar(
title: Text(tr('settings.display.images.theming.title')),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(context).pop();
},
),
),
body: body,
);
}
}

View File

@ -1,5 +1,6 @@
/*
Copyright 2020-2021 Vishesh Handa <me@vhanda.in>
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.
@ -37,6 +38,7 @@ import 'package:gitjournal/repository_manager.dart';
import 'package:gitjournal/screens/debug_screen.dart';
import 'package:gitjournal/screens/feature_timeline_screen.dart';
import 'package:gitjournal/screens/settings_bottom_menu_bar.dart';
import 'package:gitjournal/screens/settings_display_images.dart';
import 'package:gitjournal/screens/settings_editors.dart';
import 'package:gitjournal/screens/settings_experimental.dart';
import 'package:gitjournal/screens/settings_git_remote.dart';
@ -185,6 +187,17 @@ class SettingsListState extends State<SettingsList> {
setState(() {});
},
),
ListTile(
title: Text(tr("settings.display.images.title")),
subtitle: Text(tr("settings.display.images.subtitle")),
onTap: () {
var route = MaterialPageRoute(
builder: (context) => SettingsDisplayImagesScreen(),
settings: const RouteSettings(name: '/settings/display_images'),
);
Navigator.of(context).push(route);
},
),
ProOverlay(
feature: Feature.customizeHomeScreen,
child: ListPreference(

View File

@ -1,5 +1,6 @@
/*
Copyright 2020-2021 Vishesh Handa <me@vhanda.in>
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.
@ -69,6 +70,28 @@ class Settings extends ChangeNotifier {
SettingsHomeScreen homeScreen = SettingsHomeScreen.Default;
SettingsTheme theme = SettingsTheme.Default;
// Display - Image - Theming
bool themeRasterGraphics = false;
SettingsImageTextType themeOverrideTagLocation =
SettingsImageTextType.Default;
Set<String> doNotThemeTags = {"notheme", "!nt"};
Set<String> doThemeTags = {"dotheme", "!dt"};
SettingsThemeVectorGraphics themeVectorGraphics =
SettingsThemeVectorGraphics.Default;
bool themeSvgWithBackground = false;
bool matchCanvasColor = true;
SettingsVectorGraphicsAdjustColors vectorGraphicsAdjustColors =
SettingsVectorGraphicsAdjustColors.Default;
// Display - Image - Caption
bool overlayCaption = true;
bool transparentCaption = true;
bool blurBehindCaption = true;
bool tooltipFirst = false;
SettingsImageTextType useAsCaption = SettingsImageTextType.Default;
Set<String> doNotCaptionTags = {"nocaption", "!nc"};
Set<String> doCaptionTags = {"docaption", "!dc"};
SettingsMarkdownDefaultView markdownDefaultView =
SettingsMarkdownDefaultView.Default;
SettingsMarkdownDefaultView markdownLastUsedView =
@ -148,6 +171,35 @@ class Settings extends ChangeNotifier {
SettingsHomeScreen.fromInternalString(_getString(pref, "homeScreen"));
theme = SettingsTheme.fromInternalString(_getString(pref, "theme"));
// Display - Image - Theming
themeRasterGraphics =
_getBool(pref, "themeRasterGraphics") ?? themeRasterGraphics;
themeOverrideTagLocation = SettingsImageTextType.fromInternalString(
_getString(pref, "themeOverrideTagLocation"));
doNotThemeTags = _getStringSet(pref, "doNotThemeTags") ?? doNotThemeTags;
doThemeTags = _getStringSet(pref, "doThemeTags") ?? doThemeTags;
themeVectorGraphics = SettingsThemeVectorGraphics.fromInternalString(
_getString(pref, "themeVectorGraphics"));
themeSvgWithBackground =
_getBool(pref, "themeSvgWithBackground") ?? themeSvgWithBackground;
matchCanvasColor = _getBool(pref, "matchCanvasColor") ?? matchCanvasColor;
vectorGraphicsAdjustColors =
SettingsVectorGraphicsAdjustColors.fromInternalString(
_getString(pref, "vectorGraphicsAdjustColors"));
// Display - Image - Caption
overlayCaption = _getBool(pref, "overlayCaption") ?? overlayCaption;
transparentCaption =
_getBool(pref, "transparentCaption") ?? transparentCaption;
blurBehindCaption =
_getBool(pref, "blurBehindCaption") ?? blurBehindCaption;
tooltipFirst = _getBool(pref, "tooltipFirst") ?? tooltipFirst;
useAsCaption = SettingsImageTextType.fromInternalString(
_getString(pref, "useAsCaption"));
doNotCaptionTags =
_getStringSet(pref, "doNotCaptionTag") ?? doNotCaptionTags;
doCaptionTags = _getStringSet(pref, "doCaptionTag") ?? doCaptionTags;
imageLocationSpec =
_getString(pref, "imageLocationSpec") ?? imageLocationSpec;
@ -184,6 +236,10 @@ class Settings extends ChangeNotifier {
return pref.getStringList(id + '_' + key);
}
Set<String> _getStringSet(SharedPreferences pref, String key) {
return _getStringList(pref, key)?.toSet();
}
int _getInt(SharedPreferences pref, String key) {
return pref.getInt(id + '_' + key);
}
@ -255,6 +311,49 @@ class Settings extends ChangeNotifier {
defaultSet.homeScreen.toInternalString());
await _setString(pref, "theme", theme.toInternalString(),
defaultSet.theme.toInternalString());
// Display - Image - Theme
await _setBool(pref, "themeRasterGraphics", themeRasterGraphics,
defaultSet.themeRasterGraphics);
await _setString(
pref,
"themeOverrideTagLocation",
themeOverrideTagLocation.toInternalString(),
defaultSet.themeOverrideTagLocation.toInternalString());
await _setStringSet(
pref, "doNotThemeTags", doNotThemeTags, defaultSet.doNotThemeTags);
await _setStringSet(
pref, "doThemeTags", doThemeTags, defaultSet.doThemeTags);
await _setString(
pref,
"themeVectorGraphics",
themeVectorGraphics.toInternalString(),
defaultSet.themeVectorGraphics.toInternalString());
await _setBool(pref, "themeSvgWithBackground", themeSvgWithBackground,
defaultSet.themeSvgWithBackground);
await _setBool(pref, "matchCanvasColor", matchCanvasColor,
defaultSet.matchCanvasColor);
await _setString(
pref,
"vectorGraphicsAdjustColors",
vectorGraphicsAdjustColors.toInternalString(),
defaultSet.vectorGraphicsAdjustColors.toInternalString());
// Display - Image - Caption
await _setBool(
pref, "overlayCaption", overlayCaption, defaultSet.overlayCaption);
await _setBool(pref, "transparentCaption", transparentCaption,
defaultSet.transparentCaption);
await _setBool(pref, "blurBehindCaption", blurBehindCaption,
defaultSet.blurBehindCaption);
await _setBool(pref, "tooltipFirst", tooltipFirst, defaultSet.tooltipFirst);
await _setString(pref, "useAsCaption", useAsCaption.toInternalString(),
defaultSet.useAsCaption.toInternalString());
await _setStringSet(
pref, "doNotCaptionTag", doNotCaptionTags, defaultSet.doNotCaptionTags);
await _setStringSet(
pref, "doCaptionTag", doCaptionTags, defaultSet.doCaptionTags);
await _setString(pref, "imageLocationSpec", imageLocationSpec,
defaultSet.imageLocationSpec);
await _setBool(pref, "zenMode", zenMode, defaultSet.zenMode);
@ -372,6 +471,25 @@ class Settings extends ChangeNotifier {
'markdownLastUsedView': markdownLastUsedView.toInternalString(),
'homeScreen': homeScreen.toInternalString(),
'theme': theme.toInternalString(),
// Display - Image - Theming
'themeRasterGraphics': themeRasterGraphics.toString(),
'themeOverrideTagLocation': themeOverrideTagLocation.toInternalString(),
'doNotThemeTags': csvTags(doNotThemeTags),
'doThemeTags': csvTags(doThemeTags),
'themeVectorGraphics': themeVectorGraphics.toInternalString(),
'themeSvgWithBackground': themeSvgWithBackground.toString(),
'matchCanvasColor': matchCanvasColor.toString(),
'vectorGraphicsAdjustColors':
vectorGraphicsAdjustColors.toInternalString(),
// Display - Image - Caption
'overlayCaption': overlayCaption.toString(),
'transparentCaption': transparentCaption.toString(),
'blurBehindCaption': blurBehindCaption.toString(),
'tooltipFirst': tooltipFirst.toString(),
'useAsCaption': useAsCaption.toInternalString(),
'doNotCaptionTag': csvTags(doNotCaptionTags),
'doCaptionTag': csvTags(doCaptionTags),
//
'imageLocationSpec': imageLocationSpec,
'zenMode': zenMode.toString(),
'titleSettings': titleSettings.toInternalString(),
@ -427,7 +545,7 @@ class NoteFileNameFormat {
static const FromTitle =
NoteFileNameFormat("FromTitle", 'settings.NoteFileNameFormat.title');
static const SimpleDate =
NoteFileNameFormat("SimpleDate", 'settings.NoteFileNameFormat.simmple');
NoteFileNameFormat("SimpleDate", 'settings.NoteFileNameFormat.simple');
static const UuidV4 =
NoteFileNameFormat("uuidv4", 'settings.NoteFileNameFormat.uuid');
static const Zettelkasten = NoteFileNameFormat(
@ -804,6 +922,169 @@ class SettingsHomeScreen {
}
}
class SettingsImageTextType {
static const AltTool = SettingsImageTextType(
"settings.display.images.imageTextType.altAndTooltip", "alt_and_tooltip");
static const Tooltip = SettingsImageTextType(
"settings.display.images.imageTextType.tooltip", "tooltip");
static const Alt =
SettingsImageTextType("settings.display.images.imageTextType.alt", "alt");
static const None = SettingsImageTextType(
"settings.display.images.imageTextType.none", "none");
static const Default = AltTool;
final String _str;
final String _publicString;
const SettingsImageTextType(this._publicString, this._str);
String toInternalString() {
return _str;
}
String toPublicString() {
return tr(_publicString);
}
static const options = <SettingsImageTextType>[
AltTool,
Tooltip,
Alt,
None,
];
static SettingsImageTextType fromInternalString(String str) {
for (var opt in options) {
if (opt.toInternalString() == str) {
return opt;
}
}
return Default;
}
static SettingsImageTextType fromPublicString(String str) {
for (var opt in options) {
if (opt.toPublicString() == str) {
return opt;
}
}
return Default;
}
@override
String toString() {
assert(false,
"SettingsThemeOverrideTagLocation toString should never be called");
return "";
}
}
class SettingsThemeVectorGraphics {
static const On = SettingsThemeVectorGraphics(
"settings.display.images.theming.themeVectorGraphics.on", "on");
static const Off = SettingsThemeVectorGraphics(
"settings.display.images.theming.themeVectorGraphics.off", "off");
static const Filter = SettingsThemeVectorGraphics(
"settings.display.images.theming.themeVectorGraphics.filter", "filter");
static const Default = On;
final String _str;
final String _publicString;
const SettingsThemeVectorGraphics(this._publicString, this._str);
String toInternalString() {
return _str;
}
String toPublicString() {
return tr(_publicString);
}
static const options = <SettingsThemeVectorGraphics>[
On,
Off,
Filter,
];
static SettingsThemeVectorGraphics fromInternalString(String str) {
for (var opt in options) {
if (opt.toInternalString() == str) {
return opt;
}
}
return Default;
}
static SettingsThemeVectorGraphics fromPublicString(String str) {
for (var opt in options) {
if (opt.toPublicString() == str) {
return opt;
}
}
return Default;
}
@override
String toString() {
assert(
false, "SettingsThemeVectorGraphics toString should never be called");
return "";
}
}
class SettingsVectorGraphicsAdjustColors {
static const All = SettingsVectorGraphicsAdjustColors(
"settings.display.images.theming.adjustColors.all", "all");
static const BnW = SettingsVectorGraphicsAdjustColors(
"settings.display.images.theming.adjustColors.blackAndWhite",
"black_and_white");
static const Grays = SettingsVectorGraphicsAdjustColors(
"settings.display.images.theming.adjustColors.grays", "grays");
static const Default = All;
final String _str;
final String _publicString;
const SettingsVectorGraphicsAdjustColors(this._publicString, this._str);
String toInternalString() {
return _str;
}
String toPublicString() {
return tr(_publicString);
}
static const options = <SettingsVectorGraphicsAdjustColors>[
BnW,
Grays,
All,
];
static SettingsVectorGraphicsAdjustColors fromInternalString(String str) {
for (var opt in options) {
if (opt.toInternalString() == str) {
return opt;
}
}
return Default;
}
static SettingsVectorGraphicsAdjustColors fromPublicString(String str) {
for (var opt in options) {
if (opt.toPublicString() == str) {
return opt;
}
}
return Default;
}
@override
String toString() {
assert(false,
"SettingsVectorGraphicsAdjustColors toString should never be called");
return "";
}
}
String generateRandomId() {
return Uuid().v4().substring(0, 8);
}
@ -920,3 +1201,16 @@ class SettingsTitle {
return "";
}
}
Set<String> parseTags(String tags) {
return tags
.toLowerCase()
.split(",")
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.toSet();
}
String csvTags(Set<String> tags) {
return tags.join(", ");
}

View File

@ -0,0 +1,57 @@
/*
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:flutter/material.dart';
class HeroDialogRoute<T> extends PageRoute<T> {
HeroDialogRoute({this.builder}) : super();
final WidgetBuilder builder;
@override
bool get opaque => false;
@override
bool get barrierDismissible => true;
@override
Duration get transitionDuration => const Duration(milliseconds: 200);
@override
bool get maintainState => true;
@override
Color get barrierColor => Colors.black54;
@override
Widget buildTransitions(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
return FadeTransition(
opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut),
child: child);
}
@override
Widget buildPage(BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation) {
return builder(context);
}
@override
String get barrierLabel => "close preview";
}

View File

@ -0,0 +1,479 @@
/*
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:flutter_svg/flutter_svg.dart';
import 'package:flutter_svg/svg.dart';
import 'package:gitjournal/utils/logger.dart';
import 'package:provider/provider.dart';
import 'package:gitjournal/settings.dart';
import 'package:gitjournal/utils/hero_dialog.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 }

View File

@ -1,5 +1,6 @@
/*
Copyright 2020-2021 Vishesh Handa <me@vhanda.in>
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.
@ -14,12 +15,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:function_types/function_types.dart';
@ -33,6 +31,7 @@ import 'package:gitjournal/folder_views/common.dart';
import 'package:gitjournal/utils.dart';
import 'package:gitjournal/utils/link_resolver.dart';
import 'package:gitjournal/utils/logger.dart';
import 'package:gitjournal/widgets/image_renderer.dart';
class MarkdownRenderer extends StatelessWidget {
final Note note;
@ -59,7 +58,7 @@ class MarkdownRenderer extends StatelessWidget {
var markdownStyleSheet = MarkdownStyleSheet.fromTheme(theme).copyWith(
code: theme.textTheme.bodyText2.copyWith(
backgroundColor: theme.dialogBackgroundColor,
fontFamily: "monospace",
fontFamily: 'monospace',
fontSize: theme.textTheme.bodyText2.fontSize * 0.85,
),
tableBorder: TableBorder.all(color: theme.highlightColor, width: 0),
@ -111,15 +110,16 @@ class MarkdownRenderer extends StatelessWidget {
try {
await launch(link);
} catch (e, stackTrace) {
Log.e("Opening Link", ex: e, stacktrace: stackTrace);
Log.e('Opening Link', ex: e, stacktrace: stackTrace);
showSnackbar(
context,
tr('widgets.NoteViewer.linkNotFound', args: [link]),
);
}
},
imageBuilder: (url, title, alt) => kDefaultImageBuilder(
url, note.parent.folderPath + p.separator, null, null),
imageBuilder: (url, title, alt) => ThemableImage(
url, note.parent.folderPath + p.separator,
titel: title, altText: alt),
extensionSet: markdownExtensions(),
);
@ -134,91 +134,7 @@ class MarkdownRenderer extends StatelessWidget {
markdownExtensions.inlineSyntaxes.insert(1, TaskListSyntax());
return markdownExtensions;
}
/*
Widget _buildFooter(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 8.0, bottom: 8.0),
child: Row(
children: <Widget>[
IconButton(
icon: Icon(Icons.arrow_left),
tooltip: 'Previous Entry',
onPressed: showPrevNoteFunc,
),
Expanded(
flex: 10,
child: Text(''),
),
IconButton(
icon: Icon(Icons.arrow_right),
tooltip: 'Next Entry',
onPressed: showNextNoteFunc,
),
],
),
);
}
*/
}
//
// Copied from flutter_markdown
// But it uses CachedNetworkImage
//
typedef Widget ImageBuilder(
Uri uri, String imageDirectory, double width, double height);
final ImageBuilder kDefaultImageBuilder = (
Uri uri,
String imageDirectory,
double width,
double height,
) {
if (uri.scheme == 'http' || uri.scheme == 'https') {
return CachedNetworkImage(
imageUrl: uri.toString(),
width: width,
height: height,
placeholder: (context, url) => const CircularProgressIndicator(),
errorWidget: (context, url, error) => const Icon(Icons.error),
);
} else if (uri.scheme == 'data') {
return _handleDataSchemeUri(uri, width, height);
} else if (uri.scheme == "resource") {
return Image.asset(uri.path, width: width, height: height);
} else {
Uri fileUri = imageDirectory != null
? Uri.parse(imageDirectory + uri.toString())
: uri;
if (fileUri.scheme == 'http' || fileUri.scheme == 'https') {
return CachedNetworkImage(
imageUrl: fileUri.toString(),
width: width,
height: height,
placeholder: (context, url) => const CircularProgressIndicator(),
errorWidget: (context, url, error) => const Icon(Icons.error),
);
} else {
return Image.file(File.fromUri(fileUri), width: width, height: height);
}
}
};
Widget _handleDataSchemeUri(Uri uri, final double width, final double height) {
final String mimeType = uri.data.mimeType;
if (mimeType.startsWith('image/')) {
return Image.memory(
uri.data.contentAsBytes(),
width: width,
height: height,
);
} else if (mimeType.startsWith('text/')) {
return Text(uri.data.contentAsString());
}
return const SizedBox();
}
/*
/// Parse ==Words==