v2.0.5-rc (#41)
BlockStorage/repertory/pipeline/head This commit looks good

Reviewed-on: #41
This commit is contained in:
sgraves committed 2025-03-26 07:02:38 -05:00
1 parent 24418ba03d
commit 62555e6125
135 files changed
+6792 -796

No files matched your search

+8
View File
@@ -0,0 +1,8 @@
autofocus
canvaskit
cupertino
cupertinoicons
fromargb
onetwothree
renterd
rocksdb
+47
View File
@@ -0,0 +1,47 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
.flutter-companion
pubspec.lock
+30
View File
@@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "35c388afb57ef061d06a39b537336c87e0e3d1b1"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
- platform: web
create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'
+16
View File
@@ -0,0 +1,16 @@
# repertory
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
+28
View File
@@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
+24
View File
@@ -0,0 +1,24 @@
import 'package:flutter/material.dart' show GlobalKey, NavigatorState;
import 'package:sodium_libs/sodium_libs.dart';
const addMountTitle = 'Add New Mount';
const appLogonTitle = 'Repertory Portal Login';
const appSettingsTitle = 'Portal Settings';
const appTitle = 'Repertory Management Portal';
const logonWidth = 300.0;
const databaseTypeList = ['rocksdb', 'sqlite'];
const downloadTypeList = ['default', 'direct', 'ring_buffer'];
const eventLevelList = ['critical', 'error', 'warn', 'info', 'debug', 'trace'];
const padding = 15.0;
const protocolTypeList = ['http', 'https'];
const providerTypeList = ['Encrypt', 'Remote', 'S3', 'Sia'];
const ringBufferSizeList = ['128', '256', '512', '1024', '2048'];
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
Sodium? _sodium;
void setSodium(Sodium sodium) {
_sodium = sodium;
}
Sodium get sodium => _sodium!;
+402
View File
@@ -0,0 +1,402 @@
import 'package:convert/convert.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/models/auth.dart';
import 'package:sodium_libs/sodium_libs.dart' show SecureKey, StringX;
typedef Validator = bool Function(String);
class NullPasswordException implements Exception {
String error() => 'password cannot be null';
}
class AuthenticationFailedException implements Exception {
String error() => 'failed to authenticate user';
}
// ignore: prefer_function_declarations_over_variables
final Validator noRestrictedChars = (value) {
return [
'!',
'"',
'\$',
'&',
"'",
'(',
')',
'*',
';',
'<',
'>',
'?',
'[',
']',
'`',
'{',
'}',
'|',
].firstWhereOrNull((char) => value.contains(char)) ==
null;
};
// ignore: prefer_function_declarations_over_variables
final Validator notEmptyValidator = (value) => value.isNotEmpty;
// ignore: prefer_function_declarations_over_variables
final Validator portIsValid = (value) {
int? intValue = int.tryParse(value);
if (intValue == null) {
return false;
}
return (intValue > 0 && intValue < 65536);
};
// ignore: prefer_function_declarations_over_variables
final Validator trimNotEmptyValidator = (value) => value.trim().isNotEmpty;
createUriValidator<Validator>({host, port}) {
return (value) =>
Uri.tryParse('http://${host ?? value}:${port ?? value}/') != null;
}
createHostNameOrIpValidators() => <Validator>[
trimNotEmptyValidator,
createUriValidator(port: 9000),
];
Map<String, dynamic> createDefaultSettings(String mountType) {
switch (mountType) {
case 'Encrypt':
return {
'EncryptConfig': {'EncryptionToken': '', 'Path': ''},
};
case 'Remote':
return {
'RemoteConfig': {
'ApiPort': 20000,
'EncryptionToken': '',
'HostNameOrIp': '',
},
};
case 'S3':
return {
'S3Config': {
'AccessKey': '',
'Bucket': '',
'Region': 'any',
'SecretKey': '',
'URL': '',
'UsePathStyle': false,
'UseRegionInURL': false,
},
};
case 'Sia':
return {
'HostConfig': {
'ApiPassword': '',
'ApiPort': 9980,
'HostNameOrIp': 'localhost',
},
'SiaConfig': {'Bucket': 'default'},
};
}
return {};
}
void displayAuthError(Auth auth) {
if (!auth.authenticated || constants.navigatorKey.currentContext == null) {
return;
}
displayErrorMessage(
constants.navigatorKey.currentContext!,
"Authentication failed",
clear: true,
);
}
void displayErrorMessage(context, String text, {bool clear = false}) {
if (!context.mounted) {
return;
}
final messenger = ScaffoldMessenger.of(context);
if (clear) {
messenger.removeCurrentSnackBar();
}
messenger.showSnackBar(
SnackBar(content: Text(text, textAlign: TextAlign.center)),
);
}
String formatMountName(String type, String name) {
if (type == 'remote') {
return name.replaceAll('_', ':');
}
return name;
}
String getBaseUri() {
if (kDebugMode || !kIsWeb) {
return 'http://127.0.0.1:30000';
}
return Uri.base.origin;
}
String? getSettingDescription(String settingPath) {
switch (settingPath) {
case 'ApiPassword':
return "HTTP basic authentication password";
case 'ApiUser':
return "HTTP basic authentication user";
case 'HostConfig.ApiPassword':
return "RENTERD_API_PASSWORD";
default:
return null;
}
}
List<Validator> getSettingValidators(String settingPath) {
switch (settingPath) {
case 'ApiPassword':
return [notEmptyValidator];
case 'DatabaseType':
return [(value) => constants.databaseTypeList.contains(value)];
case 'PreferredDownloadType':
return [(value) => constants.downloadTypeList.contains(value)];
case 'EventLevel':
return [(value) => constants.eventLevelList.contains(value)];
case 'EncryptConfig.EncryptionToken':
return [notEmptyValidator];
case 'EncryptConfig.Path':
return [trimNotEmptyValidator, noRestrictedChars];
case 'HostConfig.ApiPassword':
return [notEmptyValidator];
case 'HostConfig.ApiPort':
return [portIsValid];
case 'HostConfig.HostNameOrIp':
return createHostNameOrIpValidators();
case 'HostConfig.Protocol':
return [(value) => constants.protocolTypeList.contains(value)];
case 'Path':
return [trimNotEmptyValidator];
case 'RemoteConfig.ApiPort':
return [notEmptyValidator, portIsValid];
case 'RemoteConfig.EncryptionToken':
return [notEmptyValidator];
case 'RemoteConfig.HostNameOrIp':
return createHostNameOrIpValidators();
case 'RemoteMount.ApiPort':
return [notEmptyValidator, portIsValid];
case 'RemoteMount.EncryptionToken':
return [notEmptyValidator];
case 'RemoteMount.HostNameOrIp':
return createHostNameOrIpValidators();
case 'RingBufferFileSize':
return [(value) => constants.ringBufferSizeList.contains(value)];
case 'S3Config.AccessKey':
return [trimNotEmptyValidator];
case 'S3Config.Bucket':
return [trimNotEmptyValidator];
case 'S3Config.SecretKey':
return [trimNotEmptyValidator];
case 'S3Config.URL':
return [trimNotEmptyValidator, (value) => Uri.tryParse(value) != null];
case 'SiaConfig.Bucket':
return [trimNotEmptyValidator];
}
return [];
}
String initialCaps(String txt) {
if (txt.isEmpty) {
return txt;
}
if (txt.length == 1) {
return txt[0].toUpperCase();
}
return txt[0].toUpperCase() + txt.substring(1).toLowerCase();
}
bool validateSettings(
Map<String, dynamic> settings,
List<String> failed, {
String? rootKey,
}) {
settings.forEach((key, value) {
final settingKey = rootKey == null ? key : '$rootKey.$key';
if (value is Map<String, dynamic>) {
validateSettings(value, failed, rootKey: settingKey);
return;
}
for (var validator in getSettingValidators(settingKey)) {
if (validator(value.toString())) {
continue;
}
failed.add(settingKey);
}
});
return failed.isEmpty;
}
Future<Map<String, dynamic>> convertAllToString(
Map<String, dynamic> settings,
SecureKey key,
) async {
Future<Map<String, dynamic>> convert(Map<String, dynamic> settings) async {
for (var entry in settings.entries) {
if (entry.value is Map<String, dynamic>) {
await convert(entry.value);
continue;
}
if (entry.key == 'ApiPassword' ||
entry.key == 'EncryptionToken' ||
entry.key == 'SecretKey') {
if (entry.value.isEmpty) {
continue;
}
settings[entry.key] = encryptValue(entry.value, key);
continue;
}
if (entry.value is String) {
continue;
}
settings[entry.key] = entry.value.toString();
}
return settings;
}
return convert(settings);
}
String encryptValue(String value, SecureKey key) {
if (value.isEmpty) {
return value;
}
final sodium = constants.sodium;
final crypto = sodium.crypto.aeadXChaCha20Poly1305IETF;
final nonce = sodium.secureRandom(crypto.nonceBytes).extractBytes();
final data = crypto.encrypt(
additionalData: Uint8List.fromList('repertory'.toCharArray()),
key: key,
message: Uint8List.fromList(value.toCharArray()),
nonce: nonce,
);
return hex.encode(nonce + data);
}
Map<String, dynamic> getChanged(
Map<String, dynamic> original,
Map<String, dynamic> updated,
) {
if (DeepCollectionEquality().equals(original, updated)) {
return {};
}
Map<String, dynamic> changed = {};
original.forEach((key, value) {
if (DeepCollectionEquality().equals(value, updated[key])) {
return;
}
if (value is Map<String, dynamic>) {
changed[key] = <String, dynamic>{};
value.forEach((subKey, subValue) {
if (DeepCollectionEquality().equals(subValue, updated[key][subKey])) {
return;
}
changed[key][subKey] = updated[key][subKey];
});
return;
}
changed[key] = updated[key];
});
return changed;
}
Future<String?> editMountLocation(
context,
List<String> available, {
bool allowEmpty = false,
String? location,
}) async {
String? currentLocation = location;
final controller = TextEditingController(text: currentLocation);
return await showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setState) {
return AlertDialog(
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () => Navigator.of(context).pop(null),
),
TextButton(
child: const Text('OK'),
onPressed: () {
final result = getSettingValidators('Path').firstWhereOrNull(
(validator) => !validator(currentLocation ?? ''),
);
if (result != null) {
return displayErrorMessage(
context,
"Mount location is not valid",
);
}
Navigator.of(context).pop(currentLocation);
},
),
],
content:
available.isEmpty
? TextField(
autofocus: true,
controller: controller,
onChanged:
(value) => setState(() => currentLocation = value),
)
: DropdownButton<String>(
hint: const Text("Select drive"),
value: currentLocation,
onChanged:
(value) => setState(() => currentLocation = value),
items:
available.map<DropdownMenuItem<String>>((item) {
return DropdownMenuItem<String>(
value: item,
child: Text(item),
);
}).toList(),
),
title: const Text('Mount Location', textAlign: TextAlign.center),
);
},
);
},
);
}
+126
View File
@@ -0,0 +1,126 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart';
import 'package:repertory/models/auth.dart';
import 'package:repertory/models/mount.dart';
import 'package:repertory/models/mount_list.dart';
import 'package:repertory/screens/add_mount_screen.dart';
import 'package:repertory/screens/auth_screen.dart';
import 'package:repertory/screens/edit_mount_screen.dart';
import 'package:repertory/screens/edit_settings_screen.dart';
import 'package:repertory/screens/home_screen.dart';
import 'package:sodium_libs/sodium_libs.dart' show SodiumInit;
void main() async {
try {
constants.setSodium(await SodiumInit.init());
} catch (e) {
debugPrint('$e');
}
final auth = Auth();
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => auth),
ChangeNotifierProvider(create: (_) => MountList(auth)),
],
child: const MyApp(),
),
);
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
@override
Widget build(context) {
final snackBarTheme = SnackBarThemeData(
width: MediaQuery.of(context).size.width * 0.50,
behavior: SnackBarBehavior.floating,
);
return MaterialApp(
navigatorKey: constants.navigatorKey,
themeMode: ThemeMode.dark,
darkTheme: ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: ColorScheme.fromSeed(
brightness: Brightness.dark,
onSurface: Colors.white70,
seedColor: Colors.deepOrange,
surface: Color.fromARGB(255, 32, 33, 36),
surfaceContainerLow: Color.fromARGB(255, 41, 42, 45),
),
snackBarTheme: snackBarTheme,
),
title: constants.appTitle,
initialRoute: '/auth',
routes: {
'/':
(context) =>
const AuthCheck(child: HomeScreen(title: constants.appTitle)),
'/add':
(context) => const AuthCheck(
child: AddMountScreen(title: constants.addMountTitle),
),
'/auth': (context) => const AuthScreen(title: constants.appTitle),
'/settings':
(context) => const AuthCheck(
child: EditSettingsScreen(title: constants.appSettingsTitle),
),
},
onGenerateRoute: (settings) {
if (settings.name != '/edit') {
return null;
}
final mount = settings.arguments as Mount;
return MaterialPageRoute(
builder: (context) {
return AuthCheck(
child: EditMountScreen(
mount: mount,
title:
'${mount.provider} [${formatMountName(mount.type, mount.name)}] Settings',
),
);
},
);
},
);
}
}
class AuthCheck extends StatelessWidget {
final Widget child;
const AuthCheck({super.key, required this.child});
@override
Widget build(BuildContext context) {
return Consumer<Auth>(
builder: (context, auth, __) {
if (!auth.authenticated) {
Future.delayed(Duration(milliseconds: 1), () {
if (constants.navigatorKey.currentContext == null) {
return;
}
Navigator.of(
constants.navigatorKey.currentContext!,
).pushNamedAndRemoveUntil('/auth', (Route<dynamic> route) => false);
});
return child;
}
return child;
},
);
}
}
+64
View File
@@ -0,0 +1,64 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart';
import 'package:repertory/models/mount_list.dart';
import 'package:sodium_libs/sodium_libs.dart';
class Auth with ChangeNotifier {
bool _authenticated = false;
SecureKey _key = SecureKey.random(constants.sodium, 32);
String _user = "";
MountList? mountList;
bool get authenticated => _authenticated;
SecureKey get key => _key;
Future<void> authenticate(String user, String password) async {
final sodium = constants.sodium;
final keyHash = sodium.crypto.genericHash(
outLen: sodium.crypto.aeadXChaCha20Poly1305IETF.keyBytes,
message: Uint8List.fromList(password.toCharArray()),
);
_authenticated = true;
_key = SecureKey.fromList(sodium, keyHash);
_user = user;
notifyListeners();
}
Future<String> createAuth() async {
try {
final response = await http.get(
Uri.parse(Uri.encodeFull('${getBaseUri()}/api/v1/nonce')),
);
if (response.statusCode != 200) {
logoff();
return "";
}
final nonce = jsonDecode(response.body)["nonce"];
return encryptValue('${_user}_$nonce', key);
} catch (e) {
debugPrint('$e');
}
return "";
}
void logoff() {
_authenticated = false;
_key = SecureKey.random(constants.sodium, 32);
_user = "";
notifyListeners();
mountList?.clear();
}
}
+285
View File
@@ -0,0 +1,285 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:repertory/helpers.dart';
import 'package:repertory/models/auth.dart';
import 'package:repertory/models/mount_list.dart';
import 'package:repertory/types/mount_config.dart';
class Mount with ChangeNotifier {
final Auth _auth;
final MountConfig mountConfig;
final MountList? _mountList;
bool _isMounting = false;
bool _isRefreshing = false;
Mount(this._auth, this.mountConfig, this._mountList, {isAdd = false}) {
if (isAdd) {
return;
}
refresh();
}
String? get bucket => mountConfig.bucket;
String get id => '${type}_$name';
bool? get mounted => mountConfig.mounted;
String get name => mountConfig.name;
String get path => mountConfig.path;
String get provider => mountConfig.provider;
String get type => mountConfig.type;
Future<void> _fetch() async {
try {
final auth = await _auth.createAuth();
final response = await http.get(
Uri.parse(
Uri.encodeFull(
'${getBaseUri()}/api/v1/mount?auth=$auth&name=$name&type=$type',
),
),
);
if (response.statusCode == 401) {
_auth.logoff();
return;
}
if (response.statusCode == 404) {
_mountList?.reset();
return;
}
if (response.statusCode != 200) {
return;
}
if (_isMounting) {
return;
}
mountConfig.updateSettings(jsonDecode(response.body));
notifyListeners();
} catch (e) {
debugPrint('$e');
}
}
Future<void> _fetchStatus() async {
try {
final auth = await _auth.createAuth();
final response = await http.get(
Uri.parse(
Uri.encodeFull(
'${getBaseUri()}/api/v1/mount_status?auth=$auth&name=$name&type=$type',
),
),
);
if (response.statusCode == 401) {
_auth.logoff();
return;
}
if (response.statusCode == 404) {
_mountList?.reset();
return;
}
if (response.statusCode != 200) {
return;
}
if (_isMounting) {
return;
}
mountConfig.updateStatus(jsonDecode(response.body));
notifyListeners();
} catch (e) {
debugPrint('$e');
}
}
Future<void> setMountLocation(String location) async {
try {
mountConfig.path = location;
final auth = await _auth.createAuth();
final response = await http.put(
Uri.parse(
Uri.encodeFull(
'${getBaseUri()}/api/v1/mount_location?auth=$auth&name=$name&type=$type&location=$location',
),
),
);
if (response.statusCode == 401) {
_auth.logoff();
return;
}
if (response.statusCode == 404) {
_mountList?.reset();
return;
}
return refresh();
} catch (e) {
debugPrint('$e');
}
}
Future<List<String>> getAvailableLocations() async {
try {
final auth = await _auth.createAuth();
final response = await http.get(
Uri.parse(
Uri.encodeFull('${getBaseUri()}/api/v1/locations?auth=$auth'),
),
);
if (response.statusCode == 401) {
_auth.logoff();
return <String>[];
}
if (response.statusCode != 200) {
return <String>[];
}
return (jsonDecode(response.body) as List).cast<String>();
} catch (e) {
debugPrint('$e');
}
return <String>[];
}
Future<String?> getMountLocation() async {
try {
final auth = await _auth.createAuth();
final response = await http.get(
Uri.parse(
Uri.encodeFull(
'${getBaseUri()}/api/v1/mount_location?auth=$auth&name=$name&type=$type',
),
),
);
if (response.statusCode == 401) {
_auth.logoff();
return null;
}
if (response.statusCode != 200) {
return null;
}
final location = jsonDecode(response.body)['Location'] as String;
return location.trim().isEmpty ? null : location;
} catch (e) {
debugPrint('$e');
}
return null;
}
Future<bool> mount(bool unmount, {String? location}) async {
try {
_isMounting = true;
mountConfig.mounted = null;
notifyListeners();
var count = 0;
while (_isRefreshing && count++ < 10) {
await Future.delayed(Duration(seconds: 1));
}
final auth = await _auth.createAuth();
final response = await http.post(
Uri.parse(
Uri.encodeFull(
'${getBaseUri()}/api/v1/mount?auth=$auth&unmount=$unmount&name=$name&type=$type&location=$location',
),
),
);
if (response.statusCode == 401) {
displayAuthError(_auth);
_auth.logoff();
return false;
}
if (response.statusCode == 404) {
_isMounting = false;
_mountList?.reset();
return true;
}
final badLocation = (!unmount && response.statusCode == 500);
if (badLocation) {
mountConfig.path = "";
}
await refresh(force: true);
_isMounting = false;
return !badLocation;
} catch (e) {
debugPrint('$e');
}
_isMounting = false;
return true;
}
Future<void> refresh({bool force = false}) async {
if (!force && (_isMounting || _isRefreshing)) {
return;
}
_isRefreshing = true;
try {
await _fetch();
await _fetchStatus();
} catch (e) {
debugPrint('$e');
}
_isRefreshing = false;
}
Future<void> setValue(String key, String value) async {
try {
final auth = await _auth.createAuth();
final response = await http.put(
Uri.parse(
Uri.encodeFull(
'${getBaseUri()}/api/v1/set_value_by_name?auth=$auth&name=$name&type=$type&key=$key&value=$value',
),
),
);
if (response.statusCode == 401) {
_auth.logoff();
return;
}
if (response.statusCode == 404) {
_mountList?.reset();
return;
}
if (response.statusCode != 200) {
return;
}
return refresh();
} catch (e) {
debugPrint('$e');
}
}
}
+201
View File
@@ -0,0 +1,201 @@
import 'dart:convert';
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' show ModalRoute;
import 'package:http/http.dart' as http;
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart';
import 'package:repertory/models/auth.dart';
import 'package:repertory/models/mount.dart';
import 'package:repertory/types/mount_config.dart';
class MountList with ChangeNotifier {
final Auth _auth;
MountList(this._auth) {
_auth.mountList = this;
_auth.addListener(() {
if (_auth.authenticated) {
_fetch();
}
});
}
List<Mount> _mountList = [];
Auth get auth => _auth;
UnmodifiableListView<Mount> get items =>
UnmodifiableListView<Mount>(_mountList);
bool hasBucketName(String mountType, String bucket, {String? excludeName}) {
final list = items.where(
(item) => item.type.toLowerCase() == mountType.toLowerCase(),
);
return (excludeName == null
? list
: list.whereNot(
(item) =>
item.name.toLowerCase() == excludeName.toLowerCase(),
))
.firstWhereOrNull((Mount item) {
return item.bucket != null &&
item.bucket!.toLowerCase() == bucket.toLowerCase();
}) !=
null;
}
bool hasConfigName(String name) {
return items.firstWhereOrNull(
(item) => item.name.toLowerCase() == name.toLowerCase(),
) !=
null;
}
Future<void> _fetch() async {
try {
final auth = await _auth.createAuth();
final response = await http.get(
Uri.parse('${getBaseUri()}/api/v1/mount_list?auth=$auth'),
);
if (response.statusCode == 401) {
displayAuthError(_auth);
_auth.logoff();
return;
}
if (response.statusCode == 404) {
reset();
return;
}
if (response.statusCode != 200) {
return;
}
List<Mount> nextList = [];
jsonDecode(response.body).forEach((type, value) {
nextList.addAll(
value
.map(
(name) =>
Mount(_auth, MountConfig(type: type, name: name), this),
)
.toList(),
);
});
_sort(nextList);
_mountList = nextList;
notifyListeners();
} catch (e) {
debugPrint('$e');
}
}
void _sort(list) {
list.sort((a, b) {
final res = a.type.compareTo(b.type);
if (res != 0) {
return res;
}
return a.name.compareTo(b.name);
});
}
Future<bool> add(
String type,
String name,
Map<String, dynamic> settings,
) async {
var ret = false;
var apiPort = settings['ApiPort'] ?? 10000;
for (var mount in _mountList) {
var port = mount.mountConfig.settings['ApiPort'] as int?;
if (port != null) {
apiPort = max(apiPort, port + 1);
}
}
settings["ApiPort"] = apiPort;
displayError() {
if (constants.navigatorKey.currentContext == null) {
return;
}
displayErrorMessage(
constants.navigatorKey.currentContext!,
'Add mount failed. Please try again.',
);
}
try {
final auth = await _auth.createAuth();
final map = await convertAllToString(
jsonDecode(jsonEncode(settings)),
_auth.key,
);
final response = await http.post(
Uri.parse(
Uri.encodeFull(
'${getBaseUri()}/api/v1/add_mount?auth=$auth&name=$name&type=$type&config=${jsonEncode(map)}',
),
),
);
switch (response.statusCode) {
case 200:
ret = true;
break;
case 401:
displayAuthError(_auth);
_auth.logoff();
break;
case 404:
reset();
break;
default:
displayError();
break;
}
} catch (e) {
debugPrint('$e');
displayError();
}
if (ret) {
await _fetch();
}
return ret;
}
void clear() {
_mountList = [];
notifyListeners();
}
Future<void> reset() async {
if (constants.navigatorKey.currentContext == null ||
ModalRoute.of(constants.navigatorKey.currentContext!)?.settings.name !=
'/') {
await constants.navigatorKey.currentState?.pushReplacementNamed('/');
}
displayErrorMessage(
constants.navigatorKey.currentContext!,
'Mount removed externally. Reloading...',
);
clear();
return _fetch();
}
}
@@ -0,0 +1,245 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart';
import 'package:repertory/models/auth.dart';
import 'package:repertory/models/mount.dart';
import 'package:repertory/models/mount_list.dart';
import 'package:repertory/types/mount_config.dart';
import 'package:repertory/widgets/mount_settings.dart';
class AddMountScreen extends StatefulWidget {
final String title;
const AddMountScreen({super.key, required this.title});
@override
State<AddMountScreen> createState() => _AddMountScreenState();
}
class _AddMountScreenState extends State<AddMountScreen> {
Mount? _mount;
final _mountNameController = TextEditingController();
String _mountType = "";
final Map<String, Map<String, dynamic>> _settings = {
"": {},
"Encrypt": createDefaultSettings("Encrypt"),
"Remote": createDefaultSettings("Remote"),
"S3": createDefaultSettings("S3"),
"Sia": createDefaultSettings("Sia"),
};
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
actions: [
Consumer<Auth>(
builder: (context, auth, _) {
return IconButton(
icon: const Icon(Icons.logout),
onPressed: () => auth.logoff(),
);
},
),
],
),
body: Padding(
padding: const EdgeInsets.all(constants.padding),
child: Consumer<Auth>(
builder: (context, auth, _) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
children: [
Card(
margin: EdgeInsets.all(0.0),
child: Padding(
padding: const EdgeInsets.all(constants.padding),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Provider Type'),
const SizedBox(width: constants.padding),
DropdownButton<String>(
autofocus: true,
value: _mountType,
onChanged:
(mountType) =>
_handleChange(auth, mountType ?? ''),
items:
constants.providerTypeList
.map<DropdownMenuItem<String>>((item) {
return DropdownMenuItem<String>(
value: item,
child: Text(item),
);
})
.toList(),
),
],
),
),
),
if (_mountType.isNotEmpty && _mountType != 'Remote')
const SizedBox(height: constants.padding),
if (_mountType.isNotEmpty && _mountType != 'Remote')
Card(
margin: EdgeInsets.all(0.0),
child: Padding(
padding: const EdgeInsets.all(constants.padding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Configuration Name'),
const SizedBox(width: constants.padding),
TextField(
autofocus: true,
controller: _mountNameController,
keyboardType: TextInputType.text,
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp(r'\s')),
],
onChanged: (_) => _handleChange(auth, _mountType),
),
],
),
),
),
if (_mount != null) const SizedBox(height: constants.padding),
if (_mount != null)
Expanded(
child: Card(
margin: EdgeInsets.all(0.0),
child: Padding(
padding: const EdgeInsets.all(constants.padding),
child: MountSettingsWidget(
isAdd: true,
mount: _mount!,
settings: _settings[_mountType]!,
showAdvanced: false,
),
),
),
),
if (_mount != null) const SizedBox(height: constants.padding),
if (_mount != null)
Row(
children: [
ElevatedButton.icon(
label: const Text('Add'),
icon: const Icon(Icons.add),
onPressed: () async {
final mountList = Provider.of<MountList>(context);
List<String> failed = [];
if (!validateSettings(
_settings[_mountType]!,
failed,
)) {
for (var key in failed) {
displayErrorMessage(
context,
"Setting '$key' is not valid",
);
}
return;
}
if (mountList.hasConfigName(
_mountNameController.text,
)) {
return displayErrorMessage(
context,
"Configuration name '${_mountNameController.text}' already exists",
);
}
if (_mountType == "Sia" || _mountType == "S3") {
final bucket =
_settings[_mountType]!["${_mountType}Config"]["Bucket"]
as String;
if (mountList.hasBucketName(_mountType, bucket)) {
return displayErrorMessage(
context,
"Bucket '$bucket' already exists",
);
}
}
final success = await mountList.add(
_mountType,
_mountType == 'Remote'
? '${_settings[_mountType]!['RemoteConfig']['HostNameOrIp']}_${_settings[_mountType]!['RemoteConfig']['ApiPort']}'
: _mountNameController.text,
_settings[_mountType]!,
);
if (!success || !context.mounted) {
return;
}
Navigator.pop(context);
},
),
if (_mountType == 'Sia' || _mountType == 'S3') ...[
const SizedBox(width: constants.padding),
ElevatedButton.icon(
label: const Text('Test'),
icon: const Icon(Icons.check),
onPressed: () async {},
),
],
],
),
],
);
},
),
),
);
}
void _handleChange(Auth auth, String mountType) {
setState(() {
final changed = _mountType != mountType;
_mountType = mountType;
if (_mountType == 'Remote') {
_mountNameController.text = 'remote';
} else if (changed) {
_mountNameController.text = mountType == 'Sia' ? 'default' : '';
}
_mount =
(_mountNameController.text.isEmpty)
? null
: Mount(
auth,
MountConfig(
name: _mountNameController.text,
settings: _settings[mountType],
type: mountType,
),
null,
isAdd: true,
);
});
}
@override
void setState(VoidCallback fn) {
if (!mounted) {
return;
}
super.setState(fn);
}
}
+116
View File
@@ -0,0 +1,116 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/models/auth.dart';
class AuthScreen extends StatefulWidget {
final String title;
const AuthScreen({super.key, required this.title});
@override
State<AuthScreen> createState() => _AuthScreenState();
}
class _AuthScreenState extends State<AuthScreen> {
bool _enabled = true;
final _passwordController = TextEditingController();
final _userController = TextEditingController();
@override
Widget build(context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
),
body: Consumer<Auth>(
builder: (context, auth, _) {
if (auth.authenticated) {
Future.delayed(Duration(milliseconds: 1), () {
if (constants.navigatorKey.currentContext == null) {
return;
}
Navigator.of(
constants.navigatorKey.currentContext!,
).pushNamedAndRemoveUntil('/', (Route<dynamic> route) => false);
});
return SizedBox.shrink();
}
createLoginHandler() {
return _enabled
? () async {
setState(() => _enabled = false);
await auth.authenticate(
_userController.text,
_passwordController.text,
);
setState(() => _enabled = true);
}
: null;
}
return Center(
child: Card(
child: Padding(
padding: const EdgeInsets.all(constants.padding),
child: SizedBox(
width: constants.logonWidth,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
constants.appLogonTitle,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: constants.padding),
TextField(
autofocus: true,
decoration: InputDecoration(labelText: 'Username'),
controller: _userController,
textInputAction: TextInputAction.next,
),
const SizedBox(height: constants.padding),
TextField(
obscureText: true,
decoration: InputDecoration(labelText: 'Password'),
controller: _passwordController,
textInputAction: TextInputAction.go,
onSubmitted: (_) {
final handler = createLoginHandler();
if (handler == null) {
return;
}
handler();
},
),
const SizedBox(height: constants.padding),
ElevatedButton(
onPressed: createLoginHandler(),
child: const Text('Login'),
),
],
),
),
),
),
);
},
),
);
}
@override
void setState(VoidCallback fn) {
if (!mounted) {
return;
}
super.setState(fn);
}
}
@@ -0,0 +1,70 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:repertory/models/auth.dart';
import 'package:repertory/models/mount.dart';
import 'package:repertory/widgets/mount_settings.dart';
class EditMountScreen extends StatefulWidget {
final Mount mount;
final String title;
const EditMountScreen({super.key, required this.mount, required this.title});
@override
State<EditMountScreen> createState() => _EditMountScreenState();
}
class _EditMountScreenState extends State<EditMountScreen> {
bool _showAdvanced = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
actions: [
Row(
children: [
Row(
children: [
const Text("Advanced"),
IconButton(
icon: Icon(
_showAdvanced ? Icons.toggle_on : Icons.toggle_off,
),
onPressed:
() => setState(() => _showAdvanced = !_showAdvanced),
),
],
),
Consumer<Auth>(
builder: (context, auth, _) {
return IconButton(
icon: const Icon(Icons.logout),
onPressed: () => auth.logoff(),
);
},
),
],
),
],
),
body: MountSettingsWidget(
mount: widget.mount,
settings: jsonDecode(jsonEncode(widget.mount.mountConfig.settings)),
showAdvanced: _showAdvanced,
),
);
}
@override
void setState(VoidCallback fn) {
if (!mounted) {
return;
}
super.setState(fn);
}
}
@@ -0,0 +1,87 @@
import 'dart:convert' show jsonDecode, jsonEncode;
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
import 'package:repertory/helpers.dart';
import 'package:repertory/models/auth.dart';
import 'package:repertory/widgets/ui_settings.dart';
class EditSettingsScreen extends StatefulWidget {
final String title;
const EditSettingsScreen({super.key, required this.title});
@override
State<EditSettingsScreen> createState() => _EditSettingsScreenState();
}
class _EditSettingsScreenState extends State<EditSettingsScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: Text(widget.title),
actions: [
Consumer<Auth>(
builder: (context, auth, _) {
return IconButton(
icon: const Icon(Icons.logout),
onPressed: () => auth.logoff(),
);
},
),
],
),
body: FutureBuilder(
builder: (context, snapshot) {
if (!snapshot.hasData) {
return Center(child: CircularProgressIndicator());
}
return UISettingsWidget(
origSettings: jsonDecode(jsonEncode(snapshot.requireData)),
settings: snapshot.requireData,
showAdvanced: false,
);
},
future: _grabSettings(),
initialData: <String, dynamic>{},
),
);
}
Future<Map<String, dynamic>> _grabSettings() async {
try {
final authProvider = Provider.of<Auth>(context, listen: false);
final auth = await authProvider.createAuth();
final response = await http.get(
Uri.parse('${getBaseUri()}/api/v1/settings?auth=$auth'),
);
if (response.statusCode == 401) {
authProvider.logoff();
return {};
}
if (response.statusCode != 200) {
return {};
}
return jsonDecode(response.body);
} catch (e) {
debugPrint('$e');
}
return {};
}
@override
void setState(VoidCallback fn) {
if (!mounted) {
return;
}
super.setState(fn);
}
}
@@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/models/auth.dart';
import 'package:repertory/widgets/mount_list_widget.dart';
class HomeScreen extends StatefulWidget {
final String title;
const HomeScreen({super.key, required this.title});
@override
State<HomeScreen> createState() => _HomeScreeState();
}
class _HomeScreeState extends State<HomeScreen> {
@override
Widget build(context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
leading: IconButton(
onPressed: () => Navigator.pushNamed(context, '/settings'),
icon: const Icon(Icons.storage),
),
title: Text(widget.title),
actions: [
Consumer<Auth>(
builder: (context, auth, _) {
return IconButton(
icon: const Icon(Icons.logout),
onPressed: () => auth.logoff(),
);
},
),
],
),
body: Padding(
padding: const EdgeInsets.all(constants.padding),
child: MountListWidget(),
),
floatingActionButton: FloatingActionButton(
onPressed: () => Navigator.pushNamed(context, '/add'),
tooltip: 'Add Mount',
child: const Icon(Icons.add),
),
);
}
@override
void setState(VoidCallback fn) {
if (!mounted) {
return;
}
super.setState(fn);
}
}
+390
View File
@@ -0,0 +1,390 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart' show Validator, displayErrorMessage;
import 'package:settings_ui/settings_ui.dart';
void createBooleanSetting(
context,
List<Widget> list,
Map<String, dynamic> settings,
String key,
value,
bool isAdvanced,
bool showAdvanced,
widget,
Function setState, {
String? description,
IconData icon = Icons.quiz,
}) {
if (!isAdvanced || showAdvanced) {
list.add(
SettingsTile.switchTile(
leading: Icon(icon),
title: createSettingTitle(context, key, description),
initialValue: (value as bool),
onPressed: (_) => setState(() => settings[key] = !value),
onToggle: (bool nextValue) {
setState(() => settings[key] = nextValue);
},
),
);
}
}
void createIntListSetting(
context,
List<Widget> list,
Map<String, dynamic> settings,
String key,
value,
List<String> valueList,
defaultValue,
IconData icon,
bool isAdvanced,
bool showAdvanced,
widget,
Function setState, {
String? description,
}) {
if (!isAdvanced || widget.showAdvanced) {
list.add(
SettingsTile.navigation(
title: createSettingTitle(context, key, description),
leading: Icon(icon),
value: DropdownButton<String>(
value: value.toString(),
onChanged: (newValue) {
setState(
() =>
settings[key] = int.parse(
newValue ?? defaultValue.toString(),
),
);
},
items:
valueList.map<DropdownMenuItem<String>>((item) {
return DropdownMenuItem<String>(value: item, child: Text(item));
}).toList(),
),
),
);
}
}
void createIntSetting(
context,
List<Widget> list,
Map<String, dynamic> settings,
String key,
value,
bool isAdvanced,
bool showAdvanced,
widget,
Function setState, {
String? description,
IconData icon = Icons.onetwothree,
List<Validator> validators = const [],
}) {
if (!isAdvanced || widget.showAdvanced) {
list.add(
SettingsTile.navigation(
leading: Icon(icon),
title: createSettingTitle(context, key, description),
value: Text(value.toString()),
onPressed: (_) {
String updatedValue = value.toString();
showDialog(
context: context,
builder: (context) {
return AlertDialog(
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
child: const Text('OK'),
onPressed: () {
final result = validators.firstWhereOrNull(
(validator) => !validator(updatedValue),
);
if (result != null) {
return displayErrorMessage(
context,
"Setting '$key' is not valid",
);
}
setState(() => settings[key] = int.parse(updatedValue));
Navigator.of(context).pop();
},
),
],
content: TextField(
autofocus: true,
controller: TextEditingController(text: updatedValue),
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
keyboardType: TextInputType.number,
onChanged: (nextValue) => updatedValue = nextValue,
),
title: createSettingTitle(context, key, description),
);
},
);
},
),
);
}
}
void createPasswordSetting(
context,
List<Widget> list,
Map<String, dynamic> settings,
String key,
value,
bool isAdvanced,
bool showAdvanced,
widget,
Function setState, {
String? description,
IconData icon = Icons.password,
List<Validator> validators = const [],
}) {
if (!isAdvanced || widget.showAdvanced) {
list.add(
SettingsTile.navigation(
leading: Icon(icon),
title: createSettingTitle(context, key, description),
value: Text('*' * (value as String).length),
onPressed: (_) {
String updatedValue1 = value;
String updatedValue2 = value;
bool hidePassword1 = true;
bool hidePassword2 = true;
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, setDialogState) {
return AlertDialog(
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
child: const Text('OK'),
onPressed: () {
if (updatedValue1 != updatedValue2) {
return displayErrorMessage(
context,
"Setting '$key' does not match",
);
}
final result = validators.firstWhereOrNull(
(validator) => !validator(updatedValue1),
);
if (result != null) {
return displayErrorMessage(
context,
"Setting '$key' is not valid",
);
}
setState(() => settings[key] = updatedValue1);
Navigator.of(context).pop();
},
),
],
content: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(
child: TextField(
autofocus: true,
controller: TextEditingController(
text: updatedValue1,
),
obscureText: hidePassword1,
obscuringCharacter: '*',
onChanged: (value) => updatedValue1 = value,
),
),
IconButton(
onPressed:
() => setDialogState(
() => hidePassword1 = !hidePassword1,
),
icon: Icon(
hidePassword1
? Icons.visibility
: Icons.visibility_off,
),
),
],
),
const SizedBox(height: constants.padding),
Row(
children: [
Expanded(
child: TextField(
autofocus: false,
controller: TextEditingController(
text: updatedValue2,
),
obscureText: hidePassword2,
obscuringCharacter: '*',
onChanged: (value) => updatedValue2 = value,
),
),
IconButton(
onPressed:
() => setDialogState(
() => hidePassword2 = !hidePassword2,
),
icon: Icon(
hidePassword2
? Icons.visibility
: Icons.visibility_off,
),
),
],
),
],
),
title: createSettingTitle(context, key, description),
);
},
);
},
);
},
),
);
}
}
Widget createSettingTitle(context, String key, String? description) {
if (description == null) {
return Text(key);
}
return Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(key, textAlign: TextAlign.start),
Text(
description,
style: Theme.of(context).textTheme.titleSmall,
textAlign: TextAlign.start,
),
],
);
}
void createStringListSetting(
context,
List<Widget> list,
Map<String, dynamic> settings,
String key,
value,
List<String> valueList,
IconData icon,
bool isAdvanced,
bool showAdvanced,
widget,
Function setState, {
String? description,
}) {
if (!isAdvanced || widget.showAdvanced) {
list.add(
SettingsTile.navigation(
title: createSettingTitle(context, key, description),
leading: Icon(icon),
value: DropdownButton<String>(
value: value,
onChanged: (newValue) => setState(() => settings[key] = newValue),
items:
valueList.map<DropdownMenuItem<String>>((item) {
return DropdownMenuItem<String>(value: item, child: Text(item));
}).toList(),
),
),
);
}
}
void createStringSetting(
context,
List<Widget> list,
Map<String, dynamic> settings,
String key,
value,
IconData icon,
bool isAdvanced,
bool showAdvanced,
widget,
Function setState, {
String? description,
List<Validator> validators = const [],
}) {
if (!isAdvanced || widget.showAdvanced) {
list.add(
SettingsTile.navigation(
leading: Icon(icon),
title: createSettingTitle(context, key, description),
value: Text(value),
onPressed: (_) {
String updatedValue = value;
showDialog(
context: context,
builder: (context) {
return AlertDialog(
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () => Navigator.of(context).pop(),
),
TextButton(
child: const Text('OK'),
onPressed: () {
final result = validators.firstWhereOrNull(
(validator) => !validator(updatedValue),
);
if (result != null) {
return displayErrorMessage(
context,
"Setting '$key' is not valid",
);
}
setState(() => settings[key] = updatedValue);
Navigator.of(context).pop();
},
),
],
content: TextField(
autofocus: true,
controller: TextEditingController(text: updatedValue),
inputFormatters: [
FilteringTextInputFormatter.deny(RegExp(r'\s')),
],
onChanged: (value) => updatedValue = value,
),
title: createSettingTitle(context, key, description),
);
},
);
},
),
);
}
}
+33
View File
@@ -0,0 +1,33 @@
import 'package:collection/collection.dart';
import 'package:repertory/helpers.dart' show initialCaps;
class MountConfig {
bool? mounted;
final String _name;
String path = '';
Map<String, dynamic> _settings = {};
final String _type;
MountConfig({required name, required type, Map<String, dynamic>? settings})
: _name = name,
_type = type {
if (settings != null) {
_settings = settings;
}
}
String? get bucket => _settings['${provider}Config']?["Bucket"] as String;
String get name => _name;
String get provider => initialCaps(_type);
UnmodifiableMapView<String, dynamic> get settings =>
UnmodifiableMapView<String, dynamic>(_settings);
String get type => _type;
void updateSettings(Map<String, dynamic> settings) {
_settings = settings;
}
void updateStatus(Map<String, dynamic> status) {
path = status['Location'] as String;
mounted = status['Active'] as bool;
}
}
@@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/models/mount_list.dart';
import 'package:repertory/widgets/mount_widget.dart';
class MountListWidget extends StatelessWidget {
const MountListWidget({super.key});
@override
Widget build(BuildContext context) {
return Consumer<MountList>(
builder: (context, MountList mountList, _) {
return ListView.builder(
itemBuilder: (context, idx) {
return ChangeNotifierProvider(
create: (context) => mountList.items[idx],
key: ValueKey(mountList.items[idx].id),
child: Padding(
padding: EdgeInsets.only(
bottom:
idx == mountList.items.length - 1
? 0.0
: constants.padding,
),
child: const MountWidget(),
),
);
},
itemCount: mountList.items.length,
);
},
);
}
}
File diff suppressed because it is too large. Load diff
+206
View File
@@ -0,0 +1,206 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart';
import 'package:repertory/models/mount.dart';
class MountWidget extends StatefulWidget {
const MountWidget({super.key});
@override
State<MountWidget> createState() => _MountWidgetState();
}
class _MountWidgetState extends State<MountWidget> {
bool _enabled = true;
bool _editEnabled = true;
Timer? _timer;
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.all(0.0),
child: Consumer<Mount>(
builder: (context, Mount mount, _) {
final textColor = Theme.of(context).colorScheme.onSurface;
final subTextColor =
Theme.of(context).brightness == Brightness.dark
? Colors.white38
: Colors.black87;
final nameText = SelectableText(
formatMountName(mount.type, mount.name),
style: TextStyle(color: subTextColor),
);
return ListTile(
isThreeLine: true,
leading: IconButton(
icon: Icon(Icons.settings, color: textColor),
onPressed:
() => Navigator.pushNamed(context, '/edit', arguments: mount),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
nameText,
SelectableText(
mount.path.isEmpty && mount.mounted == null
? 'loading...'
: mount.path.isEmpty
? '<mount location not set>'
: mount.path,
style: TextStyle(color: subTextColor),
),
],
),
title: SelectableText(
mount.provider,
style: TextStyle(color: textColor, fontWeight: FontWeight.bold),
),
trailing: Row(
mainAxisAlignment: MainAxisAlignment.end,
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (mount.mounted != null && !mount.mounted!)
IconButton(
icon: const Icon(Icons.edit),
color: subTextColor,
tooltip: 'Edit mount location',
onPressed: () async {
setState(() => _editEnabled = false);
final available = await mount.getAvailableLocations();
if (context.mounted) {
final location = await editMountLocation(
context,
available,
location: mount.path,
);
if (location != null) {
await mount.setMountLocation(location);
}
}
setState(() => _editEnabled = true);
},
),
IconButton(
icon: Icon(
mount.mounted == null
? Icons.hourglass_top
: mount.mounted!
? Icons.toggle_on
: Icons.toggle_off,
color:
mount.mounted ?? false
? Color.fromARGB(255, 163, 96, 76)
: subTextColor,
),
tooltip:
mount.mounted == null
? ''
: mount.mounted!
? 'Unmount'
: 'Mount',
onPressed: _createMountHandler(context, mount),
),
],
),
);
},
),
);
}
VoidCallback? _createMountHandler(context, Mount mount) {
return _enabled && mount.mounted != null
? () async {
if (mount.mounted == null) {
return;
}
final mounted = mount.mounted!;
setState(() {
_enabled = false;
});
final location = await _getMountLocation(context, mount);
cleanup() {
setState(() {
_enabled = true;
});
}
if (!mounted && location == null) {
displayErrorMessage(context, "Mount location is not set");
return cleanup();
}
final success = await mount.mount(mounted, location: location);
if (success ||
mounted ||
constants.navigatorKey.currentContext == null ||
!constants.navigatorKey.currentContext!.mounted) {
return cleanup();
}
displayErrorMessage(
context,
"Mount location is not available: $location",
);
cleanup();
}
: null;
}
@override
void dispose() {
_timer?.cancel();
_timer = null;
super.dispose();
}
Future<String?> _getMountLocation(context, Mount mount) async {
if (mount.mounted ?? false) {
return null;
}
if (mount.path.isNotEmpty) {
return mount.path;
}
String? location = await mount.getMountLocation();
if (location != null) {
return location;
}
if (!context.mounted) {
return location;
}
return editMountLocation(context, await mount.getAvailableLocations());
}
@override
void initState() {
super.initState();
_timer = Timer.periodic(const Duration(seconds: 5), (_) {
Provider.of<Mount>(context, listen: false).refresh();
});
}
@override
void setState(VoidCallback fn) {
if (!mounted) {
return;
}
super.setState(fn);
}
}
+155
View File
@@ -0,0 +1,155 @@
import 'dart:convert' show jsonEncode;
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart'
show
convertAllToString,
displayAuthError,
getBaseUri,
getChanged,
getSettingDescription,
getSettingValidators,
trimNotEmptyValidator;
import 'package:repertory/models/auth.dart';
import 'package:repertory/settings.dart';
import 'package:settings_ui/settings_ui.dart';
class UISettingsWidget extends StatefulWidget {
final bool showAdvanced;
final Map<String, dynamic> settings;
final Map<String, dynamic> origSettings;
const UISettingsWidget({
super.key,
required this.origSettings,
required this.settings,
required this.showAdvanced,
});
@override
State<UISettingsWidget> createState() => _UISettingsWidgetState();
}
class _UISettingsWidgetState extends State<UISettingsWidget> {
@override
Widget build(BuildContext context) {
List<SettingsTile> commonSettings = [];
widget.settings.forEach((key, value) {
switch (key) {
case 'ApiPassword':
{
createPasswordSetting(
context,
commonSettings,
widget.settings,
key,
value,
false,
widget.showAdvanced,
widget,
setState,
description: getSettingDescription(key),
validators: getSettingValidators(key),
);
}
break;
case 'ApiPort':
{
createIntSetting(
context,
commonSettings,
widget.settings,
key,
value,
false,
widget.showAdvanced,
widget,
setState,
description: getSettingDescription(key),
validators: getSettingValidators(key),
);
}
break;
case 'ApiUser':
{
createStringSetting(
context,
commonSettings,
widget.settings,
key,
value,
Icons.person,
false,
widget.showAdvanced,
widget,
setState,
description: getSettingDescription(key),
validators: [...getSettingValidators(key), trimNotEmptyValidator],
);
}
break;
}
});
return SettingsList(
shrinkWrap: false,
sections: [
SettingsSection(title: const Text('Settings'), tiles: commonSettings),
],
);
}
@override
void dispose() {
final settings = getChanged(widget.origSettings, widget.settings);
if (settings.isNotEmpty) {
final key =
Provider.of<Auth>(
constants.navigatorKey.currentContext!,
listen: false,
).key;
convertAllToString(settings, key)
.then((map) async {
try {
final authProvider = Provider.of<Auth>(
constants.navigatorKey.currentContext!,
listen: false,
);
final auth = await authProvider.createAuth();
final response = await http.put(
Uri.parse(
Uri.encodeFull(
'${getBaseUri()}/api/v1/settings?auth=$auth&data=${jsonEncode(map)}',
),
),
);
if (response.statusCode == 401) {
displayAuthError(authProvider);
authProvider.logoff();
}
} catch (e) {
debugPrint('$e');
}
})
.catchError((e) {
debugPrint('$e');
});
}
super.dispose();
}
@override
void setState(VoidCallback fn) {
if (!mounted) {
return;
}
super.setState(fn);
}
}
+95
View File
@@ -0,0 +1,95 @@
name: repertory
description: "Repertory Management Portal"
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: ^3.7.0
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
collection: ^1.19.1
http: ^1.3.0
provider: ^6.1.2
settings_ui: ^2.0.2
sodium_libs: ^3.4.4+1
convert: ^3.1.2
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package
+30
View File
@@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:repertory/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

+44
View File
@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="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="repertory">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png">
<title>repertory</title>
<link rel="manifest" href="manifest.json">
<script type="text/javascript" src="sodium.js" async="true"></script>
</head>
<body>
<script>
window.flutterConfiguration = {
canvasKitBaseUrl: "/canvaskit/"
};
</script>
<script src="flutter_bootstrap.js" async=""></script>
</body>
</html>
+35
View File
@@ -0,0 +1,35 @@
{
"name": "repertory",
"short_name": "repertory",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"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"
}
]
}
+1
View File
@@ -0,0 +1 @@
!function(A){function g(A){"use strict";var g;void 0===(g=A)&&(g={});var I=g;"object"!=typeof I.sodium&&("object"==typeof global?I=global:"object"==typeof window&&(I=window));var C=g;return g.ready=new Promise((function(A,g){(e=C).onAbort=g,e.print=function(A){},e.printErr=function(A){},e.onRuntimeInitialized=function(){try{e._crypto_secretbox_keybytes(),A()}catch(A){g(A)}},e.useBackupModule=function(){return new Promise((function(A,g){(e={}).onAbort=g,e.onRuntimeInitialized=function(){Object.keys(C).forEach((function(A){"getRandomValue"!==A&&delete C[A]})),Object.keys(e).forEach((function(A){C[A]=e[A]})),A()};var I,e=void 0!==e?e:{},B="object"==typeof window,a="function"==typeof importScripts,i="object"==typeof process&&"object"==typeof process.versions&&"string"==typeof process.versions.node,r=Object.assign({},e),t="";if(i){var o=require("fs"),Q=require("path");t=__dirname+"/",I=A=>(A=K(A)?new URL(A):Q.normalize(A),o.readFileSync(A)),!e.thisProgram&&process.argv.length>1&&process.argv[1].replace(/\\/g,"/"),process.argv.slice(2),"undefined"!=typeof module&&(module.exports=e)}else(B||a)&&(a?t=self.location.href:"undefined"!=typeof document&&document.currentScript&&(t=document.currentScript.src),t=t.startsWith("blob:")?"":t.substr(0,t.replace(/[?#].*/,"").lastIndexOf("/")+1),a&&(I=A=>{var g=new XMLHttpRequest;return g.open("GET",A,!1),g.responseType="arraybuffer",g.send(null),new Uint8Array(g.response)}));e.print;var c,E=e.printErr||void 0;Object.assign(e,r),r=null,e.arguments&&e.arguments,e.thisProgram&&e.thisProgram,e.quit&&e.quit,e.wasmBinary&&(c=e.wasmBinary);var y,_={Memory:function(A){this.buffer=new ArrayBuffer(65536*A.initial)},Module:function(A){},Instance:function(A,g){this.exports=function(A){for(var g,I=new Uint8Array(123),C=25;C>=0;--C)I[48+C]=52+C,I[65+C]=C,I[97+C]=26+C;function e(A,g,C){for(var e,B,a=0,i=g,r=C.length,t=g+(3*r>>2)-("="==C[r-2])-("="==C[r-1]);a<r;a+=4)e=I[C.charCodeAt(a+1)],B=I[C.charCodeAt(a+2)],A[i++]=I[C.charCodeAt(a)]<<2|e>>4,i<t&&(A[i++]=e<<4|B>>2),i<t&&(A[i++]=B<<6|I[C.charCodeAt(a+3)])}function B(){throw new Error("abort")}return I[43]=62,I[47]=63,function(A){var I=new ArrayBuffer(16777216),C=new Int8Array(I),a=(new Int16Array(I),new Int32Array(I)),i=new Uint8Array(I),r=(new Uint16Array(I),new Uint32Array(I)),t=(new Float32Array(I),new Float64Array(I),Math.imul),o=(Math.fround,Math.abs,Math.clz32),Q=(Math.min,Math.max,Math.floor,Math.ceil,Math.trunc,Math.sqrt,A.a),c=Q.a,E=Q.b,y=Q.c,_=Q.d,n=103152,h=0;function s(A,g){var I,e,B,a,r,t=0,o=0,Q=0,c=0,E=0,y=0,_=0,n=0,s=0,p=0,f=0,D=0,w=0,k=0,d=0,l=0,u=0,v=0,S=0,b=0,F=0,M=0,N=0,K=0,H=0,G=0,Y=0,U=0,J=0,x=0,m=0,P=0,R=0,L=0,T=0,z=0,q=0,j=0,X=0,O=0,V=0,W=0,Z=0,$=0,AA=0,gA=0,IA=0,CA=0,eA=0,BA=0,aA=0,iA=0,rA=0,tA=0,oA=0,QA=0,cA=0,EA=0,yA=0,_A=0,nA=0,hA=0,sA=0,pA=0,fA=0,DA=0,wA=0,kA=0,dA=0,lA=0,uA=0,SA=0,bA=0,FA=0;EA=i[g+40|0]|i[g+41|0]<<8|i[g+42|0]<<16|i[g+43|0]<<24,_A=t=i[g+44|0]|i[g+45|0]<<8|i[g+46|0]<<16|i[g+47|0]<<24,IA=i[g+104|0]|i[g+105|0]<<8|i[g+106|0]<<16|i[g+107|0]<<24,nA=t=i[g+108|0]|i[g+109|0]<<8|i[g+110|0]<<16|i[g+111|0]<<24,t=i[g+68|0]|i[g+69|0]<<8|i[g+70|0]<<16|i[g+71|0]<<24,q=i[g+64|0]|i[g+65|0]<<8|i[g+66|0]<<16|i[g+67|0]<<24,eA=t,uA=t=i[g+36|0]|i[g+37|0]<<8|i[g+38|0]<<16|i[g+39|0]<<24,u=t,rA=i[g+120|0]|i[g+121|0]<<8|i[g+122|0]<<16|i[g+123|0]<<24,fA=t=i[g+124|0]|i[g+125|0]<<8|i[g+126|0]<<16|i[g+127|0]<<24,B=t=i[g+92|0]|i[g+93|0]<<8|i[g+94|0]<<16|i[g+95|0]<<24,I=i[g+88|0]|i[g+89|0]<<8|i[g+90|0]<<16|i[g+91|0]<<24,z=t,iA=i[g+80|0]|i[g+81|0]<<8|i[g+82|0]<<16|i[g+83|0]<<24,sA=t=i[g+84|0]|i[g+85|0]<<8|i[g+86|0]<<16|i[g+87|0]<<24,j=t,BA=i[A+16|0]|i[A+17|0]<<8|i[A+18|0]<<16|i[A+19|0]<<24,t=(oA=i[A+52|0]|i[A+53|0]<<8|i[A+54|0]<<16|i[A+55|0]<<24)+u|0,L=(tA=i[A+48|0]|i[A+49|0]<<8|i[A+50|0]<<16|i[A+51|0]<<24)+(QA=i[g+32|0]|i[g+33|0]<<8|i[g+34|0]<<16|i[g+35|0]<<24)|0,t=(i[A+20|0]|i[A+21|0]<<8|i[A+22|0]<<16|i[A+23|0]<<24)+(tA>>>0>L>>>0?t+1|0:t)|0,t=(BA=(o=L)>>>0>(L=L+BA|0)>>>0?t+1|0:t)+_A|0,aA=yA=L+EA|0,yA=t=yA>>>0<L>>>0?t+1|0:t,L=vA(L^(i[A+80|0]|i[A+81|0]<<8|i[A+82|0]<<16|i[A+83|0]<<24)^-79577749,BA^(i[A+84|0]|i[A+85|0]<<8|i[A+86|0]<<16|i[A+87|0]<<24)^528734635,32),kA=t=h,t=t+1013904242|0,BA=L,V=t=(L=L-23791573|0)>>>0<4271175723?t+1|0:t,oA=vA(L^tA,t^oA,40),t=(t=yA)+(yA=h)|0,tA=vA(BA^(s=tA=oA+aA|0),kA^(p=s>>>0<oA>>>0?t+1|0:t),48),t=V+(P=h)|0,k=t=(tA=L+(f=tA)|0)>>>0<L>>>0?t+1|0:t,tA=t=vA(oA^(w=tA),yA^t,1),V=L=h,yA=i[g+24|0]|i[g+25|0]<<8|i[g+26|0]<<16|i[g+27|0]<<24,kA=t=i[g+28|0]|i[g+29|0]<<8|i[g+30|0]<<16|i[g+31|0]<<24,cA=i[A+8|0]|i[A+9|0]<<8|i[A+10|0]<<16|i[A+11|0]<<24,L=(oA=i[g+16|0]|i[g+17|0]<<8|i[g+18|0]<<16|i[g+19|0]<<24)+(BA=i[A+40|0]|i[A+41|0]<<8|i[A+42|0]<<16|i[A+43|0]<<24)|0,t=(SA=i[g+20|0]|i[g+21|0]<<8|i[g+22|0]<<16|i[g+23|0]<<24)+(FA=i[A+44|0]|i[A+45|0]<<8|i[A+46|0]<<16|i[A+47|0]<<24)|0,t=(i[A+12|0]|i[A+13|0]<<8|i[A+14|0]<<16|i[A+15|0]<<24)+(L>>>0<BA>>>0?t+1|0:t)|0,t=kA+(aA=(o=L)>>>0>(L=L+cA|0)>>>0?t+1|0:t)|0,t=(cA=L+yA|0)>>>0<L>>>0?t+1|0:t,o=vA(L^(i[A+72|0]|i[A+73|0]<<8|i[A+74|0]<<16|i[A+75|0]<<24)^725511199,aA^(i[A+76|0]|Line truncated