mirror of
https://github.com/GitJournal/GitJournal.git
synced 2025-06-26 00:29:20 +08:00
feat: ✨ SVG Rendering, Image theming and Image caption
fixes issues #405 #410 #246
This commit is contained in:

committed by
Vishesh Handa

parent
27f314eb65
commit
24cdc236e1
@ -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 “” 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 “” 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:
|
||||
|
77
lib/screens/settings_display_images.dart
Normal file
77
lib/screens/settings_display_images.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
191
lib/screens/settings_display_images_caption.dart
Normal file
191
lib/screens/settings_display_images_caption.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
210
lib/screens/settings_display_images_theming.dart
Normal file
210
lib/screens/settings_display_images_theming.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
@ -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(", ");
|
||||
}
|
||||
|
57
lib/utils/hero_dialog.dart
Normal file
57
lib/utils/hero_dialog.dart
Normal 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";
|
||||
}
|
479
lib/widgets/image_renderer.dart
Normal file
479
lib/widgets/image_renderer.dart
Normal 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 }
|
@ -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==
|
||||
|
Reference in New Issue
Block a user