mirror of
https://github.com/LinwoodDev/Butterfly.git
synced 2026-03-13 09:20:32 +08:00
349 lines
11 KiB
Dart
349 lines
11 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:butterfly/cubits/settings.dart';
|
|
import 'package:butterfly/models/defaults.dart';
|
|
import 'package:butterfly_api/butterfly_api.dart';
|
|
import 'package:butterfly_api/butterfly_text.dart' as text;
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:idb_shim/idb.dart';
|
|
import 'package:lw_file_system/lw_file_system.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
Uint8List encodeNoteData(NoteData data) =>
|
|
Uint8List.fromList(data.exportAsBytes());
|
|
NoteData decodeNoteData(Uint8List data) => NoteData.fromData(data);
|
|
|
|
Uint8List encodeNoteFile(NoteFile file) => file.data;
|
|
NoteFile decodeNoteFile(Uint8List data) => NoteFile(data);
|
|
|
|
const butterflySubDirectory = '/Linwood/Butterfly';
|
|
|
|
String? overrideButterflyDirectory;
|
|
|
|
Future<String> getButterflyDirectory({bool usePrefs = true}) async {
|
|
var directory = overrideButterflyDirectory;
|
|
if (directory != null) return directory;
|
|
String? path;
|
|
if (usePrefs) {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
if (prefs.containsKey('document_path')) {
|
|
path = prefs.getString('document_path');
|
|
}
|
|
}
|
|
if (path == '') {
|
|
path = null;
|
|
}
|
|
if (path != null) {
|
|
return path;
|
|
}
|
|
if (Platform.isAndroid) {
|
|
if (path != null) {
|
|
path = Uri.tryParse(path)?.toFilePath();
|
|
}
|
|
path ??= (await getExternalStorageDirectory())?.path;
|
|
}
|
|
path ??= (await getApplicationDocumentsDirectory()).path;
|
|
path = path.replaceAll('\\', '/');
|
|
path += butterflySubDirectory;
|
|
return path;
|
|
}
|
|
|
|
Future<String> Function(ExternalStorage? storage) _getRemoteDirectory(
|
|
String subDirectory,
|
|
) => (storage) async {
|
|
var path = await getButterflyDirectory();
|
|
if (storage != null) {
|
|
final bytes = utf8.encode(storage.identifier);
|
|
final directory = base64.encode(bytes);
|
|
path += '/Remotes/$directory';
|
|
}
|
|
path += '/$subDirectory';
|
|
return path;
|
|
};
|
|
|
|
Future<String> getButterflyDocumentsDirectory([ExternalStorage? storage]) =>
|
|
_getRemoteDirectory('Documents')(storage);
|
|
typedef DocumentFileSystem = TypedDirectoryFileSystem<NoteFile>;
|
|
typedef TemplateFileSystem = TypedKeyFileSystem<NoteData>;
|
|
typedef PackFileSystem = TypedKeyFileSystem<NoteData>;
|
|
|
|
final PasswordStorage passwordStorage = SecureStoragePasswordStorage();
|
|
|
|
class ButterflyFileSystem {
|
|
// ignore: unused_field
|
|
final BuildContext _context;
|
|
final SettingsCubit settingsCubit;
|
|
final FileSystemConfig _documentConfig, _templateConfig, _packConfig;
|
|
|
|
final _documentCache = <String, DocumentFileSystem>{};
|
|
final _templateCache = <String, TemplateFileSystem>{};
|
|
final _packCache = <String, PackFileSystem>{};
|
|
|
|
ButterflyFileSystem(this._context, this.settingsCubit)
|
|
: _documentConfig = FileSystemConfig(
|
|
passwordStorage: passwordStorage,
|
|
storeName: 'documents',
|
|
variant: 'documents',
|
|
getDirectory: getButterflyDocumentsDirectory,
|
|
database: _database,
|
|
databaseVersion: _databaseVersion,
|
|
onDatabaseUpgrade: _upgradeDatabase,
|
|
),
|
|
_templateConfig = FileSystemConfig(
|
|
passwordStorage: passwordStorage,
|
|
storeName: 'templates',
|
|
variant: 'templates',
|
|
getDirectory: _getRemoteDirectory('Templates'),
|
|
database: _database,
|
|
databaseVersion: _databaseVersion,
|
|
onDatabaseUpgrade: _upgradeDatabase,
|
|
defaultStorageKey: 'defaultTemplate',
|
|
),
|
|
_packConfig = FileSystemConfig(
|
|
passwordStorage: passwordStorage,
|
|
storeName: 'packs',
|
|
variant: 'packs',
|
|
getDirectory: _getRemoteDirectory('Packs'),
|
|
database: _database,
|
|
databaseVersion: _databaseVersion,
|
|
onDatabaseUpgrade: _upgradeDatabase,
|
|
defaultStorageKey: 'defaultPack',
|
|
) {
|
|
_listenSettings();
|
|
}
|
|
|
|
void _listenSettings() {
|
|
var previousState = settingsCubit.state;
|
|
settingsCubit.stream.listen((state) {
|
|
if (state == previousState) return;
|
|
final previousRemotes = previousState.connections.toSet();
|
|
final currentRemotes = state.connections.toSet();
|
|
final removedRemotes = previousRemotes.difference(currentRemotes);
|
|
for (final remote in removedRemotes) {
|
|
removeCachedFileSystem(remote);
|
|
}
|
|
if (previousState.documentPath != state.documentPath) {
|
|
removeCachedFileSystem(null);
|
|
}
|
|
previousState = state;
|
|
});
|
|
}
|
|
|
|
factory ButterflyFileSystem.build(BuildContext context) =>
|
|
ButterflyFileSystem(context, context.read<SettingsCubit>());
|
|
|
|
static const _database = 'butterfly.db';
|
|
static const _databaseVersion = 4;
|
|
|
|
static Future<void> _upgradeDatabase(VersionChangeEvent event) async {
|
|
final db = event.database;
|
|
if (event.oldVersion < 1) {
|
|
db.createObjectStore('documents');
|
|
}
|
|
if (event.oldVersion < 2) {
|
|
var txn = event.transaction;
|
|
var store = txn.objectStore('documents');
|
|
var cursor = store.openCursor();
|
|
await Future.wait(
|
|
await cursor.map<Future<dynamic>>((cursor) async {
|
|
// Add type to each document
|
|
var doc = cursor.value as Map<dynamic, dynamic>;
|
|
doc['type'] = 'document';
|
|
await store.put(doc);
|
|
}).toList(),
|
|
);
|
|
}
|
|
if (event.oldVersion < 3) {
|
|
db.createObjectStore('templates');
|
|
}
|
|
if (event.oldVersion < 4) {
|
|
db.createObjectStore('packs');
|
|
db.createObjectStore('documents-data');
|
|
var txn = event.transaction;
|
|
var store = txn.objectStore('templates');
|
|
var cursor = store.openCursor();
|
|
await Future.wait(
|
|
await cursor.map<Future<dynamic>>((cursor) async {
|
|
final value = cursor.value;
|
|
if (value is! Map) return value;
|
|
var type = value['type'];
|
|
if (type != 'document') return value;
|
|
final data = utf8.encode(jsonEncode(value));
|
|
return {'type': 'document', 'data': data};
|
|
}).toList(),
|
|
);
|
|
var documentStore = txn.objectStore('documents');
|
|
await Future.wait(
|
|
await documentStore.openCursor().map<Future<dynamic>>((cursor) async {
|
|
var doc = cursor.value as Map<dynamic, dynamic>;
|
|
var data = doc['data'];
|
|
if (data is! String) return;
|
|
var path = cursor.key as String;
|
|
var dataStore = txn.objectStore('documents-data');
|
|
await dataStore.put(data, path);
|
|
doc['data'] = null;
|
|
await documentStore.put(doc, path);
|
|
}).toList(),
|
|
);
|
|
await txn.completed;
|
|
}
|
|
}
|
|
|
|
Future<void> _createDefaultPacks(PackFileSystem fs) async {
|
|
final pack = await DocumentDefaults.getCorePack();
|
|
await fs.createFile('${pack.name}.bfly', pack);
|
|
}
|
|
|
|
String _cacheKey(ExternalStorage? storage) => storage?.identifier ?? 'local';
|
|
|
|
TypedDirectoryFileSystem<NoteFile> buildDocumentSystem([
|
|
ExternalStorage? storage,
|
|
bool forceRecreate = false,
|
|
]) {
|
|
final key = _cacheKey(storage);
|
|
if (!forceRecreate) {
|
|
final cached = _documentCache[key];
|
|
if (cached != null) return cached;
|
|
}
|
|
final system = TypedDirectoryFileSystem.build(
|
|
_documentConfig,
|
|
onEncode: encodeNoteFile,
|
|
onDecode: decodeNoteFile,
|
|
storage: storage,
|
|
useIsolates: true,
|
|
);
|
|
_documentCache[key] = system;
|
|
return system;
|
|
}
|
|
|
|
ExternalStorage? _cacheAllStorage(ExternalStorage? storage, String variant) {
|
|
if (storage is! RemoteStorage) {
|
|
return storage;
|
|
}
|
|
return storage.copyWith.pinnedPaths.put(variant, ['/']);
|
|
}
|
|
|
|
TypedKeyFileSystem<NoteData> buildTemplateSystem([
|
|
ExternalStorage? storage,
|
|
bool forceRecreate = false,
|
|
]) {
|
|
final key = _cacheKey(storage);
|
|
if (!forceRecreate) {
|
|
final cached = _templateCache[key];
|
|
if (cached != null) return cached;
|
|
}
|
|
final system = TypedKeyFileSystem.build(
|
|
_templateConfig,
|
|
onEncode: encodeNoteData,
|
|
onDecode: decodeNoteData,
|
|
storage: _cacheAllStorage(storage, _templateConfig.variant),
|
|
);
|
|
_templateCache[key] = system;
|
|
return system;
|
|
}
|
|
|
|
TypedKeyFileSystem<NoteData> buildPackSystem([
|
|
ExternalStorage? storage,
|
|
bool forceRecreate = false,
|
|
]) {
|
|
final key = _cacheKey(storage);
|
|
if (!forceRecreate) {
|
|
final cached = _packCache[key];
|
|
if (cached != null) return cached;
|
|
}
|
|
final system = TypedKeyFileSystem.build(
|
|
_packConfig,
|
|
onEncode: encodeNoteData,
|
|
onDecode: decodeNoteData,
|
|
storage: _cacheAllStorage(storage, _packConfig.variant),
|
|
createDefault: _createDefaultPacks,
|
|
);
|
|
_packCache[key] = system;
|
|
return system;
|
|
}
|
|
|
|
DocumentFileSystem buildDefaultDocumentSystem({bool forceRecreate = false}) =>
|
|
buildDocumentSystem(
|
|
settingsCubit.state.getDefaultRemote(),
|
|
forceRecreate,
|
|
);
|
|
|
|
Map<String, DocumentFileSystem> buildAllDocumentSystems({
|
|
bool includeLocal = true,
|
|
bool forceRecreate = false,
|
|
}) {
|
|
final map = <String, DocumentFileSystem>{};
|
|
if (includeLocal) {
|
|
map[''] = buildDocumentSystem(null, forceRecreate);
|
|
}
|
|
for (final remote in settingsCubit.state.connections) {
|
|
map[remote.identifier] = buildDocumentSystem(remote, forceRecreate);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
TemplateFileSystem buildDefaultTemplateSystem({bool forceRecreate = false}) =>
|
|
buildTemplateSystem(
|
|
settingsCubit.state.getDefaultRemote(),
|
|
forceRecreate,
|
|
);
|
|
|
|
PackFileSystem buildDefaultPackSystem({bool forceRecreate = false}) =>
|
|
buildPackSystem(settingsCubit.state.getDefaultRemote(), forceRecreate);
|
|
|
|
Future<PackItem<T>?> findPack<T extends PackAsset>(
|
|
NamedItem<T>? Function(NoteData) test, [
|
|
ExternalStorage? storage,
|
|
]) async {
|
|
final system = buildPackSystem(storage);
|
|
await system.initialize();
|
|
final files = await system.getFiles();
|
|
for (final file in files) {
|
|
final pack = file.data!;
|
|
final palette = test(pack);
|
|
if (palette == null) continue;
|
|
final name = file.pathWithoutLeadingSlash;
|
|
return palette.toPack(pack, name);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Future<PackItem<text.TextStyleSheet>?> findDefaultStyleSheet([
|
|
ExternalStorage? storage,
|
|
]) => findPack((pack) => pack.getNamedStyles().firstOrNull, storage);
|
|
Future<PackItem<ColorPalette>?> findDefaultPalette([
|
|
ExternalStorage? storage,
|
|
]) => findPack((pack) => pack.getNamedPalettes().firstOrNull, storage);
|
|
|
|
Future<void> updatePack(PackAssetLocation location, NoteData newPack) =>
|
|
buildPackSystem(
|
|
settingsCubit.getRemote(location.namespace),
|
|
).updateFile(location.key, newPack);
|
|
|
|
void removeCachedDocumentSystem(ExternalStorage? storage) {
|
|
final key = _cacheKey(storage);
|
|
_documentCache.remove(key);
|
|
}
|
|
|
|
void removeCachedTemplateSystem(ExternalStorage? storage) {
|
|
final key = _cacheKey(storage);
|
|
_templateCache.remove(key);
|
|
}
|
|
|
|
void removeCachedPackSystem(ExternalStorage? storage) {
|
|
final key = _cacheKey(storage);
|
|
_packCache.remove(key);
|
|
}
|
|
|
|
void removeCachedFileSystem(ExternalStorage? storage) {
|
|
removeCachedDocumentSystem(storage);
|
|
removeCachedTemplateSystem(storage);
|
|
removeCachedPackSystem(storage);
|
|
}
|
|
}
|