Added (very!) experimental web support
This commit is contained in:
parent
4667f7a69b
commit
6a5a123026
21
.metadata
21
.metadata
|
|
@ -4,7 +4,7 @@
|
||||||
# This file should be version controlled and should not be manually edited.
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
version:
|
version:
|
||||||
revision: "a14f74ff3a1cbd521163c5f03d68113d50af93d3"
|
revision: "b0850beeb25f6d5b10426284f506557f66181b36"
|
||||||
channel: "stable"
|
channel: "stable"
|
||||||
|
|
||||||
project_type: app
|
project_type: app
|
||||||
|
|
@ -13,20 +13,17 @@ project_type: app
|
||||||
migration:
|
migration:
|
||||||
platforms:
|
platforms:
|
||||||
- platform: root
|
- platform: root
|
||||||
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
|
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
|
||||||
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
|
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
|
||||||
- platform: android
|
- platform: android
|
||||||
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
|
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
|
||||||
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
|
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
|
||||||
- platform: linux
|
|
||||||
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
|
|
||||||
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
|
|
||||||
- platform: web
|
- platform: web
|
||||||
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
|
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
|
||||||
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
|
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
|
||||||
- platform: windows
|
- platform: windows
|
||||||
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
|
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
|
||||||
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
|
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
|
||||||
|
|
||||||
# User provided section
|
# User provided section
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1060,7 +1060,35 @@ class _MainAppState extends State<MainApp> {
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 200, child: MoveWindow()))
|
height: 200, child: MoveWindow()))
|
||||||
]
|
]
|
||||||
: [Expanded(child: selector)]),
|
: desktopLayoutRequired(context)
|
||||||
|
? [
|
||||||
|
// bottom left tile
|
||||||
|
const SizedBox(width: 304, height: 200),
|
||||||
|
SizedBox(
|
||||||
|
height: 200,
|
||||||
|
child: AnimatedOpacity(
|
||||||
|
opacity: menuVisible ? 1.0 : 0.0,
|
||||||
|
duration:
|
||||||
|
const Duration(milliseconds: 300),
|
||||||
|
child: VerticalDivider(
|
||||||
|
width: 2,
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.onSurface
|
||||||
|
.withAlpha(20)))),
|
||||||
|
AnimatedOpacity(
|
||||||
|
opacity: desktopTitleVisible ? 1.0 : 0.0,
|
||||||
|
duration: desktopTitleVisible
|
||||||
|
? const Duration(milliseconds: 300)
|
||||||
|
: const Duration(milliseconds: 0),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: selector,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Expanded(child: SizedBox(height: 200))
|
||||||
|
]
|
||||||
|
: [Expanded(child: selector)]),
|
||||||
actions: desktopControlsActions(context, [
|
actions: desktopControlsActions(context, [
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
allowMultipleChats
|
allowMultipleChats
|
||||||
|
|
|
||||||
|
|
@ -520,7 +520,7 @@ class _ScreenSettingsState extends State<ScreenSettings> {
|
||||||
context: context,
|
context: context,
|
||||||
description:
|
description:
|
||||||
"\n${AppLocalizations.of(context)!.settingsDescriptionInterface}"),
|
"\n${AppLocalizations.of(context)!.settingsDescriptionInterface}"),
|
||||||
(!desktopFeature())
|
(!desktopFeature(web: true))
|
||||||
? button(
|
? button(
|
||||||
AppLocalizations.of(context)!
|
AppLocalizations.of(context)!
|
||||||
.settingsTitleVoice,
|
.settingsTitleVoice,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
// ignore: avoid_web_libraries_in_flutter
|
||||||
|
import 'dart:html' as html;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import '../main.dart';
|
import '../main.dart';
|
||||||
import '../worker/haptic.dart';
|
import '../worker/haptic.dart';
|
||||||
|
|
@ -42,18 +45,37 @@ class _ScreenSettingsExportState extends State<ScreenSettingsExport> {
|
||||||
button(AppLocalizations.of(context)!.settingsExportChats,
|
button(AppLocalizations.of(context)!.settingsExportChats,
|
||||||
Icons.upload_rounded, () async {
|
Icons.upload_rounded, () async {
|
||||||
selectionHaptic();
|
selectionHaptic();
|
||||||
var path = await FilePicker.platform.saveFile(
|
var name =
|
||||||
type: FileType.custom,
|
"ollama-export-${DateFormat('yyyy-MM-dd-H-m-s').format(DateTime.now())}.json";
|
||||||
allowedExtensions: ["json"],
|
var content =
|
||||||
fileName:
|
jsonEncode(prefs!.getStringList("chats") ?? []);
|
||||||
"ollama-export-${DateFormat('yyyy-MM-dd-H-m-s').format(DateTime.now())}.json",
|
if (kIsWeb) {
|
||||||
bytes: utf8.encode(
|
final bytes = utf8.encode(content);
|
||||||
jsonEncode(prefs!.getStringList("chats") ?? [])));
|
final blob = html.Blob([bytes]);
|
||||||
selectionHaptic();
|
final url = html.Url.createObjectUrlFromBlob(blob);
|
||||||
if (path == null) return;
|
final anchor = html.document.createElement("a")
|
||||||
if (desktopFeature()) {
|
as html.AnchorElement
|
||||||
File(path).writeAsString(
|
..href = url
|
||||||
jsonEncode(prefs!.getStringList("chats") ?? []));
|
..style.display = "none"
|
||||||
|
..download = name;
|
||||||
|
html.document.body!.children.add(anchor);
|
||||||
|
|
||||||
|
anchor.click();
|
||||||
|
|
||||||
|
html.document.body!.children.remove(anchor);
|
||||||
|
html.Url.revokeObjectUrl(url);
|
||||||
|
} else {
|
||||||
|
var path = await FilePicker.platform.saveFile(
|
||||||
|
type: FileType.custom,
|
||||||
|
allowedExtensions: ["json"],
|
||||||
|
fileName: name,
|
||||||
|
bytes: utf8.encode(jsonEncode(
|
||||||
|
prefs!.getStringList("chats") ?? [])));
|
||||||
|
selectionHaptic();
|
||||||
|
if (path == null) return;
|
||||||
|
if (desktopFeature()) {
|
||||||
|
File(path).writeAsString(content);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
allowMultipleChats
|
allowMultipleChats
|
||||||
|
|
@ -95,10 +117,18 @@ class _ScreenSettingsExportState extends State<ScreenSettingsExport> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
File file = File(
|
String content;
|
||||||
result.files.single.path!);
|
try {
|
||||||
var content =
|
File file = File(
|
||||||
await file.readAsString();
|
result.files.single.path!);
|
||||||
|
content =
|
||||||
|
await file.readAsString();
|
||||||
|
} catch (_) {
|
||||||
|
content = utf8.decode(result
|
||||||
|
.files
|
||||||
|
.single
|
||||||
|
.bytes as List<int>);
|
||||||
|
}
|
||||||
List<dynamic> tmpHistory =
|
List<dynamic> tmpHistory =
|
||||||
jsonDecode(content);
|
jsonDecode(content);
|
||||||
List<String> history = [];
|
List<String> history = [];
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
import '../main.dart';
|
import '../main.dart';
|
||||||
import '../worker/haptic.dart';
|
import '../worker/haptic.dart';
|
||||||
|
|
@ -342,69 +343,74 @@ class _ScreenSettingsInterfaceState extends State<ScreenSettingsInterface> {
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
SegmentedButton(
|
!kIsWeb
|
||||||
segments: [
|
? SegmentedButton(
|
||||||
ButtonSegment(
|
segments: [
|
||||||
value: "device",
|
ButtonSegment(
|
||||||
label: Text(AppLocalizations.of(context)!
|
value: "device",
|
||||||
.settingsThemeDevice),
|
label: Text(AppLocalizations.of(context)!
|
||||||
icon: const Icon(Icons.devices_rounded)),
|
.settingsThemeDevice),
|
||||||
ButtonSegment(
|
icon: const Icon(Icons.devices_rounded)),
|
||||||
value: "ollama",
|
ButtonSegment(
|
||||||
label: Text(AppLocalizations.of(context)!
|
value: "ollama",
|
||||||
.settingsThemeOllama),
|
label: Text(AppLocalizations.of(context)!
|
||||||
icon: const ImageIcon(
|
.settingsThemeOllama),
|
||||||
AssetImage("assets/logo512.png")))
|
icon: const ImageIcon(
|
||||||
],
|
AssetImage("assets/logo512.png")))
|
||||||
selected: {
|
],
|
||||||
(prefs?.getBool("useDeviceTheme") ?? false)
|
selected: {
|
||||||
? "device"
|
(prefs?.getBool("useDeviceTheme") ?? false)
|
||||||
: "ollama"
|
? "device"
|
||||||
},
|
: "ollama"
|
||||||
onSelectionChanged: (p0) {
|
},
|
||||||
selectionHaptic();
|
onSelectionChanged: (p0) {
|
||||||
showDialog(
|
selectionHaptic();
|
||||||
context: context,
|
showDialog(
|
||||||
builder: (context) {
|
context: context,
|
||||||
return StatefulBuilder(
|
builder: (context) {
|
||||||
builder: (context, setLocalState) {
|
return StatefulBuilder(
|
||||||
return AlertDialog(
|
builder: (context, setLocalState) {
|
||||||
title: Text(AppLocalizations.of(context)!
|
return AlertDialog(
|
||||||
.settingsThemeRestartTitle),
|
title: Text(
|
||||||
content: Column(
|
AppLocalizations.of(context)!
|
||||||
mainAxisSize: MainAxisSize.min,
|
.settingsThemeRestartTitle),
|
||||||
children: [
|
content: Column(
|
||||||
Text(AppLocalizations.of(context)!
|
mainAxisSize: MainAxisSize.min,
|
||||||
.settingsThemeRestartDescription),
|
children: [
|
||||||
]),
|
Text(AppLocalizations.of(
|
||||||
actions: [
|
context)!
|
||||||
TextButton(
|
.settingsThemeRestartDescription),
|
||||||
onPressed: () {
|
]),
|
||||||
selectionHaptic();
|
actions: [
|
||||||
Navigator.of(context).pop();
|
TextButton(
|
||||||
},
|
onPressed: () {
|
||||||
child: Text(AppLocalizations.of(
|
selectionHaptic();
|
||||||
context)!
|
Navigator.of(context).pop();
|
||||||
.settingsThemeRestartCancel)),
|
},
|
||||||
TextButton(
|
child: Text(AppLocalizations.of(
|
||||||
onPressed: () async {
|
context)!
|
||||||
selectionHaptic();
|
.settingsThemeRestartCancel)),
|
||||||
await prefs!.setBool(
|
TextButton(
|
||||||
"useDeviceTheme",
|
onPressed: () async {
|
||||||
p0.elementAt(0) == "device");
|
selectionHaptic();
|
||||||
if (desktopFeature()) {
|
await prefs!.setBool(
|
||||||
exit(0);
|
"useDeviceTheme",
|
||||||
} else {
|
p0.elementAt(0) ==
|
||||||
Restart.restartApp();
|
"device");
|
||||||
}
|
if (desktopFeature()) {
|
||||||
},
|
exit(0);
|
||||||
child: Text(AppLocalizations.of(
|
} else {
|
||||||
context)!
|
Restart.restartApp();
|
||||||
.settingsThemeRestartRestart))
|
}
|
||||||
]);
|
},
|
||||||
});
|
child: Text(AppLocalizations.of(
|
||||||
});
|
context)!
|
||||||
}),
|
.settingsThemeRestartRestart))
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
: const SizedBox.shrink(),
|
||||||
titleDivider(),
|
titleDivider(),
|
||||||
toggle(context, "Fix to code block not scrollable",
|
toggle(context, "Fix to code block not scrollable",
|
||||||
(prefs!.getBool("fixCodeblockScroll") ?? false),
|
(prefs!.getBool("fixCodeblockScroll") ?? false),
|
||||||
|
|
|
||||||
|
|
@ -6,26 +6,30 @@ import 'package:flutter/foundation.dart';
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
|
|
||||||
bool desktopFeature({bool web = false}) {
|
bool desktopFeature({bool web = false}) {
|
||||||
return (Platform.isWindows ||
|
try {
|
||||||
Platform.isLinux ||
|
return (Platform.isWindows ||
|
||||||
Platform.isMacOS ||
|
Platform.isLinux ||
|
||||||
(web ? kIsWeb : false));
|
Platform.isMacOS ||
|
||||||
|
(web ? kIsWeb : false));
|
||||||
|
} catch (_) {
|
||||||
|
return web ? kIsWeb : false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool desktopLayout(BuildContext context,
|
bool desktopLayout(BuildContext context,
|
||||||
{bool web = false, double? value, double valueCap = 1000}) {
|
{bool web = true, double? value, double valueCap = 1000}) {
|
||||||
value ??= MediaQuery.of(context).size.width;
|
value ??= MediaQuery.of(context).size.width;
|
||||||
return (desktopFeature(web: web) || value >= valueCap);
|
return (desktopFeature(web: web) || value >= valueCap);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool desktopLayoutRequired(BuildContext context,
|
bool desktopLayoutRequired(BuildContext context,
|
||||||
{bool web = false, double? value, double valueCap = 1000}) {
|
{bool web = true, double? value, double valueCap = 1000}) {
|
||||||
value ??= MediaQuery.of(context).size.width;
|
value ??= MediaQuery.of(context).size.width;
|
||||||
return (desktopFeature(web: web) && value >= valueCap);
|
return (desktopFeature(web: web) && value >= valueCap);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool desktopLayoutNotRequired(BuildContext context,
|
bool desktopLayoutNotRequired(BuildContext context,
|
||||||
{bool web = false, double? value, double valueCap = 1000}) {
|
{bool web = true, double? value, double valueCap = 1000}) {
|
||||||
value ??= MediaQuery.of(context).size.width;
|
value ??= MediaQuery.of(context).size.width;
|
||||||
return (value >= valueCap);
|
return (value >= valueCap);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ Future<bool> updatesSupported(Function setState,
|
||||||
"com.machiav3lli.fdroid",
|
"com.machiav3lli.fdroid",
|
||||||
"nya.kitsunyan.foxydroid"
|
"nya.kitsunyan.foxydroid"
|
||||||
];
|
];
|
||||||
if (!desktopFeature()) {
|
if (!desktopFeature(web: true)) {
|
||||||
if ((await InstallReferrer.referrer !=
|
if ((await InstallReferrer.referrer !=
|
||||||
InstallationAppReferrer.androidManually) ||
|
InstallationAppReferrer.androidManually) ||
|
||||||
(installerApps
|
(installerApps
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@
|
||||||
],
|
],
|
||||||
|
|
||||||
"it": [
|
"it": [
|
||||||
|
"deleteChat",
|
||||||
|
"renameChat",
|
||||||
"settingsDescriptionBehavior",
|
"settingsDescriptionBehavior",
|
||||||
"settingsDescriptionInterface",
|
"settingsDescriptionInterface",
|
||||||
"settingsDescriptionVoice",
|
"settingsDescriptionVoice",
|
||||||
|
|
@ -30,6 +32,8 @@
|
||||||
],
|
],
|
||||||
|
|
||||||
"tr": [
|
"tr": [
|
||||||
|
"deleteChat",
|
||||||
|
"renameChat",
|
||||||
"settingsDescriptionBehavior",
|
"settingsDescriptionBehavior",
|
||||||
"settingsDescriptionInterface",
|
"settingsDescriptionInterface",
|
||||||
"settingsDescriptionVoice",
|
"settingsDescriptionVoice",
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
|
|
@ -0,0 +1,101 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<!-- <base href="$FLUTTER_BASE_HREF"> -->
|
||||||
|
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
|
<meta name="description" content="A modern and easy-to-use client for Ollama">
|
||||||
|
|
||||||
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
|
<meta name="apple-mobile-web-app-title" content="ollama_app">
|
||||||
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
|
<link rel="icon" type="image/png" href="favicon.png" />
|
||||||
|
|
||||||
|
<title>Ollama App</title>
|
||||||
|
<link rel="manifest" href="manifest.json">
|
||||||
|
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
width: 28px;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: black;
|
||||||
|
transform-origin: top;
|
||||||
|
display: grid;
|
||||||
|
animation: l3-0 1s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader::before,
|
||||||
|
.loader::after {
|
||||||
|
content: "";
|
||||||
|
grid-area: 1/1;
|
||||||
|
background: lightgrey;
|
||||||
|
border-radius: 50%;
|
||||||
|
transform-origin: top;
|
||||||
|
animation: inherit;
|
||||||
|
animation-name: l3-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader::after {
|
||||||
|
background: black;
|
||||||
|
--s: 180deg;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes l3-0 {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
20% {
|
||||||
|
transform: rotate(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes l3-1 {
|
||||||
|
50% {
|
||||||
|
transform: rotate(var(--s, 90deg))
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<style>
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader::after {
|
||||||
|
background: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="loader"></div>
|
||||||
|
<script src="flutter_bootstrap.js" async></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"name": "Ollama App",
|
||||||
|
"short_name": "Ollama",
|
||||||
|
"start_url": ".",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#000000",
|
||||||
|
"theme_color": "#000000",
|
||||||
|
"description": "A modern and easy-to-use client for Ollama",
|
||||||
|
"orientation": "portrait-primary",
|
||||||
|
"prefer_related_applications": false,
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-maskable-192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "icons/Icon-maskable-512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png",
|
||||||
|
"purpose": "maskable"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue