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. # 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

View File

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

View File

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

View File

@ -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 = [];

View File

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

View File

@ -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);
} }

View File

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

View File

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

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