Updates for web
This commit is contained in:
parent
a816cf8f65
commit
b1c11ba21f
|
@ -15,6 +15,11 @@
|
|||
"description": "Text displayed for settings option",
|
||||
"context": "Visible in the side bar"
|
||||
},
|
||||
"optionInstallPwa": "Install Webapp",
|
||||
"@optionInstallPwa": {
|
||||
"description": "Text displayed for install PWA option",
|
||||
"context": "Visible in the side bar"
|
||||
},
|
||||
"optionNoChatFound": "No chats found",
|
||||
"@optionNoChatFound": {
|
||||
"description": "Text displayed when no chats are found",
|
||||
|
@ -160,6 +165,22 @@
|
|||
"description": "Text displayed when the model name is invalid",
|
||||
"context": "Visible in the model dialog"
|
||||
},
|
||||
"modelDialogAddAllowanceTitle": "Allow Proxy",
|
||||
"modelDialogAddAllowanceDescription": "Ollama App must check if the entered model is valid. For that, we normally send a web request to the Ollama model list and check the status code, but because you're using the web client, we can't do that directly. Instead, the app will send the request to a different api, hosted by JHubi1, to check for us.\nThis is a one-time request and will only be sent when you add a new model.\nYour IP address will be sent with the request and might be stored for up to ten minutes to prevent spamming with potential harmful intentions.\nIf you accept, your selection will be remembered in the future; if not, nothing will be sent and the model won't be added.",
|
||||
"@modelDialogAddAllowanceDescription": {
|
||||
"description": "Description of the allow proxy dialog",
|
||||
"context": "Visible in the model dialog"
|
||||
},
|
||||
"modelDialogAddAllowanceAllow": "Allow",
|
||||
"@modelDialogAddAllowanceAllow": {
|
||||
"description": "Text displayed for allow button, should be capitalized",
|
||||
"context": "Visible in the model dialog"
|
||||
},
|
||||
"modelDialogAddAllowanceDeny": "Deny",
|
||||
"@modelDialogAddAllowanceDeny": {
|
||||
"description": "Text displayed for deny button, should be capitalized",
|
||||
"context": "Visible in the model dialog"
|
||||
},
|
||||
"modelDialogAddAssuranceTitle": "Add {model}?",
|
||||
"@modelDialogAddAssuranceTitle": {
|
||||
"description": "Title of the add model assurance dialog",
|
||||
|
@ -363,14 +384,14 @@
|
|||
"description": "Text displayed when the host is being checked",
|
||||
"context": "Visible in the settings view"
|
||||
},
|
||||
"settingsHostInvalid": "Issue: {type, select, url{Invalid URL} host{Invalid Host} timeout{Request Failed. Server issues} other{Request Failed}}",
|
||||
"settingsHostInvalid": "Issue: {type, select, url{Invalid URL} host{Invalid Host} timeout{Request Failed. Server issues} ratelimit{Too many requests} other{Request Failed}}",
|
||||
"@settingsHostInvalid": {
|
||||
"description": "Text displayed when the host is invalid",
|
||||
"context": "Visible in the settings view",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"type": "String",
|
||||
"description": "Type of the issue, either 'url' or 'other' (preferably 'host')"
|
||||
"description": "Type of the issue, either 'url', 'host', 'timeout', 'ratelimit' or 'other' (preferably 'host')"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'dart:math';
|
|||
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
|
||||
|
@ -35,6 +36,8 @@ import 'package:permission_handler/permission_handler.dart';
|
|||
import 'package:dynamic_color/dynamic_color.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:version/version.dart';
|
||||
import 'package:pwa_install/pwa_install.dart' as pwa;
|
||||
import 'package:universal_html/html.dart' as html;
|
||||
|
||||
// client configuration
|
||||
|
||||
|
@ -89,6 +92,10 @@ void Function(void Function())? setGlobalState;
|
|||
void Function(void Function())? setMainAppState;
|
||||
|
||||
void main() {
|
||||
pwa.PWAInstall().setup(installCallback: () {
|
||||
debugPrint('APP INSTALLED!');
|
||||
});
|
||||
|
||||
runApp(const App());
|
||||
|
||||
if (desktopFeature()) {
|
||||
|
@ -195,7 +202,9 @@ class _MainAppState extends State<MainApp> {
|
|||
left: desktopLayoutRequired(context) ? 17 : 12,
|
||||
right: desktopLayoutRequired(context) ? 17 : 12);
|
||||
return List.from([
|
||||
desktopFeature() ? const SizedBox(height: 8) : const SizedBox.shrink(),
|
||||
(desktopLayoutNotRequired(context) || kIsWeb)
|
||||
? const SizedBox(height: 8)
|
||||
: const SizedBox.shrink(),
|
||||
desktopLayoutNotRequired(context)
|
||||
? const SizedBox.shrink()
|
||||
: (Padding(
|
||||
|
@ -324,6 +333,47 @@ class _MainAppState extends State<MainApp> {
|
|||
const SizedBox(width: 16),
|
||||
])))))
|
||||
: const SizedBox.shrink(),
|
||||
(pwa.PWAInstall().installPromptEnabled &&
|
||||
pwa.PWAInstall().launchMode == pwa.LaunchMode.browser)
|
||||
? (Padding(
|
||||
padding: padding,
|
||||
child: InkWell(
|
||||
enableFeedback: false,
|
||||
customBorder: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(50))),
|
||||
onTap: () {
|
||||
selectionHaptic();
|
||||
if (!desktopLayout(context)) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
pwa.PWAInstall().onAppInstalled = () {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
pwa.setLaunchModePWA();
|
||||
setMainAppState!(() {});
|
||||
});
|
||||
};
|
||||
pwa.PWAInstall().promptInstall_();
|
||||
setState(() {});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 16, bottom: 16),
|
||||
child: Row(children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 12),
|
||||
child: desktopLayoutNotRequired(context)
|
||||
? const Icon(Icons.install_desktop_rounded)
|
||||
: const Icon(Icons.install_mobile_rounded)),
|
||||
Expanded(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.optionInstallPwa,
|
||||
softWrap: false,
|
||||
overflow: TextOverflow.fade,
|
||||
style:
|
||||
const TextStyle(fontWeight: FontWeight.w500)),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
])))))
|
||||
: const SizedBox.shrink(),
|
||||
(desktopLayoutNotRequired(context) &&
|
||||
(!allowMultipleChats && !allowSettings))
|
||||
? const SizedBox.shrink()
|
||||
|
@ -437,7 +487,8 @@ class _MainAppState extends State<MainApp> {
|
|||
}
|
||||
});
|
||||
},
|
||||
onLongPress: desktopFeature()
|
||||
onLongPress: (desktopFeature() ||
|
||||
(kIsWeb && desktopLayoutNotRequired(context)))
|
||||
? null
|
||||
: () async {
|
||||
selectionHaptic();
|
||||
|
@ -486,7 +537,10 @@ class _MainAppState extends State<MainApp> {
|
|||
AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
child:
|
||||
((desktopFeature() &&
|
||||
(((desktopFeature() ||
|
||||
(kIsWeb &&
|
||||
desktopLayoutNotRequired(
|
||||
context))) &&
|
||||
(hoveredChat ==
|
||||
jsonDecode(item)["uuid"])) ||
|
||||
!allowMultipleChats)
|
||||
|
@ -643,7 +697,9 @@ class _MainAppState extends State<MainApp> {
|
|||
))
|
||||
: const SizedBox(width: 16)),
|
||||
]))));
|
||||
return desktopFeature() || !allowMultipleChats
|
||||
return (desktopFeature() ||
|
||||
(kIsWeb && desktopLayoutNotRequired(context))) ||
|
||||
!allowMultipleChats
|
||||
? child
|
||||
: Dismissible(
|
||||
key: Key(jsonDecode(item)["uuid"]),
|
||||
|
@ -689,6 +745,10 @@ class _MainAppState extends State<MainApp> {
|
|||
super.initState();
|
||||
mainContext = context;
|
||||
|
||||
if (kIsWeb) {
|
||||
html.querySelector(".loader")?.remove();
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) async {
|
||||
if (prefs == null) {
|
||||
|
@ -1529,7 +1589,7 @@ class _MainAppState extends State<MainApp> {
|
|||
inputTextColor: themeLight().colorScheme.onSurface,
|
||||
inputBorderRadius: BorderRadius.circular(32),
|
||||
inputPadding: const EdgeInsets.all(16),
|
||||
inputMargin: EdgeInsets.only(left: (MediaQuery.of(context).viewInsets.bottom == 0.0 && !desktopFeature()) ? 8 : 6, right: (MediaQuery.of(context).viewInsets.bottom == 0.0 && !desktopFeature()) ? 8 : 6, bottom: (MediaQuery.of(context).viewInsets.bottom == 0.0 && !desktopFeature()) ? 0 : 8),
|
||||
inputMargin: EdgeInsets.only(left: !desktopFeature(web: true) ? 8 : 6, right: !desktopFeature(web: true) ? 8 : 6, bottom: (MediaQuery.of(context).viewInsets.bottom == 0.0 && !desktopFeature(web: true)) ? 0 : 8),
|
||||
messageMaxWidth: (MediaQuery.of(context).size.width >= 1000)
|
||||
? (MediaQuery.of(context).size.width >= 1600)
|
||||
? (MediaQuery.of(context).size.width >= 2200)
|
||||
|
@ -1567,9 +1627,9 @@ class _MainAppState extends State<MainApp> {
|
|||
attachmentButtonMargin: EdgeInsets.zero,
|
||||
inputBackgroundColor: themeDark().colorScheme.onSurface.withAlpha(40),
|
||||
inputTextColor: themeDark().colorScheme.onSurface,
|
||||
inputBorderRadius: const BorderRadius.all(Radius.circular(64)),
|
||||
inputBorderRadius: BorderRadius.circular(32),
|
||||
inputPadding: const EdgeInsets.all(16),
|
||||
inputMargin: EdgeInsets.only(left: 8, right: 8, bottom: (MediaQuery.of(context).viewInsets.bottom == 0.0 && !desktopFeature()) ? 0 : 8),
|
||||
inputMargin: EdgeInsets.only(left: !desktopFeature(web: true) ? 8 : 6, right: !desktopFeature(web: true) ? 8 : 6, bottom: (MediaQuery.of(context).viewInsets.bottom == 0.0 && !desktopFeature(web: true)) ? 0 : 8),
|
||||
messageMaxWidth: (MediaQuery.of(context).size.width >= 1000)
|
||||
? (MediaQuery.of(context).size.width >= 1600)
|
||||
? (MediaQuery.of(context).size.width >= 2200)
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../main.dart';
|
||||
import '../worker/haptic.dart';
|
||||
|
@ -313,8 +312,19 @@ class _ScreenSettingsInterfaceState extends State<ScreenSettingsInterface> {
|
|||
Icons.calculate_rounded,
|
||||
null,
|
||||
onlyDesktopDescription: false,
|
||||
description:
|
||||
"\n${((prefs!.getDouble("timeoutMultiplier") ?? 1) == 10) ? "${(prefs!.getDouble("timeoutMultiplier") ?? 1).round()}." : (prefs!.getDouble("timeoutMultiplier") ?? 1)} x 30s = ${((prefs!.getDouble("timeoutMultiplier") ?? 1) * 30).round()}s ${secondsBeautify((prefs!.getDouble("timeoutMultiplier") ?? 1) * 30)}"),
|
||||
// making it complicated because web is weird and doesn't like to round numbers
|
||||
description: "\n${() {
|
||||
var value =
|
||||
(prefs!.getDouble("timeoutMultiplier") ?? 1);
|
||||
if (value == 10) {
|
||||
return "${value.round()}.";
|
||||
} else {
|
||||
if (!value.toString().contains(".")) {
|
||||
return "${value.toString()}.0";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
}.call()} x 30s = ${((prefs!.getDouble("timeoutMultiplier") ?? 1) * 30).round()}s ${secondsBeautify((prefs!.getDouble("timeoutMultiplier") ?? 1) * 30)}"),
|
||||
titleDivider(context: context),
|
||||
toggle(
|
||||
context,
|
||||
|
@ -368,7 +378,7 @@ class _ScreenSettingsInterfaceState extends State<ScreenSettingsInterface> {
|
|||
setState(() {});
|
||||
}),
|
||||
const SizedBox(height: 8),
|
||||
!kIsWeb
|
||||
(colorSchemeLight != null && colorSchemeDark != null)
|
||||
? SegmentedButton(
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'desktop.dart';
|
||||
|
@ -187,11 +188,8 @@ void setModel(BuildContext context, Function setState) {
|
|||
scrollDirection: Axis.vertical,
|
||||
child: Wrap(
|
||||
spacing: desktopLayout(context) ? 10.0 : 5.0,
|
||||
runSpacing: desktopFeature()
|
||||
? desktopFeature()
|
||||
? 10.0
|
||||
: 5.0
|
||||
: 0.0,
|
||||
runSpacing:
|
||||
desktopFeature(web: true) ? 10.0 : 0.0,
|
||||
alignment: WrapAlignment.center,
|
||||
children: List<Widget>.generate(
|
||||
models.length,
|
||||
|
@ -280,7 +278,7 @@ void setModel(BuildContext context, Function setState) {
|
|||
])));
|
||||
});
|
||||
|
||||
if (desktopFeature()) {
|
||||
if (desktopLayoutNotRequired(context)) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
|
@ -312,6 +310,7 @@ void addModel(BuildContext context, Function setState) async {
|
|||
baseUrl: "$host/api");
|
||||
bool canceled = false;
|
||||
bool networkError = false;
|
||||
bool ratelimitError = false;
|
||||
bool alreadyExists = false;
|
||||
final String invalidText =
|
||||
AppLocalizations.of(context)!.modelDialogAddPromptInvalid;
|
||||
|
@ -319,6 +318,8 @@ void addModel(BuildContext context, Function setState) async {
|
|||
AppLocalizations.of(context)!.settingsHostInvalid("other");
|
||||
final timeoutErrorText =
|
||||
AppLocalizations.of(context)!.settingsHostInvalid("timeout");
|
||||
final ratelimitErrorText =
|
||||
AppLocalizations.of(context)!.settingsHostInvalid("ratelimit");
|
||||
final alreadyExistsText =
|
||||
AppLocalizations.of(context)!.modelDialogAddPromptAlreadyExists;
|
||||
final downloadSuccessText =
|
||||
|
@ -337,6 +338,7 @@ void addModel(BuildContext context, Function setState) async {
|
|||
if (model == "") return false;
|
||||
canceled = false;
|
||||
networkError = false;
|
||||
ratelimitError = false;
|
||||
alreadyExists = false;
|
||||
try {
|
||||
var request = await client.listModels().timeout(Duration(
|
||||
|
@ -353,11 +355,48 @@ void addModel(BuildContext context, Function setState) async {
|
|||
networkError = true;
|
||||
return false;
|
||||
}
|
||||
var endpoint = "https://ollama.com/library/";
|
||||
if (kIsWeb) {
|
||||
if (!(prefs!.getBool("allowWebProxy") ?? false)) {
|
||||
bool returnValue = false;
|
||||
await showDialog(
|
||||
context: mainContext!,
|
||||
barrierDismissible: false,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!
|
||||
.modelDialogAddAllowanceTitle),
|
||||
content: SizedBox(
|
||||
width: 640,
|
||||
child: Text(AppLocalizations.of(context)!
|
||||
.modelDialogAddAllowanceDescription),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
canceled = true;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!
|
||||
.modelDialogAddAllowanceDeny)),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
returnValue = true;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!
|
||||
.modelDialogAddAllowanceAllow))
|
||||
]);
|
||||
});
|
||||
if (!returnValue) return false;
|
||||
prefs!.setBool("allowWebProxy", true);
|
||||
}
|
||||
endpoint = "https://end.jhubi1.com/ollama-proxy/";
|
||||
}
|
||||
http.Response response;
|
||||
try {
|
||||
response = await http
|
||||
.get(Uri.parse("https://ollama.com/library/$model"))
|
||||
.timeout(Duration(
|
||||
response = await http.get(Uri.parse("$endpoint$model")).timeout(
|
||||
Duration(
|
||||
seconds: (10.0 * (prefs!.getDouble("timeoutMultiplier") ?? 1.0))
|
||||
.round()));
|
||||
} catch (_) {
|
||||
|
@ -376,8 +415,11 @@ void addModel(BuildContext context, Function setState) async {
|
|||
return AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!
|
||||
.modelDialogAddAssuranceTitle(model)),
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.modelDialogAddAssuranceDescription(model)),
|
||||
content: SizedBox(
|
||||
width: 640,
|
||||
child: Text(AppLocalizations.of(context)!
|
||||
.modelDialogAddAssuranceDescription(model)),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
|
@ -398,10 +440,14 @@ void addModel(BuildContext context, Function setState) async {
|
|||
resetSystemNavigation(mainContext!);
|
||||
return returnValue;
|
||||
}
|
||||
if (response.statusCode == 429) {
|
||||
ratelimitError = true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
validatorErrorCallback: (content) {
|
||||
if (networkError) return networkErrorText;
|
||||
if (ratelimitError) return ratelimitErrorText;
|
||||
if (alreadyExists) return alreadyExistsText;
|
||||
if (canceled) return null;
|
||||
return invalidText;
|
||||
|
@ -774,8 +820,8 @@ Future<String> prompt(BuildContext context,
|
|||
left: 16,
|
||||
right: 16,
|
||||
top: 16,
|
||||
bottom: desktopFeature()
|
||||
? 16
|
||||
bottom: desktopFeature(web: true)
|
||||
? 12
|
||||
: MediaQuery.of(context).viewInsets.bottom),
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../main.dart';
|
||||
|
@ -11,29 +12,12 @@ void resetSystemNavigation(BuildContext context,
|
|||
Color? statusBarColor,
|
||||
Color? systemNavigationBarColor,
|
||||
Duration? delay}) {
|
||||
ColorScheme getColorScheme() {
|
||||
final ColorScheme schemeLight = themeLight().colorScheme;
|
||||
final ColorScheme schemeDark = themeDark().colorScheme;
|
||||
if (themeMode() == ThemeMode.system) {
|
||||
if (MediaQuery.of(context).platformBrightness == Brightness.light) {
|
||||
return schemeLight;
|
||||
} else {
|
||||
return schemeDark;
|
||||
}
|
||||
} else {
|
||||
if (themeMode() == ThemeMode.light) {
|
||||
return schemeLight;
|
||||
} else {
|
||||
return schemeDark;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (delay != null) {
|
||||
await Future.delayed(delay);
|
||||
}
|
||||
color ??= getColorScheme().surface;
|
||||
// ignore: use_build_context_synchronously
|
||||
color ??= themeCurrent(context).colorScheme.surface;
|
||||
SystemChrome.setSystemUIOverlayStyle(SystemUiOverlayStyle(
|
||||
statusBarIconBrightness:
|
||||
(((statusBarColor != null) ? statusBarColor : color)!
|
||||
|
@ -42,8 +26,10 @@ void resetSystemNavigation(BuildContext context,
|
|||
? Brightness.dark
|
||||
: Brightness.light,
|
||||
statusBarColor:
|
||||
(((statusBarColor != null) ? statusBarColor : color)!.value !=
|
||||
getColorScheme().surface.value)
|
||||
((((statusBarColor != null) ? statusBarColor : color)!.value !=
|
||||
// ignore: use_build_context_synchronously
|
||||
themeCurrent(context).colorScheme.surface.value) ||
|
||||
kIsWeb)
|
||||
? (statusBarColor != null)
|
||||
? statusBarColor
|
||||
: color
|
||||
|
@ -56,7 +42,7 @@ void resetSystemNavigation(BuildContext context,
|
|||
|
||||
ThemeData themeModifier(ThemeData theme) {
|
||||
return theme.copyWith(
|
||||
// https://docs.flutter.dev/platform-integration/android/predictive-back#set-up-your-app
|
||||
// https://docs.flutter.dev/platform-integration/android/predictive-back#set-up-your-app
|
||||
pageTransitionsTheme: const PageTransitionsTheme(
|
||||
builders: <TargetPlatform, PageTransitionsBuilder>{
|
||||
TargetPlatform.android: PredictiveBackPageTransitionsBuilder(),
|
||||
|
@ -64,6 +50,22 @@ ThemeData themeModifier(ThemeData theme) {
|
|||
));
|
||||
}
|
||||
|
||||
ThemeData themeCurrent(BuildContext context) {
|
||||
if (themeMode() == ThemeMode.system) {
|
||||
if (MediaQuery.of(context).platformBrightness == Brightness.light) {
|
||||
return themeLight();
|
||||
} else {
|
||||
return themeDark();
|
||||
}
|
||||
} else {
|
||||
if (themeMode() == ThemeMode.light) {
|
||||
return themeLight();
|
||||
} else {
|
||||
return themeDark();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ThemeData themeLight() {
|
||||
if (!(prefs?.getBool("useDeviceTheme") ?? false) ||
|
||||
colorSchemeLight == null) {
|
||||
|
|
16
pubspec.lock
16
pubspec.lock
|
@ -509,6 +509,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.19.0"
|
||||
js:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: js
|
||||
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.7"
|
||||
json_annotation:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
@ -733,6 +741,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
pwa_install:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: pwa_install
|
||||
sha256: "79a6c38e67db12da98489258ead7e025ce4860c9010cde226d0b53ad6d403fd9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.0.5"
|
||||
scroll_to_index:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
|
@ -42,6 +42,7 @@ dependencies:
|
|||
dynamic_color: ^1.7.0
|
||||
volume_controller: ^2.0.7
|
||||
universal_html: ^2.2.4
|
||||
pwa_install: ^0.0.5
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"de": [
|
||||
"optionInstallPwa",
|
||||
"deleteChat",
|
||||
"renameChat",
|
||||
"tooltipReset",
|
||||
|
@ -7,6 +8,10 @@
|
|||
"modelDialogAddPromptDescription",
|
||||
"modelDialogAddPromptAlreadyExists",
|
||||
"modelDialogAddPromptInvalid",
|
||||
"modelDialogAddAllowanceTitle",
|
||||
"modelDialogAddAllowanceDescription",
|
||||
"modelDialogAddAllowanceAllow",
|
||||
"modelDialogAddAllowanceDeny",
|
||||
"modelDialogAddAssuranceTitle",
|
||||
"modelDialogAddAssuranceDescription",
|
||||
"modelDialogAddAssuranceAdd",
|
||||
|
@ -41,6 +46,7 @@
|
|||
],
|
||||
|
||||
"it": [
|
||||
"optionInstallPwa",
|
||||
"deleteChat",
|
||||
"renameChat",
|
||||
"tooltipReset",
|
||||
|
@ -48,6 +54,10 @@
|
|||
"modelDialogAddPromptDescription",
|
||||
"modelDialogAddPromptAlreadyExists",
|
||||
"modelDialogAddPromptInvalid",
|
||||
"modelDialogAddAllowanceTitle",
|
||||
"modelDialogAddAllowanceDescription",
|
||||
"modelDialogAddAllowanceAllow",
|
||||
"modelDialogAddAllowanceDeny",
|
||||
"modelDialogAddAssuranceTitle",
|
||||
"modelDialogAddAssuranceDescription",
|
||||
"modelDialogAddAssuranceAdd",
|
||||
|
@ -82,6 +92,7 @@
|
|||
],
|
||||
|
||||
"tr": [
|
||||
"optionInstallPwa",
|
||||
"deleteChat",
|
||||
"renameChat",
|
||||
"tooltipReset",
|
||||
|
@ -89,6 +100,10 @@
|
|||
"modelDialogAddPromptDescription",
|
||||
"modelDialogAddPromptAlreadyExists",
|
||||
"modelDialogAddPromptInvalid",
|
||||
"modelDialogAddAllowanceTitle",
|
||||
"modelDialogAddAllowanceDescription",
|
||||
"modelDialogAddAllowanceAllow",
|
||||
"modelDialogAddAllowanceDeny",
|
||||
"modelDialogAddAssuranceTitle",
|
||||
"modelDialogAddAssuranceDescription",
|
||||
"modelDialogAddAssuranceAdd",
|
||||
|
@ -123,6 +138,7 @@
|
|||
],
|
||||
|
||||
"zh": [
|
||||
"optionInstallPwa",
|
||||
"deleteChat",
|
||||
"renameChat",
|
||||
"tooltipReset",
|
||||
|
@ -130,6 +146,10 @@
|
|||
"modelDialogAddPromptDescription",
|
||||
"modelDialogAddPromptAlreadyExists",
|
||||
"modelDialogAddPromptInvalid",
|
||||
"modelDialogAddAllowanceTitle",
|
||||
"modelDialogAddAllowanceDescription",
|
||||
"modelDialogAddAllowanceAllow",
|
||||
"modelDialogAddAllowanceDeny",
|
||||
"modelDialogAddAssuranceTitle",
|
||||
"modelDialogAddAssuranceDescription",
|
||||
"modelDialogAddAssuranceAdd",
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<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="viewport" content="width=device-width, initial-scale=1.0">
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||
|
@ -91,6 +92,19 @@
|
|||
}
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
let deferredPrompt;
|
||||
window.addEventListener("beforeinstallprompt", (e) => { deferredPrompt = e; });
|
||||
function promptInstall() { deferredPrompt.prompt(); }
|
||||
window.addEventListener("appinstalled", () => { deferredPrompt = null; appInstalled(); });
|
||||
function getLaunchMode() {
|
||||
const isStandalone = window.matchMedia("(display-mode: standalone)").matches;
|
||||
if (deferredPrompt) hasPrompt();
|
||||
if (document.referrer.startsWith("android-app://")) { appLaunchedAsTWA(); }
|
||||
else if (navigator.standalone || isStandalone) { appLaunchedAsPWA(); }
|
||||
else { window.appLaunchedInBrowser(); }
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
Loading…
Reference in New Issue