refactor ui
All checks were successful
BlockStorage/repertory/pipeline/head This commit looks good

This commit is contained in:
2025-09-05 10:11:57 -05:00
parent f5df53f781
commit 47cac7e71c
11 changed files with 479 additions and 441 deletions

View File

@@ -1,17 +1,12 @@
// helpers.dart // helpers.dart
import 'dart:ui';
import 'package:convert/convert.dart'; import 'package:convert/convert.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:repertory/constants.dart' as constants; import 'package:repertory/constants.dart' as constants;
import 'package:repertory/models/auth.dart'; import 'package:repertory/models/auth.dart';
import 'package:repertory/models/settings.dart';
import 'package:repertory/widgets/app_dropdown.dart'; import 'package:repertory/widgets/app_dropdown.dart';
import 'package:repertory/widgets/aurora_sweep.dart';
import 'package:sodium_libs/sodium_libs.dart' show SecureKey, StringX; import 'package:sodium_libs/sodium_libs.dart' show SecureKey, StringX;
Future doShowDialog(BuildContext context, Widget child) => showDialog( Future doShowDialog(BuildContext context, Widget child) => showDialog(
@@ -419,7 +414,7 @@ Future<String?> editMountLocation(
controller: controller, controller: controller,
onChanged: (value) => setState(() => currentLocation = value), onChanged: (value) => setState(() => currentLocation = value),
) )
: AppDropdownFormField<String>( : AppDropdown<String>(
labelOf: (s) => s, labelOf: (s) => s,
labelText: "Select drive", labelText: "Select drive",
onChanged: (value) => setState(() => currentLocation = value), onChanged: (value) => setState(() => currentLocation = value),
@@ -434,168 +429,6 @@ Future<String?> editMountLocation(
); );
} }
Scaffold createCommonScaffold(
BuildContext context,
String title,
List<Widget> children, {
Widget? advancedWidget,
Widget? floatingActionButton,
bool showBack = false,
}) {
final scheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Scaffold(
body: SafeArea(
child: Stack(
children: [
Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: constants.gradientColors,
),
),
),
Consumer<Settings>(
builder: (_, settings, _) =>
AuroraSweep(enabled: settings.enableAnimations),
),
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6),
child: Container(color: Colors.black.withValues(alpha: 0.06)),
),
),
Padding(
padding: const EdgeInsets.all(constants.padding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
if (!showBack) ...[
SizedBox(
width: 40,
height: 40,
child: Image.asset(
'assets/images/repertory.png',
fit: BoxFit.contain,
errorBuilder: (_, _, _) {
return Icon(
Icons.folder,
color: scheme.primary,
size: 32,
);
},
),
),
const SizedBox(width: constants.padding),
],
if (showBack) ...[
Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
onTap: () => Navigator.of(context).pop(),
child: Ink(
width: 40,
height: 40,
decoration: BoxDecoration(
color: scheme.surface.withValues(alpha: 0.40),
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
border: Border.all(
color: scheme.outlineVariant.withValues(
alpha: 0.08,
),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.22),
blurRadius: constants.borderRadius,
offset: Offset(0, constants.borderRadius),
),
],
),
child: const Icon(Icons.arrow_back),
),
),
),
const SizedBox(width: constants.padding),
],
Expanded(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
color: scheme.onSurface.withValues(alpha: 0.96),
),
),
),
const SizedBox(width: constants.padding),
if (!showBack) ...[
const Text("Auto-start"),
Consumer<Settings>(
builder: (context, settings, _) {
return IconButton(
icon: Icon(
settings.autoStart
? Icons.toggle_on
: Icons.toggle_off,
),
color: settings.autoStart
? scheme.primary
: scheme.onSurface.withValues(alpha: 0.70),
onPressed: () =>
settings.setAutoStart(!settings.autoStart),
);
},
),
IconButton(
tooltip: 'Settings',
icon: const Icon(Icons.settings),
onPressed: () {
Navigator.pushNamed(context, '/settings');
},
),
const SizedBox(width: constants.padding),
],
if (showBack && advancedWidget != null) ...[
advancedWidget,
const SizedBox(width: constants.padding),
],
Consumer<Auth>(
builder: (context, auth, _) => IconButton(
tooltip: 'Log out',
icon: const Icon(Icons.logout),
onPressed: auth.logoff,
),
),
],
),
const SizedBox(height: constants.padding),
...children,
],
),
),
],
),
),
floatingActionButton: floatingActionButton,
);
}
InputDecoration createCommonDecoration( InputDecoration createCommonDecoration(
ColorScheme colorScheme, ColorScheme colorScheme,
String label, { String label, {

View File

@@ -11,6 +11,7 @@ import 'package:repertory/models/mount_list.dart';
import 'package:repertory/types/mount_config.dart'; import 'package:repertory/types/mount_config.dart';
import 'package:repertory/utils/safe_set_state_mixin.dart'; import 'package:repertory/utils/safe_set_state_mixin.dart';
import 'package:repertory/widgets/app_dropdown.dart'; import 'package:repertory/widgets/app_dropdown.dart';
import 'package:repertory/widgets/app_scaffold.dart';
import 'package:repertory/widgets/mount_settings.dart'; import 'package:repertory/widgets/mount_settings.dart';
class AddMountScreen extends StatefulWidget { class AddMountScreen extends StatefulWidget {
@@ -45,148 +46,158 @@ class _AddMountScreenState extends State<AddMountScreen>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme; final scheme = Theme.of(context).colorScheme;
return createCommonScaffold(context, widget.title, [ return AppScaffold(
AppDropdownFormField<String>( title: widget.title,
constrainToIntrinsic: true, showBack: true,
isExpanded: false, children: [
labelOf: (s) => s, AppDropdown<String>(
labelText: 'Provider Type', constrainToIntrinsic: true,
onChanged: (mountType) { isExpanded: false,
_handleChange( labelOf: (s) => s,
Provider.of<Auth>(context, listen: false), labelText: 'Provider Type',
mountType ?? '', onChanged: (mountType) {
); _handleChange(
}, Provider.of<Auth>(context, listen: false),
prefixIcon: Icons.miscellaneous_services, mountType ?? '',
value: _mountType.isEmpty ? null : _mountType, );
values: constants.providerTypeList, },
widthMultiplier: 2.0, prefixIcon: Icons.miscellaneous_services,
), value: _mountType.isEmpty ? null : _mountType,
if (_mountType.isNotEmpty && _mountType != 'Remote') ...[ values: constants.providerTypeList,
const SizedBox(height: constants.padding), widthMultiplier: 2.0,
TextField(
autofocus: true,
controller: _mountNameController,
keyboardType: TextInputType.text,
inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'\s'))],
onChanged: (_) => _handleChange(
Provider.of<Auth>(context, listen: false),
_mountType,
),
decoration: createCommonDecoration(
scheme,
'Configuration Name',
hintText: 'Enter a unique name',
icon: Icons.drive_file_rename_outline,
),
), ),
], if (_mountType.isNotEmpty && _mountType != 'Remote') ...[
if (_mount != null) ...[ const SizedBox(height: constants.padding),
const SizedBox(height: constants.padding), TextField(
Expanded( autofocus: true,
child: Padding( controller: _mountNameController,
padding: const EdgeInsets.symmetric(horizontal: constants.padding), keyboardType: TextInputType.text,
child: ClipRRect( inputFormatters: [FilteringTextInputFormatter.deny(RegExp(r'\s'))],
borderRadius: BorderRadius.circular(constants.borderRadius), onChanged: (_) => _handleChange(
child: MountSettingsWidget( Provider.of<Auth>(context, listen: false),
isAdd: true, _mountType,
mount: _mount!, ),
settings: _settings[_mountType]!, decoration: createCommonDecoration(
showAdvanced: false, scheme,
'Configuration Name',
hintText: 'Enter a unique name',
icon: Icons.drive_file_rename_outline,
),
),
],
if (_mount != null) ...[
const SizedBox(height: constants.padding),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: constants.padding,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(constants.borderRadius),
child: MountSettingsWidget(
isAdd: true,
mount: _mount!,
settings: _settings[_mountType]!,
showAdvanced: false,
),
), ),
), ),
), ),
), const SizedBox(height: constants.padding),
const SizedBox(height: constants.padding), Row(
Row( children: [
children: [ IntrinsicWidth(
IntrinsicWidth( child: ElevatedButton.icon(
child: ElevatedButton.icon( label: const Text('Test'),
label: const Text('Test'), icon: const Icon(Icons.check),
icon: const Icon(Icons.check), style: ElevatedButton.styleFrom(
style: ElevatedButton.styleFrom( backgroundColor: scheme.primary.withValues(alpha: 0.18),
backgroundColor: scheme.primary.withValues(alpha: 0.18), foregroundColor: scheme.primary,
foregroundColor: scheme.primary, elevation: 0,
elevation: 0, shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(
borderRadius: BorderRadius.circular(constants.borderRadius), constants.borderRadius,
side: BorderSide( ),
color: scheme.outlineVariant.withValues(alpha: 0.15), side: BorderSide(
width: 1, color: scheme.outlineVariant.withValues(alpha: 0.15),
width: 1,
),
), ),
), ),
onPressed: _handleProviderTest,
), ),
onPressed: _handleProviderTest,
), ),
), const SizedBox(width: constants.padding),
const SizedBox(width: constants.padding), IntrinsicWidth(
IntrinsicWidth( child: ElevatedButton.icon(
child: ElevatedButton.icon( label: const Text('Add'),
label: const Text('Add'), icon: const Icon(Icons.add),
icon: const Icon(Icons.add), style: ElevatedButton.styleFrom(
style: ElevatedButton.styleFrom( backgroundColor: scheme.primary,
backgroundColor: scheme.primary, foregroundColor: scheme.onPrimary,
foregroundColor: scheme.onPrimary, elevation: 8,
elevation: 8, shadowColor: scheme.primary.withValues(alpha: 0.45),
shadowColor: scheme.primary.withValues(alpha: 0.45), shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(
borderRadius: BorderRadius.circular(constants.borderRadius), constants.borderRadius,
),
),
), ),
), onPressed: () async {
onPressed: () async { final mountList = Provider.of<MountList>(
final mountList = Provider.of<MountList>(
context,
listen: false,
);
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, context,
"Configuration name '${_mountNameController.text}' already exists", listen: false,
); );
}
if (_mountType == "Sia" || _mountType == "S3") { List<String> failed = [];
final bucket = if (!validateSettings(_settings[_mountType]!, failed)) {
_settings[_mountType]!["${_mountType}Config"]["Bucket"] for (var key in failed) {
as String; displayErrorMessage(
if (mountList.hasBucketName(_mountType, bucket)) { context,
"Setting '$key' is not valid",
);
}
return;
}
if (mountList.hasConfigName(_mountNameController.text)) {
return displayErrorMessage( return displayErrorMessage(
context, context,
"Bucket '$bucket' already exists", "Configuration name '${_mountNameController.text}' already exists",
); );
} }
}
final success = await mountList.add( if (_mountType == "Sia" || _mountType == "S3") {
_mountType, final bucket =
_mountType == 'Remote' _settings[_mountType]!["${_mountType}Config"]["Bucket"]
? '${_settings[_mountType]!['RemoteConfig']['HostNameOrIp']}_${_settings[_mountType]!['RemoteConfig']['ApiPort']}' as String;
: _mountNameController.text, if (mountList.hasBucketName(_mountType, bucket)) {
_settings[_mountType]!, return displayErrorMessage(
); context,
"Bucket '$bucket' already exists",
);
}
}
if (!success || !context.mounted) return; final success = await mountList.add(
_mountType,
_mountType == 'Remote'
? '${_settings[_mountType]!['RemoteConfig']['HostNameOrIp']}_${_settings[_mountType]!['RemoteConfig']['ApiPort']}'
: _mountNameController.text,
_settings[_mountType]!,
);
Navigator.pop(context); if (!success || !context.mounted) return;
},
Navigator.pop(context);
},
),
), ),
), ],
], ),
), ],
], ],
], showBack: true); );
} }
void _handleChange(Auth auth, String mountType) { void _handleChange(Auth auth, String mountType) {

View File

@@ -3,9 +3,9 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:repertory/constants.dart' as constants; import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart';
import 'package:repertory/models/mount.dart'; import 'package:repertory/models/mount.dart';
import 'package:repertory/utils/safe_set_state_mixin.dart'; import 'package:repertory/utils/safe_set_state_mixin.dart';
import 'package:repertory/widgets/app_scaffold.dart';
import 'package:repertory/widgets/mount_settings.dart'; import 'package:repertory/widgets/mount_settings.dart';
class EditMountScreen extends StatefulWidget { class EditMountScreen extends StatefulWidget {
@@ -25,27 +25,8 @@ class _EditMountScreenState extends State<EditMountScreen>
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme; final scheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme; final textTheme = Theme.of(context).textTheme;
return createCommonScaffold( return AppScaffold(
context, title: widget.title,
widget.title,
[
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: constants.padding),
child: ClipRRect(
borderRadius: BorderRadius.circular(constants.borderRadius),
child: MountSettingsWidget(
mount: widget.mount,
settings: jsonDecode(
jsonEncode(widget.mount.mountConfig.settings),
),
showAdvanced: _showAdvanced,
),
),
),
),
const SizedBox(height: constants.padding),
],
showBack: true, showBack: true,
advancedWidget: Row( advancedWidget: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -67,6 +48,24 @@ class _EditMountScreenState extends State<EditMountScreen>
), ),
], ],
), ),
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: constants.padding),
child: ClipRRect(
borderRadius: BorderRadius.circular(constants.borderRadius),
child: MountSettingsWidget(
mount: widget.mount,
settings: jsonDecode(
jsonEncode(widget.mount.mountConfig.settings),
),
showAdvanced: _showAdvanced,
),
),
),
),
const SizedBox(height: constants.padding),
],
); );
} }
} }

View File

@@ -9,6 +9,7 @@ import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart'; import 'package:repertory/helpers.dart';
import 'package:repertory/models/auth.dart'; import 'package:repertory/models/auth.dart';
import 'package:repertory/utils/safe_set_state_mixin.dart'; import 'package:repertory/utils/safe_set_state_mixin.dart';
import 'package:repertory/widgets/app_scaffold.dart';
import 'package:repertory/widgets/ui_settings.dart'; import 'package:repertory/widgets/ui_settings.dart';
class EditSettingsScreen extends StatefulWidget { class EditSettingsScreen extends StatefulWidget {
@@ -23,32 +24,36 @@ class _EditSettingsScreenState extends State<EditSettingsScreen>
with SafeSetState<EditSettingsScreen> { with SafeSetState<EditSettingsScreen> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return createCommonScaffold(context, widget.title, [ return AppScaffold(
Expanded( title: widget.title,
child: Padding( showBack: true,
padding: const EdgeInsets.symmetric(horizontal: constants.padding), children: [
child: ClipRRect( Expanded(
borderRadius: BorderRadius.circular(constants.borderRadius), child: Padding(
child: FutureBuilder<Map<String, dynamic>>( padding: const EdgeInsets.symmetric(horizontal: constants.padding),
future: _grabSettings(), child: ClipRRect(
initialData: const <String, dynamic>{}, borderRadius: BorderRadius.circular(constants.borderRadius),
builder: (context, snapshot) { child: FutureBuilder<Map<String, dynamic>>(
if (!snapshot.hasData) { future: _grabSettings(),
return const Center(child: CircularProgressIndicator()); initialData: const <String, dynamic>{},
} builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(child: CircularProgressIndicator());
}
return UISettingsWidget( return UISettingsWidget(
origSettings: jsonDecode(jsonEncode(snapshot.requireData)), origSettings: jsonDecode(jsonEncode(snapshot.requireData)),
settings: snapshot.requireData, settings: snapshot.requireData,
showAdvanced: false, showAdvanced: false,
); );
}, },
),
), ),
), ),
), ),
), const SizedBox(height: constants.padding),
const SizedBox(height: constants.padding), ],
], showBack: true); );
} }
Future<Map<String, dynamic>> _grabSettings() async { Future<Map<String, dynamic>> _grabSettings() async {

View File

@@ -2,7 +2,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:repertory/constants.dart' as constants; import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart'; import 'package:repertory/widgets/app_scaffold.dart';
import 'package:repertory/widgets/mount_list_widget.dart'; import 'package:repertory/widgets/mount_list_widget.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
@@ -18,17 +18,8 @@ class _HomeScreeState extends State<HomeScreen> {
Widget build(context) { Widget build(context) {
final scheme = Theme.of(context).colorScheme; final scheme = Theme.of(context).colorScheme;
return createCommonScaffold( return AppScaffold(
context, title: widget.title,
widget.title,
[
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: constants.padding),
child: const MountListWidget(),
),
),
],
floatingActionButton: Padding( floatingActionButton: Padding(
padding: const EdgeInsets.all(constants.padding), padding: const EdgeInsets.all(constants.padding),
child: Hero( child: Hero(
@@ -74,6 +65,14 @@ class _HomeScreeState extends State<HomeScreen> {
), ),
), ),
), ),
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: constants.padding),
child: const MountListWidget(),
),
),
],
); );
} }
} }

View File

@@ -57,7 +57,7 @@ void createIntListSetting(
SettingsTile.navigation( SettingsTile.navigation(
title: createSettingTitle(context, key, description), title: createSettingTitle(context, key, description),
leading: Icon(icon), leading: Icon(icon),
value: AppDropdownFormField<String>( value: AppDropdown<String>(
labelOf: (s) => s, labelOf: (s) => s,
constrainToIntrinsic: true, constrainToIntrinsic: true,
onChanged: (newValue) { onChanged: (newValue) {
@@ -310,7 +310,7 @@ void createStringListSetting(
SettingsTile.navigation( SettingsTile.navigation(
title: createSettingTitle(context, key, description), title: createSettingTitle(context, key, description),
leading: Icon(icon), leading: Icon(icon),
value: AppDropdownFormField<String>( value: AppDropdown<String>(
constrainToIntrinsic: true, constrainToIntrinsic: true,
labelOf: (s) => s, labelOf: (s) => s,
onChanged: (newValue) => setState(() => settings[key] = newValue), onChanged: (newValue) => setState(() => settings[key] = newValue),

View File

@@ -2,8 +2,8 @@ import 'package:flutter/material.dart';
import 'package:repertory/constants.dart' as constants; import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart'; import 'package:repertory/helpers.dart';
class AppDropdownFormField<T> extends StatelessWidget { class AppDropdown<T> extends StatelessWidget {
const AppDropdownFormField({ const AppDropdown({
super.key, super.key,
required this.labelOf, required this.labelOf,
required this.values, required this.values,

View File

@@ -0,0 +1,60 @@
// app_icon_button_framed.dart
import 'package:flutter/material.dart';
import 'package:repertory/constants.dart' as constants;
class AppIconButtonFramed extends StatelessWidget {
final IconData icon;
final VoidCallback? onTap;
final Color? iconColor;
const AppIconButtonFramed({
super.key,
required this.icon,
this.onTap,
this.iconColor,
});
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final radius = BorderRadius.circular(constants.borderRadiusSmall);
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: radius,
child: Ink(
width: 46,
height: 46,
decoration: BoxDecoration(
color: scheme.primary.withValues(alpha: 0.12),
borderRadius: radius,
border: Border.all(
color: scheme.outlineVariant.withValues(alpha: 0.08),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.24),
blurRadius: constants.borderRadiusSmall / 2.0,
offset: const Offset(0, 5),
),
],
),
child: Center(
child: Transform.scale(
scale: 0.90,
child: Icon(
icon,
color: iconColor ?? scheme.onSurface.withValues(alpha: 0.92),
size: 32.0,
),
),
),
),
),
);
}
}

View File

@@ -0,0 +1,181 @@
import 'dart:ui';
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/models/settings.dart';
import 'package:repertory/widgets/aurora_sweep.dart';
class AppScaffold extends StatelessWidget {
const AppScaffold({
super.key,
required this.children,
required this.title,
this.advancedWidget,
this.floatingActionButton,
this.showBack = false,
});
final List<Widget> children;
final String title;
final Widget? advancedWidget;
final Widget? floatingActionButton;
final bool showBack;
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return Scaffold(
body: SafeArea(
child: Stack(
children: [
Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: constants.gradientColors,
),
),
),
Consumer<Settings>(
builder: (_, settings, _) =>
AuroraSweep(enabled: settings.enableAnimations),
),
Positioned.fill(
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 6, sigmaY: 6),
child: Container(color: Colors.black.withValues(alpha: 0.06)),
),
),
Padding(
padding: const EdgeInsets.all(constants.padding),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Row(
children: [
if (!showBack) ...[
SizedBox(
width: 40,
height: 40,
child: Image.asset(
'assets/images/repertory.png',
fit: BoxFit.contain,
errorBuilder: (_, _, _) {
return Icon(
Icons.folder,
color: scheme.primary,
size: 32,
);
},
),
),
const SizedBox(width: constants.padding),
],
if (showBack) ...[
Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
onTap: () => Navigator.of(context).pop(),
child: Ink(
width: 40,
height: 40,
decoration: BoxDecoration(
color: scheme.surface.withValues(alpha: 0.40),
borderRadius: BorderRadius.circular(
constants.borderRadius,
),
border: Border.all(
color: scheme.outlineVariant.withValues(
alpha: 0.08,
),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.22),
blurRadius: constants.borderRadius,
offset: Offset(0, constants.borderRadius),
),
],
),
child: const Icon(Icons.arrow_back),
),
),
),
const SizedBox(width: constants.padding),
],
Expanded(
child: Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
color: scheme.onSurface.withValues(alpha: 0.96),
),
),
),
const SizedBox(width: constants.padding),
if (!showBack) ...[
const Text("Auto-start"),
Consumer<Settings>(
builder: (context, settings, _) {
return IconButton(
icon: Icon(
settings.autoStart
? Icons.toggle_on
: Icons.toggle_off,
),
color: settings.autoStart
? scheme.primary
: scheme.onSurface.withValues(alpha: 0.70),
onPressed: () =>
settings.setAutoStart(!settings.autoStart),
);
},
),
IconButton(
tooltip: 'Settings',
icon: const Icon(Icons.settings),
onPressed: () {
Navigator.pushNamed(context, '/settings');
},
),
const SizedBox(width: constants.padding),
],
if (showBack && advancedWidget != null) ...[
advancedWidget!,
const SizedBox(width: constants.padding),
],
Consumer<Auth>(
builder: (context, auth, _) => IconButton(
tooltip: 'Log out',
icon: const Icon(Icons.logout),
onPressed: auth.logoff,
),
),
],
),
const SizedBox(height: constants.padding),
...children,
],
),
),
],
),
),
floatingActionButton: floatingActionButton,
);
}
}

View File

@@ -0,0 +1,37 @@
// app_toggle_button_framed.dart
import 'package:flutter/material.dart';
import 'package:repertory/widgets/app_icon_button_framed.dart';
class AppToggleButtonFramed extends StatelessWidget {
final bool? mounted;
final VoidCallback? onPressed;
const AppToggleButtonFramed({
super.key,
required this.mounted,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final bool isOn = mounted ?? false;
IconData icon = Icons.hourglass_top;
Color iconColor = scheme.onSurface.withValues(alpha: 0.60);
if (mounted != null) {
icon = isOn ? Icons.toggle_on : Icons.toggle_off;
iconColor = isOn
? scheme.primary
: scheme.onSurface.withValues(alpha: 0.55);
}
return AppIconButtonFramed(
icon: icon,
iconColor: iconColor,
onTap: mounted == null ? null : onPressed,
);
}
}

View File

@@ -7,6 +7,8 @@ import 'package:repertory/constants.dart' as constants;
import 'package:repertory/helpers.dart'; import 'package:repertory/helpers.dart';
import 'package:repertory/models/mount.dart'; import 'package:repertory/models/mount.dart';
import 'package:repertory/utils/safe_set_state_mixin.dart'; import 'package:repertory/utils/safe_set_state_mixin.dart';
import 'package:repertory/widgets/app_icon_button_framed.dart';
import 'package:repertory/widgets/app_toggle_button_framed.dart';
class MountWidget extends StatefulWidget { class MountWidget extends StatefulWidget {
const MountWidget({super.key}); const MountWidget({super.key});
@@ -82,7 +84,8 @@ class _MountWidgetState extends State<MountWidget>
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
_GearBadge( AppIconButtonFramed(
icon: Icons.settings,
onTap: () { onTap: () {
Navigator.pushNamed( Navigator.pushNamed(
context, context,
@@ -107,7 +110,7 @@ class _MountWidgetState extends State<MountWidget>
], ],
), ),
), ),
_ToggleFramed( AppToggleButtonFramed(
mounted: mount.mounted, mounted: mount.mounted,
onPressed: _createMountHandler(context, mount), onPressed: _createMountHandler(context, mount),
), ),
@@ -281,96 +284,6 @@ class _MountWidgetState extends State<MountWidget>
} }
} }
class _FramedBox extends StatelessWidget {
final IconData icon;
final VoidCallback? onTap;
final Color? iconColor;
const _FramedBox({required this.icon, this.onTap, this.iconColor});
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final radius = BorderRadius.circular(constants.borderRadiusSmall);
return Material(
color: Colors.transparent,
child: InkWell(
onTap: onTap,
borderRadius: radius,
child: Ink(
width: 46,
height: 46,
decoration: BoxDecoration(
color: scheme.primary.withValues(alpha: 0.12),
borderRadius: radius,
border: Border.all(
color: scheme.outlineVariant.withValues(alpha: 0.08),
width: 1,
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.24),
blurRadius: constants.borderRadiusSmall / 2.0,
offset: const Offset(0, 5),
),
],
),
child: Center(
child: Transform.scale(
scale: 0.90,
child: Icon(
icon,
color: iconColor ?? scheme.onSurface.withValues(alpha: 0.92),
size: 32.0,
),
),
),
),
),
);
}
}
class _GearBadge extends StatelessWidget {
final VoidCallback onTap;
const _GearBadge({required this.onTap});
@override
Widget build(BuildContext context) {
return _FramedBox(icon: Icons.settings, onTap: onTap);
}
}
class _ToggleFramed extends StatelessWidget {
final bool? mounted;
final VoidCallback? onPressed;
const _ToggleFramed({required this.mounted, required this.onPressed});
@override
Widget build(BuildContext context) {
final scheme = Theme.of(context).colorScheme;
final bool isOn = mounted ?? false;
IconData icon = Icons.hourglass_top;
Color iconColor = scheme.onSurface.withValues(alpha: 0.60);
if (mounted != null) {
icon = isOn ? Icons.toggle_on : Icons.toggle_off;
iconColor = isOn
? scheme.primary
: scheme.onSurface.withValues(alpha: 0.55);
}
return _FramedBox(
icon: icon,
iconColor: iconColor,
onTap: mounted == null ? null : onPressed,
);
}
}
class _EditPathButton extends StatelessWidget { class _EditPathButton extends StatelessWidget {
final bool enabled; final bool enabled;
final VoidCallback onPressed; final VoidCallback onPressed;