Added (very!) experimental web support

This commit is contained in:
JHubi1 2024-08-20 00:15:51 +02:00
parent 4667f7a69b
commit 6a5a123026
No known key found for this signature in database
GPG Key ID: F538DC3FC5B07498
15 changed files with 306 additions and 101 deletions

View File

@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.
version:
revision: "a14f74ff3a1cbd521163c5f03d68113d50af93d3"
revision: "b0850beeb25f6d5b10426284f506557f66181b36"
channel: "stable"
project_type: app
@ -13,20 +13,17 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
- platform: android
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
- platform: linux
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
- platform: web
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
- platform: windows
create_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
base_revision: a14f74ff3a1cbd521163c5f03d68113d50af93d3
create_revision: b0850beeb25f6d5b10426284f506557f66181b36
base_revision: b0850beeb25f6d5b10426284f506557f66181b36
# User provided section

View File

@ -1060,7 +1060,35 @@ class _MainAppState extends State<MainApp> {
child: SizedBox(
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, [
const SizedBox(width: 4),
allowMultipleChats

View File

@ -520,7 +520,7 @@ class _ScreenSettingsState extends State<ScreenSettings> {
context: context,
description:
"\n${AppLocalizations.of(context)!.settingsDescriptionInterface}"),
(!desktopFeature())
(!desktopFeature(web: true))
? button(
AppLocalizations.of(context)!
.settingsTitleVoice,

View File

@ -1,7 +1,10 @@
import 'dart:io';
import 'dart:convert';
// ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html;
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import '../main.dart';
import '../worker/haptic.dart';
@ -42,18 +45,37 @@ class _ScreenSettingsExportState extends State<ScreenSettingsExport> {
button(AppLocalizations.of(context)!.settingsExportChats,
Icons.upload_rounded, () async {
selectionHaptic();
var path = await FilePicker.platform.saveFile(
type: FileType.custom,
allowedExtensions: ["json"],
fileName:
"ollama-export-${DateFormat('yyyy-MM-dd-H-m-s').format(DateTime.now())}.json",
bytes: utf8.encode(
jsonEncode(prefs!.getStringList("chats") ?? [])));
selectionHaptic();
if (path == null) return;
if (desktopFeature()) {
File(path).writeAsString(
jsonEncode(prefs!.getStringList("chats") ?? []));
var name =
"ollama-export-${DateFormat('yyyy-MM-dd-H-m-s').format(DateTime.now())}.json";
var content =
jsonEncode(prefs!.getStringList("chats") ?? []);
if (kIsWeb) {
final bytes = utf8.encode(content);
final blob = html.Blob([bytes]);
final url = html.Url.createObjectUrlFromBlob(blob);
final anchor = html.document.createElement("a")
as html.AnchorElement
..href = url
..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
@ -95,10 +117,18 @@ class _ScreenSettingsExportState extends State<ScreenSettingsExport> {
return;
}
File file = File(
result.files.single.path!);
var content =
await file.readAsString();
String content;
try {
File file = File(
result.files.single.path!);
content =
await file.readAsString();
} catch (_) {
content = utf8.decode(result
.files
.single
.bytes as List<int>);
}
List<dynamic> tmpHistory =
jsonDecode(content);
List<String> history = [];

View File

@ -1,6 +1,7 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import '../main.dart';
import '../worker/haptic.dart';
@ -342,69 +343,74 @@ class _ScreenSettingsInterfaceState extends State<ScreenSettingsInterface> {
});
}),
const SizedBox(height: 8),
SegmentedButton(
segments: [
ButtonSegment(
value: "device",
label: Text(AppLocalizations.of(context)!
.settingsThemeDevice),
icon: const Icon(Icons.devices_rounded)),
ButtonSegment(
value: "ollama",
label: Text(AppLocalizations.of(context)!
.settingsThemeOllama),
icon: const ImageIcon(
AssetImage("assets/logo512.png")))
],
selected: {
(prefs?.getBool("useDeviceTheme") ?? false)
? "device"
: "ollama"
},
onSelectionChanged: (p0) {
selectionHaptic();
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setLocalState) {
return AlertDialog(
title: Text(AppLocalizations.of(context)!
.settingsThemeRestartTitle),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(AppLocalizations.of(context)!
.settingsThemeRestartDescription),
]),
actions: [
TextButton(
onPressed: () {
selectionHaptic();
Navigator.of(context).pop();
},
child: Text(AppLocalizations.of(
context)!
.settingsThemeRestartCancel)),
TextButton(
onPressed: () async {
selectionHaptic();
await prefs!.setBool(
"useDeviceTheme",
p0.elementAt(0) == "device");
if (desktopFeature()) {
exit(0);
} else {
Restart.restartApp();
}
},
child: Text(AppLocalizations.of(
context)!
.settingsThemeRestartRestart))
]);
});
});
}),
!kIsWeb
? SegmentedButton(
segments: [
ButtonSegment(
value: "device",
label: Text(AppLocalizations.of(context)!
.settingsThemeDevice),
icon: const Icon(Icons.devices_rounded)),
ButtonSegment(
value: "ollama",
label: Text(AppLocalizations.of(context)!
.settingsThemeOllama),
icon: const ImageIcon(
AssetImage("assets/logo512.png")))
],
selected: {
(prefs?.getBool("useDeviceTheme") ?? false)
? "device"
: "ollama"
},
onSelectionChanged: (p0) {
selectionHaptic();
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setLocalState) {
return AlertDialog(
title: Text(
AppLocalizations.of(context)!
.settingsThemeRestartTitle),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(AppLocalizations.of(
context)!
.settingsThemeRestartDescription),
]),
actions: [
TextButton(
onPressed: () {
selectionHaptic();
Navigator.of(context).pop();
},
child: Text(AppLocalizations.of(
context)!
.settingsThemeRestartCancel)),
TextButton(
onPressed: () async {
selectionHaptic();
await prefs!.setBool(
"useDeviceTheme",
p0.elementAt(0) ==
"device");
if (desktopFeature()) {
exit(0);
} else {
Restart.restartApp();
}
},
child: Text(AppLocalizations.of(
context)!
.settingsThemeRestartRestart))
]);
});
});
})
: const SizedBox.shrink(),
titleDivider(),
toggle(context, "Fix to code block not scrollable",
(prefs!.getBool("fixCodeblockScroll") ?? false),

View File

@ -6,26 +6,30 @@ import 'package:flutter/foundation.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart';
bool desktopFeature({bool web = false}) {
return (Platform.isWindows ||
Platform.isLinux ||
Platform.isMacOS ||
(web ? kIsWeb : false));
try {
return (Platform.isWindows ||
Platform.isLinux ||
Platform.isMacOS ||
(web ? kIsWeb : false));
} catch (_) {
return web ? kIsWeb : false;
}
}
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;
return (desktopFeature(web: web) || value >= valueCap);
}
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;
return (desktopFeature(web: web) && value >= valueCap);
}
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;
return (value >= valueCap);
}

View File

@ -37,7 +37,7 @@ Future<bool> updatesSupported(Function setState,
"com.machiav3lli.fdroid",
"nya.kitsunyan.foxydroid"
];
if (!desktopFeature()) {
if (!desktopFeature(web: true)) {
if ((await InstallReferrer.referrer !=
InstallationAppReferrer.androidManually) ||
(installerApps

View File

@ -16,6 +16,8 @@
],
"it": [
"deleteChat",
"renameChat",
"settingsDescriptionBehavior",
"settingsDescriptionInterface",
"settingsDescriptionVoice",
@ -30,6 +32,8 @@
],
"tr": [
"deleteChat",
"renameChat",
"settingsDescriptionBehavior",
"settingsDescriptionInterface",
"settingsDescriptionVoice",

BIN
web/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
web/icons/Icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

BIN
web/icons/Icon-512.png Normal file

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

101
web/index.html Normal file
View File

@ -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>

35
web/manifest.json Normal file
View File

@ -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"
}
]
}