From 24cdc236e138cec691ddaac36ee98a3b8488766b Mon Sep 17 00:00:00 2001 From: Roland Fredenhagen Date: Fri, 5 Mar 2021 01:07:19 +0100 Subject: [PATCH] feat: :sparkles: SVG Rendering, Image theming and Image caption fixes issues #405 #410 #246 --- assets/langs/en.yaml | 70 +++ lib/screens/settings_display_images.dart | 77 +++ .../settings_display_images_caption.dart | 191 +++++++ .../settings_display_images_theming.dart | 210 ++++++++ lib/screens/settings_screen.dart | 13 + lib/settings.dart | 296 ++++++++++- lib/utils/hero_dialog.dart | 57 +++ lib/widgets/image_renderer.dart | 479 ++++++++++++++++++ lib/widgets/markdown_renderer.dart | 98 +--- 9 files changed, 1399 insertions(+), 92 deletions(-) create mode 100644 lib/screens/settings_display_images.dart create mode 100644 lib/screens/settings_display_images_caption.dart create mode 100644 lib/screens/settings_display_images_theming.dart create mode 100644 lib/utils/hero_dialog.dart create mode 100644 lib/widgets/image_renderer.dart diff --git a/assets/langs/en.yaml b/assets/langs/en.yaml index de8aad5c..9e1e01b8 100644 --- a/assets/langs/en.yaml +++ b/assets/langs/en.yaml @@ -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 “ - ” + altText: Current order is “ - ” + 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: diff --git a/lib/screens/settings_display_images.dart b/lib/screens/settings_display_images.dart new file mode 100644 index 00000000..f0d0f022 --- /dev/null +++ b/lib/screens/settings_display_images.dart @@ -0,0 +1,77 @@ +/* +Copyright 2020-2021 Roland Fredenhagen + +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 { + final doNotThemeTagsKey = GlobalKey>(); + final doThemeTagsKey = GlobalKey>(); + + @override + Widget build(BuildContext context) { + final body = ListView(children: [ + 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, + ); + } +} diff --git a/lib/screens/settings_display_images_caption.dart b/lib/screens/settings_display_images_caption.dart new file mode 100644 index 00000000..06c058a3 --- /dev/null +++ b/lib/screens/settings_display_images_caption.dart @@ -0,0 +1,191 @@ +/* +Copyright 2020-2021 Roland Fredenhagen + +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 { + final doNotCaptionTagsKey = GlobalKey>(); + final doCaptionTagsKey = GlobalKey>(); + @override + Widget build(BuildContext context) { + var settings = Provider.of(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: [ + 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, + ); + } +} diff --git a/lib/screens/settings_display_images_theming.dart b/lib/screens/settings_display_images_theming.dart new file mode 100644 index 00000000..6450ac0a --- /dev/null +++ b/lib/screens/settings_display_images_theming.dart @@ -0,0 +1,210 @@ +/* +Copyright 2020-2021 Roland Fredenhagen + +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 { + final doNotThemeTagsKey = GlobalKey>(); + final doThemeTagsKey = GlobalKey>(); + + @override + Widget build(BuildContext context) { + var settings = Provider.of(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: [ + 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, + ); + } +} diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 41963ce8..caf0c091 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -1,5 +1,6 @@ /* Copyright 2020-2021 Vishesh Handa + Roland Fredenhagen 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 { 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( diff --git a/lib/settings.dart b/lib/settings.dart index 532007b1..0add54bc 100644 --- a/lib/settings.dart +++ b/lib/settings.dart @@ -1,5 +1,6 @@ /* Copyright 2020-2021 Vishesh Handa + Roland Fredenhagen 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 doNotThemeTags = {"notheme", "!nt"}; + Set 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 doNotCaptionTags = {"nocaption", "!nc"}; + Set 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 _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 = [ + 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 = [ + 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 = [ + 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 parseTags(String tags) { + return tags + .toLowerCase() + .split(",") + .map((e) => e.trim()) + .where((e) => e.isNotEmpty) + .toSet(); +} + +String csvTags(Set tags) { + return tags.join(", "); +} diff --git a/lib/utils/hero_dialog.dart b/lib/utils/hero_dialog.dart new file mode 100644 index 00000000..ab340f03 --- /dev/null +++ b/lib/utils/hero_dialog.dart @@ -0,0 +1,57 @@ +/* +Copyright 2020-2021 Roland Fredenhagen + +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 extends PageRoute { + 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 animation, + Animation secondaryAnimation, Widget child) { + return FadeTransition( + opacity: CurvedAnimation(parent: animation, curve: Curves.easeOut), + child: child); + } + + @override + Widget buildPage(BuildContext context, Animation animation, + Animation secondaryAnimation) { + return builder(context); + } + + @override + String get barrierLabel => "close preview"; +} diff --git a/lib/widgets/image_renderer.dart b/lib/widgets/image_renderer.dart new file mode 100644 index 00000000..8fac1068 --- /dev/null +++ b/lib/widgets/image_renderer.dart @@ -0,0 +1,479 @@ +/* +Copyright 2020-2021 Roland Fredenhagen + +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 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(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(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(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(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 + + ''), + 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 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([ + -(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 } diff --git a/lib/widgets/markdown_renderer.dart b/lib/widgets/markdown_renderer.dart index d3836124..86c47590 100644 --- a/lib/widgets/markdown_renderer.dart +++ b/lib/widgets/markdown_renderer.dart @@ -1,5 +1,6 @@ /* Copyright 2020-2021 Vishesh Handa + Roland Fredenhagen 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: [ - 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==